“This is the 11th day of my participation in the First Challenge 2022. For details: First Challenge 2022”

preface

The company just started to work, and the back-end staff asked for leave for rest, so there is no contact person for our work, and the demand staff has not worked out a complete product document. What should we do with this rare spare time? How to use Antd tables in React projects to achieve Echarts thermal map effects? .

Demand background

The original requirement is to make a page to statistics the 52 weeks’ working hour intensity table of all staff in the department or branch company every year, and intuitively display the strong and weak relationship of each staff’s working hour. Mine? The requirement is pretty simple. Note that this is the original requirement, which we have iterated over for at least 10 versions.

Old rules, first on the design drawing, intuitive understanding of the needs:

This is what the original requirement looked like. The header of the table had a default column and was supposed to be fixed, the bottom two rows were supposed to be fixed, and two columns were supposed to be sorted. Is not our needs are unusual road, how difficult to give you how, others for you to dig a hole is waiting for you to jump.

One release in the iteration:

The color changes? Look carefully at the top of the watch, is there a slider, the meaning of the slider is that AS long as I drag at will, the color block below only retain the slider range of color, is not very reasonable? You can’t argue with that, but it’s perfectly fine to do what I want.

That’s it? Impossible, during the data visualization, Boss saw that Echarts had a thermal map effect, come on, can our timesheet be changed to the same thermal map effect as Echarts? Cough, now using Echarts to reconstruct even the original data structure is not supported, I need to discuss with the back end to have an accurate transformation plan, the boss let down to study. Ah, see I will, can have what way, the life of the worker is not easy in these drops.

Here is the final result:

thinking

Thinking about what? When we get this requirement, we don’t want to go straight to the code, but we need to think about how difficult this page is, what are the difficulties? What about the extensibility of our code? What are the data structures that interact with the back end? We had to think about how we could make small changes to the front and back without affecting the overall delivery time.

In terms of the above requirements, I summarize the following difficulties:

  1. Antd Table component does not support Table header column can default? How to achieve the effect of design drawing design without support? How to fix the first two lines to the header style? How to implement the sorting function without Antd header sorting API?
  2. How is a fixed row at the end of a table realistic?
  3. Legend enumeration, color enumeration how to define?
  4. What is the data structure like? Should the thermal map effect be redeveloped on the existing basis? Does the thermal map meet the existing page function? Can you achieve the thermal map effect in other ways?
  5. Do you use the Slider component provided by Antd or encapsulate it yourself? What changes do I need to make with the Slider component? How to realize the mouse moving in and out of the table color block following the change of the effect? How to calculate the distance the mouse moves over the Slider to find the enumeration value of the color value?
  6. How to add event support to view color changes?

This is what I thought after I got the demand, and it is also what I thought would be difficult in the development process. When I think about these points thoroughly, the problems will not be a problem, they will be solved easily, and I will get twice the result with half the effort.

The development of

Now that we have listed all the problems we will encounter during development, we can solve them one by one.

Problem a painted painted painted fostered fostered

Here’s what I did for the first thought:

I looked at Antd’s Table component and found that it does not support the header column default, so it is impossible to write a Table with a div. That would be ridiculous. Table headers are not supported. Then I don’t want the header, as it is the data line, and then according to the serial number to judge it is the first line, if there is data on the black, no data on the white; The second line of style according to the design of the grey background of the white style written; If it’s not the first line, the second line, then the color value enumerates the color logic. Yes, this is a bit more reliable, much easier than using the table header, now the only drawback is to have to write their own sorting function logic. I’ll leave it to style control to fix it. I’ll use the new property sticky of position. Code first:

Layout code:

<Table className="custom-table strength-table h ofa" rowKey="id" rowClassName={(record: any, index: number) => { let className: any = []; If (index === 0) {className= ['first-tr-line', 'table-first-tr-sticky']; if (index === 0) {className= ['first-tr-line', 'table-first-tr-sticky']; } // if (index === 1) {className= ['second-tr-line', 'table-second-tr-sticky']; } return className.join(' '); }} bordered sticky={true} dataSource={dataSource} columns={columns} loading={loading} showHeader={false} // Hide table headers pagination={false} </>Copy the code

Logical code:

/** * const turnListColsClassName = (index: Number, value: any, style: number, workHour: number): string => {// If (index === 0 &&! value) { return 'no-data-td'; } // If (index === 0 && value) {return 'has-data-td'; } // if (index === 1) {return 'second-tr-line'; } // return isTdCellBgColor(style, workHour); } /** * const isTdCellBgColor = (style: number, workHour: number): string => { if (! workHour) { return ''; } // Out of range color blocks disappear, default background color is white if (! sliderArr.includes(style)) { return; } let colorArr: any[] = [ 'bit-bg-0to37', 'bit-bg-37to50', 'bit-bg-50to55', 'bit-bg-55to60', 'bit-bg-60to65', 'bit-bg-65to70', 'bit-bg-70to80', 'bit-bg-80over' ]; let cssStyle: string = ''; // If the slider value or legend value is equal to the color value, If (hoverNum == style) {cssStyle = 'bit-num-shadow'} return [cssStyle, colorArr[style]]. Join ("); }Copy the code

Style code:

// fix the first row. Table-first-tr-sticky {td {position: sticky! important; top: 0px; z-index: 1 ! important; background-color: #fff; }} // fix the second line. Table-second-tr-sticky {td {position: sticky! important; top: 24px; z-index: 1 ! important; background-color: #647386; } } .first-tr-line { height: 24px ! important; td { height: 24px; color: #fff; background-color: #647386 ! important; } .no-data-td { background-color: #fff ! important; border-right-color: #fff ! important; } .has-data-td { background-color: #263238 ! important; border-right-color: #fff ! important; } .no-sort-color { color: #fff; } .has-sort-color { color: #409EFF } } .second-tr-line { color: #fff; background-color: #647386 ! important; }Copy the code

The first question is how to write the sorting function. I did not write it as a component, because the sorting function used elsewhere is provided by the table. I need to write the page myself, so it is not common, so I did not propose a separate component; Also, if you look at the diagram, you can see that the total column and the weekly column merge the first and second rows, so you should also consider merging cells when doing these two columns. See the following code for specific implementation:

/ * * * combined column and weekly series content * @ param index * @ param value * @ param orderFiled * / const turnTotalAndWeekColContent = (index: number, value: any, orderFiled: string): any => { let className: string = ''; let children: any = null; let rowSpan: number = null; If (index === 0) {rowSpan = 2; className = 'first-tr-td'; children = ( <div> <div>{value}</div> <div className='flexc crp' onClick={() => onSortHandle(orderFiled)}> <CaretUpOutlined className={[ 'no-sort-color', isSorterStyle(orderFiled, 1) ].join(' ')} /> <CaretDownOutlined style={{ marginTop: '-5px' }} className={[ 'no-sort-color', isSorterStyle(orderFiled, 2) ].join(' ')} /> </div> </div> ) } if (index === 1) { rowSpan = 0; } if (index ! == 0 && index ! == 1) { className = 'td-bold-col'; } if (index ! == 0) { children = <div>{value}</div>; } return {children, props: {className, rowSpan}}; } @param orderField */ const onSortHandle = (orderField: string) => {let obj: any = sorterArray.find((v: any) => v.orderField === orderField); let newSorterArray: any[] = [...sorterArray]; let newDataSource: any[] = JSON.parse(JSON.stringify(dataSource)) || []; newSorterArray.forEach((v: any) => { if (v.orderField === orderField) { v.isSort = true; if (v.orderMode ! = 2) { obj.orderMode += 1; } else { obj.orderMode = 0; } } else { v.orderMode = 0; v.isSort = false; }}); setSorterArray(newSorterArray); If (obj. OrderMode === 0) {setDataSource(dataSourceBak); return; } let tempList: any[] = newDataSource.slice(0, 2); let aray: any[] = newDataSource.slice(2).sort(compare(orderField, obj.orderMode)); setDataSource([...tempList, ...aray]); } /** * const compare = (orderField: string, orderMode: string, orderMode: string, orderMode: string); Number) => {return function (a: any, b: any) {// Ascending if (orderMode === 1) {return a[orderField] -b [orderField]; } // descending order if (orderMode === 2) {return b[orderField] -a [orderField]; } return 0; @param orderField @param orderMode 1 ascending, 2 descending */ const isSorterStyle = (orderField: string, orderMode: number): string => { let sorterClassName: string = ''; sorterArray.forEach((v: any) => { if (v.isSort && v.orderField === orderField && v.orderMode === orderMode) { sorterClassName = 'has-sort-color'; }}); return sorterClassName; } /** * const columns= [{dataIndex: 'name', className: 'table-right-border', align: 'center', width: 66, render: (text: any, record: any, index: number) => { const {id} = record; let className: string = ''; if (index === 0) { className = 'first-tr-td'; } if (index === 1) { className = 'second-tr-td';  } if (index !== 0 && index ! == 1) { className = 'td-first-col'; } if (selectUserId === id) { className = 'bit-first-td-active'; } let children: any = ( <div title={text} className={[index !== 0 && index!== 1 ? "bit-name-div hcp": "", "w eps"].join(' ')} onClick={() => clickNameHandle(id, index)} >{text}</div> ) return {children, props: {className}}; } }, { dataIndex: 'total', className: 'table-right-border', align: 'center', width: 41, render: (text: any, record: any, index: number) => { return turnTotalAndWeekColContent(index, text, 'total'); } }, { dataIndex: 'weekTotal', className: 'table-right-border', align: 'center', width: 41, render: (text: any, record: any, index: number) => { return turnTotalAndWeekColContent(index, text, 'weekTotal'); } }, ...buildListColumns('tableVos') ];Copy the code

Well, the first question related to the explanation of the question, the solution to the problem or need to master a certain level of knowledge, which is due to the accumulation of the usual project, so a good memory is better than bad writing, encounter problems or record up so that the next encounter and meng.

Question 2 u u u fostered fostered

How to implement the fixed row at the end of the table? This problem can be solved by positioning in question 1, but I did not choose that way, because I fixed the end of the Table by using the SUMMARY (summary bar) API provided by the Table component and positioning in question 1. I adhere to the concept of: use it if you have it, change it if you don’t, and write if you change it. Add the following code to the Table component:

Layout code:

<Table className="custom-table strength-table h ofa" rowKey="id" rowClassName={(record: any, index: number) => { let className: any = []; if (index === 0) { className= ['first-tr-line', 'table-first-tr-sticky']; } if (index === 1) { className= ['second-tr-line', 'table-second-tr-sticky']; } return className.join(' '); }} bordered sticky={true} dataSource={dataSource} columns={columns} loading={loading} showHeader={false} Pagination ={false} // summary={() => {const {total: avgTotal, weekTotal: avgWeekTotal, tableVos: avgList = []} = avgLine || {}; const {total: sumTotal, weekTotal: sumWeekTotal, tableVos: sumList = []} = sumLine || {}; return ( <> { ObjTest.isNotEmptyArray(dataSource) ? <> {/* average Row */} < table.summary. Row> < table.summary. Cell index={0} className="bit-summary-line1 table-right-border"> <div ClassName ="bit-sumline-total"> Average </div> </ table.summary. Cell> < table.summary. Cell index={1} className="bit-summary-line1 table-right-border"> <div className="bit-sumline-font">{avgTotal}</div> </Table.Summary.Cell> <Table.Summary.Cell index={2} className="bit-summary-line1 table-right-border"> <div className="bit-sumline-font">{avgWeekTotal}</div> </Table.Summary.Cell> { avgList.map((v: any, index: number) => <Table.Summary.Cell key={index} index={index+10} className={ [ "bit-summary-line1", v.isLine ? "table-right-border" : "", isDescOrCellBgColor1(v.colorEnum - 1, v.workHour) ].join(' ')}> <div className="bit-sumline-font">{v.workHour}</div> </Table.Summary.Cell> ) } </ table.summary. Row> {/* Summary Row */} < table.summary. Row> < table.summary. Cell index={0} className="bit-summary-line2 Table-right-border "> <div className="bit-sumline-total"> <Tooltip title={<div> total number of people: {userCount}</div>}> (<span>{userCount}</span>) </Tooltip> </div> </Table.Summary.Cell> <Table.Summary.Cell index={1} className="bit-summary-line2 table-right-border"> <div className="bit-sumline-font">{sumTotal}</div> </Table.Summary.Cell> <Table.Summary.Cell index={2} className="bit-summary-line2 table-right-border"> <div className="bit-sumline-font">{sumWeekTotal}</div> </Table.Summary.Cell> { sumList.map((v: any, index: number) => <Table.Summary.Cell key={index} index={index+10} className={ [ "bit-summary-line2", v.isLine ? "table-right-border" : "" ].join(' ')}> <div className="bit-sumline-font">{v.workHour}</div> </Table.Summary.Cell> ) } </Table.Summary.Row> </>  : null } </> ); }} / >Copy the code

Style code:

.bit-summary-line1, .bit-summary-line2 {
    position: sticky;
    bottom: 28px;
    height: 28px;
    text-align: center;
    color: rgba(0, 0, 0, 0.85);
    background-color: #fff;
    border-top: 1px solid #CFD8DB;

    .bit-sumline-total {
        text-align: center;
        font-size: 12px;
        font-weight: bold;
        color: rgba(0, 0, 0, 0.85);
    }

    .bit-sumline-font {
        text-align: center;
        font-weight: bold;
    }
}

.bit-summary-line2 {
    bottom: 0px;
    border-top: 0;
}
Copy the code

Painted three fostered fostered fostered fostered

Enumeration value definition this should be discussed with the backend students, to avoid both sides according to their own docking neither want to change the more embarrassing, so these are not negligible details.

// Color enumeration const ColorEnum: any = [{style: 1, label: '0~37.5'}, {style: 2, label: '37.5~50'}, {style: 3, label: '50~55'}, {style: 4, label: '55~60'}, {style: 5, label: '60~65'}, {style: 6, label: '65~70'}, {style: 7, label: '70~80'}, {style: 8, label: '80 '}];Copy the code

Question 4 u u u do do

Data structure is also to discuss with the backend students, it is best to define the structure and field name in advance, but for me to define the structure is ok, as for the definition of field name is not how to match the meaning will be put forward to let the backend modify, the most important is the structure must not change. The data structure of our page looks like this:

Footer fixed end after two lines respectively to independent balanceRow and totalRow two objects, intermediate form data for deptWorkingIntensityDetailsVos array, actually dunk don’t have to be such, can put the two rows of data into an array of footer, Maybe he did it for our convenience

Didn’t the background mention that when we were doing data visualization, the boss asked us to change the finished page to Echarts thermal map effect? In fact, we have thought about this problem, made relevant research and comparison, and found that Echarts thermal map has limitations, such as the default, fixed, sorting effect of table header mentioned in the requirements; Table tail fixing effect; When there is a lot of data, the table can be rolled to view. However, the thermal map is drawn on Canvas and displayed on one screen. The page effect is crowded and not intuitive at that time, so we did not carry out reconstruction after much thought. Of course, the original data structure will not support the thermal map display, so the page will be redeveloped, pushed back to the beginning, the boss did not accept the development time, but wanted that effect. So what do we do? I suggest that we change on the existing basis, add a slider can drag left and right to view the corresponding color block, and then do the middle version, only can drag without the mouse move in and out of the effect; After a few days on line, I found that I still couldn’t forget, or wanted the effect of mouse moving in and out, so we had to think about how to achieve that effect, and finally achieved the desired effect in the end.

Painted painted painted five u do

In fact, this thought is the most important one of the requirements, and all the interactions are generated by it.

The first thing that comes to mind is that Antd provides a Slider input bar component, which can slide left and right back and forth, which is quite suitable for our business. Can we use it? Since I had this idea, I went to study it and found that its range of drag-and-drop effect was quite in line with our expectations, as shown in the picture below:

Its value is 0~100, so I’ll change it to our enumeration value 0~7. Wrong style? Just change it. Change is our strength. We can fix it in a minute. Oh, come all right ^_^.

I repackaged the Slider component, because it didn’t provide enough events to meet our needs. I also needed to add additional mouse in and out events to it. The page was relatively complex, so I made it a separate component to facilitate reference processing logic.

The Slider component itself does not provide mouse events, so how do we add them? Put a div around it, give the div a mousein, mouseout click release event, and set a default width to calculate the enumeration value of the mouseover position and pass it to the parent component, so that the color block corresponding to the enumeration value is shaded and highlighted to achieve the thermal map effect.

The idea is as above, the code implementation is as follows:

createCustomSlider.tsxFile:

import React, {useEffect, useRef, useState} from 'react'; import {Slider} from 'antd'; import Decimal from "decimal.js"; interface CustomSliderProps { flag: number; // Whether to reset the default value onMouseMoveHandle: (value: number) => void; OnMouseLeaveHandle: (value: number) => void; OnAfterChangeHandle: (arr: number[], flag: number) => void; } /** * CustomSlider = (props:) {/** * const CustomSlider = (props:); CustomSliderProps) => { const { flag = 0, onMouseMoveHandle = (value: number) => {}, onMouseLeaveHandle = (value: number) => {}, onAfterChangeHandle = (arr: number[], flag: number) => {} } = props; const [sliderValue, setSliderValue] = useState<[number, number]>([0, 7]); const sliderRef = useRef<any>(null); const boolRef = useRef<boolean>(false); useEffect(() => { if(flag === 1) { setSliderValue([0, 7]); } }, [flag]); return ( <div className="bit-slider-box" ref={sliderRef} onMouseMove={(e: any) => { if (boolRef.current) { return; } if (sliderref.current) {let mouseOffsetX: string = e.clientx; let boxOffsetX: string = sliderRef.current.offsetLeft; let digital: number = new Decimal(new Decimal(mouseOffsetX).sub(boxOffsetX).toNumber()).div(sliderRef.current.clientWidth).toNumber(); Let value: number = number (new Decimal(digital).div(0.125).tofixed (0)); onMouseMoveHandle(value); } }} onMouseDown={() => boolRef.current = true} onMouseUp={() => boolRef.current = false} onMouseLeave={() => onMouseLeaveHandle(-1)} > <Slider range min={0} max={7} value={sliderValue} tooltipVisible={false} onAfterChange={(value: [number, number]) => { let arr: number[] = []; for (let i = value[0]; i <= value[1]; i++) { arr.push(i); } boolRef.current = false; onAfterChangeHandle(arr, 0); }} onChange={(value: [number, number]) => setSliderValue(value)} /> </div> ) } export default CustomSlider;Copy the code

Style code:

.bit-slider-box { width: 160px; margin-right: 20px; .ant-slider{ height: 14px; margin: 0; padding: 0; .ant-slider-rail { height: 14px; background: linear-gradient(90deg, #60AA3C 0%, #FFD3D3 47%, #900908 100%); } .ant-slider-track { height: 14px; background-color: transparent; } .ant-slider-step { height: 14px; } .ant-slider-handle { margin-top: 0; }}}Copy the code

It can be seen that we used the X-axis coordinate distance of mouse movement minus the distance between the outer box of Slider component and the left boundary to divide by the actual width of the box to get a proportion, and then divided by the proportion of each piece (1/8 = 0.125) to get a rounded value as the enumeration value. Of course, the rounded value is not so accurate, but it can be ignored, after all, the background color of our slider is gradient color, just to solve the situation of approximate value; In addition, I defined boolRef as the switch, which can only be dragged when the mouse is down, otherwise the mouse will be Silder to follow and drag, so WE used boolRef as the switch to control whether to move in, move out or click and drag events.

Using components:

<div className="search-form-desc-wrap flexic">
    <CustomSlider
        flag={flag}
        onAfterChangeHandle={(arr: number[], flag: number) => {
            setSliderArr(arr);
            setFlag(flag);
        }}
        onMouseMoveHandle={(value: number) => {setHoverNum(value)}}
        onMouseLeaveHandle={(value: number) => {setHoverNum(value)}}
    />
</div>
Copy the code

Logical code:

/ / const buildListColumns = (dataIndex): any[] => {let column: any[] = []; if (ObjTest.isNotEmptyArray(dataSource)) { let firstList: any[] = dataSource[0][dataIndex] || []; let temp: any[] = firstList.map((v: any, i: number) => { return { className: `${v.isLine ? 'table-right-border': ''}`, align: 'center', width: 31, render: (text: any, record: any, index: number) => { let {month, week, workHour, beginTime, endTime, colorEnum} = record[dataIndex][i]; let beginDate: string = beginTime ? beginTime.substring(5) : ''; let endDate: string = endTime ? endTime.substring(5) : ''; let className: string = turnListColsClassName(index, month, colorEnum - 1, workHour); let children: any = ( <div className="bit-tr-cell"> { index === 0 ? <div>{month}</div> : null } { index === 1 ? <Tooltip title={<div>{beginDate}~{endDate}</div>}> <div>{week}</div> </Tooltip> : null } { index ! == 0 && index ! == 1 && sliderArr.includes(colorEnum - 1) ? <div data-value={colorEnum - 1} className={[index !== 0 && index !== 1 ? "bit-num-hover" : ""].join(' ')} >{workHour}</div> : null } </div> ); return {children, props: {className}}; }}}); column = [...temp]; } return column; }Copy the code

Painted painted six fostered fostered fostered

Slider is so difficult to add mouse events we add, general div we can not add? Impossible third, strike while the iron is hot, directly on the code:

Layout code:

<div className="search-form-desc-wrap flexic"> <CustomSlider flag={flag} onAfterChangeHandle={(arr: number[], flag: number) => { setSliderArr(arr); setFlag(flag); }} onMouseMoveHandle={(value: number) => {setHoverNum(value)}} onMouseLeaveHandle={(value: Number) => {setHoverNum(value)}} /> <div className="bit-pic-box flexic"  { ColorEnum.map(v => <div className="ml6 flexic" style={{cursor: "pointer"}} key={v.style} onMouseOver={() => { if (hoverNum === v.style - 1) { return; } setHoverNum(v.style - 1); }} onMouseOut={() => { setHoverNum(-1); }} > <div className={["bit-color-block", isDescOrCellBgColor(v.style - 1)].join(' ')}></div> <div className="ml4">{v.label}</div> </div> ) } </div> </div>Copy the code

Logical code:

/** ** @param style */ const isDescOrCellBgColor = (style: number): string => {let colorArr: any[] = [ 'bit-bg-0to37', 'bit-bg-37to50', 'bit-bg-50to55', 'bit-bg-55to60', 'bit-bg-60to65', 'bit-bg-65to70', 'bit-bg-70to80', 'bit-bg-80over' ]; return colorArr[style]; }Copy the code

The above logic to realize that the color block in the chart on the legend will be highlighted is mainly set according to setHoverNum(), and then according to the isTdCellBgColor() function in the code shown in question 1 to determine whether to add the shadow effect, so as to achieve the effect of highlighting with changes.

Style code:

.bit-tr-cell {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    .bit-num-hover {
        width: 100%;
        height: 32px;
        line-height: 31px;
        cursor: pointer;
    }
    .bit-num-hover:hover {
        box-shadow: #666 0px 0px 10px;
    }
}

.bit-num-shadow {
    .bit-num-hover {
        width: 100%;
        height: 32px;
        line-height: 31px;
        cursor: pointer;
        box-shadow: #000000BF 0px 0px 10px;
    }
    .bit-num-hover:hover {
        box-shadow: #000000BF 0px 0px 10px;
    }
}

.bit-bg-0to37 {
    background-color: #60AA3C;
}

.bit-bg-37to50 {
    background-color: #9EBA77;
}

.bit-bg-50to55 {
    background-color: #D2E7C8;
}

.bit-bg-55to60 {
    background-color: #FFD5D5;
    border: 1px solid #FFD5D5;
}

.bit-bg-60to65 {
    background-color: #EAADAD;
}

.bit-bg-65to70 {
    background-color: #D28180;
}

.bit-bg-70to80 {
    background-color: #B85252;
}

.bit-bg-80over {
    background-color: #900908;
}
Copy the code

This is enough for thinking about how to achieve Echarts thermal map effect, which is the process from getting requirements to thinking about requirements and then implementing them. You can refer to your ideas and implementation logic. Of course, the solution to each of the problems mentioned above may not be the optimal solution. You must have other better solutions or problems THAT I haven’t thought about, so I hope you can send them out for discussion.

As an aside, have you seen the GIF above? Basically, our working hours per week are around 55 or above, so the “9-9” mentioned in some previous articles is not groundless. I can only sigh that life is not easy. In other words, if you need a complete page code, you can contact me first, of course, it is best for you to masturbate again, so that you will be impressed.

Past wonderful articles

  • How to introduce and use external fonts in Vue or React projects?
  • How to use the Amap plugin and wrap popover components in React projects?
  • Data Visualization – How do I use the Echarts plug-in and encapsulate diagram components in React projects?
  • How can I change the default DatePicker week selector in Antd?
  • How to encapsulate the Vue watermark component and how to use it in React?
  • The most powerful rich text editor? TinyMCE Series [3]
  • The most powerful rich text editor? TinyMCE Series [2]
  • The most powerful rich text editor? TinyMCE Series of articles [1]
  • React project to implement a Checkbox style effect component
  • 2022 First update: 3 Progress bar effects in front end projects
  • Front-end Tech Trends 2022: You don’t come in and see how to earn it a small target?
  • If ancient have programmers writing summaries, probably is such | 2021 year-end summary
  • Front-end development specifications in front end team practice
  • Javascript high-level constructors
  • Javascript high-level inheritance
  • Recursion and closure

After the language

Guys, if you find this article helpful, click 👍 or ➕. In addition, if there are questions or do not understand the part of this article, you are welcome to comment on the comment section, we discuss together.