PHP
Article

Building OctoberCMS Form Field Widgets like a Pro

By Younes Rafie

OctoberCMS logo

Creating your business website with any CMS requires you to make the back-end user friendly, and that means making the forms meaningful and accessible. In this article, we’re going to explore OctoberCMS form widgets and create a widget called UniqueValue, which helps the user enter a unique value. This could be useful for entering emails, usernames, post slugs, etc. Let’s get started.

Available Form Widgets

OctoberCMS provides a list of simple field types like email input, password, dropdown options, etc. The documentation has a list of all available fields. Moreover, the CMS provides some custom widgets like the media manager widget which lets you select an item from your media library or the WYSIWYG editor, Markdown editor, etc.

An interesting widget we should mention here is the repeater widget. Let’s say you have a recipes website. The cook will enter the recipe name and start filling in the ingredients. You might ask the user “how many ingredients do you need?” and based on that, you can generate the form fields. Another clean way to do it is to have a button at the bottom of the form that says Add new ingredient, which will generate the necessary fields for the cook when needed.

Here is an example configuration for the recipe form:

// models/recipe/fields.yaml

fields:
    name:
        label: Name
        type: text
        required: true
    ingredients:
        label: Ingredients
        type: repeater
        prompt: Add new ingredient
        form:
            fields:
                ingredient:
                    label: Ingredient
                    type: text
                how_much:
                    label: How much
                    type: number
                unit:
                    label: Unit
                    type: dropdown
                    options:
                        spoon: Spoon
                        ounce: Ounce
                        # etc

ingredients

Creating a Form Widget

If you read the previous OctoberCMS article (you should, it’s great!), you’ll know that we built a CRUD plugin. We’ll be using it in this article, so make sure to clone it into your OctoberCMS installation.

# Inside your plugins folder

git clone git@github.com:Whyounes/OctoberCMS_Sitepoint_plugin_demo.git rafie/sitepointDemo

You can check the final code for this tutorial in the uniqueValue-formwidget branch in the same repo.

To start building our widget, we use the create:formwidget scaffolding command which creates a view folder, assets folder and a FormWidgetBase class.

php artisan create:formwidget rafie.SitepointDemo UniqueValue

Our UniqueValue widget requires three properties:

  • modelClass: The model class that will have the unique field.
  • selectFrom: The field name inside the model. Defaults to name.
  • pattern: The displayed input type (text, email, number, url). Defaults to text.

After our form widget class is constructed, it will automatically call the inherited init method, which is responsible for preparing our widget for rendering.

// formwidgets/UniqueValue.php

class UniqueValue extends FormWidgetBase
{
    /**
     * {@inheritDoc}
     */
    protected $defaultAlias = 'rafie_sitepointDemo_uniquevalue';

    /**
     * {@inheritDoc}
     */
    public function init()
    {
    }
}

Our parent WidgetBase class provides a fillFromConfig helper method which maps the passed config properties from the fields.yaml file to the form widget class’ attributes.

// formwidgets/UniqueValue.php

class UniqueValue extends FormWidgetBase
{
    /*
     * Config attributes
     */
    protected $modelClass = null;
    protected $selectFrom = 'name';
    protected $pattern = 'text';

    /**
     * {@inheritDoc}
     */
    protected $defaultAlias = 'rafie_sitepointDemo_uniquevalue';

    /**
     * {@inheritDoc}
     */
    public function init()
    {
        $this->fillFromConfig([
            'modelClass',
            'selectFrom',
            'pattern'
        ]);
        $this->assertModelClass();

        parent::init();
    }
    
    // ...
}

After calling the fillFromConfig function, we assert that the model class exists and then call the parent init method.

// formwidgets/UniqueValue.php

class UniqueValue extends FormWidgetBase
{
    // ...
    
    protected function assertModelClass()
    {
        if( !isset($this->modelClass) || !class_exists($this->modelClass) )
        {
            throw new \InvalidArgumentException(sprintf("Model class {%s} not found.", $this->modelClass));
        }
    }
    
    // ...
}
// formwidgets/uniquevalue/UniqueValue.php

class UniqueValue extends FormWidgetBase
{
    // ...
    
    public function render()
    {
        $this->prepareVars();

        return $this->makePartial('uniquevalue');
    }

    /**
     * Prepares the form widget view data
     */
    public function prepareVars()
    {
        $this->vars['inputType'] = $this->pattern;
        $this->vars['name'] = $this->formField->getName();
        $this->vars['value'] = $this->getLoadValue();
        $this->vars['model'] = $this->model;
    }
}

OctoberCMS will look for the partial inside the partials folder and pass the $this->vars array to it.

// formwidgets/uniquevalue/partials/_uniquevalue.htm

<?php if ($this->previewMode): ?>

    <div class="form-control">
        <?= $value ?>
    </div>

<?php else: ?>
    <div class="input-group">
        <input
            type="<?= $inputType ?>"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control unique_widget"
            autocomplete="off"
        />
        <span class="input-group-addon oc-icon-remove"></span>
    </div>

<?php endif ?>

The input has a preview and an editing mode. When editing, we display an input and fill the type using the specified pattern. The value is automatically set in this case if we are updating a record. The span.input-group-addon element will display a check or a remove icon depending on the entered value.

Using AJAX

OcotberCMS has a set of scripts that let you update your content using HTML5 data attributes and AJAX handlers. We may cover it in detail in another article, but you can refer to the documentation for more details right now, if you’re curious.

We’re going to check if the entered value is unique when the input value changes. First we need to add the data-request attribute which specifies the backend method handler name.

// formwidgets/uniquevalue/partials/_uniquevalue.htm

// ...
        <input
            type="<?= $inputType ?>"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control unique_widget"
            autocomplete="off" 
            data-request="onChange"
        />
// ...

Next, we specify the JS function that will handle a successful request’s response using the data-request-success attribute. It will receive a list of parameters, but the most important ones are $el, which refers to our input, and the data parameter, which holds the request’s result.

// formwidgets/uniquevalue/partials/_uniquevalue.htm

// ...
        <input
            type="<?= $inputType ?>"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control unique_widget"
            autocomplete="off"
            data-request="onChange"
            data-request-success="uniqueInputChanged($el, context, data, textStatus, jqXHR);"
        />
// ...

OctoberCMS’ AJAX framework provides a data-track-input attribute to trigger the data-request handler if the element has changed. It accepts an optional delay parameter which we can use to minimize the number of sent requests.

// formwidgets/uniquevalue/partials/_uniquevalue.htm

// ...
        <input
            type="<?= $inputType ?>"
            id="<?= $this->getId('input') ?>"
            name="<?= $name ?>"
            value="<?= $value ?>"
            class="form-control unique_widget"
            autocomplete="off"
            data-request="onChange"
            data-request-success="uniqueInputChanged($el, context, data, textStatus, jqXHR);"
            data-track-input="500"
        />
// ...

We still didn’t define our onChange handler method inside our form widget class. OctoberCMS will look for a handler with the same name inside the backend page controller or any other widgets used. To avoid conflicts, we use the fully prefixed handler name which will include our widget alias.

// formwidgets/uniquevalue/partials/_uniquevalue.htm

// ...
        <input
            // ...
            data-request="<?= $this->getEventHandler('onChange') ?>"
            // ...
        />
// ...
// formwidgets/uniquevalue/UniqueValue.php

class UniqueValue extends FormWidgetBase
{
    // ...
    
    public function onChange()
    {
        $formFieldValue = post($this->formField->getName());
        $modelRecords = $this->model->newQuery()->where($this->selectFrom, $formFieldValue);

        return ['exists' => (boolean) $modelRecords->count()];
    }
    
    // ...
}

$this->formField->getName() returns the input name that we use to get the input value from the post data. Then, we call the modelClass::where method with the selectFrom config value and the post data.

The only thing left is to process the request’s result using the JavaScript function defined in our data-request-success attribute. Our CSS and JavaScript assets are loaded inside the UniqueValue@loadAsserts method.

// formwidgets/uniquevalue/UniqueValue.php

class UniqueValue extends FormWidgetBase
{
    // ...
    
    public function loadAssets()
    {
        // $this->addCss('css/uniquevalue.css', 'rafie.SitepointDemo');
        $this->addJs('js/uniquevalue.js', 'rafie.SitepointDemo');
    }
    
    // ...
}
// formwidgets/uniquevalue/assets/js/uniquevalue.js

function uniqueInputChanged($el, context, data, textStatus, jqXHR)
{
    var addon = $el.parents('.input-group').find('.input-group-addon');
    
    if( !$el.val().trim() || data.exists )
    {
        addon.removeClass('oc-icon-check').addClass('oc-icon-remove');

        return;
    }

    addon.removeClass('oc-icon-remove').addClass('oc-icon-check');
}

We query our addon element, test the return value, and set the appropriate icon class on the element. You can test the widget by inserting the below code inside your model fields configuration file.

// fields.yaml

fields:
    slug:
        label: Slug
        type: \Rafie\SitepointDemo\FormWidgets\UniqueValue
        modelClass: \RainLab\Blog\Models\Post
        selectFrom: slug
        pattern: text

This is a final demo of the widget.

Test UniqueValue Form Widget

As a final step, we will register our form widget as a system widget (text, checkboxlist, etc).

// Plugin.php

// ...
/**
 * Registers any form widgets implemented in this plugin.
 */
public function registerFormWidgets()
{
    return [
        'Rafie\SitepointDemo\FormWidgets\UniqueValue' => [
            'label' => 'Unique Value',
            'code' => 'uniquevalue'
        ],
    ];
}
// ...

Now, we can use our widget by using the registered code.

// fields.yaml

fields:
    slug:
        label: Slug
        type: uniquevalue
        modelClass: \RainLab\Blog\Models\Post
        selectFrom: slug
        pattern: text

Conclusion

In this article, we explored the OcotberCMS backend form field widgets and we built a simple demo widget to test it. You can extend the final widget by adding new functionality, like adding a preventSubmit option when the value is not unique, etc. You can check the final version on Github and if you have any questions or comments you can leave them below and I’ll do my best to answer them.

  • Aaron Humphreys

    Just don’t use a repeater field within a repeater field. It gets messy quickly.

  • sunnyindustries

    How to get a value of a dropdown from a table?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.