• Document Up to Date
  • Updated On 4.0.1

Building Form Engine Data Source Project Plugins

Crafter Studio allows plugins for form engine data sources through the getPluginFile API found here getPluginFile

What is a Data Source

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. To accomplish this objective we use a data source pattern where by the form control widget code is concerned with rendering and facilitating the data capture/selection process but delegates the retrieval of the content to a separate swappable component interface known as a data source.

Content Type Editor

Form Engine data sources are #5 in the image above.

Out of the box data sources are:

Datasource
Description
Form Data Sources - Components
Details are in the Components Data Source page.
Form Data Sources - Shared Content
Details are in the Shared Content Data Source page.
Form Data Sources - Embedded Content
Details are in the Embedded Content Data Source page.
Form Data Sources - Image Uploaded From Desktop
Form Data Sources - Image From Repository
Details are in the Image from Repository Data Source page.
Form Data Sources - File Uploaded From Desktop
Form Data Sources - File Browse
Details are in the File Browse Data Source page.
Form Data Sources - WebDAV Repo
Form Data Sources - WebDAV Image Repo
Form Data Sources - WebDAV Video Repo
Form Data Sources - WebDAV Upload
Details are in the WebDAV File Upload Data Source page.
Form Data Sources - WebDAV Image Upload
Details are in the WebDAV Image Upload Data Source page.
Form Data Sources - WebDAV Video Upload
Details are in the WebDAV Video Upload Data Source page.
Form Data Sources - S3 Repo
Details are in the S3 Repository Data Source page.
Form Data Sources - S3 Image Repo
Details are in the Image from S3 Repository Data Source page.
Form Data Sources - S3 Video Repo
Details are in the Video from S3 Repository Data Source page.
Form Data Sources - S3 Upload
Details are in the S3 File Upload Data Source page.
Form Data Sources - S3 Image Upload
Details are in the S3 Image Upload Data Source page.
Form Data Sources - S3 Video Upload
Details are in the S3 Video Upload Data Source page.
Form Data Sources - Video Upload then Transcode from S3 Repo
Form Data Sources - Video Uploaded From Desktop
Form Data Sources - Video From Repository
Details are in the Video from Repository Data Source page.
Form Data Sources - Static Key Value Pairs
Details are in the Static Key Value Pairs Data Source page.
Form Data Sources - Simple Taxonomy
Details are in the Simple Taxonomy Data Source page.

The anatomy of a Data Source Project Plugin

Data Sources consist of (at a minimum)

  • A single JavaScript file which implements the data source interface.

    • The JS file name and the data source name in the configuration does not need to be the same. The JS file name can be any meaningful name, different from the data source name in the configuration.

  • Configuration in a Crafter Studio project to make that data source available for use.

Data Source Interface

Data Source Interface

 1/**
 2 * Constructor: Where .X is substituted with your class name
 3 */
 4CStudioForms.Datasources.ConfiguredList = CStudioForms.Datasources.X ||
 5function(id, form, properties, constraints)  {
 6}
 7
 8/**
 9 * Extension of the base class
10 */
11YAHOO.extend(CStudioForms.Datasources.X, CStudioForms.CStudioFormDatasource, {
12
13    /**
14     * Return a user friendly name for the data source (will show up in content type builder UX
15     */
16    getLabel: function() {  },
17
18    /**
19     * return a string that represents the type of data returned by the data source
20     * This is often of type "item"
21     */
22    getInterface: function() { },
23
24    /**
25     * return a string that represents the kind of data source (this is the same as the file name)
26     */
27    getName: function() { },
28
29    /**
30     * return a list of properties supported by the data source.
31     * properties is an array of objects with the following structure { label: "", name: "", type: "" }
32     */
33    getSupportedProperties: function() { },
34
35    /**
36     * method responsible for getting the actual values.  Caller must pass callback which meets interface:
37     * { success: function(list) {}, failure: function(exception) }
38     */
39    getList: function(cb) { }
40});

Project Plugin Directory Structure

When creating plugins, the JS files location for the plugins uses a convention where the data source files needs to go in the following location:

  • Data Sources : authoring/static-assets/plugins/{yourPluginId}/datasource/{yourPluginName}/JS_FILE.js

where:

  • yourPluginName : Name of form engine data source plugin

  • JS_FILE.js : JavaScript file containing the data source interface implementation

Form Engine Data Source Project Plugin Example

Let’s take a look at an example of a data source plugin. We will be adding a data source named parent-content.

Form Engine Data Source Code

The first thing we have to do is to create the folder structure where we will be placing the JS file for our data source. 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.examples, 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.examples. Under the plugins folder, create the folder org. Under the org folder, create the folder craftercms. Under the craftercms folder, create the folder plugin. Next, we’ll create the folder for the plugin type, datasource. Under the plugin folder, create the folder datasource. Under the datasource folder, create the folder parent-content, which is the name of the data source we’re building. We will be placing the JS file implementing the data source interface under the parent-content folder. In the example below, the JS file is main.js

Form Engine Data Source Plugin Directory Structure
<plugin-folder>/
  craftercms-plugin.yaml
  authoring/
    static-assets/
      plugins/
        org/
          craftercms/
            plugin/
              examples/
                datasource/
                  parent-content/
                    main.js

For our example, the <plugin-folder> is located here: /users/myuser/myplugins/form-datasource-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 data source. For our example, the prefix is parent-content as shown in the example.

authoring/static-assets/plugins/org/craftercms/plugin/examples/datasource/parent-content/main.js
 1CStudioForms.Datasources.ParentContent= CStudioForms.Datasources.ParentContent ||
 2function(id, form, properties, constraints)  {
 3    this.id = id;
 4    this.form = form;
 5    this.properties = properties;
 6    this.constraints = constraints;
 7    this.selectItemsCount = -1;
 8    this.type = "";
 9    this.defaultEnableCreateNew = true;
10    this.defaultEnableBrowseExisting = true;
11    this.countOptions = 0;
12
13    for(var i=0; i<properties.length; i++) {
14            if(properties[i].name == "repoPath") {
15                    this.repoPath = properties[i].value;
16            }
17            if(properties[i].name == "browsePath") {
18                    this.browsePath = properties[i].value;
19            }
20
21            if(properties[i].name == "type"){
22                    this.type = (Array.isArray(properties[i].value))?"":properties[i].value;
23            }
24
25        if(properties[i].name === "enableCreateNew"){
26            this.enableCreateNew = properties[i].value === "true" ? true : false;
27            this.defaultEnableCreateNew = false;
28            properties[i].value === "true" ? this.countOptions ++ : null;
29        }
30
31        if(properties[i].name === "enableBrowseExisting"){
32            this.enableBrowseExisting = properties[i].value === "true" ? true : false;
33            this.defaultEnableBrowseExisting = false;
34            properties[i].value === "true" ? this.countOptions ++ : null;
35        }
36    }
37
38    if(this.defaultEnableCreateNew){
39        this.countOptions ++;
40    }
41    if(this.defaultEnableBrowseExisting){
42        this.countOptions ++;
43    }
44
45    return this;
46}
47
48YAHOO.extend(CStudioForms.Datasources.ParentContent, CStudioForms.CStudioFormDatasource, {
49    .
50    .
51    .
52    getName: function() {
53            return "parent-content";
54    },
55
56    getSupportedProperties: function() {
57            return [
58            { label: CMgs.format(langBundle, "Enable Create New"), name: "enableCreateNew", type: "boolean", defaultValue: "true"  },
59            { label: CMgs.format(langBundle, "Enable Browse Existing"), name: "enableBrowseExisting", type: "boolean", defaultValue: "true" },
60                    { label: CMgs.format(langBundle, "repositoryPath"), name: "repoPath", type: "string" },
61                    { label: CMgs.format(langBundle, "browsePath"), name: "browsePath", type: "string" },
62                    { label: CMgs.format(langBundle, "defaultType"), name: "type", type: "string" }
63            ];
64    },
65
66    getSupportedConstraints: function() {
67            return [
68            ];
69    }
70
71});
72
73CStudioAuthoring.Module.moduleLoaded("parent-content", CStudioForms.Datasources.ParentContent);

Here’s the complete example form data source plugin file for the parent-content data source (Click on the triangle on the left to expand/collapse):

Sample form data source plugin file "main.js".
  1/*
  2 * Copyright (C) 2007-2021 Crafter Software Corporation. All Rights Reserved.
  3 *
  4 * This program is free software: you can redistribute it and/or modify
  5 * it under the terms of the GNU General Public License version 3 as published by
  6 * the Free Software Foundation.
  7 *
  8 * This program is distributed in the hope that it will be useful,
  9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 11 * GNU General Public License for more details.
 12 *
 13 * You should have received a copy of the GNU General Public License
 14 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 15 */
 16
 17CStudioForms.Datasources.ParentContent = function(id, form, properties, constraints) {
 18    this.id = id;
 19    this.form = form;
 20    this.properties = properties;
 21    this.constraints = constraints;
 22    this.selectItemsCount = -1;
 23    this.type = '';
 24    this.defaultEnableCreateNew = true;
 25    this.defaultEnableBrowseExisting = true;
 26    this.countOptions = 0;
 27    //const i18n = CrafterCMSNext.i18n;
 28    //this.formatMessage = i18n.intl.formatMessage;
 29    //this.parentContentDSMessages = i18n.messages.parentContentDSMessages;
 30  
 31    for (var i = 0; i < properties.length; i++) {
 32      if (properties[i].name == 'repoPath') {
 33        this.repoPath = properties[i].value;
 34      }
 35      if (properties[i].name == 'browsePath') {
 36        this.browsePath = properties[i].value;
 37      }
 38  
 39      if (properties[i].name == 'type') {
 40        this.type = Array.isArray(properties[i].value) ? '' : properties[i].value;
 41      }
 42  
 43      if (properties[i].name === 'enableCreateNew') {
 44        this.enableCreateNew = properties[i].value === 'true' ? true : false;
 45        this.defaultEnableCreateNew = false;
 46        properties[i].value === 'true' ? this.countOptions++ : null;
 47      }
 48  
 49      if (properties[i].name === 'enableBrowseExisting') {
 50        this.enableBrowseExisting = properties[i].value === 'true' ? true : false;
 51        this.defaultEnableBrowseExisting = false;
 52        properties[i].value === 'true' ? this.countOptions++ : null;
 53      }
 54    }
 55  
 56    if (this.defaultEnableCreateNew) {
 57      this.countOptions++;
 58    }
 59    if (this.defaultEnableBrowseExisting) {
 60      this.countOptions++;
 61    }
 62  
 63    return this;
 64  };
 65  
 66  YAHOO.extend(CStudioForms.Datasources.ParentContent, CStudioForms.CStudioFormDatasource, {
 67    itemsAreContentReferences: true,
 68  
 69    createElementAction: function(control, _self, addContainerEl) {
 70      if (this.countOptions > 1) {
 71        control.addContainerEl = null;
 72        control.containerEl.removeChild(addContainerEl);
 73      }
 74      if (_self.type === '') {
 75        CStudioAuthoring.Operations.createNewContent(
 76          CStudioAuthoringContext.site,
 77          _self.processPathsForMacros(_self.repoPath),
 78          false,
 79          {
 80            success: function(formName, name, value) {
 81              control.insertItem(value, formName.item.internalName, null, null, _self.id);
 82              control._renderItems();
 83            },
 84            failure: function() {}
 85          },
 86          true
 87        );
 88      } else {
 89        CStudioAuthoring.Operations.openContentWebForm(
 90          _self.type,
 91          null,
 92          null,
 93          _self.processPathsForMacros(_self.repoPath),
 94          false,
 95          false,
 96          {
 97            success: function(contentTO, editorId, name, value) {
 98              control.insertItem(name, value, null, null, _self.id);
 99              control._renderItems();
100              CStudioAuthoring.InContextEdit.unstackDialog(editorId);
101            },
102            failure: function() {}
103          },
104          [{ name: 'childForm', value: 'true' }]
105        );
106      }
107    },
108  
109    browseExistingElementAction: function(control, _self, addContainerEl) {
110      if (this.countOptions > 1) {
111        control.addContainerEl = null;
112        control.containerEl.removeChild(addContainerEl);
113      }
114      // if the browsePath property is set, use the property instead of the repoPath property
115      // otherwise continue to use the repoPath for both cases for backward compatibility
116      var browsePath = _self.repoPath;
117      if (_self.browsePath != undefined && _self.browsePath != '') {
118        browsePath = _self.browsePath;
119      }
120      CStudioAuthoring.Operations.openBrowse(
121        '',
122        _self.processPathsForMacros(browsePath),
123        _self.selectItemsCount,
124        'select',
125        true,
126        {
127          success: function(searchId, selectedTOs) {
128            for (var i = 0; i < selectedTOs.length; i++) {
129              var item = selectedTOs[i];
130              var value = item.internalName && item.internalName != '' ? item.internalName : item.uri;
131              control.insertItem(item.uri, value, null, null, _self.id);
132              control._renderItems();
133            }
134          },
135          failure: function() {}
136        }
137      );
138    },
139  
140    add: function(control, onlyAppend) {
141      var CMgs = CStudioAuthoring.Messages;
142      var langBundle = CMgs.getBundle('contentTypes', CStudioAuthoringContext.lang);
143  
144      var _self = this;
145  
146      var addContainerEl = control.addContainerEl ? control.addContainerEl : null;
147  
148      var datasourceDef = this.form.definition.datasources,
149        newElTitle = '';
150  
151      for (var x = 0; x < datasourceDef.length; x++) {
152        if (datasourceDef[x].id === this.id) {
153          newElTitle = datasourceDef[x].title;
154        }
155      }
156  
157      if (!addContainerEl && (this.countOptions > 1 || onlyAppend)) {
158        addContainerEl = document.createElement('div');
159        control.containerEl.appendChild(addContainerEl);
160        YAHOO.util.Dom.addClass(addContainerEl, 'cstudio-form-control-node-selector-add-container');
161        control.addContainerEl = addContainerEl;
162        control.addContainerEl.style.left = control.addButtonEl.offsetLeft + 'px';
163        control.addContainerEl.style.top = control.addButtonEl.offsetTop + 22 + 'px';
164      }
165  
166      if (this.enableCreateNew || this.defaultEnableCreateNew) {
167        if (this.countOptions > 1 || onlyAppend) {
168          addContainerEl.create = document.createElement('div');
169          addContainerEl.appendChild(addContainerEl.create);
170          YAHOO.util.Dom.addClass(addContainerEl.create, 'cstudio-form-controls-create-element');
171  
172          var createEl = document.createElement('div');
173          YAHOO.util.Dom.addClass(createEl, 'cstudio-form-control-node-selector-add-container-item');
174          createEl.textContent = CMgs.format(langBundle, 'createNew') + ' - ' + newElTitle;
175          control.addContainerEl.create.appendChild(createEl);
176          var addContainerEl = control.addContainerEl;
177          YAHOO.util.Event.on(
178            createEl,
179            'click',
180            function() {
181              _self.createElementAction(control, _self, addContainerEl);
182            },
183            createEl
184          );
185        } else {
186          _self.createElementAction(control, _self);
187        }
188      }
189  
190      if (this.enableBrowseExisting || this.defaultEnableBrowseExisting) {
191        if (this.countOptions > 1 || onlyAppend) {
192          addContainerEl.browse = document.createElement('div');
193          addContainerEl.appendChild(addContainerEl.browse);
194          YAHOO.util.Dom.addClass(addContainerEl.browse, 'cstudio-form-controls-browse-element');
195  
196          var browseEl = document.createElement('div');
197          browseEl.textContent = CMgs.format(langBundle, 'browseExisting') + ' - ' + newElTitle;
198          YAHOO.util.Dom.addClass(browseEl, 'cstudio-form-control-node-selector-add-container-item');
199          control.addContainerEl.browse.appendChild(browseEl);
200          var addContainerEl = control.addContainerEl;
201          YAHOO.util.Event.on(
202            browseEl,
203            'click',
204            function() {
205              _self.browseExistingElementAction(control, _self, addContainerEl);
206            },
207            browseEl
208          );
209        } else {
210          _self.browseExistingElementAction(control, _self);
211        }
212      }
213    },
214  
215    edit: function(key, control) {
216      var _self = this;
217      CStudioAuthoring.Service.lookupContentItem(CStudioAuthoringContext.site, key, {
218        success: function(contentTO) {
219          CStudioAuthoring.Operations.editContent(
220            contentTO.item.contentType,
221            CStudioAuthoringContext.siteId,
222            contentTO.item.mimeType,
223            contentTO.item.nodeRef,
224            contentTO.item.uri,
225            false,
226            {
227              success: function(contentTO, editorId, name, value) {
228                if (control) {
229                  control.updateEditedItem(value, _self.id);
230                  CStudioAuthoring.InContextEdit.unstackDialog(editorId);
231                }
232              }
233            }
234          );
235        },
236        failure: function() {}
237      });
238    },
239  
240    updateItem: function(item, control) {
241      if (item.key && item.key.match(/\.xml$/)) {
242        var getContentItemCb = {
243          success: function(contentTO) {
244            item.value = contentTO.item.internalName || item.value;
245            control._renderItems();
246          },
247          failure: function() {}
248        };
249  
250        CStudioAuthoring.Service.lookupContentItem(CStudioAuthoringContext.site, item.key, getContentItemCb);
251      }
252    },
253  
254    getLabel: function() {
255      //return this.formatMessage(this.parentContentDSMessages.parentContent);
256      return "Parent Content";
257    },
258  
259    getInterface: function() {
260      return 'item';
261    },
262  
263    getName: function() {
264      return 'parent-content';
265    },
266  
267    getSupportedProperties: function() {
268      return [
269        {
270          label: CMgs.format(langBundle, 'Enable Create New'),
271          name: 'enableCreateNew',
272          type: 'boolean',
273          defaultValue: 'true'
274        },
275        {
276          label: CMgs.format(langBundle, 'Enable Browse Existing'),
277          name: 'enableBrowseExisting',
278          type: 'boolean',
279          defaultValue: 'true'
280        },
281        { label: CMgs.format(langBundle, 'repositoryPath'), name: 'repoPath', type: 'string' },
282        { label: CMgs.format(langBundle, 'browsePath'), name: 'browsePath', type: 'string' },
283        { label: CMgs.format(langBundle, 'defaultType'), name: 'type', type: 'string' }
284      ];
285    },
286  
287    getSupportedConstraints: function() {
288      return [];
289    }
290  });
291  
292  CStudioAuthoring.Module.moduleLoaded('parent-content', CStudioForms.Datasources.ParentContent);
293  


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-datasource
 3   elementXpath: //datasource/plugin[pluginId='org.craftercms.plugin.examples']
 4   element:
 5     name: datasource
 6     children:
 7       - name: plugin
 8         children:
 9           - name: pluginId
10             value: org.craftercms.plugin.examples
11           - name: type
12             value: datasource
13           - name: name
14             value: parent-content
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-datasource-plugin

Let’s take a look at the auto-wiring performed during installation of the plugin. Form data sources 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<datasources>
 2    <datasource>
 3        <name>img-desktop-upload</name>
 4        .
 5        .
 6    </datasource>
 7    .
 8    .
 9    <datasource>
10        <plugin>
11            <pluginId>org.craftercms.plugin.examples</pluginId>
12            <type>datasource</type>
13            <name>parent-content</name>
14            <filename>main.js</filename>
15        </plugin>
16        <icon>
17            <class>fa-users</class>
18        </icon>
19    </datasource>
20</datasources>

Here’s our plugin data source added to the list of data sources in content types

Form Engine Data Source Project Plugin Added to Content Type