I've created a mixin to change the page titles, using document.title and global mixins.
My mixin file (title.ts):
import { Vue, Component } from 'vue-property-decorator'
function getTitle(vm: any): string {
const title: string = vm.title
if (title) {
return `${title} | site.com`
}
return 'Admin panel | site.com'
}
#Component
export default class TitleMixin extends Vue {
public created(): void {
const title: string = getTitle(this)
if (title) {
document.title = title
}
}
}
Then i registered this mixin globally in main.ts:
import titleMixin from '#/mixins/title'
Vue.mixin(titleMixin)
Then setting up the title in a Vue component:
#Component
export default class Login extends Vue {
public title: string = 'New title'
}
I have like 5 components in my project, if i use console.log in a mixin, i can see that it fired in every component, step by step, thus document.title is set by a last component created() hook.
How to correctly set a title for a CURRENT page?
As you said, a global mixin will affect every component in your Vue app, which means that the logic to set the document.title will fire in the created hook of every component in your app.
I think what you're looking for is VueRouter's beforeRouteEnter hook, which is one of the navigation guards that the library makes available to any of your components. A component's beforeRouteEnter hook fires immediately before the route changes to whichever one it's associated with.
In your case it would look like this:
#Component
export default class TitleMixin extends Vue {
public beforeRouteEnter(to, from, next): void {
next(vm => {
const title: string = getTitle(vm)
if (title) {
document.title = title
}
})
}
}
You'll notice that the next function (which needs to be called for the route to resolve) is being passed a callback which has a reference to the component's instance (vm), which we're passing to getTitle instead of this. This is necessary because the beforeRouteEnter hook does not have a reference to this. You can read the docs I linked to for more info.
Instead of creating a global mixin, try using a route meta field along with a global resolve guard.
First, we'll start by adding a meta field to each RouteConfig in the /router/routes.ts file:
import { RouteConfig } from 'vue-router'
export default [
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: 'login-view' */ '#views/Login.vue'),
meta: {
title: 'Login', // Set the view title
},
},
// ... Add the title meta field to each `RouteConfig`
] as RouteConfig[]
Then, we'll create a global resolve guard, to set the title meta field as the document title, in the /router/index.ts file:
import Vue from 'vue'
import Router, { Route } from 'vue-router'
import routes from './routes'
Vue.use(Router)
const router = new Router({
// ... RouterOptions
})
// Before each route resolves...
// Resolve guards will be called right before the navigation is confirmed,
// after all in-component guards and async route components are resolved.
router.beforeResolve((routeTo, routeFrom, next) => {
const documentTitle = getRouteTitle(routeTo)
// If the `Route` being navigated to has a meta property and a title meta field,
// change the document title
if (documentTitle ) {
document.title = documentTitle
}
// Call `next` to continue...
next()
function getRouteTitle(route: Route): string {
const title: string = route.meta && route.meta.title
if (title) {
return `${title} | site.com`
}
return 'Admin panel | site.com'
}
})
export default router
You should use the mixin only in the parent component for your page (the one that holds all the page itself).
Using your vue-property-decorator should be in this way:
import { Vue, Component, Mixins } from 'vue-property-decorator';
#Component
export default class Login extends Mixins(titleMixin) {
public title: string = 'New title'
}
And do not import it globally with Vue.mixin(titleMixin). In this way it is imported for all the components.
Related
I wanted to know how can I add a class to a modal in a navbar components? My navbar is in App.vue and I wanted to create a message that would add the class "is-active" to a modal in my navbar when I click on it. But I can't find the way to do that..
Thank you
Usually when you have a parent -> child relationship you can use events. In this case since you have two components that are not linked (directly) then you have two alternatives.
Using store (it is usually used in cases where your application is of a considerate size)
You can use vuex to have a central place where you will have your global state. A simple example would be:
store/main.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isModalOpen: false
},
getters: {
isModalOpen => (state) => state.isModalOpen,
},
mutations: {
setIsModalOpen (state, isOpen) {
state.isModalOpen = isOpen;
}
}
})
then you can access the store in your component as such:
<template>
<navbar :class="[isNavBarOpen ? "is-active" : ""]" />
</template>
export default {
computed: {
isNavBarOpen () {
this.$store.getters['isModalOpen']
}
}
}
Event bus (it is usually used in cases where you have a small app and do not need a global state manager)
Read more about EventBus here.
You can create a simple EventBus
services/eventBus.js
import Vue from 'vue';
const export EventBus = new Vue();
then on your component when the modal is open you can do:
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
methods: {
openStore: () => {
// your logic to open modal
EventBus.$emit('modal-open');
}
}
}
then on your App.vue you just listen to this event
App.vue
<template>
<navbar :class="[isModalOpen ? "is-active" : ""]" />
</template>
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
data() {
return {
isModalOpen: false,
}
},
created() {
EventBus.$on('modal-open', this.onModalOpen);
},
methods: {
onModalOpen() {
this.isModalOpen = true;
}
}
}
The one you will pick depends on our application structure and if you think it is complex enough to use a central state management (vuex).
There might contain some errors in the code but the main idea is there.
I'm working with typescript and vue.
In my app there is a service which is a global for every sub component.
I found this native vue solution on vue JS to inject this property on the child components.
on main.ts
const service = new MyService(...);
new Vue({
router,
provide() { return { service }; } // provide the service to be injected
render: h => h(App),
}).$mount("#app");
on any typescript vue component
import Vue from "vue";
export default Vue.extend({
inject: ["service"], // inject the service
mounted() {
console.log(this.service); // this.service does exists
},
});
This way I'm able to get the injected service on my Child components.
However I'm getting the fallowing error
Property 'service' does not exist on type 'CombinedVueInstance < Vue, {}, {}, {}, Readonly < Record < never, any > > >'.
How can I solve this typescript compilation error?
You don't need to use class components in order to get this. For object component declaration you have two ways of doing it:
Patching data return type
export default {
inject: ['myInjection'],
data(): { myInjection?: MyInjection } {
return {}
},
}
The downside is that you'll have to mark it as optional to be able to add it do data return.
Extending Vue context
declare module 'vue/types/vue' {
interface Vue {
myInjection: MyInjection
}
}
export default {
inject: ['myInjection'],
}
Using plugins
If you whant to use some service in all Vue components, you can try to use plugins.
In main.ts you just import it:
import Vue from "vue";
import "#/plugins/myService";
In plugins/myService.ts you must write someting like this:
import _Vue from "vue";
import MyService from "#/services/MyService";
export default function MyServicePlugin(Vue: typeof _Vue, options?: any): void {
Vue.prototype.$myService = new MyService(...); // you can import 'service' from other place
}
_Vue.use(MyServicePlugin);
And in any vue component you can use this:
<template>
<div> {{ $myService.name }}</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
#Component
export default class MyComponent extends Vue {
private created() {
const some = this.$myService.getStuff();
}
}
</script>
Don't forget to declare $myService on d.ts file. Somewhere in your project add file myService.d.ts with the following content:
import MyService from "#/services/MyService";
declare module "vue/types/vue" {
interface Vue {
$myService: MyService;
}
}
Using Vue property decorator
Vue-property-decorator, which internally re-exports decorators from vue-class-component, expose a series of typescript decorators that give really good intellisense. You must use the class api though.
#Inject and #Provide are two of such decorators:
In the provider:
import {Vue, Component, Provide} from 'vue-property-decorator';
#Component
export default class MyClass {
#Provide('service') service: Service = new MyServiceImpl(); // or whatever this is
}
In the provided component:
import {Vue, Component, Inject} from 'vue-property-decorator';
#Component
export default class MyClass {
#inject('service') service!: Service; // or whatever type this service has
mounted() {
console.log(this.service); // no typescript error here
},
}
This I think is the optimal solution, in the sense it gives the better intellisense available when working with Vue.
Now, however, you may don't want to change the definition of all your components or simply cannot due to external constraints. In such case you can do the next trick:
Casting this
You can cast this to any whenever you're about to use this.service. Not probably the best thing, but it works:
mounted() {
console.log((this as any).service);
},
There must be other ways, but I'm not used to Vue.extends api anymore. If you have the time and the opportunity, I strongly suggest you to switch to the class API and start using the vue-property-decorators, they really give the best intellisense.
Extend Vue for injected components only
Create an interface for the injection and extend Vue for the specific components where it is needed:
main.ts:
const service = new MyService(...);
export interface ServiceInjection {
service: MyService;
}
new Vue({
router,
provide(): ServiceInjection { return { service }; } // provide the service to be injected
render: h => h(App),
}).$mount("#app");
A components that uses your interface
import Vue from "vue";
import { ServiceInjection } from 'main.ts';
export default ( Vue as VueConstructor<Vue & ServiceInjection> ).extend({
inject: ["service"], // inject the service
mounted() {
console.log(this.service); // this.service does exists
},
});
I want to dynamically set the title of the window for each route, so in each routes: [] child object I have a meta: { title: ... } object. For example:
routes: [
{
path: 'profile/:id',
name: 'Profile',
component: Profile,
meta: {
title: function (to, cb) {
const profileId = parseInt(to.params.id);
// ... do stuff ...
}
}
}
]
I call this title function in an afterEach hook:
router.afterEach((to) => {
document.title = 'My Site';
if (to.meta && to.meta.title) {
to.meta.title(router.app, to, (result) => { document.title += ' | ' + result; });
}
});
In the ... do stuff ... portion I want to call a method from my mixin GetAndStore.js called loadProfile(profileId). I added GetAndStore into the router's mixins, but loadProfile is not available (this.loadProfile is undefined). I loaded GetAndStore globally and tried again with the same results. I've tried every configuration I can think of for the past hour I've not found any way at all to access the methods from GetAndStore from within this setup.
Any ideas of what I'm missing or what I'd need to restructure in order to access mixin methods from within routes->element->meta->title ?
The issue is that...
Mixins are a flexible way to distribute reusable functionalities for Vue components
Vue-router is not a component, nor do you have access to the component loaded for the route.
What I would suggest is making loadProfile a named export from your GetAndStore mixin. Assuming your mixin is exported like
import axios from 'axios' // just an example
export default {
methods: {
loadProfile (profileId) {
return axios.get(...)
}
}
}
you can move your function out of the default export and name it...
export function loadProfile (profileId) {
return axios.get(...)
}
export default {
methods: {
loadProfile
}
}
then you can import just the loadProfile function in your route definitions...
import { loadProfile } from 'GetAndStore'
Of course, you could also import your mixin as it is and just use
import GetAndStore from 'GetAndStore'
// snip
GetAndStore.methods.loadProfile(to.params.id).then(...)
Maybe you can try do it on beforeRouteEnter inside Profile component. So there you can grab meta title and set title of page and there you will have access to mixin methods:
beforeRouteEnter (to, from, next) {
if (to.meta && to.meta.title) {
to.meta.title(router.app, to, (result) => { document.title += ' | ' + result; });
}
},
Docs: https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards
I am building a simple website in which I have a route to category pages. I want to use a single dynamic route to move between pages.I am using vue-router for this project and the routes need to load different component
These are the desired routes for the website
example: '/shop/men' , '/shop/women','/shop/kids'
This my index.js file for router in which gender is appended in the last deciding which component to load the issue I am facing is how to handle this and load different component on depending on it
router-> index.js:
{
name: 'shop',
path: '/shop/:gender',
component: menCategoryViewsHandler('mencategory')
}
views -> viewHandler -> mencategory.js:
'use strict'
import Handle from '../mencategory.vue'
const camelize = str => str.charAt(0).toUpperCase() + str.slice(1)
// This is a factory function for dynamically creating root-level views,
// since they share most of the logic except for the type of items to display.
// They are essentially higher order components wrapping the respective vue file.
export default function ViewsHandler (type) {
console.log('1',type)
return {
name: `${type}-mencategory-view`,
asyncData ({store, route}) {
//#todo : add the ssr and routerbefore load change script here
return Promise.resolve({})
},
title: camelize(type),
render (h) {
return h(Handle,
{
props: {type},
},
)
},
}
}
You need to use dynamic route matching along with a wrapper component which renders the correct Category component. This would handled by passing props to components.
// CategoryResolver.vue
import menCategory from './mencategory'
import womenCategory from './womencategory'
import kidsCategory from './kidscategory'
const components = {
menCategory,
womenCategory,
kidsCategory
}
export default {
functional: true,
props: ['category'],
render(h, ctx) {
return h(`components[${category}Category`], ctx.data, ctx.children)
}
}
Then your router would be defined as such:
const router = new VueRouter({
routes: [
{ path: '/shop/:category', component: CategoryResolver, props: true }
]
})
Say menCategoryViewsHandler('mencategory') returns a component called MenCat. It must have a prop that matches the route above, in this example category. In MenCat you would define:
export default {
props: ['category'],
...
}
Vue router will pass the matching url prop into your component for you.
I have a Higher Order Component that receives another component as a parameter:
HOC
export default function HOC(Comp) {
return class extends Component {
doSomething() {
const temp = // the Comp's clientId prop???
}
........
}
}
Sub Component
#HOC
export default class SubComponent extends Component {
.....
static proptypes = {
clientId: PropTypes.string.isRequired
};
.......
}
Question:
Is it possible in the scenario above for the HOC to be aware of SubComponent's clientId property in its arguements and if so, how can I make the HOC aware of it for my doSomething function?
Since it's really the HOC that receives the props (or rather the component the function returns), you can just access them with this.props:
const temp = this.props.clientId;