ReactWheels08-dialog组件

Dialog组件

设计API

  • 我们用的是React ,最好的办法就是抄同行,交互方式是用户学习软件的过程
  • 你的dialog 不能和同行有明显的不同,否则用户学起来费劲
  • API 最好一致,否则别人迁移到你的UI也很费劲
  1. 确定UI
  2. API
1
2
3
4
5
6
7
8
9
// 第一种方式 标签
<Dialog visable={x}>
你的内容
</Dialog>

// 第二种方式 js
alert('你好').then(fn)
confirm('确定').then(sure,cancel)
model(<table>...</table>)

初版 dialog

lib/dialog/dialog.example.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from 'react';
import Dialog from './dialog';

export default function(){
const[x,setX] = useState(false);
return (
<div>
<button onClick={ ()=>setX(!x) }>click</button>
<Dialog visible={x}>
<div>hi</div>
</Dialog>
</div>
)
};

lib/dialog/dialog.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';

interface Props {
visible:boolean;
}

const Dialog:React.FunctionComponent<Props> = (props) => {
return props.visible ?
// 对于 react来说 你必须返回一个节点或者 null
// 直接 {props.children} 报错
<div>{props.children}</div> :
null
}
export default Dialog;

Fragment 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Fragment } from 'react';

interface Props {
visible:boolean;
}

const Dialog:React.FunctionComponent<Props> = (props) => {
return props.visible ?
// 遮罩层的div 和 dialog分开,因为点击遮罩层要消失
// <Fragment> 是为了渲染时不多渲染一个 div 和通过编译
// 不能直接返回多个节点必须一个根节点,
// 而是只它里面的内容
<Fragment>
<div className="fui-dialog-mask"></div>
<div className="fui-dialog">
{props.children}
</div>
</Fragment>
:
null
}
export default Dialog;

dialog垂直水平居中

1
2
3
4
position:fixed;
top:50%;
left:50%;
transform: translate(-50%,-50%);

dialog.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.fui-dialog{
position: fixed;
background: white;
width:10em;
height: 10em;
border-radius: 4px;
top:50%;
left:50%;
transform: translate(-50%,-50%);
&-mask{
position: fixed;
top:0;
left:0;
width:100%;
height: 100%;
background: fade-out(black, 0.5);
}
}

dialog.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Fragment } from 'react';
import './dialog.scss';
import {Icon} from '../index';

interface Props {
visible:boolean;
}

const Dialog:React.FunctionComponent<Props> = (props) => {
return props.visible ?
// 遮罩层的div 和 dialog分开,因为点击遮罩层要消失
// <Fragment> 是为了渲染时不多渲染一个 div 和通过编译
// 不能直接返回多个节点必须一个根节点,
// 而是只它里面的内容

// 关闭按钮不要在 header里 因为这样就必须有 header
<Fragment>
<div className="fui-dialog-mask"></div>
<div className="fui-dialog">
<div className="fui-dialog-close">
<Icon name="close"/>
</div>
<header className="fui-dialog-header">提示</header>
<main className="fui-dialog-main">
{props.children}
</main>
<footer className="fui-dialog-footer">
<button>ok</button>
<button>cancel</button>
</footer>
</div>
</Fragment>
:
null
}
export default Dialog;

此时的问题是 如何简化 className很重复,而且麻烦

  • 偏函数使用
1
2
3
4
5
6
7
8
9
10
11
function scopedClassMaker(prefix: string){
return function x(name?: string){
return [prefix,name].filter(Boolean).join('-');
}
}

const scopedClass = scopedClassMaker('fui-dialog');
const sc = scopedClass;

sc() // fui-dialog
sc('hello') // fui-dialog-hello

给我们的组件加一个样式

  • 我们的组件样式应该有一个全局的控制如字体和盒模型
  • 新建 lib/index.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们的组件都是 fui 开头的

[class^=fui-]{
box-sizing: border-box;
}

// 这个选择器的缺陷是无法选取到 伪元素
// 这个选择器的缺陷是无法选取到 伪元素
// 这个选择器的缺陷是无法选取到 伪元素
// 优化后

[class^=fui-]{
box-sizing: border-box;
&::after,
&::before{
box-sizing: border-box;
}
}

svg 为父亲元素的颜色

1
fill:currentColor;

React.cloneElement 的用法

  • dialog 传递的按钮是个数组 buttons
1
2
3
4
5
6
7
8
9
10
11
12
13
// 直接这样会报错 ,意思是你缺少key
<footer>
{props.buttons}
</footer>

// 答案就是使用 map 和 React.cloneElement
<footer>
{props.buttons.map((button,index) =>
React.cloneElement(button,{key:index})
)}
</footer>

// 不过这里有个性能的损失,就是每次都会 map 产生新的 button

能凑乎使用的 dialog

dialog.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React, { Fragment, ReactElement } from 'react';
import './dialog.scss';
import {Icon} from '../index';
import {scopedClassMaker} from '../classes';

interface Props {
visible:boolean;
buttons:Array<ReactElement>;
onClose:React.MouseEventHandler;
closeOnClickMask?:boolean;
}

const scopedClass = scopedClassMaker('fui-dialog');
const sc = scopedClass;

const Dialog:React.FunctionComponent<Props> = (props) => {
const onClickClose:React.MouseEventHandler = (e)=>{
props.onClose(e);
}
const onClickMask:React.MouseEventHandler = (e)=>{
if(props.closeOnClickMask){
props.onClose(e);
}
}
return props.visible ?
<Fragment>
<div className={sc('mask')} onClick={onClickMask}></div>
<div className={sc()}>
<div className={sc('close')} onClick={onClickClose}>
<Icon name="close"/>
</div>
<header className={sc('header')}>提示</header>
<main className={sc('main')}>
{props.children}
</main>
<footer className={sc('footer')}>
{props.buttons.map((button,index) =>
React.cloneElement(button,{key:index})
)}
</footer>
</div>
</Fragment>
:
null
}
Dialog.defaultProps = {
closeOnClickMask : false
}
export default Dialog;

dialog.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@import "../helper";
.fui-dialog{
position: fixed;
background: white;
min-width:20em;
border-radius: 4px;
top:50%;
left:50%;
transform: translate(-50%,-50%);
&-mask{
position: fixed;
top:0;
left:0;
width:100%;
height: 100%;
background: fade-out(black, 0.5);
}
&-header{
font-size: 22px;
padding:8px 16px;
border-bottom: 1px solid grey;
}
&-main{
padding:8px 16px;
min-height: 6em;
}
&-footer{
padding:8px 16px;
border-top: 1px solid grey;
display: flex;
justify-content: flex-end;
}
&-close{
position: absolute;
bottom:100%;
left:100%;
background: $main-color;
width: 2em;
height: 2em;
border-radius: 50%;
transform: translate(-50%,50%);
display: flex;
justify-content: center;
align-items: center;
color: white;;
}
}

dialog.example.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { useState } from 'react';
import Dialog from './dialog';

export default function(){
const[x,setX] = useState(false);
const[y,setY] = useState(false);
return (
<div>
<div>
<h1>example1</h1>
<button onClick={ ()=>setX(!x) }>click</button>
<Dialog visible={x}
buttons={
[
<button onClick={()=>{setX(false)}}>1</button>,
<button onClick={()=>{setX(false)}}>2</button>
]
}
onClose={()=>{setX(false)}}
>
<div>hi</div>
</Dialog>
</div>

<div>
<h1>example2</h1>
<button onClick={ ()=>setY(!y) }>click</button>
<Dialog visible={y}
closeOnClickMask={true}
buttons={
[
<button onClick={()=>{setY(false)}}>1</button>,
<button onClick={()=>{setY(false)}}>2</button>
]
}
onClose={()=>{setY(false)}}
>
<div>hi</div>
</Dialog>
</div>
</div>
)
};

一个 z-indexBug

  • 场景如下
1
2
3
4
5
6
<div style="z-index=10">
aaa
</div>

<Dialog/>
// 此时我们的 dialog 没有盖住 div
  • 你可能会想给我们的 dialog 设置 z-index:9999
    • 如果是平级那没问题
    • 但如果你的 Dialog 在一个容器里那个容器的 z-index:9 你就算是 z-index:99999 也没用
      1
      2
      3
      4
      5
      6
      7
      <div style="z-index=10">
      aaa
      </div>
      <div style="z-index:9">
      <!-- 就算dialog 是 z-index:99999 也没用,因为父 div这个层级已经被压住了 -->
      <Dialog>
      </div>

解决办法就是:不要出现在任何元素的里面

解决方案是 react portal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
render() {
// React mounts a new div and renders the children into it
return (
<div>
{this.props.children}
</div>
);
}

// react portal

render() {
// React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
return ReactDOM.createPortal(
this.props.children,
domNode
);
}

推荐dialog的 z-index 设置为1 ,因为用户可以改啊!

z-index 结构划分

1
2
3
4
5
6
7
8
背景区 0~5

内容区 10~19
菜单区 50~59

对话框 100~120

广告区 150~170

为什么推荐我们的 dialog z-index:1呢?

  • 因为层级低容易出现,这样别人用的时候再去覆盖 fui-dialog{z-index:100}

如何不通过标签创造dialog呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
const alert = (content:string) =>{
const component = <Dialog visible={true} onClose={()=>{
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
// 从div上卸载 dialog
ReactDOM.unmountComponentAtNode(div);
// 删除div
div.remove();

}}>{content}</Dialog>;
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
}

ReactElement 和 ReactNode 区别

  • ReactElement 必须是个标签
  • ReactNode 不仅可以是标签还可以是 “字符串”

重构API

原则就是 立刻完成功能后立刻重构

  • 别过一个小时
  • 别过一个星期后重构
  • 立刻!

重构的目的就是

  • 移除重复,三则重构

dialog.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import React, { Fragment, ReactElement, ReactNode, ReactFragment } from 'react';
import ReactDOM from 'react-dom';
import './dialog.scss';
import {Icon} from '../index';
import {scopedClassMaker} from '../classes';

interface Props {
visible:boolean;
buttons?:Array<ReactElement>;
onClose:React.MouseEventHandler;
closeOnClickMask?:boolean;
}

const scopedClass = scopedClassMaker('fui-dialog');
const sc = scopedClass;




const Dialog:React.FunctionComponent<Props> = (props) => {
const onClickClose:React.MouseEventHandler = (e)=>{
props.onClose(e);
}
const onClickMask:React.MouseEventHandler = (e)=>{
if(props.closeOnClickMask){
props.onClose(e);
}
}
const x = props.visible ?
<Fragment>
<div className={sc('mask')} onClick={onClickMask}></div>
<div className={sc()}>
<div className={sc('close')} onClick={onClickClose}>
<Icon name="close"/>
</div>
<header className={sc('header')}>提示</header>
<main className={sc('main')}>
{props.children}
</main>
{
props.buttons && props.buttons.length > 0 &&
<footer className={sc('footer')}>
{props.buttons && props.buttons.map((button,index) =>
React.cloneElement(button,{key:index})
)}
</footer>
}

</div>
</Fragment>
:
null

/*
直接 {props.buttons} 会报错 因为 buttons 是数组
需要一个key
*/
return (
ReactDOM.createPortal(x,document.body)
);
}
// 设置默认 props
Dialog.defaultProps = {
closeOnClickMask : false
}

const alert = (content:string) =>{
const onClose = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
ReactDOM.unmountComponentAtNode(div);
div.remove();
}
const component =
<Dialog
visible={true}
onClose={onClose}
buttons={[ <button onClick={onClose}>OK</button>]}
>
{content}
</Dialog>;
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
}


const confirm = (content:string,yes:()=>void,no:()=>void) =>{
const onClose = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
ReactDOM.unmountComponentAtNode(div);
div.remove();
}
const onYes = () => {
onClose()
yes && yes();
};
const onNo = () => {
onClose()
no && no();
};
const component = (
<Dialog
visible={true}
onClose={onNo}
buttons={[
<button onClick={onYes}>yes</button>,
<button onClick={onNo}>no</button>
]}
>
{content}
</Dialog>);
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
}

const model = (content: ReactNode | ReactFragment) =>{
const onClose = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
// 从div上卸载 dialog
ReactDOM.unmountComponentAtNode(div);
// 删除div
div.remove();
}
const component = (
<Dialog visible={true}
onClose={onClose}
>
{content}
</Dialog>
);
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);

return onClose;
}

export default Dialog;
export {alert, confirm , model };

提取公共

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const model = (content: ReactNode , buttons?: Array<ReactElement>,afterClose?:() => void) => {
const close = () => {
ReactDOM.render(React.cloneElement(component,{visable:false}),div);
ReactDOM.unmountComponentAtNode(div);
div.remove();
console.log('close')
}
const component =
<Dialog
visible={true}
onClose={()=>{
close();
afterClose && afterClose();
}}
buttons={buttons}
>
{content}
</Dialog>;
const div = document.createElement('div');
document.body.append(div);
ReactDOM.render(component,div);
return close;
}

const alert = (content:string) =>{
const button = <button onClick={() => close()}>OK</button>;
const close = model(content,[button])
}


const confirm = (content:string,yes?:() => void,no?:() => void) =>{
const onYes = () => {
close()
yes && yes();
};
const onNo = () => {
close()
no && no();
};
const buttons = [
<button onClick={onYes}>yes</button>,
<button onClick={onNo}>no</button>
]
const close = model(content,buttons, no);
}

总结

  • scopedClass 高阶函数
  • Fragment 可以不生成多余的节点
  • react portal 传送门

    1
    ReactDOM.createPortal(渲染元素 , document.body)
  • 动态生成组件 参考 alert 实现

  • 闭包传 API 如 close

代码连接

react-gulu-test-4