cancel
Showing results for 
Search instead for 
Did you mean: 

Cascading Dropdown in alfresco share

joshijimit
Champ on-the-rise
Champ on-the-rise
Hi All,

I have a requirement in my project to implement cascading dropdown in alfresco share.
I have followed the below link of jean barmash's blog and implemented cascading dropdown in alfresco explorer.

http://blogs.alfresco.com/wp/jbarmash/2008/08/18/implementing-cascading-dropdowns-for-editing-proper...

I have also got success to show the dropdown fields in alfresco share by wrapping it up in custom aspect.
but the problem now is custom component generator(TextFieldGenerator) is not being utilize in alfresco share as alfresco uses "Forms" to edit metadata.

I am finding the workaround to make it possible.
Any help from anyone would be of great help.

Regards,
Jimit
1 ACCEPTED ANSWER

scouil
Star Contributor
Star Contributor
As expected, defining the onchange event in javascript fixes it.
I ended up refactoring a bit my code and spliting it into 2 files:

/org/alfresco/components/form/controls/cascading-dropdowns.ftl :

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

<!– current value –>
<#assign fieldValue=field.value>

<!– handle default value for empty current value –>
<#if fieldValue?string == "" && field.control.params.defaultValueContextProperty??>
   <#if context.properties[field.control.params.defaultValueContextProperty]??>
      <#assign fieldValue = context.properties[field.control.params.defaultValueContextProperty]>
   <#elseif args[field.control.params.defaultValueContextProperty]??>
      <#assign fieldValue = args[field.control.params.defaultValueContextProperty]>
   </#if>
</#if>

<div class="form-field">
   <!– readonly mode. No action required, display the composed value as is –>
   <#if form.mode == "view">
      <div class="viewmode-field">
         <#if field.mandatory && !(fieldValue?is_number) && fieldValue?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 fieldValue?string == "">
            <#assign valueToShow=msg("form.control.novalue" )>
         <#else>
            <#assign valueToShow=fieldValue>
            <#if field.control.params.options?? && field.control.params.options != "">
               <#list field.control.params.options?split(optionSeparator) as nameValue>
                  <#if nameValue?index_of(labelSeparator) == -1>
                     <#if nameValue == fieldValue?string || (fieldValue?is_number && fieldValue?c == nameValue)>
                        <#assign valueToShow=nameValue>
                        <#break>
                     </#if>
                  <#else>
                     <#assign choice=nameValue?split(labelSeparator)>
                     <#if choice[0] == fieldValue?string || (fieldValue?is_number && fieldValue?c == choice[0])>
                        <#assign valueToShow=msgValue(choice[1])>
                        <#break>
                     </#if>
                  </#if>
               </#list>
            </#if>
         </#if>
         <span class="viewmode-value">${valueToShow?html}</span>
      </div>
   <#else>
     <!– Editable mode. –>
     <!– let's keep a single label for both fields –>
      <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.options?? && field.control.params.options != "">
       <!– first select for the parent value –>
         <select id="parent-${fieldHtmlId}" 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>>
         </select>
      
       <!– second select for the child value –>
         <select id="child-${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>>
         </select>
      
       <!– hidden field containing the final value (e.g. the one sent to the server and stored in database) –>
       <input type="hidden" id="${fieldHtmlId}" name="${field.name}" />
      
         <@formLib.renderFieldHelp field=field />
      <#else>
         <div id="${fieldHtmlId}" class="missing-options">${msg("form.control.selectone.missing-options" )}</div>
      </#if>
   </#if>
</div>

<!– IMPORTANT: this script must execute after the fields are created –>
<script type="text/javascript">//<![CDATA[
   setTimeout(function(){populateSelects("${field.control.params.options}", "${fieldValue}", "parent-${fieldHtmlId}", "child-${fieldHtmlId}", "${fieldHtmlId}");}, 1000);
//]]></script>


and /org/alfresco/components/form/controls/common/cascadingDropdownUtils.inc.ftl :

<!– defines how the option list elements are separated when passed to the client –>
<#if field.control.params.optionSeparator??>
   <#assign optionSeparator=field.control.params.optionSeparator>
<#else>
   <#assign optionSeparator=",">
</#if>

<!– defines how the label list elements are separated when passed to the client –>
<#if field.control.params.labelSeparator??>
   <#assign labelSeparator=field.control.params.labelSeparator>
<#else>
   <#assign labelSeparator="|">
</#if>

<script type="text/javascript">//<![CDATA[
<!– defines how the option list elements are separated when passed to the client –>
<#if field.control.params.optionSeparator??>
   <#assign optionSeparator=field.control.params.optionSeparator>
<#else>
   <#assign optionSeparator=",">
</#if>

<!– defines how the label list elements are separated when passed to the client –>
<#if field.control.params.labelSeparator??>
   <#assign labelSeparator=field.control.params.labelSeparator>
<#else>
   <#assign labelSeparator="|">
</#if>

<script type="text/javascript">//<![CDATA[
   /* string used to separate the parent from the child */
   var OPTION_SEPARATOR = "/";

   /**
   * Functions used to split and synchronize the data between the 2 lists.
   **/
   
   /**
   *   @param stringValues: string containing the available values for the given field
   *   @returns a javascript object containing those values
   **/
   function getValsArrayFromString(stringValues) {
      //array with the options
      var valsArray = stringValues.split("${optionSeparator}");
      //remove the labels
      for (var i = 0; i < valsArray.length; i++) {
         if(valsArray.indexOf("${labelSeparator}") != -1) {
            valsArray = valsArray.substr(0,valsArray.indexOf("${labelSeparator}"));
         }
      }
      return valsArray;
   }

   /**
   * Extracts the parent value from the full value of the option
   **/
   function getParentValue(fullValue) {
      return fullValue.substr(0,fullValue.indexOf(OPTION_SEPARATOR));
   }

   /**
   * Extracts the child value from the full value of the option
   **/
   function getChildValue(fullValue) {
      return fullValue.substr(fullValue.indexOf(OPTION_SEPARATOR) + 1);
   }

   /**
   * Parent value have been changed.
   * Reset the current total value, child value and updates the available child values
   **/
   function onChangeParentValue(valsArray, parentId, childId, totalId) {
      var parentValue = document.getElementById(parentId).value;
      var childSelect = document.getElementById(childId);
      
      /* empty the child select */
      while(childSelect.firstChild) {
         childSelect.removeChild(childSelect.firstChild);
      }
      
      /* fill the child select with the new values */
      for (var i = 0; i < valsArray.length; i++) {
         if ( getParentValue(valsArray) == parentValue) {
            var childValue = getChildValue(valsArray);
            var childOption = document.createElement("option");
            childOption.value = childValue;
            childOption.innerHTML = childValue;
            childSelect.appendChild(childOption);
         }
      }
   }

   /**
   * Child value have been changed.
   * Update the total value
   **/
   function onChangeChildValue(parentId, childId, totalId) {
      var parentValue = document.getElementById(parentId).value;
      var childValue = document.getElementById(childId).value;
      document.getElementById(totalId).value = parentValue + OPTION_SEPARATOR + childValue;
   }

   /**
   * Initial filling of the parent values available
   * TODO: handle default value
   **/
   function populateSelects(stringValues, initialValue, parentId, childId, totalId) {
      /* get an array from the string values */
      var valsArray = getValsArrayFromString(stringValues);
      var parentSelect = document.getElementById(parentId);
      /* we only want to display each parent value once. This object keeps track of the already added ones */
      var uniqueParentValues = {};
      
      /* populate parent select */
      for (var i = 0; i < valsArray.length; i++) {
         var realV = getParentValue(valsArray);
         if(!uniqueParentValues.hasOwnProperty(realV)) {
            uniqueParentValues[realV] = true;
            
            /* create new option for the parent select */
            var parOption = document.createElement("option");
            parOption.value = realV;
            parOption.innerHTML = realV;
            if(realV == getParentValue(initialValue)) {
               parOption.selected = "selected";
            }
            parentSelect.appendChild(parOption);
         }
      }
      parentSelect.onchange = function(){onChangeParentValue(valsArray, parentId, childId, totalId);};
      
      /* populate child select */
      onChangeParentValue(valsArray, parentId, childId, totalId);
      
      /* sets the correct child value */
      var childSelect = document.getElementById(childId);
      childSelect.value = getChildValue(initialValue);
      childSelect.onchange = function(){onChangeChildValue(parentId, childId, totalId);};
      onChangeChildValue(parentId, childId, totalId);
   }
//]]></script>


IE advanced search now works as expected.

View answer in original post

28 REPLIES 28

rfernandes
Confirmed Champ
Confirmed Champ
Hi Guys,

Maybe this is useful to you.

You can check the source code at the Google Code Project: http://code.google.com/p/share-form-control-dependency/

The idea is if you select value A for the main property the conditioned property (or properties) should automatically just present the corresponding allowed values.

The basic idea is to have specific form controls for Alfresco Share aware of the convention used to configure in our content model those dependencies and also have those generic form controls empowered (through client javascript) to update the corresponding form controls according to the updates made by the user on the other associated properties.

So if we define our constraints like (like the sample model on our google code project):

<constraints>
      <constraint name="sample:parentConstraint" type="LIST">
         <parameter name="allowedValues">
           <list>
               <value></value>
               <value>1 - VALUE A</value>
               <value>2 - VALUE B</value>
               <value>3 - VALUE C</value>
           </list>
         </parameter>
      </constraint>
      <constraint name="sample:constraintOne" type="LIST">
         <parameter name="allowedValues">
           <list>
               <value></value>
               <value>1 - SUBVALUE ONE AX</value>
               <value>1 - SUBVALUE ONE AY</value>
               <value>1 - SUBVALUE ONE AZ</value>
               <value>2 - SUBVALUE ONE BX</value>
               <value>2 - SUBVALUE ONE BY</value>
               <value>3 - SUBVALUE ONE CY</value>
               <value>3 - SUBVALUE ONE CZ</value>
           </list>
         </parameter>
      </constraint>
      <constraint name="sample:constraintTwo" type="LIST">
         <parameter name="allowedValues">
           <list>
               <value></value>
               <value>1.2.3 - SUBVALUE TWO ABCX</value>
               <value>2.3 - SUBVALUE TWO BCX</value>
               <value>2.3 - SUBVALUE TWO BCY</value>
               <value>2.3 - SUBVALUE TWO BCZ</value>
               <value>3 - SUBVALUE TWO CX</value>
               <value>3 - SUBVALUE TWO CY</value>
               <value>3 - SUBVALUE TWO CZ</value>
           </list>
         </parameter>
      </constraint>
   </constraints>

What we are saying (through convention) is that some possible values for properties one and two (to which will apply the constraintOne and constraintTwo) just should shown if the corresponding group value (the number which starts the value) was selected for the main property. If the value selected for the main property changes we will also want to change the shown values for the other properties one and two.  For cases  like 1.2.3 – SUBVALUE TWO ABCX in the constraintTwo, we will want the value to be available for property two for whatever value (1, 2 or 3) is selected  for the main property. We also don’t want the values to be presented with the preceding main property association 1 -, 2 -, etc. And finally we want all updates to happen dynamically as user fulfills the form.

Well that’s exactly what our custom form control components do. They are generic and can be used for any equivalent case. Just build the jar, incorporate in your project and finally configure your form for Alfresco Share:

<form>
        <field-visibility>
          <show id="sample:mainProperty" />
          <show id="sample:propertyOne" />
          <show id="sample:propertyTwo" />
          <show id="sample:propertyThree" />
        </field-visibility>
        <appearance>         
          <field id="sample:mainProperty">
            <control template="/org/alfresco/components/form/controls/parent-filter-selectone.ftl">
          <control-param name="filteredProperty">sample_propertyOne,sample_propertyTwo</control-param>
            </control>
          </field>
          <field id="sample:propertyOne">
            <control template="/org/alfresco/components/form/controls/filtered-selectone.ftl"/>
          </field>
          <field id="sample:propertyTwo">
            <control template="/org/alfresco/components/form/controls/filtered-selectone.ftl"/>
          </field>
        </appearance>
      </form>

As you see we are using the same custom form control filtered-selectone.ftl for both conditioned properties, and another parent-filter-selectone.ftl for the main one. This last one must also receive the list of conditioned properties (separated by comma) as a control-param. As the dependency is “injected” this way at the form control configuration level this allows for us to have many separated groups of associated properties in the same form.

And with that in place the magic will happen.


Pay attention as besides the dynamic filtering of values we also have the initial numbers who define our dependency convention ripped from the values shown in the form.

The good stuff is that this is fully generic and can be used for any form, type or aspect, including search forms, data-lists types, and so on.

Finally the last advantage this pattern has is that: it allows you to manage the associated dynamic behavior of your properties, in your types and aspects, just by configuring your content model constraints (that can be located in a dynamic model for example and so allow for hot update without server restarts).

Hi Rui,

I tried the above solution and it worked like a charm, but when I try it out in advanced search alfresco is not able to search the document. Do you know why ?? Any specific reason ??

P.S. - I tried searching the doc using the Node Browser and I am able to search the document.

akash066
Champ in-the-making
Champ in-the-making
Hi Jimit,

If you have succeeded in this work then can you share the steps regarding the same.

With Regards,
Akash

scouil
Star Contributor
Star Contributor
Hello everyone,

I've personally implemented yet another approach.
Since the main problem is that constraints and form fields are handled independently, I chose to represent the cascading dropdowns as a single property.
I define it as a basic listOfValues constraint:

<parameter name="allowedValues">
    <list>
   <value>parent1/test1</value>
   <value>parent1/test2</value>
   <value>parent1/test3</value>
   <value>parent2/test4</value>
   <value>parent2/test5</value>
    </list
</parameter


All the dependancies and display is handled in a single client-side custom form component:


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

<!– defines how the option list elements are separated when passed to the client –>
<#if field.control.params.optionSeparator??>
   <#assign optionSeparator=field.control.params.optionSeparator>
<#else>
   <#assign optionSeparator=",">
</#if>

<!– defines how the label list elements are separated when passed to the client –>
<#if field.control.params.labelSeparator??>
   <#assign labelSeparator=field.control.params.labelSeparator>
<#else>
   <#assign labelSeparator="|">
</#if>

<!– current value –>
<#assign fieldValue=field.value>

<!– handle default value for empty current value –>
<#if fieldValue?string == "" && field.control.params.defaultValueContextProperty??>
   <#if context.properties[field.control.params.defaultValueContextProperty]??>
      <#assign fieldValue = context.properties[field.control.params.defaultValueContextProperty]>
   <#elseif args[field.control.params.defaultValueContextProperty]??>
      <#assign fieldValue = args[field.control.params.defaultValueContextProperty]>
   </#if>
</#if>

<script type="text/javascript">
   /* string used to separate the parent from the child */
   var OPTION_SEPARATOR = "/";

   /**
   * Scripts used to split and synchronize the data between the 2 lists.
   **/
   
   /**
   *   @param stringValues: string containing the available values for the given field
   *   @returns a javascript object containing those values
   **/
   function getValsArrayFromString(stringValues) {
      //array with the options
      var valsArray = stringValues.split("${optionSeparator}");
      //remove the labels
      for (var i = 0; i < valsArray.length; i++) {
         if(valsArray.indexOf("${labelSeparator}") != -1) {
            valsArray = valsArray.substr(0,valsArray.indexOf("${labelSeparator}"));
         }
      }
      return valsArray;
   }

   /**
   * Extracts the parent value from the full value of the option
   **/
   function getParentValue(fullValue) {
      return fullValue.substr(0,fullValue.indexOf(OPTION_SEPARATOR));
   }

   /**
   * Extracts the child value from the full value of the option
   **/
   function getChildValue(fullValue) {
      return fullValue.substr(fullValue.indexOf(OPTION_SEPARATOR) + 1);
   }

   /**
   * Parent value have been changed.
   * Reset the current total value, child value and updates the available child values
   **/
   function onChangeParentValue(stringValues, parentId, childId, totalId) {
      /* get an array from the string values */
      var valsArray = getValsArrayFromString(stringValues);
      var parentValue = document.getElementById(parentId).value;
      var childSelect = document.getElementById(childId);
      
      /* empty the child select */
      while(childSelect.firstChild) {
         childSelect.removeChild(childSelect.firstChild);
      }
      
      /* fill the child select with the new values */
      for (var i = 0; i < valsArray.length; i++) {
         if ( getParentValue(valsArray) == parentValue) {
            var childValue = getChildValue(valsArray);
            var childOption = document.createElement("option");
            childOption.value = childValue;
            childOption.innerHTML = childValue;
            childSelect.appendChild(childOption);
         }
      }
   }

   /**
   * Child value have been changed.
   * Update the total value
   **/
   function onChangeChildValue(stringValues, parentId, childId, totalId) {
      var parentValue = document.getElementById(parentId).value;
      var childValue = document.getElementById(childId).value;
      document.getElementById(totalId).value = parentValue + OPTION_SEPARATOR + childValue;
   }

   /**
   * Initial filling of the parent values available
   * TODO: handle default value
   **/
   function populateSelects(stringValues, initialValue, parentId, childId, totalId) {
      /* get an array from the string values */
      var valsArray = getValsArrayFromString(stringValues);
      var parentSelect = document.getElementById(parentId);
      /* we only want to display each parent value once. This object keeps track of the already added ones */
      var uniqueParentValues = {};
      
      /* populate parent select */
      for (var i = 0; i < valsArray.length; i++) {
         var realV = getParentValue(valsArray);
         if(!uniqueParentValues.hasOwnProperty(realV)) {
            uniqueParentValues[realV] = true;
            
            /* create new option for the parent select */
            var parOption = document.createElement("option");
            parOption.value = realV;
            parOption.innerHTML = realV;
            if(realV == getParentValue(initialValue)) {
               parOption.selected = "selected";
            }
            parentSelect.appendChild(parOption);
         }
      }
      
      /* populate child select */
      onChangeParentValue(stringValues, parentId, childId, totalId);
      
      /* sets the correct child value */
      var childSelect = document.getElementById(childId);
      childSelect.value = getChildValue(initialValue);
      onChangeChildValue(stringValues, parentId, childId, totalId);
   }
</script>

<div class="form-field">
   <!– readonly mode. No action required, display the composed value as is –>
   <#if form.mode == "view">
      <div class="viewmode-field">
         <#if field.mandatory && !(fieldValue?is_number) && fieldValue?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 fieldValue?string == "">
            <#assign valueToShow=msg("form.control.novalue")>
         <#else>
            <#assign valueToShow=fieldValue>
            <#if field.control.params.options?? && field.control.params.options != "">
               <#list field.control.params.options?split(optionSeparator) as nameValue>
                  <#if nameValue?index_of(labelSeparator) == -1>
                     <#if nameValue == fieldValue?string || (fieldValue?is_number && fieldValue?c == nameValue)>
                        <#assign valueToShow=nameValue>
                        <#break>
                     </#if>
                  <#else>
                     <#assign choice=nameValue?split(labelSeparator)>
                     <#if choice[0] == fieldValue?string || (fieldValue?is_number && fieldValue?c == choice[0])>
                        <#assign valueToShow=msgValue(choice[1])>
                        <#break>
                     </#if>
                  </#if>
               </#list>
            </#if>
         </#if>
         <span class="viewmode-value">${valueToShow?html}</span>
      </div>
   <#else>
     <!– Editable mode. –>
     <!– let's keep a single label for both fields –>
      <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.options?? && field.control.params.options != "">
       <!– first select for the parent value –>
         <select id="parent-${fieldHtmlId}" 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>
            onchange="onChangeParentValue('${field.control.params.options}', 'parent-${fieldHtmlId}', 'child-${fieldHtmlId}', '${fieldHtmlId}');" >
         </select>
      
       <!– second select for the child value –>
         <select id="child-${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>
            onchange="onChangeChildValue('${field.control.params.options}', 'parent-${fieldHtmlId}', 'child-${fieldHtmlId}', '${fieldHtmlId}');" >
         </select>
      
       <!– hidden field containing the final value (e.g. the one sent to the server and stored in database) –>
       <input type="hidden" id="${fieldHtmlId}" name="${field.name}" />
      
         <@formLib.renderFieldHelp field=field />
      <#else>
         <div id="${fieldHtmlId}" class="missing-options">${msg("form.control.selectone.missing-options")}</div>
      </#if>
   </#if>
</div>

<!– IMPORTANT: this script must execute after the fields are created –>
<script type="text/javascript">
   setTimeout(function(){populateSelects("${field.control.params.options}", "${fieldValue}", "parent-${fieldHtmlId}", "child-${fieldHtmlId}", "${fieldHtmlId}");}, 1000);
</script>


Then all you have to do is override the views (there might be several of them) where your custom constraint appears to explicitely ask for it to be rendered with this new form component (let's call it "cascading-dropdowns.ftl"):


<field id="my:cascadingProperty" label-id="workflow.field.cascadingProperty">
   <control template="/org/alfresco/components/form/controls/cascading-dropdowns.ftl" />
</field>


Pros:
- easy to deploy
- relatively standard, so there are few chances that a new version of Alfresco breaks it

Cons:
- The list of available values can be huge depending on your needs.
- The value stored in database isn't 2 clean fields but a single one you'd have to parse if you retrieve it programatically/via CMIS/etc.
- I have no idea if you could write a similar component to override its display in alfresco explorer. The current one only works for Share.

There still is room for improvement (on the size parameter or the field title/labels) but I hope you get the idea and can improve it further to meet your needs.

alexsolomatin
Champ on-the-rise
Champ on-the-rise
Hi everyone,

Has anybody tried to use such custom controls in Advanced Search? I implemented a solution when all dynamic constraints are defined in JSON files which are loaded by AJAX calls when rendering custom controls. Everything is fine in the latest Chrome and Firefox but I have a strange problem with IE on the Advanced Search page. It looks like no any javascripts defined in the custom cotrol template are loaded, so when the change event occurs, the browser can't just find the event handler. By the way, everything is OK on the Edit properties page.

I discoverd that almost all controls on the Advanced Search page are loaded by the YUI library. Maybe there are some differences in the implementation for IE and the rest of browsers… Has anyone had similar problems?

scouil
Star Contributor
Star Contributor
Hello,

Good point there. IE was beyond my testing scope (lucky me!) but it is a troubling issue.
I noticed something was different in the advanced search since the script line at the end of my file was triggered BEFORE the html was injected.
But I didn't get into it more deeply and simply delayed my script call.

I guess a solution would be to dynamically create the onchange events in javascript. This will ensure that the closures are correctly set.

I'll repost the new code if it succeeds.

scouil
Star Contributor
Star Contributor
As expected, defining the onchange event in javascript fixes it.
I ended up refactoring a bit my code and spliting it into 2 files:

/org/alfresco/components/form/controls/cascading-dropdowns.ftl :

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

<!– current value –>
<#assign fieldValue=field.value>

<!– handle default value for empty current value –>
<#if fieldValue?string == "" && field.control.params.defaultValueContextProperty??>
   <#if context.properties[field.control.params.defaultValueContextProperty]??>
      <#assign fieldValue = context.properties[field.control.params.defaultValueContextProperty]>
   <#elseif args[field.control.params.defaultValueContextProperty]??>
      <#assign fieldValue = args[field.control.params.defaultValueContextProperty]>
   </#if>
</#if>

<div class="form-field">
   <!– readonly mode. No action required, display the composed value as is –>
   <#if form.mode == "view">
      <div class="viewmode-field">
         <#if field.mandatory && !(fieldValue?is_number) && fieldValue?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 fieldValue?string == "">
            <#assign valueToShow=msg("form.control.novalue" )>
         <#else>
            <#assign valueToShow=fieldValue>
            <#if field.control.params.options?? && field.control.params.options != "">
               <#list field.control.params.options?split(optionSeparator) as nameValue>
                  <#if nameValue?index_of(labelSeparator) == -1>
                     <#if nameValue == fieldValue?string || (fieldValue?is_number && fieldValue?c == nameValue)>
                        <#assign valueToShow=nameValue>
                        <#break>
                     </#if>
                  <#else>
                     <#assign choice=nameValue?split(labelSeparator)>
                     <#if choice[0] == fieldValue?string || (fieldValue?is_number && fieldValue?c == choice[0])>
                        <#assign valueToShow=msgValue(choice[1])>
                        <#break>
                     </#if>
                  </#if>
               </#list>
            </#if>
         </#if>
         <span class="viewmode-value">${valueToShow?html}</span>
      </div>
   <#else>
     <!– Editable mode. –>
     <!– let's keep a single label for both fields –>
      <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.options?? && field.control.params.options != "">
       <!– first select for the parent value –>
         <select id="parent-${fieldHtmlId}" 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>>
         </select>
      
       <!– second select for the child value –>
         <select id="child-${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>>
         </select>
      
       <!– hidden field containing the final value (e.g. the one sent to the server and stored in database) –>
       <input type="hidden" id="${fieldHtmlId}" name="${field.name}" />
      
         <@formLib.renderFieldHelp field=field />
      <#else>
         <div id="${fieldHtmlId}" class="missing-options">${msg("form.control.selectone.missing-options" )}</div>
      </#if>
   </#if>
</div>

<!– IMPORTANT: this script must execute after the fields are created –>
<script type="text/javascript">//<![CDATA[
   setTimeout(function(){populateSelects("${field.control.params.options}", "${fieldValue}", "parent-${fieldHtmlId}", "child-${fieldHtmlId}", "${fieldHtmlId}");}, 1000);
//]]></script>


and /org/alfresco/components/form/controls/common/cascadingDropdownUtils.inc.ftl :

<!– defines how the option list elements are separated when passed to the client –>
<#if field.control.params.optionSeparator??>
   <#assign optionSeparator=field.control.params.optionSeparator>
<#else>
   <#assign optionSeparator=",">
</#if>

<!– defines how the label list elements are separated when passed to the client –>
<#if field.control.params.labelSeparator??>
   <#assign labelSeparator=field.control.params.labelSeparator>
<#else>
   <#assign labelSeparator="|">
</#if>

<script type="text/javascript">//<![CDATA[
<!– defines how the option list elements are separated when passed to the client –>
<#if field.control.params.optionSeparator??>
   <#assign optionSeparator=field.control.params.optionSeparator>
<#else>
   <#assign optionSeparator=",">
</#if>

<!– defines how the label list elements are separated when passed to the client –>
<#if field.control.params.labelSeparator??>
   <#assign labelSeparator=field.control.params.labelSeparator>
<#else>
   <#assign labelSeparator="|">
</#if>

<script type="text/javascript">//<![CDATA[
   /* string used to separate the parent from the child */
   var OPTION_SEPARATOR = "/";

   /**
   * Functions used to split and synchronize the data between the 2 lists.
   **/
   
   /**
   *   @param stringValues: string containing the available values for the given field
   *   @returns a javascript object containing those values
   **/
   function getValsArrayFromString(stringValues) {
      //array with the options
      var valsArray = stringValues.split("${optionSeparator}");
      //remove the labels
      for (var i = 0; i < valsArray.length; i++) {
         if(valsArray.indexOf("${labelSeparator}") != -1) {
            valsArray = valsArray.substr(0,valsArray.indexOf("${labelSeparator}"));
         }
      }
      return valsArray;
   }

   /**
   * Extracts the parent value from the full value of the option
   **/
   function getParentValue(fullValue) {
      return fullValue.substr(0,fullValue.indexOf(OPTION_SEPARATOR));
   }

   /**
   * Extracts the child value from the full value of the option
   **/
   function getChildValue(fullValue) {
      return fullValue.substr(fullValue.indexOf(OPTION_SEPARATOR) + 1);
   }

   /**
   * Parent value have been changed.
   * Reset the current total value, child value and updates the available child values
   **/
   function onChangeParentValue(valsArray, parentId, childId, totalId) {
      var parentValue = document.getElementById(parentId).value;
      var childSelect = document.getElementById(childId);
      
      /* empty the child select */
      while(childSelect.firstChild) {
         childSelect.removeChild(childSelect.firstChild);
      }
      
      /* fill the child select with the new values */
      for (var i = 0; i < valsArray.length; i++) {
         if ( getParentValue(valsArray) == parentValue) {
            var childValue = getChildValue(valsArray);
            var childOption = document.createElement("option");
            childOption.value = childValue;
            childOption.innerHTML = childValue;
            childSelect.appendChild(childOption);
         }
      }
   }

   /**
   * Child value have been changed.
   * Update the total value
   **/
   function onChangeChildValue(parentId, childId, totalId) {
      var parentValue = document.getElementById(parentId).value;
      var childValue = document.getElementById(childId).value;
      document.getElementById(totalId).value = parentValue + OPTION_SEPARATOR + childValue;
   }

   /**
   * Initial filling of the parent values available
   * TODO: handle default value
   **/
   function populateSelects(stringValues, initialValue, parentId, childId, totalId) {
      /* get an array from the string values */
      var valsArray = getValsArrayFromString(stringValues);
      var parentSelect = document.getElementById(parentId);
      /* we only want to display each parent value once. This object keeps track of the already added ones */
      var uniqueParentValues = {};
      
      /* populate parent select */
      for (var i = 0; i < valsArray.length; i++) {
         var realV = getParentValue(valsArray);
         if(!uniqueParentValues.hasOwnProperty(realV)) {
            uniqueParentValues[realV] = true;
            
            /* create new option for the parent select */
            var parOption = document.createElement("option");
            parOption.value = realV;
            parOption.innerHTML = realV;
            if(realV == getParentValue(initialValue)) {
               parOption.selected = "selected";
            }
            parentSelect.appendChild(parOption);
         }
      }
      parentSelect.onchange = function(){onChangeParentValue(valsArray, parentId, childId, totalId);};
      
      /* populate child select */
      onChangeParentValue(valsArray, parentId, childId, totalId);
      
      /* sets the correct child value */
      var childSelect = document.getElementById(childId);
      childSelect.value = getChildValue(initialValue);
      childSelect.onchange = function(){onChangeChildValue(parentId, childId, totalId);};
      onChangeChildValue(parentId, childId, totalId);
   }
//]]></script>


IE advanced search now works as expected.

umaistudio
Champ in-the-making
Champ in-the-making
Created custom properties using becpg model designer. Can u suggest your ideas how to create cascading drop down please?

alexsolomatin
Champ on-the-rise
Champ on-the-rise
Thanks, Scouil!

Your solution works well.