I like to implement some wheels myself when development is not urgent; This time, we’ll do a very common component called Message

For components such as Message, it is possible to use them in various pages. But we don’t want to have to introduce a container for every page, and we don’t want to have to do special component mounts that work, but not very well.

I wanted to create an out-of-the-box component that would normally just need to call a simple function in JS, and eventually allow for user customization.

message.success("Success")
message.error("fail")
Copy the code

Final effect

Component design

  • There is no need to manually mount the component at call time
  • A general purpose container
  • You can use options to configure component message content and disable delay
  • Perfect for friendly animation display
  • more…

Component implementation

Automatically mount components

When it comes to auto-mount, my idea is to automatically run the code for the mount component when importing Message. My idea is to execute the function immediately

(function initModalContainer() {
  let ele = document.getElementById("source-modal-contain");
  if(! ele) {// If no container exists, create it
    let sourceModalContainer = document.createElement("div");
    sourceModalContainer.id = "source-modal-contain";
    document.body.append(sourceModalContainer);
    ele = document.getElementById("source-modal-contain");
    // Mount the container on the real DOM via ReactDOM
    ReactDOM.render(<ModalContainer />, ele);
  }
})();
Copy the code

Ps: When I was designing Message, I determined that the mount part was actually quite universal. Pop-ups like Modal components could be inserted into the outermost container. So here you’ll see the container name is ModalContainer

Container Outer Container and ContainItem Container child UI implementation

Don’t stay up late don’t stay up late HHH, I was writing the container component at 2am, and the next day it was hard to imagine that I was using the activeIDList method to control the current active messages.

Implementation idea:

  • Write a nodeList to hold the message, so that we can uninstall the message simply by deleting the corresponding message from the list.
  • A layer of components is wrapped around the node node, which we control to animate

Here I actually encountered a problem:

  • Since useState in a hook function is an asynchronous operation and does not provide a callback like setState does, when we perform multiple similar operationsmessage.success()I ended up using the class component because asynchrony had unintended effects. My original idea was to call the control method in the child component to hide the operation

ModalContainer

class ModalContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      nodeList: [].activeIDList: [],}; }render() {
    return (
      <div className={` ${styles["modal-container"]} `} >
        {this.state.nodeList.map((item, index) => (
          <ModalItem
            key={item.id}
            show={this.state.activeIDList.indexOf(item.id)! = =1}
            config={item.config}
          >
            {item.node}
          </ModalItem>
        ))}
      </div>); }}Copy the code

The corresponding CSS

.modal-container{
    width: 100vw;
    position: fixed;
    z-index: 5000;
    left: 0;
    top: 0;
    display: flex;
    flex-direction: column;
}
Copy the code

This is the basic framework for ModalContainer, passing in a configuration for each ModalItem. Show is controlled by this.state.activeidList in the outer layer.

Because we want to change show to be the same as changing props. Show for ModalItem, using ModalItem directly will inevitably cause component re-rendering, so we implement ModalItem like this:

ModalItem

function ModalItem(props) {
  const [show, setShow] = useState(props.show);
  useEffect(() = > {
    if (props.show === false) setShow(false)
  }, [props.show]);
  return (
    <div
      className={`flexCenterThe ${styles["modal-item"]} ${
        show ? "" : styles["modal-item-hidden"]} `}style={{ "--duration--": props.config.duration+"ms}} ">
      {props.children}
    </div>
  );
}
Copy the code

The corresponding CSS

.modal-item{
    margin:.375rem;
    transform: translateY(-0.12 rem);
    opacity:.2;
    animation: modalItemShow var(--duration--) ease forwards;
}
@keyframes modalItemShow {
    to{transform: translateY(0);opacity: 1;}
}
.modal-item-hidden{
    animation: modalItemHidden var(--duration--) ease forwards;
}
@keyframes modalItemHidden {
    to{transform: translateY(-2.5 rem);opacity: 0;}
}
Copy the code

Implement component control

Obviously, we need to manipulate nodeList and activeList, but what should be exposed is not the ability to directly modify nodeList and activeList. My idea is to implement addChild and removeChild;

First prepare an object in the outermost layer

const modalControl = {
  addChild: null.removeChild: null};Copy the code

Then implement both functions in the Constructor function of ModalContainer;

addChild()

What the addChild function does is add a node to nodeList as follows

{
   node: item, // This is a message (or a popover), ReactComponent
   config,     // This is the configuration information for this message
   id          // The unique ID generated by the timestamp
}
Copy the code

Finally, this message should be transmitted to the index in nodeList after setState after ununwrapping is complete (is it necessary to change to a unique ID here? It is worth considering).

const addChild = async (item, config) => {
      let nodeNew = [...this.state.nodeList];
      let id = new Date().getTime();
      nodeNew.push({
        node: item,
        config,
        id
      });
      let newActiveIDList = [...this.state.activeIDList, id];
      // Add this id to the activeIDList
      return new Promise((resolve) = > {
        this.setState(
          {
            activeIDList: newActiveIDList,
            nodeList: nodeNew,
          },
          () = > {
            resolve(nodeNew.length - 1); }); }); };Copy the code

removeChild()

RemoveChild () removes the key from addChild() from the active message list to complete the animation, and removes the node from nodeList when the animation is complete

const removeChild = async (key) => {
      let {config,id:nodeID} = this.state.nodeList[key];
      return new Promise((resolve, reject) = > {
        setTimeout(() = > {
          let newActiveIDList = this.state.activeIDList.filter(item= >item ! == nodeID);this.setState(
            {
              activeIDList: newActiveIDList,
            },
            () = > {
              let newNodeList = this.state.nodeList.filter(item= >item.id ! == nodeID);setTimeout(() = > {
                this.setState(
                  {
                    nodeList: newNodeList,
                  },
                  () = >{ resolve(); }); }, config.duration); }); }, config.delay); }); };Copy the code

As a final step, we attach these two functions to the modalControl object, and the final code for the entire container is as follows

import React, { useEffect , useState } from "react";
import ReactDOM from "react-dom";
import styles from "./modal.module.css";
const modalControl = {
  addChild: null.removeChild: null};function ModalItem(props) {... }class ModalContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      nodeList: [].activeIDList: [],};const addChild = async (item, config) => {
      ...
    };
    const removeChild = async (key) => {
      ...
    };
    modalControl.addChild = addChild;
    modalControl.removeChild = removeChild;
  }
  render() {
    return (
      <div className={` ${styles["modal-container"]} `} >
        {this.state.nodeList.map((item, index) => (
          <ModalItem
            key={item.id}
            show={this.state.activeIDList.indexOf(item.id)! = =1}
            config={item.config}
          >
            {item.node}
          </ModalItem>
        ))}
      </div>); }} (function initModalContainer() {
  ...
})();
export { modalControl };
Copy the code

At this point, we’ve actually implemented a generic container that we can easily build a Message component from

Finish the final Message

Start by writing a Message template. I only implemented the Success template here

import successSvg from '.. /images/success.svg';
const svgmap={
   "success":successSvg
}
function MessageTemplate(props){
    return (
        <div className={` ${styles[props.type+ '-template']} ${styles['template` ']}}onClick={()= >{console.log('test')}}>
            <img src={svgmap[props.type]} width="30" height="30" style={{margin:'6px'}} ></img>
            <span>{props.content}</span>
        </div>)}Copy the code

We then implement the default messageSuccess function, which calls the default template

const defaultConfig={
    delay:1500.duration:360
}
async function messageSuccess(content){
    let key=await modalControl.addChild(
        <MessageTemplate type="success" content={content}/>,
        defaultConfig
    )
    await modalControl.removeChild(key)
}
Copy the code

Then implement a messageSuccessConfig function that allows custom configuration. Calling this function will result in a messageSuccess that calls the custom config

function messageSuccessConfig(e){
    letoptions={ ... defaultConfig, ... e };return async function(content){
        let key=await modalControl.addChild(
            <MessageTemplate type="success" content={content||options.content}/>,
            options
        )
        await modalControl.removeChild(key)
    }
}
Copy the code

Components use

We built messageSuccess and messageSuccessConfig above, and using the Message component is as simple as we thought at first

  messageSuccess("Success!)
  let test=messageSuccessConfig({
    delay:2400
  })
  test("success!")
Copy the code

It all works out in the end

conclusion

Because container is abstracted out, it’s easy to implement Modal and Dialog. We can also write our own prompt box and add it to the message queue via addChild. This component leaves a lot to be desired, from clickback to active closing to manual closing, and there are many areas that can be optimized