Thursday, February 21, 2013

Custom form controls in Alfresco

Basics

It's possible to use custom form controls to show your properties. The following snippet explains how to use them:

<config evaluator="node-type" condition="ecg:mytype">

 <forms>
      <form>
        <field-visibility>

            <!-- My property which is only visible in the view edit mode but not in the view mode-->
            <show id="ecg:myprop" for-mode="edit"/>

        </field-visibility>

        <appearance>

             <!-- Define a panel on which my property should be shown -->

             <set id="myPanel" appearance="bordered-panel" label="My Properties"/>

             

             <!-- Add the property to the panel by defining which control should be used -->

             <field id="ecg:myProp" set="myPanel" label="My Property">
                <control template="/de/ecmgeek/alfresco/mycontrol.ftl">
                    <control-param name="myFirstParam">myFirstParamValue</control-param>
                    <control-param name="mySecondParam">mySeceondParamValue</control-param>
                </control>
            </field> 

        </appearance> 

     </form> 

</forms> 

</config> 




Structure of a form control

 So a form control is just a freemarker template wich can be referenced in your form configuration. Therefore you need to store it under for instance 'web-extension/site-webscripts/de/ecmgeek/alfresco'. But how does such a form look like? Here the code of the textfield.ftl which is provided by Alfresco.


<div class="form-field">
   <#if form.mode == "view">
      <div class="viewmode-field">
         <#if field.mandatory && !(field.value?is_number) && field.value == "">
            <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.control.params.activateLinks?? && field.control.params.activateLinks == "true">
            <#assign fieldValue=field.value?html?replace("((http|ftp|https):\\/\\/[\\w\\-_]+(\\.[\\w\\-_]+)+([\\w\\-\\.,@?\\^=%&:\\/~\\+#]*[\\w\\-\\@?\\^=%&\\/~\\+#])?)", "<a href=\"$1\" target=\"_blank\">$1</a>", "r")>
         <#else>
            <#if field.value?is_number>
               <#assign fieldValue=field.value?c>
            <#else>
               <#assign fieldValue=field.value?html>
            </#if>
         </#if>
         <span class="viewmode-value"><#if fieldValue == "">${msg("form.control.novalue")}<#else>${fieldValue}</#if></span>
      </div>
   <#else>
      <label for="${fieldHtmlId}">${field.label?html}:<#if field.mandatory><span class="mandatory-indicator">${msg("form.required.fields.marker")}</span></#if></label>
      <input id="${fieldHtmlId}" name="${field.name}" tabindex="0"
             <#if field.control.params.password??>type="password"<#else>type="text"</#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.value?is_number>value="${field.value?c}"<#else>value="${field.value?html}"</#if>
             <#if field.description??>title="${field.description}"</#if>
             <#if field.control.params.maxLength??>maxlength="${field.control.params.maxLength}"</#if>
             <#if field.control.params.size??>size="${field.control.params.size}"</#if>
             <#if field.disabled && !(field.control.params.forceEditable?? && field.control.params.forceEditable == "true")>disabled="true"</#if> />
      <@formLib.renderFieldHelp field=field />
   </#if>
</div>




As you can see the following elements are out of the box available:
  • Parameters via E.G. 'field.control.params.myFirstParam'
  • The value of the field via 'field.value'. It makes sense to make it available as via ${fieldValue} in order to use it in your HTML or JavaScript. Therefore Freemarker's assign command can be used '<#assign fieldValue=field.value> . (The term 'field.value' is only available within Freemarker directives. But it is possible to assign them to a place holder (interpolation) by using the assign directive.)
  • The form mode via 'form.mode'
  • The enabled/disabled mode of the field by using 'field.disabled'.
  • The information if the field is a madatory one or not by using 'field.mandatory
  • The field description via 'field.description' 
  • The field name directly via ${fieldName}
  • The field id via ${fieldHtmlId}. The field id is important. It will be replaced by an unique id which depends on the form id and the prefix and name of your property. So you can later reference your field by using this unique id.  
 So from what we know it is easy to create static controls those are looking as we want by chaning the HTML code. But what if we want to have a more dynamic control, for instace an autocompletion one or a custom category picker?

Within your control the Share API is available (http://sharextras.org/jsdoc/share/community-4.0.c/symbols/Alfresco.Share.html). Additionally you have access to the YUI stuff. (YUI is the UI framework behind Alfresco Share) (http://developer.yahoo.com/yui/2/). So you can mix the mentioned HTML with Java Script to perform AJAX requests in order to get data from the repository layer (provided by Web-Scripts). This client side Java Script can then directly manipulate your controls DOM tree. Important is that you run the script after your control was completely available (rendered). Here an example:



<script type="text/javascript">//<![CDATA[

(function()
{
    YAHOO.util.Event.onContentReady("${fieldHtmlId}", 
    function ()
    {
        var successHandler = function (response)
        {
             //Handle the AJAX request

             var data = response.json;



             //Use the data ... 
         };    
         
         var failureHandler = function (response) { 
                Alfresco.logger.error("Could not perform the AJAX request: " +               response.serverResponse.responseText);
         };
         

         var myparamValue = "${field.control.params.myFirstParam}";
         var url = Alfresco.constants.PROXY_URI + "ecg/mywebscript.json?myparam=" + myparamValue ;
        
         var config =
         {
            method: "GET",
            url: url,
            successCallback: 
            { 
                fn: successHandler, 
                scope: this
            },
            failureCallback:
            {
                fn: failureHandler,
                scope: this
            }
         };
        
         Alfresco.util.Ajax.request(config);
    }
    ,this);
})();

//]]></script>



 <div class="form-field">

... 

</div>


 
 How the form submission works

So as you can see it is possible to get data from the repository layer by accessing RESTFul web services. Such a RESTFul web service. Such a service can be implemented as Web Script. You then can use the data which was provided by the service in order to manipulate the DOM of your control. An auto completion control could ask the service for matching words based on the input of the user by writing out a list into a DIV container with the id '${fieldHtmlId}-acc'. A user may select multiple (by the autocompletion recommended) values. But how would you submit the selected values. The answer is: Via the value attribute of the input field with the id ' ${fieldHtmlId}'. Dependent on the type of your field, the form processor will handle the values inside this field in a different way. So if you have a classification (or user) property then the value inside the field should be a node reference. If you have a text input then the value should be just the text to show. If you want to submit multiple node references then the value shoud be a comma seperated list of node references. Because it is often not really useful to show the user a field which shows a comma seperated list of values, the trick is to have one input field (or Picker, ...) which is visible but has a different id, E.G. '${fieldHtmlId}-show' whereby storing your seperated list of node references inside a hidden field with the id '${fieldHtml${fieldHtmlId}'. So you show one field for working purposes but you use the value of the on one with the id '${fieldHtml${fieldHtmlId}' during the submission. I used the following Java Script to add new node references to the value attribute of the hidden input field with the id '${fieldHtmlId}'${fieldHtmlId} '${fieldHtmlId}':}${fieldHtml${fieldHtmlId}${fieldHtml${fieldHtmlId}':


 //Add to the hidden field
  var hiddenField = document.getElementById("${fieldHtmlId}");
  var value = hiddenField.getAttributeNode("value");
           
   var bkpValue = value.nodeValue;
   var newValue = "";
               
   if (bkpValue == "")
   {
      newValue = myNodeRef;
   }
   else
   {
        newValue = bkpValue + "," + myNodeRef;
    }
               
    value.nodeValue    = newValue;



Room for improvements

You may use one control for multiple times on one single form. This causes that you can see tha above mentioned Java Script code for multiple times in the client side Java Script code (by using Firebug ...). To avoid this you can put the JavaScript into a custom Java Script library. Which then could be for instance accessed this way:  'MyLib.addToValues(fieldId, nodeRef)'.)nfiibavi