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 data sources are:

Control
Description
Form Section



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

Details are in the Form Section Control page.
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.
Input


A simple textual input line.

Details are in the Input Control page.
Text Area


A simple block of plain text.

Details are in the Text Area Control page.
Rich Text Editor


A block of HTML.

Details are in the Rich Text Editor Control page.
Dropdown


Dropdown list of items to pick from.

Details are in the Dropdown Control page.
Date / Time


Date and Time field with a picker.

Details are in the Date/Time Control page.
Checkbox


True/False checkbox.

Details are in the Checkbox Control page.
Grouped
Checkboxes

Several checkboes (true/false).

Details are in the Grouped Checkboxes Control page.
Item Selector




Details are in the Item Selector Control page.
Image


Image selector from a Data Source.

Details are in the Image Control page.
Video


Video selector from a Data Source.

Details are in the Video Control page.
Label




Details are in the Label Control page.
Page Order




Details are in the Page Order Control page.
File Name




Details are in the Filename Control page.
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 data source available for use

Control Interface

 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
    /**
     * Constructor: Where .X is substituted with your class name
     * ID is the variable name
     * FORM is the form object
     * OWNER is the parent section/form
     * PROPERTIES is the collection of configured property values
     * CONSTRAINTS is the collection of configured constraint values
     * READONLY is a true/false flag indicating re-only mode
     */
    CStudioForms.Controls.X = CStudioForms.Controls.X ||
    function(id, form, owner, properties, constraints, readonly)  { }

    YAHOO.extend(CStudioForms.Controls.X, CStudioForms.CStudioFormField, {

        /**
         * Return a user friendly name for the control (will show up in content type builder UX)
         */
        getLabel: function() { },

        /**
         * method is called by the engine when the value of the control is changed
         */
        _onChange: function(evt, obj) { },

        /**
         * method is called by the engine to invoke the control to render.  The control is responsible for creating and managing its own HTML.
         * CONFIG is a structure containing the form definition and other control configuration
         * CONTAINER EL is the containing element the control is to render in to.
         */
        render: function(config, containerEl) { },

        /**
         * returns the current value of the control
         */
        getValue: function() { },

        /**
         * sets the value of the control
         */
        setValue: function(value) { },

        /**
         * return a string that represents the kind of control (this is the same as the file name)
         */
        getName: function() {  },

        /**
         * return a list of properties supported by the control.
         * properties is an array of objects with the following structure { label: "", name: "", type: "" }
         */
        getSupportedProperties: function() { },

        /**
         * return a list of constraints supported by the control.
         * constraints is an array of objects with the following structure { label: "", name: "", type: "" }
         */
        getSupportedConstraints: function() { }
    });

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
  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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
    CStudioForms.Controls.CheckBoxGroup = CStudioForms.Controls.CheckBoxGroup ||
    function(id, form, owner, properties, constraints, readonly)  {
        this.owner = owner;
        this.owner.registerField(this);
        this.errors = [];
        this.properties = properties;
        this.constraints = constraints;
        this.inputEl = null;
        this.countEl = null;
        this.required = false;
        this.value = "_not-set";
        this.form = form;
        this.id = id;
        this.readonly = readonly;
        this.minSize = 0;
        this.hiddenEl = null;
        // Stores the type of data the control is now working with (this value is fetched from the datasource controller)
        this.dataType = null;

        amplify.subscribe("/datasource/loaded", this, this.onDatasourceLoaded);

        return this;
    }

    YAHOO.extend(CStudioForms.Controls.CheckBoxGroup, CStudioForms.CStudioFormField, {

        /**
        * Return a user friendly name for the control (will show up in content type builder UX)
        */
        getLabel: function() {
            return CMgs.format(langBundle, "groupedCheckboxes");
        },

        getRequirementCount: function() {
            var count = 0;

            if(this.minSize > 0){
                count++;
            }

            return count;
        },

        /**
        * validates the supported constraints of the control
        */
        validate : function () {
            if(this.minSize > 0) {
                if(this.value.length < this.minSize) {
                    this.setError("minCount", "# items are required");
                    this.renderValidation(true, false);
                }
                else {
                    this.clearError("minCount");
                    this.renderValidation(true, true);
                }
            }
            else {
                this.renderValidation(false, true);
            }
            this.owner.notifyValidation();
        },

        /**
        * sets "edited" property as true. This property will be verified when the engine form is canceled
        */
        _onChangeVal: function(evt, obj) {
            obj.edited = true;
        },

        /**
        * method is called when datasource is loaded
        */
        onDatasourceLoaded: function ( data ) {
            if (this.datasourceName === data.name && !this.datasource) {
                var datasource = this.form.datasourceMap[this.datasourceName];
                this.datasource = datasource;
                this.dataType = datasource.getDataType();
                if (!this.dataType.match(/^value$/)) {
                    this.dataType += "mv";
                }
                datasource.getList(this.callback);
            }
        },

        /**
         * method is called by the engine to invoke the control to render.  The control is responsible for creating and managing its own HTML.
         * CONFIG is a structure containing the form definition and other control configuration
         * CONTAINER EL is the containing element the control is to render in to.
         */
        render: function(config, containerEl, isValueSet) {
            containerEl.id = this.id;
            this.containerEl = containerEl;
            this.config = config;

            var _self = this,
                datasource = null;

            for(var i=0;i<config.constraints.length;i++){
                var constraint = config.constraints[i];

                if(constraint.name == "minSize" && constraint.value != ""){
                    this.minSize = parseInt(constraint.value);
                }
            }

            for(var i=0; i<config.properties.length; i++) {
                var prop = config.properties[i];

                if(prop.name == "datasource") {
                    if(prop.value && prop.value != "") {
                        this.datasourceName = (Array.isArray(prop.value)) ? prop.value[0] : prop.value;
                        this.datasourceName = this.datasourceName.replace("[\"","").replace("\"]","");
                    }
                }

                if(prop.name == "selectAll" && prop.value == "true"){
                    this.selectAll = true;
                }

                if(prop.name == "readonly" && prop.value == "true"){
                    this.readonly = true;
                }
            }

            if(this.value === "_not-set" || this.value === "") {
                this.value = [];
            }

            var cb = {
                success: function(list) {
                    var keyValueList = list,

                    // setValue will provide an array with the values that were checked last time the form was saved (datasource A).
                    // If someone decides to tie this control to a different datasource (datasource B): none, some or all of values
                    // from datasource A may be present in datasource B. If there were values checked in datasource A and they are
                    // also found in datasource B, then they will remain checked. However, if there were values checked in
                    // datasource A that are no longer found in datasource B, these need to be removed from the control's value.
                        newValue = [],
                        rowEl, textEl, inputEl;

                    containerEl.innerHTML = "";
                    var titleEl = document.createElement("span");

                    YAHOO.util.Dom.addClass(titleEl, 'cstudio-form-field-title');
                    titleEl.innerHTML = config.title;

                    var controlWidgetContainerEl = document.createElement("div");
                    YAHOO.util.Dom.addClass(controlWidgetContainerEl, 'cstudio-form-control-input-container');

                    var validEl = document.createElement("span");
                    YAHOO.util.Dom.addClass(validEl, 'validation-hint');
                    YAHOO.util.Dom.addClass(validEl, 'cstudio-form-control-validation');
                    controlWidgetContainerEl.appendChild(validEl);

                    var hiddenEl = document.createElement("input");
                    hiddenEl.type = "hidden";
                    YAHOO.util.Dom.addClass(hiddenEl, 'datum');
                    controlWidgetContainerEl.appendChild(hiddenEl);
                    _self.hiddenEl = hiddenEl;

                    var groupEl = document.createElement("div");
                    groupEl.className = "checkbox-group";

                    if (_self.selectAll && !_self.readonly) {
                        rowEl = document.createElement("label");
                        rowEl.className = "checkbox select-all";
                        rowEl.setAttribute("for", _self.id + "-all");

                        textEl = document.createElement("span");
                        textEl.innerHTML = "Select All";

                        inputEl = document.createElement("input");
                        inputEl.type = "checkbox";
                        inputEl.checked = false;
                        inputEl.id = _self.id + "-all";

                        YAHOO.util.Event.on(inputEl, 'focus', function(evt, context) { context.form.setFocusedField(context) }, _self);
                        YAHOO.util.Event.on(inputEl, 'change', _self.toggleAll, inputEl, _self);

                        rowEl.appendChild(inputEl);
                        rowEl.appendChild(textEl);
                        groupEl.appendChild(rowEl);
                    }

                    controlWidgetContainerEl.appendChild(groupEl);

                    for(var j=0; j<keyValueList.length; j++) {
                        var item = keyValueList[j];

                        rowEl = document.createElement("label");
                        rowEl.className = "checkbox";
                        rowEl.setAttribute("for", _self.id + "-" + item.key);

                        textEl = document.createElement("span");
                        // TODO:
                        // we might need to create something on the datasource
                        // to get the value based on the list of possible value holding properties
                        // using datasource.getSupportedProperties
                        textEl.innerHTML = item.value || item.value_f || item.value_smv || item.value_imv
                            || item.value_fmv || item.value_dtmv || item.value_htmlmv;

                        inputEl = document.createElement("input");
                        inputEl.type = "checkbox";

                        if (_self.isSelected(item.key)) {
                            newValue.push(_self.updateDataType(item));
                            inputEl.checked = true;
                        } else {
                            inputEl.checked = false;
                        }

                        inputEl.id = _self.id + "-" + item.key;

                        if(_self.readonly == true){
                            inputEl.disabled = true;
                        }

                        YAHOO.util.Event.on(inputEl, 'focus', function(evt, context) { context.form.setFocusedField(context) }, _self);
                        YAHOO.util.Event.on(inputEl, 'change', _self.onChange, inputEl, _self);
                        inputEl.context = _self;
                        inputEl.item = item;

                        rowEl.appendChild(inputEl);
                        rowEl.appendChild(textEl);
                        groupEl.appendChild(rowEl);
                    }
                    _self.value = newValue;
                    _self.form.updateModel(_self.id, _self.getValue());

                    var helpContainerEl = document.createElement("div");
                    YAHOO.util.Dom.addClass(helpContainerEl, 'cstudio-form-field-help-container');
                    controlWidgetContainerEl.appendChild(helpContainerEl);

                    _self.renderHelp(config, helpContainerEl);

                    var descriptionEl = document.createElement("span");
                    YAHOO.util.Dom.addClass(descriptionEl, 'description');
                    YAHOO.util.Dom.addClass(descriptionEl, 'cstudio-form-field-description');
                    descriptionEl.innerHTML = config.description;

                    containerEl.appendChild(titleEl);
                    containerEl.appendChild(controlWidgetContainerEl);
                    containerEl.appendChild(descriptionEl);

                    // Check if the value loaded is valid or not
                    _self.validate();
                }
            }

            if(isValueSet) {

                var datasource = this.form.datasourceMap[this.datasourceName];
                // This render method is currently being called twice (on initialization and on the setValue).
                // We need the value to know which checkboxes should be checked or not so restrict the rendering to only
                // after the value has been set.
                if(datasource){
                    this.datasource = datasource;
                    this.dataType = datasource.getDataType() || "value";    // Set default value for dataType (for backwards compatibility)
                    if (!this.dataType.match(/^value$/)) {
                        this.dataType += "mv";
                    }
                    datasource.getList(cb);
                }else{
                    this.callback = cb;
                }
            }
        },

        /**
         * selects/unselects all checkboxes inside the control
         */
        toggleAll: function (evt, el) {
            var ancestor = YAHOO.util.Dom.getAncestorByClassName(el, "checkbox-group"),
                checkboxes = YAHOO.util.Selector.query('.checkbox input[type="checkbox"]', ancestor),
                _self = this;

            this.value = [];
            this.value.length = 0;
            if (el.checked) {
                // select all
                checkboxes.forEach( function (el) {
                    var valObj = {}

                    el.checked = true;
                    if (el.item) {
                        // the select/deselect toggle button doesn't have an item attribute
                        valObj.key = el.item.key;
                        valObj[_self.dataType] = el.item.value || el.item[_self.dataType];
                        _self.value.push(valObj);
                    }
                });
            } else {
                // unselect all
                checkboxes.forEach( function (el) {
                    el.checked = false;
                });
            }
            this.form.updateModel(this.id, this.getValue());
            this.hiddenEl.value = this.valueToString();
            this.validate();
            this._onChangeVal(evt, this);
        },

        /**
         * method is called by the engine when the value of the control is changed
         */
        onChange: function(evt, el) {
            var checked = (el.checked);

            if(checked) {
                this.selectItem(el.item.key, el.item.value || el.item[this.dataType]);
            }
            else {
                this.unselectItem(el.item.key);
            }
            this.form.updateModel(this.id, this.getValue());
            this.hiddenEl.value = this.valueToString();
            this.validate();
            this._onChangeVal(evt, this);
        },

        /**
         * validates if the checkbox is selected
         */
        isSelected: function(key) {
            var selected = false;
            var values = this.getValue();

            for(var i=0; i<values.length; i++) {
                if(values[i].key == key) {
                    selected = true;
                    break;
                }
            }
            return selected;
        },

        getIndex: function(key) {
            var index = -1;
            var values = this.getValue();

            for(var i=0; i<values.length; i++) {
                if(values[i].key == key) {
                    index = i;
                    break;
                }
            }

            return index;
        },

        /**
         * adds the selected item into the value of the control
         */
        selectItem: function(key, value) {
            var valObj = {};

            if(!this.isSelected(key)) {
                valObj.key = key;
                valObj[this.dataType] = value;

                this.value[this.value.length] = valObj;
            }
        },

        /**
         * removes the unselect item from the value of the control
         */
        unselectItem: function(key) {
            var index = this.getIndex(key);

            if(index != -1) {
                this.value.splice(index, 1);
            }
        },

        /**
         * returns the current value of the control
         */
        getValue: function() {
            return this.value;
        },

        updateDataType: function (valObj) {
            if (this.dataType) {
                for (var prop in valObj) {
                    if (prop.match(/value/)) {
                        if (prop !== this.dataType) {
                            // Rename the property (e.g. "value") to the current data type ("value_s")
                            valObj[this.dataType] = valObj[prop];
                            delete valObj[prop];
                        }
                    }
                }
                return valObj;
            } else {
                throw new TypeError("Function updateDataType (checkbox-group.js) : module variable dataType is undefined");
            }
        },

        /**
         * sets the value of the control
         */
        setValue: function(value) {
            if(value === "") {
                value = [];
            }

            this.value = value;
            this.form.updateModel(this.id, this.getValue());
            this.render(this.config, this.containerEl, true);
            this.hiddenEl.value = this.valueToString();
        },

        /**
         * sets the value of the control to string
         */
        valueToString: function() {
            var strValue = "[";
            var values = this.getValue();
            var item = null;
            if(values === '')
                values = [];

            for(var i = 0; i < values.length; i++){
                item = values[i];
                strValue += '{ "key": "' + item.key + '", "' + this.dataType + '":"' + item[this.dataType] + '"}';
                if( i != values.length -1){
                    strValue += ",";
                }
            }

            strValue += "]";
            return strValue;
        },

        /**
         * return a string that represents the kind of control (this is the same as the file name)
         */
        getName: function() {
            return "checkbox-group";
        },

        /**
         * return a list of properties supported by the control.
         * properties is an array of objects with the following structure { label: "", name: "", type: "" }
         */
        getSupportedProperties: function() {
            return [
                { label: CMgs.format(langBundle, "datasource"), name: "datasource", type: "datasource:item" },
                { label: CMgs.format(langBundle, "showSelectAll"), name: "selectAll", type: "boolean" },
                { label: CMgs.format(langBundle, "readonly"), name: "readonly", type: "boolean" }
            ];
        },

        /**
         * return a list of constraints supported by the control.
         * constraints is an array of objects with the following structure { label: "", name: "", type: "" }
         */
        getSupportedConstraints: function() {
            return [
                { label:CMgs.format(langBundle, "minimumSelection"), name:"minSize", type: "int"}
            ];
        }

    });

    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) /company-home/cstudio/config/sites/SITENAME/administration/tools.xml

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