Create a custom field type

You can create your own field types to meet customer specific requirements. Follow the steps below.

Steps

  • Define field type metadata
  • Implement JSON converter
  • Implement Excel converter
  • Implement edit field type converter
  • Implement settings panel controller and template
  • Implement edit panel controller and template

Field type metadata

The field type metadata defines the behavior for the field type For example if field is of type array. Field type metadata is responsible for creating the actual field type from the field definition. The field definition contains information and settings for the instance of the field, and the field type contains information about operations that are possible to use for filtering and sorting.

namespace Litium.Studio.Accelerator.FieldFramework
{
    using System.Collections.Generic;
    using System.Linq;
    using Litium.FieldFramework;

    public class FilterFieldTypeMetadata : FieldTypeMetadataBase
    {
        public override string Id => "FilterFields";
        public override bool IsArray => true;

        public override IFieldType CreateInstance(IFieldDefinition fieldDefinition)
        {
            var item = new FilterFieldType();
            item.Init(fieldDefinition);
            return item;
        }

        public class FilterFieldType : FieldTypeBase
        {
            private Option _option;

            public override object GetValue(ICollection<FieldData> fieldDatas)
            {
                if (fieldDatas.Count == 1 && fieldDatas.First().BooleanValue.HasValue)
                {
                    return null;
                }
                return fieldDatas.Where(x => !string.IsNullOrEmpty(x.TextValue)).Select(x => x.TextValue).ToList();
            }

            public override void Init(IFieldDefinition fieldDefinition)
            {
                base.Init(fieldDefinition);
                _option = fieldDefinition.Option as Option ?? new Option();
            }

            protected override ICollection<FieldData> PersistFieldDataInternal(object item)
            {
                var data = item as IEnumerable<string>;
                if (data == null)
                {
                    return new[] {new FieldData {BooleanValue = true}};
                }
                return data.Select(x => new FieldData {TextValue = x}).ToList();
            }
        }

        public class Option
        {
            public virtual List<string> Items { get; set; } = new List<string>();
        }
    }
}

JSON field type converter

The JSON field type converter is used to convert the field between entity and JSON. 

using System.Collections.Generic;
using Litium.FieldFramework.Converters;
using Litium.Runtime.DependencyInjection;
using Newtonsoft.Json.Linq;

namespace Litium.Accelerator.FieldFramework
{
    [Service(Name = "FilterFields")]
    internal class FilterJsonFieldTypeConverter : IJsonFieldTypeConverter
    {
        public object ConvertFromJsonValue(JsonFieldTypeConverterArgs args, JToken item)
        {
            var array = item as JArray;
            List<string> items = null;

            if (array != null)
            {
                items = array.ToObject<List<string>>();
            }

            var value = item as JValue;
            if (value != null)
            {
                items = new List<string>(new[] { value.ToObject<string>() });
            }
            return items;
        }

        public JToken ConvertToJsonValue(JsonFieldTypeConverterArgs args, object item)
        {
            var items = item as IList<string> ?? new List<string>();
            return new JArray(items);
        }
    }
}

Excel field type converter (optional)

The Excel field type converter is used to convert the field between entity and Excel columns data.

using System;
using System.Collections.Generic;
using System.Linq;
using Litium.Runtime.DependencyInjection;
using Litium.Web.Administration.FieldFramework;

[Service(Name = "FilterFields")]
internal class FilterExcelFieldTypeConverter : IExcelFieldTypeConverter
{
    private const string _delimiter = ",";

    public object ConvertFromExcelValue(ExcelFieldTypeConverterArgs args, object item)
    {
        var options = (FilterFieldTypeMetadata.Option)args.FieldDefinition.Option;

        var items = GetOptionValue(item as string, options, args.CultureInfo.Name);
        return items;
    }

    public object ConvertToExcelValue(ExcelFieldTypeConverterArgs args, object item)
    {
        var options = (FilterFieldTypeMetadata.Option)args.FieldDefinition.Option;

        var items = item as List<string> ?? new List<string>();
        return string.Join(_delimiter, items);
    }

    private List<string> GetOptionValue(string value, FilterFieldTypeMetadata.Option option, string cultureName)
    {
        var newValues = new List<string>();
        var importValues = value.Split(new[] {_delimiter}, StringSplitOptions.None).ToList();
        foreach (var importValue in importValues)
        {
            string fieldValue;
            if (string.IsNullOrEmpty(cultureName))
            {
                fieldValue = option.Items.FirstOrDefault(i => i == importValue);
            }
            else
            {
                fieldValue = option.Items.FirstOrDefault(i => i == importValue);
            }

            if (fieldValue != null)
            {
                newValues.Add(importValue);
            }
            else
            {
                //value doesn't exist
                newValues = null;
                break;
            }
        }
        return newValues;
    }
}

Edit field type converter

To be able to use an instance of the field in the administration interface, the edit field type converter need to be implemented. The edit field type converter is responsible to provide the administration interface with information about the controller and template that should be used for the edit or the settings view. The settings controller and template are used when setting up the field definition from system settings and the edit controller and template are used when an administrator is editing the entity.

using System.Collections.Generic;
using Litium.Runtime.DependencyInjection;
using Litium.Web.Administration.FieldFramework;
using Newtonsoft.Json.Linq;

namespace Litium.Accelerator.FieldFramework
{
    [Service(Name = "FilterFields")]
    internal class FilterEditFieldTypeConverter : IEditFieldTypeConverter
    {
        public object ConvertFromEditValue(EditFieldTypeConverterArgs args, JToken item)
        {
            var array = item as JArray;
            List<string> items = null;

            if (array != null)
            {
                items = array.ToObject<List<string>>();
            }

            var value = item as JValue;
            if (value != null)
            {
                items = new List<string>(new[] { value.ToObject<string>() });
            }
            return items;
        }

        public JToken ConvertToEditValue(EditFieldTypeConverterArgs args, object item)
        {
            var items = item as IList<string> ?? new List<string>();
            return new JArray(items);
        }

        public object CreateOptionsModel() => new FilterFieldTypeMetadata.Option();
        public string EditControllerName { get; } = "accelerator_filterFieldEdit";
        public string EditControllerTemplate { get; } = "~/Site/Administration/fieldFramework/FilterFieldEdit.html";
        public string SettingsControllerName { get; } = "accelerator_filterFieldSettings";
        public string SettingsControllerTemplate { get; } = "~/Site/Administration/fieldFramework/FilterFieldSettings.html";
    }
}

The administration interface is built with AngularJS and to extend and load extra script files and or add extra modules into application the Litium.Web.Administration.IAppExtension need to be implemented.

using System.Collections.Generic;
using Litium.Web.Administration;

namespace Litium.Studio.Accelerator.FieldFramework
{
    internal class FilterAdministrationExtension : IAppExtension
    {
        public IEnumerable<string> ScriptFiles { get; } = new[]
        {
            "~/Site/Administration/fieldFramework/accelerator_filterFieldEdit.js",
            "~/Site/Administration/fieldFramework/accelerator_filterFieldSettings.js"
        };

        public IEnumerable<string> AngularModules { get; } = null;
        public IEnumerable<string> StylesheetFiles { get; } = null;
    }
}

Settings panel controller

The scope parameter “model” is added to the controller and contains information about the options for the field definition based on the field type.

app.controller("accelerator_filterFieldSettings",
[
    "$scope", "$q", "languagesService", "pimFieldsService", "$translate", "$filter",
    function($scope, $q, languagesService, pimFieldsService, $translate, $filter) {
        "use strict";
        // scope methods
        $scope.add = function(fieldDefinition) {
            if (!Array.isArray($scope.model.items))
                $scope.model.items = [];

            if (fieldDefinition === undefined || fieldDefinition == null) return;

            if ($scope.model.items.indexOf(fieldDefinition.id) === -1) {
                $scope.model.items.push(fieldDefinition.id);
            }
        };

        $scope.remove = function(index) {
            $scope.model.items.splice(index, 1);
        };

        // scope fields
        if ($scope.model.items === undefined)
            $scope.model.items = [];

        $scope.modelServer = {}
        angular.copy($scope.model, $scope.modelServer);

        $scope.getFieldName = function(fieldId) {
            var item = $filter("filter")($scope.fields, { id: fieldId }, true);
            if (item && item.length) {
                return item[0].title;
            } else {
                $scope.model.items.splice($scope.model.items.indexOf(fieldId), 1);
            }
        }

        // scope init
        var translations = {};
        $translate([
                "pim.template.fieldgroup.systemdefined",
                "pim.template.fieldgroup.userdefined",
                "accelerator.filterfield.filternews",
                "accelerator.filterfield.filterprice",
                "accelerator.filterfield.predefined"
            ])
            .then(function(data) {
                angular.extend(translations, data);
                $q.all([
                        languagesService.getCultures()
                        .then(function(data) {
                            $scope.cultures = data;
                        }),
                        pimFieldsService.query(true)
                        .then(function(response) {
                            $scope.fields = response.data;
                            angular.forEach($scope.fields,
                                function(x) {
                                    x.groupName = x.systemDefined
                                        ? translations["pim.template.fieldgroup.systemdefined"]
                                        : translations["pim.template.fieldgroup.userdefined"];
                                });

                            $scope.fields.push({
                                "id": "#Price",
                                "title": translations["accelerator.filterfield.filterprice"],
                                "systemDefined": false,
                                "groupName": translations["accelerator.filterfield.predefined"]
                            });
                            $scope.fields.push({
                                "id": "#News",
                                "title": translations["accelerator.filterfield.filternews"],
                                "systemDefined": false,
                                "groupName": translations["accelerator.filterfield.predefined"]
                            });

                        })
                    ])
                    .then(function() {
                        $scope.loaded = true;
                    });
            });
    }
]);

Settings template

<div class="form__group" ng-form="optionsForm" ng-if="loaded">
    <div class="form__group__header" translate>accelerator.filterfield.options</div>
    <div class="form__group__item" form-field model="model.items" model-server="modelServer.items" hide-edit="true" field="formFieldDictionary['items']">
        <p class="base__align__right">
            <select ng-model="$scope.fieldDefinitionSelections" ng-options="o.title group by o.groupName  for o in (fields | orderBy:['-systemDefined', 'title'])"></select>
            <a href="#" ng-click="add($scope.fieldDefinitionSelections)" translate>general.add</a>
        </p>
        <table class="form__table">
            <thead>
                <tr>
                    <td colspan="2" translate>accelerator.filterfield.field</td>
                </tr>
            </thead>
            <tbody ui-sortable="{connectWith: '.fields-container', axis: 'y', forceHelperSize : true}" ng-model="model.items" class="fields-container">
                <tr ng-repeat="field in model.items">
                    <td>{{getFieldName(field)}}</td>
                    <td class="base__align__right"><i ng-click="remove($index)" class="fa fa-2x fa-close base__cursor__pointer"></i></td>
                </tr>
                <tr ng-if="group.fields.length == 0">
                    <td>&nbsp;</td>
                    <td>&nbsp;</td>
                </tr>
            </tbody>
        </table>
    </div>
    <div ng-debug="model"></div>
    <div ng-debug="fields"></div>
</div>

Edit panel controller

The scope parameter “model” is added to the controller and contains information about field edit view.

The model contains:

  • formField: the current form field that also contains the options that is define on the field definition.
  • fields: the collection of fields that exists on the entity.
  • culture: the culture editor has selected to use.
app.controller("accelerator_filterFieldEdit", [
    "$scope", "$q", "languagesService", "$filter", "pimFieldsService","$translate",
    function ($scope, $q, languagesService, $filter, pimFieldsService, $translate) {
        "use strict";

        $scope.filter = {
            open: false,
            search: ""
        };

        $scope.toggleFilter = function (state) {
            $scope.filter.open = state;
            $scope.filter.search = state ? "" : $scope.filter.search;
        };

        $scope.select = function (value, culture) {
            if (!culture) {
                culture = "*";
            }

            if ($scope.model.fields[$scope.model.formField.id] == null) {
                $scope.model.fields[$scope.model.formField.id] = {};
                $scope.model.fields[$scope.model.formField.id][culture] = {};
            }

            $scope.model.fields[$scope.model.formField.id][culture] = [value];
        }

        $scope.toggleSelected = function (option, culture) {
            culture = culture || '*';
            if (!($scope.model.formField.id in $scope.model.fields) || !$scope.model.fields[$scope.model.formField.id]) {
                $scope.model.fields[$scope.model.formField.id] = {};
            }
            if (!(culture in $scope.model.fields[$scope.model.formField.id])) {
                $scope.model.fields[$scope.model.formField.id][culture] = [];
            }
            var selectedIndex = $scope.model.fields[$scope.model.formField.id][culture].indexOf(option);
            if (selectedIndex < 0) {
                $scope.model.fields[$scope.model.formField.id][culture].push(option);
            }
            else {
                $scope.model.fields[$scope.model.formField.id][culture].splice(selectedIndex, 1);
            }
        }

        $scope.getOptionName = function (option, culture) {
            if (angular.isArray(option)) {
                var optionList = [];
                option.forEach(function (opt) {
                    optionList.push($scope.getOptionName(opt, culture));
                });
                return optionList;
            } else if (option) {
                var item = $filter("filter")($scope.fields, { id: option }, true);
                if (item && item.length) {
                    return item[0].title;
                }
                return option;
            } else {
                return null;
            }
        }

        $scope.setEditLanguage = function (language) {
            $scope.editLanguage = language;
        }

        $scope.setViewLanguage = function (language) {
            $scope.viewLanguage = language;
        }

        $scope.add = function () {
            $scope.formData.items[$scope.formData.items.length] = {};
        }
        $scope.remove = function (index) {
            $scope.formData.items.splice(index, 1);
        };

        var isInherited = function () {
            var field = $scope.model.fields[$scope.model.formField.id];
            var languageSupport = $scope.model.formField.languageSupport;
            return field == undefined || (languageSupport && field[$scope.editLanguage.id] == undefined) || (!languageSupport && field["*"] == undefined);
        }

        $scope.inheritChanged = function(v) {
            console.log("inheritChanged", v);
            if (v === true) {
                if ($scope.model.formField.languageSupport) {
                    $scope.model.fields[$scope.model.formField.id][$scope.editLanguage.id] = undefined;
                } else {
                    $scope.model.fields[$scope.model.formField.id]["*"] = undefined;
                }
            } else {
                $scope.model.fields[$scope.model.formField.id] = {}
                if ($scope.model.formField.languageSupport) {
                    $scope.model.fields[$scope.model.formField.id][$scope.editLanguage.id] = [];
                } else {
                    $scope.model.fields[$scope.model.formField.id]["*"] = [];
                }
            }

        }
        $scope.dummy = { inherited: false };
        $scope.$watch(function() { return isInherited(); },
            function (v) {
                console.log("isInheritedIsChanged", v, $scope.dummy.inherited);
                $scope.dummy.inherited = v;
                console.log("isInheritedIsChanged", v, $scope.dummy.inherited);
            });

        // scope init
        $scope.$watch("model.formField.editable", function (v) {
            if (v === true) {
                if ($scope.model.culture) {
                    var parentLanguage = $filter("filter")($scope.languageList, { id: $scope.model.culture }, true);
                    if (parentLanguage && parentLanguage.length > 0) {
                        $scope.editLanguage = parentLanguage[0];
                        var notParentLanguage = $filter("filter")($scope.languageList, { id: !$scope.model.culture }, true);
                        if (notParentLanguage && notParentLanguage.length > 0) {
                            $scope.viewLanguage = notParentLanguage[0];
                        }
                    }
                }
            }
        });


        languagesService.getCultures().then(function (data) {
            $scope.languageList = data.map(function (item) {
                return { id: item.id, value: item.title };
            });

            if ($scope.model.culture) {
                var parentLanguage = $filter("filter")($scope.languageList, { id: $scope.model.culture }, true);
                if (parentLanguage && parentLanguage.length > 0) {
                    $scope.editLanguage = parentLanguage[0];
                    var notParentLanguage = $filter("filter")($scope.languageList, { id: !$scope.model.culture }, true);
                    if (notParentLanguage && notParentLanguage.length > 0) {
                        $scope.viewLanguage = notParentLanguage[0];
                    }
                }
            }

            if (!$scope.editLanguage || $scope.languageList.indexOf($scope.editLanguage) === -1) {
                $scope.editLanguage = $scope.languageList[0];
            }
            if (!$scope.viewLanguage) {
                $scope.viewLanguage = $scope.languageList[$scope.languageList.length > 1 ? 1 : 0];
            }
        });

        var translations = {};
        $translate([
                "pim.template.fieldgroup.systemdefined", "pim.template.fieldgroup.userdefined",
                "accelerator.filterfield.filternews", "accelerator.filterfield.filterprice",
                "accelerator.filterfield.predefined"
            ])
            .then(function(data) {
                angular.extend(translations, data);
                pimFieldsService.query(true)
                    .then(function(response) {
                        $scope.fields = response.data;
                        $scope.fields.push({
                            "id": "#Price",
                            "title": translations["accelerator.filterfield.filterprice"],
                            "systemDefined": false,
                            "groupName": translations["accelerator.filterfield.predefined"]
                        });
                        $scope.fields.push({
                            "id": "#News",
                            "title": translations["accelerator.filterfield.filternews"],
                            "systemDefined": false,
                            "groupName": translations["accelerator.filterfield.predefined"]
                        });
                    });
            });
    }
]);

Edit template

<div class="form__group__item__container">
    <div ng-if="!model.formField.editable">
        <div ng-if="!model.formField.languageSupport">
            <span ng-if="model.fields[model.formField.id]['*'] != undefined">{{ getOptionName(model.fields[model.formField.id]['*'], model.culture).join(', ') }}</span>
            <span ng-if="model.fields[model.formField.id]['*'] == undefined" translate>accelerator.filterfield.inherited</span>
        </div>
        <div ng-if="model.formField.languageSupport">
            <span ng-if="model.fields[model.formField.id][model.culture] != undefined">{{ getOptionName(model.fields[model.formField.id][model.culture], model.culture).join(', ') }}</span>
            <span ng-if="model.fields[model.formField.id][model.culture] == undefined" translate>accelerator.filterfield.inherited</span>
        </div>
    </div>

    <div ng-if="model.formField.editable">
        <div ng-class="{'row': model.formField.languageSupport, 'row__col__xs__6': model.formField.languageSupport}">
            <div ng-show="model.formField.languageSupport">
                <span><translate>general.edit</translate>: </span>
                <select name="{{model.formField.id}}_culture" ng-model="editLanguage" ng-change="setEditLanguage(editLanguage)" ng-options="culture.value for culture in languageList track by culture.id"></select>
            </div>

            <div>
                <div>
                    <input class="form__checkbox" type="checkbox" ng-click="inheritChanged(dummy.inherited)" ng-model="dummy.inherited" id="{{model.formField.id}}_inherited"/> <label for="{{model.formField.id}}_inherited" translate> accelerator.filterfield.inherited</label>
                </div>
                <div ng-if="!dummy.inherited" class="pim__filter">
                    <div class="filter" ng-cloak ng-mouseleave="toggleFilter(false)">
                        <div class="filter__button" ng-click="toggleFilter(!filter.open)">
                            <span translate>general.select</span>
                            <i class="fa filter__button__icon" ng-class="{'fa-chevron-down': !filter.open, 'fa-chevron-up': filter.open}"></i>
                        </div>
                        <div class="filter__dropdown" ng-show="filter.open">
                            <div class="filter__dropdown__search">
                                <input placeholder="Search" ng-model="filter.search"><i class="fa fa-search filter__button__icon"></i>
                            </div>
                            <ul class="filter__dropdown__list">
                                <li ng-repeat="item in model.formField.options.items | filter:filter.search" ng-class="{ 'highlight': $index == focusIndex }">
                                    <div ng-if="!model.formField.languageSupport">
                                        <input type="checkbox" name="{{model.formField.id}}" class="form__checkbox" id="option_{{model.formField.id + '_' + $index}}" ng-checked="model.fields[model.formField.id]['*'].indexOf(item) > -1" ng-click="toggleSelected(item)"/> <label for="option_{{model.formField.id + '_' + $index}}">{{ getOptionName(item, model.culture) }}</label>
                                    </div>
                                    <div ng-if="model.formField.languageSupport">
                                        <input type="checkbox" name="{{model.formField.id}}" class="form__checkbox" id="option_{{model.formField.id + '_' + $index}}" ng-checked="model.fields[model.formField.id][editLanguage.id].indexOf(item) > -1" ng-click="toggleSelected(item, editLanguage.id)"/> <label for="option_{{model.formField.id + '_' + $index}}">{{ getOptionName(item, editLanguage.id) }}</label>
                                    </div>
                                </li>
                            </ul>
                            <div class="filter__add__button__container">
                                <button ng-click="filter.open = false">OK</button>
                            </div>
                        </div>
                    </div>
                    <br ng-if="model.formField.languageSupport"/>
                    <div class="filter__added">
                        <ul>
                            <li class="base__cursor" ng-if="!model.formField.languageSupport" ng-repeat="item in model.fields[model.formField.id]['*']">
                                <span>{{ getOptionName(item, model.culture) }}</span>
                                <i ng-click="toggleSelected(item)" class="fa fa-close base__cursor__pointer"></i>
                            </li>
                            <li class="base__cursor" ng-if="model.formField.languageSupport" ng-repeat="item in model.fields[model.formField.id][editLanguage.id]">
                                <span>{{ getOptionName(item, editLanguage.id) }}</span>
                                <i ng-click="toggleSelected(item,[editLanguage.id])" class="fa fa-close base__cursor__pointer"></i>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>

        </div>
        <div class="row__col__xs__6" ng-if="model.formField.languageSupport">
            <span><translate>general.view</translate>: </span>
            <select ng-model="viewLanguage" ng-change="setViewLanguage(viewLanguage)" ng-options="culture.value for culture in languageList"></select>
            <br/>
            <span ng-if="model.fields[model.formField.id][viewLanguage.id] == undefined" translate>accelerator.filterfield.inherited</span>
            <span ng-if="model.fields[model.formField.id][viewLanguage.id] != undefined">{{ getOptionName(model.fields[model.formField.id][viewLanguage.id], viewLanguage.id).join(', ') }}</span>
        </div>
    </div>
</div>