How pass a v-model on the parent into a template - javascript

I'm trying to compose the UI for a search page, but wanna use components to reuse code. However, I need a way to pass the model of the page to the search component, and don't see how:
In index.html:
<template id="search">
<q-search inverted placeholder="Look" float-label="Search" v-model="search" /> <-- BIND HERE
</template>
<template id="ListCustomersPage">
<q-layout>
<q-layout-header>
<search v-model="search"></search> <-- HOW PASS INTO THIS
</q-layout-header>
</q-layout>
</template>
And the code:
const search = {
template: '#search',
props: ['search']
};
const ListCustomersPage = {
key: 'ListCustomersPage',
template: '#ListCustomersPage',
components: { search },
data() {
return {
title: 'Select Customer',
search:'' <-- FROM THIS TO 'BIND HERE'
}
}
};

I'm not sure if I'm 100% following what you're asking, but it seems like you just want to pass a property to a child component?
<search :search="search"></search> <-- HOW PASS THIS
Passing a prop to a child is done with v-bind or the colon short hand for it.
<child-component :property="parent_data"></child-component>
<child-component v-bind:property="parent_data"></child-component>
See the documentation here.

Related

How to pass a ref to a child input component with Vue?

I'm currently trying to pass a ref to get the value of the input (base-input component) on submit. You will find below the two components. With the console.log in handleSubmit, email is always undefined.
Thanks in advance for your help.
Parent component
<template>
<form #submit.prevent="handleSubmit">
<div class="flex flex-col mt-10">
<form-label forInput="email" label="Email Address" />
<base-input type="email" name="email" ref="email" />
</div>
</form>
</template>
<script>
import BaseInput from "../UI/BaseInput.vue";
export default {
components: {
BaseInput,
},
methods: {
handleSubmit() {
const email = this.$refs.email.value;
console.log(email);
},
},
};
</script>
Child Input component
<template>
<input
:type="type"
:name="name"
:ref="name"
/>
</template>
<script>
export default {
props: ["type", "name"],
};
</script>
If you want to access the value from the child's input field in the parent component, you need a way for data to flow from child to parent component, and that is done through emits.
But wouldn't it be nice to be able to use v-model with your custom BaseInput component? The same way one would use form input binding?
<input
:value="text"
#input="event => text = event.target.value">
or v-model to simplify
<input v-model="text">
Something like this
<BaseInput v-model="email" />
Well, we can do just that. What you need is a modelValue prop and update:modelValue emit event.
You can wrap both inside a writeable computed property for a cleaner and clearer syntax.
const props = defineProps({
modelValue: {
type: String,
},
});
const emit = defineEmits(['update:modelValue']);
const internalValue = computed({
get() {
return props.modelValue;
},
set(value: string) {
return emit('update:modelValue', value);
},
});
When internalValue is set to a new value, it emits that event to the parent component and it syncs it through props.modelValue. Meaning, you can change props.modelValue in the parent component and the change would be reflected inside your custom component. And vice versa.
I like this approach since it gives you a very natural way of reasoning about your component. Now, this concept isn't restricted to v-model per se, you can use it with any prop you want to synchronize to the parent component. Just use name prop, update:name emit in child component and use v-model:name in parent component.
Resources:
https://vuejs.org/guide/components/events.html#usage-with-v-model
first the BaseInput is spelt wrong in the template.
Instead of
<base-input type="email" name="email" ref="email" />
you need to change it to
<BaseInput :type="'email'" :name="'email'" ref="email" />
A way better approach will be to use #emit()
Child.vue
<template>
<input
:type="type"
:name="name"
#change="$emit('inputContent', Content)"
v-model="Content"
/>
</template>
<script>
export default {
emits: ['inputContent'],
data() {
return {
Content: '',
}
},
props: ["type", "name"],
};
</script>
Don't forget to declare your props as strings. 😉
Parent.vue
<template>
<BaseInput :type="'email'" :name="'email'" ref="email" #inputContent="handleSubmit"/>
</template>
<script>
import BaseInput from "../UI/BaseInput.vue";
export default {
components: {
BaseInput,
},
methods: {
handleSubmit(content) {
const email = content;
console.log(email);
},
},
};
</script>
Here is something about emits in the vue docs
and how to use v-model
I hope this helps :)

What's the best way to trigger a child emit from the parent component?

Right now I'm passing a trigger prop from the parent to child component, which triggers the emit from the child to the parent.
parent component:
<form #submit.prevent="state.store=true" method="post" enctype="multipart/form-data">
<child-component :triggerEmit=state.store #emitSomething="getSomething()"/>
child component:
const emit = defineEmits([
'emitBody'
])
watchEffect(async () => {
if (props.triggerEmit) {
emit('emitSomething', value)
}
})
This gets confusing quickly, if the components grow in size and I was wondering if there is a simpler way to trigger child emits from the parent, since this seems to be a common use case.
Edit:
Trying to trigger the child method directly from the parent (not working).
child:
const childMethod = () => {
console.log('check')
}
parent:
html:
<child ref="childRef"/>
script setup:
const childRef = ref()
childRef.value.childMethod()
Page throws error:
Cannot read properties of undefined (reading 'childMethod')
As per my understanding you want to access multiple child component methods/properties from the parent component. If Yes, you can achieve that by create a ref and access the methods.
In template :
<!-- parent.vue -->
<template>
<button #click="$refs.childComponentRef.childComponentMethod()">Click me</button>
<child-component ref="childComponentRef" />
</template>
In script :
With Vue 2 :
this.$refs.childComponentRef.childComponentMethod( );
With Vue 3 Composition Api :
setup( )
{
const childComponentRef = ref( );
childComponentRef.value.childComponentMethod( )
return {
childComponentRef
}
}
In this case, the parent's trigger is effectively querying the child for its event data so that it could call getSomething() on it. The parent already owns getSomething(), so it really only needs the child data.
Another way to get that data is to use v-model to track the child data:
In the child component, implement v-model for a prop (a string for example) by declaring a modelValue prop and emitting an 'update:modelValue' event with the new value as the event data:
<!-- ChildName.vue -->
<script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>
<template>
<label>Name
<input type="text" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
</label>
</template>
In the parent, add a reactive object, containing a field for each child's v-model:
<!-- ParentForm.vue -->
<script setup>
import { reactive } from 'vue'
const formData = reactive({
name: '',
age: 0,
address: {
city: '',
state: '',
},
})
</script>
<template>
<form>
<child-name v-model="formData.name" />
<child-age v-model="formData.age" />
<child-address v-model:city="formData.address.city" v-model:state="formData.address.state" />
<button>Submit</button>
</form>
</template>
Now, the parent can call getSomething() on each field upon submitting the form:
<!-- ParentForm.vue -->
<script setup>
import { toRaw } from 'vue'
â‹®
const getSomething = field => {
console.log('getting', field)
}
const submit = () => {
Object.entries(toRaw(formData)).forEach(getSomething)
}
</script>
<template>
<form #submit.prevent="submit">
â‹®
</form>
</template>
demo

Access scoped slot from component

I got this component:
<template>
<Popover v-slot="{ open }">
<PopoverButton>
{{ title }}
</PopoverButton>
<div v-if="computedOpen">
<PopoverPanel static>
<slot name="popover"></slot>
</PopoverPanel>
</div>
</Popover>
</template>
<script>
import {Popover, PopoverButton, PopoverPanel} from '#headlessui/vue'
import {computed, ref, watch} from 'vue'
import {useRoute} from 'vue-router'
export default {
name: 'DropdownMenuButton',
mixins: [slots],
props: {
name: {
type: String,
},
title: {
type: String,
default: ''
},
},
components: {
Popover,
PopoverButton,
PopoverPanel,
ChevronDownIcon,
},
setup(props) {
const isOpen = ref(null);
const route = useRoute()
watch(route, () => {
isOpen.value = false;
});
const computedOpen = computed(() => {
let open = ...? //this is missing...
return isOpen.value && open.value;
});
return {
computedOpen
}
},
}
</script>
This component makes use of headless UI's popover.
Now I'd like to close the popover once the route changes. While the route-change is being detected fine, I can not access the <Popover>'s open value in the setup() method to determine, whether computedOpen should return true or false.
My question: How can I access v-slot="{ open } in the computed value?
What you want is not possible.
Think about it:
Everything inside <Popover> element (the slot content) is compiled by Vue to a function returning the virtual DOM representation of the content
This function is passed to the Popover component as slots.default and when the Popover component is rendering, it calls that function passing the open value as an argument
So open value is Popover component's internal state and is only accessible inside the template (ie. slot render function)
So I think your best bet is throw away the computed idea and just use the open value directly in the template or use method instead, passing open as an argument
<div v-if="isOpen && open">
<PopoverPanel static>
<slot name="popover"></slot>
</PopoverPanel>
</div>
<div v-if="isPanelOpen(open)">
<PopoverPanel static>
<slot name="popover"></slot>
</PopoverPanel>
</div>

Add condition to .sync in vue.js

I have a component that takes an options property. The options can be synced. As described below.
<component :options.sync="options">...</component>
The component is found in a parent component that has a shouldSync prop. As described below:
<template>
<div>
<component :options.sync="options">...</component>
</div>
</template>
<script>
export default {
name: 'parent',
props: ['shouldSync']
}
</script>
I want to use .sync modifier only if shouldSync property of the parent component is true. I tried using a dynamic prop with a computed property as below but it didn't work as expected.
<template>
<div>
<component :[optionsProp]="options">...</component>
</div>
</template>
<script>
export default {
name: 'parent',
props: ['shouldSync'],
computed: {
optionsProp: () => {
return this.shouldSync ? 'options.sync' : 'options'
}
}
}
</script>
Unfortunately, it didn't work.
Another alternative would be to duplicate the component tag, one with .sync and the other without, and use the v-if directive to determine which one to use. As described below:
<template>
<div>
<component v-if="shouldSync" :options.sync="options">...</component>
<component v-else :options="options">...</component>
</div>
</template>
But I don't want to do this because the default slot of <component /> contains much code and I don't want to duplicate that. I don't also want to transfer the default slot codes to a new component and include it here.
Is there any better way I can handle this situation? Thanks.
<component :options.sync="options"> is just syntactic sugar for <component :options="options" #update:options="options = $event">
So just use:
<component :options="options" #update:options="onUpdateOptions">
methods: {
onUpdateOptions(newValue) {
if(this.shouldSync)
this.options = newValue
}
}
or...
<component :options="options" #update:options="options = shouldSync ? $event : options">

vue.js and slot in attribute

I'm trying to build a vue.js template that implements following:
<MyComponent></MyComponent> generates <div class="a"></div>
<MyComponent>b</MyComponent> generates <div class="a" data-text="b"></div>.
Is such a thing possible?
EDIT
Here is the best I can reach:
props: {
text: {
type: [Boolean, String],
default: false
}
},
and template
<template>
<div :class="classes()" :data-text="text">
<slot v-bind:text="text"></slot>
</div>
</template>
but the binding does not work, text always contains false.
You can use the mounted() method to get text using $slot.default property of the component to get the enclosing text. Create a text field in data and update inside mounted() method like this :
Vue.component('mycomponent', {
data: () => ({
text: ""
}),
template: '<div class="a" :data-text=text></div>',
mounted(){
let slot = this.$slots.default[0];
this.text=slot.text;
}
});
Note: It will only work for text, not for Html tags or components.
You're mixing slots and properties here. You'll have to pass whatever you want to end up as your data-text attribute as a prop to your component.
<MyComponent text="'b'"></MyComponent>
And in your template you can remove the slot
<template>
<div :class="classes()" :data-text="text"></div>
</template>
Another thing: it looks like your binding your classes via a method. This could be done via computed properties, take a look if you're not familiar.
You can try this.
<template>
<div :class="classes()">
<slot name="body" v-bind:text="text" v-if="hasDefaultSlot">
</slot>
</div>
</template>
computed: {
hasDefaultSlot() {
console.log(this)
return this.$scopedSlots.hasOwnProperty("body");
},
}
Calling
<MyComponent>
<template v-slot:body="props">
b
</template>
</MyComponent>

Categories

Resources