It is usually sufficient to use the multi-field field type to create a composite field from other field types. However, in special scenarios, you may want to create your own field types from scratch. This section explains how it is done.
In Litium version 7.0 and earlier versions, custom field-type components are made with AngularJS in the Products area, and with Angular in the Websites, Customers and Media areas. From version 7.1, however, AngularJS components no longer have to be implemented. The same Angular component that is used in Websites, Customers and Media, will be used in Products too. So, if you are using Litium 7.1, or later versions, you can skip the steps below that deal with AngularJS. The first three steps are the same for both AngularJS and Angular.
In this tutorial, we will create a custom text field, which is rendered as a textbox in the UI. The field has CustomTextField as Id.
- Define field type metadata.
- Implement edit field type converter.
- Excel field type converter
Then proceed with the steps under Angular or AngularJS, depending on which area you are creating the custom field type for.
Angular
- Create the Edit component.
- Register the component in the module.
AngularJS
(For the Products area in Litium 7.0, and earlier versions)
- Implement edit panel controller and template.
- Modify Webpack.
Build
Build.
Routing
Scripts routing registration
Common steps
The field-type metadata defines the behavior of the field type. For example, if the field is an array. The 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 can be used for filtering and sorting. Let's create a file named CustomTextFieldTypeMetadata.cs under Litium.Accelerator.FieldTypes project.
using Litium.FieldFramework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Litium.Accelerator.FieldTypes
{
public class CustomTextFieldTypeMetadata : FieldTypeMetadataBase
{
public override string Id => "CustomTextField";
public override bool CanBeGridColumn => true;
public override bool CanBeGridFilter => false;
public override bool CanSort => false;
public override Type JsonType => typeof(string);
public override IFieldType CreateInstance(IFieldDefinition fieldDefinition)
{
var item = new CustomTextFieldType();
item.Init(fieldDefinition);
return item;
}
public class CustomTextFieldType : FieldTypeBase
{
public override object GetValue(ICollection<FieldData> fieldDatas) => fieldDatas.FirstOrDefault()?.TextValue;
public override ICollection<FieldData> PersistFieldData(object item) => PersistFieldDataInternal(item);
protected override ICollection<FieldData> PersistFieldDataInternal(object item) => new[] { new FieldData { TextValue = (string)item } };
}
}
}
Back to steps
To be able to use an instance of the field in back office, the edit field type converter needs to be implemented. The edit field type converter is responsible for providing back office with information about which controller and template to use for the edit or the settings view. The settings controller and template are used when setting up the field definition in the system settings, while the edit controller and template are used when an administrator edits the entity.
Litium.Accelerator.FieldTypes\CustomTextEditFieldTypeConverter.cs:
using Litium.FieldFramework;
using Litium.Runtime.DependencyInjection;
using Litium.Web.Administration.FieldFramework;
using Newtonsoft.Json.Linq;
namespace Litium.Accelerator.FieldTypes
{
[Service(Name = "CustomTextField")]
internal class CustomTextEditFieldTypeConverter : IEditFieldTypeConverter
{
private readonly IFieldTypeMetadata _fieldTypeMetadata;
public CustomTextEditFieldTypeConverter(FieldTypeMetadataService fieldTypeMetadataService)
{
_fieldTypeMetadata = fieldTypeMetadataService.Get("CustomTextField");
}
public object CreateOptionsModel() => null;
public virtual object ConvertFromEditValue(EditFieldTypeConverterArgs args, JToken item)
{
var fieldTypeInstance = _fieldTypeMetadata.CreateInstance(args.FieldDefinition);
return fieldTypeInstance.ConvertFromJsonValue(item.ToObject(_fieldTypeMetadata.JsonType));
}
public virtual JToken ConvertToEditValue(EditFieldTypeConverterArgs args, object item)
{
var fieldTypeInstance = _fieldTypeMetadata.CreateInstance(args.FieldDefinition);
var value = fieldTypeInstance.ConvertToJsonValue(item);
if (value == null)
{
return JValue.CreateNull();
}
return JToken.FromObject(value);
}
/// <summary>
/// The AngularJS controller name to edit the CustomText field.
/// </summary>
public string EditControllerName => "fieldEditorCustomText";
/// <summary>
/// The AngularJS template to edit the CustomText field.
/// </summary>
public string EditControllerTemplate => "~/Litium/Client/Scripts/dist/fieldEditorCustomText.html";
/// <summary>
/// The Angular component to edit the CustomText field.
/// </summary>
/// <remark>
/// The extension module should have the module name (Accelerator in this case)
/// as prefix, followed by the component name (FieldEditorCustomText), to be able to find the correct component on the client side.
/// </remark>
public string EditComponentName => "Accelerator#FieldEditorCustomText";
/**
* Since we don't have the setting UI for this CustomText field, we can skip the setting controllers and components.
* */
public string SettingsControllerName => null;
public string SettingsControllerTemplate => null;
public string SettingsComponentName => string.Empty;
}
}
EditComponentName, the last property in the code example above, defines which edit components will be used in the UI. As mentioned in the remarks, the value should have the module name as prefix, followed by the # character and the component name.
- Module prefix: In this case Accelerator, which is the name of the extension module. The extension module is the module developed in the solution that should be injected into the back office UI. You can see how the module is defined in the file extension.ts in Litium.Accelerator.FieldTypes\src\Accelerator. The module name must be the same in the path \src\Accelerator, in the file extension.ts and in the attributes EditorComponentName. If you want to change the module name to something else, make sure you change it in all places.
- Component name: In this case FieldEditorFilterFields, which was declared as component in the Angular project and registered in the file extension.ts for the module. Check the files in Litium.Accelerator.FieldTypes\src\Accelerator\Components. You have to declare the components in the Angular module to be able to use them. If you create a new component, make sure you add it in the declarations arrays of the module.
To extend and load extra script files and/or add extra modules to the application, Litium.Web.Administration.IAppExtension needs to be implemented.
Litium.Accelerator.FieldsTypes\CustomTextAdministrationExtension.cs:
using Litium.Web.Administration;
using System.Collections.Generic;
namespace Litium.Accelerator.FieldTypes
{
internal class CustomTextAdministrationExtension : IAppExtension
{
public IEnumerable<string> ScriptFiles { get; } = new[]
{
"~/Litium/Client/Scripts/dist/fieldEditorCustomText.js",
};
public IEnumerable<string> AngularModules { get; } = null;
public IEnumerable<string> StylesheetFiles { get; } = null;
}
}
New from Litium 7.1
From version 7.1, we no longer need to implement and configure the AngularJS files, since the Angular component will be used. Legacy implementation where AngularJS controller and template are used still work properly, without any modification needed. However, implementing them in Angular is recommended since the same component will be used across different modules.
According to the above example, CustomTextEditFieldTypeConverter can be updated to return null for the fields EditControllerName and EditControllerTemplate:
/// <summary>
/// Sets the AngularJS controller as null to use Angular component instead.
/// </summary>
public string EditControllerName => null;
/// <summary>
/// Sets the AngularJS template as null to use Angular component instead.
/// </summary>
public string EditControllerTemplate => null;
By doing this, the system will use the Angular component that we have configured in the EditComponentName field. CustomTextAdministrationExtension.cs can also be deleted, since we don't need to register the extra script.
Back to steps
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;
}
}
Back to steps
Angular
Since we configured the EditComponentName as Accelerator#FieldEditorCustomText in the CustomTextEditFieldTypeConverter, we need to create that component in Angular.
In Litium Accelerator, the Edit component is located in the folder Litium.Accelerator.FieldTypes\src\Accelerator\components\field-editor-custom-text. Notice the field-editor prefix. All components used for edit fields have this prefix to distinguish between them and normal components. Components for edit fields should have the option to change between preview and edit mode. It should also support multiple languages.
A component in Angular consists of two parts, the template and the code. Usually they are placed in two different files, one .html and one .ts.
Template file
Litium.Accelerator.FieldTypes\src\Accelerator\components\field-editor-custom-text\field-editor-custom-text.component.html:
You can read more about the template syntax in Angular here.
It is now much easier than before to create a field edit component, and by using the built-in component in Litium, it will be easier to maintain. This is what the code looks like:
<field-editor [field]="field">
<p preview>{{ getValue(viewLanguage)}}</p>
<input edit type="text" [id]="name" #control
[ngModel]="getValue(editLanguage)"
(ngModelChange)="valueChange($event, editLanguage)"
/>
</field-editor>
Field-editor is the component that should be used when you want to create a field edit component. By using field-editor, and setting its field property like in the code above, you don't have to manage the language change or the preview/edit mode.
Inside the <field-editor> tag, you need to define what should be displayed in preview and edit mode. Note the preview and edit attributes in the two <div> tags. Those are the slot names which are used to project custom content to the pre-defined component field-editor. <div preview> defines what will be displayed in preview mode, and <div edit> defines what will be displayed in edit mode.
Code file
Litium.Accelerator.FieldTypes\src\Accelerator\components\field-editor-custom-text\field-editor-custom-text.component.ts:
The Edit component is inherited from the built-in BaseFieldEditor, and it does not need to override anything since everything is managed by the base class. Note that common class components can be imported from the litium-ui package. The template is configured in the component annotation, by setting the templateUrl.
import { Component, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import { BaseFieldEditor } from 'litium-ui';
@Component({
selector: 'field-editor-custom-text',
templateUrl: './field-editor-custom-text.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FieldEditorCustomText extends BaseFieldEditor {
constructor(changeDetectorRef: ChangeDetectorRef) {
super(changeDetectorRef);
}
}
Back to steps
The next step is to register the components in the module, extensions.ts. Note that the "FieldEditorCustomText" component has been imported and registered in declarations in the Accelerator module below:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { FieldEditorFilterFields } from './components';
import { UiModule, ReducerRegistry } from 'litium-ui';
import { accelerator } from './redux/stores/accelerator.reducer';
import { FieldEditorCustomText } from './components/field-editor-custom-text/field-editor-custom-text.component';
@NgModule({
declarations: [
FieldEditorFilterFields,
FieldEditorCustomText,
],
imports: [
CommonModule,
UiModule,
TranslateModule,
]
})
export class Accelerator {
constructor(private _reducerRegistery: ReducerRegistry) {
this._reducerRegistery.register({ accelerator });
}
}
Back to steps
AngularJS (Obsolete since Litium 7.1)
The scope parameter model is added to the controller and contains information about the field edit view.
The model contains:
- formField: the current form field that also contains the options that are defined in the field definition.
- fields: the collection of fields in the entity.
- culture: the culture editor selected to use.
Litium.Accelerator.FieldTypes\src\Accelerator\components\field-editor-custom-text\fieldEditorCustomText.js:
app.controller('fieldEditorCustomText', [
'$scope', '$q', 'languagesService', '$filter',
($scope, $q, languagesService, $filter) => {
$scope.$watch('model.formField.editable', v => {
if (v === true) {
if ($scope.model.culture) {
const parentLanguage = $filter('filter')($scope.languageList, { id: $scope.model.culture }, true);
if (parentLanguage && parentLanguage.length > 0) {
$scope.editLanguage = parentLanguage[0];
const notParentLanguage = $filter('filter')($scope.languageList, { id: !$scope.model.culture }, true);
if (notParentLanguage && notParentLanguage.length > 0) {
$scope.viewLanguage = notParentLanguage[0];
}
}
}
}
});
const init = function() {
languagesService.getCultures()
.then(data => {
$scope.languageList = data.map(item => ({ id: item.id, value: item.title }));
if ($scope.model.culture) {
const parentLanguage = $filter('filter')($scope.languageList, { id: $scope.model.culture }, true);
if (parentLanguage && parentLanguage.length > 0) {
$scope.editLanguage = parentLanguage[0];
const 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];
}
}, angular.noop);
};
init();
}
]);
Litium.Accelerator.FieldTypes\src\Accelerator\components\field-editor-custom-text\fieldEditorCustomText.html:
<div class="form__group__item__container" ng-if="!model.formField.languageSupport">
<p ng-if="!model.formField.editable">{{model.fields[model.formField.id]['*']}}</p>
<p ng-if="model.formField.editable">
<input ng-required="model.formField.required" server-validate="{{model.formField.id}}" type="text" name="{{model.formField.id}}" ng-model="model.fields[model.formField.id]['*']" />
</p>
</div>
<div class="form__group__item__container" ng-if="model.formField.languageSupport">
<p ng-if="!model.formField.editable">{{ model.fields[model.formField.id][model.culture] }}</p>
<div ng-if="model.formField.editable" class="row">
<div class="row__col__xs__6">
<p>
<span><translate>general.edit</translate>: </span>
<select name="{{model.formField.id}}_culture" ng-model="editLanguage" ng-options="language.value for language in languageList track by language.id"></select>
</p>
<p>
<input type="text" ng-required="model.formField.required" name="{{model.formField.id}}"
ng-model="model.fields[model.formField.id][editLanguage.id]" server-validate="{{model.formField.id}}"
ng-if="model.formField.id === '_name'" suggest-url />
<input type="text" ng-required="model.formField.required" name="{{model.formField.id}}"
ng-model="model.fields[model.formField.id][editLanguage.id]" server-validate="{{model.formField.id}}"
ng-if="model.formField.id !== '_name'" />
</p>
</div>
<div class="row__col__xs__6">
<p>
<span><translate>general.view</translate>: </span>
<select ng-model="viewLanguage" ng-options="language.value for language in languageList track by language.id"></select>
</p>
<p>{{model.fields[model.formField.id][viewLanguage.id]}}</p>
</div>
</div>
</div>
In CustomTextEditFieldTypeConverter.cs, we have configured the AngularJS's controller and template as:
/// <summary>
/// The AngularJS controller name to edit the CustomText field.
/// </summary>
public string EditControllerName => "fieldEditorCustomText";
/// <summary>
/// The AngularJS template to edit the CustomText field.
/// </summary>
public string EditControllerTemplate => "~/Litium/Client/Scripts/dist/fieldEditorCustomText.html";
We need to modify webpack to copy these files to the correct location. Let's edit Litium.Accelerator.FieldTypes\config\webpack\webpack.js:
Import the CopyWebpackPlugin:
const CopyWebpackPlugin = require('copy-webpack-plugin');
Add it under plug-in array, below ForkTsCheckerWebpackPlugin for example:
new ForkTsCheckerWebpackPlugin({ tsconfig: helpers.root('tsconfig.json') }),
new CopyWebpackPlugin([
{ from: helpers.root('src/' + moduleName + '/components/field-editor-custom-text/fieldEditorCustomText.js'), to: helpers.root('dist') },
{ from: helpers.root('src/' + moduleName + '/components/field-editor-custom-text/fieldEditorCustomText.html'), to: helpers.root('dist') },
]),
Back to steps
If you have modified existing components, or created new ones, you have to build them.
In Litium Accelerator we are using a client library with name litium-ui which contains the base, common classes and components. This helps you build the component for the custom field type. As you can see in the code snippets above, you import and use classes from the litium-ui package.
The client library is automatic downloaded from https://packages.litium.com/Npm/litium-ui?{version number} where you need to ensure that the {version number} is correct for the version you are currently using in your solution. Example https://packages.litium.com/Npm/litium-ui?7.0.0 is used for the Litium version 7.0.0.
After verifying that litium-ui have the correct version number in package.json, open a command prompt and change the base path to src\Litium.Accelerator.FieldTypes, then execute the following two commands:
- yarn install
- yarn run build
Finally, build the Litium Accelerator solution. Note that we are using Yarn, but NPM works fine too.
Scripts routing registration
JavaScript resources are built to the src\Litium.Accelerator.FieldTypes\dist folder. Accelerator.js and Accelerator.js.map are configured to be embedded resources. This is needed in order to deploy and load scripts in a production environment, since we only need to deploy the assembly. The routing to these scripts is preconfigured in Litium Accelerator.
In Litium 7.1 and earlier versions, scripts routing registration is done in ExtensionsModuleRouterInitTask.cs. It loads JavaScript's file if it exists in src\Litium.Accelerator.FieldTypes\dist folder, and fall back to load from assembly embedded resources. The actual routing implementation is done in ExtensionsModuleRouterInitTask.Init. However, this implementation has some limitations.
From Litium 7.2, ExtensionsModuleRouterInitTask.cs has been removed and AngularModuleAttribute is used instead. Here is how script routings are configured in src\Litium.Accelerator.FieldTypes\Properties\AssemblyInfo.cs:
using Litium.Web.Administration;
[assembly: AngularModule("Accelerator")]
AngularModuleAttribute is used to register Accelerator.js and Accelerator.js.map in the routing, under the name Accelerator. Please note:
- The module name, Accelerator in this case, must be unique. If you register multiple modules with the same module name, an exception will be thrown at runtime.
- The system finds and loads resources from src\Litium.Accelerator.FieldTypes\dist folder, and fall back to assembly embedded resources if it couldn't find the file.
- By default, the dist folder is used to locate the file. AngularModuleAttribute accepts Folder property to customize this path.
Back to steps