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.
Form Engine controls are #4 in the image above.
Out of the box controls are:
Control 
 | 
Description 
 | 
![]()  | 
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. 
 | 
![]()  | 
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. 
 | 
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
|
![]()  | 
Transcoded Video selector from Video Transcoding Data Source. 
Details are in the Transcoded Video 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¶
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>

















