preface

Select component is one of the most popular components in the PC. Because it is difficult to customize the native Select component style, various browser styles are “in full flourish”, so we have to customize the Select component by ourselves. Build your own wheels in the spirit of learning hooks, this article will describe the process and thinking of writing the entire component yourself step by step.

Thinking about composition and UI layering

Solution a:

Wrapping the display box component and the drop-down box component with a parent component is simple and crude, and can solve most scenarios, but there are several problems:

  1. There will be display occlusion problem in Scroll container
  2. When the parent component container is at a lower level, components at the higher level overlap with components in the drop-down box

As a coder, of course, this is not enough

Scheme 2:

Use createPortal provided by React to implement render body under the body node.

Here, of course, we choose the render Body scheme. The whole component idea is: click the display component and calculate the position of the drop-down box by locating the position of the display component. Drop-down box The drop-down box disappears automatically when selected or clicked elsewhere on the screen. If selected, the corresponding value is displayed. If the Select component is in a container with scroll bars, listen for the container’s scroll to change the position of the drop-down box.

If you want to start rolling up your sleeves, just a minute, we’ve done a component breakdown plan before we write the code, so we can anticipate some problems.

Here I split the components into:

  • Select component (displays selection results)
  • Menu component (displays selection list)
  • Position component (used to locate the display Position of the drop-down box)

Ready, start output

Menu component

  • Label Display value of the display item
  • Value Indicates the value of the display item
  • className

menu.css

.ll-selected{
    background: # 000;
    color: #fff;
}
Copy the code

Menu.jsx

const SelectMenu = (props) = > {
    const [ selected, setSelected ] = useState(false);
    const { label, value, className = ' ', handleSelect, defaultValue } = props;

    useEffect((a)= > {
        if (defaultValue === value) {
            setSelected(true);
        }
    }, [value, defaultValue])
    return (
        <div 
            onClick={()= > handleSelect({value, label})} 
            className={`${className} ${selected ? 'll-selected': ''}`}>{label}</div>)}Copy the code

Menu is the easiest implementation of the Select component, whether its own internal implementation is selected. Click to pass the selected data up

The Position component

  • TargetRef locates based on which component location
  • GetContainer gets the location node, default render Body
  • The onNotVisibleArea component is called when it is not in the visible area

position.css

.ll-position {
    position: absolute;
    z-index: 99;
    background: #fff;
}
Copy the code

Position.jsx

let instance = null;

const Position = (props) = > {
    const { targetRef, children, getContainer, onNotVisibleArea } = props;
    const container = getContainer && getContainer();
    
    if(! instance) { instance =document.createElement('div');
        instance.className = 'll-position';
        document.body.appendChild(instance);
    }

    useEffect((a)= > {

        function setInstanceStyle() {
            const { top, left, height } = targetRef.current.getBoundingClientRect();
            const style = {
                top: document.documentElement.scrollTop + top + height + 10 + 'px'.left: document.documentElement.scrollLeft + left + 'px'
            }
    
            instance.style.top = style.top;
            instance.style.left = style.left;

            return { top, left, height }
        }

        setInstanceStyle();

        function handleScroll() {
            const { top, height } = setInstanceStyle();
            
            if (container.offsetTop > top) {
                onNotVisibleArea();
            }
            if(top - container.offsetTop + height > container.offsetHeight) { onNotVisibleArea(); }}if (container) {
            container.addEventListener('scroll', handleScroll, false);
        }

        return (a)= > {
            if (container) {
                container.removeEventListener('scroll', handleScroll, false);
            }
        }

    }, [targetRef])

    return instance && ReactDOM.createPortal(children, instance);
}
Copy the code

The Position component gets the dom location through the targetRef passed in. The instance is not destroyed so that the root node does not need to be recreated the next time it is clicked. Here with the React. CreateProtal components to create a root node, reference: zh-hans.reactjs.org/docs/portal… . If the location component is in an Scroll container, receive a getContainer method to get the Scroll container and move the location component by listening for the container’s Scroll event. If the targetRef is no longer in the visible area, call onNotVisibleArea(). To notify the upper-layer components.

Select the component

  • DefaultValue Specifies the selected value by default
  • OnChange The method that is called when the value is changed
  • GetContainer gets the parent of the menu render, default render Body

Select.jsx

const Select = (props) = > {
    const { defaultValue, onChange, getContainer } = props;
    // Control the drop-down box to show/hide
    const [visible, setVisible] = useState(false);
    // The currently selected value
    const [data, setData] = useState({ value: defaultValue, label: ' ' });
    // Whether to set the default value
    const [defaultValueState, setDefaultValueState] = useState(true)
    const inputRef = useRef(null);

    // Find the label corresponding to defaultValue and display it
    useEffect((a)= > {
        if(! defaultValueState)return;
        const i = props.children.findIndex(n= > n.props.value === defaultValue);
        if (i > - 1) {
            setData(props.children[i].props);
            setDefaultValueState(true);
        }
    }, [defaultValue, props.children, defaultValueState])

    function handleSelect(data) {
        setData(data);
        onChange && onChange(data);
        setVisible(false);
    }

    function bindBodyClick(e) {
        if (e.target === inputRef.current) return;
        setVisible(false);
    }

    useEffect((a)= > {
        document.addEventListener('click', bindBodyClick, false);
        return (a)= > {
            document.removeEventListener('click', bindBodyClick, false);
        }
    }, [visible])

    return (
        <React.Fragment>
            <input
                defaultValue={data.label}
                onClick={() => setVisible(true)}
                ref={inputRef}
                readOnly />
            {
                visible ?
                    <Position 
                        onNotVisibleArea={() => setVisible(false)}
                        getContainer={getContainer} 
                        targetRef={inputRef}>
                        {
                            React.Children.map(props.children, child => (
                                React.cloneElement(child, {
                                    defaultValue: data.value,
                                    handleSelect
                                })
                            ))
                        }
                    </Position> : null
            }
        </React.Fragment>
    )
}
Copy the code

The above code shows a simple Select component that uses Visible to show or hide the dropdown.

  • Input receives a defaultValue to set the initial value, and the initial value we pass in is the corresponding value, not the label, so HERE I’m going to use the props. Children loop to find the corresponding label, and then show the input defaultValue. Input Click to display the dropdown box.

  • The Position component is the positioning component we mentioned above. If the Position component is not visible, use the noNotVisibleArea() method to make the dropdown not appear.

  • Use the React. CloneElement on props. Children to transport the new props and reference website: zh-hans.reactjs.org/docs/react-… .

  • Listen for the Document click event to make the click space drop box disappear. But because React events themselves encapsulate a layer of non-native events, there are bubbling and capturing issues with native and synthesized events. Refer to the website: zh-hans.reactjs.org/docs/events…

Final Use rendering

<div>
    <div style={{background: 'red', height: '200px'}} ></div>
    <div id="area" style={{ margin: 10.overflow: 'scroll', height: 200}} >
        <div style={{ padding: 100.height: 1000.background: '#eee' }}>
            <h4>Rolling area</h4>
            <h4>Rolling area</h4>
            <Select getContainer={()= > document.getElementById('area')}>
                <SelectMenu label="The first" value="1"></SelectMenu>
                <SelectMenu label="A second" value="2"></SelectMenu>
                <SelectMenu label="The third" value="3"></SelectMenu>
                <SelectMenu label="4" value="4"></SelectMenu>
            </Select>
        </div>
    </div>
    <Select>
        <SelectMenu label="The first" value="1"></SelectMenu>
        <SelectMenu label="A second" value="2"></SelectMenu>
        <SelectMenu label="The third" value="3"></SelectMenu>
        <SelectMenu label="4" value="4"></SelectMenu>
    </Select>
</div>
Copy the code

At the end

Ignores all the unnecessary styling of select and many of the detailed features and optimizations, such as multiple selection, disable selection, search filtering, throttling, and so on. To implement their own encountered a lot of problems, and then to read someone else’s source code harvest a lot. The level is limited, if the error please point out, thank you.