Monday, November 28, 2016

Sitecore Data Exchange Framework - Sql Server provider

As you have probably heard Sitecore team has released a new module for integration Sitecore application with other systems. I have written an import tool of my own, and was very curious to see what Sitecore developers came up with. Out of the box, Data Exchange Framework module comes with Sitecore provider, which is great because it can be used with Sitecore to Sitecore migrations or for providers that need to write to Sitecore. However, I was more interested in using this module for importing content from non-Sitecore systems where the data often is stored in a SQL database.

I have to say that documentation for this module is written very well, and implementing a new provider was a very simple task just by following instructions from Developer Guide (http://integrationsdn.sitecore.net/DataExchangeFramework/v1.1/). I am not going to try to re-write the documentation, but will provide the code I arrived at after I had implemented everything that the developer guide had specified.

The code that you can find at https://github.com/jcore/Sitecore.DataExchange.Providers.SqlServer contains classes and a Sitecore package for provider. The package does not include the template for items that would be created by import process. It would be different for each solution, so I decided to omit it.

Here is screenshot of the project structure. As you can see it is pretty slim. It took me a couple of hours to implement this version of provider, which I thought was great. I really liked how easy it was to create new provider to fit what I needed to do. 

There are more to explore with this module. I am especially interested in processing of sql values before they are stored in Sitecore.

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 = {}));