Vue.js course · Module 14: Directives & Plugins

Creating Useful Plugins

2 min read
In this lesson3

In NOVA LAB, every station system is an independent module. Let's see how to create plugins that are truly useful: toast notifications and i18n (internationalization).

Toast Plugin - Notifications

A toast is a short notification displayed temporarily. The plugin will provide a show() method available throughout the entire application:

1const ToastPlugin = {
2  install(app, options = {}) {
3    const duration = options.duration || 3000
4    const position = options.position || 'top-right'
5
6    const toasts = ref([])
7    let toastId = 0
8
9    const toast = {
10      show(message, type = 'info') {
11        const id = toastId++
12        toasts.value.push({ id, message, type })
13        setTimeout(() => {
14          toasts.value = toasts.value.filter(t => t.id !== id)
15        }, duration)
16      },
17      success(msg) { this.show(msg, 'success') },
18      error(msg) { this.show(msg, 'error') },
19      warning(msg) { this.show(msg, 'warning') },
20      list: toasts
21    }
22
23    app.provide('toast', toast)
24  }
25}

Usage in a component:

1<script setup>
2import { inject } from 'vue'
3
4const toast = inject('toast')
5
6function onSave() {
7  toast.success('Data saved!')
8}
9
10function onError() {
11  toast.error('Satellite connection error!')
12}
13</script>

i18n Plugin - Multilingual Support

A plugin for translating texts into different languages:

1const I18nPlugin = {
2  install(app, options) {
3    const locale = ref(options.defaultLocale || 'pl')
4    const messages = options.messages || {}
5
6    function t(key) {
7      const keys = key.split('.')
8      let result = messages[locale.value]
9      for (const k of keys) {
10        result = result?.[k]
11      }
12      return result || key
13    }
14
15    function setLocale(lang) {
16      if (messages[lang]) {
17        locale.value = lang
18      }
19    }
20
21    app.provide('i18n', {
22      t,
23      locale,
24      setLocale,
25      availableLocales: Object.keys(messages)
26    })
27
28    app.directive('t', (el, binding) => {
29      el.textContent = t(binding.value)
30    })
31  }
32}

Usage:

1app.use(I18nPlugin, {
2  defaultLocale: 'pl',
3  messages: {
4    pl: {
5      nav: { home: 'Strona główna', settings: 'Ustawienia' },
6      status: { online: 'Połączony', offline: 'Rozłączony' }
7    },
8    en: {
9      nav: { home: 'Home', settings: 'Settings' },
10      status: { online: 'Connected', offline: 'Disconnected' }
11    }
12  }
13})

In a component:

1<script setup>
2import { inject } from 'vue'
3const { t, setLocale, locale } = inject('i18n')
4</script>
5
6<template>
7  <nav>
8    <a>{{ t('nav.home') }}</a>
9    <a>{{ t('nav.settings') }}</a>
10  </nav>
11  <p>Status: {{ t('status.online') }}</p>
12  <button @click="setLocale('en')">EN</button>
13  <button @click="setLocale('pl')">PL</button>
14</template>

Good Plugin Patterns

  1. Provide/Inject - prefer over globalProperties
  2. Options with default values - the plugin must work without configuration
  3. Typing - export interfaces for TypeScript
  4. Cleanup - if the plugin modifies the DOM, ensure proper cleanup
  5. Documentation - describe available options and methods
Code for this lesson: App.vue
1<script setup>
2import { inject, ref } from 'vue'
3
4const toast = inject('toast')
5const { t, locale, setLocale } = inject('i18n')
6
7function showSuccess() {
8  toast.success(t('toast.saved'))
9}
10function showError() {
11  toast.error(t('toast.connectionError'))
12}
13function showWarning() {
14  toast.warning(t('toast.lowOxygen'))
15}
16</script>
17
18<template>
19  <div class="nova-lab">
20    <h1>{{ t('title') }}</h1>
21
22    <section class="section">
23      <h2>Toast Plugin</h2>
24      <div class="btn-row">
25        <button @click="showSuccess" class="btn success">{{ t('toast.successBtn') }}</button>
26        <button @click="showError" class="btn error">{{ t('toast.errorBtn') }}</button>
27        <button @click="showWarning" class="btn warn">{{ t('toast.warnBtn') }}</button>
28      </div>
29    </section>
30
31    <section class="section">
32      <h2>i18n Plugin</h2>
33      <p>{{ t('status.label') }}: {{ t('status.online') }}</p>
34      <p>{{ t('crew.label') }}: {{ t('crew.count') }}</p>
35      <div class="btn-row" style="margin-top: 10px">
36        <button
37          @click="setLocale('pl')"
38          :class="['btn', { active: locale === 'pl' }]"
39        >PL</button>
40        <button
41          @click="setLocale('en')"
42          :class="['btn', { active: locale === 'en' }]"
43        >EN</button>
44      </div>
45      <p class="hint">Switch language and see how the texts change</p>
46    </section>
47
48    <!-- Toast container -->
49    <div class="toast-container">
50      <div
51        v-for="t in toast.list.value"
52        :key="t.id"
53        :class="['toast', t.type]"
54      >
55        {{ t.message }}
56      </div>
57    </div>
58  </div>
59</template>
60
61<style scoped>
62.nova-lab {
63  background: #0a0e27;
64  color: #c0c0c0;
65  min-height: 100vh;
66  padding: 20px;
67  font-family: monospace;
68}
69h1 { color: #00ff88; margin-bottom: 20px; }
70h2 { color: #00b4d8; font-size: 16px; margin-bottom: 10px; }
71.section {
72  background: rgba(0,180,216,0.05);
73  border: 1px solid rgba(0,180,216,0.2);
74  padding: 15px;
75  border-radius: 8px;
76  margin-bottom: 15px;
77}
78.btn-row { display: flex; gap: 10px; }
79.btn {
80  background: rgba(0,255,136,0.15);
81  color: #00ff88;
82  border: 1px solid #00ff88;
83  padding: 8px 16px;
84  border-radius: 6px;
85  cursor: pointer;
86  font-family: monospace;
87}
88.btn.active { background: #00ff88; color: #0a0e27; }
89.btn.success { color: #2ecc71; border-color: #2ecc71; }
90.btn.error { color: #e74c3c; border-color: #e74c3c; }
91.btn.warn { color: #f39c12; border-color: #f39c12; }
92.toast-container {
93  position: fixed; top: 20px; right: 20px;
94  display: flex; flex-direction: column; gap: 8px;
95  z-index: 1000;
96}
97.toast {
98  padding: 12px 20px;
99  border-radius: 8px;
100  font-family: monospace;
101  font-size: 14px;
102  animation: slideIn 0.3s ease;
103}
104.toast.success { background: rgba(46,204,113,0.9); color: white; }
105.toast.error { background: rgba(231,76,60,0.9); color: white; }
106.toast.warning { background: rgba(243,156,18,0.9); color: white; }
107.toast.info { background: rgba(0,180,216,0.9); color: white; }
108@keyframes slideIn {
109  from { transform: translateX(100%); opacity: 0; }
110  to { transform: translateX(0); opacity: 1; }
111}
112.hint { color: #666; font-size: 12px; margin-top: 8px; }
113</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. How do you implement toasts that automatically disappear after 3 seconds?

  2. 2. What does the function t('nav.home') do in an i18n plugin?

Hands-on tasks in the game

  • Vertical ordering

    Arrange the steps of how the toast.show() method works in the correct order:

  • Code editor

    Implement a ToastPlugin with a reactive array of toasts and show/success/error methods.

  • Click in order

    Arrange the correct syntax of app.provide in a plugin:

  • Horizontal ordering

    Arrange the correct inject call to retrieve the toast:

Useful articles