• Document Up to Date

Building Form Engine Control Project Plugins

Crafter Studio allows plugins for form engine controls through the getPluginFile API found here https://app.swaggerhub.com/apis/craftercms/studio/4.0.2.0#/plugin/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.

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 - Numeric Input


A simple numeric input line.

Details are in the Numeric 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


A block of HTML.

Details are in the Rich Text Editor 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 - Time


Time field with a picker.

Details are in the 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


Displays text

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


Allows changing the page order

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


A simple text filename

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




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




Details are in the Internal Name Control page.
Form Controls - Locale Selector




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

Form Engine Control Plugin Directory Structure
<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.

authoring/js/control/text-input/main.js
 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

craftercms-plugin.yaml
 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

Form Engine Control Project Plugin Added to Content Type