TODO: DRUPAL 8 + VUE.JS - REUSABLE COMPONENTS


Is there a simplest way to put them together without an headache? There are cases where you need to improve your UX, then you might think on include a powerful javascript framework, however it seems that you need to install some extra pieces of software to just get started, even tho if you only want to include one single feature.

As a backend guy, sometimes to include this kind of new feature would take your time in order to setup everything regarding with theming bases, however this article is about to plug and play, plug-in Vue.js in a simplest way then play with Drupal 8 to create your new cool feature within a custom module without need to get out of module scope.

Before get started?

In order to have everything ready to get started, I usually recomment to setup your local enviroment by using what you feel more familar, in my case, I choose to use a virtual-machine to host my local environment, Drupal VM (A VM for Drupal, built with Ansible), I won't explain how to setup it however it's pretty straightforward,

drupalvm

It will provide you everything what you need, as an example Drupal Console (The Drupal CLI. A tool to generate boilerplate code, interact with and debug Drupal), since we go to run some commands to make some stuff done.

drupalconsole

 

At last but not least, a contributed module named REST UI (A user interface for configuring Drupal 8's REST module) to manage a rest resource that we are about to create,

restui

 

So far, it seems that we're ready to go!

 

 

Let's code!

Once DrupalVM is up and running, we have a fresh Drupal 8 installation where we go to create our TODO List by using Vue.js.

 

 

Install contribute module

We are about to use Drupal Console to download and install RestUI module.

drupal module:install restui
drupalconsole-install-restui

Have you heard about "The power of one line of code?" kinda sorta, it's pretty much everything what we need to download and install that contribute module.

 

 

Generate Drupal custom module

We need to create a new custom module, then let's use Drupal Console for that task,

drupal generate:module --module vue_todo
generate-module

It might looks crazy but it is not, those questions are configurations to define what we want to, so far, we only have one file, vue_info.yml which's our module heart to work with Drupal.

 

 

Generate Block plugin

Next step we need to generate a block plugin to expose our Vue.js component,

drupal generate:plugin:block --module=vue_todo
drupal-generate-block

It seems a simple block plugin, and actually it is, a raw block plugin where we need to expose our Vue application.

 

 

Generate Rest Resource

Firstly we need to create a new Rest resource plugin within our new custom module, it can be easily achieve by executing a Drupal Console command,

drupal generate:plugin:rest:resource --module vue_todo
rest-resource

Some importans questions regarding to what resource type we need, then it will be created properly.

 

 

Define new libraries

Based on Drupal documentation about "Adding stylesheets (CSS) and JavaScript (JS) to a Drupal 8 module" we need to include a new file named "vue_todo.libraries.yml" at root of our custom module,

axios:
  remote: https://github.com/axios/axios
  version: "v2.5.16"
  license:
    name: MIT
    url: https://github.com/axios/axios/blob/master/README.md
    gpl-compatible: true
  js:
    //unpkg.com/axios/dist/axios.min.js: { type: external, minified: true }

vue-js:
  remote: https://github.com/vuejs/vue
  version: "v2.5.16"
  license:
    name: MIT
    url: https://github.com/vuejs/vue/blob/dev/README.md
    gpl-compatible: true
  js:
    //cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js: { type: external, minified: true }

vue-todo:
  js:
    js/vue-todo.js: {}
  css:
    component:
      css/vue-todo.css: {}
  dependencies:
    - vue_todo/axios
    - vue_todo/vue-js

As you might noticed, we are defining three entries, those are the following ones:

  • axios

There're different ways to consume an API but we take this approach in first place, because Vue.js already adopt it, there's more information into Vue.js Cookbook chapter the chapter named Using Axios to Consume APIs. Instead of download the file and store it locally, we are using given CDN (Check install secion on readme about axios)

  • vue-js

Vue provides us options to install this framework, reason why we bet to same approach by using their CDN.

  • vue-todo

At last but not least, we are defining where our custom javascript file will go to live, then we need to create a folder named "js" at root of our custom module and put inside a `vue-todo.js` file. This file should looks like this:

var markup = `
    <div id="app" class="vue-todo-container">

      <div>
        <span>New:</span><input v-model="newItem.text" name="text" type="text" />
        <span>Due:</span><input v-model="newItem.due" name="due" type="date" />
        <button v-on:click="addNew">Add</button>
      </div>

      <hr/>

      <div v-for="(todo, idx) in todos" class="vue-todo-margins">

        <div class="vue-todo-items">

          <h4 class="vue-todo-margins vue-todo-inline">
            <strike v-if="todo.completed">{{todo.text}}</strike>
            <span v-else>{{todo.text}}</span>
          </h4>

          <p class="vue-todo-inline">
            <small>Due: {{todo.due}}</small>
          </p>

          <div class="vue-todo-inline">
            <button v-if="!todo.completed"
                    v-on:click="done(idx)">
                    Done
            </button>
            <button v-else
                    v-on:click="undone(idx)">
                    Undo
            </button>
            <a v-on:click="remove(idx)" href='#'>
              <span>&times;</span>
            </a>
          </div>

        </div>

      </div>

    </div>
`;

var app = new Vue({
    el: '#app',
    data: {
        newItem: {
            text: null,
            due: null,
            completed: false
        },
        todos: null,
        csrfToken: null
    },
    template: markup,
    methods: {
        addNew: function () {
            //
            app.todos.push(app.newItem);
            //
            app.newItem = {
                text: null,
                due: null,
                completed: false
            };
        },
        done: (idx) => {
            app.todos[idx].completed = true;
            app.update(app.todos);
        },
        undone: (idx) => {
            app.todos[idx].completed = false;
            app.update(app.todos);
        },
        remove: (idx) => {
            //
            app.todos = app.todos.filter((item, key) => {
                return key !== idx
            });
        },
        update: (data) => {
            //
            if(null === app.csrfToken) {
                return;
            }
            //
            axios.put('/api/vue/todo?_format=json', data, {
                headers: {
                    'X-CSRF-Token': app.csrfToken,
                    'Content-Type': 'application/json'
                }
            }).then(function (response) {
                console.log(response.data);
            }).catch(function (error) {
                console.error(error);
            });
        }
    },
    watch: {
        todos: (newTodos) => (app.update(newTodos))
    },
    mounted() {
        //
        axios.get('/api/vue/todo?_format=json')
            .then(response => (this.todos = response.data));
        //
        axios.get('/rest/session/token')
            .then(response => (this.csrfToken = response.data));
    }
});

Let's break it down, into pieces, firstly we're creating our custom template (it is just mixed HTML and vue declaratives) in order to have more flexibility, then we assign it to a variable named `markup`, then we create a new vue component, where we're defining different attributes:

  • el
  • data
  • template
  • methods
  • watch
  • mounted

Also it defined a css section which will load a style-sheet file when this library is attached, then we need to create a folder named: `css` at root of project with a file named `vue-todo.css`, it will looks like this,

.vue-todo-container {
    text-align: left;
    width: 75%;
}

.vue-todo-margins {
    margin-left: 50px;
    margin-bottom: 15px;
}

.vue-todo-items {
    width: 50%;
    border: 1px solid black;
    border-radius: 50px 20px;
}

.vue-todo-inline {
    display: inline-block;
}

 

 

Since we defined `vue_todo/vue` and `vue_todo/axios` as dependencies Drupal is smart enough to load them properly then they will be available when our custom javascript file is executed.

 

Setup Block plugin

A block plugin is an excelent place to setup our entry point of this new vue component, basically we need to create a new markup element with an HTML element that match with `Vue.el` attribute since it will be used by Vue Framework to mount our new component, also we must to attach our custom library that will execute all magic for us, it should looks like this,

<?php

namespace Drupal\vue_todo\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'VueTODOBlock' block.
 *
 * @Block(
 *  id = "vue_todo_block",
 *  admin_label = @Translation("Vue TODO"),
 * )
 */
class VueTODOBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    //
    return [
      'vue_todo_block' => [
        '#markup'   => '<div id="app"></div>',
        '#attached' => [
          'library' => [
            'vue_todo/vue-todo',
          ],
        ],
      ],
    ];
  }
}

 

 

Setup Rest Resource

Next thing, defines what our Rest resource has to do and how, we want to make the information persistent, for this example, we are using Drupal 8 State API then we are including new attribute to our `VueTODORestResource` class and by dependency inject into constructor method. Also we are defning two method which will represent HTTP methods, first one named `get` will retrieve anything information stored into state "vue-todo" key where explicit we're removing whole cache, and the second one  named `put` where we're updating and making persistent data into state "vue-todo" key; whole file should looks like this,

<?php

namespace Drupal\vue_todo\Plugin\rest\resource;

use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\State\StateInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Provides a resource to get view modes by entity and bundle.
 *
 * @RestResource(
 *   id = "vue_todo_rest_resource",
 *   label = @Translation("Vue TODO Rest Resource"),
 *   uri_paths = {
 *     "canonical" = "/api/vue/todo",
 *     "update" = "/api/vue/todo"
 *   }
 * )
 */
class VueTODORestResource extends ResourceBase {

  /**
   * A current user instance.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Drupal\Core\State\StateInterface definition.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * Constructs a new VueTODORestResource object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param array $serializer_formats
   *   The available serialization formats.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   A current user instance.
   * @param \Drupal\Core\State\StateInterface $state
   *   Drupal site current state
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    array $serializer_formats,
    LoggerInterface $logger,
    AccountProxyInterface $current_user,
    StateInterface $state
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition,
      $serializer_formats, $logger);

    $this->currentUser = $current_user;
    $this->state = $state;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition
  ) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->getParameter('serializer.formats'),
      $container->get('logger.factory')->get('vue_todo'),
      $container->get('current_user'),
      $container->get('state')
    );
  }

  /**
   * Responds to GET requests.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The HTTP response object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   *   Throws exception expected.
   */
  public function get() {

    // You must to implement the logic of your REST Resource here.
    // Use current user after pass authentication to validate access.
    if (!$this->currentUser->hasPermission('access content')) {
      throw new AccessDeniedHttpException();
    }

    $dependency = [
      '#cache' => [
        'max-age' => 0,
      ],
    ];
    $data = $this->state->get('vue-todo');
    return (new ResourceResponse($data))->addCacheableDependency($dependency);
  }

  /**
   * Responds to PUT requests.
   *
   * @param array $data
   *   Given data to store into vue-todo state.
   *
   * @return \Drupal\rest\ResourceResponse
   *   The HTTP response object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   *   Throws exception expected.
   */
  public function put($data) {
    //
    $this->state->set('vue-todo', $data ?? []);
    return new ResourceResponse(['saved' => TRUE]);
  }
}

 

 

 

Final stage

Last scene is to enable our custom module, it is done by executing a similar Drupal console commands as we did with Rest UI module, by using our custom module machine name,

drupal module:install vue_todo
drupal-install-vue-todo

Once it was installed, we need to browser to Rest UI admin page (/admin/config/services/rest) where we need to find our custom Rest resource and enabled it, finally we enabled both methods (GET and PUT), enabled JSON request format and allow `cookie` as Authentication provider.

rest-resource-settings

 

It should be ready to see our new feature up and running, we only need to place our custom block at some region (/admin/structure/block),  we might defined as "featured top" section also it will be available only at `<front>` page,

block-settings

Once we go back to front page, it should be shows up there, keep in mind that our Rest resource is available only to users already logged in,

vue-todo-component

Now you're able to include items to your Vue TODO list,

vue-todo-in-action

 

 

Conclusion

Now you are able to use that block across wide site when you need it, I hope it helps, this article was created because of DRUPAL 8 + VUE.JS - REUSABLE COMPONENTS session at DrupalCampCR 2018,

Happy coding!

 

PD