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>