Vue.js course · Module 14: Directives & Plugins
Creating Useful Plugins
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
- Provide/Inject - prefer over globalProperties
- Options with default values - the plugin must work without configuration
- Typing - export interfaces for TypeScript
- Cleanup - if the plugin modifies the DOM, ensure proper cleanup
- 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. How do you implement toasts that automatically disappear after 3 seconds?
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: