Creating a Nuxt module

Jamie Curnow
Jul 19, 2019 · 17 min read
Image for post
Image for post
Photo by Daniel Chen on Unsplash

Let’s get started…

yarn init
mkdir -p {components/lib,middleware,plugins/helpers,store/modules} \
&& touch module.js debug.js \
{components,components/lib,middleware,plugins,plugins/helpers,store}/index.js \
components/lib/counterAdjuster.vue \
plugins/helpers/{message,counter}.js \
store/modules/counter.js

Setting up the entry point

// module.js
export default function(moduleOptions) {}
// module.js
module.exports.meta = require('./package.json')
// module.js
export default function(moduleOptions) {}
module.exports.meta = require('./package.json')
// nuxt.config.js
export default {
// ...
// register modules:
modules: [
// ...
[@carepenny/custom-counter, { option1: 'something' }]
]
}
{ option1: 'something' }
// nuxt.config.js
export default {
// ...
// register modules:
modules: [
// ...
'@carepenny/custom-counter'
],
// custom-counter options:
customCounter: { option1: 'something' }
}
// module.js
export default function(moduleOptions) {
console.log(this.options.customCounter) // { option1: 'something' }
}
module.exports.meta = require('./package.json')
// module.js
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}
}
module.exports.meta = require('./package.json')
// nuxt.config.js
export default {
// ...
// register modules:
modules: [
// ...
['@carepenny/custom-counter', { namespace: 'counter' }]
],
// custom-counter options:
customCounter: { initialValue: 6, debug: true }
}

Building out the module

// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// add the debug plugin
this.addPlugin({
src: resolve(__dirname, 'debug.js'),
fileName: join('customCounter', 'debug.js'),
options: {
...options,
somethingElse: false
}
})
}
module.exports.meta = require('./package.json')
// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options

// add the debug plugin
this.addPlugin({
src: resolve(__dirname, 'debug.js'),
// ** NOTE: we now use namespace here: **
fileName: join(namespace, 'debug.js'),
options // just pass through the options object
})
}
module.exports.meta = require('./package.json')

Creating the plugin files

// debug.js
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
const { debug, namespace } = options
if (debug) {
console.log(`${namespace} options: `, options)
}
{
namespace: 'counter',
initialValue: 6,
debug: true
}
// plugins/helpers/message.js
// funtion to allow a user to console.log() a value,
// prefixing that value with the namespace option of our module.
export const message = ({ namespace, string }) => {
return console.log(namespace, string)
}
// plugins/helpers/counter.js
// functions to get, adjust and log the counter in the store
// the store module in question will be created with the namespace
// module option as it's name
// mini function to handle if no store, or no store module
// with our namespace exists
const storeModuleExists = ({ state, namespace }) => {
if (!state || !state[namespace]) {
console.error(`${namespace} nuxt module error: store not initialized`)
return false
} else {
return true
}
}
// function to return the current value of the count
export const value = ({ state, namespace }) => {
// handle no store:
if (!storeModuleExists({ state, namespace })) return undefined
// return the counter vale from the store
return state[namespace].count
}
// functions to adjust the counter
export const adjust = ({ state, store, namespace, adjustment }) => {
// handle no store:
if (!storeModuleExists({ state, namespace })) return undefined
// the adjustment shoud be of type number, err if not
if (typeof adjustment !== 'number') {
return console.error(`${namespace} nuxt module error: adjustment should be of type 'number'.`)
}
// adjust the value of the counter using a store mutation
return store.commit(`${namespace}/adjust`, adjustment)
}
// function to console log the current value of the count
export const log = ({ state, namespace }) => {
// handle no store:
if (!storeModuleExists({ state, namespace })) return undefined
// get the current count from the store
const { count } = state[namespace]
// console log it
return console.log(count)
}
// plugins/helpers/index.js
export * from './counter'
export * from './message'
// some/nuxt/plugin/file.js
export default (context, inject) => {
inject('myFunction', (string) => console.log('Hello', string))
}
// some/vue/file.vue
export default {
// ...
mounted () {
this.$myFunction('World')
}
}
// some/vuex/store/module.js
// ...
export const actions = {
someAction () {
this.$myFunction('Store')
}
}
// some/other/nuxt/plugin/file.js
export default ({ app }) => {
app.$myFunction('This is awesome')
}
// plugins/index.js
import * as helpers from './helpers/index.js'
// get the options out using lodash templates
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
// extract the namespace from the options
const { namespace } = options
// create the plugin
export default ({ store }, inject) => {
// get a reference to the vuex store's state
const { state } = store
// inject an object of functions into the app
inject(namespace, {
value() {
return helpers.value({ state, namespace })
},
adjust(adjustment) {
return helpers.adjust({ state, store, namespace, adjustment })
},
log() {
return helpers.log({ state, namespace })
},
message(string) {
return helpers.message({ namespace, string })
}
})
}

Creating Vuex store modules

// store/modules/counter.js
export default options => ({
namespaced: true,
state: () => ({
options,
count: options.initialValue
}),
mutations: {
adjust(state, data) {
state.count += data
}
},
getters: {
count: state => state.count
}
})
// store/index.js
import counterModule from './modules/counter'
// get the options out using lodash templates
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
// extract the namespace var
const { namespace } = options
// create the plugin
export default ({ store }, inject) => {
// register the module using namespace as the name.
// counter module takes the options object and returns an object that is a
// vuex store defenition
store.registerModule(namespace, counterModule(options), {
preserveState: Boolean(store.state[namespace]) // if the store module already exists, preserve it
})
}

Defining and registering components

<template lang="html">
<div class="custom-counter">
<!-- Basic html to render the current count and provide adjustment buttons -->
<h1>Count is: {{ count }}</h1>
<button class="custom-counter--button" @click="adjust(-1)">Minus</button>
<button class="custom-counter--button" @click="adjust(+1)">Add</button>
</div>
</template>
<script>
// components/lib/counterAdjuster.vue
export default {
computed: {
pluginOptions() {
// _customCounterOptions will be added as a prop on component registration.
// it will be the plugin's options object
return this._customCounterOptions || {}
},
// helper to get the name of our injected plugin using the namespace option
injectedPluginName() {
const { pluginOptions } = this
return pluginOptions.namespace ? '$' + pluginOptions.namespace : undefined
},
// helper to return the current value of the counter using our injected plugin function
count() {
const { injectedPluginName } = this
return injectedPluginName
? this[injectedPluginName].value() // same as this.$count.value()
: undefined
}
},
methods: {
// method to adjust the counter using our injected plugin function
adjust(adjustment) {
const { injectedPluginName } = this
this[injectedPluginName].adjust(adjustment)
}
}
}
</script>
<style lang="scss" scoped></style>
// components/lib/index.js
import counterAdjuster from './counterAdjuster.vue'
export default {
counterAdjuster
}
// components/index.js
import Vue from 'vue'
import components from './lib'
// get options passed from module.js
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
// loop through components and register them
for (const name in components) {
Vue.component(name, {
// extend the original component
extends: components[name],
// add a _customCounterOptions prop to gain access to the options in the component
props: {
_customCounterOptions: {
type: Object,
default: () => ({ ...options })
}
}
})
}
// some/random/page.vue
<template lang="html">
<counterAdjuster></counterAdjuster>
</template>
<script>
export default {}
</script>

Adding Nuxt Middleware

// middleware/index.js
import Middleware from '../../middleware'
const options = JSON.parse(`<%= JSON.stringify(options) %>`)
const { namespace } = options
Middleware[namespace] = context => {
const { route, store, app } = context
// simply console logging here to demonstrate access to app context.
console.log('Counter middleware route', route.path)
console.log('Counter middleware store', store.state[namespace].count)
console.log('Counter middleware app', app[`$${namespace}`].value())
}
// nuxt.config.js
{
// ...
router: {
middleware: ['counter']
},
// ...
modules: [
['@carepenny/custom-counter', { namespace: 'counter' }]
]
}

Registering all of our plugins and adding files to the nuxt build folder

// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options

// add the debug plugin
this.addPlugin({
src: resolve(__dirname, 'debug.js'),
// ** NOTE: we now use namespace here: **
fileName: join(namespace, 'debug.js'),
options // just pass through the options object
})
}
module.exports.meta = require('./package.json')
// module.js
const { resolve, join } = require('path')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}

// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options

// add all of the initial plugins
const pluginsToSync = [
'components/index.js',
'store/index.js',
'plugins/index.js',
'debug.js',
'middleware/index.js'
]
for (const pathString of pluginsToSync) {
this.addPlugin({
src: resolve(__dirname, pathString),
fileName: join(namespace, pathString),
options
})
}
}
module.exports.meta = require('./package.json')
this.addTemplate({
src: resolve(__dirname, 'something.js'),
fileName: 'customCounter/something.js',
options
})
// module.js
const { resolve, join } = require('path')
const { readdirSync } = require('fs')
export default function(moduleOptions) {
// get all options for the module
const options = {
...moduleOptions,
...this.options.customCounter
}
// expose the namespace / set a default
if (!options.namespace) options.namespace = 'customCounter'
const { namespace } = options
// add all of the initial plugins
const pluginsToSync = [
'components/index.js',
'store/index.js',
'plugins/index.js',
'debug.js',
'middleware/index.js'
]
for (const pathString of pluginsToSync) {
this.addPlugin({
src: resolve(__dirname, pathString),
fileName: join(namespace, pathString),
options
})
}
// sync all of the files and folders to revelant places in the nuxt build dir (.nuxt/)
const foldersToSync = ['plugins/helpers', 'store/modules', 'components/lib']
for (const pathString of foldersToSync) {
const path = resolve(__dirname, pathString)
for (const file of readdirSync(path)) {
this.addTemplate({
src: resolve(path, file),
fileName: join(namespace, pathString, file),
options
})
}
}
}
module.exports.meta = require('./package.json')

To conclude

Carepenny

Next-generation healthcare software.

Jamie Curnow

Written by

I am a javascript developer living in beautiful Cornwall, UK. Ever curious, always learning.

Carepenny

Carepenny

Next-generation healthcare software. Our team is working to solve some of the biggest challenges in healthcare software.

Jamie Curnow

Written by

I am a javascript developer living in beautiful Cornwall, UK. Ever curious, always learning.

Carepenny

Carepenny

Next-generation healthcare software. Our team is working to solve some of the biggest challenges in healthcare software.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store