Crafter Studio Plugin Examples

Crafter Studio supports several types of authoring plugins. UI widget plugins add React components to extension points in the Studio interface. Form Engine control plugins add custom input controls to content types. Form Engine data source plugins add custom data pickers that controls use to retrieve content.

This article walks through creating each type of plugin and wiring it into Studio.

Here’s a list of the plugin examples described in this article:

Plugin Type

Plugin type

Where it appears

Sidebar

sidebar

The panel on the left of Studio

Experience Builder Panel

experiencebuilder

The panel on the right of Studio, shown in Edit/Move mode

Toolbar

toolbar

The fixed bar at the top of Studio

Dashboard

dashboard

A dashlet on the project dashboard

Project Tools

project-tool

A tool under projectTools

Navigation Menu

navmenu

A tile in the Navigation Menu (Launcher)

Form Engine Control

control

A control in the Content Type Editor

Form Engine Data Source

datasource

A data source in the Content Type Editor

All the examples below use a project called My Editorial created using the Website Editorial blueprint.


Studio UI Widget Plugins

Creating Your Studio UI Widget Plugin

Here are the steps for creating your Studio UI widget plugin:

  1. Create the folder structure

  2. Create the plugin’s JavaScript file

  3. Auto-wire the plugin

  4. Install the plugin

  5. See it in action

The steps are identical for every UI widget location. The only two things that change are the plugin type (which is also the CATEGORY directory) and the installation block that positions the widget. Both of those are provided in the Positioning the Widget in the UI section for your target location.

  1. Create the folder structure.
    We’ll follow the convention listed in UI Plugin Directory Structure, {PLUGIN_DIRECTORY}/authoring/static-assets/plugins/{ID}/{CATEGORY}/{NAME}/, where CATEGORY is the plugin type for your target location and NAME is your plugin name.

    In a local folder, create the descriptor file craftercms-plugin.yaml and set plugin.id (for example, org.craftercms.plugin.exampleui), then create the directory structure. See the target location’s section below for the exact CATEGORY/NAME and a concrete tree.

  2. Create the plugin’s JavaScript file.
    Follow the instructions in the plugin example here, which will generate the index.js file.

    Inside the {NAME} folder, create two empty files, index.css and script.js, and place the index.js file in it.

  3. Auto-wire the plugin.
    To have the plugin automatically wired into the corresponding configuration file in Studio (for all these UI locations, that is the User Interface Configuration file) during installation, add an installation block to your craftercms-plugin.yaml descriptor file. The parentXpath and element differ by location — use the snippet from your target location’s section below.

    Note

    Remember to use the same value used in plugin.id (found at the top of the descriptor file) for the installation section plugin id.

    See here for more information on how auto-wiring works and what each field in the installation block means.

  4. Install the plugin.
    After placing your plugin files and setting up auto-wiring, install the plugin 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, install the plugin to the project my-editorial by running the following (adjust the --path to point at your plugin folder):

    ./crafter-cli copy-plugin -e local -s my-editorial --path /users/myuser/myplugins/<plugin-folder>
    
  5. See it in action.
    Reload Studio and open the relevant part of the UI. The exact place to look, along with the resulting auto-wired configuration, is shown in your target location’s section below.


Positioning the Widget in the UI

Each section below provides the location-specific pieces for the process above: the plugin type, the directory structure, the installation auto-wiring block, where to see the widget in action, and the resulting section in the User Interface Configuration file after installation.

Experience Builder Panel

The Experience Builder panel is the panel on the right of Studio that is enabled by clicking on Edit mode (pencil icon) or Move mode (two vertical ellipsis icon) on the top right of Studio, or by hitting the e or m key on your keyboard.

Experience Builder Panel

For this example, the plugin type (CATEGORY) is experiencebuilder, the NAME is test-experiencebuilder, and the plugin.id is org.craftercms.plugin.exampleexperiencebuilder.

Experience Builder Plugin Directory Structure
<plugin-folder>/
  craftercms-plugin.yaml
  authoring/
    static-assets/
      plugins/
        org/
          craftercms/
            plugin/
              exampleexperiencebuilder/
                experiencebuilder/
                  test-experiencebuilder/

Add the following installation block to your craftercms-plugin.yaml descriptor file:

craftercms-plugin.yaml
 1installation:
 2  - type: preview-app
 3    parentXpath: //widget[@id='craftercms.components.ICEToolsPanel']
 4    elementXpath: //plugin[@id='org.craftercms.sampleExperienceBuilderPlugin.components.reactComponent']
 5    element:
 6      name: configuration
 7      children:
 8      - name: widgets
 9        children:
10        - name: widget
11          attributes:
12          - name: id
13            value: org.craftercms.sampleExperienceBuilderPlugin.components.reactComponent
14          children:
15          - name: plugin
16            attributes:
17            - name: id
18              value: org.craftercms.plugin.exampleexperiencebuilder
19            - name: type
20              value: experiencebuilder
21            - name: name
22              value: test-experiencebuilder
23            - name: file
24              value: index.js

To see the plugin in action, click on the pencil icon at the top right of your browser to open the Experience Builder panel:

Experience Builder plugin in action

Here’s the auto-wired section in the configuration after installing the plugin:

 1<siteUi>
 2  ...
 3  <widget id="craftercms.components.ICEToolsPanel">
 4    <configuration>
 5      <widgets>
 6        <widget id="craftercms.components.ToolsPanelPageButton">
 7          <configuration>
 8            <target id="icePanel"/>
 9            <title id="previewSearchPanel.title" defaultMessage="Search"/>
10            <icon id="@mui/icons-material/SearchRounded"/>
11            <widgets>
12              <widget id="craftercms.components.PreviewSearchPanel"/>
13            </widgets>
14          </configuration>
15        </widget>
16        ...
17        <widget id="org.craftercms.sampleExperienceBuilderPlugin.components.reactComponent">
18          <plugin id="org.craftercms.plugin.exampleexperiencebuilder"
19                  type="experiencebuilder"
20                  name="test-experiencebuilder"
21                  file="index.js"/>
22        </widget>
23       </widgets>
24    </configuration>
25   </widget>
26   ...

Toolbar

The toolbar is a fixed element at the top of Studio. It provides contextual workflow and other options relative to the page you are looking at, content you have selected, or tool you are using.

Studio Toolbar

For this example, the plugin type (CATEGORY) is toolbar, the NAME is test-toolbar, and the plugin.id is org.craftercms.plugin.exampletoolbar.

Toolbar Plugin Directory Structure
<plugin-folder>/
  craftercms-plugin.yaml
  authoring/
    static-assets/
      plugins/
        org/
          craftercms/
            plugin/
              exampletoolbar/
                toolbar/
                  test-toolbar/

Add the following installation block to your craftercms-plugin.yaml descriptor file. Note that a toolbar widget is wired into one of the toolbar’s sections (leftSection, middleSection, or rightSection); this example uses rightSection:

craftercms-plugin.yaml
 1installation:
 2  - type: preview-app
 3    parentXpath: //widget[@id='craftercms.components.PreviewToolbar']
 4    elementXpath: //plugin[@id='org.craftercms.sampleToolbarPlugin.components.reactComponent']
 5    element:
 6      name: configuration
 7      children:
 8      - name: rightSection
 9        children:
10        - name: widgets
11          children:
12          - name: widget
13            attributes:
14            - name: id
15              value: org.craftercms.sampleToolbarPlugin.components.reactComponent
16            children:
17            - name: plugin
18              attributes:
19              - name: id
20                value: org.craftercms.plugin.exampletoolbar
21              - name: type
22                value: toolbar
23              - name: name
24                value: test-toolbar
25              - name: file
26                value: index.js

To see the plugin in action, refresh your browser:

Toolbar project plugin in action

Here’s the auto-wired section in the configuration after installing the plugin:

 1<siteUi>
 2...
 3  <widget id="craftercms.components.PreviewToolbar">
 4    <configuration>
 5      <leftSection>
 6        <widgets>
 7          <widget id="craftercms.components.SiteSwitcherSelect"/>
 8          <widget id="craftercms.components.QuickCreate"/>
 9        </widgets>
10      </leftSection>
11      <middleSection>
12        <widgets>
13          <widget id="craftercms.components.PreviewAddressBar"/>
14        </widgets>
15      </middleSection>
16      <rightSection>
17        <widgets>
18          <widget id="craftercms.components.EditModesSwitcher"/>
19          <widget id="craftercms.components.PublishingStatusButton">
20            <configuration>
21              <variant>icon</variant>
22            </configuration>
23          </widget>
24          <widget id="craftercms.components.WidgetDialogIconButton">
25            <configuration>
26              <title id="words.search" defaultMessage="Search"/>
27              <icon id="@mui/icons-material/SearchRounded"/>
28              <widget id="craftercms.components.EmbeddedSearchIframe"/>
29            </configuration>
30          </widget>
31          <widget id="org.craftercms.sampleToolbarPlugin.components.reactComponent">
32            <plugin id="org.craftercms.plugin.exampletoolbar"
33                    type="toolbar"
34                    name="test-toolbar"
35                    file="index.js"/>
36          </widget>
37        </widgets>
38      </rightSection>
39    </configuration>
40  </widget>
41  ...

Dashboard

The dashboard contains different dashlets that show, at a glance, all items currently in workflow, all items recently modified by the current user, etc. Dashlets shown vary depending on the user’s role. For more information on the Dashboard, see here.

Studio Dashboard

For this example, the plugin type (CATEGORY) is dashboard, the NAME is test-dashboard, and the plugin.id is org.craftercms.plugin.exampledashboard.

Dashboard Plugin Directory Structure
<plugin-folder>/
  craftercms-plugin.yaml
  authoring/
    static-assets/
      plugins/
        org/
          craftercms/
            plugin/
              exampledashboard/
                dashboard/
                  test-dashboard/

Add the following installation block to your craftercms-plugin.yaml descriptor file:

craftercms-plugin.yaml
 1installation:
 2  - type: preview-app
 3    parentXpath: /siteUi/widget[@id='craftercms.components.Dashboard']
 4    elementXpath: //plugin[@id='org.craftercms.sampleDashboardPlugin.components.reactComponent']
 5    element:
 6      name: configuration
 7      children:
 8      - name: widgets
 9        children:
10        - name: widget
11          attributes:
12          - name: id
13            value: org.craftercms.sampleDashboardPlugin.components.reactComponent
14          children:
15          - name: plugin
16            attributes:
17            - name: id
18              value: org.craftercms.plugin.exampledashboard
19            - name: type
20              value: dashboard
21            - name: name
22              value: test-dashboard
23            - name: file
24              value: index.js

To see the plugin in action, click on the CrafterCMS logo at the top left of your browser to open the sidebar, then click on Dashboard:

Dashboard plugin in action

You may also open the Dashboard anywhere via the Launcher, which is opened by clicking the apps icon on the top right:

Open Dashboard from the Launcher

Here’s the auto-wired section in the configuration after installing the plugin:

 1<siteUi>
 2  ...
 3  <widget id="craftercms.components.Dashboard">
 4    <configuration>
 5      <widgets>
 6        <widget id="craftercms.components.AwaitingApprovalDashlet">
 7          <permittedRoles>
 8            <role>admin</role>
 9            <role>developer</role>
10            <role>publisher</role>
11          </permittedRoles>
12        </widget>
13        ...
14        <widget id="org.craftercms.sampleDashboardPlugin.components.reactComponent">
15          <plugin id="org.craftercms.plugin.exampledashboard"
16                  type="dashboard"
17                  name="test-dashboard"
18                  file="index.js"/>
19        </widget>
20        ...

Project Tools

projectTools contains tools that project administrators use for daily activities. For more information on the available tools in projectTools, see Navigating Project Tools.

Studio Project Tools

For this example, the plugin type (CATEGORY) is project-tool, the NAME is test-project-tools, and the plugin.id is org.craftercms.plugin.exampleprojecttools.

Project Tools Plugin Directory Structure
<plugin-folder>/
  craftercms-plugin.yaml
  authoring/
    static-assets/
      plugins/
        org/
          craftercms/
            plugin/
              exampleprojecttools/
                project-tool/
                  test-project-tools/

Add the following installation block to your craftercms-plugin.yaml descriptor file:

craftercms-plugin.yaml
 1installation:
 2  - type: preview-app
 3    parentXpath: //reference[@id='craftercms.siteTools']
 4    elementXpath: //plugin[@id='org.craftercms.sampleProjectToolsPlugin.components.reactComponent']
 5    element:
 6      name: tools
 7      children:
 8      - name: tool
 9        children:
10        - name: title
11          attributes:
12          - name: id
13            value: "test.projecttool"
14          - name: defaultMessage
15            value: "Test Adding Project Tool"
16        - name: icon
17          attributes:
18          - name: id
19            value: "@mui/icons-material/WidgetsOutlined"
20        - name: url
21          value: test
22        - name: widget
23          attributes:
24          - name: id
25            value: org.craftercms.sampleProjectToolsPlugin.components.reactComponent
26          children:
27          - name: plugin
28            attributes:
29            - name: id
30              value: org.craftercms.plugin.exampleprojecttools
31            - name: type
32              value: project-tool
33            - name: name
34              value: test-project-tools
35            - name: file
36              value: index.js

To see the plugin in action, click on the CrafterCMS logo at the top left of your browser to open the sidebar, then click on Project Tools:

Project Tools project plugin in action

Here’s the auto-wired section in the configuration after installing the plugin:

 1<siteUi>
 2  ...
 3  <references>
 4    <reference id="craftercms.siteTools">
 5      <tools>
 6        ...
 7        <tool>
 8          <title id="PluginManagement.title" defaultMessage="Plugin Management"/>
 9          <icon id="@mui/icons-material/ExtensionOutlined"/>
10          <url>plugins</url>
11          <widget id="craftercms.components.PluginManagement"/>
12        </tool>
13        <tool>
14          <title id="test.sitetool" defaultMessage="Test Adding Project Tool"/>
15          <icon id="@mui/icons-material/WidgetsOutlined"/>
16          <url>test</url>
17          <widget id="org.craftercms.sampleProjectToolsPlugin.components.reactComponent">
18             <plugin id="org.craftercms.plugin.exampleprojecttools"
19                     type="project-tool"
20                     name="test-project-tools"
21                     file="index.js"/>
22          </widget>
23        </tool>
24      </tools>
25    ...

Form Engine Control and Data Source

Crafter Studio allows plugins for Form Engine controls and data sources through the getPluginFile API found here.

Content Type Editor

Form Engine controls (#4 in the image above) are UX elements that help authors capture and edit content and metadata properties. Form Engine data sources (#5) are swappable components that controls delegate to for retrieving the content authors select. Controls should be written independently of the data they capture so they can be reused across a wide range of data sets.

The process for creating and installing a Form Engine plugin is the same for controls and data sources:

  1. Create the folder structure and craftercms-plugin.yaml descriptor file

  2. Implement the JavaScript interface in main.js

  3. Add an installation block to auto-wire the plugin

  4. Install the plugin with crafter-cli copy-plugin

  5. Verify the plugin in the Content Type Editor

The only differences are the JavaScript interface, directory CATEGORY (control vs datasource), installation block, and the section of site-config-tools.xml where the plugin is registered. Both are covered below.

Form Engine Control Plugin

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.

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});

Control Plugin Directory and Example

When creating control plugins, the JS file goes in the following location:

authoring/static-assets/plugins/{yourPluginId}/control/{yourPluginName}/JS_FILE.js

where {yourPluginName} is the name of the form engine control plugin and JS_FILE.js is the JavaScript file containing the control interface implementation.

Let’s take a look at an example of a control plugin. We will be adding a control named text-input to My Editorial.

For this example, the plugin.id is org.craftercms.plugin.excontrol, the CATEGORY is control, the NAME is text-input, and 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/static-assets/plugins/org/craftercms/plugin/excontrol/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.

Auto-wiring and Installing a Control 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.

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 projectTools -> 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

Form Engine Data Source Plugin

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.

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

Details are in the Image Uploaded from Desktop Data Source page.

Form Data Sources - Image From Repository

Details are in the Image from Repository Data Source page.

Form Data Sources - File Uploaded From Desktop

Details are in the File Uploaded from Desktop Data Source page.

Form Data Sources - File Browse

Details are in the File Browse Data Source page.

Form Data Sources - WebDAV Repo

Details are in the File from WebDAV Repository Data Source page.

Form Data Sources - WebDAV Image Repo

Details are in the Image from WebDAV Repository Data Source page.

Form Data Sources - WebDAV Video Repo

Details are in the Video from WebDAV Repository Data Source page.

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

Details are in the Video Upload then Transcode from S3 using MediaConvert Data Source page.

Form Data Sources - Video Uploaded From Desktop

Details are in the Video Uploaded from Desktop Data Source page.

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.

Form Data Sources - Audio Uploaded from Desktop

Details are in the Audio Uploaded from Desktop Data Source page.

Form Data Sources - Audio from Repository

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

 1/**
 2 * Constructor: Where .X is substituted with your class name
 3 */
 4CStudioForms.Datasources.X = 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});

Data Source Plugin Directory and Example

When creating data source plugins, the JS file goes in the following location:

authoring/static-assets/plugins/{yourPluginId}/datasource/{yourPluginName}/JS_FILE.js

where {yourPluginName} is the name of the form engine data source plugin and JS_FILE.js is the JavaScript file containing the data source interface implementation.

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

For this example, the plugin.id is org.craftercms.plugin.examples, the CATEGORY is datasource, the NAME is parent-content, and 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  

Auto-wiring and Installing a Data Source Plugin

To setup our form data source to be automatically wired in the corresponding configuration file in Studio (which for a form data source, 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.

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 projectTools -> 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