preface

This paper starts with the code structure of SELECT component to understand how to organize parent-child component communication, and focuses on the analysis of the clever implementation of ElSelect component and ElOption component. It also describes how the four functions of the SELECT component are implemented.

The code structure

Element uses a div to simulate select. The select component contains navigation-mixin.js, option-group.vue, option.vue, select-dropdown.vue, select.vue and other files

The HTML structure of the select. Vue main file is similar to that of the main simulation select, except that Element is more complex. The HTML structure of the SELECT component is as follows (some code is omitted for clarity) :

<div class="el-select">
  <! -- Multiple choices -->
  <div v-if="multiple" class="el-select__tags">
    <span>
      <! -- Place the selected tag in multiple selection, display it as a tag, or combine it into a paragraph of text -->
    </span>
    <! -- Search function -->
    <input v-model="query" v-if="filterable" />
  </div>
  <! -- The selected value is displayed when the option is selected -->
  <el-input v-model="selectedLabel" :class="{ 'is-focus': visible }">
    <! -- xxx -->
  </el-input>

  <! -- Drop down box -->
  <el-select-menu>
    <el-scrollbar v-show="options.length > 0 && ! loading">
      <! -- Option content -->
      <el-option :value="query" created v-if="showNewOption"> </el-option>
      <slot></slot>
    </el-scrollbar>
    <! -- default text with options blank or select loaded -->
    <template
      v-if="emptyText && (! allowCreate || loading || (allowCreate && options.length === 0 ))"
    >
      <! -- xxx -->
    </template>
  </el-select-menu>
</div>
Copy the code

How do you organize parent and child components

When we looked at the structure of the code in the last section, you might have thought that the SELECT component was complicated. How does it handle the communication between parent and child components? I have analyzed the organization method of its Table component before, using simple store mode, and select component using broadcast/dispatch and Inject /provide. (PS: Broadcast is written by ElementUI itself, vue 2.0 has removed $dispatch and $broadcast)

Inject and dojo.provide

Vue2.2.0 new API, this pair of options need to be used together to allow an ancestor component to inject a dependency to all of its descendants, regardless of component level, for as long as the upstream and downstream relationship is established. In a nutshell: Ancestor components provide variables through providers, and descendant components inject variables through inject.

I’ll look at how ElementUI uses inject and provide, and why this.$parent and this.$children can’t communicate with each other

Dispatch and broadcast

Broadcast code is placed in ‘elder-UI/SRC /mixins/ Emitter ‘, and mixed with mixins, using eg:this.broadcast(‘ElOption’, ‘queryChange’, ”); And this dispatch (” ElSelect “, “setSelected”);

As a little over a year old front end, I have never used broadcast. See Vue$Dispatch and $Broadcast for details

Although broadcast has its drawbacks, for example, the component tree-based event flow approach is rather confusing and does not solve the communication problem between sibling components. But in parent-child nested components, it’s neat to use $dispatch and $broadcast to call events remotely to a parent or child, avoiding the need to pass props or use refs to call component instance methods.

ElSelect and ElOption

If I were to write this component, I might use
, that is, the options of the select component are passed to the component through options. It may be possible eventually, but doing so feels a bit over-encapsulated. The structure of this component is not very similar to native SELECT. We often use the Select component this way, isn’t it intuitive:

<el-select v-model="input" filterable clearable placeholder="Please select">
  <el-option label="foo" value="foo"></el-option>
  <el-option label="foo" value="foo"></el-option>
</el-select>
Copy the code

There is a default slot in the select component, which is fine, but considering that the Select component needs to communicate with the Option component, for example, the Option component needs to know whether the select component has multiple selections, whether it can search, and so on, The select component needs to know the number of options components and so on. How do you do that? The answer is inject and provide

The option component uses this. Select to modify the parent component’s properties directly:

provide() {
  return {
    'select': this
  };
}
Copy the code

Create as many instances of El-Option as there are options, and look at the created life cycle of El-Option:

created() {
  // Push the el-Option instance into the options of the parent component select
  this.select.options.push(this);
  this.select.cachedOptions.push(this);
  this.select.optionsCount++;
  this.select.filteredOptionsCount++;
  // Listen for custom events
  this.$on('queryChange'.this.queryChange);
  this.$on('handleGroupDisabled'.this.handleGroupDisabled);
}

Copy the code

This.select.options.push (this); Select. Options has a lot of values. Don’t worry, look at the beforeDestroy life cycle of El-Option

beforeDestroy() {
  this.select.onOptionDestroy(this.select.options.indexOf(this));
}
Copy the code

The onOptionDestroy method of the parent component select is destroyed by el-Option and removed from the options of the parent component:

onOptionDestroy(index) {
  if (index > - 1) {
      this.optionsCount--;
      this.filteredOptionsCount--;
      // cachedOptions is not removed
      this.options.splice(index, 1); }}Copy the code

function

Alternatives are displayed in groups

The HTML structure of option-group.vue is very simple:

<ul class="el-select-group__wrap" v-show="visible">
  <li class="el-select-group__title">{{ label }}</li>
  <li>
    <ul class="el-select-group">
      <slot></slot>
    </ul>
  </li>
</ul>
Copy the code

$parent = this.$parent = this.$parent = this.$parent = this. It is appropriate to use inject and provide to be compatible with both cases where el-Option is a child component or a grandchild component.

Local search

It’s actually local filtering, so to enable local search you need to pass in the Filterable field instead of the searchable field, and filtering is really nice. As I said before, you can create as many instances of el-Option as you want, so when you enable filtering, Simply hide the el-Option instances that do not match the re. The code looks like this (simpler than you think) :

queryChange(query) {
   // Since select can also create entries manually, manually created entries must be displayed
  this.visible =
    new RegExp(escapeRegexpString(query), 'i').test(this.currentLabel) ||
    this.created;
  if (!this.visible) {
    // The number of filtered options is reduced by one
    this.select.filteredOptionsCount--; }}Copy the code

The remote search

That’s the simple part of the code, because we know that the remote search, the options are returned by the server, just create a new el-Option component.

if (this.remote && typeof this.remoteMethod === 'function') {
  this.hoverIndex = - 1;
        this.remoteMethod(val); }}Copy the code

Select The remote search component command output

Element-ui When your options are fixed, it will display the corresponding label based on the selected value, but the remote search component is a problem because options are not fixed.

The solution is to pass in options for the selected value. For example, if I have a component ArticleSelect and I select id [1,2], the component will not echo if I don’t do anything about it. Only one, two tags are displayed dryly. But I can echo the title by passing the options of the selected value (for example, [{value:1,label:’ first ‘},{value:2,label:’ second ‘}]) into the component.

The select component searches for options dynamically based on the search term. Let’s look at the code for the ElementUI Select component to set the selected value:

setSelected() {
    // omit code for cases that are not multiple choices
    / / multi-select
    let result = [];
    if (Array.isArray(this.value)) {
      this.value.forEach(value= > {
        // Note that this is a push operation and the getOption is fetched from cachedOptions (cachedOptions are cached and will not be destroyed because of el-Option destruction)
        result.push(this.getOption(value));
      });
    }
    this.selected = result;
    // Recalculate the height of the options box after setting
    this.$nextTick((a)= > {
      this.resetInputHeight();
    });
  }
Copy the code

As can be seen from the code, the selected value set by Element is a push operation, so subsequent changes of options will not affect the selected value, which perfectly meets my needs

Create entries

Here, more subtly, LET me show the code structure of the drop-down box:

<el-scrollbar v-show="options.length > 0 && ! loading">
  <! -- Option content -->
  <! ShowNewOption --> showNewOption
  <el-option :value="query" created v-if="showNewOption"> </el-option>
  <slot></slot>
</el-scrollbar>
Copy the code

The code to calculate the property showNewOption:

showNewOption() {
  let hasExistingOption = this.options.filter(option= >! option.created) .some(option= > option.currentLabel === this.query);
    // When this.query is null, the el-Option component is destroyed because v-if is the true conditional rendering.
  return this.filterable && this.allowCreate && this.query ! = =' ' && !hasExistingOption;
}
Copy the code

conclusion

Element’s SELECT component is a bit complicated, but the functionality, especially the code structure, is really neat. As for the CSS part, not my strengths, I will not analyze.

Refer to the article

  • Vue $dispatch and $broadcast details
  • There are six ways to communicate between Vue components
  • vm.$on( event, callback )
  • Element source code analysis series 7-Select(drop down Select box)