Vue in a legacy application

December 2018 - Stéphane Reiss

How we did it at Alayacare

Who am I?

  • Web developer
  • PHP background
  • I love javascript

Stéphane Reiss

  • Tech company providing software to home care agencies (B2B)
  • 5 years old
  • 133 employees  in 3 countries
    (Canada, US, Australia)
  • 87 clients in 4 continents
  • Daily usage: 7k care providers and 2k admins

 

P.S. We are hiring!

Legacy App

  • Monolith PHP app
  • 1 big database schema
  • jQuery everywhere
  • No tests
  • No documentation
<head>
    <script src="/scripts/files/jquery.js"></script>
    <script type="text/javascript" src="..."></script>
    // ... more files ...
    <script type="text/javascript" src="..."></script>    
</head>
<body>
    <div class="body_1">
        <div id="top">
            // ... more elements ...
            <div id="content" class="content-center-resize relative">
                <div class="primary-content"></div>
                <div class="secondary-content"></div>
            </div>
            // ... more elements ...
        </div>
    </div>
</body>
$(window).bind('hashchange', function(e) {

    var url = $.param.fragment();
    if (url == '') {
        return
    }
    Dash.loadContent(url);

})

ROUTING

PAGE LAYOUT

Game plan

  • Micro service architecture for the backend
  • Frontend needs modularity, maintainability
  • Two way data-binding

REQUIREJS + LESS + BOWER

First try

<head>
  //...
  <?php if (ENVIRONMENT == 'prod'): ?>

    <?= Ui::getVersionedCSSTag('/web/css/main_old.css'); ?>
    <?= Ui::getVersionedScriptTag('/web/js/built/old_app.js') ?>

  <?php else: ?>

    <link rel="stylesheet/less" type="text/css" href="/web/less/old_layout/main.less" />

    <script type="text/javascript">var less = less || {}; less.env='development';</script>
    <script src="/web/js/lib/vendor/less.min.js"></script>

    <script type="text/javascript" src="/web/js/lib/vendor/require.js"></script>
    <script type="text/javascript">
        require(['/web/js/config.js'], function() {
            require(['old_app']);
        });
    </script>

  <?php endif; ?>
  //...
</head>
define(['t3', 'vue'], (Box, Vue) => Box.Application.addModule('calendar-legend', () => {
  const el = 'calendar-legend';
  const props = ['statusList']; // [ { name }, { name }, ... ]
  const template = '#calendar-legend-template';

  return {
    init() {
      this.vue = new Vue({
        el,
        props,
        template,
      });
    },
    destroy() {
      this.vue.$destroy();
    },
  };
}));
<template id="calendar-legend-template">
    <div class="calendar-legend-container">
        <ul id="calendar-legend" class="calendar-legend">
            <li v-for="status in statusList"
                :class="status.cssClass">
                {{ status.ui_name }}
            </li>
        </ul>
    </div>
</template>

<div data-module="calendar-legend">
    <calendar-legend :status-list="<?= //... ?>"></calendar-legend>
</div>

PHP

T3 + VUEJS in a require module

Our first vue component

Let's start over!

<body>
    <div class="body_1">
        <div id="top">
            // ... more elements ...
            <div id="content" class="content-center-resize relative">
                <div class="primary-content"></div>
                <div class="secondary-content"></div>
            </div>
            // ... more elements ...
        </div>
    </div>



</body>

PAGE LAYOUT

<script type="text/javascript" src="/web/js/built/build.js"></script>

with webpack and Vue.js

module.exports = {
  entry: {
    build: './web/js/main.js',
    css: './web/less/main.less',
    old_css: './web/less/old_layout/main.less',
  },
  output: {
    path: path.resolve(__dirname, './web/js/built'),
    publicPath: '/web/js/built/',
    filename: '[name].js',
    hotUpdateChunkFilename: 'hot/hot-update.js',
    hotUpdateMainFilename: 'hot/hot-update.json',
  },
  //...
};

webpack.config.js

import Vue from 'vue';
import tagsDefined from './components-export';

let components = [];

observeDOMChanges();
scanNodeForVueComponent(document);


function observeDOMChanges() {...}

function scanNodeForVueComponent(node) {...}

function createVueComponent(node) {...}

function destroyVueComponent(node) {...}

function destroyChildren(vueInstance) {..}

main.js

new Vue({
  el: node,
  components: {
    [tagName]: tagsDefined[tagName],
  },
});
vueInstance.$destroy();
export default {'component-a': require('./<path>/component-a'), ... 'component-x': require('./<path>/component-x'), }
<div class="widget">
    <calendar-legend
        :status-list="<?= //... ?>"
    />
</div>

Backend response

  1. Detect DOM Change
  2. Scan for components
  3. Create Vue components
  4. Destroy removed components

Vuex

Allow cross components communication through a centralized state

import store from 'components/vuex/store';

//...
  new Vue({
    el: node,
    store,
    components: {
      [tagName]: tagsDefined[tagName],
    },
  });
//...

main.js

Vue router

Vue Router is the official router for Vue.js

$(window).bind('hashchange', function(e) {

    var url = $.param.fragment();
    if (url == '') {
        return
    }
    Dash.loadContent(url);

})

ROUTING

import store from 'components/vuex/store';
import router from 'components/router';

//...
  new Vue({
    el: node,
    store,
    router,
    components: {
      [tagName]: tagsDefined[tagName],
    },
  });
//...

main.js

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [
    {
      path: '*',
      component: LegacySection,
    }
  ],
});

export default router;

router.js

<div class="primary-content">
  <app>
    <router-view></router-view>
  </app>
</div>

PAGE LAYOUT

//...
{
  path: '/settings/scheduling',
  redirect: '/settings/scheduling/offer-responses',
  component: VerticalTabs,
  props: {
    header: 'Scheduling Settings',
    headerIcon: 'fa fa-cog',
    mode: 'vertical',
    showBreadcrumb: true,
  },
  children: [
    {
      name: 'recurrence-template',
      path: 'recurrence-template',
      component: LegacySection,
      props: {
        legacyUrl: '/scheduling/recurrencetemplate/list',
      },
      meta: {
        label: i18n.t('Recurrence Template'),
      },
    },
    {
      name: 'entity-tags',
      path: 'entity-tags',
      component: LegacySection,
      props: {
        legacyUrl: '/scheduling/tag/list',
      },
      meta: {
        label: i18n.t('Entity Tags'),
      },
    }
  ],
},
//...

More complex routes

router.js

Things I wish I knew

Thank you!

VueJs in legacy app

By Stéphane Reiss

VueJs in legacy app

How we introduced Vue.js at Alayacare.

  • 725