5
头图

View the effect

Let's first look at the effect of the example to be implemented today, as follows:

Well, next we also saw the effect of this example, let's get to the point and start coding happily.

Technology stack introduction

This small program, we will use React + typescript + css in js syntax, and use the latest and more popular tool vite to build.

Initialize the project

We can choose to hold shift on the computer, then right-click and select powershell, which is the default system terminal. Then enter the command:

 mkdir react-elevator

Create a directory, after creating it, then we open the directory in vscode, after opening, open the terminal in vscode, enter the following command:

 npm init vite@latest react-elevator -- --template react-ts

Note that in the command interface, we have to choose react, react-ts. After initializing the project, we are entering the command:

 cd react-elevator
npm install
npm run dev

Check to see if we initialized the project successfully.

Special Disclaimer: Please note that node.js and npm tools are installed

css in js

As you can see, the initialization of our project has been completed. Well, next, we need to install some additional dependencies encountered in the project, such as css in js, we need to install @emotion/styled, @emotion/react dependencies. Go ahead and enter the command:

 npm install @emotion/styled @emotion/react --save-dev

After installation, we use this syntax in the project.

First introduce styled, as follows:

 import styled from "@emotion/styled"

Then create a style component. CSS in js actually treats each component as a style component. We can pass styled followed by the html tag name, and then followed by the template string. The structure is as follows:

 const <组件名> = styled.<html标签名>`
    //这里写样式代码
`

E.g:

 const Link = styled.a`
    color:#fff;
`

The above code is to write a hyperlink component with a white font color, and then we can write the link component directly in jsx. As follows:

 <div>
    <Link>这是一个超链接组件</Link>
</div>

Of course, emotion also supports object writing, but we basically only use template string syntax here.

Moving on to the main topic, let's delete some of the initialization code first, because we don't need to use it.

The structure of the analysis program

After deleting it, let's take a look at the structure of the elevator applet we want to implement:

  1. elevator shaft (that is, where the elevator goes up or down)
  2. elevator
  3. Elevator door (divided into left and right doors)
  4. floor
    4.1 Number of floors
    4.2 Floor buttons (including up and down buttons)

After the structure is complete, let's take a look at the functions:

  1. Click on the floor to drive the elevator up or down
  2. When the elevator arrives at the corresponding floor, the left and right doors of the elevator open
  3. After the door opened, the beautiful woman inside came out
  4. The button will have a click-selected effect

Let's analyze the structure first. According to the above split, we can roughly divide the whole applet into the following components:

  1. Building (container assembly)
  2. elevator shaft components
    2.1 Elevator Components
    2.1.1 The door on the left side of the elevator
    2.1.1 The door on the right side of the elevator
  3. Floor components
    3.1 Floor Control Components
    3.1.1 Floor Up Button Assembly
    3.1.2 Floor Descending Button Assembly
    3.2 Floor Count Components

Let's write the components and styles first, and then complete the functionality.

Building components

The first is our building component, we create a components directory, and then create a new ElevatorBuild.tsx component, and write the following code in it:

 import styled from "@emotion/styled"

const StyleBuild = styled.div`
    width: 350px;
    max-width: 100%;
    min-height: 500px;
    border: 6px solid var(--elevatorBorderColor--);
    overflow: hidden;
    display: flex;
    margin: 3vh auto;
`

const ElevatorBuild = () => {
    return (
        <StyleBuild></StyleBuild>
    )
}

export default ElevatorBuild

In this way, one of our building components is complete, and then we import it in App.tsx and use it:

 //这里是新增的代码
import ElevatorBuild from "./components/ElevatorBuild"

const App = () => (
  <div className="App">
    {/*这里是新增的代码 */}
    <ElevatorBuild />
  </div>
)

export default App

global style

Here, we define the global css variable style, so create global.css in the current directory, import it in main.tsx, and then write the following code in the style file:

 :root {
    --elevatorBorderColor--: rgba(0,0,0.85);
    --elevatorBtnBgColor--: #fff;
    --elevatorBtnBgDisabledColor--: #898989;
    --elevatorBtnDisabledColor--: #c2c3c4;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

elevator shaft components

Next, let's continue to complete the elevator shaft component, and also create a new ElevatorShaft.tsx component in the components directory, and write the following code in it:

 import styled from "@emotion/styled"

const StyleShaft = styled.div`
    width: 200px;
    position: relative;
    border-right: 2px solid var(--elevatorBorderColor--);
    padding: 1px;
`

const ElevatorShaft = () => {
    return (
        <StyleShaft></StyleShaft>
    )
}

export default ElevatorShaft

Then we import and use it in the building component as follows:

 import styled from "@emotion/styled"
//这里是新增的代码
import ElevatorShaft from "./ElevatorShaft"

const StyleBuild = styled.div`
    width: 350px;
    max-width: 100%;
    min-height: 500px;
    border: 6px solid var(--elevatorBorderColor--);
    overflow: hidden;
    display: flex;
    margin: 3vh auto;
`

const ElevatorBuild = () => {
    return (
        <StyleBuild>
            {/*这里是新增的代码 */}
            <ElevatorShaft></ElevatorShaft>
        </StyleBuild>
    )
}

export default ElevatorBuild

elevator door assembly

Next, let's complete the elevator door component. We can see that the elevator door component has some common style parts, so we can extract it, create a new Door.tsx, and write the following code:

 import styled from '@emotion/styled';

const StyleDoor = styled.div`
    width:50%;
    position: absolute;
    top: 0;
    height: 100%;
    background-color: var(--elevatorBorderColor--);
    border: 1px solid var(--elevatorBtnBgColor--);
`;

const StyleLeftDoor = styled(StyleDoor)`
    left: 0;
`;

const StyleRightDoor = styled(StyleDoor)`
    right: 0;
`;


export { StyleLeftDoor,StyleRightDoor }

Since our function will need to set the styles of these two components, and our style is set on the style attribute, we can pass it through props. Now we first write the typescript interface class, create a type directory, and create a new style. d.ts global interface file, and write the following code:

 export interface StyleProps {
    style: CSSProperties
}

Elevator Components

Next, we can start writing the elevator component as follows:

 import styled from "@emotion/styled"

const StyleElevator = styled.div`
    height: 98px;
    background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
    border: 1px solid var(--elevatorBorderColor--);
    width: calc(100% - 2px);
    padding: 1px;
    transition-timing-function: ease-in-out;
    position: absolute;
    left: 1px;
    bottom: 1px;
`

const Elevator = (props: Partial<ElevatorProps>) => {
    return (
        <StyleElevator>
     
        </StyleElevator>
    )
}

export default Elevator

Next, let's look at the two elevator door assemblies, first the door on the left, which looks like this:

 import { StyleProps } from "../type/style"
import { StyleLeftDoor } from "./Door"

const ElevatorLeftDoor = (props: Partial<StyleProps>) => {
    const { style } = props
    return (
        <StyleLeftDoor style={style}></StyleLeftDoor>
    )
}

export default ElevatorLeftDoor

Partial is a generic type that passes in an interface, which means turning each attribute of the interface into an optional attribute. According to this principle, we can know that the component code of the right door is also similar. as follows:

 import { StyleProps } from '../type/style';
import { StyleRightDoor } from './Door'
const ElevatorRightDoor = (props: Partial<StyleProps>) => {
    const { style } = props;
    return (
        <StyleRightDoor style={style}/>
    )
}

export default ElevatorRightDoor;

After these two components are written, we will introduce and use them in the elevator component. Since the functional logic will need to set the style, we pass the style again through props. As follows:

 import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"

const StyleElevator = styled.div`
    height: 98px;
    background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
    border: 1px solid var(--elevatorBorderColor--);
    width: calc(100% - 2px);
    padding: 1px;
    transition-timing-function: ease-in-out;
    position: absolute;
    left: 1px;
    bottom: 1px;
`

export interface ElevatorProps {
    leftDoorStyle: StyleProps['style'];
    rightDoorStyle: StyleProps['style'];
}

const Elevator = (props: Partial<ElevatorProps>) => {
    const { leftDoorStyle,rightDoorStyle } =  props;
    return (
        <StyleElevator>
            <ElevatorLeftDoor style={leftDoorStyle} />
            <ElevatorRightDoor style={rightDoorStyle} />
        </StyleElevator>
    )
}

export default Elevator

After completing the elevator component, next we introduce the elevator component into the elevator shaft component. Note that we will set the style of the elevator component and the elevator door component in the subsequent logic here. Therefore, in the elevator shaft component, we need to pass the style through props.

 import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import Elevator from "./Elevator"


const StyleShaft = styled.div`
    width: 200px;
    position: relative;
    border-right: 2px solid var(--elevatorBorderColor--);
    padding: 1px;
`

export interface ElevatorProps {
    leftDoorStyle: StyleProps['style'];
    rightDoorStyle: StyleProps['style'];
    elevatorStyle: StyleProps['style'];
}

const ElevatorShaft = (props: Partial<ElevatorProps>) => {
    const { leftDoorStyle,rightDoorStyle,elevatorStyle } = props;
    return (
        <StyleShaft>
            <Elevator style={elevatorStyle} leftDoorStyle={leftDoorStyle} rightDoorStyle={rightDoorStyle}></Elevator>
        </StyleShaft>
    )
}

export default ElevatorShaft

The opening animation of the elevator door assembly

We can see that when a certain time is reached, the elevator door will have an opening animation, which we obviously did not add here, so we can add a props for each elevator door whether to open or not for transmission, and continue to modify Door.tsx as follows:

 import styled from '@emotion/styled';

const StyleDoor = styled.div`
    width:50%;
    position: absolute;
    top: 0;
    height: 100%;
    background-color: var(--elevatorBorderColor--);
    border: 1px solid var(--elevatorBtnBgColor--);
`;

const StyleLeftDoor = styled(StyleDoor)<{ toggle?:boolean }>`
    left: 0;
    ${({toggle}) => toggle ? 'animation: doorLeft 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' }
    @keyframes doorLeft {
        0% {
            left: 0px;
        }
        25% {
            left: -90px;
        }
        50% {
            left: -90px;
        }
        100% {
            left:0;
        }
    }
`;

const StyleRightDoor = styled(StyleDoor)<{ toggle?:boolean }>`
    right: 0;
    ${({toggle}) => toggle ? 'animation: doorRight 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' };
    @keyframes doorRight {
        0% {
            right: 0px;
        }
        25% {
            right: -90px;
        }
        50% {
            right: -90px;
        }
        100% {
            right:0;
        }
    }
`;


export { StyleLeftDoor,StyleRightDoor }

The emotion syntax can return a css property through a function, so as to achieve the purpose of dynamically setting the property. A pair of angle brackets, which is actually a generic type in typescript, represents whether the toggle data is passed in. Next, modify ElevatorLeftDoor.tsx and ElevatorRightDoor.tsx . as follows:

 import { StyleProps } from "../type/style";
import { StyleLeftDoor } from "./Door"

export interface ElevatorLeftDoorProps extends StyleProps {
    toggle: boolean
}

const ElevatorLeftDoor = (props: Partial<ElevatorLeftDoorProps>) => {
    const { style,toggle } = props;
    return (
        <StyleLeftDoor style={style} toggle={toggle}></StyleLeftDoor>
    )
}

export default ElevatorLeftDoor
 import { StyleProps } from '../type/style'
import { StyleRightDoor } from './Door'

export interface ElevatorRightDoorProps extends StyleProps {
    toggle: boolean
}

const ElevatorRightDoor = (props: Partial<ElevatorRightDoorProps>) => {
    const { style,toggle } = props;
    return (
        <StyleRightDoor style={style} toggle={toggle} />
    )
}

export default ElevatorRightDoor

Modify elevator and elevator shaft components

Similarly we also need to modify the elevator assembly and elevator shaft assembly as follows:

 import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"

const StyleElevator = styled.div`
    height: 98px;
    background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
    border: 1px solid var(--elevatorBorderColor--);
    width: calc(100% - 2px);
    padding: 1px;
    transition-timing-function: ease-in-out;
    position: absolute;
    left: 1px;
    bottom: 1px;
`

export interface ElevatorProps extends StyleProps {
    leftDoorStyle: StyleProps['style']
    rightDoorStyle: StyleProps['style']
    leftToggle: boolean
    rightToggle: boolean
}

const Elevator = (props: Partial<ElevatorProps>) => {
    const { leftDoorStyle,rightDoorStyle,leftToggle,rightToggle } =  props;
    return (
        <StyleElevator>
            <ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
            <ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
        </StyleElevator>
    )
}

export default Elevator
 import styled from "@emotion/styled";
import { StyleProps } from "../type/style";
import Elevator from "./Elevator";

const StyleShaft = styled.div`
  width: 200px;
  position: relative;
  border-right: 2px solid var(--elevatorBorderColor--);
  padding: 1px;
`;

export interface ElevatorProps {
  leftDoorStyle: StyleProps["style"];
  rightDoorStyle: StyleProps["style"];
  elevatorStyle: StyleProps["style"];
  leftToggle: boolean;
  rightToggle: boolean;
}

const ElevatorShaft = (props: Partial<ElevatorProps>) => {
  const {
    leftDoorStyle,
    rightDoorStyle,
    elevatorStyle,
    leftToggle,
    rightToggle,
  } = props;
  return (
    <StyleShaft>
      <Elevator
        style={elevatorStyle}
        leftDoorStyle={leftDoorStyle}
        rightDoorStyle={rightDoorStyle}
        leftToggle={leftToggle}
        rightToggle={rightToggle}
      ></Elevator>
    </StyleShaft>
  );
};

export default ElevatorShaft;

But don't forget that the elevator component we have here needs to go up and down, so it also needs to set the style. Modify the code of the elevator component again as follows:

 import styled from "@emotion/styled"
import { StyleProps } from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"

const StyleElevator = styled.div`
    height: 98px;
    background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
    border: 1px solid var(--elevatorBorderColor--);
    width: calc(100% - 2px);
    padding: 1px;
    transition-timing-function: ease-in-out;
    position: absolute;
    left: 1px;
    bottom: 1px;
`

export interface ElevatorProps extends StyleProps {
    leftDoorStyle: StyleProps['style']
    rightDoorStyle: StyleProps['style']
    leftToggle: boolean
    rightToggle: boolean
}

const Elevator = (props: Partial<ElevatorProps>) => {
    const { style,leftDoorStyle,rightDoorStyle,leftToggle,rightToggle } =  props;
    return (
        <StyleElevator style={style}>
            <ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
            <ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
        </StyleElevator>
    )
}

export default Elevator

Floor Container Assembly

So far, our left half part has been completed, next, let's complete the floor number and control button components of the right half part, our floor is dynamically generated, so we need a container component to wrap it, write this first Floor container components, as follows:

 import styled from "@emotion/styled"

const StyleStoreyZone = styled.div`
    width: auto;
    height: 100%;
`

const Storey = () => {
    return (
        <StyleStoreyZone>
            
        </StyleStoreyZone>
    )
}

export default Storey

Floor components

It can be seen that the floor container component is relatively simple. Next, let's look at the floor component. As follows:

 import styled from "@emotion/styled";
import { createRef, useEffect, useState } from "react";
import useComponentDidMount from "../hooks/useComponentDidMount";

const StyleStorey = styled.div`
  display: flex;
  align-items: center;
  height: 98px;
  border-bottom: 1px solid var(--elevatorBorderColor--);
`;

const StyleStoreyController = styled.div`
  width: 70px;
  height: 98px;
  padding: 8px 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
`;

const StyleStoreyCount = styled.div`
  width: 80px;
  height: 98px;
  text-align: center;
  font: 56px / 98px 微软雅黑, 楷体;
`;

const StyleButton = styled.button`
  width: 36px;
  height: 36px;
  border: 1px solid var(--elevatorBorderColor--);
  border-radius: 50%;
  outline: none;
  cursor: pointer;
  background-color: var(--elevatorBtnBgColor--);
  &:last-of-type {
    margin-top: 8px;
  }
  &.checked {
    background-color: var(--elevatorBorderColor--);
    color: var(--elevatorBtnBgColor--);
  }
  &[disabled] {
    cursor: not-allowed;
    background-color: var(--elevatorBtnBgDisabledColor--);
    color: var(--elevatorBtnDisabledColor--);
  }
`;

export interface MethodProps {
  onUp(v: number, t: number, h?: number): void;
  onDown(v: number, t: number, h?: number): void;
}

export interface StoreyProps extends MethodProps{
  count: number
}

export interface StoreyItem {
   key: string
   disabled: boolean
}

const Storey = (props: Partial<StoreyProps>) => {
  const { count = 6 } = props;
  const storeyRef = createRef<HTMLDivElement>();
  const [storeyList, setStoreyList] = useState<StoreyItem []>();
  const [checked, setChecked] = useState<string>();
  const [type, setType] = useState<keyof MethodProps>();
  const [offset,setOffset] = useState(0)
  const [currentFloor, setCurrentFloor] = useState(1);
  useComponentDidMount(() => {
    let res: StoreyItem [] = [];
    for (let i = count - 1; i >= 0; i--) {
      res.push({
        key: String(i + 1),
        disabled: false
      });
    }
    setStoreyList(res);
  });

  useEffect(() => {
    if(storeyRef){
      setOffset(storeyRef.current?.offsetHeight as number)
    }
  },[storeyRef])

  const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
    setChecked(key)
    setType(method)
    const moveFloor = count - index
    const diffFloor = Math.abs(moveFloor - currentFloor)
    setCurrentFloor(moveFloor)    
    props[method]?.(diffFloor, offset * (moveFloor - 1))
    // 也许这不是一个好的方法
    if(+key !== storeyList?.length && +key !== 1){
        setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
    }
    setTimeout(() => {
      setChecked(void 0);
      if(+key !== storeyList?.length && +key !== 1){
        setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
      }
    }, diffFloor * 1000);
  };
  return (
    <>
      {storeyList?.map((item,index) => (
        <StyleStorey key={item.key} ref={storeyRef}>
          <StyleStoreyController>
            <StyleButton
              disabled={Number(item.key) === storeyList.length || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onUp')}
              className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
            >
              ↑
            </StyleButton>
            <StyleButton
              disabled={Number(item.key) === 1 || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onDown')}
              className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
            >
              ↓
            </StyleButton>
          </StyleStoreyController>
          <StyleStoreyCount>{item.key}</StyleStoreyCount>
        </StyleStorey>
      ))}
    </>
  );
};

export default Storey;

It can be seen that the logic of the floor components is very much, but in fact, it is not difficult to analyze one by one.

Next, we introduce the container component and import this component in the building component, and then we can get the structure of our entire elevator applet.

Here we will analyze the logic of the floor component step by step,

floor number

First of all, the floors are dynamically generated and passed through the parent component, so we define a count in the props, the default value is 6, which represents the number of floors generated by default. This is what our line of code means:

 export interface StoreyProps extends MethodProps{
  count: number
}
const { count = 6 } = props;

The rise and fall of floors

Secondly, when we ascend and descend the elevator, we need to obtain the height of each floor, which is actually the height of the floor container element. How to obtain the actual height of the DOM element? Let's think about it first, if it is a real DOM element, we only need to get the offsetHeight, namely:

 //这里的el显然是一个dom元素
const offset: number = el.offsetHeight;

In react, how should we get the real DOM element? Using the ref attribute, first import the createRef method, create a storeyRef, and then bind the storeyRef to the component container element, namely:

 const storeyRef = createRef<HTMLDivElement>();
//...
<StyleStorey ref={storeyRef}></StyleStorey>

Then we can use the useEffect method, which is a lifecycle hook function in react hooks, to monitor this storeyRef. If it is monitored, we can directly get the dom element and use a state to store the height value. which is:

 const [offset,setOffset] = useState(0)
//...
useEffect(() => {
    //storeyRef.current显然就是我们实际拿到的DOM元素
    if(storeyRef){
      setOffset(storeyRef.current?.offsetHeight as number)
    }
},[storeyRef])

Floor List Rendering

Next, let's look at the dynamic generation of the number of floors. We know that the dynamic generation of list elements in react is actually the use of the map method of the array, so we need to generate an array according to the count. Here, we can generate a key array , but since we want to control the disabling of the button, we add an additional disabled attribute, so this is also the meaning of the following code:

 export interface StoreyItem {
   key: string
   disabled: boolean
}
const [storeyList, setStoreyList] = useState<StoreyItem []>();
useComponentDidMount(() => {
    let res: StoreyItem [] = [];
    for (let i = count - 1; i >= 0; i--) {
      res.push({
        key: String(i + 1),
        disabled: false
      });
    }
    setStoreyList(res);
});

This involves a simulated useComponentDidMount hook function. It is very simple. Create a new useComponentDidMount.ts in the hooks directory, and then write the following code:

 import { useEffect } from 'react';
const useComponentDidMount = (onMountHandler: (...args:any) => any) => {
  useEffect(() => {
    onMountHandler();
  }, []);
};
export default useComponentDidMount;

Then it is to render the floor component, as follows:

 (
    <>
      {storeyList?.map((item,index) => (
        <StyleStorey key={item.key} ref={storeyRef}>
          <StyleStoreyController>
            <StyleButton
              disabled={Number(item.key) === storeyList.length || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onUp')}
              className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
            >
              ↑
            </StyleButton>
            <StyleButton
              disabled={Number(item.key) === 1 || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onDown')}
              className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
            >
              ↓
            </StyleButton>
          </StyleStoreyController>
          <StyleStoreyCount>{item.key}</StyleStoreyCount>
        </StyleStorey>
      ))}
    </>
);

<></> is a way of writing React.Fragment. It can be understood that it is a placeholder tag, which has no actual meaning. The default value of storeyList is undefined, so you need to add ? to represent an optional chain, which contains two part, the first part is the control button, and the second part is the display floor number.

In fact, the key in the element array we generated is the number of floors, which is the meaning of this line of code:

 <StyleStoreyCount>{item.key}</StyleStoreyCount>

In addition, when react generates a list, it needs to bind a key attribute to facilitate the calculation of virtual DOM and diff algorithm. There is no need to talk about it here. Next, let's look at the logic of the button component.

Floor Button Components

The logic of the button component consists of three parts:

  1. disable effect
  2. Click to make the elevator go up and down
  3. check effect

We know that the rise of the highest building cannot be raised, so it needs to be disabled. Similarly, the descent of the ground floor also needs to be disabled, so these two lines of code mean this:

 Number(item.key) === storeyList.length
Number(item.key) === 1

Next, there is another condition. This item.disabled is actually mainly to prevent the problem of repeated clicks. Of course, this is not a good solution, but for now, let's do this first, and define a type state, which represents whether the click is rising or not. decline:

 //接口类型,type应只能是onUp或者onDown,代表只能是上升还是下降
export interface MethodProps {
  onUp(v: number, t: number, h?: number): void;
  onDown(v: number, t: number, h?: number): void;
}
const [type, setType] = useState<keyof MethodProps>();

Then define a checked state, representing whether the current button is selected:

 //checked存储key值,所以
const [checked, setChecked] = useState<string>();
className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}

It should be noted that the judgment here is:

 item.key === checked && type === 'onUp' //以及 ${item.key === checked && type === 'onDown'

We have added checked in our style, there is nothing to say about this.

Then, we also need to cache which floor the current floor is, because the next time we click, we need to calculate based on the current floor instead of starting from scratch.

 const [currentFloor, setCurrentFloor] = useState(1);

Finally, our click up and down logic is still a bit complicated:

 const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
    setChecked(key)
    setType(method)
    const moveFloor = count - index
    const diffFloor = Math.abs(moveFloor - currentFloor)
    setCurrentFloor(moveFloor)    
    props[method]?.(diffFloor, offset * (moveFloor - 1))
    // 也许这不是一个好的方法
    if(+key !== storeyList?.length && +key !== 1){
        setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
    }
    setTimeout(() => {
      setChecked(void 0);
      if(+key !== storeyList?.length && +key !== 1){
        setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
      }
    }, diffFloor * 1000);
};

This function has three parameters. The first one represents the key value of the current floor, which is the number of floors. The second one represents the index of the current floor. Note that the index and the number of floors are different. The third one is whether the click is up or down. . Our first and third parameters are used to set the selection effect of the button, namely:

 setChecked(key)
setType(method)

Next, we need to calculate the execution time of the animation. For example, if we go from the first layer to the fifth layer, if we calculate it by one second to one layer, then the first layer to the fifth layer will take 4s. Similarly, our The offset should be the height of each floor and the height of the building that needs to be moved minus 1. Therefore, to calculate the height of the building that needs to be moved we are:

 const moveFloor = count - index
const diffFloor = Math.abs(moveFloor - currentFloor)
//设置当前楼层
setCurrentFloor(moveFloor) 
props[method]?.(diffFloor, offset * (moveFloor - 1))

Note that we are throwing the event to the parent component, because our elevator component and the floor container component are at the same level, and only in this way can we set the style of the elevator component. which is:

 //传入两个参数,代表动画的执行时间和偏移量,props[method]其实就是动态获取props中的属性
props[method]?.(diffFloor, offset * (moveFloor - 1))

Then the logic here is the code to prevent repeated clicks, which is not a good solution:

 if(+key !== storeyList?.length && +key !== 1){
    setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true })))
}
setTimeout(() => {
    //...
    if(+key !== storeyList?.length && +key !== 1){
      setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false })))
    }
}, diffFloor * 1000);

Modify Floor Container Components

Well, the analysis of this component ends here. Since we have thrown the event to the parent component, we also need to modify the code of its parent component StoreyZone.tsx, as follows:

 import styled from "@emotion/styled";
import Storey from "./Storey";

const StyleStoreyZone = styled.div`
  width: auto;
  height: 100%;
`;
export interface StoreyZoneProps {
  onUp(v: number, h?: number): void;
  onDown(v: number, h?: number): void;
}
const StoreyZone = (props: Partial<StoreyZoneProps>) => {
  const { onUp, onDown } = props;
  return (
    <StyleStoreyZone>
      <Storey
        onUp={(k: number, h: number) => onUp?.(k, h)}
        onDown={(k: number, h: number) => onDown?.(k, h)}
      />
    </StyleStoreyZone>
  );
};

export default StoreyZone;

Then there is the modification of the final ElevatorBuild.tsx component, as follows:

 import styled from "@emotion/styled";
import { useState } from "react";
import { StyleProps } from "../type/style";
import ElevatorShaft from "./ElevatorShaft";
import StoreyZone from "./StoreyZone";

const StyleBuild = styled.div`
  width: 350px;
  max-width: 100%;
  min-height: 500px;
  border: 6px solid var(--elevatorBorderColor--);
  overflow: hidden;
  display: flex;
  margin: 3vh auto;
`;

const ElevatorBuild = () => {
  const [elevatorStyle, setElevatorStyle] = useState<StyleProps["style"]>();
  const [doorStyle, setDoorStyle] = useState<StyleProps["style"]>();
  const [open,setOpen] = useState(false)
  const move = (diffFloor: number, offset: number) => {
    setElevatorStyle({
      transitionDuration: diffFloor + 's',
      bottom: offset,
    });
    setOpen(true)
    setDoorStyle({
      animationDelay: diffFloor + 's'
    });

    setTimeout(() => {
        setOpen(false)
    },diffFloor * 1000 + 3000)
  };
  return (
    <StyleBuild>
      <ElevatorShaft
        elevatorStyle={elevatorStyle}
        leftDoorStyle={doorStyle}
        rightDoorStyle={doorStyle}
        leftToggle={open}
        rightToggle={open}
      ></ElevatorShaft>
      <StoreyZone onUp={(k: number,h: number) => move(k,h)} onDown={(k: number,h: number) => move(k,h)}></StoreyZone>
    </StyleBuild>
  );
};

export default ElevatorBuild;

Finally, let's check the code to see if there is anything that can be optimized. We can see that our button disable logic can be reused. Let's recreate a function, namely:

 const changeButtonDisabled = (key:string,status: boolean) => {
    if(+key !== storeyList?.length && +key !== 1){
      setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: status })))
    }
}

const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
    //...
    changeButtonDisabled(key,true)
    setTimeout(() => {
      //...
      changeButtonDisabled(key,false)
    }, diffFloor * 1000);
};

So far, even if one of our elevator applet is completed, it is not complicated. To summarize the knowledge points we have learned:

  1. css in js we use @emotion library
  2. Communication between parent and child components, using props
  3. Manipulate the DOM, use ref
  4. There are two ways to communicate state within a component, use useState, and how to modify the state
  5. Hook function useEffect
  6. The operation and event of the class name and the setting of the style
  7. React list rendering
  8. Definition of the typescript interface, as well as some built-in types

at last

Of course, we can also expand here, such as the limit on the number of floors, and add the animation effect of the beauty inside after the door is opened. If you are interested, you can refer to the source code to expand by yourself.

Online example

Finally, thank you for watching. If you think this article is helpful to you, I hope you don't hesitate to like and subscribe, and click star with your little hands, hehe.

Special statement: This small example is only suitable for beginners to learn, even if it is a big guy, this small program is very simple for a big guy.

夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。