vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.87k stars 546 forks source link

Mutable props with `model` option #165

Open KorHsien opened 4 years ago

KorHsien commented 4 years ago

Motivation

v-model comes in handy when we have form inputs in our components, it has a unified interface and allows us to have a same simpler mental model when dealing with various types of inputs.

When it comes to creating custom inputs, however, it's a little cumbersome, we have to think of and deal with it as a combo of props and events.

With the composition API, I found myself writing something like this:

export const propToModel = (props, emit, key) => computed({
  get: () => props[key],
  set: (val) => emit(`update:${key}`, val),
})
export default {
  template: '<input type="number" v-model.number="count" >',
  props: {
    count: Number,
  },
  emits: ['update:count'],
  setup(props, { emit }) {
    const count = propToModel(props, emit, 'count')

    // some other stuff

    return {
      count,
    }
  },
}

This abstract the combo behind a Ref, so instead of emitting events, we could just mutating the Ref.

When we wrap form inputs, we could use the Ref in v-model directly. This is convenient and minimize the burden of extracting components.

It might seem not much of a difference, but it is more aligned with the composition API and the mental model shifts a little bit.

Once the model props become mutable Refs, general composition functions could easily applied to them. For example, this general useDebounce composition function could be used with model props directly:

export const useDebounce = (ref, wait = 200) => computed({
  get: () => ref.value,
  set: debounce((val) => { ref.value = val }, wait),
})

Proposed Solution

Add a boolean typed model option to props:

  1. The default value is false, which has no effect;
  2. When it set to true, the prop will be mutable, every mutation on the prop emits an update event for the prop.

Basic Example

export default {
  template: `
    <input type="number" v-model.number="count" >
    <button @click="reset" >Reset</button>
  `,
  props: {
    count: {
      type: Number,
      model: true,
    },
  },
  setup(props) {
    const count = useDebounce(toRef(props, 'count'))

    return {
      count,
    }
  },
  methods: {
    reset() {
      this.count = 0
    },
  },
}

Additional Thoughts

Object Props

When an object passed as a prop, the child component get the reference of the original object, thus it can be mutated.

Now it appears to us that the model option controls the mutability of props, it seems natural that the model option would also control the mutability of the objects passed in.

So if we adopt this idea, the prop with a model option set to false would have a readonly reference to the original object.

This will enforce the mutability of props being explicitly stated, so instead of asking "Should we mutate the object props?" maybe we should ask "Is this prop a model?"

One Step Further

Since we are abstracting away the combo of props and events, would it be a good idea to just build the v-model feature upon the reactivity system?

Currently any Ref referred in templates would be automatically unwrapped. But we could pass a Ref as is to the child component in hand-writing render function, and the child component could use it just as normal Refs.