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

Monday, February 4, 2013

Notifications made easy

Did you ever miss a general notification service. I just had the situation that the built in notifications of the given system ( ... it was not Alfresco, but this big parent of it) were note suitable enough. So I decided to realize my own notification system by using Freemarker templates (http://freemarker.sourceforge.net).

The task is to create an HTML email body by using an email template instead to build your text by using String concatenation. So what I did is the following:

  •  I created 3 classes: Model, Template and Processor
  • The Model class is just a wrapper which allows you to add key-value-pairs to a root-HashMap. I just used it to avoid to add data to a Map<Object,Object>.
  • The Template has a constructor which allows you to specify the template root location. The default constructor reads the 'folder' attribute from a property file which must be stored within the classpath (template.properties). The get-Method returns an instance of the Freemarker template which has the requested name (E.G. simple.ftl)
  • The Processor generates the output dependent on the model and the template.
  • I also added a helper class for reading the configuration from the template.properties file.
  • 2 Exception classes were created to differ between 'Template not found' and 'Template could not be processed'.
  • The Main class has only testing purposes

My quite simple mail body template has 4 regions. One for the headers, one for the text of the message, one to print out some key value pairs and finally one for a footer.

<html>



<div id="header" style="font-family:Arial; color:#5678a9;">

    <h2>${title}</h2>

    <br/>

    <h3>${subtitle}</h3>

</div>



<br/>



<div id="content" style="font-family:Arial;">

${text}

</div>



<br/>

<br/>



<div id="props" style="font-family:Arial;">



<#list props as prop>

  <b>${prop.key}</b>: ${prop.value} </br>

</#list>

</div>



<br/>

<br/>



<div id="footer" style="font-family:Arial;">

${footer}

</div>



</html> 





The Main class wires the model and the template together by using the Processor class.

        Model model = new Model();

       

        model.add("title", "Notification");

        model.add("subtitle", "Important info");

        model.add("text", "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.");

       

        List<Map<String,String>> keyvaluePairs = new ArrayList<Map<String,String>>();

       

        Map<String, String> keyvaluePair1 = new HashMap<String, String>();

        keyvaluePair1.put("key", "User");

        keyvaluePair1.put("value", "David Maier");

       

        Map<String, String> keyvaluePair2 = new HashMap<String, String>();

        keyvaluePair2.put("key", "E-Mail");

        keyvaluePair2.put("value", "info@magicable.de");

       

        keyvaluePairs.add(keyvaluePair1);

        keyvaluePairs.add(keyvaluePair2);

       

        model.addModel("props",keyvaluePairs);

       

        model.add("footer", "More ECM stuff is available here:  <a href='http://www.ecmgeek.de'>ecmgeek.de</a>");

       

        String result = Processor.process(model, "simple.ftl");

       

        System.out.println(result);





The result looks already quite good:

I also already added the Java Mail API dependencies and the source code tree is now the following one. Maybe I will publish the code later.