8

In my application I have a lot of forms and most inputs look exactly like this:

<div class="form-group">
    <label for="language">{{ $t('form.language')}}</label>
    <input type="text" class="form-control" id="language" name="form.language" v-model="language" v-validate.initial="'required'" :data-vv-as="$t('form.language')" />
    <span class="invalid-feedback">{{ errors.first('language') }}</span>
</div>

This gets duplicated over and over again. The only thing that really changes is the name of the field and the input type. Sometimes it is a select and sometimes it is a more complex component instead of simple HTML one.

My idea is to create some kind of wrapper component. So I dont have to copy all this and simply use something like that:

<form-group name="language">
    <input type="text" v-model="form.language">
</form-group>

I tried to implement it that way, but it doesnt work:

<template>
    <div class="form-group">
        <label :for="name">{{ $t('form.' + name)}}</label>
        <slot class="form-control"
              :id="name"
              :data-vv-name="name"
              v-validate.initial="'required'"
              :data-vv-as="$t('form.'+ name)">
        </slot>
        <span class="invalid-feedback">{{ errors.first(name) }}</span>
    </div>
</template>

<script>
    export default {
        props: ['name']
    }
</script>

Do you have any ideas? The problem is that I cannot pass mixins and props easily to the slotted element/component.

Sibeesh Venu
  • 18,755
  • 12
  • 103
  • 140
Marvin Rabe
  • 4,141
  • 3
  • 25
  • 43
  • If I understand correctly, you want to have a component with slot A, and in this given slot you want to add "default" attributes to the slot? Such as id, data-vv-name and similar. Is this correct? – Antony Jan 17 '18 at 20:40
  • If it is the case, you are sending properties to the "slot" itself, and not the slot content (input). Depending on how diverse are your inputs, I would recommend having a "type" property on your component and various "v-if", which would generate the correct component. Otherwise, a more complex solution would be to use a component which uses a render function. – Antony Jan 17 '18 at 20:43
  • @Antony This is correct. Well passing type is not feasible because sometimes I have an select field or a more complex component (e.g. money input with masking, password input with strength indicator). And to switch between the inputs with v-if isnt usefull either, because then I have to hard code any possible type. What I want to avoid to make the component not too messy. – Marvin Rabe Jan 18 '18 at 12:52

2 Answers2

1

How about scoped slots (Vue 2.5.0+)?

<!-- form-group.vue -->
<template>
  <div>
    <label />
    <slot v-bind="$props" />
    <span />
  </div>
</template>

Above, all the props of <form-group> is bound to the slot using v-bind. You may want to specify certain fields only: <slot :id="name" :data-vv-name="name" />

<form-group name="age">
  <input type="number" slot-scope="slotProps" v-bind="slotProps" />
</form-group>

Here, the <input> can access the slot props by using slot-scope, giving it a name (here slotProps). slotProps will contain all the props of <slot> as defined in form-group.vue.

Some more examples:

<form-group name="language">
  <input type="text" slot-scope="sp" v-bind="sp" />
</form-group>

<form-group name="hello" value="friend">
  <span slot-scope="sp">
    {{ sp.name }}: {{ sp.value }}
  </span>
</form-group>
tony19
  • 125,647
  • 18
  • 229
  • 307
Kalabasa
  • 506
  • 5
  • 14
0

As said in the comments, it is not possible to pass the props from the to the content of the slot, which is your <input>.

As this is quite complex case, it would require the use of a Render Function, which sends the custom attributes into the new tag while applying a set of default attributes.

I have done a proof of concept here: https://codepen.io/anon/pen/Ozadop?editors=1010

Vue.component('my-input', {
  render: function(createElement) {
    const defaultSlot = this.$slots.default[0];
    const domProps = Object.assign({
      value: this.value,
      someProp: 'foobar',
      test: "asdf",
      class: defaultSlot.data.staticClass
    }, defaultSlot.data.attrs);
    return createElement(
      defaultSlot.tag, // tag name
      {
        attrs: domProps,
        props: domProps,
        on: {
          input: (event) => {
            this.value = event.target.value
            this.$emit('input', event.target.value)
          }
        }
      }
    )
  },
  props: {
    name: {
      type: String,
      required: true
    },
    value: {
      type: String,
      required: true
    }
  }
})

new Vue({
  el: "#app",
  data() {
    return {
      name: "Your Name",
      age: 5
    }
  }
});
.red {
  border: 2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
<div id="app">
  Hello, {{name}}!! You are {{age}} years old.
  <br/>
  <my-input v-model="name">
    <input customProp="myCustomProp"></input>
  </my-input>

  <my-input v-model="age">
    <input type="number" required class="red" custom="otherCustom"></input>
  </my-input>

</div>

Note that this will requires everything which is not an attribute to be applied to the wrapping Vue component with the current context.

This would need to be adapted to support all of your needs, such as the select fields and similar content, but I believe it is a good start.

Antony
  • 1,253
  • 11
  • 19