Symphonious

Living in a state of accord.

Moolah Diaries: Making inject-loader and vuetify-loader play nice

I’ve been upgrading Moolah after a long period of neglect. One of the tasks is to update it to WebPack 4. Mostly this went smoothly by just creating a new project with vue-cli and bringing its generated build setup over to replace the old one.  Then a bunch of tweaking.

One thing that did bite however, was tests using inject-loader started failing as soon as I added vuetify-loader to the project with:

Error: Module parse failed: 'import' and 'export' may only appear at the top level

There may be a better way to fix this in config, but the simple answer is to adjust the inject-loader import line from something like:

import categoryStoreLoader from 'inject-loader!categoryStore';

to:

import categoryStoreLoader from 'inject-loader!babel-loader!categoryStore';

Adding in the extra babel-loader ensures the imports are processed and replaced by require and everything works.

 

Internationalising Vue Components

As Vue gradually spreads through our JavaScript, it’s reached the point where we need to support internationalisation. We already have a system for localising numbers and dates that we can reuse easily enough with Vue filters. We also have an existing system for translations, but it’s tied into the mustache templates we’ve used to date so we’ll need to adapt or change it somehow to work with Vue.

In mustache we simply wrap any strings to be translated with {{#i18n}}…{{/i18n}} markers and a webpack loader takes care of providing a translation function as the i18n property of the state for the compiled template.

There are a number of vue translations plugins around all of which tend to take one of two approaches:

  1. a directive that automatically pulls out the element text (e.g. vue-translate)
    <div v-trans title=hello>hello</div>
  2. a method you call (e.g. vue-i18n-mixin)
    <h1 v-text=”t(‘header.title’, ‘fr’)”></h1>
    <h1>{{ t(‘header.title’) }}</h1>

Using a directive is nice and simple but doesn’t work if the text is dynamic (any {{data}} values are already resolved in the element text). Explicitly calling a function to perform the translation leads to pretty unwieldy templates.

What we’ve settled on is a pretty direct translation of our mustache system to Vue. We use <i18n></i18n> instead of {{#i18n}}{{/i18n}} since the content of {{…}} in Vue is expected to be valid JavaScript and IDEs flag it as an error if it’s not. So a simple example might be:

<p><i18n>Hello world!</i18n></p>

To make it actually translate, we use a HTML pre-loader for the webpack vue-loader that translates that into:

<p>{{translate("Hello world!")}}</p>

Finally, on the client side, a global Vue mixin provides the implementation of translate that looks up the translated version of that string and replace it.

When the string includes parameters we write them in the same way as a normal Vue template, for example:

<p><i18n>Hello {{name}}!</i18n></p>

And our pre-loader translates it in exactly the same way:

<p>{{translate("Hello {{name\}\}!")}}</p>

The only slightly tricky bit is that the pre-loader escapes the } characters, otherwise Vue treats them as the end of the template string and it results in JavaScript errors.

At this point, Vue won’t replace {{name}} with the actual value of name because it’s part of a JavaScript string, not part of the template. The good part of that is that our translation function will now be looking up the string to translate prior to variable substitution.  So it will look for a translation for “Hello {{name}}!” rather than needing translations for every possible name. Once we find the translation, the translate function then uses a regex to find and replace each placeholder with the actual value from the vue instance. 

Notably, this process means that you can’t use anything but a simple variable name in placeholders within translated strings (though it can refer to a computed property). This is actually a good thing because we don’t want to send a string off to the translators that contains complex JavaScript they won’t understand. In fact, we have tests that place additional restrictions on strings to translate to ensure they are easily manageable through the translation process.

Moolah Diaries – The Moment Dilemma

moment is one of those javascript libraries that just breaks my heart. It does so much right – nearly every date function you’d want right at your finger tips, no major gotchas causing bugs, clean API and good docs. I’ve used moment in some pretty hard code, very date/time orientated apps and been very happy.

But it breaks my heart because when you pull it in via webpack, it winds up being a giant monolithic blob. I only need a couple of date functions but with moment I wind up getting every possible date function you can imagine plus translations into a huge range of languages I don’t need. If I use moment, about 80% of Moolah’s client javascript is moment.

So I was quite pleased to find date-fns which also provides every date function you can imagine but is specifically design to be modular so you only pull in what you actually need. Works a treat, so Moolah uses that for the date operations it needs and it barely adds any size at all.

Except then when I went to use a graph library, it turns out that Chart.js which I was keen to try depends on moment. Sigh.

Moolah Diaries – Tracking Account Balances

Moolah has two separate Vuex modules, transactions and accounts. That’s a fairly reasonably logical separation, but in the new Moolah, it’s not a complete separation. Moolah only ever loads a subset of transactions – typically the most recent transactions from the currently selected account.  As a result, accounts have their own current balance property because they can’t always calculate it off of their transactions.

That means that sometimes the transaction Vuex module needs to make changes to state owned by the account module which is a rather unpleasant breach of module separation. While we ensured transaction balances remained consistent inside each mutation, we can’t do that for account balances because Vuex (presumably deliberately) makes it impossible for a mutation to access or change state outside of the module itself. Instead, I’ve used a simple Vuex plugin that watches the balance on the most recent transaction and notifies the account module when it changes:

import {mutations as accountsMutations} from './accountsStore';
export default store => store.watch(
    (state) => {
        return state.transactions.transactions.length > 0 
? state.transactions.transactions[0].balance
: undefined; }, (newValue, oldValue) => { if (store.getters["accounts/selectedAccount"] !== undefined &&
newValue !== undefined) { store.commit('accounts/' + accountsMutations.updateAccount, { id: store.state.selectedAccountId,
patch: {balance: newValue} }); } });

Most of the complexity there is dealing with the case where we don’t yet have transaction or don’t have a selected account. This essentially makes the syncing between transaction balances and account balances a separate responsibility that belongs to this plugin.

The plugin doesn’t completely handle transfers between accounts. The balance for the account currently being edited updates correctly, but the balance for the account on the other side of the transfer doesn’t update. I’m not particularly happy with the solution, but at least for now the responsibility of reacting to transfer changes is in the transaction module’s actions. The action can easily calculate the effects of transaction changes on the balances of other accounts and use Vuex’s dispatch to send a notification over to the account module to perform the update. Mutations can’t dispatch events so it has to be the action that does it.

This leaves responsibility for updating the current account’s balance with the plugin and the responsibility for updating other account’s balances with the transaction module which is ugly. Perhaps the transaction module actions should be responsible for all account balances. Probably the right answer is to extract a separate class/function that the transaction module notifies at the end of each action with the state of the transaction before and after. Then that new class/function is responsible for updating account balances. Will need to prod the code a bit to see if that really can be done…

 

 

Moolah Diaries – Maintaining Invariants with Vuex Mutations

Previously on the Moolah Diaries I had plans to recalculate the current balance at each transaction as part of any Vuex action that changed a transaction. Tracking balances is a good example of an invariant – at the completion of any atomic change, the balance for a transaction should be the initial balance plus the sum of all transaction amounts prior to the current transaction.

The other invariant around transactions is that they should be sorted in reverse chronological order. To make the order completely deterministic, transactions with the same dates are then sorted by ID. This isn’t strictly necessary, but it avoids having transactions jump around unexpectedly.

My original plan was to ensure both of these invariants were preserved as part of each Vuex action, but actions aren’t the atomic operations in Vuex – mutations are. So we should update balances and adjust sort order as part of any mutation that affects transactions. Since only mutations can change state in Vuex, we can be sure that if our mutations preserve the invariants then they will always hold true. With actions, there’s always the risk that something would use mutations directly and break the invariants.

So lesson number 1 – in Vuex, it should be mutations that are responsible for maintaining invariants. That’s probably not news to anyone who’s used Vuex for long.

We could take that a step further and verify that the invariants always hold true using a vuex plugin:

const invariantPlugin = store => {
if (process.env.NODE_ENV !== 'production') {
store.subscribe((mutation, state) => {
// Verify invariants
});
}
};

However, the way vuex recommends testing actions and mutations means registered plugins don’t get loaded. Tests that cover views that use the store will most likely stub out the actions since they’ll usually make HTTP requests.  So mostly the invariant check will only run during manual testing. That may still be worth it, but I’m not entirely sure yet…