Search code examples
vuejs2vue-componentelement-ui

how to make el-select and v-model work together when extracting a custom component


I'm using el-select to build a select component. Something like this:

<template>
    //omitted code
    <el-select v-model="filterForm.client"
                    filterable
                    remote
                    placeholder="Please enter a keyword"
                    :remote-method="filterClients"
                    :loading="loading">
                <el-option
                        v-for="item in clientCandidates"
                        :key="item._id"
                        :label="item.name"
                        :value="item._id">
                </el-option>
            </el-select>
</template>
<scripts>
    export default {
        data() {
           filterForm: {
                client: ''
           },
           clientCandidates: [],
           loading: false
        },
        methods: {
            filterClients(query) {
                if (query !== '') {
                    this.loading = true;
                    setTimeout(() => {
                        this.loading = false;
                        this.clientCandidates = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
                    }, 200);
                } else {
                    this.clientCandidates = [];
                }
            }
        }
    }
</scripts>

So far so good, but since the component will appear in different pages, so I want to extract a custom component to avoid duplication.

According to the guideline,

v-model="fullName"

is equivalent to

v-bind:value="fullName"
v-on:input="$emit('input', $event)"

So I extracted the select component like this:

<template>
<el-select
        v-bind:value="clientId"
        v-on:input="$emit('input', $event)"
        placeholder="Filter by short name"
        filterable="true"
        remote="true"
        :remote-method="filter"
        :loading="loading">
    <el-option
            v-for="item in clients"
            :key="item._id"
            :label="item.name"
            :value="item._id">
    </el-option>
</el-select>
</template>
<scripts>
export default {
    props: {
        clientId: {
            type: String,
            required: true
        }
    },
    data() {
        return {
            clients: [],
            loading: false,
        }
    },
    methods: {
        filter(query) {
            if (query !== '') {
                this.loading = true;
                setTimeout(() => {
                    this.loading = false;
                    this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
                }, 200);
            } else {
                this.clients = [];
            }
        }
    }
}
</scripts>

And the parent component looks like this:

<select-client v-model="filterForm.clientId"></select-client>

The select drop down works fine, but unfortunately, the select does not reveal the option I selected, it remains empty after I choose an option. I suspect that maybe I should switch the v-on:input to 'v-on:change', but it does not work either.

UPDATE I created a simple example, you can clone it here, please checkout the el-select-as-component branch. Run

npm install
npm run dev

You will see a simple page with 3 kinds of select:
The left one is a custom component written in raw select, it works fine.
The middle one is a custom component written in el-select, the dropdown remains empty but you can see the filterForm.elClientId in the console once you click Filter button. This is why I raise this question.
The right one is a plain el-select, it works fine.


Solution

  • The guideline says v-model is equivalent to v-bind:value and v-on:input but if you look closer, in the listener function, the variable binded is set with the event property. What you do in your exemple isn't the same, in your listener you emit another event. Unless you catch this new event, your value will never be set.

    Another thing is you can't modify a props, you should consider it like a read-only variable.

    If you want to listen from the parent to the emitted event into the child component, you have to do something like this

    <template>
      <el-select
        :value="selected"
        @input="dispatch"
        placeholder="Filter by short name"
        :filterable="true"
        :remote="true"
        :remote-method="filter"
        :loading="loading">
        <el-option
          v-for="item in clients"
          :key="item._id"
          :label="item.name"
          :value="item._id">
        </el-option>
      </el-select>
    </template>
    
    <script>
    export default {
      name: 'SelectClient',
    
      data() {
        return {
          selected: '',
          clients: [],
          loading: false,
        }
      },
    
      methods: {
        filter(query) {
          if (query !== '') {
            this.loading = true;
            setTimeout(() => {
              this.loading = false
              this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}]
            }, 200)
          } else {
            this.clients = []
          }
        },
    
        dispatch (e) {
          this.$emit('input', e)
          this.selected = e
        }
      }
    }
    </script>
    

    NB: a v-model + watch pattern will work too. The important thing is to $emit the input event, so the v-model in the parent will be updated.

    And in your parent you can use this component like this: <select-client v-model="clientId"/>.

    Tips: if you want to modify the same data in different place, you should have a single source of truth and prefer something like vuex. Then your component will be like this

    <template lang="html">
      <select
        v-model="clientId">
        <option
          disabled
          value="">Please select one</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
      </select>
    </template>
    
    <script>
    export default {
      data () {
        return {
          clientId: ''
        }
      },
    
      watch: {
        clientId (newValue) {
          // Do something else here if you want then commit it
          // Of course, listen for the 'setClientId' mutation in your store
          this.$store.commit('setClientId', newValue)
        }
      }
    }
    </script>
    

    Then in your other components, you can listen to $store.state.clientId value.