Vue in a legacy application

December 2018 - Stéphane Reiss

How we did it at Alayacare

September 2020

Who am I?

  • Web developer
  • Backend background
  • I love javascript

Stéphane Reiss

AlayaCare Cloud

  • 332 tenants across 3 regions (CAN, USA, AUS)
  • 86k active employees
  • 321K active patients

Legacy App - 2015

  • 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 - 2015

<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 - 2016

Let's start over! - 2016

<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

Vue dev tools

main app

Vue dev tools

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

//...
{
  name: ACCOUNTING_BILLING_ROUTES_NAME.MEDICAID_FUNDER_SUMMARY,
  path: 'medicaid/funders',
  component: { template: '<router-view/>' },
  children: ACCOUNTING_BILLING_MEDICAID_ROUTES_DEFINITION,
  redirect: {
    name: ACCOUNTING_BILLING_MEDICAID_ROUTES_NAME.INDEX,
  },
  meta: {
    label: i18n.t('Medicaid Summary'),
    acls: [ACL_ACCOUNTING_DASHBOARD],
    features(store) {
      return (
        store.getters.hasFeature(FEATURES.ACCOUNTING) &&
        store.getters.hasFeature(FEATURES.ELECTRONIC_BILLING_MEDICAID) &&
        !store.getters.hasFeature(FEATURES.ELECTRONIC_BILLING_V1_HIDE)
      );
    },
  },
}
//...

Even more complex routes

<...>/routes/accounting/billing/routes.definition.js

Things I wish I knew

Things I wish I knew - 2 years later!

  • Give proper ownership to your core components
    • Ours grew in an unmaintainable way 
    • We started their refactorDesign System
  • Renderless components!
  • Think about the vuex store architecture and keep an eye on it
  • The "component-export" pattern could have been replaced by vue-router and the legacy section faster
    • We started to remove it only now, as part of the performance improvements
  • Don't include components in your store, it makes the tests harder
    • We even have some circular dependencies

Thank you!

VueJs in legacy app - 2 years later

By Stéphane Reiss

VueJs in legacy app - 2 years later

See original talk: https://slides.com/sreiss/vuejs-in-legacy-app/fullscreen

  • 406