I'm working on the small library for showing notifications/toasts for vue 3.
My idea is to append an invisible container for my notifications during the plugin registration. So end user should not care about rendering this area. Is it possible at all?
My current plugin look like this:
export const plugin = {
install: (app: App, options?) => {
options = reactive(options || defaultOptions);
app.provide(symbol, instance);
app.component('vue3-notification', Notification);
app.component('vue3-notifications', Overlay);
console.log('app', app); // app._component is null at this point
var test = Overlay.render({ notifications: instance });
console.log('test', test); // how to attach Overlay component to app?
}
};
It seems like when a plugin is installed the vue root container is not available yet. I managed to render my component providing needed dependency(at least I hope so, it's logged to the console in the last line) but I don't know how to mount it and integrate with the main app.
My overlay component that I want to render automatically from plugin look like this:
<div class="notifications-overlay">
<Teleport to="body">
<vue3-notification
v-for="(n, index) in notifications.stack.value"
:key="n.id"
v-bind="n"
v-bind:hide="() => hide(n.id)"
></vue3-notification>
</Teleport>
</div>
And it has fixed position:
.notifications-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
}
So it does not matter where it's rendered exactly, I just want to be it automatically available inside the vue app after using my plugin.
Any thoughts?
In Vue 2, we have Vue.extend for creating a "subclass" of the base Vue constructor, and it also allows you to mount the instance on an element, which is great for this purpose.
However, this API has been removed in Vue 3. Have a read on the RFC for global API changes.
Since the global Vue is no longer a new-able constructor, Vue.extend no longer makes sense in terms of constructor extension.
The good news is, we can still achieve pretty much the same goal by leveraging createApp to render our plugin component and mount it to a DOM element.
If you don't like the idea of instantiating multiple Vue instances, you may want to check out this unofficial library called mount-vue-component. I haven't tried it myself, but it allows you to mount a component without using createApp. Although, it seems to use some internal properties (like _context) to get things done. I'd say whatever is undocumented, is likely to change. But hey.
So, back to the createApp approach. And we won't be using Teleport here. The following steps are just my preferences, so feel free to adjust them according to your use case.
Adding interfaces
import { ComponentPublicInstance } from 'vue';
export interface INotify {
(message: string): void;
}
export type CustomComponentPublicInstance = ComponentPublicInstance & {
notify: INotify;
}
We are using intersection type for our custom component instance.
Plugin implementation
import { App, createApp } from 'vue';
import Notifier from './path/to/component/Notifier.vue';
export const injectionKeyNotifier = Symbol('notifier');
export default {
install(app: App) {
const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);
const notifier = createApp(Notifier).mount(mountPoint) as CustomComponentPublicInstance;
app.provide(injectionKeyNotifier, notifier.notify);
}
}
At this point, we simply need to expose a public method (see INotify above) from the target component (Notifier.vue). I'm calling this method notify. And it takes a string argument for the message.
The component: Notify.vue
<template>
<div class="my-notifier">
<div class="msg" v-text="message"></div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent(() => {
const message = ref('');
function notify(msg: string) {
message.value = msg;
}
return {
message,
notify
}
})
</script>
Notice a named function called notify. This is the public method we talked about earlier and we'll need it exported.
Now use() it
On your entry file (e.g. main.ts):
import { createApp } from 'vue'
import App from './App.vue'
import Notifier from 'my-custom-notifier'; // Assumes a library from the NPM registry (if you mean to publish it)
createApp(App)
.use(Notifier)
.mount('#app');
Example usage that displays random notification:
<template>
<div class="home">
<button #click="showNotifs">Show notification</button>
</div>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue';
import { INotify, injectionKeyNotifier } from 'my-custom-notifier';
export default defineComponent({
name: 'Home',
setup() {
const notify = inject(injectionKeyNotifier) as INotify;
function showNotifs() {
notify('You have x unread messages.');
}
return {
showNotifs
}
}
})
</script>
And that's it! We are auto-registering the component without our users having to manually add it on the template.
Related
I am new to vue but scince i am fresh starting i decied to go straight up to TS and Setup tag because seems like the newest and best way to write vue js components.
Anyways i am now looking at this framework and more specific i'm into this example:
import { IonPage, onIonViewWillEnter, onIonViewDidEnter, onIonViewWillLeave, onIonViewDidLeave } from '#ionic/vue';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Home',
components: {
IonPage,
},
setup() {
onIonViewDidEnter(() => {
console.log('Home page did enter');
});
... the other hooks ...
},
});
And my question come from this block:
name: 'Home',
components: {
IonPage,
},
since i only worked with options api and setup tag i an not sure What does it imply to put an object in the components object and how could i acomplish the same objective with setup tag.
My objective is to make sure i am doing what this Note in the guide warns me about:
Note Pages in your app need to be using the IonPage component in order
for lifecycle methods and hooks to fire properly.
In <script setup> syntax, Home.vue would look like this:
<script setup lang="ts">
import { IonPage, onIonViewDidEnter, ...other hooks used here... } from '#ionic/vue';
onIonViewDidEnter(() => {
console.log('Home page did enter');
});
...other hooks used here...
</script>
The note you quoted draws attention to the template of any page/view contents needing to be wrapped in a <ion-page></ion-page> wrapper for the layout to function as intended, like in their examples.
Generic example:
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Some title</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
some content...
</ion-content>
</ion-page>
</template>
For the above layout, you'd need to import all used components:
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '#ionic/vue'
With <script setup> you don't need to declare them as local components, <script setup> detects them and does it for you, behind the scenes. They're available for usage inside the <template> once imported.
The component name is also inferred by <script setup> from the name of the file. In the above case, it would be Home.
To sum up: behind the scenes, <script setup> takes its contents and wraps it in a
export default defineComponent({
name: // infer from file name,
components: {
// list all imported vue components
},
setup() {
// actual contents of `<script setup>`
}
})
For this to be possible, some helpers were added (defineEmits, defineProps), which allow declaring those parts of Options API inside the setup() function.
Most notably, in this syntax setup() no longer needs a return value. All variables declared or imported inside it are made available to <template>.
Important: <script setup> is a useful tool, designed to reduce boilerplate in the majority of cases, not to replace defineComponent() completely. Read more about it in the docs.
I have a top-level component that gets data from an API at regular intervals. I want to make a single API request and get all the data for my app in one place to reduce the number of requests to the API server. (FYI, my project looks like it's using Typescript but I'm not yet.)
Everything works fine in my top-level component:
//Parent
<script lang="ts">
import { defineComponent, ref, provide, inject, onMounted } from 'vue'
import getData from '#/data.ts'
export default defineComponent({
setup(){
const workspaces = ref([])
onMounted(async () => {
let api = inject('api') //global var from main.ts
let data = await getData(api) //API request inside data.ts
console.log(data.workspaces) //<-- data looks good here
workspaces.value = data.workspaces
//Trying to share workspaces with other components
provide('workspaces', data.workspaces)
})
return {
workspaces
}
}
})
</script>
<template>
{{ workspaces}} <!-- workspaces render fine here -->
</template>
But my child can't use the provide data via inject:
//Child
<script lang="ts">
import { defineComponent, inject, onMounted, ref } from 'vue'
export default defineComponent({
setup(){
let workspaces = ref([])
onMounted(async () => {
workspaces.value = await inject('workspaces') //<-- just a guess; doesn't work
})
return{
workspaces
}
}
})
</script>
<template>
{{ workspaces }} <!-- nothing here -->
</template>
I've made a couple assumptions as to the cause of the problem:
The Child component loads before the parent's async stuff is done, and is therefore empty.
I probably can't use project/inject in async scenarios like this.
So how can I share async data from an API across components in my app? Is my only option to go back to old-school props and pass the data down manually?
provide/inject are misused and subject to race conditions. Composition API is generally supposed to be at used on component initialization (setup, before any await) and not in onMounted. Even if there weren't such restriction, onMounted in parent component runs after the one in child component and can't provide a value at the time when a child is mounted.
The purpose of refs is to provide a reference to a value that can be changed later, so it could be passed by reference and stay reactive, this property isn't currently used.
It should be in parent component:
setup(){
const workspaces = ref([])
provide('workspaces', workspaces)
let api = inject('api')
onMounted(async () => {
let data = await getData(api)
workspaces.value = data.workspaces
})
return { workspaces }
In child component:
setup(){
let workspaces = inject('workspaces')
return { workspaces }
I'm using the plugin vue-flag-icon - https://www.npmjs.com/package/vue-flag-icon for flags, In their documentation I saw the following steps for initialising.
import FlagIcon from 'vue-flag-icon'
Vue.use(FlagIcon);
Do I need to have this? This is not specified in their docs!
export default {
components: {
FlagIcon. /// do i need to give it here ?
},
}
What is the purpose of this Vue.use(...), It's working fine even if I remove that. Can somebody help me out?
Checked the vue documentation - https://v2.vuejs.org/v2/guide/plugins.html.
Did not get a clear idea about it
Vue.use automatically prevents you from using the same plugin more than once, so calling it multiple times on the same plugin will install the plugin only once.
For the flag component, it declares a global component that you can refer within your components, such that in the following example will render correctly.
in vue-flag-icon source code
install: function (Vue) {
if (VuePlugin.installed) {
return;
}
VuePlugin.installed = true;
Vue.component('flag', Flag);
}
You can see that with Vue.component('flag', Flag) that this is a root level component declaration, therefore, in your components, you do not require to declare something like following
Unnecessary if using Vue.use
import { Flag } from "vue-flag-icon"
export default {
components: { Flag }
}
If Vue.use is not used, the flag tag in the template will throw an error if you do not include it as a component within your vue init.
<template>
<div id="app">
<img src="./assets/logo.png">
<flag iso="it" />
<flag iso="gb" />
<flag iso="us" />
</div>
</template>
<script>
export default {
name: 'app',
}
</script>
I have Vue SPA that I'm trying to migrate to nuxt. I am using vue2leaflet in a component that I enclosed in <client-only> tags but still getting an error from nuxt saying that window is not defined.
I know I could use nuxt-leaflet or create a plugin but that increases the vendor bundle dramatically and I don't want that. I want to import the leaflet plugin only for the components that need it. Any way to do this?
<client-only>
<map></map>
</client-only>
And the map component:
<template>
<div id="map-container">
<l-map
style="height: 80%; width: 100%"
:zoom="zoom"
:center="center"
#update:zoom="zoomUpdated"
#update:center="centerUpdated"
#update:bounds="boundsUpdated"
>
<l-tile-layer :url="url"></l-tile-layer>
</l-map>
</div>
</template>
<script>
import {
LMap,
LTileLayer,
LMarker,
LFeatureGroup,
LGeoJson,
LPolyline,
LPolygon,
LControlScale
} from 'vue2-leaflet';
import { Icon } from 'leaflet';
import 'leaflet/dist/leaflet.css';
// this part resolve an issue where the markers would not appear
delete Icon.Default.prototype._getIconUrl;
export default {
name: 'map',
components: {
LMap,
LTileLayer,
LMarker,
LFeatureGroup,
LGeoJson,
LPolyline,
LPolygon,
LControlScale
},
//...
I found a way that works though I'm not sure how. In the parent component, you move the import statement inside component declarations.
<template>
<client-only>
<map/>
</client-only>
</template>
<script>
export default {
name: 'parent-component',
components: {
Map: () => if(process.client){return import('../components/Map.vue')},
},
}
</script>
<template>
<client-only>
<map/>
</client-only>
</template>
<script>
export default {
name: 'parent-component',
components: {
Map: () =>
if (process.client) {
return import ('../components/Map.vue')
},
},
}
</script>
The solutions above did not work for me.
Why? This took me a while to find out so I hope it helps someone else.
The "problem" is that Nuxt automatically includes Components from the "components" folder so you don't have to include them manually. This means that even if you load it dynamically only on process.client it will still load it server side due to this automatism.
I have found the following two solutions:
Rename the "components" folder to something else to stop the automatic import and then use the solution above (process.client).
(and better option IMO) there is yet another feature to lazy load the automatically loaded components. To do this prefix the component name with "lazy-". This, in combination with will prevent the component from being rendered server-side.
In the end your setup should look like this
Files:
./components/map.vue
./pages/index.html
index.html:
<template>
<client-only>
<lazy-map/>
</client-only>
</template>
<script>
export default {
}
</script>
The <client-only> component doesn’t do what you think it does. Yes, it skips rendering your component on the server side, but it still gets executed!
https://deltener.com/blog/common-problems-with-the-nuxt-client-only-component/
Answers here are more focused towards import the Map.vue component while the best approach is probably to properly load the leaflet package initially inside of Map.vue.
Here, the best solution would be to load the components like so in Map.vue
<template>
<div id="map-container">
<l-map style="height: 80%; width: 100%">
<l-tile-layer :url="url"></l-tile-layer>
</l-map>
</div>
</template>
<script>
import 'leaflet/dist/leaflet.css'
export default {
name: 'Map',
components: {
[process.client && 'LMap']: () => import('vue2-leaflet').LMap,
[process.client && 'LTileLayer']: () => import('vue2-leaflet').LTileLayer,
},
}
</script>
I'm not a leaflet expert, hence I'm not sure if Leaflet care if you import it like import('vue2-leaflet').LMap but looking at this issue, it looks like it doesn't change a lot performance-wise.
Using Nuxt plugins is NOT a good idea as explained by OP because it will increase the whole bundle size upfront. Meaning that it will increase the loading time of your whole application while the Map is being used only in one place.
My How to fix navigator / window / document is undefined in Nuxt answer goes a bit more in detail about this topic and alternative approaches to solve this kind of issues.
Especially if you want to import a single library like vue2-editor, jsplumb or alike.
Here is how I do it with Nuxt in Universal mode:
this will: 1. Work with SSR
2. Throw no errors related to missing marker-images/shadow
3. Make sure leaflet is loaded only where it's needed (meaning no plugin is needed)
4. Allow for custom icon settings etc
5. Allow for some plugins (they were a pain, for some reason I thought you could just add them as plugins.. turns out adding them to plugins would defeat the local import of leaflet and force it to be bundled with vendors.js)
Wrap your template in <client-only></client-only>
<script>
let LMap, LTileLayer, LMarker, LPopup, LIcon, LControlAttribution, LControlZoom, Vue2LeafletMarkerCluster, Icon
if (process.client) {
require("leaflet");
({
LMap,
LTileLayer,
LMarker,
LPopup,
LIcon,
LControlAttribution,
LControlZoom,
} = require("vue2-leaflet/dist/vue2-leaflet.min"));
({
Icon
} = require("leaflet"));
Vue2LeafletMarkerCluster = require('vue2-leaflet-markercluster')
}
import "leaflet/dist/leaflet.css";
export default {
components: {
"l-map": LMap,
"l-tile-layer": LTileLayer,
"l-marker": LMarker,
"l-popup": LPopup,
"l-icon": LIcon,
"l-control-attribution": LControlAttribution,
"l-control-zoom": LControlZoom,
"v-marker-cluster": Vue2LeafletMarkerCluster,
},
mounted() {
if (!process.server) //probably not needed but whatever
{
// This makes sure the common error that the images are not found is solved, and also adds the settings to it.
delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
// iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), // if you want the defaults
// iconUrl: require('leaflet/dist/images/marker-icon.png'), if you want the defaults
// shadowUrl: require('leaflet/dist/images/marker-shadow.png') if you want the defaults
shadowUrl: "/icon_shadow_7.png",
iconUrl: "/housemarkerblue1.png",
shadowAnchor: [10, 45],
iconAnchor: [16, 37],
popupAnchor: [-5, -35],
iconSize: [23, 33],
// staticAnchor: [30,30],
});
}
},
And there's proof using nuxt build --modern=server --analyze
https://i.stack.imgur.com/kc6q4.png
I am replicating my answer here since this is the first post that gets reached searching for this kind of problem, and using the solutions above still caused nuxt to crash or error in my case.
You can import your plugin in your mounted hook, which should run in the client only. So:
async mounted() {
const MyPlugin = await import('some-vue-plugin');
Vue.use(MyPlugin);
}
I do not know about the specific plugin you are trying to use, but in my case I had to call Vue.use() on the default property of the plugin, resulting in Vue.use(MyPlugin.default).
I have a Vue 2 project that has many (50+) single-file components. I use Vue-Router for routing and Vuex for state.
There is a file, called helpers.js, that contains a bunch of general-purpose functions, such as capitalizing the first letter of a string. This file looks like this:
export default {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
My main.js file initializes the app:
import Vue from 'vue'
import VueResource from "vue-resource"
import store from "./store"
import Router from "./router"
import App from "./components/App.vue"
Vue.use(VueResource)
const app = new Vue({
router: Router,
store,
template: '<app></app>',
components: { App },
}).$mount('#app')
My App.vue file contains the template:
<template>
<navbar></navbar>
<div class="container">
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {
// stuff
}
}
}
</script>
I then have a bunch of single-file components, which Vue-Router handles navigating to inside the <router-view> tag in the App.vue template.
Now let's say that I need to use the capitalizeFirstLetter() function inside a component that is defined in SomeComponent.vue. In order to do this, I first need to import it:
<template>Some Component</template>
<script>
import {capitalizeFirstLetter} from '../helpers.js'
export default {
data() {
return {
myString = "test"
}
},
created() {
var newString = this.capitalizeFirstLetter(this.myString)
}
}
</script>
This becomes a problem quickly because I end up importing the function into many different components, if not all of them. This seems repetitive and also makes the project harder to maintain. For example if I want to rename helpers.js, or the functions inside it, I then need to go into every single component that imports it and modify the import statement.
Long story short: how do I make the functions inside helpers.js globally available so that I can call them inside any component without having to first import them and then prepend this to the function name? I basically want to be able to do this:
<script>
export default {
data() {
return {
myString = "test"
}
},
created() {
var newString = capitalizeFirstLetter(this.myString)
}
}
</script>
inside any component without having to first import them and then prepend this to the function name
What you described is mixin.
Vue.mixin({
methods: {
capitalizeFirstLetter: str => str.charAt(0).toUpperCase() + str.slice(1);
}
})
This is a global mixin. with this ALL your components will have a capitalizeFirstLetter method, so you can call this.capitalizeFirstLetter(...) from component methods or you can call it directly as capitalizeFirstLetter(...) in component template.
Working example: http://codepen.io/CodinCat/pen/LWRVGQ?editors=1010
See the documentation here: https://v2.vuejs.org/v2/guide/mixins.html
Otherwise, you could try to make your helpers function a plugin:
import Vue from 'vue'
import helpers from './helpers'
const plugin = {
install () {
Vue.helpers = helpers
Vue.prototype.$helpers = helpers
}
}
Vue.use(plugin)
In your helper.js export your functions, this way:
const capFirstLetter = (val) => val.charAt(0).toUpperCase() + val.slice(1);
const img2xUrl = (val) => `${val.replace(/(\.[\w\d_-]+)$/i, '#2x$1')} 2x`;
export default { capFirstLetter, img2xUrl };
or
export default {
capFirstLetter(val) {
return val.charAt(0).toUpperCase() + val.slice(1);
},
img2xUrl(val) {
return `${val.replace(/(\.[\w\d_-]+)$/i, '#2x$1')} 2x`;
},
};
You should then be able to use them anywhere in your components using:
this.$helpers.capitalizeFirstLetter()
or anywhere in your application using:
Vue.helpers.capitalizeFirstLetter()
You can learn more about this in the documentation: https://v2.vuejs.org/v2/guide/plugins.html
Create a new mixin:
"src/mixins/generalMixin.js"
Vue.mixin({
methods: {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
}
})
Then import it into your main.js like:
import '#/mixins/generalMixin'
From now on you will be able to use the function like this.capitalizeFirstLetter(str) within your component script or without this in a template. i.e.:
<template>
<div>{{ capitalizeFirstLetter('hello') }}</div>
</template>
You have to use this because you mixed a method into the main Vue instance. If there are ways of removing this it will probably involve something unconventional, this at least is a documented way of sharing functions which will be easy to understand for any future Vue devs to your project.
Using Webpack v4
Create a separate file for readability (just dropped mine in plugins folder).
Reproduced from #CodinCat and #digout responses.
//resources/js/plugins/mixin.js
import Vue from 'vue';
Vue.mixin({
methods: {
capitalizeFirstLetter: str => str.charAt(0).toUpperCase() + str.slice(1),
sampleFunction() {
alert('Global Functions');
},
}
});
Then, import in your main.js or app.js file.
//app.js
import mixin from './plugins/mixin';
USAGE:
Call this.sampleFunction() or this.capitalizeFirstLetter().
Use a global filter if it only concerns how data is formatted when rendered. This is the first example in the docs:
{{ message | capitalize }}
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
Great question. In my research I found vue-inject can handle this in the best way. I have many function libraries (services) kept separate from standard vue component logic handling methods. My choice is to have component methods just be delegators that call the service functions.
https://github.com/jackmellis/vue-inject
Import it in the main.js file just like 'store' and you can access it in all the components.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
render: h => h(App)
})