Introduction to the

The cascade selector, shown below, is also a common component that is a bit more complex

main.vue
menu.vue
Click here to

The HTML structure of the cascade selector input box

Vue represents the input field. The simplified HTML structure is as follows

<span class="el-cascader">
    <el-input>
        <template slot="suffix">
            <i v-if></i>
            <i v-else></i>
        </template>
    </el-input>
    <span class="el-cascader__label">... </span> </span>Copy the code

The structure is simple. The outermost span wraps all elements, which is relatively positioned and inline-block, and contains an

input box component that searches for the target content (the content is the data of the cascading selector).

then a template in

is used as a slot for 2 I tags. Note that slot=”suffix” is used to insert the 2 I tags as named slots in

, with a down arrow and a button to clear the input field. Then there is a span, which is the text in the input box shown below



Did you not find the HTML structure of the dropdown menu? Because the dropdown menu is mounted on document.body and controlled by popper.js, the structure is separated out

Cascade selector input box code analysis

Let’s look at the code for the outermost span

<span
    class="el-cascader"
    :class="[ { 'is-opened': menuVisible, 'is-disabled': cascaderDisabled }, cascaderSize ? 'el-cascader--' + cascaderSize : '' ]"
    @click="handleClick"
    @mouseenter="inputHover = true"
    @focus="inputHover = true"
    @mouseleave="inputHover = false"
    @blur="inputHover = false"
    ref="reference"
    v-clickoutside="handleClickoutside"
    @keydown="handleKeydown"
  >
Copy the code

As the outer span of a component, its main function is to pop up/hide the drop-down box after clicking on it. The previous section of the class is the style to control whether the input box is disabled. Is-opened is a very surprised class, it is not found in the source code, and the inspection element also found that the class is empty. To control the display of the dropdown menu, you can naturally imagine that the code below @click=”handleClick” controls this variable as follows

handleClick() {
      if (this.cascaderDisabled) return;
      this.$refs.input.focus();
      if (this.filterable) {
        this.menuVisible = true;
        return; } this.menuVisible = ! this.menuVisible; },Copy the code

This.$refs.input.focus() is used to retrieve the

element in the component and focus it.

:readonly=”readonly”; readonly=”readonly”; readonly=”readonly”

readonly() { const isIE = ! this.$isServer && !isNaN(Number(document.documentMode));
      return! this.filterable || (! isIE && ! this.menuVisible); }Copy the code

If yes, return false. If yes, return false. IsNaN (Number (document. DocumentMode) can easily determine whether, ie before I remember is generally use the navigator userAgent. IndexOf (” MSIE “) > 0 to judge, DocumentMode is an IE specific property

document.documentMode! ==undefined
(! isIE && ! this.menuVisible)

Return to handleClick. If (this.filterable) means that if you have search enabled, you can click on the input box and return to it without switching the dropdown menu. This makes sense because you want the dropdown menu to be displayed all the time. This. MenuVisible =! This. MenuVisible is the statement that actually switches

Now look at these four sentences on SPAN

@mouseenter="inputHover = true"
@focus="inputHover = true"
@mouseleave="inputHover = false"
@blur="inputHover = false"
Copy the code

This is the fork button that controls the display of the input field and is used to empty the input field, as shown below

@keydown="handleKeydown"

@keyDown =”handleKeydown” < span > < span > < span > < span > < span > < span > < span > Then bubbles to the parent span and triggers the parent span focus, at which point the key triggers the parent span keydown

Look again at the code for

<el-input
      ref="input"
      :readonly="readonly"
      :placeholder="currentLabels.length ? undefined : placeholder"
      v-model="inputValue"
      @input="debouncedInputChange"
      @focus="handleFocus"
      @blur="handleBlur"
      @compositionstart.native="handleComposition"
      @compositionend.native="handleComposition"
      :validate-event="false"
      :size="size"
      :disabled="cascaderDisabled"
      :class="{ 'is-focus': menuVisible }"
    >
Copy the code

The v-model=”inputValue” specifies the value bound to the input field. When the user types a character, the value is updated. InputValue is a property in the data in the component. @input=”debouncedInputChange”; @input=”debouncedInputChange”; @input=”debouncedInputChange”; Because the search function will call Ajax, it is asynchronous. Therefore, the request frequency to the server needs to be controlled. If it is not set, it will trigger the debouncedInputChange once after input a character, which is obviously too high frequency. Take a look at debouncedInputChange

this.debouncedInputChange = debounce(this.debounce, value => {
      const before = this.beforeFilter(value);
      if (before && before.then) {
        this.menu.options = [{
          __IS__FLAT__OPTIONS: true,
          label: this.t('el.cascader.loading'),
          value: ' ',
          disabled: true
        }];
        before
          .then(() => {
            this.$nextTick(() => {
              this.handleInputChange(value);
            });
          });
      } else if(before ! = =false) {
        this.$nextTick(() => { this.handleInputChange(value); }); }}); },Copy the code

Here debounce is a higher-order function, a complete implementation of the stabilization function, as described in NPM. The first argument is the stabilization time, and the second argument is the specified callback function, which returns a new function as the function bound to the input event. BeforeFilter (value) the beforeFilter of const before = this.beforeFilter(value) is a function

beforeFilter: {
      type: Function,
      default: () => (() => {})
    },
Copy the code

This function is a function, and before is the return value. This function is passed in by the user to filter the hook before the search function. The parameter is the input value, and the filter is stopped if false or a Promise is returned and reject is rejected.

If (before && before. Then) If (before && before. Then) if this function returns true and has then methods, it is a promise. This. HandleInputChange (value); else if this is not a promise and returns true, then handleInputChange(); I don’t know why nextTick is used here


@compositionstart.native=”handleComposition” listens for a native event. Note that this is the native event on the

component that listens for the root element, not the native HTML element. So you have to use the native modifier

And then notice the mounted method has this statement

mounted() {
    this.flatOptions = this.flattenOptions(this.options);
  }
Copy the code

This. Options is an array of data passed in by the user to render the dropdown menu. Each value of the array is an object, including value, label, and children. So why flatten? The reason is that the search function traverses all the data items, so a flattened array is easier to traverse. Here’s the code

flattenOptions(options, ancestor = []) {
      let flatOptions = [];
      options.forEach((option) => {
        const optionsStack = ancestor.concat(option);
        if(! option[this.childrenKey]) { flatOptions.push(optionsStack); }else {
          if(this.changeOnSelect) { flatOptions.push(optionsStack); } flatOptions = flatOptions.concat(this.flattenOptions(option[this.childrenKey], optionsStack)); }});return flatOptions;
    },
Copy the code

If so, recursively call itself and concat to flatOptions and return, otherwise push directly. Here, the second parameter of this method is used to save multi-level menu, and then look in the search code, the core search logic is as follows

let filteredFlatOptions = flatOptions.filter(optionsStack => {
        return optionsStack.some(option => new RegExp(escapeRegexpString(value), 'i')
          .test(option[this.labelKey]));
      });
Copy the code

Value is the value entered by the user to query for. In this case, optionStack is the array. If any of the items in the array are true, it returns true. Some higher-order functions are used to obtain the results of the filteredFlatOptions search

Cascading selector drop-down menu analysis

By looking at the code in main.vue, you can see that the HTML section does not have a drop-down menu structure. In fact, the drop-down menu is mounted on the body. Looking at the source code, we find an initMenu method that will be called the first time we showMenu, as shown below

initMenu() {
      this.menu = new Vue(ElCascaderMenu).$mount(a); this.menu.options = this.options; this.menu.props = this.props; this.menu.expandTrigger = this.expandTrigger; this.menu.changeOnSelect = this.changeOnSelect; this.menu.popperClass = this.popperClass; this.menu.hoverThreshold = this.hoverThreshold; this.popperElm = this.menu.$el;
      this.menu.$refs.menus[0].setAttribute('id', `cascader-menu-${this.id}`);
      this.menu.$on('pick', this.handlePick);
      this.menu.$on('activeItemChange', this.handleActiveItemChange);
      this.menu.$on('menuLeave', this.doDestroy);
      this.menu.$on('closeInside', this.handleClickoutside);
    },
Copy the code

This. Menu = new Vue(ElCascaderMenu).$mount() $mount() does not pass an argument to render out of the document, but does not mount to the DOM. The specific mount operation is performed in vue-popper.js, where the this.menu is used to save the instance of the pull-down menu, so for user manipulation of the pull-down menu, This. PopperElm = this.menu.$el assigns the root dom element of the dropdown menu to popperElm. Here’s how it came about

const popperMixin = {
  props: {
    placement: {
      type: String,
      default: 'bottom-start'
    },
    appendToBody: Popper.props.appendToBody,
    arrowOffset: Popper.props.arrowOffset,
    offset: Popper.props.offset,
    boundariesPadding: Popper.props.boundariesPadding,
    popperOptions: Popper.props.popperOptions
  },
  methods: Popper.methods,
  data: Popper.data,
  beforeDestroy: Popper.beforeDestroy
};
Copy the code

Use popperMixin to mix vue-popper.js methods, data, etc. into the input field. The purpose of this is to be able to manipulate popper components within this component. The last few lines of initMenu are the various events that $emit fires in the listener drop-down menu

So far I haven’t seen how the dropdown is mounted on the body, not in initMenu, so let’s go ahead and see, when I click on the input box it brings up the dropdown, showMenu, showMenu, okay

showMenu() {
      if (!this.menu) {
        this.initMenu();
      }
      ...
      this.$nextTick(_ => {
        this.updatePopper();
        this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
      });
    },
Copy the code

You can see that this.updatePopper is the update dropdown, and you need to have nextTick because you’ve changed data in initMenu, and you need to get the updated DOM. UpdatePopper is mixed into the input field section via popperMixin, which is in vue-popper.js

updatePopper() {
      const popperJS = this.popperJS;
      if (popperJS) {
        popperJS.update();
        if(popperJS._popper) { popperJS._popper.style.zIndex = PopupManager.nextZIndex(); }}else{ this.createPopper(); }},Copy the code

This.createpopper () initializes the popperJS plugin by checking whether the popperJS plugin exists or not. Continue to watch this. CreatePopper ()

createPopper() {... const popper = this.popperElm = this.popperElm || this.popper || this.$refs.popper; .if (this.appendToBody) document.body.appendChild(this.popperElm);
}
Copy the code

This. PopperElm gets the root dom element of the dropdown menu from the previous analysis, and then mounts it to the body. If it’s the old direct appendCHild, then the dropdown menu is mounted. It’s a hassle.

Let’s look at the HTML structure of the drop-down menu

return (
        <transition name="el-zoom-in-top" on-before-enter={this.handleMenuEnter} on-after-leave={this.handleMenuLeave}>
          <div
            v-show={visible}
            class={[
              'el-cascader-menus el-popper',
              popperClass
            ]}
            ref="wrapper"
          >
            <div x-arrow class="popper__arrow"></div>
            {menus}
          </div>
        </transition>
      );
Copy the code

Transform scaleY(1); transform scaleY(1); transform scaleY(1); ScaleY (0) and the scaling process the other way around, and then look at the two hook functions, this.handleMenuEnter, that fire when the dom is inserted into the drop-down menu. So what’s going on here?

handleMenuEnter() {
        this.$nextTick(() => this.$refs.menus.forEach(menu => this.scrollMenu(menu)));
      }
Copy the code

There is a layer of nextTick wrapped here, because you can’t call it until the DOM is inserted, otherwise you might get an error. Then watch scrollMenu

scrollMenu(menu) {
        scrollIntoView(menu, menu.getElementsByClassName('is-active') [0]); },Copy the code

As the name suggests, all it does is move the dom element of the second parameter into the dom visible from the first parameter

export default function scrollIntoView(container, selected) {
  if(! selected) { container.scrollTop = 0;return;
  }
  const offsetParents = [];
  let pointer = selected.offsetParent;
  while(pointer && container ! == pointer && container.contains(pointer)) { offsetParents.push(pointer); pointer = pointer.offsetParent; } const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0); const bottom = top + selected.offsetHeight; const viewRectTop = container.scrollTop; const viewRectBottom = viewRectTop + container.clientHeight;if (top < viewRectTop) {
    container.scrollTop = top;
  } else if(bottom > viewRectBottom) { container.scrollTop = bottom - container.clientHeight; }}Copy the code

The core idea of this code is to increment the offsetTop value of the selected element. The while loop uses pointer.offsetParent to fetch its offsetParent element. OffsetParent is the parent element whose position is not static, which is stored as an array, and offsetTop is added by offsetparents. reduce. This results in the distance between the bottom of the Selected element and the top of the Container element. ScrollIntoView is actually a new feature in H5. It is a new API that allows elements to be moved into the page view. However, it does not work for scrolling elements in containers, and it is not very compatible.

Visible is updated in main.vue, that is, in the showMenu when the user clicks on the input field. Then the class section ‘el-Cascader-Menus’ declares some basic menus, and the El-popper class makes the dropdown menu a margin away from the input box.

represents the triangular arrow in the drop-down menu. This is the classic way to write three border transparent, one border has color to form a triangle.

Then there is only one {menu} in the div, which is the UL list in the drop-down menu. The UL list is generated using the following method

const menus = this._l(activeOptions, (menu, menuIndex) => {
    ...
    const items = this._l(menu, item => {
        ...
         return( <li>... </li> ) }return (<ul>{items}</ul)
}
Copy the code

This method is very long, the above is the simplified logic, so it can be seen as the li list of each ul, then the ul list, so the question is: what is this._l? In this file and related files are not searched, finally found is actually Vue source inside the thing. RenderList is the alias of this method. RenderList is shown below

export functionrenderList ( val: any, render: ( val: any, keyOrIndex: string | number, index? : number ) => VNode ): ? Array<VNode> {letret: ? Array<VNode>, i, l, keys, keyif (Array.isArray(val) || typeof val === 'string')
    ret = new Array(val.length)
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    }
    
  ...
  
  return ret
}
Copy the code

So here’s the code for the flow format, for type control, the renderList is a higher-order function, the second argument is passed to a Rander method, and then the logic inside the if argument is if val is an array, Render ret[I] = render(val[I], I) executes each item of the array and saves the result in the ret array before, finally returns. This function processes each item of the passed val argument and returns the new array. So this._l(activeOptions, (menu, menuIndex) above returns an array of

  • , which is then inserted into HTML for rendering
  • Const menus = this._l(activeOptions, (menu, menuIndex) => {} Finally, a look at the click event function when you click on an item in the drop-down menu

    Component/Form /rate scoring

    activeItem(item, menuIndex) {
            const len = this.activeOptions.length;
            this.activeValue.splice(menuIndex, len, item.value);
            this.activeOptions.splice(menuIndex + 1, len, item.children);
            if (this.changeOnSelect) {
              this.$emit('pick', this.activeValue.slice(), false);
            } else {
              this.$emit('activeItemChange', this.activeValue); }},Copy the code

    The first parameter is li, and the second parameter is menu index. This value is passed by const menus = this._l(activeOptions, (menu, menuIndex) => {}. ActiveOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions = activeOptions

    this.activeValue.splice(menuIndex, len, item.value)

    The splice above deletes len elements from menuIndex, and updates the selected item by adding the newly selected value of item.value to the array. Note next enclosing activeOptions. Splice (menuIndex + 1, len, item. Children here the first parameter is the menuIndex + 1, because want to delete the submenu rather than himself, so is the next position, Then add the new submenu to the array

    conclusion

    The code of this component is a little complicated, and some of the code does not understand, anyway, slowly look, the first time to see a lot of places do not understand