I am facing a problem using the onUpdated lifecycle hook in Vuejs3 Composition API.
It is not called when a reactive value is updated.
I have reproduced the issue with a very simple app.
It has one child component:
<script setup lang="ts">
import { h, ref, onUpdated } from 'vue'
const open = ref(false)
const toggle = () => {
open.value = !open.value
}
defineExpose({ toggle })
onUpdated(() => {
console.log("Updated")
})
const render = () =>
open.value ? h(
'DIV',
"Child Component"
) :
null
</script>
<template>
<render />
</template>
Then this component is used by the app:
<script setup lang="ts">
import { ref } from 'vue'
import Child from './components/Child.vue'
const menu = ref(null)
</script>
<template>
<main>
<button #click="menu.toggle()">Click Me</button>
<Child ref="menu" />
</main>
</template>
<style>
</style>
But when the button is clicked in the app, although the "Child Component" text is shown, proving that the render function is called, the onUpdated callback is not executed.
This must have something to do with the way the render function is called, or the conditional rendering because if I use v-if in a template instead, it works fine. But in my case, I do need an explicit render function.
Can anyone help?
This is probably a bug in <script setup>, as that same code works in setup().
A workaround is to switch to setup() if you need to use onUpdated():
<script lang="ts">
import { h, ref, onUpdated } from 'vue'
export default {
setup(props, { expose }) {
const open = ref(false)
const toggle = () => {
open.value = !open.value
}
expose({ toggle })
onUpdated(() => {
console.log("Updated")
})
const render = () => open.value ? h('DIV', "Child Component") : null
return render
}
}
</script>
demo
Related
I have a Parent and child components as the following :
Parent Component :
<script setup>
import ChildComp from '#/components/ChildComp.vue'
import { ref, computed } from 'vue'
const childComp = ref(null)
const clickMe = () => {
console.log(childComp.value.selectedRadio) // <====== UNDEFINED
childComp.value.hello() // <======= RETURNS hello is not a function
};
</script>
<template>
<ChildComp ref="childComp" />
<button #click="clickMe()" type="button">Click Me!</button>
</template>
Child component :
<script setup>
import { ref } from 'vue'
const selectedRadio = ref("")
selectedRadio.value = "test"
const hello = () => {
console.log("hey from child");
};
defineExpose({
hello,
selectedRadio,
});
</script>
<template>
<div>
<h3>Hi! I am the child component</h3>
</div>
</template>
As described in code, when the button is clicked I have the following errors :
console.log(childComp.value.selectedRadio) // <====== UNDEFINED
childComp.value.hello() // <======= RETURNS hello is not a function
Knowing that the childComp returns correctly the component Object and when accessing childComp.value.$.exposed I can see both selectedRadio property and hello function.
Vue version : 3.2.45
Could you please help me?
Thank you
As far as I can see your code is fine, take a look here pls.
I solved the issue by adding : #vue/compiler-sfc": "^3.2.45 in my devDependencies
I am using the composition api plugin for vue2 (https://github.com/vuejs/composition-api) to reuse composables in my app.
I have two components that reuse my modalTrigger.js composable, where I'd like to declare some sort of shared state (instead of using a bloated vuex state management).
So in my components I do something like:
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'SearchButton',
setup(props, context) {
const { getModalOpenState, setModalOpenState } = modalTrigger();
return {
getModalOpenState,
setModalOpenState,
};
},
};
And in my modalTrigger I have code like:
import { computed, ref, onMounted } from '#vue/composition-api';
let modalOpen = false; // needs to be outside to be accessed from multiple components
export default function () {
modalOpen = ref(false);
const getModalOpenState = computed(() => modalOpen.value);
const setModalOpenState = (state) => {
console.log('changing state from: ', modalOpen.value, ' to: ', state);
modalOpen.value = state;
};
onMounted(() => {
console.log('init trigger');
});
return {
getModalOpenState,
setModalOpenState,
};
}
This works, but only because I declare the modalOpen variable outside of the function.
If I use this:
export default function () {
const modalOpen = ref(false); // <------
const getModalOpenState = computed(() => modalOpen.value);
...
It is not reactive because the modalTrigger is instantiated twice, both with it's own reactive property.
I don't know if that is really the way to go, it seems, that I am doing something wrong.
I also tried declaring the ref outside:
const modalOpen = ref(false);
export default function () {
const getModalOpenState = computed(() => modalOpen.value);
But this would throw an error:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function.
So what would be the correct way to achieve this?
I somehow expected Vue to be aware of the existing modalTrigger instance and handling duplicate variable creation itself...
Well, anyway, thanks a lot in advance for any hints and tipps.
Cheers
Edit:
The complete header.vue file:
<template>
<header ref="rootElement" :class="rootClasses">
<button #click="setModalOpenState(true)">SET TRUE</button>
<slot />
</header>
</template>
<script>
import { onMounted, computed } from '#vue/composition-api';
import subNavigation from '../../../../composables/subNavigation';
import mobileNavigation from '../../../../composables/mobileNavigation';
import search from '../../../../composables/searchButton';
import { stickyNavigation } from '../../../../composables/stickyNav';
import metaNavigation from '../../../../composables/metaNavigation';
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'Header',
setup(props, context) {
const { rootElement, rootClasses } = stickyNavigation(props, context);
mobileNavigation();
subNavigation();
search();
metaNavigation();
const { getModalOpenState, setModalOpenState } = modalTrigger();
onMounted(() => {
console.log('Header: getModalOpenState: ', getModalOpenState.value);
setModalOpenState(true);
console.log('Header: getModalOpenStat: ', getModalOpenState.value);
});
return {
rootClasses,
rootElement,
getModalOpenState,
setModalOpenState,
};
},
};
</script>
The composition API is setup somewhere else where there are Vue components mounted a bit differently than you normally would.
So I can't really share the whole code,but it has this inside:
import Vue from 'vue';
import CompositionApi from '#vue/composition-api';
Vue.use(CompositionApi)
The composition API and every other composable works just fine...
I am using the experimental script setup to create a learn enviroment. I got a selfmade navigation bar with open a single component.
I am having trouble using the <component :is="" /> method. This method is described in the docs under component basics -> dynamic-components
In the Vue 3 Composition API, it works as expected:
<template>
<NavigationBar
#switchTab="changeTab"
:activeTab="tab"
/>
<component :is="tab" />
</template>
<script>
import { ref } from 'vue'
import NavigationBar from './components/NavigationBar.vue'
import TemplateSyntax from './components/TemplateSyntax.vue'
import DataPropsAndMethods from './components/DataPropsAndMethods.vue'
export default {
components: {
NavigationBar,
TemplateSyntax,
DataPropsAndMethods
},
setup () {
const tab = ref('DataPropsAndMethods')
function changeTab (newTab) {
tab.value = newTab
}
return {
changeTab,
tab
}
}
}
</script>
My approach with the script setup fails:
<template>
<NavigationBar
#switchTab="changeTab"
:activeTab="tab"
/>
<component :is="tab" />
</template>
<script setup>
import NavigationBar from './components/NavigationBar.vue'
import TemplateSyntax from './components/TemplateSyntax.vue'
import DataPropsAndMethods from './components/DataPropsAndMethods.vue'
import { ref } from 'vue'
const tab = ref('DataPropsAndMethods')
function changeTab (newTab) {
tab.value = newTab
}
</script>
do you got any idea how to solve this with the script setup method?
It seems with <script setup>, tab needs to reference the component definition itself instead of the component name.
To reference the component definition, which does not need reactivity, use markRaw() before setting tab.value:
<script setup>
import DataPropsAndMethods from './components/DataPropsAndMethods.vue'
import { ref, markRaw } from 'vue'
const tab = ref(null)
changeTab(DataPropsAndMethods)
// newTab: component definition (not a string)
function changeTab (newTab) {
tab.value = markRaw(newTab)
}
</script>
demo 1
If you need to pass the component name to changeTab(), you could use a lookup:
<script setup>
import DataPropsAndMethods from './components/DataPropsAndMethods.vue'
import { ref, markRaw } from 'vue'
const tab = ref(null)
changeTab('DataPropsAndMethods')
// newTab: component name (string)
function changeTab (newTab) {
const lookup = {
DataPropsAndMethods,
/* ...other component definitions */
}
tab.value = markRaw(lookup[newTab])
}
</script>
demo 2
Tested with Vue 3.0.9 setup with Vue CLI 5.0.0-alpha.8
I'm trying to use createEventDispatcher to catch the child component's event from the parent's component but seems like doesn't work. If I remove the custom element, the dispatcher event works. Am I doing something wrong or svelte custom element doesn't support the dispatcher event?
child component Inner.svelte
<svelte:options tag="my-inner"/>
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
parent component App.svelte
<svelte:options tag="my-app" />
<script>
import {} from './Inner.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<my-inner on:message={handleMessage}></my-inner>
My rollup.config.js settings
plugins: [
svelte({
compilerOptions: {
customElement: true,
tag: null
},
}),
This is known issue. At least in 3.32 and before, events are not emitted from svelte component compiled into custom-element.
See https://github.com/sveltejs/svelte/issues/3119
This thread discuss about various workaround, but it depends of your usecase. The simple one seem to emit yourself an event :
import { createEventDispatcher } from 'svelte';
import { get_current_component } from 'svelte/internal';
const component = get_current_component();
const svelteDispatch = createEventDispatcher();
const dispatch = (name, detail) => {
svelteDispatch(name, detail);
component.dispatchEvent && component.dispatchEvent(new CustomEvent(name, { detail }));
// or use optional chaining (?.)
// component?.dispatchEvent(new CustomEvent(name, { detail }));
};
I have the following parent component which has to render a list of dynamic children components:
<template>
<div>
<div v-for="(componentName, index) in supportedComponents" :key="index">
<component v-bind:is="componentName"></component>
</div>
</div>
</template>
<script>
const Component1 = () => import("/components/Component1.vue");
const Component2 = () => import("/components/Component2.vue");
export default {
name: "parentComponent",
components: {
Component1,
Component2
},
props: {
supportedComponents: {
type: Array,
required: true
}
}
};
</script>
The supportedComponents property is a list of component names which I want to render in the parent conponent.
In order to use the children components in the parent I have to import them and register them.
But the only way to do this is to hard code the import paths of the components:
const Component1 = () => import("/components/Component1.vue");
const Component2 = () => import("/components/Component2.vue");
And then register them like this:
components: {
Component1,
Component2
}
I want to keep my parentComponent as generic as possible. This means I have to find a way to avoid hard coded components paths on import statements and registering. I want to inject into the parentComponent what children components it should import and render.
Is this possible in Vue? If yes, then how?
You can load the components inside the created lifecycle and register them according to your array property:
<template>
<div>
<div v-for="(componentName, index) in supportedComponents" :key="index">
<component :is="componentName"></component>
</div>
</div>
</template>
<script>
export default {
name: "parentComponent",
components: {},
props: {
supportedComponents: {
type: Array,
required: true
}
},
created () {
for(let c=0; c<this.supportedComponents.length; c++) {
let componentName = this.supportedComponents[c];
this.$options.components[componentName] = () => import('./' + componentName + '.vue');
}
}
};
</script>
Works pretty well
Here's a working code, just make sure you have some string inside your dynamic import otherwise you'll get "module not found"
<component :is="current" />
export default { data () {
return {
componentToDisplay: null
}
},
computed: {
current () {
if (this.componentToDisplay) {
return () => import('#/components/notices/' + this.componentToDisplay)
}
return () => import('#/components/notices/LoadingNotice.vue')
}
},
mounted () {
this.componentToDisplay = 'Notice' + this.$route.query.id + '.vue'
}
}
Resolving dynamic webpack import() at runtime
You can dynamically set the path of your import() function to load different components depending on component state.
<template>
<component :is="myComponent" />
</template>
<script>
export default {
props: {
component: String,
},
data() {
return {
myComponent: '',
};
},
computed: {
loader() {
return () => import(`../components/${this.component}`);
},
},
created() {
this.loader().then(res => {
// components can be defined as a function that returns a promise;
this.myComponent = () => this.loader();
},
},
}
</script>
Note: JavaScript is compiled by your browser right before it runs. This has nothing to do with how webpack imports are resolved.
I think we need some plugin that can have code and every time it should load automatically. This solution is working for me.
import { App, defineAsyncComponent } from 'vue'
const componentList = ['Button', 'Card']
export const registerComponents = async (app: App): void => {
// import.meta.globEager('../components/Base/*.vue')
componentList.forEach(async (component) => {
const asyncComponent = defineAsyncComponent(
() => import(`../components/Base/${component}.vue`)
)
app.component(component, asyncComponent)
})
}
you can also try glob that also work pretty well but I have checked it for this solution but check this out worth reading
Dynamic import
[Update]
I tried same with import.meta.globEage and it works only issue its little bit lazy loaded you may feel it loading slow but isn't noticeable much.
import { App, defineAsyncComponent } from 'vue'
export const registerComponents = async (app: App): void => {
Object.keys(import.meta.globEager('../components/Base/*.vue')).forEach(
async (component) => {
const asyncComponent = defineAsyncComponent(
() => import(/* #vite-ignore */ component)
)
app.component(
(component && component.split('/').pop()?.split('.')[0]) || '',asyncComponent
)
})
}