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 operations
message.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