Compound component with passing props #12955
-
Hi, I hope I used right term in title, if not, Im sorry.
My question is: lets say, I have FieldWrapper component. This component has some props (like ID for "for" label, label text, hint, required, disabled, etc). So basically its simple wrapper around actual input. Because it doesnt care about what kind of input you use, you dont need to create component for every input, selector, checkbox, etc. You just wrap them.
So basic implementation is like this (just example)
// FieldWrapper
<script setup lang="ts">
defineProps<{
id: string,
required?: boolean
}>()
</script>
<template>
<div class="some-class">
<label :for="id">{{ label }}</label>
<slot /> // some input
<span v-if="hint">{{ hint }}</span>
</div>
</template>
And you will use it like this
<FieldWrapper id="name" required>
<input type="text" />
</FieldWrapper>
What should FieldWrapper internally do:
- it should take some props like ID, required, disabled, etc and apply them to input
Is it possible to create it using script setup or I need to use defineComponent with return () => ... ??
Thanks
Beta Was this translation helpful? Give feedback.
All reactions
@LadIQe Thanks for providing the repo, I have roughly read the code.
I have written a demo Playground for your reference.
It should be noted that you should not put the render function
(createElement) directly into the normal function. You need to use the defineComponent
to define the local component, otherwise it will cause re-rendering every time you input, which will cause the input to lose focus automatically:
const createElement = defineComponent({ render() { const slotContent = slots.default?.()?.[0] if (!slotContent) return null const _props = mergeProps( { ...props, ...attrs, value: modelValue.value, onInput(event) { ...
Replies: 2 comments 9 replies
-
You can solve this using scoped slots: Playground
Beta Was this translation helpful? Give feedback.
All reactions
-
I solved it like this
// FieldWrapper.vue
<script setup lang="ts">
import { h } from 'vue'
type ElementEvent = EventTarget & {
value: string | number | Date | null | undefined
}
defineOptions({
name: 'FieldWrapper'
})
const props = defineProps<{
id: string
label: string
required?: boolean
}>()
const slot = defineSlots<{
default: () => (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)[]
}>()
const model = defineModel<string | number | Date | null>()
const createElement = () => {
const element = slot.default()[0]
return h(
element,
{
id: props.id,
required: props.required,
value: model.value,
onChange: ({ target }: { target: ElementEvent }) => {
model.value = target.value
}
}
)
}
</script>
<template>
<div class="field-wrapper">
<label :for="id">
{{ label }}
<span v-if="required">*</span>
</label>
<component :is="createElement()" />
</div>
</template>
<style scoped>
.field-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
width: 100%;
}
.field-wrapper > * {
width: 100%;
}
</style>
// Usage
<FieldWrapper v-model="formData.date" label="Order Date" id="date" required>
<input type="datetime-local" />
</FieldWrapper>
Beta Was this translation helpful? Give feedback.
All reactions
-
Looks nice! :)
Beta Was this translation helpful? Give feedback.
All reactions
-
@LadIQe I assume you might be referring to Fallthrough Attributes
?
You can achieve this using v-bind="$attrs"
, which automatically forwards the properties passed from the parent component to the elements in the slot. You can check the Vue official docs for more information about $attrs
.
If you predefined some props, the undefined props
attributes will be summarized into $attrs
, at which point you can pass $attrs
into <slot />
, for example:
// FieldWrapper <script setup lang="ts"> defineProps<{ id: string label: string hint?: string }>() </script> <template> <div class="some-class"> <label :for="id">{{ label }}</label> - <slot /> // some input + <!-- Pass all the attributes from the Parent Component that are not defined as props to the slot element --> + <!-- Only forward the not defined props (eg: disabled, required, placeholder...) --> + <slot v-bind="$attrs" /> <span v-if="hint">{{ hint }}</span> </div> </template>
Usage:
<FieldWrapper id="Test id" label="Test label" required <!-- 👈 pass `$attrs` into `<slot />` --> disabled <!-- 👈 pass `$attrs` into `<slot />` --> hint="This is a hint" placeholder="Test Placeholder..." <!-- 👈 pass `$attrs` into `<slot />` --> > <input type="text" /> </FieldWrapper>
If you solve your problem, it's better:)
Beta Was this translation helpful? Give feedback.
All reactions
-
hmm I tried your approach, but $attrs are not passed to child, because its more of scoped slots. So you have to access those scoped slots props and pass them to the child. Only this approach worked, or you can use h function like in my example
<FieldWrapper
id="Test id"
label="Test label"
required <!-- 👈 pass `$attrs` into `<slot />` -->
disabled <!-- 👈 pass `$attrs` into `<slot />` -->
hint="This is a hint"
placeholder="Test Placeholder..." <!-- 👈 pass `$attrs` into `<slot />` -->
#default="props" <!-- 👈 access props from `<slot />` -->
>
<input type="text" v-bind="props" /> <!-- 👈 pass `$attrs` into child -->
</FieldWrapper>
Beta Was this translation helpful? Give feedback.
All reactions
-
If you're using Vue 3, it's recommended to pass it through the <template #xxx/>
rather than directly in the parent component:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> <!-- with shorthand --> <template #[dynamicSlotName]> ... </template> </base-layout>
If possible, could you share your latest modified code?
Beta Was this translation helpful? Give feedback.
All reactions
-
you can see it here
here is actual use
its just project for fun (trying new things, nothing serious)
Beta Was this translation helpful? Give feedback.
All reactions
-
@LadIQe Thanks for providing the repo, I have roughly read the code.
I have written a demo Playground for your reference.
It should be noted that you should not put the render function
(createElement) directly into the normal function. You need to use the defineComponent
to define the local component, otherwise it will cause re-rendering every time you input, which will cause the input to lose focus automatically:
const createElement = defineComponent({ render() { const slotContent = slots.default?.()?.[0] if (!slotContent) return null const _props = mergeProps( { ...props, ...attrs, value: modelValue.value, onInput(event) { const target = event.target modelValue.value = target.value } }, ) return h( slotContent, _props ) } })
Usage:
<FieldWrapper v-model="formData.customerName" label="Customer Name" id="customerName" required disabled <!-- 👈 pass `attrs` into `createElement` --> > <input type="text" /> </FieldWrapper>
Beta Was this translation helpful? Give feedback.
All reactions
-
wow, thats new for me. Looks so clean and professional :) In my case, I didnt lose focus, but I bet, there could be some situations where it could do more harm than good. Your approach is much much better.
Thank you, learning something new every day :)
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1