Vue.js course Β· Module 14: Directives & Plugins
Plugin Ecosystem and Integration
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.
Popular Vue 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 MyPluginComposable vs Plugin - When to Use Which?
| Feature | Composable | Plugin |
|---|---|---|
| Registration | import in component | app.use() in main.js |
| Scope | Local (per component) | Global (entire application) |
| Global components | No | Yes (app.component) |
| Global directives | No | Yes (app.directive) |
| Provide/Inject | Possible | Natural |
| Dependency tree | Explicit imports | Implicit (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. Which of the following is the official Vue plugin for internationalization (i18n)?
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: