Introduction to the

The Element drop-down selector is shown below

<select>
select.vue

import Emitter from 'element-ui/src/mixins/emitter';
import Focus from 'element-ui/src/mixins/focus';
import Locale from 'element-ui/src/mixins/locale';
import ElInput from 'element-ui/packages/input';
import ElSelectMenu from './select-dropdown.vue';
import ElOption from './option.vue';
import ElTag from 'element-ui/packages/tag';
import ElScrollbar from 'element-ui/packages/scrollbar';
import debounce from 'throttle-debounce/debounce';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
import { t } from 'element-ui/src/locale';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import { getValueByPath } from 'element-ui/src/utils/util';
import { valueEquals } from 'element-ui/src/utils/util';
import NavigationMixin from './navigation-mixin';
import { isKorean } from 'element-ui/src/utils/shared';
Copy the code

However, there are a lot of things worth learning about these imports

Drop down the HTML structure of the selector

Let’s first analyze the HTML structure of the drop-down selector. The simplified HTML code is as follows

<template>
    <div class="el-select" >
        <div class="el-select__tags"
        </div>
        
        <el-input></el-input>
        
        <transition>
            <el-select-menu>
            <el-select-menu>
        </transtion>
    </div>
</template>
Copy the code

The outermost div wraps all the child elements (relative positioning). The first div inside is a wrap div that displays the tag of the drop-down selector, as shown below. Transform :translateY(-50%) is centered vertically inside the outermost div

The second

is the input component wrapped by Element. As described in the previous article, this input box is as wide as the outermost div, as shown below. The arrow button on the right is placed in its padding position

<transtion>
<el-select-menu>
Therefore, only the input in the middle of the whole drop-down component is relative positioning, the other are absolute positioning, and to be good at reuse their own existing components, rather than re-write

Some functional source code analysis

It would take at least a week to write all the features, so only part of it

Drop-down box main operation flow logic comb

The following analysis of the main operation process of the drop-down box and its data transfer process first look at the use of the drop-down box, the official website code is as follows

<el-select v-model="value" placeholder="Please select">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value">
    </el-option>
</el-select>
Copy the code

The data section is as follows

<script>
  export default {
    data() {
      return {
        options: [{
          value: 'option 1',
          label: 'Gold cake'
        }, {
          value: 'option 2',
          label: 'Double skin milk'
        }]
        value: ' '
      }
    }
  }
</script>
Copy the code



has a v-Model. This is the v-Model usage of the component.

























Represents the displayed text and the actual value of the drop-down item, and options in data also provide the corresponding key. Note here that

is inserted as a slot in

, so

is required to hold content in

. If the component does not contain an element, any content passed to it is discarded. Looking at the HTML code, the slot locations are as follows






























<el-select-menu
        <el-scrollbar>
          <el-option>
          </el-option>
          
          <slot></slot>
          
        </el-scrollbar>
        <p
         ...
        </p>
</el-select-menu>
Copy the code

Slot is contained within the

scrollbar component. The implementation of this component is very basic and slightly complex, so all options are put into the scrollbar component

When the user clicks the drop-down box in the initial state,toggleMenu is triggered and displayed. ToggleMenu is shown as follows

toggleMenu() {
    if(! this.selectDisabled) {if (this.menuVisibleOnFocus) {
        this.menuVisibleOnFocus = false;
      } else{ this.visible = ! this.visible; }if (this.visible) {
        (this.$refs.input || this.$refs.reference).focus(); }}},Copy the code

This. MenuVisibleOnFocus is not triggered if it is in the disabled state. If it is in the disabled state, it will not trigger the event. At this point, another input box in the component (at the cursor in the image below) will be rendered, and then the input box will be focused, so the drop-down menu does not need to be hidden (so that you can see existing items), so the if judgment is made here. this.visible = ! This. visible then this is the state of the dropdown menu

After the drop-down menu is displayed, clicking on an option closes the drop-down menu and passes the value to the parent component. Look at the contents of the Option component first

<template>
  <li
    @mouseenter="hoverItem"
    @click.stop="selectOptionClick"
    class="el-select-dropdown__item"
    v-show="visible"
    :class="{ 'selected': itemSelected, 'is-disabled': disabled || groupDisabled || limitReached, 'hover': hover }">
    <slot>
      <span>{{ currentLabel }}</span>
    </slot>
  </li>
</template>
Copy the code

Very simple, wrapped around the Li element. @mouseEnter =”hoverItem” means that the hoverItem event is triggered when you hover with the mouse over something. Here you might ask, why do you hover with the mouse? When you hover over an option, press Enter to select the item, or click, so update the hover option to see the contents of the hoverItem

hoverItem() {
    if(! this.disabled && ! this.groupDisabled) { this.select.hoverIndex = this.select.options.indexOf(this); }},Copy the code

????? Black question mark! What the hell is going on? Select = this.select = this.select = this.select = this.select = this.select = this.select = this.select

inject: ['select'].Copy the code

It’s not a prop or a data. It’s a dependency injection. The idea behind dependency injection is to give descendant components access to the contents of the parent component, because if it’s a parent component, the parent component can access the parent component through $parent. So with dependency injection, dependency injection can be used simply by declaring the following properties in the ancestor component: provide property, value is the method or property of the ancestor component

provide: function () {
  return {
    xxMethod: this.xxMethod
  }
}
Copy the code

Then declare the following in the descendant component

inject: ['xxMethod']
Copy the code

Back to the dependency injection select of the Option component, its location is in the ancestor (not the parent)

, in the drop-down selector component of this article, as follows

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

It returns this, which is an instance of the dropdown selector component, so it can use the hoverIndex property on the this.select.hoverIndex dropdown selector, Then continue to analyze this. Select. HoverIndex = this. Select the options. The indexOf (this), the meaning of this sentence is after press enter, move the mouse suspended the option in the options of the serial number assigned to hoverIndex,

takes care of the rest of the logic. Mouse hover can also be selected by pressing Enter, this is how to achieve it? As you can guess, the keydown.enter event must be bound to the input

@keydown.native.enter.prevent="selectOption"
Copy the code

What’s with all these modifiers here? Native modifiers are mandatory. If you want to use v-Ons to listen for custom events, you need to use native modifiers to listen for native events. Prevent does not trigger the default Enter event, such as press Enter to submit a form. And then look at the selectOption method

 selectOption() {
        if(! this.visible) { this.toggleMenu(); }else {
          if(this.options[this.hoverIndex]) { this.handleOptionSelect(this.options[this.hoverIndex]); }}},Copy the code

HoverIndex is used to update the selected item. Now, how does handleOptionSelect update the selected item? This method passes in an Option instance

 handleOptionSelect(option, byClick) {
        if (this.multiple) {
          const value = this.value.slice();
          const optionIndex = this.getValueIndex(value, option.value);
          if (optionIndex > -1) {
            value.splice(optionIndex, 1);
          } else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) {
            value.push(option.value);
          }
          this.$emit('input', value); this.emitChange(value); . }else {
          this.$emit('input', option.value);
          this.emitChange(option.value);
          this.visible = false; }... },Copy the code






It’s important to understand what value is. Value is a prop of this component, a product of the v-model syntax sugar. Value in the V-Model above is the data item in the data passed by the user. So if this value changes, it will cause the user’s incoming value to change. If there is an option in the value array, splice will remove it. If there is no option in the value array, then push it into the value array and emit the input event of the parent component to change the value. At the same time, trigger the change of the parent component to notify the user that my value has changed. If it is in single mode, it is easy to emit directly.




SelectOptionClick in @click.stop=”selectOptionClick” is triggered when a direct mouse click on an option

selectOptionClick() {
        if(this.disabled ! = =true&& this.groupDisabled ! = =true) {
          this.dispatch('ElSelect'.'handleOptionClick', [this, true]); }},Copy the code

This method with the general method of dispatch in < el – select > trigger handleOptionClick events, introduced to the current option instance, the dispatch is completed the child component to the ancestors component logic of events, There must be an on method to receive the event at

, as follows

this.$on('handleOptionClick', this.handleOptionSelect)
Copy the code

You can see that this handleOptionSelect is a method, so clicking on an option and pressing Enter will eventually trigger this method to update the value

In summary, this is a complete logical description of the process

Click outside the Select box to collapse the dropdown menu

Look at the outermost div code

<div
    class="el-select"
    :class="[selectSize ? 'el-select--' + selectSize : '']"
    @click.stop="toggleMenu"
    v-clickoutside="handleClose">
Copy the code

Here @click binds the click event to toggle menu hide and display. Below v-clickOutside =”handleClose” is the key. This is a Vue directive. The logic in handleClose is this. Visible = false to set the menu visible to False to hide the dropdown menu, and to trigger the handleClose when the mouse clicks outside the dropdown component. This is a common requirement, but it’s not easy to implement. The idea is to bind the document to a mouseup event that determines whether the clicked target is included in the target component. This directive corresponding object through the import Clickoutside from ‘element – the UI/SRC/utils/Clickoutside’ introduction, because a lot of components with this method, so to pull away out alone on the util directory, Enter the method’s bind method here to see the following two statements

! Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e)); ! Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});
Copy the code

This binds the Document to a mouse-down lift event (invalid server rendering), records a pressed DOM element when it is pressed, iterates over all the DOM with that instruction when it is lifted, and then executes documentHandler to decide, as follows

function createDocumentHandler(el, binding, vnode) {
  return function(mouseup = {}, mousedown = {}) {
    if(! vnode || ! vnode.context || ! mouseup.target || ! mousedown.target || el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || (vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target))))return;

    if (binding.expression &&
      el[ctx].methodName &&
      vnode.context[el[ctx].methodName]) {
      vnode.context[el[ctx].methodName]();
    } else{ el[ctx].bindingFn && el[ctx].bindingFn(); }}; }Copy the code

Notice that this is creating a Document enthandler from createDocumentHandler, Contains (mouseup.target),el. Contains (mousedown.target), which uses the native contains method to determine if the click is contained in the DOM element EL. Call vnode.context[el[CTX].methodName]() and call the handleClose method in v-ClickOutside =”handleClose” to hide the dropdown menu. El [CTX]. MethodName is initialized in the bind method of the directive, as follows

bind(el, binding, vnode) {
    nodeList.push(el);
    const id = seed++;
    el[ctx] = {
      id,
      documentHandler: createDocumentHandler(el, binding, vnode),
      methodName: binding.expression,
      bindingFn: binding.value
    };
  },
Copy the code

Assign expression to methodName. What is CTX? CTX on top const CTX = ‘@@clickOutsidecontext’ which I think adds a property to the DOM el that starts with two @ signs, which means it’s special, it’s not easy to override, and then the value of that property is an object that stores a lot of information, The logic here is basically that when the instruction is bound to the DOM element for the first time, it adds attributes such as the method to be executed to the DOM element, and then binds the mouseup event to the document. Later, when the user clicks, it takes out the DOM of the corresponding element for judgment. If the judgment is true, it takes out the previously bound method on the DOM and executes it

Location of the drop-down menu

You might think that the dropdown is definitely located in the input field, but that would be wrong. The dropdown is actually added to the Document.body

<el-select>

popper.js

 createPopper() {...if(! popper || ! reference)return;
      if (this.visibleArrow) this.appendArrow(popper);
      
      if (this.appendToBody) document.body.appendChild(this.popperElm);
      
      if(this.popperJS && this.popperJS.destroy) { this.popperJS.destroy(); }... this.popperJS = new PopperJS(reference, popper, options); this.popperJS.onCreate(_ => { this.$emit('created', this);
        this.resetTransformOrigin();
        this.$nextTick(this.updatePopper);
      });
    
    },

Copy the code

CreatPopper logic is initialized, inside the if (this. AppendToBody) document. The body. The appendChild (enclosing popperElm) this sentence is the key, Move the pop-up drop-down menu to the body via appendChild, noting that the appendChild argument moves it if it is an existing element. Then you’ll notice that the drop down menu moves along with the mouse wheel, notice that the drop down menu is on the body, so the move logic is implemented in popperJS. It’s a little complicated. First of all, there has to be an addEventListener listening for scroll events

Popper.prototype._setupEventListeners = function() {
        // NOTE: 1 DOM access here
        this.state.updateBound = this.update.bind(this);
        root.addEventListener('resize', this.state.updateBound);
        // if the boundariesElement is window we don't need to listen for the scroll event if (this._options.boundariesElement ! = = 'window') { var target = getScrollParent(this._reference); // here it could be both `body` or `documentElement` thanks to Firefox, we then check both if (target === root.document.body || target === root.document.documentElement) { target = root; } target.addEventListener('scroll', this.state.updateBound); this.state.scrollTarget = target; }};Copy the code

Target.addeventlistener (‘scroll’, this.state.updatebound); If you look at updateBound, it’s bound to this using the update method, which looks like this

 /**
     * Updates the position of the popper, computing the new offsets and applying the new style
     * @method
     * @memberof Popper
     */
    Popper.prototype.update = function() {
        var data = { instance: this, styles: {} };

        // store placement inside the data object, modifiers will be able to edit `placement` if needed
        // and refer to _originalPlacement to know the original value
        data.placement = this._options.placement;
        data._originalPlacement = this._options.placement;

        // compute the popper and reference offsets and put them inside data.offsets
        data.offsets = this._getOffsets(this._popper, this._reference, data.placement);

        // get boundaries
        data.boundaries = this._getBoundaries(data, this._options.boundariesPadding, this._options.boundariesElement);

        data = this.runModifiers(data, this._options.modifiers);

        if (typeof this.state.updateCallback === 'function') { this.state.updateCallback(data); }};Copy the code

As the name implies, update is used to update the location information of the pop-up box, which is the corresponding position update of each submethod