Vue3 updating values between components - vue.js

I have a basic SPA with two child components, a header and a side menu (left drawer).
I wish the user to be able to click a button on the header component to call a function in the side menu component.
I understand I can use props to access a variable between parent & child components however how can I update a value between two sibling components?
Header
<q-btn dense flat round icon="menu" #click="toggleLeftDrawer" />
Left Drawer
import { ref } from 'vue'
export default {
setup () {
const leftDrawerOpen = ref(false)
return {
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
}
}

Use global stores. Create a file
/store.js (you can obviously use any name)
Inside this file store/write the following code:-
import { reactivity } from 'vue'
export const global = reactive({
yourVariable: 'initialValue'
})
You can then import this variable and interact with it from anywhere and the change will be global. See the code below:
In header component:-
<script setup>
import { global } from './store.js'
const clicked = ()=> {
global.yourVariable = 'changed'
}
</script>
<template>
<button #click="clicked">
</button>
</template>
In leftDrawer Component:-
<script setup>
import { global } from './store.js'
</script>
<template>
<div>
{{ global.yourVariable }}
<!--You'll see the change:)-->
</div>
</template>
Then add these two in your main vue:-
<script setup>
import headerComponent from '...'
import leftDrawerComponent from '....'
//....
</script>
<template>
<div>
<header-component />
<left-drawer-component />
</div>
</template>

Related

Access components method via template ref

When I have a child component like this:
<script setup>
import { defineExpose } from 'vue'
const validate = () => {
console.log('validate')
}
defineExpose({ validate })
</script>
<template>
hello
</template>
and parent component in which I use child:
<script setup>
import { ref } from 'vue'
const test = ref()
const validate = () => {
console.log('test', test.value)
}
</script>
<template>
<div ref="test">
<Child />
</div>
<button #click="validate">
click me
</button>
</template>
Is it possible to access validate method from the child component via template ref which is on the wrapper div in parent component?
EDIT:
I update my playground link in which I completed the task but I'm using parent instance instead of provide/inject:
https://sfc.vuejs.org/#eNqNU9FuozAQ/BWfXyBSYt4jqK463Un9hqOqKCypW7At29BWEf/eXcAkJVXaSEHszu4wnl0f+a0xou+A73nqSiuNZw58Z25yJVujrWdHZqFmA6utblmEpdEC/dO2nfMioYCYTvCdMp1f8DGaC0qtHCLUnhF9vMkVYyHvAR8Zizcsu2FHQqhS9EXTAT1lVXigliFXaTKpRr0YeGhNgyBFPh3lIXuWcyLIOaYZ/tJJWPKDMB2PNb6Gf/rYea8V+102snxBbpK7cI9J1sLUPJUilCaLNL7lkz+7tjDi2WmF3o+nzGfA5Xw/nZty6BjFOX/y3rh9kri6JBufndD2kOCbsJ3ysgUBrt09Wv3qwCJxzrdnHAkme7A7C6oCC/Ya56r0gpdo0fsBjxKmfm1/Kqilgr9vRjtYLVIY+TxVqdWttcU7Tv///QqDi5VgcQPnrUzXa6JN8PGUn3aN5NPnz7XFx+Vb2wtFA7ZdW7ZK9mGBXKNP+zPlP89/uQrXXDNW97JCJQfwfzqLw/B3aEehym9MXBlFmG5ANPoQR0uJJAm/oukSBQIZ+LMvPjr6hvpqFoc6YQqqEP7dgHh4UEWLrVnGItqKaPZ+XQyj11W4yMFgYTr3FAd931/un/s9fAD8ILMq
How to actually get rid of parent instance and use provide inject to achieve same result as in the playground from link above?
The ref needs to be on the actual Child element, not the parent div. The method is a property of test.value, so if the method is called "validate" you can run it with test.value.validate().
You also need to make sure the Child component is imported
Try this SFC Playground instead. The "click me" button will console.log the word "validate" which comes from the Child component.
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const test = ref()
const childFunc = () => {
test.value.validate()
}
</script>
<template>
<div>
<Child ref="test" />
</div>
<button #click="childFunc">
click me
</button>
</template>

Render slot as v-html (Vue 3)

Goal
How to implement a component that renders an html string (eg fetched from a CMS) passed as a slot like this :
// app.vue
<script setup>
import MyComponent from "./MyComponent.vue"
const htmlStr = `not bold <b>bold</b>`
</script>
<template>
<MyComponent>{{htmlStr}}</MyComponent>
</template>
Explanation
To render an html string (eg fetch from a CMS) we can use v-html :
// app.vue
<script setup>
const htmlStr = `not bold <b>bold</b>`
</script>
<template>
<p v-html="htmlStr"></p>
</template>
Failed attempts
I have tried with no success :
// component.vue
<script>
import { h } from "vue";
export default {
setup(props, { slots }) {
return () =>
h("p", {
innerHTML: slots.default(),
});
},
};
</script>
Renders
[object Object]
Link to playground
Workaround with props
As a workaround, we can of course use props but it's verbose.
// app.vue
<template>
<MyComponent :value="htmlStr">{{htmlStr}}</MyComponent>
</template>
// component.vue
<template>
<p v-html="value"></p>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps(['value'])
</script>
slots.default() returns an array of your passed slot elements, try to map that content and render it :
h("p", {
innerHTML: slots.default().map(el=>el.children).join(''),
});
Playground

Vue 3: component `:is` in for loop fails

I'm trying to loop over a list of component described by strings (I get the name of the component from another , like const componentTreeName = ["CompA", "CompA"].
My code is a simple as:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="compName"></component>
</template>
</template>
Demo here
EDIT
I tried this with not much success.
Use resolveComponent() on the component name to look up the global component by name:
<script setup>
import { resolveComponent, markRaw } from 'vue'
const myGlobalComp = markRaw(resolveComponent('my-global-component'))
</script>
<template>
<component :is="myGlobalComp" />
<template>
demo 1
If you have a mix of locally and globally registered components, you can use a lookup for local components, and fall back to resolveComponent() for globals:
<script setup>
import LocalComponentA from '#/components/LocalComponentA.vue'
import LocalComponentB from '#/components/LocalComponentB.vue'
import { resolveComponent, markRaw } from 'vue'
const localComponents = {
LocalComponentA,
LocalComponentB,
}
const lookupComponent = name => {
const c = localComponents[name] ?? resolveComponent(name)
return markRaw(c)
}
const componentList = [
'GlobalComponentA',
'GlobalComponentB',
'LocalComponentA',
'LocalComponentB',
].map(lookupComponent)
</script>
<template>
<component :is="c" v-for="c in componentList" />
</template>
demo 2
Note: markRaw is used on the component definition because no reactivity is needed on it.
When using script setup, you need to reference the component and not the name or key.
To get it to work, I would use an object where the string can be used as a key to target the component from an object like this:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
const components = {CompA};
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="components[compName]"></component>
</template>
</template>
To use a global component, you could assign components by pulling them from the app context. But this would require the app context to be available and the keys known.
example:
import { app } from '../MyApp.js'
const components = {
CompA: app.component('CompA')
}
I haven't tested this, but this might be worth a try to check with getCurrentInstance
import { ref,getCurrentInstance } from 'vue'
const components = getCurrentInstance().appContext.components;

Vue 3, $emit never firing

The documentation is not enough to be able to do the emit. I have seen many tutorials and nothing works, now I am testing this
Child component
<div #click="$emit('sendjob', Job )"></div>
With the Vue DevTools plugin I can see that the data is sent in the PayLoad, but I can't find a way to receive this emit from the other component.
Many people do this
Any other component
<template>
<div #sendjob="doSomething"></div>
</template>
<script>
export default {
methods:{
doSomething(){
console.log('It works')
}
}
}
</script>
In my case it doesn't work
You should import the child component in the parent component and use it instead of the regular div tag.
I'm sharing examples for your reference to achieve emits in Vue 3 using <script setup> and Composition API. I strongly suggest going with <script setup if you are going to use Composition API in Single File Component. However, the choice is yours.
Example with <script setup>: https://v3.vuejs.org/api/sfc-script-setup.html
<!-- App.vue -->
<template>
<UserDetail #user-detail-submitted="userDetailSubmitted"/>
</template>
<script setup>
import UserDetail from './components/UserDetail';
function userDetailSubmitted(name) {
console.log({ name })
}
</script>
<!-- UserDetail.vue -->
<template>
<input type="text" v-model="name" #keyup.enter="$emit('user-detail-submitted', name)" />
</template>
<script setup>
import { ref } from 'vue';
const name = ref('');
</script>
Example using Composition API: https://v3.vuejs.org/api/composition-api.html
<!-- App.vue -->
<template>
<UserDetail #user-detail-submitted="userDetailSubmitted"/>
</template>
<script>
import UserDetail from "./components/UserDetail";
export default {
components: {
UserDetail,
},
setup() {
function userDetailSubmitted(name) {
console.log({ name });
}
return {
userDetailSubmitted
}
},
};
</script>
<!-- UserDetail.vue -->
<template>
<input type="text" v-model="name" #keyup.enter="$emit('user-detail-submitted', name)" />
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const name = ref('');
return {
name,
}
}
}
</script>
You should import this child-component in the parent. And don't rename it to the html's original tag.vue3. You'd better use the Composition API.

How to test this component?

I have a Page level component which implements a component BookingInformation with slots. In the Page component, it's got another component BookingInformationHeader with slots. header and default.
My question is, how should I set up my test so that I can test that the GoogleConversionTrackingImage is visible when #Reservation.State wasBookingJustMade changes to true?
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import { Reservation } from "#/store/vuex-decorators";
import { BookingInformation, BookingInformationHeader } from "#/components";
import GoogleConversionTrackingImage from './components/GoogleConversionTrackingImage.vue';
#Component({
components: {
BookingInformation,
BookingInformationHeader,
GoogleConversionTrackingImage
}
})
export default class ConfirmationPage extends Vue {
renderTrackingImage: boolean = false;
#Reservation.State wasBookingJustMade: boolean;
}
</script>
<template>
<booking-information page-type="confirmation-page" class="confirmation-page">
<template slot="header" slot-scope="bookingInfo">
<booking-information-header>
<template slot="buttons">
// some buttons
</template>
</booking-information-header>
<google-conversion-tracking-image v-if="wasBookingJustMade" />
</template>
</booking-information>
</template>
By using vue test utils https://vue-test-utils.vuejs.org/ and chai https://www.chaijs.com/ in your test file you can do something like:
import mount from "#vue/test-utils";
import expect from "chai";
const wrapper = mount(BookingInformation,<inner components you want to test>);
expect(googleImage.exists()).to.be.true;
wrapper.setData({
wasBookingJustMade: true,
});
const googleImage = wrapper.find("google-conversion-tracking-image");
expect(googleImage.exists()).to.be.false;
You'll probably need to import the page level component as well.
You can give an id to the component you want to find and then search by id.