Vuex
If you use Vue.js, then you probably use (or should consider using) Vuex for state management. Like most Vue.js related things, it’s at its easiest when used with Node tooling, but you can also use it with Apps Script HtmlService to manage state in reactive add-ons.
The cdn references
<script src="https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js" integrity="sha512-n/iV5SyKXzLRbRczKU75fMgHO0A1DWJSWbK5llLNAqdcoxtUK3NfgfszYpjhvcEqS6nEXwu7gQ5bIkx6z8/lrA==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.3.20/dist/vuetify.min.js"></script>
cdn.html
The Store
As usual, we want to keep the global space relatively uncluttered, so as per the template we’ll create the vuex store as a member of the Store namespace. The first job is to create the initial state of the store and define the getter, mutations and actions. The benefit of using Vuex is that it provides a focal point for all state changes and actions that modify state, and UI Vue components can automatically adapt rendering to the current state.
// the initial contents of the vxStore
const _initial = {
state: {
// whether polling is paused
pollingPaused: false,
// this is the latest package received from the server
latestPackage: null,
// this is the reconstructed current data
// initially it just needs to know the sheetnames required
currentPackage: {
sheetNames: ["users"]
},
showThrow: false,
throwMessage: null,
pollQueue: null,
simmer: null
},
getters: {
simTime(state) {
return state.simmer.time;
},
simRate(state) {
return state.simmer.rate;
},
dataSets(state) {
return state.currentPackage && state.currentPackage.everything;
},
users(state, getters) {
return Packer.getData(getters.dataSets, "users");
},
getUser: (state, getters) => (userId) => {
return getters.users.find((f) => f.id === userId);
},
transferring(state) {
return state.pollQueue.activeSize() > 0;
},
dataChanged(state) {
return state.currentPackage && state.currentPackage.changed;
}
},
actions: {
// can use this to start/restart the polling
pause({ dispatch, commit, state }, value) {
commit("setPollingPaused", value);
commit("setTimerPaused", value);
if (!value) {
state.simmer.start();
state.pollQueue.startQueue();
dispatch("getData");
} else {
state.simmer.stop();
state.pollQueue.stopQueue();
}
},
stopEverything({ state }) {
state.simmer.stop();
state.pollQueue.stopQueue();
},
startSimmer({ state }) {
state.simmer.start();
},
getData(sob) {
return _poller(sob);
}
},
mutations: {
setSimmer(state, value) {
state.simmer = value;
},
setSimRate(state, value) {
state.simmer.rate = value;
},
setPollQueue(state, value) {
state.pollQueue = value;
},
setPollingPaused(state, value) {
state.pollingPaused = value;
},
// populate an error message
setThrowMessage(state, value) {
state.throwMessage = value;
},
// signal that an error should be shown
setThrow(state, value) {
state.showThrow = value;
},
// take the latest polling data and populate it with anything that hasnt changed
setLatestPackage(state, value) {
// this is the latest response from the server
// if everything is null, then nothing has changed
const { result, entry } = value;
const { changed, cached } = result;
state.latestPackage = result;
state.currentPackage = Packer.reconstructEverything({
current: state.currentPackage,
latest: state.latestPackage
});
// only allow clearables on the first pass
state.clearables = null;
}
}
};
initialising Vuex
Mappers
Vuex provides some handy mappers that make it easy to include store defintions in Vue components. I’m taking this a bit further by automatically generating the mappers from the initial structure of the store.
// handy for mapping in the components
const _vxMaps = Object.keys(_initial).reduce((p, c) => {
p = Object.keys(_initial);
return p;
}, {});
ns.mapGetters = Vuex.mapGetters(_vxMaps.getters);
ns.mapState = Vuex.mapState(_vxMaps.state);
ns.mapMutations = Vuex.mapMutations(_vxMaps.mutations);
ns.mapActions = Vuex.mapActions(_vxMaps.actions);
mapping
Now there’s argument that says that components should only map the state, getters, mutations and actions they plan to reference, but almost by definition, add-ons are going to be rather small so the convenience of mapping the entire store and including the mappings in each component is probably a fair tradeoff.
Using this approach anything from the Vuex store can easily be exposed in any component as easily as this
computed: {
...Store.mapGetters,
...Store.MapState
},
methods: {
...Store.mapMutations,
...Store.mapActions
}
including maps in Vue component
Initializing the store
We of course can’t initialize the store until we’ve imported Vue, Vuex and especially if we’re importing module that might be a bit later, so I always like to include an .init method which contains things that can only be executed when all the scripts are imported and the Dom is settled. Here’s the init
ns.init = ({ modules } = {}) => {
// we're using Vuex extensively
Vue.use(Vuex);
// and this is the store
ns.vxStore = new Vuex.Store(_initial);
// set up things that needed some modules loaded asyncrnously,
// which would have been done by now
const { TimeSimmer, Qottle } = modules;
const ms = TimeSimmer.ms;
const simmer = new TimeSimmer({
immediate: false,
// update the simtime every 1 sec
tickRate: ms("seconds", 1),
// run in real time- set this to 1 - 60 means 1 minute passes in a second
rate: 60,
// we start now
startedAt: new Date().getTime()
});
const qottle = new Qottle({
// polling only 1 at a time, no more than 6 every minute
// and with at least 7 secs between each one
concurrent: 1,
rateLimited: true,
rateLimitPeriod: ms("minutes", 1),
rateLimitMax: 6,
rateLimitDelay: ms("seconds", 7)
});
ns.vxStore.commit("setSimmer", simmer);
ns.vxStore.commit("setPollQueue", qottle);
return ns;
};
initializing the store
which is executed as part of the main initialization
window.onload = () => {
// need to wait for any modules to be imported
const waitForModules = Store.moduleImports || Promise.resolve(null)
waitForModules.then(modules=> {
// this is an npm module but we can install just like the others
Store.addComponent(modules.CountryFlag)
// initialize everything and register all the components
Store.init({modules})
.components
.registerAll()
// render vue
new Vue({
el: '#app',
vuetify: new Vuetify(),
store: Store.vxStore
})
// start polling
Store.load()
/**
* we need to detect when the add-on is closed to clean up
*/
window.addEventListener("beforeunload", (event)=> {
Store.unload()
// Cancel the event as stated by the standard.
event.preventDefault();
// Older browsers supported custom message
event.returnValue = '';
});
/**
* there's some action that could be taken when the tab becomes visible
*/
Store.handleVisibility(new TabVisibility())
})
main.js
Polling
Vuex and Qottle recipe: How to manage an asynchronous polling queue make a useful combination to invisibly handle keeping the add-on view of the spreadsheet data in sync. Polling communicates with the Server for the latest view of the data, and transfers only changes to the sheet content. The data is then reconstructed. The reactivity of Vuex means that the Vue components will only be re-rendered if there’s been any changes in the spreadsheet, and all this can happen without caring about the details of how all that happens.
Indeed, this is all that’s required in a Vue component to know what’s the latest data in the users tab of a spreadsheet is
computed: {
items () {
return this.users
},
...Store.mapGetters
}
bmSummary.vue
Summary
If you’re using the template from How to use Vue.js, Vuex and Vuetify to create Google Apps Script Add-ons implementing Vuex in your add-on is rather straightforward (and highly recommended)