background

Because the project needs to write a drop-down component similar to the remote search drop-down box of Element-UI, but Element’s component does not support the drop-down box and the customization of selected people, we implemented this component by emulating Element (VUE + TS).

my-demo:

demo

Design of nested structures

When you first look at this structure (the selected result is displayed in the input box and can be entered later), you can think of two ways to implement it:

  1. This scheme has two major problems: 1. The length of the selected result is uncertain, so the length of the div is uncertain, and the location of the cursor is difficult to determine; 2. 2. Line breaks will occur after multiple results are selected. It is particularly difficult to determine how many divs will occupy the entire line. It is obvious that such a scheme is not feasible
  2. The outer layer is a fake input box, the inner layer is a real input box, and then hides the outer border of the inner input box, using the style of the external fake input box to simulate the various states of an input box. The implementation of element-UI is similar to this situation. This scheme can be implemented

In the implementation of scheme 2, the implementation of element-UI is that the outermost box is a div, and then the two input boxes are positioned at the same time. The real input box is input, and the fake input box is placeholder. The selection result is displayed in the same line with the real input box and the input box is pushed behind. When I implement this scheme, I feel that two input is a bit redundant. The fake input only provides the function of a placeholder. You can use the dynamic placeholder input inside the placeholder, and place the placeholder empty after selecting the content. This eliminates the need for a fake input tag and the need for positioning (which can be more hierarchical). Select the result display is the use of Flex layout, the selected result cycle rendering in div, the most behind the input box, you can realize the input box has been selected at the end of the result, and select multiple people can be newline (to solve the problem of the height of the input box and the width of the selected result is not fixed). Then, by listening to the focus and blur events in the input box, some style changes are made to the external div to simulate the input, out-of-focus and focus effects of a real input box.

Note: I used flex layout to render the selected results with span tags or inline block elements, but ensure that the structure passed in slot does not use block-level elements.

Code implementation

<! --> <div class="demo-outBox-XL" @click="chooseInput" :class="{'demo-is-focus-XL': inputFocus}"> <! <span @click.stop= <span @click.stop="clearChecked">
        <img src="./assets/crossIcon.svg" class="demo-InputCloseIcon-XL" >
    </span>
    <div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': ! showClearIcon}" v-if="isMultiple"> <! --> <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL">
            <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
                <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot> </div> </div> <! Input --> <inputtype="text" v-model.trim="searchVal" :placeholder=! "" checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne" v-on="$listeners">
    </div>
</div>
Copy the code

Drop – down box and select the result display style design

This function is not available on Element-UI. The element dropdown and check result only support the display of a single field and cannot be customized. To implement this function, I use the named slot. Use slot-scope=”{item}” in the parent component to receive the data from the component, and set the display style in the parent component, realize the display style customization;

When implementing this function, note that the item exposed by the component will not be usable if it is received directly with slot-scope=”item”. In this case, it is a JSON string and you need to use {item} to get the passed object and use it. A default value was added to slot to ensure that the label field is displayed by default if the component is called without a slot. The writing method of slot uses the old writing method, because the project does not support the latest writing method of slot (you can see the difference between the new writing method and the abandoned writing method on the vUE official website).

Child component code sample

<! --> <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL">
            <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}"> <! --name = name v-bind = object to be exposed {{item[props. Label]}} --> <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            </div>
        </div>
Copy the code

Parent component code sample

<! --> <template slot="tag" slot-scope="{ item }">
    <img src="./img.jpg" style="width: 16px; height: 16px; border-radius: 50%;">
    <div style="line-height: 14px; font-size: 14px; margin-left:4px; word-wrap: break-word; word-break: break-word;">
        {{item.name}}
    </div>
</template>
Copy the code

Bind methods bound to components to the target tag input

The first thing that came to my mind was to use the native modifier. After trying, I found that it could not be implemented. Only the input event could be bound to it, and the change event would not be invalid. Then use the $emit bound method commonly used in the input, and within the method to use $emit to invoke the parent component bindings corresponding method, this method has some defects are not perfect, which can only be bound himself be preset some good method, if the group internal didn’t go to call the method, the binding method will be null and void in the parent component, So it also negates this method. Another problem with using this method is that the v-Model binding I wrote on the component (the default child uses value to receive the parent’s value, and calls the parent’s input method to change the parent’s value) conflicts with the input event I want to bind on the component. The value of the V-Model is null when the input event is called. By modifying the Model configuration, the V-Model default values are changed and received. The value of the V-Model binding of the change component is no longer changed using the INPUT method.

Finally, the $Listeners receive the methods of the parent component and v-on bind them to the component’s input to fix the problem. The method bound to the input does not conflict with the same method sent by the parent component, but executes both perfectly.

Code sample

<! --> <Myinput V-model ="arr" :options="options" multiple @input="inputEvent" @change="changeEvent" @keyup.enter="enterEvent"> </Myinput> <! Subcomponent --> <div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': ! showClearIcon}" v-if="isMultiple">
    <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL">
        <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
            <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            <span @click.stop="deleteChecked(item)" class="demo-icon-XL">
                <img src="./assets/crossIcon.svg" class="demo-tagIcon-XL" >
            </span>
        </div>
    </div>
    <input type="text" v-model.trim="searchVal" v-on="$listeners" :placeholder=! "" checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne">
</div>
Copy the code

Drag function implementation

This function was added to the product later. After selecting people, there are two ways to achieve it:

  1. The native Drag method
  2. Refer to the Drag plugin

Then the use of the native method to achieve this function, can be achieved, and did not find a problem, a short time to achieve this function, the code example is as follows:

<div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': ! showClearIcon}" v-if="isMultiple">
    <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL"
        :draggable="isDraggable && ! deleteStatus"
        @dragstart="handleDragStart($event, item)"
        @dragover.prevent="handleDragOver($event, item)"
        @dragenter="handleDragEnter($event, item)"
        @dragend="handleDragEnd($event, item)"
    >
        <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
            <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            <span @click.stop="deleteChecked(item)" class="demo-icon-XL">
                <img src="./assets/crossIcon.svg" class="demo-tagIcon-XL" >
            </span>
        </div>
    </div>
    <input type="text" v-model.trim="searchVal" :placeholder=! "" checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne" v-on="$listeners"> </div> // Implement drag function Private dragging: any = null; private handleDragStart(e: any, item: object): void { this.dragging = item; } private handleDragEnd(e: any, item: object): void { this.dragging = null; } private handleDragOver(e: any, item: object): void { e.dataTransfer.dropEffect ="move"; // e.dataTransfer.dropEffect="move"; // Set for the drop target in the dragenter! } private handleDragEnter(e: any, item: object): void { e.dataTransfer.effectAllowed ="move"; // Set the dragstart event for the element that needs to be movedif (item === this.dragging) {
    return; } const newArr = [...this.checkedArr]; const src = newArr.indexOf(this.dragging); const dst = newArr.indexOf(item); newArr.splice(dst, 0, ... newArr.splice(src, 1)); this.checkedArr = newArr; }Copy the code

After measuring is the beginning of a nightmare, safari cannot be achieved when dragging the input box automatic scrolling (input box limits the height, more than three lines appear scroll effect, in other browsers pull the bottom line of the content to the input box at the top of his will scroll up pages, safari, however, had no effect), had no choice but to come back. First of all, I looked up the problem on the Internet and found no solution. Then I considered two solutions: 1. 2, by listening to drag mouse position to use JS control scrolling. Then I used the vUE -Draggable plug-in. At this time, I encountered that this plug-in does not support TS. After introducing it into my TS environment, I reported an error. (I am a Windows computer and this problem only reappears on Safari on MAC, so I have to find a test reoccurrence, use the test MAC to access my local environment test, and spend two days running back and forth between my own location and the test location, how I want to have a MAC at this time) Only by listening to the mouse position (confidently write the monitoring and various situation judgment), this time should always be solved, but in the test it still did not move. There is no reason why it is impossible to monitor the mouse position. It is found by printing that the mousemove event will not be triggered during the dragging process, and this scheme does not achieve the scrolling effect. Through baidu + Google search again finally saw a drag event (didn’t do drag and drop function before, don’t know this event), should be able to use the name look like, finally through this event solves this problem, because rolling too fast to this function with a throttle, the problem it took almost one and a half days to completely get rid of. A code example is as follows:

<div class="wfc-chooseContent-XRZJ" :class="{'wfc-showInput-XRZJ': ! showPlaceholder}" v-if="isMultiple" @drag="divDragging">
    <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="wfc-checkedTag-XRZJ"
        :draggable="isDraggable && ! deleteStatus"
        @dragstart="handleDragStart($event, item)"
        @dragover.prevent="handleDragOver($event, item)"
        @dragenter="handleDragEnter($event, item)"
        @dragend="handleDragEnd($event, item)"
    >
        <div class="wfc-outContent-XRZJ" @click="chooseTag(item)" :class="{'wfc-deleteStatus-XRZJ': i === checkedArr.length - 1 && deleteStatus}">
            <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
            <span @click.stop="deleteChecked(item)" class="wfc-icon-XRZJ">
                <img src="./assets/crossIcon.svg" class="wfc-tagIcon-XRZJ" >
            </span>
        </div>
    </div>
    <input type="text" v-model.trim="searchVal" class="wfc-inInput-XRZJ" :placeholder="checkedArr.length ? '' : placeholder" @focus="handleFocus" @blur="loseFocus" @keydown.8="deleteOne" ref="inInput" v-on="$listeners">
</div>

dragInterVal: boolean = false
private divDragging(e: any) {
    if (this.dragInterVal) {
        return
    }
    if (e.x === 0 && e.y === 0) {
        return
    }
    if (e.layerY < 26 && this.domTemp.scrollTop > 0) {
        this.domTemp.scrollTop = this.domTemp.scrollTop - 24
        this.dragInterVal = true
        setTimeout(() => {
            this.dragInterVal = false
        }, 400);
    } else if (94 - e.layerY < 30 && this.domTemp.scrollHeight - this.domTemp.scrollTop - 94 > 0) {
        this.domTemp.scrollTop = this.domTemp.scrollTop + 24
        this.dragInterVal = true
        setTimeout(() => {
            this.dragInterVal = false}, 400); }}Copy the code

Search results change

This function will change the color of the drop-down box content according to the search results, that is, the query field changes color in the search results when fuzzy query, although this function does not belong to the contents of the drop-down box component, but want to take it out and provide some ideas.

Take the V-HTML directive alone. Many people know what it does, but rarely use it. It makes this function extremely simple.

 <template slot="option" slot-scope="{ item }">
    <div class="wfc-optionStyleXRZJrules">
        <img class="wfc-headPortraitXRZJrules" :src="item.photoBig">
        <div class="wfc-rightXRZJrules"> <! --> <div class="wfc-nameXRZJrules" v-html="item.newname"></div>
            <div class="wfc-emailXRZJrules">{{item.email}}</div>
            <div class="wfc-bottomXRZJrules">{{isEN ? item.org_name_mult_lang.en : item.org_name_mult_lang.zh}}</div>
        </div>
    </div>
</template>

this.options = searchUser.map((item: any) => {
    item.newname = item.name.replace(new RegExp(val, 'g'), `<span style="color: #3C8CFF;">${val}</span>`)
    return item
})
Copy the code

### For the rest of the content, I haven’t thought of any problems that need to be taken out for now, and I will supplement them later. Please leave a message if there are any bad points. Thank you for taking the time to read my book, please correct them.