While working on different projects, it might be necessary for me to create custom components. That was the case on a recent government project where monetary inputs were usually large and had to be formatted with commas and the Naira symbol to make readability easy.

In this article, I'll show how I extracted the input component into its package as I wanted to use it in other projects. The template I used is an adaptation of Vue SFC rollup.

I created the project folder vue-naira-input and added src/NairaInput.vue, src/entry.js, package.json, build/rollup.config.js and the obligatory .gitignore.

The package.json is:

{
  "name": "vue-naira-input",
  "version": "1.0.0",
  "description": "A Vue.js component that displays a clock",
  "main": "dist/vue-naira-input.ssr.js",
  "module": "dist/vue-naira-input.esm.js",
  "unpkg": "dist/vue-naira-input.min.js",
  "files": [
    "dist/*",
    "src/**/*.vue"
  ],
  "scripts": {
    "build": "cross-env NODE_ENV=production rollup --config build/rollup.config.js",
    "build:ssr": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format cjs",
    "build:es": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format es",
    "build:unpkg": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format iife"
  },
  "author": "Uk Chukundah",
  "license": "MIT",
  "dependencies": {
    "text-mask-addons": "^3.8.0",
    "vue-text-mask": "^6.1.2"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ukchukx/vue-naira-input"
  },
  "devDependencies": {
    "cross-env": "^5.2.0",
    "minimist": "^1.2.0",
    "rollup": "^1.12.1",
    "rollup-plugin-buble": "^0.19.6",
    "rollup-plugin-commonjs": "^10.0.0",
    "rollup-plugin-replace": "^2.2.0",
    "rollup-plugin-terser": "^4.0.4",
    "rollup-plugin-vue": "^5.0.0",
    "vue": "^2.6.10",
    "vue-template-compiler": "^2.6.10"
  }
}

There are scripts to build for browser, node and SSR. The first script builds for all modes of usage. Our component depends on text-mask-addons and vue-text-mask.

The component file src/NairaInput.vue contains:

<template>
  <!-- eslint-disable -->
  <masked-input type="text" v-model="amount" :mask="nairaMask" :guide="false" />
</template>
<script>
import MaskedInput from 'vue-text-mask';
import createNumberMask from 'text-mask-addons/dist/createNumberMask';

export default {
  name: 'NairaInput',
  components: {
    MaskedInput
  },
  props: {
    initialAmount: {
      type: Number,
      default: () => 0
    }
  },
  data() {

    return {
      nairaMask: createNumberMask({ prefix: '₦', allowDecimal: true }),
      amount: `${this.initialAmount}`
    };
  },
  watch: {
    amount(str) {
      const amount = str.length ? parseFloat(str.replace(/[₦,]/g, '')) : 0;
      this.$emit('input', amount);
    }
  },
  methods: {
    clear() {
      this.amount = '';
    }
  }
};
</script>

The entry file src/entry.js contains:

// Import vue component
import component from './NairaInput.vue';

// install function executed by Vue.use()
function install(Vue) {
  if (install.installed) return;
  install.installed = true;
  Vue.component('NairaInput', component);
}

// Create module definition for Vue.use()
const plugin = {
  install,
};

// To auto-install when vue is found
/* global window global */
let GlobalVue = null;
if (typeof window !== 'undefined') {
  GlobalVue = window.Vue;
} else if (typeof global !== 'undefined') {
  GlobalVue = global.Vue;
}
if (GlobalVue) {
  GlobalVue.use(plugin);
}

// Inject install function into component - allows component
// to be registered via Vue.use() as well as Vue.component()
component.install = install;

// Export component by default
export default component;

It allows us to include our component using Vue.use() or Vue.component().

Our build config file build/rollup.config.js contains:

// rollup.config.js
import vue from 'rollup-plugin-vue';
import buble from 'rollup-plugin-buble';
import commonjs from 'rollup-plugin-commonjs';
import replace from 'rollup-plugin-replace';
import { terser } from 'rollup-plugin-terser';
import minimist from 'minimist';

const argv = minimist(process.argv.slice(2));

const baseConfig = {
  input: 'src/entry.js',
  plugins: {
    preVue: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      commonjs(),
    ],
    vue: {
      css: true,
      template: {
        isProduction: true,
      },
    },
    postVue: [
      buble(),
    ],
  },
};

// UMD/IIFE shared settings: externals and output.globals
// Refer to https://rollupjs.org/guide/en#output-globals for details
const external = [
  'text-mask-addons',
  'vue-text-mask'
];
const globals = {
  // Provide global variable names to replace your external imports
  // eg. jquery: '$'
};

// Customize configs for individual targets
const buildFormats = [];
if (!argv.format || argv.format === 'es') {
  const esConfig = {
    ...baseConfig,
    output: {
      file: 'dist/vue-naira-input.esm.js',
      format: 'esm',
      exports: 'named',
    },
    plugins: [
      ...baseConfig.plugins.preVue,
      vue(baseConfig.plugins.vue),
      ...baseConfig.plugins.postVue,
      terser({
        output: {
          ecma: 6,
        },
      }),
    ],
  };
  buildFormats.push(esConfig);
}

if (!argv.format || argv.format === 'cjs') {
  const umdConfig = {
    ...baseConfig,
    external,
    output: {
      compact: true,
      file: 'dist/vue-naira-input.ssr.js',
      format: 'cjs',
      name: 'vue-naira-input',
      exports: 'named',
      globals,
    },
    plugins: [
      ...baseConfig.plugins.preVue,
      vue({
        ...baseConfig.plugins.vue,
        template: {
          ...baseConfig.plugins.vue.template,
          optimizeSSR: true,
        },
      }),
      ...baseConfig.plugins.postVue,
    ],
  };
  buildFormats.push(umdConfig);
}

if (!argv.format || argv.format === 'iife') {
  const unpkgConfig = {
    ...baseConfig,
    external,
    output: {
      compact: true,
      file: 'dist/vue-naira-input.min.js',
      format: 'iife',
      name: 'VueNairaInput',
      exports: 'named',
      globals,
    },
    plugins: [
      ...baseConfig.plugins.preVue,
      vue(baseConfig.plugins.vue),
      ...baseConfig.plugins.postVue,
      terser({
        output: {
          ecma: 5,
        },
      }),
    ],
  };
  buildFormats.push(unpkgConfig);
}

// Export config
export default buildFormats;

Pay attention to line 34:

const external = [
  'text-mask-addons',
  'vue-text-mask'
];

This is so that our dependencies are not bundled with our component. This way the dependencies are installed on usage. If we include them, there might be conflicts with a user's dependencies if (s)he uses different versions of the dependencies.

Run npm i to install our dependencies and npm run build to build our package. You should see 3 files in the dist folder: vue-naira-input.esm.js, vue-naira-input.min.js and vue-naira-input.ssr.js.

To publish to NPM, create an account (if you haven't), login from your console using npm login. You can check the logged in user using npm whoami. From the root directory of the project, run npm publish to publish your package (ensure your package has a unique name ie you can't use vue-naira-input).

After publishing, you can use the package in a Node.js project by running npm i -S vue-naira-input, or in the browser using https://unpkg.com/vue-naira-input.

The GitHub repo for the package is here.