Building Form Engine Control Project Plugins
Crafter Studio allows plugins for form engine controls through the getPluginFile
API found here getPluginFile
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. |
|
A simple textual input line. Details are in the Input Control page. |
|
A simple numeric input line. Details are in the Numeric Input Control page. |
|
A simple block of plain text. Details are in the Text Area Control page. |
|
A block of HTML. Details are in the Rich Text Editor Control page. |
|
Dropdown list of items to pick from. Details are in the Dropdown Control page. |
|
Date and Time field with a picker. Details are in the Date/Time Control page. |
|
Time field with a picker. Details are in the Time Control page. |
|
True/False checkbox. Details are in the Checkbox Control page. |
|
Several checkboxes (true/false). Details are in the Grouped Checkboxes Control page. |
|
Item selector from a Data Source. Details are in the Item Selector Control page. |
|
Image selector from a Data Source. Details are in the Image Control page. |
|
Video selector from a Data Source. Details are in the Video Control page. |
|
Transcoded Video selector from Video Transcoding Data Source. Details are in the Transcoded Video Control page. |
|
Displays text. Details are in the Label Control page. |
|
Allows changing the page order. Details are in the Page Order Control page. |
|
A simple text filename. Details are in the Filename Control page. |
|
Details are in the Auto Filename Control page. |
|
Details are in the Internal Name Control page. |
|
Details are in the Locale Selector Control page. |
The anatomy of a Control Project Plugin
Form Engine Control consist of (at a minimum)
A single JavaScript file which implements the control interface.
The JS file name and the control name in the configuration does not need to be the same. The JS file name can be any meaningful name, different from the control name in the configuration.
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 */
10CStudioForms.Controls.X = CStudioForms.Controls.X ||
11function(id, form, owner, properties, constraints, readonly) { }
12
13YAHOO.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});
Project Plugin Directory Structure
When creating plugins, the JS files location for the plugins uses a convention where the files needs to go in the following location:
Controls : authoring/static-assets/plugins/{yourPluginId}/control/{yourPluginName}/JS_FILE.js
where:
{yourPluginName} : Name of form engine control plugin
JS_FILE.js : JavaScript file containing the control interface implementation
Form Engine Control Project Plugin Example
Let’s take a look at an example of a control plugin. We will be adding a control named text-input
to the My Editorial
.
Form Engine Control Code
The first thing we have to do is to create the folder structure where we will be placing the JS file for our control. We’ll follow the convention listed above in Project Plugin Directory Structure
In a local folder, create the descriptor file for your plugin craftercms-plugin.yaml
with the plugin.id
set to org.craftercms.plugin.excontrol
, then create the folder authoring
. Under the authoring
folder, create the static-assets
folder. Under the static-assets
folder, create the folder plugins
.
We will now create the folders following the plugin id path name, org.craftercms.plugin.excontrol
. Under the plugins
folder, create the folder org
. Under the org
folder, create the folder craftercms
. Under the craftercms
folder, create the folder plugin
. Under the plugin
folder, create the folder excontrol
. Next, we’ll create the folder for the plugin type, control
. Under the excontrol
folder, create the folder control
. Under the control
folder, create the folder text-input
, which is the name of the control we’re building. We will be placing the JS file implementing the control interface under the text-input
folder. In the example below, the JS file is main.js
<plugin-folder>/ craftercms-plugin.yaml authoring/ static-assets/ plugins/ org/ craftercms/ plugin/ excontrol/ control/ text-input/ main.js
For our example, the <plugin-folder> is located here: /users/myuser/myplugins/form-control-plugin
In the JS file, please note that the CStudioAuthoring.Module
is required and that the prefix for CStudioAuthoring.Module.moduleLoaded
must be the name of the control. For our example, the prefix is text-input
as shown in the example.
1CStudioForms.Controls.textInput = CStudioForms.Controls.textInput ||
2function(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.patternErrEl = null;
10 this.countEl = null;
11 this.required = false;
12 this.value = "_not-set";
13 this.form = form;
14 this.id = id;
15 this.readonly = readonly;
16
17 return this;
18}
19
20YAHOO.extend(CStudioForms.Controls.textInput, CStudioForms.CStudioFormField, {
21
22 getLabel: function() {
23 return CMgs.format(langBundle, "Text Input");
24 },
25 .
26 .
27 .
28
29 getName: function() {
30 return "text-input";
31 },
32
33 getSupportedProperties: function() {
34 return [
35 { label: CMgs.format(langBundle, "displaySize"), name: "size", type: "int", defaultValue: "50" },
36 { label: CMgs.format(langBundle, "maxLength"), name: "maxlength", type: "int", defaultValue: "50" },
37 { label: CMgs.format(langBundle, "readonly"), name: "readonly", type: "boolean" },
38 { label: "Tokenize for Indexing", name: "tokenize", type: "boolean", defaultValue: "false" }
39 ];
40 },
41
42 getSupportedConstraints: function() {
43 return [
44 { label: CMgs.format(langBundle, "required"), name: "required", type: "boolean" },
45 { label: CMgs.format(langBundle, "matchPattern"), name: "pattern", type: "string" },
46 ];
47 }
48
49});
50
51CStudioAuthoring.Module.moduleLoaded("text-input", CStudioForms.Controls.textInput);
Here’s the complete example form control plugin file for the text-input
control (Click on the triangle on the left to expand/collapse):
Sample form control plugin file "main.js".
1CStudioForms.Controls.textInput = CStudioForms.Controls.textInput ||
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.patternErrEl = null;
10 this.countEl = null;
11 this.required = false;
12 this.value = "_not-set";
13 this.form = form;
14 this.id = id;
15 this.readonly = readonly;
16
17 return this;
18 }
19
20 YAHOO.extend(CStudioForms.Controls.textInput, CStudioForms.CStudioFormField, {
21
22 getLabel: function() {
23 return "Text Input";
24 },
25
26 _onChange: function(evt, obj) {
27 obj.value = obj.inputEl.value;
28
29 // Empty error state before new validation (for a clean state)
30 YAHOO.util.Dom.removeClass(obj.patternErrEl, 'on');
31 obj.clearError('pattern');
32
33 var validationExist = false;
34 var validationResult = true;
35 if (obj.required) {
36 if (obj.inputEl.value == '') {
37 obj.setError('required', 'Field is Required');
38 validationExist = true;
39 validationResult = false;
40 } else {
41 obj.clearError('required');
42 validationExist = true;
43 }
44 }
45
46 if ((!validationExist && obj.inputEl.value != '') || (validationExist && validationResult)) {
47 for (var i = 0; i < obj.constraints.length; i++) {
48 var constraint = obj.constraints[i];
49 if (constraint.name == 'pattern') {
50 var regex = constraint.value;
51 if (regex != '') {
52 if (obj.inputEl.value.match(regex)) {
53 // only when there is no other validation mark it as passed
54 obj.clearError('pattern');
55 YAHOO.util.Dom.removeClass(obj.patternErrEl, 'on');
56 validationExist = true;
57 } else {
58 if (obj.inputEl.value != '') {
59 YAHOO.util.Dom.addClass(obj.patternErrEl, 'on');
60 }
61 obj.setError('pattern', 'The value entered is not allowed in this field.');
62 validationExist = true;
63 validationResult = false;
64 }
65 }
66
67 break;
68 }
69 }
70 }
71 // actual validation is checked by # of errors
72 // renderValidation does not require the result being passed
73 obj.renderValidation(validationExist, validationResult);
74 obj.owner.notifyValidation();
75 const valueToSet = obj.escapeContent ? CStudioForms.Util.escapeXml(obj.getValue()) : obj.getValue();
76 obj.form.updateModel(obj.id, valueToSet);
77 },
78
79 _onChangeVal: function(evt, obj) {
80 obj.edited = true;
81 if (this._onChange) {
82 this._onChange(evt, obj);
83 }
84 },
85
86 /**
87 * perform count calculation on keypress
88 * @param evt event
89 * @param el element
90 */
91 count: function(evt, countEl, el) {
92 // 'this' is the input box
93 el = el ? el : this;
94 var text = el.value;
95
96 var charCount = text.length ? text.length : el.textLength ? el.textLength : 0;
97 var maxlength = el.maxlength && el.maxlength != '' ? el.maxlength : -1;
98
99 if (maxlength != -1) {
100 if (charCount > el.maxlength) {
101 // truncate if exceeds max chars
102 if (charCount > el.maxlength) {
103 this.value = text.substr(0, el.maxlength);
104 charCount = el.maxlength;
105 }
106
107 if (
108 evt &&
109 evt != null &&
110 evt.keyCode != 8 &&
111 evt.keyCode != 46 &&
112 evt.keyCode != 37 &&
113 evt.keyCode != 38 &&
114 evt.keyCode != 39 &&
115 evt.keyCode != 40 && // arrow keys
116 evt.keyCode != 88 &&
117 evt.keyCode != 86
118 ) {
119 // allow backspace and
120 // delete key and arrow keys (37-40)
121 // 86 -ctrl-v, 90-ctrl-z,
122 if (evt) YAHOO.util.Event.stopEvent(evt);
123 }
124 }
125 }
126
127 if (maxlength != -1) {
128 countEl.innerHTML = charCount + ' / ' + el.maxlength;
129 } else {
130 countEl.innerHTML = charCount;
131 }
132 },
133
134 render: function(config, containerEl) {
135 // we need to make the general layout of a control inherit from common
136 // you should be able to override it -- but most of the time it wil be the same
137 containerEl.id = this.id;
138
139 var titleEl = document.createElement('span');
140
141 YAHOO.util.Dom.addClass(titleEl, 'cstudio-form-field-title');
142 titleEl.textContent = config.title;
143
144 var controlWidgetContainerEl = document.createElement('div');
145 YAHOO.util.Dom.addClass(controlWidgetContainerEl, 'cstudio-form-control-input-container');
146
147 var validEl = document.createElement('span');
148 YAHOO.util.Dom.addClass(validEl, 'validation-hint');
149 YAHOO.util.Dom.addClass(validEl, 'cstudio-form-control-validation fa fa-check');
150
151 var inputEl = document.createElement('input');
152 this.inputEl = inputEl;
153 YAHOO.util.Dom.addClass(inputEl, 'datum');
154 YAHOO.util.Dom.addClass(inputEl, 'cstudio-form-control-input');
155
156 const valueToSet = this.escapeContent ? CStudioForms.Util.unEscapeXml(this.value) : this.value;
157 inputEl.value = this.value === '_not-set' ? config.defaultValue : valueToSet;
158 controlWidgetContainerEl.appendChild(inputEl);
159
160 YAHOO.util.Event.on(
161 inputEl,
162 'focus',
163 function(evt, context) {
164 context.form.setFocusedField(context);
165 },
166 this
167 );
168
169 YAHOO.util.Event.on(inputEl, 'change', this._onChangeVal, this);
170 YAHOO.util.Event.on(inputEl, 'blur', this._onChange, this);
171
172 for (var i = 0; i < config.properties.length; i++) {
173 var prop = config.properties[i];
174
175 if (prop.name == 'size') {
176 inputEl.size = prop.value;
177 }
178
179 if (prop.name == 'maxlength') {
180 inputEl.maxlength = prop.value;
181 }
182
183 if (prop.name == 'readonly' && prop.value == 'true') {
184 this.readonly = true;
185 }
186
187 if (prop.name === 'escapeContent' && prop.value === 'true') {
188 this.escapeContent = true;
189 }
190 }
191
192 if (this.readonly == true) {
193 inputEl.disabled = true;
194 }
195
196 var countEl = document.createElement('div');
197 YAHOO.util.Dom.addClass(countEl, 'char-count');
198 YAHOO.util.Dom.addClass(countEl, 'cstudio-form-control-input-count');
199 controlWidgetContainerEl.appendChild(countEl);
200 this.countEl = countEl;
201
202 var patternErrEl = document.createElement('div');
203 patternErrEl.innerHTML = 'The value entered is not allowed in this field.';
204 YAHOO.util.Dom.addClass(patternErrEl, 'cstudio-form-control-input-url-err');
205 controlWidgetContainerEl.appendChild(patternErrEl);
206 this.patternErrEl = patternErrEl;
207
208 YAHOO.util.Event.on(inputEl, 'keyup', this.count, countEl);
209 YAHOO.util.Event.on(inputEl, 'keypress', this.count, countEl);
210 YAHOO.util.Event.on(inputEl, 'mouseup', this.count, countEl);
211
212 this.renderHelp(config, controlWidgetContainerEl);
213
214 var descriptionEl = document.createElement('span');
215 YAHOO.util.Dom.addClass(descriptionEl, 'description');
216 YAHOO.util.Dom.addClass(descriptionEl, 'cstudio-form-field-description');
217 descriptionEl.textContent = config.description;
218
219 containerEl.appendChild(titleEl);
220 containerEl.appendChild(validEl);
221 containerEl.appendChild(controlWidgetContainerEl);
222 containerEl.appendChild(descriptionEl);
223 },
224
225 getValue: function() {
226 return this.value;
227 },
228
229 setValue: function(value) {
230 const valueToSet = this.escapeContent ? CStudioForms.Util.unEscapeXml(value) : value;
231
232 this.value = valueToSet;
233 this.inputEl.value = valueToSet;
234 this.count(null, this.countEl, this.inputEl);
235 this._onChange(null, this);
236 this.edited = false;
237 },
238
239 getName: function() {
240 return "text-input";
241 },
242
243 getSupportedProperties: function() {
244 return [
245 { label: CMgs.format(langBundle, "displaySize"), name: "size", type: "int", defaultValue: "50" },
246 { label: CMgs.format(langBundle, "maxLength"), name: "maxlength", type: "int", defaultValue: "50" },
247 { label: CMgs.format(langBundle, "readonly"), name: "readonly", type: "boolean" },
248 { label: "Tokenize for Indexing", name: "tokenize", type: "boolean", defaultValue: "false" }
249 ];
250 },
251
252 getSupportedConstraints: function() {
253 return [
254 { label: CMgs.format(langBundle, "required"), name: "required", type: "boolean" },
255 { label: CMgs.format(langBundle, "matchPattern"), name: "pattern", type: "string" },
256 ];
257 }
258
259});
260
261CStudioAuthoring.Module.moduleLoaded("text-input", CStudioForms.Controls.textInput);
Saving additional form control elements to XML
To save additional elements from your form control into the XML content, call registerDynamicField
from the form when initializing the form control. When updateField
is called, your element will be saved into the XML content.
this.form.registerDynamicField(this.timezoneId);
See here for an example of calling registerDynamicField
in the date-time form control code.
Configuring the Descriptor File to Wire the Plugin
To setup our form control to be automatically wired in the corresponding configuration file in Studio (which for a form control, is the Project Config Tools Configuration file) during the installation, add the following to your craftercms-plugin.yaml
descriptor file
1installation:
2 - type: form-control
3 elementXpath: //control/plugin[pluginId='org.craftercms.plugin.excontrol']
4 element:
5 name: control
6 children:
7 - name: plugin
8 children:
9 - name: pluginId
10 value: org.craftercms.plugin.excontrol
11 - name: type
12 value: control
13 - name: name
14 value: text-input
15 - name: filename
16 value: main.js
17 - name: icon
18 children:
19 - name: class
20 value: fa-pencil-square-o
See CrafterCMS Plugin Descriptor for more information on setting up automatic wiring of your plugin in Studio
Test the Plugin
After placing your JS file, the plugin may now be installed for testing/debugging using the crafter-cli
command copy-plugin
.
When running a crafter-cli
command, the connection to CrafterCMS needs to be setup via the add-environment command. Once the connection has been established, we can now install the plugin to the project my-editorial
by running the following:
./crafter-cli copy-plugin -e local -s my-editorial --path /users/myuser/myplugins/form-control-plugin
Let’s take a look at the auto-wiring performed during installation of the plugin. Form controls are setup in the site-config-tools.xml
file.
The items we setup in the descriptor file for auto-wiring above should now be in the Project Config Tools
configuration file, which can be accessed by opening the Sidebar
, then clicking on Project Tools
-> Configuration
-> Project Config Tools
Location (In Repository) SITENAME/config/studio/administration/site-config-tools.xml
1<controls>
2 <control>
3 <name>auto-filename</name>
4 .
5 .
6 </control>
7 .
8 .
9 <control>
10 <plugin>
11 <pluginId>org.craftercms.plugin.excontrol</pluginId>
12 <type>control</type>
13 <name>text-input</name>
14 <filename>main.js</filename>
15 </plugin>
16 <icon>
17 <class>fa-pencil-square-o</class>
18 </icon>
19 </control>
20</controls>
Here’s our plugin control added to the list of controls in content types