• Building extensions for Pagekit Part #5 - The first view

Building extensions for Pagekit Part #5 - The first view

Extension settings

Potentially outdated

This post is older than 365 days and may be outdated. Please use the site-search to search for updated information.

Hello again,

in the last part of my blog series we learned something about namespaces.

Today we are going to have our first actual results. Well - this post will be a bit extensive - so I apologize in advance ;).

Create our first view

In the first step we will create our first view. So open your editor or IDE and just add a new file: views/admin/settings.php.

If your IDE asks to add this file to the git repository, please confirm this request.

Phpstorm add file Phpstorm add file path Phpstorm add file to git Phpstorm created file settings view

Now we can add some content:

<?php $view->script( 'settings', 'spqr/survey:app/bundle/settings.js', [ 'vue' ] ); ?>

<div id="settings" class="uk-form uk-form-horizontal" v-cloak>
    <div class="uk-grid pk-grid-large" data-uk-grid-margin>
        <div class="pk-width-sidebar">
            <div class="uk-panel">
                <ul class="uk-nav uk-nav-side pk-nav-large" data-uk-tab="{ connect: '#tab-content' }">
                    <li><a><i class="pk-icon-large-settings uk-margin-right"></i> {{ 'General' | trans }}</a></li>
                </ul>
            </div>
        </div>
        <div class="pk-width-content">
            <ul id="tab-content" class="uk-switcher uk-margin">
                <li>
                    <div class="uk-margin uk-flex uk-flex-space-between uk-flex-wrap" data-uk-margin>
                        <div data-uk-margin>
                            <h2 class="uk-margin-remove">{{ 'General' | trans }}</h2>
                        </div>
                        <div data-uk-margin>
                            <button class="uk-button uk-button-primary" @click.prevent="save">{{ 'Save' | trans }}
                            </button>
                        </div>
                    </div>
                    <div class="uk-form-row">
                        <label for="form-restrict" class="uk-form-label">{{ 'Restrict to one vote per IP' |
                            trans }}</label>
                        <div class="uk-form-controls uk-form-controls-text">
                            <input id="form-restrict" type="checkbox" v-model="config.restrict">
                        </div>
                    </div>
                </li>
            </ul>
        </div>
    </div>
</div>

With this code we are going to create a view with a list of elements; this list only contains one element (General) at the moment, but the more complex our extension is going to be, the more stuff needs to be inserted - and in this case our switcher will be very useful.

The General-tab contains a checkbox; this checkbox is used to either restrict to one vote per IP or not.

I also added a Save-button, which will be used to save our settings.

As you can see, we are using HTML and the uikit css framework, which is automatically loaded in Pagekit's admin backend.

Did you mention the first line? There is some PHP source:

<?php $view->script( 'settings', 'spqr/survey:app/bundle/settings.js', [ 'vue' ] ); ?>

This adds some JavaScript to our view - we need this, because we are going to use Vue.js later on.

Did you mention, that I used the shorthand spqr/survey (remember?) to tell our view, where to look at?

I also added a dependency to our view: As spqr/survey:app/bundle/settings.js is using Vue.js I need to make sure, that the Vue.js framework itself is included. As you might have already guessed, the dependencies can be added as an array. In this case it's just [ 'vue' ], but it could also be [ 'vue', 'jquery' ].

I think you already mentioned the "curly" mustaches in the code. Well, these elements are processed by Vue.js.

For example {{ 'General' | trans }} is a string ("General"), which is going to be translated to the site's language, if there are any translations.

If there are not translations, the string "General" will be printed. So we always write the English expression and provide some translations later.

The v-model

If you did not work with Vue.js before, you might wonder, how we are going to process the input from the checkbox. So how do we save the value and how do we load the saved value, if the page is reloaded?

This is quite simple: In Vue.js we make use of the ViewModel. If we are going to have a closer look to the checkbox, we immediately find something we do not know from common HTML:

<input id="form-restrict" type="checkbox" v-model="config.restrict">

Did you see it? The attribute v-model="config.restrict" is nothing we knew before. But it allows us to bind some data to a form element. So this element's value is bind to the content of config.restrict. But where does this come from?

Adding module configuration

Pagekit allows us to store the module configuration very easily. We do not even need to write a model, that allows us to store something to the database.

Just open the index.php and search for this part (remember?):

'config' => [],

Now we are going to extend this just like that:

    'config' => [
        'restrict' => true
    ],

That's it. This is our module configuration, which comes with a default value (true) for restrict.

Alright - now we have set up the module configuration, but how do we pass this value to the view?

The controller

As we know (remember?), the controller does things. It saves, loads, redirects, ... . And of course our controller takes care of routing.

At first let's create our controller in src/Controller/. We are going to name it SurveyController.php, so the full path is src/Controller/SurveyController.php.

Phpstorm add file controller

Now we can create our source code:

<?php

namespace Spqr\Survey\Controller;

use Pagekit\Application as App;

/**
 * @Access(admin=true)
 * @return string
 */
class SurveyController
{
    /**
     * @Access("survey: manage surveys")
     */
    public function settingsAction()
    {
        return [
            '$view' => [
                'title' => __( 'Survey Settings' ),
                'name'  => 'spqr/survey:views/admin/settings.php'
            ],
            '$data' => [
                'config' => App::module( 'spqr/survey' )->config()
            ]
        ];
    }

}

Alright - a few words on this.

At first: We are namespacing (remember?) this class with namespace Spqr\Survey\Controller;. In your case you would choose another vendor name (e.g. namespace Acme\Survey\Controller;).

As we need to load the module configuration from the Pagekit application, we need to include it with the use statement: use Pagekit\Application as App;.

The controller is going to create a route (I'll tell you more about this later), which is calling the method settingsAction. This method returns a simple array, which is processed by Pagekit.

This array includes two elements with special keys: $view and $data.

The content of $view is used by Pagekit to actually get information about the view to render. We need to specify the name of the view - or, to be precise, the path of this file.

The view should also get a title. In this case we name it "Survey Settings". As we also want this title to be translated automatically, we are using the translate-syntax Pagekit provides for PHP-files:

'title' => __( 'Survey Settings' ),

In PHP we can translate strings just like this: __('string'). Remember that we did this in our view, too? But as our view contains HTML/Vue.js, we had to use {{ 'string' | trans }} to translate our contents.

We also want to pass some data to our view. And this is what the second part of the returned array is about:

'$data' => [
                'config' => App::module( 'spqr/survey' )->config()
            ]

So we add an entry called config to the $data array. The config entry contains the module configuration, which can be fetched using App::module( 'spqr/survey' )->config().

Do you remember? We included the Pagekit application to our controller using use Pagekit\Application as App; So App is an alias of Pagekit\Application in this case.

Annotations

As you might noticed, we are using some annotations. Well, they are often used to explain, what the code actually does, what a method returns or which parameters are expected.

Pagekit can do a lot more.

Our class is annotated with

/**
 * @Access(admin=true)
 * @return string
 */

The annotation @Access(admin=true) makes sure that only members with administrator privileges are allowed to call this class.

It also takes care of the routing. All controllers, that have this annotation, are callable from a admin route like /admin/extension/....

The settingsAction class also has an annotation:

    /**
     * @Access("survey: manage surveys")
     */

So this method is only callable by users, who have the right to "manage surveys". But where does this come from?

Add permissions

This is pretty easy: Pagekit provides a great possibility to manage, which users or usergroups are allowed to do something.

You may find these settings at Users > Permissions / Users > Roles in the Pagekit backend.

To add our own extension's permissions, we only have to edit the module definition (index.php).

Just have a look at the permissions line (remember?)

    'permissions' => [],

Now we can add our permissions:

    'permissions' => [
        'permissions' => [
            'survey: manage settings' => [
                'title' => 'Manage settings'
            ]
        ],
    ],

That's it. Very cool, huh? So - in conclusion - only users, who have the permission survey: manage settings are allowed to access the "settings" view. And we can easily assign this permission to our usergroups using Pagekit's built-in user management. But at the moment we do not need to do anything, as the "administrator" role always has all permissions by default.

Bringing things together

We are almost done. Now we need to bring things together. We somehow have to use the data the controller passes to our view ($data) in our view by binding it to the ViewModel. This ViewModel provides the possibility to easily bind the data to our checkbox.

At first we are going to create a new file: app/views/admin/settings.js.

Phpstorm add file javascript backend

Now we will create our first Vue.js instance:

window.settings = {

    el: '#settings',

    data: {
        config: $data.config
    },

    methods: {
        save: function () {
            this.$http.post('admin/survey/save', {config: this.config}, function () {
                this.$notify('Settings saved.');
            }).error(function (data) {
                this.$notify(data, 'danger');
            });
        }
    },
    components: {}
};

Vue.ready(window.settings);

Let me quickly explain, what this means.

We are creating a instance of Vue.js, which binds to an element with the id #settings. All content, which is written inside of the tag with this id are being processed by Vue.js.

Let's go back to our view:

<?php $view->script( 'settings', 'spqr/survey:app/bundle/settings.js', [ 'vue' ] ); ?>

<div id="settings" class="uk-form uk-form-horizontal" v-cloak>
    <div class="uk-grid pk-grid-large" data-uk-grid-margin>
        <div class="pk-width-sidebar">
            <div class="uk-panel">
                <ul class="uk-nav uk-nav-side pk-nav-large" data-uk-tab="{ connect: '#tab-content' }">
                    <li><a><i class="pk-icon-large-settings uk-margin-right"></i> {{ 'General' | trans }}</a></li>
                </ul>
            </div>
        </div>
        <div class="pk-width-content">
            <ul id="tab-content" class="uk-switcher uk-margin">
                <li>
                    <div class="uk-margin uk-flex uk-flex-space-between uk-flex-wrap" data-uk-margin>
                        <div data-uk-margin>
                            <h2 class="uk-margin-remove">{{ 'General' | trans }}</h2>
                        </div>
                        <div data-uk-margin>
                            <button class="uk-button uk-button-primary" @click.prevent="save">{{ 'Save' | trans }}
                            </button>
                        </div>
                    </div>
                    <div class="uk-form-row">
                        <label for="form-restrict" class="uk-form-label">{{ 'Restrict to one vote per IP' |
                            trans }}</label>
                        <div class="uk-form-controls uk-form-controls-text">
                            <input id="form-restrict" type="checkbox" v-model="config.restrict">
                        </div>
                    </div>
                </li>
            </ul>
        </div>
    </div>
</div>

There it is:

<div id="settings" class="uk-form uk-form-horizontal" v-cloak>

So everything between the

<div id="settings" class="uk-form uk-form-horizontal" v-cloak>

tag and it's closing </div>-tag is being processed by Vue.js.

Our Vue.js instance also contains some data:

    data: {
        config: $data.config
    },

There the magic happens: As you see, we create an entry called config, which gets it's values from the special key $data, our controller provided.

We also added a save-method, which is being executed if the user clicks the "Save"-button:

                            <button class="uk-button uk-button-primary" @click.prevent="save">{{ 'Save' | trans }}
                            </button>

Do you see the @click.prevent="save" attribute? This tells Vue.js to call the save-method, but prevent the page from being reloaded.

So on clicking the "Save"-button this method is being executed:

        save: function () {
            this.$http.post('admin/survey/save', {config: this.config}, function () {
                this.$notify('Settings saved.');
            }).error(function (data) {
                this.$notify(data, 'danger');
            });
        }

And this is quite simple: This method does a simple HTTP-POST to the admin/survey/save route and posts some content (our config: config: this.config), which is going to be saved.

Of course we need to create such a route in our controller. So switch over to src/Controller/SurveyController.php and simply add

    /**
     * @Request({"config": "array"}, csrf=true)
     * @param array $config
     *
     * @return array
     */
    public function saveAction( $config = [] )
    {
        App::config()->set( 'spqr/survey', $config );

        return [ 'message' => 'success' ];
    }

So the controller should look like this:

<?php

namespace Spqr\Survey\Controller;

use Pagekit\Application as App;

/**
 * @Access(admin=true)
 * @return string
 */
class SurveyController
{
    /**
     * @Access("survey: manage surveys")
     */
    public function settingsAction()
    {
        return [
            '$view' => [
                'title' => __( 'Survey Settings' ),
                'name'  => 'spqr/survey:views/admin/settings.php'
            ],
            '$data' => [
                'config' => App::module( 'spqr/survey' )->config()
            ]
        ];
    }

    /**
     * @Request({"config": "array"}, csrf=true)
     * @param array $config
     *
     * @return array
     */
    public function saveAction( $config = [] )
    {
        App::config()->set( 'spqr/survey', $config );

        return [ 'message' => 'success' ];
    }

}

As you might noticed we have an annotation here, too:

    /**
     * @Request({"config": "array"}, csrf=true)
     * @param array $config
     *
     * @return array
     */

So we expect a request containing an array. We also added a csrf check to prevent cross site scripting issues (more information).

So this method saves the submitted values to the module configuration:

App::config()->set( 'spqr/survey', $config );

As you can see, we can save the configuration easily by just using App::config()->set(); just add the name of the extension as the first parameter - and the configuration you want to save as second.

This controller simply returns a "success"-message: return [ 'message' => 'success' ];.

Routing

Now we need to take care of the routing. Just open your module definition (index.php) and search for

    'routes' => [],

Just extend it to

'routes'  => [
        '/survey'     => [
            'name'       => '@survey',
            'controller' => [
                'Spqr\\Survey\\Controller\\SurveyController'
            ]
        ]
    ],

So we are creating a route called "/survey" and name it. We are naming it @survey and add an array of controllers to it:

'controller' => [
                'Spqr\\Survey\\Controller\\SurveyController'
            ]

Giving the route a name like @survey can be very convenient, as Pagekit can resolve routes by their name. That's pretty handy as we will see later on.

Adding a menu

We are almost done - but wouldn't it be cool to have an own menu entry for our extension? This can also be done in our module definition (index.php).

Just search for this line:

    'menu' => [],

and extend it like this:

    'menu' => [
        'survey'           => [
            'label'  => 'Survey',
            'url'    => '@survey',
            'active' => '@survey(/*)?',
            'icon'   => 'spqr/survey:icon.svg'
        ],
        'survey: settings' => [
            'parent' => 'survey',
            'label'  => 'Settings',
            'url'    => '@survey/settings',
            'access' => 'survey: manage settings'
        ]
    ],

This is going to add a menu entry in Pagekit's backend for our extension.

As we do not have something else than a "settings"-view, we should redirect the route @survey to @survey/settings.

So switch over to your controller (src/Controller/SurveyController.php) and add this method:

    /**
     * @return mixed
     */
    public function indexAction()
    {
        return App::response()->redirect( '@survey/settings' );
    }

Now your controller should contain:

<?php

namespace Spqr\Survey\Controller;

use Pagekit\Application as App;

/**
 * @Access(admin=true)
 * @return string
 */
class SurveyController
{

    /**
     * @return mixed
     */
    public function indexAction()
    {
        return App::response()->redirect( '@survey/settings' );
    }

    /**
     * @Access("survey: manage surveys")
     */
    public function settingsAction()
    {
        return [
            '$view' => [
                'title' => __( 'Survey Settings' ),
                'name'  => 'spqr/survey:views/admin/settings.php'
            ],
            '$data' => [
                'config' => App::module( 'spqr/survey' )->config()
            ]
        ];
    }

    /**
     * @Request({"config": "array"}, csrf=true)
     * @param array $config
     *
     * @return array
     */
    public function saveAction( $config = [] )
    {
        App::config()->set( 'spqr/survey', $config );

        return [ 'message' => 'success' ];
    }

}

This method takes care that all requests to @survey are redirected to @survey/settings. We will replace this later, so this is some kind of a placeholder now.

Bundle things with Webpack

As you are a very observant reader you mentioned, that we created our Vue.js instance in app/views/admin/settings.js - but our view loads from app/bundle/settings.js, as we defined it in

<?php $view->script( 'settings', 'spqr/survey:app/bundle/settings.js', [ 'vue' ] ); ?>

Of course we could just load app/views/admin/settings.js in our view - but as we are going to use some components later, we will start bundling things with Webpack. Webpack is able to bundle JavaScript/Vue.js to small files. We are going to use Webpack later to create additional sections to our views.

As you already prepared your computer (remember?), we can go straight forward to work with Webpack.

Installing packages

Just create a file called package.json in the root-directory of your extension.

{
  "name": "spqr-survey",
  "scripts": {
    "archive": "webpack -p && composer archive --format=zip"
  },
  "devDependencies": {
    "babel-core": "^6.23.1",
    "babel-loader": "^6.1.0",
    "babel-plugin-transform-runtime": "^6.1.2",
    "babel-preset-env": "^1.1.8",
    "babel-preset-es2015": "^6.22.0",
    "babel-runtime": "^5.8.0",
    "vue-hot-reload-api": "^1.2.0",
    "vue-html-loader": "^1.0.0",
    "vue-loader": "^8.2.0",
    "webpack": "^1.12.9"
  }
}

As always, please choose another vendor prefix in the first line: "name": "spqr-survey".

Phpstorm packages

Now just open a terminal window. In PHPStorm you find a handy built-in terminal at the bottom.

Phpstorm terminal

Now make sure that you are in the correct directory using the pwd command.

Phpstorm terminal pwd

Alright. If you are not in the right directory, just change directory using cd /path/to/your/extension.

Now we are going to run npm install.

Phpstorm terminal npm install

This will install all packages we need - this can take a while.

When the installation is done, we can continue with the configuration of webpack.

Phpstorm terminal npm install done

Configuring Webpack

Create a file called webpack.config.js in the root-directory of your extension and insert the following configuration:

module.exports = [
    {
        entry: {
            "settings": "./app/views/admin/settings.js"
        },
        output: {
            filename: "./app/bundle/[name].js"
        },
        module: {
            loaders: [
                {test: /\.vue$/, loader: "vue"},
                {test: /\.js$/, exclude: /node_modules/, loader: "babel-loader"}
            ]
        }
    }
];

Phpstorm webpack configuration

Now open the terminal again and run webpack --optimize-minimize.

Now Webpack automatically creates the folder app/bundle and inserts settings.js.

Phpstorm webpack run

Important: If you change something inside app/views/admin/settings.js you need to run webpack --optimize-minimize again. You also can run webpack --watch - in this case Webpack watches for changes automatically.

Deploy the extension

We are ready to rumble - as we are developing our extension in a separated directory, we now can deploy our extension to our Build directory.

We should exclude some files and folders - for reasons ;)

In PHPStorm we can easily set up a deployment. Just open Tools > Deployment > Configuration.

Phpstorm deployment

Phpstorm deployment configuration

Now click on "Add" to add a new deployment.

Phpstorm deployment add

You can now choose where to deploy your extension. I am doing a simple local deployment.

Phpstorm deployment local

Now click on the "three points-button" to choose the deployment folder.

Phpstorm choose deployment folder

In my case I had to create a new folder.

Phpstorm deployment create folder

Now switch over to the "mappings"-tab and add a relative path. In my case it's just / as I would like to use the path I created.

Phpstorm deployment mappings

Now we continue to the most important step: Exclude some paths we need in our development environment, but for sure not in our extension. The node_modules folder is really large and we need this folder only for development.

So switch to the "Excluded Paths"-tab and add some paths by clicking the "Add local path"-button.

I am excluding:

  • /path/to/pagekit-survey/node_modules - As it's only relevant for development
  • /path/to/pagekit-survey/app/views - As it contains the settings.js, which is processed by Webpack. We only need the files in /path/to/pagekit-survey/app/bundle .
  • /path/to/pagekit-survey/vendor - This folder does not exist yet, but it is going to contain all the Composer dependencies, which are resolved by Pagekit's installer, so we do not need to attach them to our package.
  • /path/to/pagekit-survey/.git - As I do not want to add all the git files to my package.
  • /path/to/pagekit-survey/.idea - As this contains the PHPStorm configuration

Phpstorm deployment excluded paths

Now I can simply deploy all my work to the build directory I specified by right-clicking on my projet's file overview and choose "Upload to local".

Phpstorm deploy to local

You now should see all your files in your build-folder - except the folders we excluded in the deployment configuration.

Build folder

Now we need to create an archive of it:

Create archive

Archive

Now you can simply install the extension using Pagekit's installer.

Installation 1

Installation 2

Installation 3

Installation 4

Installation 5

Once you enabled the component you can find it in Pagekit's menu.

Pagekit menu

As you see - our icon could not be found. That doesn't matter - we are going to create it later.

The thing that matters: Our extension works. You can set a value in the settings view and save it.

Extension settings

Extension settings saved

That's it for today

Wow, we did a lot today. So take cup of coffee, enjoy the sun or just meet frieds. This was enough reading and coding for today ;)

Stay tuned: I will continue this series in a few days.

{{ message }}

{{ 'Comments are closed.' | trans }}

Wait a second!

Did you know that there's are great support channels for Pagekit? Visit us today and sign up for free using email or your GitHub-, Twitter-, Google- or Facebook-Account.

Latest blog posts

Latest comments

  • How to fork a theme

  • spqr/toc updated

  • spqr/toc updated

Like my work?

If you would like to support my work, you are invited to do so.