Sunday, June 11, 2017

Drilldown Autosuggest Drop down Properties in EPiServer

In one of our projects we had to create drill down auto suggest drop down for EPiServer properties.
We had three level of drill down based on parent and child relation. We wanted to use the existing auto suggest functionality available and tweak it little bit to fit our requirement.
We decided to write dojo JS to change the functionality for suggestion drop down.

How the Drop down looks like




Selection Model


 public class SelectionModel : SelectItem
    {
        public string PrentId { get; set; }
    }


Selection Query Classes

Competition Selection Query


 [ServiceConfiguration(typeof(ISelectionQuery))]
    public class CompetitionSelectionQuery : ISelectionQuery
    {
        IEnumerable _competitions;

        public CompetitionSelectionQuery()
        {
            _competitions = new List
            {
                new SelectItem
                {
                    Text = "Competition 1",
                    Value = "1"
                },
                 new SelectItem
                {
                    Text = "Competition 2",
                    Value = "2"
                }
            };
        }

        public ISelectItem GetItemByValue(string value)
        {
            return _competitions.FirstOrDefault(s => s.Value.ToString() == value);
        }

        public IEnumerable GetItems(string query)
        {
            return _competitions.Where(s => s.Text.Contains(query));
        }
    }


Season Selection Query


 [ServiceConfiguration(typeof(ISelectionQuery))]
    public class SeasonSelectionQuery : ISelectionQuery
    {
        IEnumerable _seasons;

        public SeasonSelectionQuery()
        {
            _seasons = new List
            {
                new SelectionModel
                {
                    Text = "Season 1",
                    Value = "1",
                    PrentId = "1"
                },
                new SelectionModel
                {
                    Text = "Season 2",
                    Value = "2",
                    PrentId = "1"
                },
                new SelectionModel
                {
                    Text = "Season 1",
                    Value = "3",
                    PrentId = "2"
                },
                new SelectionModel
                {
                    Text = "Season 2",
                    Value = "4",
                    PrentId = "2"
                },
            };
        }

        public ISelectItem GetItemByValue(string value)
        {
            return _seasons.FirstOrDefault(s => s.Value.ToString() == value);
        }

        public IEnumerable GetItems(string query)
        {
            var parent = query.Split(new[] { '-' })[0];
            return _seasons.Where(s => s.Text.Contains(query));
        }
    }


Round Selection Query


 [ServiceConfiguration(typeof(ISelectionQuery))]
    public class RoundSelectionQuery : ISelectionQuery
    {
        IEnumerable _rounds;

        public RoundSelectionQuery()
        {
            _rounds = new List
            {
                new SelectionModel
                {
                    Text = "Round 1",
                    Value = "1",
                    PrentId = "1"
                },
                new SelectionModel
                {
                    Text = "Round 2",
                    Value = "2",
                    PrentId = "1"
                },
                new SelectionModel
                {
                    Text = "Round 1",
                    Value = "3",
                    PrentId = "2"
                },
                new SelectionModel
                {
                    Text = "Round 2",
                    Value = "4",
                    PrentId = "2"
                },
            };
        }

        public ISelectItem GetItemByValue(string value)
        {
            return _rounds.FirstOrDefault(s => s.Value.ToString() == value);
        }

        public IEnumerable GetItems(string query)
        {
            var parent = query.Split(new[] { '-' })[0];
            return _rounds.Where(s => s.Text.Contains(query));
        }
    }


How to define properties in Content


        [Display(Order = 110)]
        [LinkedAutoSelection(typeof(CompetitionTagSelectionQuery), null, null, new[] { "/season/topic", "/round/topic"})]
        public virtual string Competition { get; set; }

        [LinkedAutoSelection(typeof(SeasonTagSelectionQuery), new[] { "competition" }, "/season/topic", new[] { "/round/topic" })]
        [Display(Order = 120)]
        public virtual string Season { get; set; }

        [LinkedAutoSelection(typeof(RoundTagSelectionQuery), new[] { "competition", "season" }, "/round/topic", null)]
        [Display(Order = 130)]
        public virtual string Round { get; set; }



Understanding LinkedAutoSelection Attribute


        [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class LinkedAutoSelectionAttribute : Attribute, IMetadataAware
    {
        internal Injected ModuleTable { get; set; }
        public Type SelectionFactoryType { get; set; }

        /* Dependent Parents: Child needs to pull data from parents to pass to the auto suggest query
         * We will fetch the parents values based on input name */
        public string[] Parents { get; set; }

        /* The change in value will be published to all the publishers */
        public string[] Publishers { get; set; }

        /* The subscriber will reset the value based on parent selection */
        public string Subscriber { get; set; }

        public LinkedAutoSelectionAttribute(Type selectionFactoryType, string[] parents,  string subscriber, string[] publishers)
        {
            SelectionFactoryType = selectionFactoryType;
            this.Publishers = publishers;
            this.Subscriber = subscriber;
            this.Parents = parents;
        }

        public void OnMetadataCreated(ModelMetadata metadata)
        {
            var extendedMetadata = metadata as ExtendedMetadata;
            if (extendedMetadata == null)
                return;

            extendedMetadata.ClientEditingClass = "nrl/Editors/LinkedSelectionEditor/Editor";
            extendedMetadata.CustomEditorSettings["uiType"] = (object)extendedMetadata.ClientEditingClass;
            extendedMetadata.CustomEditorSettings["uiWrapperType"] = (object)"flyout";
            string format = this.ModuleTable.Service.ResolvePath("Shell", "stores/selectionquery/{0}/");
            extendedMetadata.EditorConfiguration["storeurl"] = (object)string.Format((IFormatProvider)CultureInfo.InvariantCulture, format, new object[1]
            {
                (object) this.SelectionFactoryType.FullName
            });

            extendedMetadata.EditorConfiguration["parents"] = Parents;
            extendedMetadata.EditorConfiguration["publishers"] = Publishers;
            extendedMetadata.EditorConfiguration["subscriber"] = Subscriber;
        }
    }



Adding LinkedSelectionEditor Editor.Js File

Setting up module.config

In module.config the path to Resources/Scripts is defined as Murtaza and
inside Resources/Scripts. I have Editors -> LinkedSelectionEditor -> Editor.js
therefore,  the ClientEditingClass is Murtaza/Editors/ImageContentSelector/Editor




<module clientresourcerelativepath="" tags="EPiServerPublicModulePackage">
  <assemblies>
    <add assembly="XXX">
    <add assembly="XXX">
  </add></add></assemblies>

  <dojo>
    <paths>
      <add name="Murtaza" path="Resources/Scripts">
    </add></paths>
  </dojo>

  <clientmodule>
    <moduledependencies>
      <add dependency="CMS" type="RunAfter">
    </add></moduledependencies>
  </clientmodule>
  
</module>


Editor.Js


define("nrl/Editors/LinkedSelectionEditor/Editor",[
    "dojo/_base/declare",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "epi-cms/_ContentContextMixin",
    "dojo/topic",
    "dojo/_base/array",
     "epi/shell/form/AutoCompleteSelectionEditor"
],
    function (
        declare,
        _Widget,
        _TemplatedMixin,
        _ContentContextMixin,
        topic,
        array,
        AutoCompleteSelectionEditor
    ) {
        return declare([
            _Widget,
            _TemplatedMixin,
            _ContentContextMixin,
            AutoCompleteSelectionEditor,            
        ], {
                /* delimeter : String
                 * Delimeter for value and page name
                 * Competition111 -  NRL(NRL is the name of page)*/
                delimeter: "-",
                oldValue: "",
                
                postMixInProperties: function () {
                    this.inherited(arguments);

                    var that = this;
                    if (this.subscriber) {
                        topic.subscribe(this.subscriber, function () {
                            var found = false;
                            var argument = arguments[0];

                            /* We are in context of subscriber and as we have recieved notification for changed in parent value therefore,
                             * we are going to reset current dropdown
                             * clearing the value */

                            /* Clearing value */
                            that._setValueAttr('', true, '');

                            /* Clearing display value */
                            that._setDisplayedValueAttr('');
                        });
                    }
                },

                _setDisplayedValueAttr: function (/*String*/ label, /*Boolean?*/ priorityChange) {

                    /* If label is setting as '' then we need to verify if textbox exits.
                     * in some of the other functions during life cycle textbox does not exists
                     * After clearing up display value we are not going to call base function as it will start fetching
                     * list from server unnecessary and throws exceptions*/
                    if (label === '') {
                        if (this.textbox)
                          this.textbox.value = label;
                        return;
                    }

                    this.inherited(arguments);
                },

                _selectOption: function (/*DomNode*/ target) {

                    this.inherited(arguments);
                    var that = this;
                    if (this.getValue() != this.oldValue)
                    {
                        /* Publish the change to all pubilshers */
                        array.forEach(this.publishers, function (publisher) {

                            /* Publishing Change
                             * Create key value pair */
                            var keyValue = new Object();
                            keyValue.key = that.subscriber;
                            keyValue.value = that.value;
                            topic.publish(publisher, keyValue);
                        });
                    }
                },

                _setValueAttr: function (/*String*/ value, /*Boolean?*/ priorityChange, /*String?*/ displayedValue, /*item?*/ item) {

                   /* Saving the old value */
                    this.oldValue = this.getValue();
                    this.inherited(arguments);
                },

                _startSearch: function (/*String*/ text) {
                    /* Competition111-Season2017-Round06-{text} */
                    /* Prefixing all the pre-requisite parent values */
                    var prefixQuery = '';

                    /* Fetch values from parents */
                    for (var i = 0; this.parents && this.parents.length > i; i++) {
                        var dependantValues = document.getElementsByName(this.parents[i]);
                        if (dependantValues.length > 0) {
                            prefixQuery += this.removePageName(dependantValues[0].value) + this.delimeter;
                        }
                    }

                    /* Removing the first & last {this.delimeter} character (^\-|\-+$)/g */
                    var regex = new RegExp('(^\\' + this.delimeter + '|\\' + this.delimeter + '+$)', 'g');
                    prefixQuery = prefixQuery.replace(regex, '');
                    prefixQuery = (prefixQuery + this.delimeter + text).replace(regex, '');

                    /* Reassigning search text with all pre-requisite values
                     * Keeping old value to reassign later */
                    var oldText = text;
                    text = prefixQuery;
                    this.inherited(arguments);

                    /* Reassigning old value this property will be used in base class */
                    this._lastInput = oldText;
                },

                /* Remove the page name suffixed after {this.delimeter} character */
                removePageName: function (val)
                {
                    var trimmedValue = val;
                    var typeAndId = trimmedValue.split(this.delimeter);
                    if (typeAndId.length > 0) {
                        trimmedValue = typeAndId[0];
                    }

                    return trimmedValue;
                }
        });
    });