In a previous post I reviewed five Alfresco plugins that provide custom picklist fields. The plugins differ in the way they store the list of values, but almost all of them share the fact that the field is displayed as a picklist and that the list of values can be controlled by an admin user at runtime.

In this post I’m going to show how these plugins work behind the scenes. Not surprisingly, they’re all similar and they all follow Alfresco’s guidelines for custom fields. So chances are that they’ll be useful examples in case you need to develop a custom picklist field yourself.

A reminder of what a picklist is

My previous post discussed this in details but, in a nutshell, a picklist is a part of a form that allows the user to select a value from a number of allowed options:

Testing uploader-plus with Alfresco-listmanager

This post explains how the picklist is implemented in the five Alfresco plugins.

High level view

So here is a high-level picture of how these plugins work (bear with me for the simplifications):

Alfresco picklist control

There are three main containers: Web browser, Share and Repo. The various components of the diagram “live” inside one of the containers and interact with each other.

In 10 steps, this is what happens:

  1. In the web browser, a page needs to display a form for a certain content. A request is sent to the form service.
  2. The form service (discussed in a later section) needs to find out how to render a form for the given content. For starters, it asks the repo to provide the content’s type metadata.
  3. The form service looks at its form definitions and finds the one that can handle the given content type.
  4. The form service scans the form definition field by field. A certain field requires to be rendered as a custom picklist. It does so by pointing to a custom field “control”, basically a Freemarker template.
  5. The form service returns the requested html form to the browser.
  6. Part of the form is rendered by the custom control as an html picklist. At this point the picklist has no values/options.
  7. The picklist has some associated JavaScript code. When the form is rendered by the browser, the code is executed. YUI is Alfresco’s JS framework often involved at this stage.
  8. The YUI component/code runs an AJAX query to retrieve the picklist values. The request is sent to Alfresco’s repo, with Share working as a proxy.
  9. The custom webscript on the repository queries the picklist value store. This is where the five plugins differ the most. Two use data-lists, one uses entries in the data dictionary, another one queries users, yet another one consults the content model’s constraints. The implementations can be many.
  10. Once the AJAX call returns, the JavaScript code creates the picklist’s options.

Since the form service plays and important part in this process, in the next sections we’ll see in general what the form service is and specifically how it is used by picklist plugins.

The form service and its form controls

Alfresco has a build-it form service that is responsible for rendering forms for a variety of content types. The form service is fundamentally a Share presentation component, although internally it relies on the content metadata provided by Alfresco’s repo.

Forms are defined via xml. The definition of forms for the basic content types (cm:content, cm:folder, etc) can be found in share/WEB-INF/classes/alfresco/share-form-config.xml, but other definitions and config files can be added to deal with custom content types.

A form definition may look like this:

<config evaluator="model-type" condition="owd:hrdocument">
  <forms>
    <form>
      <field-visibility>
        <show id="cm:name" />
        <show id="cm:title" force="true" />
        <show id="cm:description" force="true" />
        <show id="owd:faculty" />
      </field-visibility>
      <appearance>
        <field id="cm:name">
          <control>
            <control-param name="maxLength">255</control-param>
          </control>
        </field>
        <field id="owd:faculty">
          <control template="/form-controls/dynamic-dropdown.ftl">
            <control-param name="picklistName">Faculty</control-param>
          </control>
        </field>
      </appearance>
    </form>
  </forms>
</config>

The form service delegates the rendition of a field to an associated control, basically a Freemarker template that generates the field’s html. Share has some 40 standard controls that can be found under share/WEB-INF/classes/alfresco/site-webscripts/org/alfresco/components/form/controls/. They range from something as simple as an html text input to something as complex as a node selector that pops-up in a dialog. There are controls to deal with workflow forms, too.

Picklist control example

One of Share’s standard controls (‘selectone.ftl’) is responsible for rendering picklists. The five picklist plugins introduce custom variations of selectone.ftl.

As an example let’s see customselectone.ftl from the Alfresco Listmanager plugin.

<#include "/org/alfresco/components/form/controls/common/utils.inc.ftl" />

<#if field.control.params.optionSeparator??>
   <#assign optionSeparator=field.control.params.optionSeparator>
<#else>
   <#assign optionSeparator=",">
</#if>
<#if field.control.params.labelSeparator??>
   <#assign labelSeparator=field.control.params.labelSeparator>
<#else>
   <#assign labelSeparator="|">
</#if>

<div class="form-field">
   <#if form.mode == "view">
      <div class="viewmode-field">
         <#if field.mandatory && !(field.value?is_number) && field.value?string == "">
            <span class="incomplete-warning"><img src="${url.context}/res/components/form/images/warning-16.png" title="${msg("form.field.incomplete")}" /><span>
         </#if>
         <span class="viewmode-label">${field.label?html}:</span>
         <#if field.value?string == "">
            <#assign valueToShow=msg("form.control.novalue")>
         <#else>
            <#assign valueToShow=field.value>
            <#if field.control.params.options??>
	            <#list field.control.params.options?split(optionSeparator) as nameValue>
	               <#if nameValue?index_of(labelSeparator) == -1>
	                  <#if nameValue == field.value?string || (field.value?is_number && field.value?c == nameValue)>
	                     <#assign valueToShow=nameValue>
	                     <#break>
	                  </#if>
	               <#else>
	                  <#assign choice=nameValue?split(labelSeparator)>
	                  <#if choice[0] == field.value?string || (field.value?is_number && field.value?c == choice[0])>
	                     <#assign valueToShow=msgValue(choice[1])>
	                     <#break>
	                  </#if>
	               </#if>
	            </#list>
	          </#if>
         </#if>
         <span class="viewmode-value">${valueToShow?html}</span>
      </div>
   <#else>
   <script>//<![CDATA[
		// Ensure Acando namespace exists
		if (typeof Acando === "undefined" || !Acando) 
		{
		    var Acando = {};
		}
		
		(function() 
		{
			Acando.CustomSelectOne = function CustomSelectOne_constructor(htmlId) 
			{
				Acando.CustomSelectOne.superclass.constructor.call(this, htmlId);
							
				this.name = "Acando.CustomSelectOne";
				this.id = htmlId;
			
				Alfresco.util.ComponentManager.register(this);
				Alfresco.util.YUILoaderHelper.require(["button", "container"], this.onComponentsLoaded, this);
			
			    return this;
			};
		
			YAHOO.extend(Acando.CustomSelectOne, Alfresco.component.Base, 
			{
				options:
				{
				},
				setOptions: function CustomSelectOne_setOptions(obj) 
				{
				    this.options = YAHOO.lang.merge(this.options, obj);
				    return this;
				},
				setMessages: function CustomSelectOne_setMessages(obj) 
				{
					Alfresco.util.addMessages(obj, this.name);
					return this;
				},
				onReady: function CustomSelectOne_onReady()
				{
					var selectedValue = this.options.selectedValue; 
					var setOptionsCallback = 
					{
						success: function(o) 
						{
							var values, i;
							var listbox = YAHOO.util.Dom.get(this.id);
						
							try 
							{
								values = YAHOO.lang.JSON.parse(o.responseText);
								listbox.remove(listbox.length - 1); // Remove "Loading..." text
							} 
							catch (x) 
							{
								console.log("Json parse failed! " + x);
								return;
							}
							
							for(i = 0; i < values.length; i++) 
							{
								var v = values[i];
								var option = new Option('option');
								
								option.value = (v.value === null || v.value === "") ? v.label : v.value;
								option.text = v.label;
								option.selected = (option.value === selectedValue);
								listbox.add(option);
							}
						},
				  		failure: function(o) 
				  		{
				  			Alfresco.util.PopupManager.displayMessage({text: "Could not load values!"});
				  		},
				  		scope: this
					};
		
					var listUrl = Alfresco.constants.PROXY_URI + "listbox/" + this.options.listBoxName;
					var transaction = YAHOO.util.Connect.asyncRequest('GET', listUrl, setOptionsCallback, null);
				}
			});
		})();
		   
		var options = {};
		options.selectedValue = "${field.value}";
		options.listBoxName = "${field.control.params.listboxname}";
		new Acando.CustomSelectOne("${fieldHtmlId}").setOptions(options);
	//]]></script>
      <label for="${fieldHtmlId}">${field.label?html}:<#if field.mandatory><span class="mandatory-indicator">${msg("form.required.fields.marker")}</span></#if></label>
      <#if field.control.params.listboxname?? && field.control.params.listboxname != "">
         <select id="${fieldHtmlId}" name="${field.name}" tabindex="0"
               <#if field.description??>title="${field.description}"</#if>
               <#if field.control.params.size??>size="${field.control.params.size}"</#if> 
               <#if field.control.params.styleClass??>class="${field.control.params.styleClass}"</#if>
               <#if field.control.params.style??>style="${field.control.params.style}"</#if>
               <#if field.disabled  && !(field.control.params.forceEditable?? && field.control.params.forceEditable == "true")>disabled="true"</#if>>
               <option>Loading...</option>
         </select>
         <@formLib.renderFieldHelp field=field />
      <#else>
         <div id="${fieldHtmlId}-missing" class="missing-options">${msg("form.control.selectone.missing-options")}</div>
      </#if>
   </#if>
</div>

That’s certainly a lot of code but, as I said, much of it is taken from the standard selectone.ftl. The part that is interesting is where JavaScript is introduced (lines 45 to 131). Some of the code is dedicated to the definition and registration of a new YUI component called Acando.CustomSelectOne, this is necessary to ensure that the component is properly initialised when the form is rendered on the browser. But the really interesting stuff happens in the onReady() method (lines 82-123) where the ajax call is made to retrieve the values from the repository.

Further examples and details

It would take too much time on my side and patience on your side to go through all the details of all the plugins, but if you’re curious here are a few pointers to the most relevant files in each plugins.

Alfresco-listmanager

Alfresco-value-assistance

Alfresco-datalist-constraints

Alfresco-colleagues-picker-form-control

Share-form-control-dependency

  • The control template This plugin piggybacks on standard Alfresco constraints and for this reason works without invoking any webscripts.

Conclusions

Share UI customisation is one of the most difficult topics in Alfresco, requiring knowledge of the architecture, of the configurations, of server-side and client-side programming and, last but not least, of the YUI framework (and more recently of Dojo/Aikau). The code discussed in this post supports my point. But plugins are made of no more than ten key files each, so they provide bite-size examples of what can be done in larger projects.