preface

In this two-week series of components, we will look at the Mini React component building process and what we learned from it. The Mini React component will look at the excellent open source component libraries in the community to understand how the React component is implemented. Then I took the best of it and applied it to my Mini React component and put it into practice (making wheels). I mainly want to urge myself to understand and learn the implementation of those excellent component libraries through this way of recording and sharing, and improve myself by making wheels (personally I think making wheels is a very tiring but rewarding process).

The list component

Thinking and preparation

Let us into today’s theme: the List component implementation process, to achieve the original intention of this component is in the process of project development has such a demand, there are a bunch of arrange elements in a certain direction, but when there is insufficient space on the container, to support content or left and right arrows to scroll beyond switch display content of the beyond. After a while, the existing component library doesn’t seem to have a particularly suitable component to implement this requirement, and it doesn’t seem to me that it’s too complicated to implement this functionality, so why not implement it yourself if you can write one yourself? Then.. Shoot yourself!

First, upload the picture! Let’s take a look at our final implementation:



This can be done with just a few lines of code:

<NodeList direction="vertical">{renderButton()}
</NodeList>
<NodeList direction="horizonal">
{renderCard()}
</NodeList>
<NodeList direction="horizonal">
{renderCardWithResize()}
</NodeList>
  const renderButton = ()=>{
        const buttonTextArr = Array.from({length: 30},(v,k)=>`button${k+1}`)
        return buttonTextArr.map(item=><button className='demo-node-list-section-btn' key={item}>{item}</button>)
    }
    const renderCard = ()=>{
        const CardTextArr = Array.from({length: 30},(v,k)=>`Card${k+1}`)
        return CardTextArr.map(item=><div className='demo-node-list-section-card' key={item}>{item}</div>)

    }
    const renderCardWithResize =()=>{
        const CardTextArr = Array.from({length: 7},(v,k)=>`Card${k+1}`)
        return CardTextArr.map(item=><div className='demo-node-list-section-card' key={item}>{item}</div>)
    }

Not bad!

Next we’ll look at how to implement such a mini components: the first is clear finally realize the function of the component, which is the purpose, here, I hope this mini list component, can serve as a container, the package can arbitrarily long content to support scrolling and left and right arrows to switch, in summary is the point of three functions:

  1. Support scrolling;
  2. Element full screen switch: Click the switch button to achieve the effect of full screen switch
  3. Monitor container size changes, shielding scrolling and switching when space is sufficient for full display;

Also, as a container, the orientation of its content should be controllable (horizontal and vertical). Having defined the goal, the next step is to find out the technical difficulties and think about the feasible technical solutions. In this List component, how to implement the content switching and scrolling effect is one of the difficulties, and there are three feasible technical solutions to achieve such effect:

  1. By controlling the css The style ofLeft (top)Attribute to achieve switching and simulation scrolling;
  2. through css3 the transform Attribute to achieve switching and simulation scrolling;
  3. Take advantage of native scroll Events and scrollto Method implementation;

Finally, among the three schemes, I prefer Scheme 2. Compared with Scheme 1, Transform adopted in Scheme 2 has better performance advantages compared with Left, while Scheme 3 relies more on native methods and is less customizable.

Once you’ve determined the solution, how do you implement it?

The implementation process

Define a container element that stores its ref and unscrolls it:

.node-list {
  position: relative;
  overflow: hidden;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
}
 <div ref={nodesWrapperRef}></div>

Define the container of a child element (the container of the actual content that controls the target element of the transform) and store its ref:

<div ref={nodeListRef} className={`${prefixCs}-content ${horizonal? ``:`${prefixCs}-content-vertical`}`} style={{ transform: `translate(${transformLeft}px, ${transformTop}px)`, }} > //children node </div>

We handle the children property of the container component (the child element, the real thing that needs to be rendered). Here we wrap up the child element method a little bit:

function parseTabList(children) { return children .map((node,index) => { if (React.isValidElement(node)) { const key = node.key ! == undefined ? String(node.key) : index; return { key, ... node.props, node, }; } return null; }) .filter((node) => node); }

With this method, we convert the child elements into objects that contain the child information and store them in the array element nodes (for robustness, we also use isValidElement to check the React element here).

Then render the child element, using the child object we just constructed:

    const nodeRender = nodes.map((node) => (
      <div
        key={`_${node.key}`}
        ref={refs(node.key)}
      >
        {node.node}
      </div>
    ));

In this way, we can retrieve the actual child elements (DOM) from ref. We can create the corresponding ref container with useRef, but if the number of child elements is uncertain, we may need to adopt some strategy to generate such a ref container and save:

function useRefs() { const cacheRefs = useRef(new Map()); function getRef(key) { if (! cacheRefs.current.has(key)) { cacheRefs.current.set(key, React.createRef()); } return cacheRefs.current.get(key); } function removeRef(key) { cacheRefs.current.delete(key); } return [getRef, removeRef]; } const [getNodeRef,removeNodeRef] = useRefs();

This is a good option to use custom hooks to generate a separate reusable piece of code that is somewhat closer to the original userRef. The multiple Refs are stored as Map elements with keys as child elements, keys as props.

Finally, we should render the two toggle button elements:

   <div ref={ref} className={optionLeftClass} onClick={onLeftClick} disabled >
        <span className={`${prefixCs}-operation-arrow`}></span>
      </div>
      <div ref={ref} className={optionRightClass} onClick={onRightClick} disabled >
        <span className={`${prefixCs}-operation-arrow`}></span>
      </div>

Based on our scenario, we need to get information about the location and size of a number of elements.

Gets the position size of the child element:

const [nodesSizes, setNodesSizes] = useState(new Map()); setNodesSizes(() => { const newSizes = new Map(); nodes.forEach(({ key }) => { const Node = getRefBykey(key).current; if (Node) { newSizes.set(key, { width: Node.offsetWidth, height: Node.offsetHeight, left: Node.offsetLeft, top: Node.offsetTop, }); }}); return newSizes; });

The state of nodesSIZES contains the actual width and height (including margin) and position (offset of left and top in the container) of each child element

Just to be clear here,
offsetWidth Property (The HTMLElement.offsetWidth read-only property returns The layout width of an element as an integer.) and


offsetHeight The htmlElement. Offsetheight read-Only property returns The height of an element, Including vertical padding and borders, as an integer.)

From the introduction, these two attributes represent a range only up to the border-box. Here, because we wrap a div element around the actual child element, we actually get the offsetWidth and offsetHeight of the div element. So you can get the full width and height of the actual child elements (including margin).

Gets the width and height of the visible area:

const offsetWidth = nodesWrapperRef.current? .offsetWidth || 0; const offsetHeight = nodesWrapperRef.current? .offsetHeight || 0;

But the viewable area may also have toggle buttons in addition to child elements, so the actual viewable area width and height should be subtracted from the toggle button width and height (if any) :

    setWrapperWidth(offsetWidth - (isOperationHidden ? 0 : newOperationWidth * 2));
    setWrapperHeight(offsetHeight - (isOperationHidden ? 0 : newOperationHeight * 2));

Get the width and height of the entire content (that is, the scrolling area) :

const newWrapperScrollWidth = nodeListRef.current? .scrollWidth || 0; const newWrapperScrollHeight = nodeListRef.current? .scrollHeight || 0;

Once we have all the information we need to get, the logic of scrolling is no longer difficult to implement.

In our Transform scheme, the effect of scrolling is actually controlling the change of the Transform property (control Transformx or TransforMy depending on the orientation of the arrangement).

Listens on the element’s scroll events and executes the logic to change the transform:

 useTouchMove(nodesWrapperRef, (offsetX, offsetY) => {
    function doMove(setState, offset) {
      setState((value) => {
        const newValue = alignInRange(value + offset, transformMin, transformMax);

        return newValue;
      });
    }

    if (horizonal) {
      // Skip scroll if place is enough
      if (wrapperWidth >= wrapperScrollWidth) {
        return false;
      }

      doMove(setTransformLeft, offsetX);
    } else {
      if (wrapperHeight >= wrapperScrollHeight) {
        return false;
      }

      doMove(setTransformTop, offsetY);
    }

    // clearTouchMoving();

    return true;
  });

Here, we use custom hook mode to wrap the logic of wheel event monitoring in custom hook UseTouchMove, and the internal code is not posted here. Its main function is to monitor the wheel event of the outer container, and calculate the rolling distance and direction into the corresponding offset. As a parameter to the doMove method.

The function doMove method converts the rolling distance of the wheel to the corresponding transform property value, but we also need to consider some boundary cases here, namely:

  • When the width and height of the viewable area is larger than the width and height of the scroll area, that is, the contents of the container can be fully displayed, the scroll event should not be responded to;
  • transform The value of an attribute should be bounded, with a maximum and minimum value (as the element scrolls to the top and bottom), so here we are setting transform Attribute values are passed first alignInRange Function processing, which is mainly the judgment of the maximum and minimum values;
if (! horizonal) { transformMin = Math.min(0, wrapperHeight - wrapperScrollHeight); transformMax = 0; } else { transformMin = Math.min(0, wrapperWidth - wrapperScrollWidth); transformMax = 0; }

The transform property has a maximum value of 0 and a minimum value of the viewable area width (height) minus the rolling area width (height). The minimum value here is negative because the element rolls to the left or up, corresponding to both transformX and transforMy having negative values.

This achieves the first goal of supporting scrolling.

And the realization of the operation button to switch the function of display content, need a little more complex.

First of all, we need to obtain the elements currently appearing in the visible region (elements appearing completely, not including elements appearing only in part). It is not difficult to achieve this under the premise that we know the current transform attribute value, the position width and height information of each child element and the width and height of the visible region:

    const len = nodes.length;
    let endIndex = len - 1;
    for (let i = 0; i < len; i += 1) {
      const offset = tabOffsets.get(nodes[i].key) || DEFAULT_SIZE;
      const deltaOffset = offset[unit]
      if (offset[position] + deltaOffset > transformSize + basicSize) {
        endIndex = i - 1;
        break;
      }
    }

    let startIndex = 0;
    for (let i = len - 1; i >= 0; i -= 1) {
      const offset = tabOffsets.get(nodes[i].key) || DEFAULT_SIZE;
      if (offset[position] < transformSize) {
        startIndex = i + 1;
        break;
      }
    }

    return [startIndex, endIndex];

We can use offsetLeft + offsetWidth > TransformSize + BasicSize to determine that the child element is not in the visible region. Where TransformSize represents the absolute value of the current Transform and BasicSize represents the width (height) of the visible region. The offsetLeft < TransformSize child element is not visible.

Thus, the starting subscript and ending subscript of the child elements in the nodes in the visual region can be calculated. The range of new child elements that should be displayed in each switch can be calculated according to the width and height of the visual region and the subscript range of the current visual element when clicking the switch button each time. Here, the switch can be achieved by combining the OnNodescroll method:

function onNodeScroll(key = activeKey, toTopOrLeft) { const nodeOffset = nodesOffset.get(key) || { width: 0, height: 0, left: 0, right: 0, top: 0, }; if (horizonal) { // ============ Align with top & bottom ============ let newTransform = transformLeft; If (nodeOffset. Left < -TransformLeft) {newTransform = -nodeOffset. Left; } // The target child element is hidden on the right, Else if (nodeOffset. Left + nodeOffset. Width >-TransformLeft + wrapperWidth) {newTransform = -(nodeOffset.left + nodeOffset.width - wrapperWidth); } setTransformTop(0); setTransformLeft(alignInRange(newTransform, transformMin, transformMax)); }

This completes our second feature point.

As for the third function point is actually the simplest, just need to listen to the resize event, in the event callback to retrieve the viewable area size, scroll area size, relatively easy to implement, I will not repeat here.

That’s all for this post, and we’ll have more to share in future posts.