• Document Up to Date

Build a Form Engine Control

What is a Control

A form control is a UX element to help authors capture and edit content and metadata properties. Crafter Studio form controls should be written in a way that makes them independent of the data they allow the user to select so that they can be (re)used across a wide range of data sets.

Content Type Editor

Form Engine controls are #4 in the image above.

Out of the box controls are:

Control
Description
Form Controls - Form Section



Create a new section in the form, this is to help the content
authors by segmenting a form into sections of similar concern.

Details are in the Form Section Control page.
Form Controls - Repeating Group




Repeating groups are used when the form has one or several controls
that repeat to capture the same data as records. For example: a
list of images in a carousel, or a list of widgets on a page.

Details are in the Repeating Group Control page.
Form Controls - Input


A simple textual input line.

Details are in the Input Control page.
Form Controls - Text Area


A simple block of plain text.

Details are in the Text Area Control page.
Form Controls - Rich Text Editor (TinyMCE 2)


A block of HTML.

Details are in the Rich Text Editor (TinyMCE 2) Control page.
Form Controls - Rich Text Editor (TinyMCE 5)


A block of HTML.

Details are in the Rich Text Editor (TinyMCE 5) Control page.
Form Controls - Dropdown


Dropdown list of items to pick from.

Details are in the Dropdown Control page.
Form Controls - Date Time


Date and Time field with a picker.

Details are in the Date/Time Control page.
Form Controls - Check Box


True/False checkbox.

Details are in the Checkbox Control page.
Form Controls - Grouped Check Box


Several checkboxes (true/false).

Details are in the Grouped Checkboxes Control page.
Form Controls - Item Selector


Item selector from a Data Source

Details are in the Item Selector Control page.
Form Controls - Image


Image selector from a Data Source.

Details are in the Image Control page.
Form Controls - Video


Video selector from a Data Source.

Details are in the Video Control page.
Form Controls - Transcoded Video


Transcoded Video selector from Video Transcoding Data Source.

Details are in the Transcoded Video Control page.
Form Controls - Label




Details are in the Label Control page.
Form Controls - Page Order




Details are in the Page Order Control page.
Form Controls - File Name




Details are in the Filename Control page.
Form Controls - Auto Filename




Details are in the Auto Filename Control page.

The anatomy of a Control Plugin

Form Engine Control consist of (at a minimum)

  • A single javascript file which implements the control interface.

    • The file name of the control is important as the system uses a convention whereby the JS file name and the control name in the configuration must be the same.

    • The module name must also be the same as the control name with “cstudio-forms-controls-” prepended to the front of it Ex: “cstudio-forms-controls-checkbox-group.”

  • Configuration in a Crafter Studio project to make that control available for use

Control Interface

 1    /**
 2     * Constructor: Where .X is substituted with your class name
 3     * ID is the variable name
 4     * FORM is the form object
 5     * OWNER is the parent section/form
 6     * PROPERTIES is the collection of configured property values
 7     * CONSTRAINTS is the collection of configured constraint values
 8     * READONLY is a true/false flag indicating re-only mode
 9     */
10    CStudioForms.Controls.X = CStudioForms.Controls.X ||
11    function(id, form, owner, properties, constraints, readonly)  { }
12
13    YAHOO.extend(CStudioForms.Controls.X, CStudioForms.CStudioFormField, {
14
15        /**
16         * Return a user friendly name for the control (will show up in content type builder UX)
17         */
18        getLabel: function() { },
19
20        /**
21         * method is called by the engine when the value of the control is changed
22         */
23        _onChange: function(evt, obj) { },
24
25        /**
26         * method is called by the engine to invoke the control to render.  The control is responsible for creating and managing its own HTML.
27         * CONFIG is a structure containing the form definition and other control configuration
28         * CONTAINER EL is the containing element the control is to render in to.
29         */
30        render: function(config, containerEl) { },
31
32        /**
33         * returns the current value of the control
34         */
35        getValue: function() { },
36
37        /**
38         * sets the value of the control
39         */
40        setValue: function(value) { },
41
42        /**
43         * return a string that represents the kind of control (this is the same as the file name)
44         */
45        getName: function() {  },
46
47        /**
48         * return a list of properties supported by the control.
49         * properties is an array of objects with the following structure { label: "", name: "", type: "" }
50         */
51        getSupportedProperties: function() { },
52
53        /**
54         * return a list of constraints supported by the control.
55         * constraints is an array of objects with the following structure { label: "", name: "", type: "" }
56         */
57        getSupportedConstraints: function() { }
58    });

Coding an example

Our example is a grouped checkbox that allows the author to select one or more items from a set of checkboxes. The control relies on a data source for the set of possible values which allows it to be used for a wide range of data capture.

Control Code

Form Engine Control Example

Location /STUDIO-WAR/default-site/static-assets/components/cstudio-forms/controls/checkbox-group.js

  1    CStudioForms.Controls.CheckBoxGroup = CStudioForms.Controls.CheckBoxGroup ||
  2    function(id, form, owner, properties, constraints, readonly)  {
  3        this.owner = owner;
  4        this.owner.registerField(this);
  5        this.errors = [];
  6        this.properties = properties;
  7        this.constraints = constraints;
  8        this.inputEl = null;
  9        this.countEl = null;
 10        this.required = false;
 11        this.value = "_not-set";
 12        this.form = form;
 13        this.id = id;
 14        this.readonly = readonly;
 15        this.minSize = 0;
 16        this.hiddenEl = null;
 17        // Stores the type of data the control is now working with (this value is fetched from the datasource controller)
 18        this.dataType = null;
 19
 20        amplify.subscribe("/datasource/loaded", this, this.onDatasourceLoaded);
 21
 22        return this;
 23    }
 24
 25    YAHOO.extend(CStudioForms.Controls.CheckBoxGroup, CStudioForms.CStudioFormField, {
 26
 27        /**
 28        * Return a user friendly name for the control (will show up in content type builder UX)
 29        */
 30        getLabel: function() {
 31            return CMgs.format(langBundle, "groupedCheckboxes");
 32        },
 33
 34        getRequirementCount: function() {
 35            var count = 0;
 36
 37            if(this.minSize > 0){
 38                count++;
 39            }
 40
 41            return count;
 42        },
 43
 44        /**
 45        * validates the supported constraints of the control
 46        */
 47        validate : function () {
 48            if(this.minSize > 0) {
 49                if(this.value.length < this.minSize) {
 50                    this.setError("minCount", "# items are required");
 51                    this.renderValidation(true, false);
 52                }
 53                else {
 54                    this.clearError("minCount");
 55                    this.renderValidation(true, true);
 56                }
 57            }
 58            else {
 59                this.renderValidation(false, true);
 60            }
 61            this.owner.notifyValidation();
 62        },
 63
 64        /**
 65        * sets "edited" property as true. This property will be verified when the engine form is canceled
 66        */
 67        _onChangeVal: function(evt, obj) {
 68            obj.edited = true;
 69        },
 70
 71        /**
 72        * method is called when datasource is loaded
 73        */
 74        onDatasourceLoaded: function ( data ) {
 75            if (this.datasourceName === data.name && !this.datasource) {
 76                var datasource = this.form.datasourceMap[this.datasourceName];
 77                this.datasource = datasource;
 78                this.dataType = datasource.getDataType();
 79                if (!this.dataType.match(/^value$/)) {
 80                    this.dataType += "mv";
 81                }
 82                datasource.getList(this.callback);
 83            }
 84        },
 85
 86        /**
 87         * method is called by the engine to invoke the control to render.  The control is responsible for creating and managing its own HTML.
 88         * CONFIG is a structure containing the form definition and other control configuration
 89         * CONTAINER EL is the containing element the control is to render in to.
 90         */
 91        render: function(config, containerEl, isValueSet) {
 92            containerEl.id = this.id;
 93            this.containerEl = containerEl;
 94            this.config = config;
 95
 96            var _self = this,
 97                datasource = null;
 98
 99            for(var i=0;i<config.constraints.length;i++){
100                var constraint = config.constraints[i];
101
102                if(constraint.name == "minSize" && constraint.value != ""){
103                    this.minSize = parseInt(constraint.value);
104                }
105            }
106
107            for(var i=0; i<config.properties.length; i++) {
108                var prop = config.properties[i];
109
110                if(prop.name == "datasource") {
111                    if(prop.value && prop.value != "") {
112                        this.datasourceName = (Array.isArray(prop.value)) ? prop.value[0] : prop.value;
113                        this.datasourceName = this.datasourceName.replace("[\"","").replace("\"]","");
114                    }
115                }
116
117                if(prop.name == "selectAll" && prop.value == "true"){
118                    this.selectAll = true;
119                }
120
121                if(prop.name == "readonly" && prop.value == "true"){
122                    this.readonly = true;
123                }
124            }
125
126            if(this.value === "_not-set" || this.value === "") {
127                this.value = [];
128            }
129
130            var cb = {
131                success: function(list) {
132                    var keyValueList = list,
133
134                    // setValue will provide an array with the values that were checked last time the form was saved (datasource A).
135                    // If someone decides to tie this control to a different datasource (datasource B): none, some or all of values
136                    // from datasource A may be present in datasource B. If there were values checked in datasource A and they are
137                    // also found in datasource B, then they will remain checked. However, if there were values checked in
138                    // datasource A that are no longer found in datasource B, these need to be removed from the control's value.
139                        newValue = [],
140                        rowEl, textEl, inputEl;
141
142                    containerEl.innerHTML = "";
143                    var titleEl = document.createElement("span");
144
145                    YAHOO.util.Dom.addClass(titleEl, 'cstudio-form-field-title');
146                    titleEl.innerHTML = config.title;
147
148                    var controlWidgetContainerEl = document.createElement("div");
149                    YAHOO.util.Dom.addClass(controlWidgetContainerEl, 'cstudio-form-control-input-container');
150
151                    var validEl = document.createElement("span");
152                    YAHOO.util.Dom.addClass(validEl, 'validation-hint');
153                    YAHOO.util.Dom.addClass(validEl, 'cstudio-form-control-validation');
154                    controlWidgetContainerEl.appendChild(validEl);
155
156                    var hiddenEl = document.createElement("input");
157                    hiddenEl.type = "hidden";
158                    YAHOO.util.Dom.addClass(hiddenEl, 'datum');
159                    controlWidgetContainerEl.appendChild(hiddenEl);
160                    _self.hiddenEl = hiddenEl;
161
162                    var groupEl = document.createElement("div");
163                    groupEl.className = "checkbox-group";
164
165                    if (_self.selectAll && !_self.readonly) {
166                        rowEl = document.createElement("label");
167                        rowEl.className = "checkbox select-all";
168                        rowEl.setAttribute("for", _self.id + "-all");
169
170                        textEl = document.createElement("span");
171                        textEl.innerHTML = "Select All";
172
173                        inputEl = document.createElement("input");
174                        inputEl.type = "checkbox";
175                        inputEl.checked = false;
176                        inputEl.id = _self.id + "-all";
177
178                        YAHOO.util.Event.on(inputEl, 'focus', function(evt, context) { context.form.setFocusedField(context) }, _self);
179                        YAHOO.util.Event.on(inputEl, 'change', _self.toggleAll, inputEl, _self);
180
181                        rowEl.appendChild(inputEl);
182                        rowEl.appendChild(textEl);
183                        groupEl.appendChild(rowEl);
184                    }
185
186                    controlWidgetContainerEl.appendChild(groupEl);
187
188                    for(var j=0; j<keyValueList.length; j++) {
189                        var item = keyValueList[j];
190
191                        rowEl = document.createElement("label");
192                        rowEl.className = "checkbox";
193                        rowEl.setAttribute("for", _self.id + "-" + item.key);
194
195                        textEl = document.createElement("span");
196                        // TODO:
197                        // we might need to create something on the datasource
198                        // to get the value based on the list of possible value holding properties
199                        // using datasource.getSupportedProperties
200                        textEl.innerHTML = item.value || item.value_f || item.value_smv || item.value_imv
201                            || item.value_fmv || item.value_dtmv || item.value_htmlmv;
202
203                        inputEl = document.createElement("input");
204                        inputEl.type = "checkbox";
205
206                        if (_self.isSelected(item.key)) {
207                            newValue.push(_self.updateDataType(item));
208                            inputEl.checked = true;
209                        } else {
210                            inputEl.checked = false;
211                        }
212
213                        inputEl.id = _self.id + "-" + item.key;
214
215                        if(_self.readonly == true){
216                            inputEl.disabled = true;
217                        }
218
219                        YAHOO.util.Event.on(inputEl, 'focus', function(evt, context) { context.form.setFocusedField(context) }, _self);
220                        YAHOO.util.Event.on(inputEl, 'change', _self.onChange, inputEl, _self);
221                        inputEl.context = _self;
222                        inputEl.item = item;
223
224                        rowEl.appendChild(inputEl);
225                        rowEl.appendChild(textEl);
226                        groupEl.appendChild(rowEl);
227                    }
228                    _self.value = newValue;
229                    _self.form.updateModel(_self.id, _self.getValue());
230
231                    var helpContainerEl = document.createElement("div");
232                    YAHOO.util.Dom.addClass(helpContainerEl, 'cstudio-form-field-help-container');
233                    controlWidgetContainerEl.appendChild(helpContainerEl);
234
235                    _self.renderHelp(config, helpContainerEl);
236
237                    var descriptionEl = document.createElement("span");
238                    YAHOO.util.Dom.addClass(descriptionEl, 'description');
239                    YAHOO.util.Dom.addClass(descriptionEl, 'cstudio-form-field-description');
240                    descriptionEl.innerHTML = config.description;
241
242                    containerEl.appendChild(titleEl);
243                    containerEl.appendChild(controlWidgetContainerEl);
244                    containerEl.appendChild(descriptionEl);
245
246                    // Check if the value loaded is valid or not
247                    _self.validate();
248                }
249            }
250
251            if(isValueSet) {
252
253                var datasource = this.form.datasourceMap[this.datasourceName];
254                // This render method is currently being called twice (on initialization and on the setValue).
255                // We need the value to know which checkboxes should be checked or not so restrict the rendering to only
256                // after the value has been set.
257                if(datasource){
258                    this.datasource = datasource;
259                    this.dataType = datasource.getDataType() || "value";    // Set default value for dataType (for backwards compatibility)
260                    if (!this.dataType.match(/^value$/)) {
261                        this.dataType += "mv";
262                    }
263                    datasource.getList(cb);
264                }else{
265                    this.callback = cb;
266                }
267            }
268        },
269
270        /**
271         * selects/unselects all checkboxes inside the control
272         */
273        toggleAll: function (evt, el) {
274            var ancestor = YAHOO.util.Dom.getAncestorByClassName(el, "checkbox-group"),
275                checkboxes = YAHOO.util.Selector.query('.checkbox input[type="checkbox"]', ancestor),
276                _self = this;
277
278            this.value = [];
279            this.value.length = 0;
280            if (el.checked) {
281                // select all
282                checkboxes.forEach( function (el) {
283                    var valObj = {}
284
285                    el.checked = true;
286                    if (el.item) {
287                        // the select/deselect toggle button doesn't have an item attribute
288                        valObj.key = el.item.key;
289                        valObj[_self.dataType] = el.item.value || el.item[_self.dataType];
290                        _self.value.push(valObj);
291                    }
292                });
293            } else {
294                // unselect all
295                checkboxes.forEach( function (el) {
296                    el.checked = false;
297                });
298            }
299            this.form.updateModel(this.id, this.getValue());
300            this.hiddenEl.value = this.valueToString();
301            this.validate();
302            this._onChangeVal(evt, this);
303        },
304
305        /**
306         * method is called by the engine when the value of the control is changed
307         */
308        onChange: function(evt, el) {
309            var checked = (el.checked);
310
311            if(checked) {
312                this.selectItem(el.item.key, el.item.value || el.item[this.dataType]);
313            }
314            else {
315                this.unselectItem(el.item.key);
316            }
317            this.form.updateModel(this.id, this.getValue());
318            this.hiddenEl.value = this.valueToString();
319            this.validate();
320            this._onChangeVal(evt, this);
321        },
322
323        /**
324         * validates if the checkbox is selected
325         */
326        isSelected: function(key) {
327            var selected = false;
328            var values = this.getValue();
329
330            for(var i=0; i<values.length; i++) {
331                if(values[i].key == key) {
332                    selected = true;
333                    break;
334                }
335            }
336            return selected;
337        },
338
339        getIndex: function(key) {
340            var index = -1;
341            var values = this.getValue();
342
343            for(var i=0; i<values.length; i++) {
344                if(values[i].key == key) {
345                    index = i;
346                    break;
347                }
348            }
349
350            return index;
351        },
352
353        /**
354         * adds the selected item into the value of the control
355         */
356        selectItem: function(key, value) {
357            var valObj = {};
358
359            if(!this.isSelected(key)) {
360                valObj.key = key;
361                valObj[this.dataType] = value;
362
363                this.value[this.value.length] = valObj;
364            }
365        },
366
367        /**
368         * removes the unselect item from the value of the control
369         */
370        unselectItem: function(key) {
371            var index = this.getIndex(key);
372
373            if(index != -1) {
374                this.value.splice(index, 1);
375            }
376        },
377
378        /**
379         * returns the current value of the control
380         */
381        getValue: function() {
382            return this.value;
383        },
384
385        updateDataType: function (valObj) {
386            if (this.dataType) {
387                for (var prop in valObj) {
388                    if (prop.match(/value/)) {
389                        if (prop !== this.dataType) {
390                            // Rename the property (e.g. "value") to the current data type ("value_s")
391                            valObj[this.dataType] = valObj[prop];
392                            delete valObj[prop];
393                        }
394                    }
395                }
396                return valObj;
397            } else {
398                throw new TypeError("Function updateDataType (checkbox-group.js) : module variable dataType is undefined");
399            }
400        },
401
402        /**
403         * sets the value of the control
404         */
405        setValue: function(value) {
406            if(value === "") {
407                value = [];
408            }
409
410            this.value = value;
411            this.form.updateModel(this.id, this.getValue());
412            this.render(this.config, this.containerEl, true);
413            this.hiddenEl.value = this.valueToString();
414        },
415
416        /**
417         * sets the value of the control to string
418         */
419        valueToString: function() {
420            var strValue = "[";
421            var values = this.getValue();
422            var item = null;
423            if(values === '')
424                values = [];
425
426            for(var i = 0; i < values.length; i++){
427                item = values[i];
428                strValue += '{ "key": "' + item.key + '", "' + this.dataType + '":"' + item[this.dataType] + '"}';
429                if( i != values.length -1){
430                    strValue += ",";
431                }
432            }
433
434            strValue += "]";
435            return strValue;
436        },
437
438        /**
439         * return a string that represents the kind of control (this is the same as the file name)
440         */
441        getName: function() {
442            return "checkbox-group";
443        },
444
445        /**
446         * return a list of properties supported by the control.
447         * properties is an array of objects with the following structure { label: "", name: "", type: "" }
448         */
449        getSupportedProperties: function() {
450            return [
451                { label: CMgs.format(langBundle, "datasource"), name: "datasource", type: "datasource:item" },
452                { label: CMgs.format(langBundle, "showSelectAll"), name: "selectAll", type: "boolean" },
453                { label: CMgs.format(langBundle, "readonly"), name: "readonly", type: "boolean" }
454            ];
455        },
456
457        /**
458         * return a list of constraints supported by the control.
459         * constraints is an array of objects with the following structure { label: "", name: "", type: "" }
460         */
461        getSupportedConstraints: function() {
462            return [
463                { label:CMgs.format(langBundle, "minimumSelection"), name:"minSize", type: "int"}
464            ];
465        }
466
467    });
468
469    CStudioAuthoring.Module.moduleLoaded("cstudio-forms-controls-checkbox-group", CStudioForms.Controls.CheckBoxGroup);

Configuring the Control to show up in Crafter Studio

Add the control’s name to the list of controls in the content type editor configuration

Location (In Repository) SITENAME/config/studio/administration/site-config-tools.xml

 1    <config>
 2            <tools>
 3                    <tool>
 4                            <name>content-types</name>
 5                            <label>Content Types</label>
 6                            <controls>
 7                                    <control>checkbox-group</control>
 8                            </controls>
 9                            <datasources>
10                                    ...
11                                    <datasource>video-desktop-upload</datasource>
12                                    <datasource>configured-list</datasource>
13                            </datasources>
14                            ...
15                    </tool>
16                    <!--tool>...</tool -->
17            </tools>
18    </config>