Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Compound component with passing props #12955

Answered by pdsuwwz
LadIQe asked this question in Help/Questions
Discussion options

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

You must be logged in to vote

@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

Comment options

You can solve this using scoped slots: Playground

You must be logged in to vote
2 replies
Comment options

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>
Comment options

Looks nice! :)

Comment options

@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:)

You must be logged in to vote
7 replies
Comment options

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>
Comment options

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?

Comment options

Comment options

@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>
Answer selected by LadIQe
Comment options

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /