Vue.js course Β· Module 14: Directives & Plugins

Plugin Ecosystem and Integration

4 min read
In this lesson5

In NOVA LAB, every space station module relies on proven, tested systems - from navigation to communication. In the Vue ecosystem, there are popular plugins that solve common problems. Let's explore them and learn how to create advanced plugins.

vue-i18n - Internationalization

vue-i18n is the official Vue plugin for multilingual support:

1import { createI18n } from 'vue-i18n'
2
3const i18n = createI18n({
4  locale: 'pl',
5  fallbackLocale: 'en',
6  messages: {
7    pl: {
8      station: {
9        name: 'NOVA LAB Station',
10        status: 'Status: {status}',
11        crew: 'Crew: {count} people'
12      }
13    },
14    en: {
15      station: {
16        name: 'NOVA LAB Station',
17        status: 'Status: {status}',
18        crew: 'Crew: {count} members'
19      }
20    }
21  }
22})
23
24app.use(i18n)

In a component:

1<script setup>
2import { useI18n } from 'vue-i18n'
3
4const { t, locale } = useI18n()
5</script>
6
7<template>
8  <h1>{{ t('station.name') }}</h1>
9  <p>{{ t('station.status', { status: 'Operational' }) }}</p>
10  <button @click="locale = 'en'">EN</button>
11  <button @click="locale = 'pl'">PL</button>
12</template>

VeeValidate - Form Validation

VeeValidate is a plugin for form validation:

1<script setup>
2import { useForm, useField } from 'vee-validate'
3import * as yup from 'yup'
4
5const schema = yup.object({
6  missionName: yup.string().required('Mission name is required'),
7  crewCount: yup.number().min(1, 'Minimum 1 person').required()
8})
9
10const { handleSubmit } = useForm({ validationSchema: schema })
11
12const { value: missionName, errorMessage: nameError } = useField('missionName')
13const { value: crewCount, errorMessage: crewError } = useField('crewCount')
14
15const onSubmit = handleSubmit(values => {
16  console.log('Mission:', values)
17})
18</script>
19
20<template>
21  <form @submit="onSubmit">
22    <input v-model="missionName" placeholder="Mission name" />
23    <span v-if="nameError">{{ nameError }}</span>
24
25    <input v-model="crewCount" type="number" placeholder="Crew" />
26    <span v-if="crewError">{{ crewError }}</span>
27
28    <button type="submit">Start mission</button>
29  </form>
30</template>

vue-toastification - Notifications

vue-toastification is a ready-made notification system:

1import Toast from 'vue-toastification'
2import 'vue-toastification/dist/index.css'
3
4app.use(Toast, {
5  position: 'top-right',
6  timeout: 3000,
7  closeOnClick: true
8})

Creating a Plugin with provide/inject

The provide/inject pattern is the best way to share data from a plugin:

1import { ref, readonly } from 'vue'
2
3const NotificationPlugin = {
4  install(app, options = {}) {
5    const notifications = ref([])
6    let nextId = 0
7
8    function notify(message, type = 'info') {
9      const id = nextId++
10      notifications.value.push({ id, message, type })
11      setTimeout(() => {
12        notifications.value = notifications.value.filter(n => n.id !== id)
13      }, options.duration || 4000)
14    }
15
16    app.provide('notifications', {
17      list: readonly(notifications),
18      notify,
19      success: (msg) => notify(msg, 'success'),
20      error: (msg) => notify(msg, 'error'),
21      warning: (msg) => notify(msg, 'warning')
22    })
23  }
24}

The key element - readonly() prevents accidental modification of the notification list from outside the plugin.

Plugin Options Pattern

A good plugin accepts configuration with sensible default values:

1const ThemePlugin = {
2  install(app, options = {}) {
3    const defaults = {
4      primaryColor: '#00ff88',
5      darkMode: true,
6      fontFamily: 'monospace',
7      borderRadius: '8px'
8    }
9
10    const theme = ref({ ...defaults, ...options })
11
12    function setTheme(newOptions) {
13      theme.value = { ...theme.value, ...newOptions }
14    }
15
16    app.provide('theme', {
17      current: readonly(theme),
18      setTheme
19    })
20
21    // Global directive using the theme
22    app.directive('themed', (el, binding) => {
23      const t = theme.value
24      el.style.color = t.primaryColor
25      el.style.fontFamily = t.fontFamily
26      if (binding.modifiers.bordered) {
27        el.style.border = '1px solid ' + t.primaryColor
28        el.style.borderRadius = t.borderRadius
29      }
30    })
31  }
32}

Installation:

1app.use(ThemePlugin, {
2  primaryColor: '#00b4d8',
3  darkMode: true
4})

Auto-Install Pattern

Plugins intended for distribution (e.g., as an npm package) should support auto-install:

1const MyPlugin = {
2  install(app, options) {
3    // plugin logic
4  }
5}
6
7// Auto-install when Vue is globally available (CDN)
8if (typeof window !== 'undefined' && window.Vue) {
9  window.Vue.use(MyPlugin)
10}
11
12export default MyPlugin

Composable vs Plugin - When to Use Which?

FeatureComposablePlugin
Registrationimport in componentapp.use() in main.js
ScopeLocal (per component)Global (entire application)
Global componentsNoYes (app.component)
Global directivesNoYes (app.directive)
Provide/InjectPossibleNatural
Dependency treeExplicit importsImplicit (inject)

Rule of thumb: If the functionality is needed in many places and requires global configuration (e.g., theme, i18n, toast) - use a plugin. If it's logic specific to a few components - use a composable.

Code for this lesson: App.vue
1<script setup>
2import { inject, ref, readonly } from 'vue'
3
4const theme = inject('theme')
5const notifications = inject('notifications')
6const { t, locale, setLocale } = inject('i18n')
7
8function testNotification(type) {
9  if (type === 'success') {
10    notifications.success(t('notifications.saved'))
11  } else if (type === 'error') {
12    notifications.error(t('notifications.connectionError'))
13  } else {
14    notifications.notify(t('notifications.scanStarted'))
15  }
16}
17</script>
18
19<template>
20  <div class="nova-lab">
21    <h1>Plugin Ecosystem i Integracja</h1>
22
23    <section class="section">
24      <h2>Theme Plugin (provide/inject + readonly)</h2>
25      <div class="theme-info">
26        <p>Primary: <span :style="{ color: theme.current.value.primaryColor }">{{ theme.current.value.primaryColor }}</span></p>
27        <p>Dark Mode: {{ theme.current.value.darkMode ? 'TAK' : 'NIE' }}</p>
28        <p>Font: {{ theme.current.value.fontFamily }}</p>
29      </div>
30      <div class="btn-row">
31        <button @click="theme.setTheme({ primaryColor: '#00ff88' })" class="btn">Green</button>
32        <button @click="theme.setTheme({ primaryColor: '#00b4d8' })" class="btn cyan">Cyan</button>
33        <button @click="theme.setTheme({ primaryColor: '#e74c3c' })" class="btn red">Red</button>
34      </div>
35    </section>
36
37    <section class="section">
38      <h2>i18n Plugin</h2>
39      <p>{{ t('station.name') }}</p>
40      <p>{{ t('station.status') }}</p>
41      <div class="btn-row">
42        <button @click="setLocale('pl')" :class="['btn', { active: locale.value === 'pl' }]">PL</button>
43        <button @click="setLocale('en')" :class="['btn', { active: locale.value === 'en' }]">EN</button>
44      </div>
45    </section>
46
47    <section class="section">
48      <h2>Notification Plugin</h2>
49      <div class="btn-row">
50        <button @click="testNotification('success')" class="btn success">Success</button>
51        <button @click="testNotification('error')" class="btn error">Error</button>
52        <button @click="testNotification('info')" class="btn info">Info</button>
53      </div>
54    </section>
55
56    <div class="notif-container">
57      <div
58        v-for="n in notifications.list.value"
59        :key="n.id"
60        :class="['notif', n.type]"
61      >{{ n.message }}</div>
62    </div>
63  </div>
64</template>
65
66<style scoped>
67.nova-lab {
68  background: #0a0e27;
69  color: #c0c0c0;
70  min-height: 100vh;
71  padding: 20px;
72  font-family: monospace;
73}
74h1 { color: #00ff88; margin-bottom: 20px; }
75h2 { color: #00b4d8; font-size: 16px; margin-bottom: 10px; }
76.section {
77  background: rgba(0,180,216,0.05);
78  border: 1px solid rgba(0,180,216,0.2);
79  padding: 15px;
80  border-radius: 8px;
81  margin-bottom: 15px;
82}
83.theme-info { margin-bottom: 10px; }
84.theme-info p { margin: 4px 0; font-size: 13px; }
85.btn-row { display: flex; gap: 8px; }
86.btn {
87  background: rgba(0,255,136,0.15);
88  color: #00ff88;
89  border: 1px solid #00ff88;
90  padding: 6px 14px;
91  border-radius: 6px;
92  cursor: pointer;
93  font-family: monospace;
94}
95.btn.active { background: #00ff88; color: #0a0e27; }
96.btn.cyan { color: #00b4d8; border-color: #00b4d8; }
97.btn.red { color: #e74c3c; border-color: #e74c3c; }
98.btn.success { color: #2ecc71; border-color: #2ecc71; }
99.btn.error { color: #e74c3c; border-color: #e74c3c; }
100.btn.info { color: #00b4d8; border-color: #00b4d8; }
101.notif-container {
102  position: fixed; top: 20px; right: 20px;
103  display: flex; flex-direction: column; gap: 8px; z-index: 1000;
104}
105.notif {
106  padding: 10px 18px; border-radius: 8px;
107  font-family: monospace; font-size: 13px;
108  animation: slideIn 0.3s ease;
109}
110.notif.success { background: rgba(46,204,113,0.9); color: white; }
111.notif.error { background: rgba(231,76,60,0.9); color: white; }
112.notif.info { background: rgba(0,180,216,0.9); color: white; }
113@keyframes slideIn {
114  from { transform: translateX(100%); opacity: 0; }
115  to { transform: translateX(0); opacity: 1; }
116}
117</style>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which of the following is the official Vue plugin for internationalization (i18n)?

  2. 2. Why use readonly() when exposing data from a plugin?

These are 2 of 4 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Implement a ThemePlugin with default options, a setTheme method, and a global v-themed directive.

  • Horizontal ordering

    Arrange the correct way to pass a readonly ref through provide:

  • Click in order

    Arrange the correct installation of ThemePlugin with options:

  • Vertical ordering

    Arrange the steps for implementing the Plugin Options Pattern:

  • Horizontal ordering

    Arrange the correct merging of default options with provided ones:

  • Vertical ordering

    Arrange the steps of the plugin auto-install pattern:

  • Click in order

    Arrange the correct registration of a global directive in a plugin:

  • Code editor

    Implement a notification plugin with a reactive array, setTimeout for auto-remove, and provide/inject.

  • Vertical ordering

    Arrange the layers of a Vue application architecture with plugins from lowest to highest:

  • Horizontal ordering

    Arrange the usage of the v-themed directive with the bordered modifier:

  • Click in order

    Arrange the correct registration of a global component in a plugin:

Useful articles