Vue: Allow props to have different internal and external names

Created on 31 Jan 2018  Β·  19Comments  Β·  Source: vuejs/vue

What problem does this feature solve?

Consider the <input> element. It maintains its own internal state for its value (by the browser) irrespective of whether or not it is being driven by v-model.

After looking at some Vue component libraries, most of the components externalize their state through props, meaning that the parent component must define the value for that component in its data and use v-model to wire it up -- the component cannot be used without v-model because it doesn't maintain the state internally to the component.

Ideally I'd like to author input components that satisfy the following requirements:

  • Can be used without v-model (the component maintains the state itself)
  • Can be used with v-model (the state is driven entirely by the prop emitting input events)

This results in somewhat messy code like this:

Vue.component('checkbox', {
  template: '<div class="checkbox" @click="toggle">{{ internalValue ? "Checked" : "Unchecked" }}</div>',

  props: {
    value: {
      type: Boolean,
      default: false,
    },
  },

  data() {
    return {
      internalValue: false,
    };
  },

  watch: {
    value: {
      immediate: true,
      handler(value) {
        this.internalValue = value;
      },
    },

    internalValue(internalValue) {
      this.$emit('input', internalValue);
    },
  },

  methods: {
    toggle() {
      this.internalValue = !this.internalValue;
    },
  },
});

In order to distinguish between the prop and the data, I've used value and internalValue respectively. The problem is that value is rarely used in the code (it's only used in the watcher) and I have to remember to use internalValue throughout the component's code and template instead of value (since internalValue is the mutable source of truth).

To avoid this, I could instead name the prop externalValue and the data value, but now I would have to use <checkbox :external-value="x"> which is not ideal (the external prop should be called simply "value").

I'm proposing a feature in which you can specify what internal name a prop should be exposed as within the component without changing the external name of that prop in templates (kind of like how method arguments work in objective-c).

What does the proposed API look like?

Vue.component('foo', {
  props: {
    value: {
      internalName: 'valueProp',
    },
  },

  template: '<div>{{ valueProp }}</div>',
});
<foo value="bar">

Most helpful comment

Here's a simple real-world example I've come across. You have a password field with a toggle icon to show/hide the password. Normally the parent component doesn't care about the internal state.

But then you have a new or change-password form which has two (or even three) password fields, and it's useful to the user if they all toggle the show/hide at the same time. You end up having to write this:

// parent:
<Password v-model="oldPassword"  :show.sync="show" label="Old Password" />
<Password v-model="newPassword1" :show.sync="show" label="New Password" />
<Password v-model="newPassword2" :show.sync="show" label="New Password (again)" />

// Password.vue
<template>
    <v-text-field
        v-bind="$attrs" v-on="$listeners" @click:append="click"
        :type="iShow?'text':'password'" :append-icon="iShow?'mdi-eye':'mdi-eye-off'"
    />
</template>

<script>
export default {
    props: {
        show: {
            type: Boolean,
            default: false,
        },
    },
    data() { return {
        iShow: this.show,
    } },
    watch: {
        show(val) {
            this.iShow = val;
        },
    },
    methods: {
        click() {
            this.iShow = !this.iShow;
            this.$emit('update:show', this.iShow);
        },
    },
}
</script>

I agree, it's relatively easy enough to do in user-land, but it could be made simpler.

Also, I think it could be made possible without any extra boilerplate... If the prop definition provides a default value then Vue could do one of two things:

  1. If a value was passed as a prop and it was passed as a v-model or v-bind:prop.sync: allow the prop to be mutated without complaining and automatically emit the update event. (probably too much of a breaking change and anti-pattern to be seriously considered).
  2. If a value wasn't passed as prop, create some internal state with the same name and default value and allow it to be mutated without complaining about mutating a prop.

All 19 comments

@decademoon Just to me, it looks like you don't need to maintain internalValue yourself:

Vue.component('checkbox', {
  template: '<div class="checkbox" @click="toggle">{{ value ? "Checked" : "Unchecked" }}</div>',

  props: {
    value: {
      type: Boolean,
      default: false,
    },
  },
  methods: {
    toggle() {
      this.$emit('input', !this.value);
    },
  },
});

That's it! This gives some additional control on Checkbox e.g.

<!-- standard usage: -->
<Checkbox v-model="isChecked" />

<!-- advanced usage: -->
<Checkbox :value="isChecked" @input="inCaseAllIsOkSetChecked($event)" />

This allows to maintain model changes from the outside of Checkbox component. I mean emitting an input event doesn't mean that Checkbox state will be changed. It only indicates that it should change but we have full control on that. I believe that is why v-model exists.

@OEvgeny Thank you for your response, but your solution won't allow the component to be used without v-model. If you check out this fiddle, you can see how the checkbox component can still be toggled without requiring its state to be specified by v-model, hence why internalValue is necessary.

The checkbox is probably a bad example, because I can't imagine a use case where you'd want to have a checkbox without v-model.

A better example would be a tabs component. Sometimes you don't care what the selected tab is and the component itself should maintain that state. In other situations you need to control the selected tab by the parent component, so a prop binding is needed.

your solution won't allow the component to be used withou

Exactly. I think it is ok. We want our components to be data/state driven. So, we want to control them from the outside.
You can create uncontrolled one utilizing controlled Checkbox:

Vue.component('uncontrolled-checkbox', {
  template: '<Checkbox :value="value" @input="toggle" />',
  props: {
    initial: { type: Boolean, default: false }
  },
  data() {
    return {
     value: this.initial
   }
  },
  methods: {
    toggle() {
      this.value = !this.value;
      this.$emit('input', this.value);
    },
  },
});

Note that it hasn't value prop which means we only pass initial value into it (and can't use it with v-model). On the other hand it also emits input event, so we can listen for its changes.
This gives us cleaner separation where checkbox state is. When we mix controlled and uncontrolled (from the outside) state in one components (your Checbox example) it becomes unclear that state it has when we use it like it was shown above:

<Checkbox :value="isChecked" @input="inCaseAllIsOkSetChecked($event)" />

Does that make sense to you?

A better example would be a tabs component.

The same approach can be used for all v-model driven components I guess.

But your uncontrolled-checkbox component isn't a checkbox, it's a new component that wraps a checkbox with the necessary state. I'd have to duplicate all the props from checkbox to uncontrolled-checkbox, and forward all emitted events too in order for uncontrolled-checkbox to fully mimic a checkbox (again, a checkbox is a bad example, a lot of my components are more complex than that). And if I have <uncontrolled-checkbox ref="cb">, then this.$refs.cb isn't a checkbox and won't have all the methods that the checkbox has.

I believe controlled/uncontrolled input is something that shouldn't be mixed. That about usage example I provided?

But your uncontrolled-checkbox component isn't a checkbox

Yes, it isn't. On the other hand if you need a generic component for checkbox you could wrap it one more time and set which to use explicitly:

<Checkbox controlled :value="myVal" :input="onInput"  />
<Checkbox :initial="true" :input="onInput"  />

Now when you have a ref you know it is a Checkbox. Although this doesn't solve a problem with props routing.

Vue.component('foo', {
  props: {
   value: {
     internalName: 'valueProp',
   },
 },
 template: '<div>{{ valueProp }}</div>',
});

I think this is achievable in userland via global mixin. All that you need is to manually set up watchers and provide additional keys in data.

Here's a mixin I created which will automatically update a prop's value in response to an event emitted from that component (demo):

Vue.mixin({
  created() {
    if (this.$options.props) {
      for (const prop of Object.keys(this.$options.props)) {
        const config = this.$options.props[prop];

        if (config.syncOn) {
          this.$on(config.syncOn, value => this[prop] = value);
        }
      }
    }
  },
});

const Checkbox = {
  template: '<div class="checkbox" @click="toggle">{{ value ? "Checked" : "Unchecked" }}</div>',

  props: {
    value: {
      type: Boolean,
      default: false,
      syncOn: 'input',
    },
  },

  methods: {
    toggle() {
      this.$emit('input', !this.value);
    },
  },
};

But Vue will output a warning about the prop being mutated...

I can disable console error like this :sweat:

const error = console.error;
console.error = () => {};

this[prop] = value;

console.error = error;

@decademoon I updated the implementation with support for mapping prop to another field and added case when internal state could be out of sync with the state provided by parent component see here.

This is solved more elegantly by using a computed setter:

Vue.component('checkbox', {
  template: '<div class="checkbox" @click="toggle">{{ internalValue ? "Checked" : "Unchecked" }}</div>',

  props: {
    value: {
      type: Boolean,
      default: false,
    },
  },

  computed: {
    internalValue: {
      get () { return this.value },
      set (v) { this.$emit('input', v) }
    }
  }

  methods: {
    toggle() {
      this.internalValue = !this.internalValue;
    },
  },
});

@posva That component won't work without v-model, all you've done is hide emitting the input event through a setter (which does make writing controlled components easier since you don't have to litter $emit('input', ...) throughout your code, but then you still have the original problem of having value and internalValue and needing to use internalValue everywhere which was what I wanted to avoid in the first place).

@OEvgeny

I believe controlled/uncontrolled input is something that shouldn't be mixed.

I agree. I think I'll just settle for making all of my components controlled, even if that means I need to use v-model on them just to make it work even if I don't plan on using that data in the parent component.

Thank you for your help everyone πŸ‘

It does work with v-model though. Maybe you're talking about the checkbox appearing checked (#7506)

@posva The main idea was to create a component which state could be driven with or without v-model. If the value provided it uses that. If not - it falls back to the internal state providing a way to react to state changes πŸ˜„

@posva I second this proposal. This will be useful in building "smart dumb components" which when used with a external prop behaves like a dumb component but when used without a prop can fallback to it's own internal state behaving like a smart component. If this sort of renaming can be done using the option.model API, I do not see why cannot be extended to any props and events in general.

Take for example this "smart dumb component":

export default {
  props: ['externalValue'],
  data () {
    return {
      internalValue: 1
    }
  },
  computed: {
    value: {
      get () {
        return this.externalValue != null ? this.externalValue : this.internalValue
      },
      set (value) {
        if (this.externalValue != null) this.$emit('update:externalValue', value)
        else this.internalValue = value
      }
    }
  }
}

As you can see, even if using the computed property approach, it will require 3 names externalValue, internalValue and value. Either my component user get used to writing v-bind:external-value.sync or I will have to rename my computed as internalValue and the underlying state as internalInternalValue which is not exactly the most clean way to express my code.

A proposed API could be:

export default {
  model: {
    'prop.value': externalValue,
    'event.update:value': 'update:externalValue'
  }
}

Here's a simple real-world example I've come across. You have a password field with a toggle icon to show/hide the password. Normally the parent component doesn't care about the internal state.

But then you have a new or change-password form which has two (or even three) password fields, and it's useful to the user if they all toggle the show/hide at the same time. You end up having to write this:

// parent:
<Password v-model="oldPassword"  :show.sync="show" label="Old Password" />
<Password v-model="newPassword1" :show.sync="show" label="New Password" />
<Password v-model="newPassword2" :show.sync="show" label="New Password (again)" />

// Password.vue
<template>
    <v-text-field
        v-bind="$attrs" v-on="$listeners" @click:append="click"
        :type="iShow?'text':'password'" :append-icon="iShow?'mdi-eye':'mdi-eye-off'"
    />
</template>

<script>
export default {
    props: {
        show: {
            type: Boolean,
            default: false,
        },
    },
    data() { return {
        iShow: this.show,
    } },
    watch: {
        show(val) {
            this.iShow = val;
        },
    },
    methods: {
        click() {
            this.iShow = !this.iShow;
            this.$emit('update:show', this.iShow);
        },
    },
}
</script>

I agree, it's relatively easy enough to do in user-land, but it could be made simpler.

Also, I think it could be made possible without any extra boilerplate... If the prop definition provides a default value then Vue could do one of two things:

  1. If a value was passed as a prop and it was passed as a v-model or v-bind:prop.sync: allow the prop to be mutated without complaining and automatically emit the update event. (probably too much of a breaking change and anti-pattern to be seriously considered).
  2. If a value wasn't passed as prop, create some internal state with the same name and default value and allow it to be mutated without complaining about mutating a prop.

What's the status on this one?

There is an rfc on the rfcs repo

Was this page helpful?
0 / 5 - 0 ratings