Dynamic layout in Vue 3 with Vite - vue.js

New to Vue and Vite but trying to get dynamic layouts working properly here. I believe I have what is needed but the issue it the meta seems to always come up as an empty object or undefined.
AppLayout.vue
<script setup lang="ts">
import AppLayoutDefault from './stub/AppLayoutDefault.vue'
import { markRaw, watch } from 'vue'
import { useRoute } from 'vue-router'
const layout = markRaw(AppLayoutDefault)
const route = useRoute()
console.log('Current path: ', route.path)
console.log('Route meta:', route.meta)
watch(
() => route.meta,
async (meta) => {
try {
const component = await import(`./stub/${meta.layout}.vue`)
layout.value = component?.default || AppLayoutDefault
} catch (e) {
layout.value = AppLayoutDefault
}
},
{ immediate: true }
)
</script>
<template>
<component :is="layout"> <router-view /> </component>
</template>
App.vue
<script setup lang="ts">
import AppLayout from '#/layouts/AppLayout.vue'
</script>
<template>
<AppLayout>
<router-view />
</AppLayout>
</template>
Each and every route has the appropriate meta set with a property called layout.
I just can't seem to get he layout applied correctly on the first load or any click of a link in the navbar(which are just router-link) for that matter.

The main problem is layout is initialized as markRaw(), which is not a reactive ref:
const layout = markRaw(AppLayoutDefault) // ❌ not reactive
Solution
Initialize layout as a ref.
Instead of watching the full route object, watch route.meta?.layout because that's the only relevant field for the handler.
Wrap layout's new values with markRaw() to avoid reactivity on the component definition.
const layout = ref() 1️⃣
watch(
() => route.meta?.layout as string | undefined, 2️⃣
async (metaLayout) => {
try {
const component = metaLayout && await import(/* #vite-ignore */ `./${metaLayout}.vue`)
layout.value = markRaw(component?.default || AppLayoutDefault) 3️⃣
} catch (e) {
layout.value = markRaw(AppLayoutDefault) 3️⃣
}
},
{ immediate: true }
)
demo

The solution from Tony is great. But you need to add computed inside the watch because it will prevent code execution for the very first "layout" variable initialized which is still undefined and it will cause unnecessary rendering. So, please add the computed function inside the watch function. Also you need to watch "route.path" not the "route.meta?.layout".
import DefaultLayout from '#/layouts/Default.vue'
import { markRaw, ref } from '#vue/reactivity'
import { computed, watch } from '#vue/runtime-core'
import { useRoute } from 'vue-router'
const layout = ref()
const route = useRoute()
watch(
computed(() => route.path), async () => {
let metaLayout = route.meta?.layout
try {
const metaLayoutComponent = metaLayout && await import(`./layouts/${metaLayout}.vue`)
layout.value = markRaw(metaLayoutComponent?.default || DefaultLayout)
} catch (error) {
layout.value = markRaw(DefaultLayout)
}
}
);

Related

How can I access context from a <script setup> tag in vue3?

Previously I used the standard < script > tag with the setup function within it:
<script>
import { ref } from 'vue'
import useLogin from '../composables/useLogin'
const email = ref('')
const password = ref('')
export default {
setup(props, context){
const {error, login} = useLogin()
const handleLoginSubmit = async () => {
await login(email.value, password.value)
if(!error.value){
context.emit('login')
}
router.push({name: 'home'})
}
return { email, password, handleLoginSubmit }
}
}
</script>
Now I tried to switch to the < script setup > tag, but I don't know how to access the context attribute here.
<script setup>
import { ref } from 'vue'
import useLogin from '../composables/useLogin'
import router from '../router'
const email = ref('')
const password = ref('')
const {error, login} = useLogin()
const handleLoginSubmit = async () => {
await login(email.value, password.value)
if(!error.value){
context.emit('login')
}
router.push({name: 'home'})
}
</script>
I get the following error due to the missing definition of the context.
Uncaught ReferenceError: context is not defined
I tried to import context from vue in different ways, but nothing seems to be the correct.
Context is not available in <script setup> if you want to use emits and props, there are some helpers(macros) exposed to you.
In your case you are trying to use emit, so in script setup it is going to be something like:
<script setup>
const emit = defineEmits(['event1','event2']);
...
emit('event1','some-value');
</script>
So, in your example, that would be:
const emit = defineEmits(['login']);
const handleLoginSubmit = async () => {
await login(email.value, password.value)
if(!error.value){
emit('login')
}
router.push({name: 'home'})
}
For props, you use defineProps. See this for more info

Slots not working in my VUE CustomElement (defineCustomElement)

I have created this initialization of CustomElement in VUE 3 from various sources on the web (doc's, stackoverflow, etc).
Unfortunately, nowhere was discussed how to deal with slots in this type of initialization.
If I understand it correctly, it should work according to the documentation.
https://vuejs.org/guide/extras/web-components.html#slots
import { defineCustomElement, h, createApp, getCurrentInstance } from "vue";
import audioplayer from "./my-audioplayer.ce.vue";
import audioplayerlight from "./my-audioplayerlight.ce.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
export const defineCustomElementWrapped = (component, { plugins = [] } = {}) =>
defineCustomElement({
styles: component.styles,
props: component.props,
setup(props, { emit }) {
const app = createApp();
plugins.forEach((plugin) => {
app.use(plugin);
});
const inst = getCurrentInstance();
Object.assign(inst.appContext, app._context);
Object.assign(inst.provides, app._context.provides);
return () =>
h(component, {
...props,
});
},
});
customElements.define(
"my-audioplayer",
defineCustomElementWrapped(audioplayer, { plugins: [pinia] })
);
customElements.define(
"my-audioplayerlight",
defineCustomElementWrapped(audioplayerlight, { plugins: [pinia] })
);
I suspect that I forgot something during initialization and the contents of the slot are not passed on.
A little late, but we are working with this approach doing Web Components with Vue 3 and this workaround, adding Vue Component context to Custom Elements.
setup(props, { slots })
And then:
return () =>
h(component, {
...props,
...slots
});
Thanks #tony19, author of this workaround.

Toggle div in parent component when button is clicked from child component using Composable

I have 3 pages:
spinner.js - composable. Function to toggle loading spinner
App.vue - parent component
Test.vue - child component
What I need to do is when I click the button from Test.vue, the App.vue should know that the value of loading has been changed and should show/hide the div accordingly.
I tried using watch but I don't really have the total grasp on how to use it. I tried reading the documents but it's still vague for me.
Should I use emit instead for this scenario? But I need to use the composable spinner.js
spinner.js
import { ref } from 'vue'
export default function useSpinner() {
const loading = ref(false)
const isLoading = async (isLoading) => {
loading.value = isLoading
}
return {
loading,
isLoading,
}
}
Test.vue
<template>
<button #click="showSpinner">Show Spinner</button>
</template>
<script>
import useSpinner from "./composables/spinner.js"
export default {
setup() {
const { isLoading } = useSpinner()
// Calls `isLoading()` from `spinner.js` to change the value of `loading`
const showSpinner = async () => {
isLoading(true)
}
return {
loading,
showSpinner,
}
},
}
</script>
App.vue
<template>
<div v-if="loading">Hello Loading Spinner</div>
</template>
<script>
import useSpinner from "./composables/spinner.js"
import { watch } from "vue";
export default {
setup() {
const { loading } = useSpinner()
// should watch when `loading` was changed to toggle div
watch(loading, (currentValue, oldValue) => {
console.log(currentValue);
console.log(oldValue);
});
return {
loading,
}
},
}
</script>
const loading = ref(false) needs to be outside the export.
example:
import { ref } from 'vue';
const loading = ref(false);
export default function useSpinner() {
const isLoading = (isLoading) => {
loading.value = isLoading
}
return {
loading,
isLoading,
};
}
If not, both Test.vue and App.vue will have their own instance of loading when you import the loading ref using the function.

Why dynamic component is not working in vue3?

Here is a working Vue2 example:
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Redone under Vue3 option does not work. No errors occur, but the component does not appear.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
Has anyone studied the vue2 beta version? Help me please. Sorry for the clumsy language, I use Google translator.
Leave everything in the template as in Vue2
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
Change only in "setup" using defineAsyncComponent
You can learn more about defineAsyncComponent here
https://labs.thisdot.co/blog/async-components-in-vue-3
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")) : '')
const onClick = () => {
isShow.value = true;
}
Try this
import DynamicComponent from './DynamicComponent.vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? DynamicComponent : '')
});
...
return {
state,
...
}
}
}
The issue with this seems to be to do with the way we register components when we use the setup script - see the official docs for more info. I've found that you need to register the component globally in order to reference it by string in the template.
For example, for the below Vue component:
<template>
<component :is="item.type" :item="item"></component>
</template>
<script setup lang="ts">
// Where item.type contains the string 'MyComponent'
const props = defineProps<{
item: object
}>()
</script>
We need to register the component in the main.ts, as such:
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
var app = createApp(App);
app.component('MyComponent', MyComponent)
app.mount('#app')
Using 'watch' everything works.
<template>
<component :is="componentPath"/>
</template>
<script lang="ts">
import {defineComponent, ref, watch, SetupContext} from "vue";
export default defineComponent({
props: {
path: {type: String, required: true}
},
setup(props: { path: string }, context: SetupContext) {
const componentPath = ref("");
watch(
() => props.path,
newPath => {
if (newPath !== "")
import("#/" + newPath + ".vue").then(val => {
componentPath.value = val.default;
context.emit("loaded", true);
});
else {
componentPath.value = "";
context.emit("loaded", false);
}
}
);
return {componentPath};
}
});
</script>

How to get current name of route in Vue?

I want to get the name of the current route of vue-router, i have a component menu with navigation to another componentes, so i want to dispaly the name of the current route.
I have this:
created(){
this.currentRoute;
//this.nombreRuta = this.$route.name;
},
computed:{
currentRoute:{
get(){
this.nombreRuta = this.$route.name;
}
}
}
But the label of the name of the route does not change, the label only show the name of the first loaded route.
Thank You
EDIT:
Image to show what i want
You are using computed incorrectly. You should return the property in the function. See the docs for more information.
Here is your adapted example:
computed: {
currentRouteName() {
return this.$route.name;
}
}
You can then use it like this:
<div>{{ currentRouteName }}</div>
You can also use it directly in the template without using a computed property, like this:
<div>{{ $route.name }}</div>
Vue 3 + Vue Router 4
Update 5/03/2021
If you are using Vue 3 and Vue Router 4, here is two simplest ways to get current name of route in setup hook:
Solution 1: Use useRoute
import { useRoute } from 'vue-router';
export default {
setup () {
const route = useRoute()
const currentRouteName = computed(() => route.name)
return { currentRouteName }
}
}
Solution 2: Use useRouter
import { useRouter } from 'vue-router';
export default {
setup () {
const router = useRouter()
const currentRouteName = computed(() => router.currentRoute.value.name;)
return { currentRouteName }
}
}
I use this...
this.$router.history.current.path
In Composition API, this works
import { useRouter } from 'vue-router'
const router = useRouter()
let currentPathObject = router.currentRoute.value;
console.log("Route Object", currentPathObject)
// Pick the values you need from the object
I used something like this:
import { useRoute } from 'vue-router';
then declared
const route = useRoute();
Finally if you log route object - you will get all properties I used path for my goal.
This is how you can access AND watch current route's name using #vue/composition-api package with Vue 2 in TypeScript.
<script lang="ts">
import { defineComponent, watch } from '#vue/composition-api';
export default defineComponent({
name: 'MyCoolComponent',
setup(_, { root }) {
console.debug('current route name', root.$route.name);
watch(() => root.$route.name, () => {
console.debug(`MyCoolComponent- watch root.$route.name changed to ${root.$route.name}`);
});
},
});
</script>
I will update this answer once Vue 3.0 and Router 4.0 gets released!
I use this...
this.$route.name
In my Laravel app I created a router.js file and I can access the router object in any vue component like this.$route
I usually get the route like this.$route.path
Using composition API,
<template>
<h1>{{Route.name}}</h1>
</template>
<script setup>
import {useRoute} from 'vue-router';
const Route = useRoute();
</script>
Using Vue 3 and Vue Router 4 with Composition API and computed:
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// computed
const currentRoute = computed(() => {
return router.currentRoute.value.name
})
</script>
<template>
<div>{{ currentRoute }}</div>
</template>
⚠ If you don't set a name in your router like so, no name will be displayed:
const routes = [
{ path: '/step1', name: 'Step1', component: Step1 },
{ path: '/step2', name: 'Step2', component: Step2 },
];
In Vue 3.2 using Composition API
<script lang="ts" setup>
import { useRoute } from "vue-router";
const route = useRoute();
const currentRouteName = computed(() => {
return route.name;
});
</script>
<template>
<div>
Using computed:{{currentRouteName}}
or without using computed: {{route.name}}
</div>
</template>
This is how you can get id (name) of current page in composition api (vue3):
import { useRoute } from 'vue-router';
export function useFetchPost() {
const currentId = useRoute().params.id;
const postTitle = ref('');
const fetchPost = async () => {
try {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${currentId}`
);
postTitle.value = response.data.title;
} catch (error) {
console.log(error);
} finally {
}
};
onMounted(fetchPost);
return {
postTitle,
};
}
I'm using this method on vue 3 & vue-router 4
It works great!
<script>
import { useRoute } from 'vue-router'
export default {
name: 'Home',
setup() {
const route = useRoute();
const routeName = route.path.slice(1); //route.path will return /name
return {
routeName
}
}
};
</script>
<p>This is <span>{{ routeName }}</span></p>
I've Tried and it Worked:
Use Following in Your Elements;
{{ this.$route.path.slice(1) }}
this.$router.currentRoute.value.name;
Works just like this.$route.name.
Vue 3 + Vue Router 4 + Pinia store (or any other place outside of vue components)
#KitKit up there gave an example how to get route if you are using Vue 3 and Vue Router 4 in setup hook. However, what about state management in Pinia store ?
In vue#2 and vue-router#3.5.1: We could have used router.currentRoute.query.returnUrl like so (example in vuex state management):
import router from "#/router";
const state = initialState;
const getters = {};
const actions = { // your actions };
const mutations = {
loginSuccess(state, user) {
let returnUrl = "";
if(router.currentRoute.query.returnUrl != undefined)
returnUrl = router.currentRoute.query.returnUrl;
},
};
export default {
state,
getters,
actions,
mutations,
};
export const authentication = {
actions: {},
mutations: {},
};
In vue#3 and vue-router#4: We have to append value to currentRoute like so:
import router from '#/router';
export const authenticationStore = defineStore('authUser', {
state: (): State => ({
// your state
}),
getters: {
// your getters
},
actions: {
loginSuccess(user: object) {
let returnUrl = '';
if (router.currentRoute.value.query.returnUrl != undefined)
returnUrl = router.currentRoute.value.query.returnUrl;
},
},
});