Welcome to Abdul Malik Ikhsan's Blog

Using Vue.js in Mezzio Application

Posted in Javascript, Mezzio, Tutorial PHP, Vue.js by samsonasik on May 30, 2020

So, another JavaScript post! If you read my post at 2015 about Ember.js usage in Zend Framework 2 application, now let’s try with Vue.js, but for Mezzio application.

Let’s start with download the mezzio skeleton:

composer create-project mezzio/mezzio-skeleton mezzio-vue

I assume next you choose the following options:

  • Type of Installation: Modular (3)
  • Container: Laminas ServiceManager (3)
  • Router: Laminas Router (3)
  • Template Engine: Laminas View (3)

Now, we are on the same page!

The scenario is same, we want to create an SPA application. In Mezzio part, to make it work, it require template handling for ajax request.

We can create middleware for that:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class XMLHttpRequestTemplateMiddleware implements MiddlewareInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        if (in_array('XMLHttpRequest', $request->getHeader('X-Requested-With'), true)) {
            (function ($template) {
                $template->layout = false;
            })->bindTo($this->template, $this->template)($this->template);
        }

        return $handler->handle($request);
    }
}

In above middleware, we set template layout to false to disable layout when request has X-Requested-With = XmlHttpRequest as an ajax detection. Let’s register above middleware in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\XMLHttpRequestTemplateMiddleware::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the pipeline before DispatchMiddleware:

<?php
 // config/pipeline.php
use App\Middleware\XMLHttpRequestTemplateMiddleware;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    // ...
};

We need to handle 404 Pages that can work in Ajax request, so we can create a new middleware for that, for example: App\Middleware\NotFoundMiddleware:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NotFoundMiddleware implements MiddlewareInterface
{
    private $template;
    private $config;

    public function __construct(TemplateRendererInterface $template, array $config)
    {
        $this->template = $template;
        $this->config   = $config;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        return new HtmlResponse(
            $this->template->render($this->config['mezzio']['error_handler']['template_404'])
        );
    }
}

The above middleware need to be registered to ConfigProvider:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\NotFoundMiddleware::class  => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

And then, add to config/pipeline after DispatchMiddleware:

<?php
 // config/pipeline.php

use App\Middleware\NotFoundMiddleware;
use App\Middleware\XMLHttpRequestTemplateMiddleware;

return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    $app->pipe(NotFoundMiddleware::class);
    // ...
};

Now, let’s add About and Contact page handlers:

1. About Page

<?php
// src/App/src/Handler/AboutPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AboutPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::about-page'));
    }
}

With templates:

<!-- src/templates/app/about-page.phtml -->
<h1>About Me</h1>
<p>
    I'm a web developer.
</p>

2. Contact Page

<?php
// src/App/src/Handler/ContactPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ContactPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::contact-page'));
    }
}

With templates:

<!-- src/templates/app/contact-page.phtml -->
<h1>Contact Me</h1>
<p>
    You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a>
</p>

Handlers Registration

Both AbooutPageHandler and ContactPageHandler need to be registered in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\AboutPageHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\ContactPageHandler::class => ReflectionBasedAbstractFactory::class,
                // ...
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/about', App\Handler\AboutPageHandler::class, 'about');
    $app->get('/contact', App\Handler\ContactPageHandler::class, 'contact');
};

Now, we have 3 html pages: Home, About, and Contact. It’s enough for demonstration.

JS dependencies

We can register js dependencies in the layout by add the following js:

<?php
// src/App/templates/layout/default.phtml

$this->headLink()
    ->prependStylesheet('https://use.fontawesome.com/releases/v5.12.1/css/all.css')
    ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css');
$this->inlineScript()

    ->prependFile('/js/app.js')
    ->prependFile('https://unpkg.com/vue-router@3.3.2/dist/vue-router.js')
    ->prependFile('https://unpkg.com/vue@2.6.11/dist/vue.js')

    ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js')
    ->prependFile('https://code.jquery.com/jquery-3.5.0.min.js');
?>

Vue dependencies are Vue.js core and Vue Router for routing. For /js/app.js, that’s our code to make routing definition. In the layout, we need element for mounting, let’s define is as “root” under body:

<!-- // src/App/templates/layout/default.phtml -->
<body class="app">
    <div id="root">
        
    </div>
    <?=$this->inlineScript()?>
</body>

Now, we can define the router links inside “root” div:

<!-- // src/App/templates/layout/default.phtml -->
<div id="root">

        <header class="app-header">
            <nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top" role="navigation">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="#navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                            <span class="navbar-toggler-icon"></span>
                        </button>
                        <router-link to="/" class="navbar-brand"><img src="https://docs.laminas.dev/img/laminas-mezzio-rgb.svg" alt="Laminas Mezzio" height="56" /></router-link>
                    </div>
                    <div class="collapse navbar-collapse" id="navbarCollapse">
                        <ul class="navbar-nav mr-auto">
                            <li class="nav-item">
                                <router-link to="/" class="nav-link">Home</router-link>
                            </li>
                            <li class="nav-item">
                                <router-link to="/about" class="nav-link">About</router-link>
                            </li>
                            <li class="nav-item">
                                <router-link to="/contact" class="nav-link">Contact</router-link>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        </header>

</div>

Next, time for the view content placeholder, we can define router-view inside “root” div as well for place to collect the content:

<!-- // src/App/templates/layout/default.phtml -->
<div id="root">
        <header class="app-header"> <!-- content app header before --> </header>

        <main class="container">
            <div id="app">
                <keep-alive> <!-- make content cached if already loaded -->
                    <router-view></router-view>
                </keep-alive>
            </div>
        </main>

        <!-- footer here -->
</div>

Now, our JS file in public/js/app.js for routing definition:

createPage = (name) => {
    return Vue.component('page-' + name, {
        data: () => {
            return  {
              content: 'Loading...'
            }
        },
        mounted () {
            (new Promise( (resolve) => {
                fetch(
                    this.$route.path,
                    {
                        method: 'GET',
                        headers: {
                            'X-Requested-With': 'XMLHttpRequest',
                        }
                    }
                ).then(response =>  resolve(response.text()));
            })).then(result => this.content = result);
        },
        template: '<div v-html="content"></div>'
    });
}

const routes = [
    { path: '/', component: createPage('home') },
    { path: '/about', component: createPage('about') },
    { path: '/contact', component: createPage('contact') },
    { path: '*', component: createPage('404') }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});
const app    = new Vue({router}).$mount('#root')

In above code, we create a page component on the fly via function createPage and apply to each path. Setup VueRouter with routes definition, and make a Vue instance with it, mount to div id #root.

Bonus

How about make page title changed after on change page? We can set meta title in each route definition, and use router.afterEach() to apply it:

// ...
const routes = [
	{ path: '/', component: createPage('home'),  meta: {
            title: 'Home'
        } },
	{ path: '/about', component: createPage('about'), meta: {
            title: 'About Me'
        } },
	{ path: '/contact', component: createPage('contact'), meta: {
            title: 'Contact Me'
        } },
    { path: '*', component: createPage('404'), meta: {
            title: '404 Not Found'
        } }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});

router.afterEach(to => document.title = to.meta.title);
// ...

Now, if we check, we will get SPA working:

That’s it! I uploaded the sample source code at github: https://github.com/samsonasik/mezzio-vue

References:
https://vuejs.org/v2/guide/
https://router.vuejs.org/guide/#html
https://medium.com/badr-interactive/mengenal-lifecycle-hooks-pada-vue-js-78cd2225a69
https://forum.vuejs.org/t/setting-a-correct-base-url-with-vue-router/24726/2
https://forum.vuejs.org/t/how-do-i-make-an-html-tag-inside-a-data-string-render-as-an-html-tag/13074/3