PHP
Article

How to Build an OctoberCMS Widget Plugin

By Younes Rafie

In a previous article we talked about the basics of creating an OctoberCMS plugin. In this one, we’re going to go deeper, and we’ll explore how we can extend the OctoberCMS backend using widgets.

OctoberCMS

What We Are Going To Build

I will assume that many of you have used WordPress. On the dashboard, we have a quick draft widget. In this article, we’re going to see how we can build a similar one for OctoberCMS.

Quick Notes Widget
Quick Note Widget Listing

Plugin Registration

To start building our plugin, we’ll use a command utility for scaffolding.

php artisan create:plugin RAFIE.quicknote

The command will create a Plugin.php file and an updates folder containing the version.yaml file.

public function pluginDetails(){
   return [
       'name'        => 'Quick Note Widget',
       'description' => 'Add and manage some drafts when you\'re in a hurry.',
       'author'      => 'RAFIE Younes',
       'icon'        => 'icon-pencil'
   ];
}

After registering our plugin we need to update our version.yaml file.

// uploads/version.yaml
1.0.1: First version of quicknote
1.0.2:
  - Created Notes Table
  - create_notes_table.php

Using Models

To create our migration and model files we can use the php artisan create:model RAFIE.quicknote Note command. Our updates/create_notes_table.php migration file handles the creation of the notes table, and will have the following structure.

Notes Table Structure

Because every note belongs to a user, we have a user_id field. Our migration file will look like this:

public function up(){
    Schema::create('rafie_quicknote_notes', function($table){
        $table->engine = 'InnoDB';
        $table->increments('id');
        $table->string('title', 255);
        $table->text('description')->nullable();
        $table->integer('user_id')->unsigned()->index();
        $table->timestamps();
    });
}

public function down(){
    Schema::dropIfExists('rafie_quicknote_notes');
}

After updating the file we need to refresh our plugin to reflect the changes using the following command, which will reinstall the plugin and create our database table.

artisan plugin:refresh RAFIE.quicknote

If you are a Laravel fan, you know that every model class must extend the Illuminate\Database\Eloquent\Model to use eloquent, yet OctoberCMS has an October\Rain\Database\Model class that extends the Eloquent model class, and provides more extensibility, like the extend method.

public function boot(){
    User::extend(function($model){
        $model->hasMany['notes'] = ['RAFIE\Quicknote\Models\Notes'];
    });
}

Inside the Plugin.php file, we override the boot method, which gets called on every request, and extend the User model to have many notes.

class Note extends Model{
    // used for automatic validation using the defined rules.
    use \October\Rain\Database\Traits\Validation;
	
    public $table = 'rafie_quicknote_notes';
	
    protected $guarded = ['*'];
	
    protected $rules = [
        'title'         => 'required|min:4'
    ];

    public $belongsTo = [ 'user' => [ 'Backend\Models\User' ] ];
}

Inside our models/Note.php files we have a basic model definition. Also, the belongsTo attribute defines our model relationship. The validation is automatically handled by the validation trait using the defined rules.

OctoberCMS Widgets

OctoberCMS widgets are blocks of content that can be integrated in different ways to your font-end or back-end. There are three different kinds of widgets.

Generic Widgets

Generic widgets are bundles functionality that can be injected to the page. They behave like components and are stored inside the widgets folder. You can read more in the docs.

Form Widgets

Form widgets are a bit special, they allow you to create new types of controls that can be used by other plugins or by the CMS itself. A good example from the docs is the CodeEditor control used for creating pages, and there is also the search box used in many places, and integrated by default inside our notes management page.

Report Widgets

Report widgets are the most known type of widgets. They add content on a specific context, in this case the dashboard. On a fresh installation of OctoberCMS we have a SYSTEM STATUS widget displaying available updates and the website status.

Scaffolding Our Widget

To begin, let’s create a QuickNoteWidget.php file where we can define our QuickNote widget, and we need also to register our widget inside our Plugin.php file.

// Plugin.php

public function registerReportWidgets(){
    return [
        'RAFIE\QuickNote\QuickNoteWidget' => [
            'label'     => 'Quick Notes',
            'context'   => 'dashboard'
        ]
    ];
}
class QuickNoteWidget extends ReportWidgetBase{

   public function render(){
       $notes = BackendAuth::getUser()->notes;

       return $this->makePartial('notes', [ 'notes' => $notes ]);
   }
}

Report widgets must extend the ReportWidgetBase class and override the render method.

The BackendAuth::getUser method returns the logged in user, which has the associated list of notes. This is only possible because we’ve defined the relation inside our models.

OctoberCMS will look for our assets inside a directory with the same name as our widget file name lowercased. The partials directory holds our views and they must start with an underscore.

//quicknotewidget/partials/_notes.htm
<div class="report-widget">
    <h3>Quick Note</h3>

    <div class="pane">
    <ul class="list-nostyle">
        <?php foreach( $notes as $note ): ?>
            <li class="list-group-item"><?= $note->title ?></li>
        <?php endforeach ?>
    </ul>
    </div>

    <br/>
	
    <?= Form::open([
            'url'       => Backend::url('rafie/quicknote/notes/store'),
            'method'    => 'POST'
        ]);
    ?>
    <div class="form-group">
        <input class="form-control" type="text" name="title" placeholder="Title" required />
    </div>

    <div class="form-group">
        <textarea class="form-control" name="description" id="" cols="30" rows="10" placeholder="You have something to say?"></textarea>
    </div>

    <div class="form-group">
        <input type="submit" class="btn btn-primary" value="Submit" />
        <a href="<?= Backend::url('rafie/quicknote/notes/index') ?>">Manage your notes</a>
    </div>

    <?= Form::close(); ?>
</div>

The widget’s HTML code must be contained within a report-widget class and optionally have an <h3> for the widget title. After that we loop through the list of user notes and show a form for creating a new note.

The Backend::url('rafie/quicknote/notes/store') will generate a link to our notes controller where we can process our form data. We could have used OctoberCMS’ Ajax framework for the form, but to keep things simple we’re going to normally submit the form and talk about using AJAX later.

Using Controllers

Controllers are stored within the controllers folder, and can be generated using a scaffolding command.

php artisan create:controller RAFIE.quicknote Notes

Controllers folder

Creating Notes

The folder contains our controller class, and another folder for our assets. When someone hits a URL, it’s parsed as the following: author/plugin/controllerClass/method. In our case, it’s rafie/quicknote/notes/store, if no method is specified, the index method is fired by default.

public function store(){
    $note = new Models\Note;
    $note->title = Input::get('title');
    $note->description = Input::get('description', null);
    $note->user_id = BackendAuth::getUser()->id;
	
    if( $note->save() ) {
	\Flash::success('Note added successfully.');
    }
    else{
        \Flash::error('Validation error' );
    }
	
    return \Redirect::to( Backend::url() );
}

We create a new note using our model and save it to the database. The Flash::success() will display a flash message to the user to confirm the insertion.

You may have noticed the Manage your notes link pointing to the rafie/quicknote/notes/index url. This is where we’re going to list our notes so that the user can add, edit, and delete notes.

Widget Configuration

Let’s say that we want to give the user the ability to choose how the widget renders. We can give him the choice to hide the list of notes from the dashboard widget, and keep just the form and the link, and even change the widget title.

// QuickNoteWidget.php

public function defineProperties(){
    return [
        'title'     => [
            'title'     => 'Widget title',
            'default'   => 'QUICK NOTE'
        ],
        'showList'  => [
            'title'     => 'Show notes',
            'type'      => 'checkbox'
        ]
    ];
}

If you don’t know what properties are, be sure to check my article about How to build an OctoberCMS plugin. After defining the properties we can retrieve them inside our view.

// quicknotewidget/partials/_notes.htm

<h3><?= $this->property('title') ?></h3>

<?php if( $this->property('showList') ): ?>
    <ul class="list-nostyle">
        <?php foreach( $notes as $note ): ?>
            <li class="list-group-item"><?= $note->title ?></li>
        <?php endforeach ?>
    </ul>

    <br/>
<?php endif; ?>

Widget Properties

Using Controllers

Up until now, we talked about rendering a report widget and submitting it to the controller. Now, we’re going to build the listing page and talk about extending the OctoberCMS backend by creating the listing and management part.

Listing Notes

Every controller has a config_list.yaml file where you can define your listing, and it’s also mapped to a model using some configuration attributes.

// controllers/notes/config_list.yaml

# Model List Column configuration
list: $/rafie/quicknote/models/note/columns.yaml

# Model Class name
modelClass: RAFIE\Quicknote\Models\Note

# List Title
title: Manage Notes

# Link URL for each record
recordUrl: rafie/quicknote/notes/update/:id

# Message to display if the list is empty
noRecordsMessage: backend::lang.list.no_records

# Records to display per page
recordsPerPage: 20

# Displays the list column set up button
showSetup: true

# Displays the sorting link on each column
showSorting: true

# Default sorting column
 defaultSort:
     column: created_at
     direction: desc

# Display checkboxes next to each record
showCheckboxes: true

# Toolbar widget configuration
toolbar:
	# Partial for toolbar buttons
	buttons: list_toolbar
	
	# Search widget configuration
	search:
	    prompt: backend::lang.list.search_prompt

The attributes are generally well explained, but let’s add more clarification to some:

  • list: path to the list columns definition.
  • showSetup: little button to toggle the columns display.
  • showCheckboxes: show a checkbox for every row, used for bulk actions.
  • toolbar -> button: partial name, if you want to show some controls above the table, like New or Delete.
// rafie/quicknote/models/note/columns.yaml

columns:
    id:
        label: ID

    title:
        label: TITLE
        searchable: true

    description:
        label: DESCRIPTION
        searchable: true

    created_at:
        label: CREATED AT
        type: date
        invisible: true

    updated_at:
        label: UPDATED AT
        type: date
        invisible: true

The columns.yaml file maps our database table to the list using label, searchable, etc. Check the docs for the full list of attributes.

// controllers/Notes.php

public function index(){
    $this->makeLists();
    $this->makeView('index');
}

// controllers/notes/index.htm

<?= $this->listRender() ?>

Inside our Notes@index method we parse the list configuration and we render it inside our index view. You can also add some extra markup if you have any extra fields.

If you hit the index controller you’ll see a basic listing of our notes. The list configuration is pulling all notes from the table including other users notes, and that’s not what we want, but we can extend the listExtendQueryBefore method from the ListController class and filter user notes.

// controllers/Notes.php

public function listExtendQueryBefore($query){
    $user_id = BackendAuth::getUser()->id;

    $query->where('user_id', '=', $user_id);
}

Now you should see that the list is filtered by the logged in user, and just to get familiar with it, let’s try to distinguish the notes that have no description by extending our columns using the listOverrideColumnValue method.

// controllers/Notes.php

public function listOverrideColumnValue($record, $columnName){
  if( $columnName == "description" && empty($record->description) )
         return "[EMPTY]";
}

You can combine the listOverrideColumnValue with listExtendColumns to add new columns and insert new values, like adding a ‘mark as read’ button for each row, etc.

public function listExtendColumns($list){
    $list->addColumns([
        'action' => [
            'label'     => 'Actions',
            'sortable'  => false
        ]
    ]);
}

If you remember, inside our config_list.yaml we had a list_toolbar field:

// controllers/notes/_list_toolbar.htm

<div data-control="toolbar">
    <a href="<?= Backend::url('rafie/quicknote/notes/create') ?>" class="btn btn-primary oc-icon-plus">New Note</a>

    <button
            id = "remove_notes"
            class="btn btn-primary oc-icon-trash-o"
            data-request="onDelete"
            data-trigger-type="enable"
            data-trigger = ".list-checkbox input[type='checkbox']"
            data-trigger-condition="checked"
            data-request-success="$el.attr('disabled', 'disabled');"
            disabled
            >
        Remove Note(s)</button>
</div>

<script>
    $("#remove_notes").click(function(){
        $(this).data('request-data', {
            notes: $('.list-checkbox input[type=\'checkbox\']').listWidget('getChecked')
        })
    });
</script>

New Notes

I know that we’ve already created a form for adding new notes, but let’s see the other way of mapping forms to models.

The controllers/notes/config_form.yaml file is responsible for displaying the create/update form using the specified model, and it can also be configured using the models/note/fields.yaml file.

// controllers/notes/config_form.yaml

# Record name
name: Note

# Model Form Field configuration
form: $/rafie/quicknote/models/note/fields.yaml

# Model Class name
modelClass: RAFIE\Quicknote\Models\Note

# Default redirect location
defaultRedirect: rafie/quicknote/notes

# Create page
create:
    title: Create Notes
    redirectClose: rafie/quicknote/notes

# Update page
update:
    title: Edit Notes
    redirectClose: rafie/quicknote/notes

Configuration attributes are self descriptive. The form attribute maps to the models/note/fields.yaml which decides how the form should be displayed. You can read about the available attribute in the docs.

fields:
  title:
    placeholder: Title

  description:
    type: textarea
    size: huge
    placeholder: You have something to say?

  important:
    type: hint
    path: @/plugins/rafie/quicknote/models/note/partials/create_note_hint.htm

We are creating only one text input for the title and a huge textarea for the description. The hint is a partial that you can use to display notifications to the user.

Create note form

If you noticed the page url rafie/quicknote/notes/create, you may be wondering where we rendered the form? The answer is the Backend\Behaviors\FormController implemented by our controller – it takes care of displaying the form using our configuration files.

After creating your new note, you can visit the database to verify records. You’ll find that the user_id is set to 0 and that’s not what we want!

public function formBeforeCreate($model){
	$model->user_id = BackendAuth::getUser()->id;
}

The FormController class provides a set of methods to hook into form events. The formBeforeCreate method is used to update the model before saving, and you can also hook to the formAfterCreate to fire some special events.

Updating Notes

For the update form, we won’t need any modification because we mapped the form fields to our note model. However, you may need to add some functionality. If you have some code that needs to be executed, you can use the update method and call the parent update method as an extension. Be sure to explore the FormController class to see the list of available methods.

public function update($recordId, $context = null)
{
	//some code here
	
    return $this->asExtension('FormController')->update($recordId, $context);
}

Removing Notes

We have two ways to implement the remove action:

  • Using the update form:
    When using the update form you have a trash icon at the bottom right of the browser and it’s already configured for you.
  • Using a bulk action:
    You remember that we’ve set the showCheckboxes to true inside the config_list.yaml file. We only need to configure the remove notes button.

The _list_toolbar.htm partial is where we have our New Note button. We will add our remove notes button using OctoberCMS’ Ajax framework. If you’re not familiar with the AJAX framework, be sure to check my building OctoberCMS theme article.

// controllers/notes/_list_toolbar.htm

<button
    id = "remove_notes"
    class="btn btn-primary oc-icon-trash-o"
    data-request="onDelete"
    data-trigger-type="enable"
    data-trigger = ".list-checkbox input[type='checkbox']"
    data-trigger-condition="checked"
    data-request-success="$el.attr('disabled', 'disabled');"
    disabled
    >
Remove Note(s)</button>

The only special attributes are:
– data-trigger: add an event listener on the specified element.
– data-trigger-condition: the condition can be checked or a value if value[myvalue] is set. Check the trigger api for more info.
– data-request-success: JavaScript code to be executed after a successful request.

// controllers/notes/_list_toolbar.htm

<script>
    $("#remove_notes").click(function(){
        $(this).data('request-data', {
            notes: $('.list-checkbox input[type=\'checkbox\']').listWidget('getChecked')
        })
    });
</script>

On click events, we need to pass the selected IDs to the request-data attribute so that we can process them on the server side. Our controller must have an onDelete method to handle the request.

// controllers/Notes.php

public function onDelete(){
    $user_id = BackendAuth::getUser()->id;
    $notes = post("notes");

    Note::whereIn('id', $notes)
			->where('user_id', '=', $user_id)
			->delete();
	
    \Flash::success('Notes Successfully deleted.');
	
    return $this->listRefresh();
}

After deleting the notes we show a flash message to the user as a feedback, and we refresh the list using listRefresh, which regenerates the list and shows the new one on the page.

Conclusion

OctoberCMS’ widget system is powerful and flexible, and it provides a set of components to create and extend other plugins. You can take a look at the final result on Github, and if you have any question or opinions let me know in the comments!

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.