Sunday, October 9, 2016

How to create a custom Coveo Facet component in Sitecore application

If you use Coveo renderings in your solution and facet controls that Coveo provides out-of-the-box don’t match the requirements you have, you can create a custom JavaScript component that would behave according to your specifications. In this blog I’ll demonstrate how to go about creating a custom dropdown facet.
  1. First I created a copy of Coveo Facet View rendering in Sitecore and a view file in my solution. The code in the view file was almost exact duplicate of out-of-the-box FacetView.cshtml. I changed the css class on the div element for my facet to ‘CoveoFacetDropdown’ and added data-default-caption='@Model.DefaultCaption' attribute.

    @Html.Coveo().RenderErrorSummary(Model.ValidateModel())
    
    <script type="text/javascript" src="/Coveo/js/CustomCoveoJsSearch.js"></script>
    
    @if (Model.IsConfigured) {
        <div>
            @if (Model.IconProperties != null) {
                <style>
                    @(Html.Raw(Model.GetIconCss()))
                </style>
            }
    
            <div id='@Model.Id'
                 class="CoveoFacetDropdown"
                 data-title='@Model.Title'
                 data-default-caption='@Model.DefaultCaption'
                 data-field='@Model.Field'
                 data-number-of-values='@Model.NumberOfValues'
                 data-id='@Model.Id'
                 data-enable-collapse='@Model.EnableCollapse'
                 data-enable-more-less='@Model.EnableMoreLess'
                 data-enable-settings='@Model.EnableSettings'
                 data-lookup-field='@Model.LookupField'
                 data-sort-criteria='@Model.Sort'
                 data-is-multi-value-field='@Model.IsMultiValueField'
                 data-show-icon='@Model.ShowIcon'
                 data-header-icon='no-icon'
                 data-computed-field='@Model.ComputedField'
                 data-computed-field-operation='@Model.ComputedFieldOperation'
                 data-computed-field-format='@Model.ComputedFieldFormat'
                 data-computed-field-caption='@Model.ComputedFieldCaption'
                 data-include-in-breadcrumb='@Model.IncludeInBreadcrumb'
                 data-number-of-values-in-breadcrumb='@Model.NumberOfValuesInBreadcrumb'
                 data-include-in-omnibox='@Model.IncludeInOmnibox'
                 data-enable-facet-search='@Model.EnableFacetSearch'
                 data-number-of-values-in-facet-search='@Model.NumberOfValuesInFacetSearch'
                 data-enable-toggling-operator='@Model.EnableTogglingOperator'
                 data-use-and='@Model.UseAnd'
                 data-page-size='@Model.MorePageSize'
                 data-injection-depth='@Model.InjectionDepth'
                 data-available-sorts='@String.Join(",", Model.AvailableSorts)'></div>
        </div>
    }
    
    
  2. Next step was to implement this facet in JavaScript. For that I created a new CustomCoveoJsSearch.js file in my solution and placed my dropdown facet implementation there. The entry point for facet lives in the following function:

    var Coveo;
    (function (Coveo) {
        var FacetDropdown = (function (_super) {
            __extends(FacetDropdown, _super);
            function FacetDropdown(element, options, bindings) {
                _super.call(this, element, Coveo.ComponentOptions.initComponentOptions(element, FacetDropdown, options), bindings, FacetDropdown.ID);
                this.element = element;
                this.options.enableFacetSearch = false;
                this.options.enableSettings = false;
                this.options.includeInOmnibox = false;
                this.options.enableMoreLess = false;
                this.options.defaultCaption = element.getAttribute("data-default-caption");
            }
    
            FacetDropdown.prototype.initFacetQueryController = function () {
                this.facetQueryController = new Coveo.FacetDropdownQueryController(this);
            };
    
            FacetDropdown.prototype.initFacetValuesList = function () {
                this.facetValuesList = new Coveo.FacetDropdownValuesList(this, Coveo.DropdownFacetSearchValueElement);
                Coveo.$(this.element).append(this.facetValuesList.build());
            };
    
            FacetDropdown.ID = 'FacetDropdown';
            FacetDropdown.parent = Coveo.Facet;
            FacetDropdown.options = {
                dateField: Coveo.ComponentOptions.buildBooleanOption({ defaultValue: false })
            };
            return FacetDropdown;
        })(Coveo.Facet);
        Coveo.FacetDropdown = FacetDropdown;
        Coveo.CoveoJQuery.registerAutoCreateComponent(FacetDropdown);
    })(Coveo || (Coveo = {}));

    Notice that the name of the object in JavaScript is "FacetDropdown" while css class in html is "CoveoFacetDropdown". It turns out that Coveo component match with corresponding css classes are done by appending "Coveo" to css class. In other words if you create a component with the name "FacetDropdown", css class has to be "CoveoFacetDropdown".
  3. To complete implementation of this custom control I added methods to instantiate component options, controller and option list methods. So at the end I arrived at the following:
    var Coveo;
    (function (Coveo) {
        var FacetDropdownQueryController = (function (_super) {
            __extends(FacetDropdownQueryController, _super);
            function FacetDropdownQueryController(facet) {
                _super.call(this, facet);
                this.facet = facet;
            }
            return FacetDropdownQueryController;
        })(Coveo.FacetQueryController);
        Coveo.FacetDropdownQueryController = FacetDropdownQueryController;
    })(Coveo || (Coveo = {}));
    
    var Coveo;
    (function (Coveo) {
        var DropdownFacetSearchValueElement = (function (_super) {
            __extends(DropdownFacetSearchValueElement, _super);
            function DropdownFacetSearchValueElement(facet, facetValue, keepDisplayedValueNextTime) {
                _super.call(this, facet, facetValue, keepDisplayedValueNextTime);
                this.facet = facet;
                this.facetValue = facetValue;
                this.keepDisplayedValueNextTime = keepDisplayedValueNextTime;
            }
            DropdownFacetSearchValueElement.prototype._handleExcludeClick = function (eventBindings) {
                this.facet.open(this.facetValue);
                _super.prototype.handleExcludeClick.call(this, eventBindings);
            };
            DropdownFacetSearchValueElement.prototype.build = function () {
                this.renderer = new Coveo.DropdownValueElementRenderer(this.facet, this.facetValue).build();
                return this;
            };
            DropdownFacetSearchValueElement.prototype.handleEventForValueElement = function (eventBindings) {
                var _this = this;
                return this;
            };
            
            return DropdownFacetSearchValueElement;
        })(Coveo.FacetValueElement);
        Coveo.DropdownFacetSearchValueElement = DropdownFacetSearchValueElement;
    })(Coveo || (Coveo = {}));
    
    var Coveo;
    (function (Coveo) {
        var FacetDropdownValuesList = (function (_super) {
            __extends(FacetDropdownValuesList, _super);
            function FacetDropdownValuesList(facet) {
                _super.call(this, facet, Coveo.DropdownFacetSearchValueElement);
                this.facet = facet;
            }
            FacetDropdownValuesList.prototype.build = function () {
                this.valueContainer = Coveo.$('<select class="coveo-facet-values"/>');
                Coveo.Component.pointElementsToDummyForm(this.valueContainer);
                this.bindEvent({ displayNextTime: false, pinFacet: this.facet.options.preservePosition });
                return this.valueContainer;
            };
            FacetDropdownValuesList.prototype.select = function (value) {
                var valueElement = this.get(value);
                valueElement.select();
                return valueElement;
            };
            FacetDropdownValuesList.prototype._getValuesToBuildWith = function () {
                var values = this.facet.values.getAll();
                if (values && values.length > 0 && values[0].value !== this.facet.options.defaultCaption) {
                    var emptyValue = new Coveo.FacetValue();
                    emptyValue.value = this.facet.options.defaultCaption;
                    emptyValue.lookupValue = this.facet.options.defaultCaption;
                    values.unshift(emptyValue);
                }
                return this.facet.facetSort.reorderValues(values);
            };
            FacetDropdownValuesList.prototype.toggleSelect = function (value) {
                var valueElement = this.get(value);
                if (valueElement.facetValue.selected) {
                    valueElement.unselect();
                }
                else {
                    valueElement.select();
                }
                return valueElement;
            };
            FacetDropdownValuesList.prototype.handleEventForSelectChange = function (eventBindings) {
                var _this = this;
                this.valueContainer.change(function (event) {
                    if (eventBindings.omniboxObject) {
                        _this.omniboxCloseEvent(eventBindings.omniboxObject);
                    }
                    _this.handleSelectValue(eventBindings);
                    if (Coveo.DeviceUtils.isMobileDevice() && !_this.facet.searchInterface.isNewDesign() && _this.facet.options.enableFacetSearch) {
                        Coveo.Defer.defer(function () {
                            Coveo.ModalBox.close(true);
                            _this.facet.facetSearch.completelyDismissSearch();
                        });
                    }
                });
            };
            FacetDropdownValuesList.prototype.bindEvent = function (eventBindings) {
                if (!Coveo.Utils.isNullOrUndefined(eventBindings.omniboxObject)) {
                    this.isOmnibox = true;
                }
                else {
                    this.isOmnibox = false;
                }
                this.handleEventForSelectChange(eventBindings);
            };
            FacetDropdownValuesList.prototype.handleSelectValue = function (eventBindings) {
                var _this = this;
                this.facet.keepDisplayedValuesNextTime = eventBindings.displayNextTime && !this.facet.options.useAnd;
                var actionCause;
                Coveo.$(this.facet.values.values).each(function (i) {
                    _this.facet.values.values[i].selected = false;
                });
                var selectedIndex = this.valueContainer[0].selectedIndex;
                this.facetValue = this.facet.values.values[selectedIndex];
                if (this.facetValue.value !== this.facet.options.defaultCaption) {
                    this.facetValue.selected = true;
                } 
                if (this.facetValue.excluded) {
                    actionCause = Coveo.AnalyticsActionCauseList.facetUnexclude;
                    this.facet.unexcludeValue(this.facetValue);
                }
                else {
                    if (this.facetValue.selected) {
                        actionCause = Coveo.AnalyticsActionCauseList.facetDeselect;
                    }
                    else {
                        actionCause = Coveo.AnalyticsActionCauseList.facetSelect;
                    }
                }
                if (this.isOmnibox) {
                    actionCause = Coveo.AnalyticsActionCauseList.omniboxFacet;
                }
                this.facet.triggerNewQuery(function () { return _this.facet.usageAnalytics.logSearchEvent(actionCause, _this.getAnalyticsFacetMeta()); });
            };
            FacetDropdownValuesList.prototype.getAnalyticsFacetMeta = function () {
                return {
                    facetId: this.facet.options.id,
                    facetValue: this.facetValue.value,
                    facetTitle: this.facet.options.title
                };
            };
            return FacetDropdownValuesList;
        })(Coveo.FacetValuesList);
        Coveo.FacetDropdownValuesList = FacetDropdownValuesList;
    })(Coveo || (Coveo = {}));
    
    var Coveo;
    (function (Coveo) {
        var DropdownValueElementRenderer = (function () {
            function DropdownValueElementRenderer(facet, facetValue) {
                this.facet = facet;
                this.facetValue = facetValue;
            }
            DropdownValueElementRenderer.prototype.withNo = function (element) {
                if (Coveo._.isArray(element)) {
                    Coveo._.each(element, function (e) {
                        if (e) {
                            e.remove();
                        }
                    });
                }
                else {
                    if (element) {
                        element.remove();
                    }
                }
                return this;
            };
            DropdownValueElementRenderer.prototype.build = function () {
                var _this = this;
                this.buildOptionValue();
                return this;
            };
            DropdownValueElementRenderer.prototype.buildOptionValue = function () {
                var caption = this.facet.getValueCaption(this.facetValue);
                this.listElement = Coveo.$('<option class="coveo-facet-value coveo-facet-selectable"/>')
                    .attr("value", this.facetValue.lookupValue)
                    .text(caption);
                if (caption === this.facetValue.value && this.facetValue.selected) {
                    this.listElement.attr("selected",true);
                }
            };
            DropdownValueElementRenderer.prototype.setCssClassOnListValueElement = function () {
                this.listElement.toggleClass('coveo-selected', this.facetValue.selected);
            };
            return DropdownValueElementRenderer;
        })();
        Coveo.DropdownValueElementRenderer = DropdownValueElementRenderer;
    })(Coveo || (Coveo = {}));
    
    
    var Coveo;
    (function (Coveo) {
        var FacetDropdown = (function (_super) {
            __extends(FacetDropdown, _super);
            function FacetDropdown(element, options, bindings) {
                _super.call(this, element, Coveo.ComponentOptions.initComponentOptions(element, FacetDropdown, options), bindings, FacetDropdown.ID);
                this.element = element;
                this.options.enableFacetSearch = false;
                this.options.enableSettings = false;
                this.options.includeInOmnibox = false;
                this.options.enableMoreLess = false;
                this.options.defaultCaption = element.getAttribute("data-default-caption");
            }
    
            FacetDropdown.prototype.initFacetQueryController = function () {
                this.facetQueryController = new Coveo.FacetDropdownQueryController(this);
            };
    
            FacetDropdown.prototype.initFacetValuesList = function () {
                this.facetValuesList = new Coveo.FacetDropdownValuesList(this, Coveo.DropdownFacetSearchValueElement);
                Coveo.$(this.element).append(this.facetValuesList.build());
            };
    
            FacetDropdown.ID = 'FacetDropdown';
            FacetDropdown.parent = Coveo.Facet;
            FacetDropdown.options = {
                dateField: Coveo.ComponentOptions.buildBooleanOption({ defaultValue: false })
            };
            return FacetDropdown;
        })(Coveo.Facet);
        Coveo.FacetDropdown = FacetDropdown;
        Coveo.CoveoJQuery.registerAutoCreateComponent(FacetDropdown);
    })(Coveo || (Coveo = {}));