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.
Form Engine data sources are #5 in the image above.
Out of the box data sources are:
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
<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.
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
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