Building OctoberCMS Form Field Widgets like a Pro
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
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 toname
.pattern
: The displayed input type (text
,email
,number
,url
). Defaults totext
.
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.
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.