Episerver edit mode use Dojo framework as a client side technology. When creating new custom property, gadget or command button we should use this JavaScript library to prepare user interface. To have static typing and class-based object-oriented programming I tried to create Dojo widget in TypeScript. In this article I will describe how to develop Alloy demo example StringList property in TypeScript.
Setup TypeScript in project
First we need to add TypeScript Dojo definitions to our projects. TypeScript type definitions file allow us to use Javascript libraries that was not written in TypeScript. Dojo, Dijit and Dojox was developed in JavaScript. The definitions containes all members public signatures. Dojo.TypeScript.DefinitelyTyped definitions are available as Nuget Package dojo.TypeScript.DefinitelyTyped.
After adding Nuget we should see a lot of *.d.ts files inside /Script/typings/dojo folder.
Because Dojo use AMD to load module dependencies we need to change TypeScript Module System to AMD. This option is available in project properties->TypeScript Build->Module System.
Now we are ready to write new property in TypeScript.
Implementing StringList property
In Alloy EPiServer demo there is a custom StringList property. It’s used to store properties of string[] type. So there are few elements implemented:
- TypeScriptWidget.Models.Properties.PropertyStringList – backing type, used to store string[] value in database.
- TypeScriptWidget.Business.EditorDescriptors.StringList – editor descriptor, used to setup edit mode StringList editor,
- alloy.editors.StringList – client side widget – JavaScript UI component. Used by Forms Editing view to edit property.
No we will change Editor descriptor ClientEditingClass to new custom widget – StringListTs
1 2 3 4 5 6 7 8 9 |
public class StringList : EditorDescriptor { public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes) { ClientEditingClass = "alloy/editors/StringListTs"; base.ModifyMetadata(metadata, attributes); } } |
Then under /CliendResources/Scripts/Editors we will create new TypeScript file – StringListTs.ts.
Dependencies on EPiServer modules
EPiServer modules doesn’t have definition files that could be downloaded from Nuget. We have to write them manually. There is no need to create all EPiServer modules definitions (it could take few years), but only necessary elements used by StringList widget.
To add definition files we will create new directory ‘epi’ under /Scripts/typings/.
StringList use _ValueRequiredMixin mixin. We need to export module namespaces structure epi>shell->widget->_ValueRequiredMixin and then export “epi/shell/widget/_ValueRequiredMixin” path.
1 2 3 4 5 6 7 8 9 10 11 12 |
declare module epi { module shell { module widget { class _ValueRequiredMixin { } } } } declare module "epi/shell/widget/_ValueRequiredMixin" { var exp: epi.shell.widget._ValueRequiredMixin; export = exp; } |
Adding widget dependencies
When adding dependencies with AMD system we use define function.
1 2 3 4 5 6 7 8 9 10 11 12 |
define([ "dojo/_base/array", "dojo/_base/connect", "dojo/_base/declare", // ... ], function ( array, connect, declare, // ... ) |
Many times I made mistakes and switch order between dependency definitions and assignment variables. For example, I forgot to add ‘array’ variable as function parameter and Connect variable contained dojo._base.array… It was difficult to fix.
Using the Typescipt all dependencies will be registered separately – the paths and variables will be defined together, each in single line.
1 2 3 |
import _declare = require("dojo/_base/declare"); import connect = require("dojo/_base/connect"); import lang = require("dojo/_base/lang"); |
Strongly typed parametrs
We could also set types parameters and returning values of functions. For example for _stringToList method
1 2 3 |
_stringToList: function (value) { // ... } |
we will set string as type of value parameter and string[] as returning type for function.
1 2 3 |
_stringToList(value: string):string[] { // ... } |
Then all places that execute _stringToList function will benefit from strongly typed parameters and returned values.
Adding text resources
In original StringList widget, the templateString was set using inline assignment. For more complex widgets we will probably use dojo/text! plugin. It also requires preparing definition file – StringListDefinitions.d.ts. The module will just define path to StringListTemplate.html file.
1 2 3 4 |
declare module "dojo/text!./StringListTemplate.html" { var text: string; export = text; } |
Then we can use template as dependency in StringList widget.
1 |
import stringListTemplate = require("dojo/text!./StringListTemplate.html"); |
Debugging StringListTs
During compilation process of TypeScripts, the *.js and *.js.map files are created. We don’t need to debug generated JavaScript. The map file allows us to debug TypeScript files directly.
Below is the source code for StringList widget impleneted using TypeScript.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
import _declare = require("dojo/_base/declare"); import connect = require("dojo/_base/connect"); import lang = require("dojo/_base/lang"); import _CssStateMixin = require("dijit/_CssStateMixin"); import _Widget = require("dijit/_Widget"); import _TemplatedMixin = require("dijit/_TemplatedMixin"); import _WidgetsInTemplateMixin = require("dijit/_WidgetsInTemplateMixin"); import Textarea = require("dijit/form/Textarea"); import epi = require("epi/epi"); import _ValueRequiredMixin = require("epi/shell/widget/_ValueRequiredMixin"); // Template import stringListTemplate = require("dojo/text!./StringListTemplate.html"); class StringList { inherited: (args: Object) => void; connect: (object: any, eventName: string, action: any) => void; _set: (object: string, value: any) => void; validate: ()=>boolean; templateString: string = stringListTemplate; baseClass: string = "epiStringList"; helptext: string = "Place items on separate lines"; intermediateChanges: boolean = false; value: string[] = null; multiple: boolean = true; required: boolean; _started: boolean; textArea: dijit.form.Textarea; onChange(value) { } postCreate(): void { // call base implementation this.inherited(arguments); // Init textarea and bind event this.textArea.set("intermediateChanges", this.intermediateChanges); this.connect(this.textArea, "onChange", this._onTextAreaChanged); } isValid(): boolean { // summary: // Check if widget's value is valid. // tags: // protected, override return !this.required || lang.isArray(this.value) && this.value.length > 0 && this.value.join() !== ""; } // Setter for value property _setValueAttr(value: string[]): void { this._setValue(value, true); } _getValueAttr(): string[] { // summary: // Returns the textbox value as array. // tags: // protected, override var val = this.textArea && this.textArea.get("value"); return this._stringToList(val); } _setReadOnlyAttr(value: boolean): void { this._set("readOnly", value); this.textArea.set("readOnly", value); } // Setter for intermediateChanges _setIntermediateChangesAttr(value: boolean): void { this.textArea.set("intermediateChanges", value); this._set("intermediateChanges", value); } // Event handler for textarea _onTextAreaChanged(value: string): void { this._setValue(value, true); } _setValue(value, updateTextarea): void { // Assume value is an array var list = value; if (typeof value === "string") { // Split list list = this._stringToList(value); } else if (!value) { // use empty array for empty value list = []; } if (this._started && epi.areEqual(this.value, list)) { return; } // set value to this widget (and notify observers) this._set("value", list); // set value to textarea updateTextarea && this.textArea.set("value", list.join("\n")); if (this._started && this.validate()) { // Trigger change event this.onChange(list); } } // Convert a string to a list _stringToList(value: string):string[] { // Return empty array for if (!value || typeof value !== "string") { return []; } // Trim whitespace at start and end var trimmed = value.replace(/^\s+|\s+$/g, ""); // Trim whitespace around each linebreak var trimmedLines = trimmed.replace(/(\s*\n+\s*)/g, "\n"); // Split into list var list = trimmedLines.split("\n"); return list; } } var exp = _declare("alloy.editors.StringListTs", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin], new StringList()); export = exp; |