Welcome to Abdul Malik Ikhsan's Blog

Using Vuex’s Vue.js and sessionStorage combo for searchable get api data and cached in Mezzio Application

Posted in Javascript, Mezzio, Tutorial PHP, Vue.js by samsonasik on June 13, 2020

So, this is the 3rd post about usage of Vue.js in Mezzio Application. If you haven’t read my previous 2 posts, I suggest you to read them first:

Ok, let’s continue. Now, we are going to use Vuex as state management (when without refresh) and native sessionStorage combo to handle searched data in next refresh to avoid unnecessary re-query data as previously already searched. For note, I use sessionStorage so next close – re-open browser will clear the data.

Load the Vuex Library

We can load Vuex library in the layout:

<?php
// src/App/templates/layout/default.phtml
// ...
    ->prependFile('/js/app.js')
    ->prependFile('https://unpkg.com/vuex@3.4.0/dist/vuex.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')
// ...

The Data

For example, we want to display portfolio data via API. For example, we have the following portfolio array data example (in real life, you an use DB ofcourse)

<?php
// data/portfolio.php
return [
    [
        'id'    => 1,
        'title' => 'Website A',
        'image' => 'https://via.placeholder.com/150/FF0000/FFFFFF?text=website%20A',
        'link'  => 'https://www.website-a.com',
    ],
    [
        'id'    => 2,
        'title' => 'Website B',
        'image' => 'https://via.placeholder.com/150/0000FF/808080?text=website%20B',
        'link'  => 'https://www.website-b.com',
    ],
    [
        'id'    => 3,
        'title' => 'Website C',
        'image' => 'https://via.placeholder.com/150/000000/FFFFFF?text=website%20C',
        'link'  => 'https://www.website-c.com',
    ]
];

If you use GIT with mezzio skeleton, the data need to be registered to .gitignore to allow to be committed:

# data/.gitignore
*
!cache
!cache/.gitkeep
!.gitignore
!portfolio.php

The API

Now, time to create API page, for example App\Handler\Api\PortfolioApiHandler:

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

namespace App\Handler\Api;

use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class PortfolioApiHandler implements RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $data  = include './data/portfolio.php';
        $keyword = $request->getQueryParams()['keyword'] ?? '';

        if ($keyword) {
            $data = array_filter($data, function ($value) use ($keyword) {
                return (
                    strpos(strtolower($value['title']), strtolower($keyword)) !== false
                    ||
                    strpos(strtolower($value['link']), strtolower($keyword)) !== false
                );
            });
        }

        return new JsonResponse($data);
    }
}

Above, we use array_filter to search portfolio data for title and link with keyword query parameter.

Next, we can register to our 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\Api\PortfolioApiHandler::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the routes:

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

The Page

We need to consume the API via a page, for example, we create handle for it: App\Handler\PortfolioPageHandler:

<?php
// src/App/src/Handler/PortfolioPageHandler.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 PortfolioPageHandler 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::portfolio-page'));
    }
}

Next, we can register to our 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\Api\PortfolioApiHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\PortfolioPageHandler::class    => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/api/portfolio', App\Handler\Api\PortfolioApiHandler::class, 'api-portfolio');
    $app->get('/portfolio', App\Handler\PortfolioPageHandler::class, 'portfolio');
};

The Template

For view, we need to display portfolio data with allow to search by keyword via input text. We can create view as follow:

<!-- src/App/templates/app/portfolio-page.phtml-->
Keyword: <input type="keyword" id="keyword" v-on:input="this.$parent.search" v-on:focus="this.$parent.search"/> <br /><br />

<table class="table">
	<tr>
		<th>Title</th>
		<th>Image</th>
        <th>Link</th>
	</tr>

    <tr v-if="this.$parent.portfolio.length == 0">
        <td colspan="3" class="text-center">No portfolio found.</td>
    </tr>

    <tr v-for="loop in this.$parent.portfolio" :key="loop.id">
        <td>{{ loop.title }}</td>
        <td><img :src="`${ loop.image }`" /></td>
        <td><a v-bind:href="`${ loop.link }`">{{ loop.link }}</a></td>
    </tr>
</table>

<script type="application/javascript">
const store = new Vuex.Store({
    state: {
        portfolio : {}
    },
    mutations: {
        search (state, data) {
            sessionStorage.setItem('search-' + data.keyword, JSON.stringify(data.value));
            state.portfolio[data.keyword] = data.value;
        }
    }
});

document.querySelector('#keyword').focus();
</script>

In Vue.js template, we can fill JavaScript with “application/javascript” script type. Above, I initialize Vuex’s Store instance with definition of portfolio state data that on search mutation, set stringified object data.value into sessionStorage item based on keyword and fill the state.portfolio[data.keyword] with data.value. We will fill data as keyword and value later in the public/js/app.js. On very first page accessed, we set focus to keyword text field that trigger search function we register in public/js/app.js when define portfolio page component. So, we can finally loop the data searched.

The JavaScript

In public/js/app.js, now, we can portfolio component with the following defintion:

const routes = [
    // ... other page definition here ...
    {
        path: '/portfolio',
        component: createPage(
            'portfolio',
            {
                portfolio : []
            },
            {
                search: function (e) {
                    let keyword = e.target.value;

                    if (typeof store.state.portfolio[keyword] !== 'undefined') {
                        this.portfolio = store.state.portfolio[keyword];

                        return;
                    }

                    if (sessionStorage.getItem('search-' + keyword)) {
                        portfolio     = JSON.parse(sessionStorage.getItem('search-' + keyword));
                        store.commit('search', { keyword: keyword, value: portfolio });
                        this.portfolio = portfolio;

                        return;
                    }

                    (async () => {
                        await new Promise( (resolve) => {
                            fetch(
                                '/api/portfolio?keyword=' + keyword,
                                {
                                    method: 'GET',
                                    headers: {
                                        'Accept': 'application/json',
                                    }
                                }
                            ).then(response =>  resolve(response.json()));
                        }).then(result => this.portfolio = result);

                        store.commit('search', { keyword: keyword, value: this.portfolio });
                    })();
                }
            }
        ),
        meta: {
            title: 'My Portfolio'
        }
    }
];

Above, in definition of portfolio component, we define a portfolio data attribute to empty array. On search function (that we know it triggered in template input focus and input event), we have the following flow:

a. get keyword from e.target.value as keyword input value
b. check if Vuex store.state.portfolio[keyword] not undefined, means it already in Vuex stage, then fill portfolio data attribute with it, then return early.
c. check if there is session storage data with item key “search-” + keyword value, means it already in session storage, then fill portfolio data attribute with its parsed to object from json stringified data, then return early.
d. otherwise, use async/await function to fill portfolio data attribute, and then commit to Vuex store.

Last but not least, add link to /portfolio page in the layout:

<div class="collapse navbar-collapse" id="navbarCollapse">
    <!-- other menu here -->

    <li class="nav-item">
        <router-link to="/portfolio" class="nav-link">Portfolio</router-link>
    </li>

</div>

That’s it, now we have fully functional searchable and cached even on refresh, unless browser is closed and re-open.

I published the code at https://github.com/samsonasik/mezzio-vue if you want to give it a try 😉

Refs:

One Response

Subscribe to comments with RSS.

  1. […] in previous JavaScript posts, I posted how to use Vue.js in Mezzio Application. Now, in this post, I will show you how to use […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: