Skip to main content
Home  › ... Razor

Fields Tutorials

Tutorial HomeFields

2sxc Custom Input Fields (11.02+)

2sxc 11 makes it very easy to create custom input fields using standard WebComponents. This example shows the most basic case - just a dummy message (so not a real input field).

You can use it to learn about things like:

  1. Naming conventions for where to put the files
  2. Naming conventions for the tagName
  3. Using customElements.define(...) to register your element
  4. How web components use constructor() and making sure you have the super() call there
  5. Using connectedCallback() and disconnectedCallback() to init/destroy your component

So just have a look and discover how simple everything can be 🚀.

Edit for Anonymous Enabled for Tutorial

Important: We opened permissions that you can experience the edit dialog - so you can save, but it will just create draft data 😉.

This example shows a dummy-field which doesn't allow editing, but will just show a message.

⬇️ Result | Source ➡️

Hit this edit button to have a look:

    @inherits Custom.Hybrid.RazorTyped
    @using System.Collections;
    
    <p>
      Hit this edit button to have a look:
      @Kit.Toolbar.Empty().New("UiEmptyHelloWorld").AsTag()
    </p>

    Source Code of index.js

    /*
      This examples shows a plain JS WebComponent that will just show some messages
      This is just to demonstrate how such a component is built.
    */
    
    // always use an IFFE to ensure you don't put variables in the window scope
    (() => {
      const tagName = 'field-empty-app-hello-world';
    
      class EmptyHelloWorld extends HTMLElement {
        
        /* Constructor for WebComponents - the first line must always be super() */
        constructor() {
          super();
          console.log('FYI: EmptyHelloWorld just constructed!');
        }
    
        /* connectedCallback() is the standard callback when the component has been attached */
        connectedCallback() {
          this.innerHTML = 'Hello <em>world</em>!';
          console.log('FYI: EmptyHelloWorld just got connected!');
        }
    
        /** disconnectedCallback() is a standard callback for clean-up */
        disconnectedCallback() {
          console.log('FYI: EmptyHelloWorld getting disconnected - nothing to clean up');
        }
      }
    
      // Register this web component - if it hasn't been registered yet
      if (!customElements.get(tagName)) customElements.define(tagName, EmptyHelloWorld);
    })();

    2sxc 11 makes it very easy to create custom input fields using standard WebComponents. This example shows a color picker using Pickr, a cool JS library.

    ⬇️ Result | Source ➡️

    This example shows a real string-field uses Pickr to provide a color picker.
    Hit this edit button to have a look:

      @inherits Custom.Hybrid.RazorTyped
      @using System.Collections;
      
      <p>
        This example shows a real string-field uses Pickr to provide a color picker. <br>
        Hit this edit button to have a look:
      </p>
      @Kit.Toolbar.Empty().New("UiStringColorPickr").AsTag()

      Source Code of index.js

      /*
        This examples shows a plain JS WebComponent which has has a Pickr color-picker
        Uses the neat Pickr from https://simonwep.github.io/pickr/
        This simple picker has a predefined set of colors and doesn't allow field configuration
      */
      
      // always use an IFFE to ensure you don't put variables in the window scope
      (() => {
      
        const tagName = 'field-string-app-color-pickr';
        const pickrJsCdn = 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js';
        const html = `
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css"/>
        <div class="pickr-container"></div>`;
      
        class StringColorPicker extends HTMLElement {
      
          /** connectedCallback() is the standard callback when the component has been attached */
          connectedCallback() {
            this.innerHTML = html;
            // if the window.Pickr doesn't exist yet, load the JS from the CDN () and then do a callback
            this.connector.loadScript('Pickr', pickrJsCdn, () => { this.initPick() });
          }
      
          /** disconnectedCallback() is a standard callback for clean-up */
          disconnectedCallback() {
            if (this.pickr) this.pickr.destroyAndRemove();
          }
      
          /** This is called when the JS is loaded from loadScript - so Pickr is ready */
          initPick() {
            this.pickr = new Pickr({
              el: '.pickr-container',
              theme: 'classic',
              default: this.connector.data.value || null,
              defaultRepresentation: 'HEXA',
              swatches: this.getSwatches(),
              components: {
                // Main components
                preview: true,
                opacity: true,
                hue: true,
      
                // Input / output Options
                interaction: {
                  hex: true,
                  rgba: true,
                  hsla: true,
                  hsva: true,
                  cmyk: true,
                  input: true,
                  cancel: true,
                  clear: true,
                  save: true,
                },
              },
            });
            
            // remember if we're working empty as of now
            this.cleared = !this.connector.data.value;
      
            // bind events for changes etc. to live-update the preview
            this.pickr.on('change', (color, instance) => this.applyColor(instance));
            this.pickr.on('changestop', (instance) => this.applyColor(instance));
            this.pickr.on('swatchselect', (color, instance) => this.applyColor(instance));
      
            this.pickr.on('save', (color,instance) => instance.hide());
            this.pickr.on('hide', (instance) => this.update(instance));
            this.pickr.on('clear', (instance) => {
              this.cleared = true;
              this.update();
            });
          }
      
          /** Update the preview */
          applyColor(instance) {
            this.cleared = false;
            instance.applyColor(true);
          }
      
          /** Update the value */
          update(instance) {
            // if it's still cleared, just save null
            if (this.cleared) {
              return this.updateIfChanged(null);
            }
            // otherwise get the current color
            var color = instance.getColor();
            if (color) color = color.toHEXA().toString();
            this.updateIfChanged(color);
          }
      
          /** Only update the value if it really changed, so form isn't dirty if nothing was set */
          updateIfChanged(value) {
            var data = this.connector.data;
            if (data.value === '' && value == null) return;
            if (data.value === value) return;
            data.update(value);
          }
          
      
          /** Create the default color recommendations for the color picker */
          getSwatches() {
            return [
              'rgba(244, 67, 54, 1)',
              'rgba(233, 30, 99, 0.95)',
              'rgba(156, 39, 176, 0.9)',
              'rgba(103, 58, 183, 0.85)',
              'rgba(63, 81, 181, 0.8)',
              'rgba(33, 150, 243, 0.75)',
              'rgba(3, 169, 244, 0.7)',
              'rgba(0, 188, 212, 0.7)',
              'rgba(0, 150, 136, 0.75)',
              'rgba(76, 175, 80, 0.8)',
              'rgba(139, 195, 74, 0.85)',
              'rgba(205, 220, 57, 0.9)',
              'rgba(255, 235, 59, 0.95)',
              'rgba(255, 193, 7, 1)',
            ]
          }
        }
      
        // Register this web component - if it hasn't been registered yet
        if (!customElements.get(tagName)) customElements.define(tagName, StringColorPicker);
      })();

      You can learn how to:

      • Use connector.loadScript(...) to load a js library
      • Use connector.data to get the value and update it
      • How to only update the data if you really change it (so users can cancel the dialog without being asked)
      • How to save null to explicitly not save or reset a value
      • Use disconnectedCallback() to destroy inner objects

      Input fields may expect some configuration - like default colors or WebApi endpoints. To enable such a configuration, we need a Content-Type which defines all the fields that can be configured. This can be in the app-data, or it can be stored in a subfolder of the field-extension, to make redestribution easier.

      ⬇️ Result | Source ➡️

      This example shows two color-picker fields, with different initial configurations.
      Hit this edit button to have a look:

        @inherits Custom.Hybrid.RazorTyped
        @using System.Collections;
        
        <p>
          This example shows two color-picker fields, with different initial configurations. <br>
          Hit this edit button to have a look: 
        </p>
        @Kit.Toolbar.Empty().New("UiStringColorPickrPro").AsTag()

        Source Code of index.js

        /*
          This examples shows a plain JS WebComponent which has has a Pickr color-picker
          Uses the neat Pickr from https://simonwep.github.io/pickr/
          This simple picker has a predefined set of colors and doesn't allow field configuration
        */
        
        // always use an IFFE to ensure you don't put variables in the window scope
        (() => {
        
          const tagName = 'field-string-app-color-pickr';
          const pickrJsCdn = 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js';
          const html = `
          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css"/>
          <div class="pickr-container"></div>`;
        
          class StringColorPicker extends HTMLElement {
        
            /** connectedCallback() is the standard callback when the component has been attached */
            connectedCallback() {
              this.innerHTML = html;
              // if the window.Pickr doesn't exist yet, load the JS from the CDN () and then do a callback
              this.connector.loadScript('Pickr', pickrJsCdn, () => { this.initPick() });
            }
        
            /** disconnectedCallback() is a standard callback for clean-up */
            disconnectedCallback() {
              if (this.pickr) this.pickr.destroyAndRemove();
            }
        
            /** This is called when the JS is loaded from loadScript - so Pickr is ready */
            initPick() {
              this.pickr = new Pickr({
                el: '.pickr-container',
                theme: 'classic',
                default: this.connector.data.value || null,
                defaultRepresentation: 'HEXA',
                swatches: this.getSwatches(),
                components: {
                  // Main components
                  preview: true,
                  opacity: true,
                  hue: true,
        
                  // Input / output Options
                  interaction: {
                    hex: true,
                    rgba: true,
                    hsla: true,
                    hsva: true,
                    cmyk: true,
                    input: true,
                    cancel: true,
                    clear: true,
                    save: true,
                  },
                },
              });
              
              // remember if we're working empty as of now
              this.cleared = !this.connector.data.value;
        
              // bind events for changes etc. to live-update the preview
              this.pickr.on('change', (color, instance) => this.applyColor(instance));
              this.pickr.on('changestop', (instance) => this.applyColor(instance));
              this.pickr.on('swatchselect', (color, instance) => this.applyColor(instance));
        
              this.pickr.on('save', (color,instance) => instance.hide());
              this.pickr.on('hide', (instance) => this.update(instance));
              this.pickr.on('clear', (instance) => {
                this.cleared = true;
                this.update();
              });
            }
        
            /** Update the preview */
            applyColor(instance) {
              this.cleared = false;
              instance.applyColor(true);
            }
        
            /** Update the value */
            update(instance) {
              // if it's still cleared, just save null
              if (this.cleared) {
                return this.updateIfChanged(null);
              }
              // otherwise get the current color
              var color = instance.getColor();
              if (color) color = color.toHEXA().toString();
              this.updateIfChanged(color);
            }
        
            /** Only update the value if it really changed, so form isn't dirty if nothing was set */
            updateIfChanged(value) {
              var data = this.connector.data;
              if (data.value === '' && value == null) return;
              if (data.value === value) return;
              data.update(value);
            }
            
        
            /** Create the default color recommendations for the color picker */
            getSwatches() {
              return [
                'rgba(244, 67, 54, 1)',
                'rgba(233, 30, 99, 0.95)',
                'rgba(156, 39, 176, 0.9)',
                'rgba(103, 58, 183, 0.85)',
                'rgba(63, 81, 181, 0.8)',
                'rgba(33, 150, 243, 0.75)',
                'rgba(3, 169, 244, 0.7)',
                'rgba(0, 188, 212, 0.7)',
                'rgba(0, 150, 136, 0.75)',
                'rgba(76, 175, 80, 0.8)',
                'rgba(139, 195, 74, 0.85)',
                'rgba(205, 220, 57, 0.9)',
                'rgba(255, 235, 59, 0.95)',
                'rgba(255, 193, 7, 1)',
              ]
            }
          }
        
          // Register this web component - if it hasn't been registered yet
          if (!customElements.get(tagName)) customElements.define(tagName, StringColorPicker);
        })();

        Source Code of System.Fields.459d4ce3-b96b-4664-9947-75e46df9d33e.json

        {
          "_": { "V": 1 },
          "ContentType": {
            "Id": "459d4ce3-b96b-4664-9947-75e46df9d33e",
            "Name": "@string-app-color-pickr-pro",
            "Scope": "System.Fields",
            "Description": "",
            "Attributes": [
              {
                "Name": "Swatches",
                "Type": "String",
                "InputType": "string-default",
                "IsTitle": true,
                "Metadata": [
                  {
                    "Id": 74876,
                    "Version": 2,
                    "Guid": "47026444-2298-4f78-8210-d088ec1ea14c",
                    "Type": { "Name": "@All", "Id": "@All" },
                    "Attributes": {
                      "String": {
                        "CustomJavaScript": { "*": "" },
                        "DefaultValue": { "*": "" },
                        "InputType": { "*": "string-default" },
                        "Name": { "*": "Swatches" },
                        "Notes": {
                          "*": "<p>Place recommended color codes - one on each line. Use HEXA format, like #770088aa</p>"
                        },
                        "ValidationRegExJavaScript": { "*": "" }
                      },
                      "Entity": { "Errors": { "*": [] }, "Warnings": { "*": [] } },
                      "Boolean": {
                        "Disabled": { "*": false },
                        "Required": { "*": false },
                        "VisibleInEditUI": { "*": true }
                      }
                    },
                    "Owner": "dnn:userid=1"
                  },
                  {
                    "Id": 74877,
                    "Version": 1,
                    "Guid": "75e73a53-b6cc-4f65-9cf8-22cdcb8fbc7f",
                    "Type": { "Name": "@String", "Id": "@String" },
                    "Attributes": {
                      "String": {
                        "DropdownValues": { "*": "" },
                        "InputType": { "*": "" }
                      }
                    },
                    "Owner": "dnn:userid=1"
                  },
                  {
                    "Id": 74878,
                    "Version": 1,
                    "Guid": "dea3bf2f-3a95-4a05-adcf-353de7ec07c0",
                    "Type": { "Name": "@string-default", "Id": "@string-default" },
                    "Attributes": { "Number": { "RowCount": { "*": 10.0 } } },
                    "Owner": "dnn:userid=1"
                  }
                ]
              }
            ],
            "Metadata": []
          }
        }

        This example contains the json-exported content-type in the folder /system/field-string-app-color-pickr-pro/.data/contenttypes/ so you could just copy the extension folder to another app and use it from there.

        So in this tutorial you'll learn how to:

        • To see how the UI changes based on field configuration
        • How to access such pre-configured settings with connector.field.settings
        • The Content-Type for configuration is included in the extension folder

        Creating an own WYSIWYG would be super difficult. This is why we decided to create a simple API where you can use the existing WYSIWYG and just reconfigure it. For the configuration you will need to understand the TinyMCE API and the names of our callbacks, but then it's really easy.

        ⬇️ Result | Source ➡️

        This example shows a reduced WYSIWYG with only 4 buttons.
        Hit this edit button to have a look:

          @inherits Custom.Hybrid.RazorTyped
          @using System.Collections;
          
          <p>
            This example shows a reduced WYSIWYG with only 4 buttons. <br>
            Hit this edit button to have a look: 
          </p>
          @Kit.Toolbar.Empty().New("UiStringWysiwygMicro").AsTag()

          Source Code of index.js

          /*
            This examples shows a JS WebComponent which makes a custom WYSIWYG
          */
          
          // always use an IFFE to ensure you don't put variables in the window scope
          (() => {
            const tagName = 'field-string-wysiwyg-micro';
            const builtInWysiwyg = '[System:Path]/system/field-string-wysiwyg/index.js';
          
            /** Our WebComponent which is a custom, lightweight wysiwyg editor */
            class StringWysiwygCustom extends HTMLElement {
          
              /* connectedCallback() is the standard callback  when the component has been attached */
              connectedCallback() {
                // We need to ensure that the standard WYSIWYG is also loaded
                this.connector.loadScript('tinymce', builtInWysiwyg, (x) => { this.initWysiwygCallback() })
              }
          
              initWysiwygCallback() {
                // 1. Create a built-in field-string-wysiwyg control
                const wysiwyg = document.createElement('field-string-wysiwyg');
                // 2. tell it if it should start in preview or edit
                wysiwyg.mode = 'edit'; // can be 'preview' or 'edit'
                // 3. attach connector
                wysiwyg.connector = this.connector;
                // 4. also attach reconfigure object which can change the TinyMCE as it's initialized
                wysiwyg.reconfigure = new WysiwygReconfigurator();
                // 5. Append it to the DOM. Do this last, as it will trigger connectedCallback() in the wysiwyg
                this.appendChild(wysiwyg);
              }
            }
          
            /** The object which helps reconfigure what the editor will do */
            class WysiwygReconfigurator {
              configureOptions(options) {
                options.toolbar = "undo redo | bold italic"
                return options;
              }
            }
          
            // Register this web component - if it hasn't been registered yet
            if (!customElements.get(tagName)) customElements.define(tagName, StringWysiwygCustom);
          })();

          You can learn how to:

          1. Use connector.loadScript(...) to load the builtin WYSIWYG
          2. ...and wait for the callback to ensure it's ready
          3. Create a inner field-string-wysiwyg
          4. Set the mode to edit (instead of preview)
          5. Attach the connector so the inner object has it as well
          6. Attach the reconfigure object
          7. Create your own Reconfigurator which can make changes
          8. Use configureOptions to set a different toolbar

          This example show how we customize more of TinyMCE.

          ⬇️ Result | Source ➡️

          This example keeps the default buttons and adds another one, with multi-language labels
          Hit this edit button to have a look:

            @inherits Custom.Hybrid.RazorTyped
            @using System.Collections;
            
            <p>
              This example keeps the default buttons and adds another one, with multi-language labels <br>
              Hit this edit button to have a look: 
            </p>
            @Kit.Toolbar.Empty().New("UiStringWysiwygButtonMl").AsTag()

            Source Code of index.js

            /*
              This examples shows a custom WYSIWYG JS WebComponent with additional language and button
            */
            
            // always use an IFFE to ensure you don't put variables in the window scope
            (() => {
              const tagName = 'field-string-wysiwyg-button-ml';
              const builtInWysiwyg = '[System:Path]/system/field-string-wysiwyg/index.js';
            
              // Button labels, mouse-over-tooltips, etc.
              const en = { "TestButton.Tooltip": "Tooltip Button EN!" };
              const de = { "TestButton.Tooltip": "Tooltip Knopf DE!" };
            
              /** Our custom Wysiwyg with an additional button having labels in 2 languages */
              class StringWysiwygCustom extends HTMLElement {
            
                /* connectedCallback() is the standard callback  when the component has been attached */
                connectedCallback() {
                  // We need to ensure that the standard WYSIWYG is also loaded
                  this.connector.loadScript('tinymce', builtInWysiwyg, (x) => { this.initWysiwygCallback() })
                }
            
                initWysiwygCallback() {
                  // 1. Create a built-in field-string-wysiwyg control
                  const wysiwyg = document.createElement('field-string-wysiwyg');
                  // 2. tell it if it should start in preview or edit
                  wysiwyg.mode = 'edit'; // can be 'preview' or 'edit'
                  // 3. attach connector
                  wysiwyg.connector = this.connector;
                  // 4. also attach reconfigure object which can change the TinyMCE as it's initialized
                  wysiwyg.reconfigure = new WysiwygReconfigurator();
                  // 5. Append it to the DOM. Do this last, as it will trigger connectedCallback() in the wysiwyg
                  this.appendChild(wysiwyg);
                }
              }
            
              /** The object which helps reconfigure what the editor will do */
              class WysiwygReconfigurator {
                addTranslations(editorManager, language) {
                  console.log("add translations to tinyMCE", editorManager, language);
                  editorManager.addI18n(language, language == 'de' ? de : en);
                }
            
                configureOptions(options) {
                  console.log('will try to modify tinyMCE options', options);
                  options.toolbar += " | testButton";
                  return options;
                }
            
                configureAddOns(addOnSettings) {
                  console.log('configure add-on settings for stuff added by the 2sxc form');
                  addOnSettings.imgSizes = [25, 10];
                  return addOnSettings;
                }
            
                editorOnInit(editor) {
                  console.log('editor init, will add button to tinyMCE', editor);
                  editor.ui.registry.addButton('testButton', {
                    icon: 'close',
                    tooltip: 'TestButton.Tooltip',
                    onAction: (_) => { alert('test-button used!') },
                  });
                }
            
                disableDnn = true;
              }
            
              // Register this web component - if it hasn't been registered yet
              if (!customElements.get(tagName)) customElements.define(tagName, StringWysiwygCustom);
            })();

            You can learn how to:

            • Use addTranslations()
            • Use configureOptions(...) to extend the toolbar
            • Use configureAddOns to change button options for the image
            • Run code when the editor is running init to add button definitions