ReactWheels09-layout组件

参考 ant-design的 Layout组件

如何给 Layout 传递 style呢?

  • CSSProperties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Layout style={{height:500}}></Layout>


import React, { CSSProperties } from 'react';

interface Props {
style: CSSProperties;
}
const Layout: React.FunctionComponent = (props) => {
return (
<div className={sc()}>
...
</div>
)
}
  • 继承 React.HTMLAttributes
1
2
3
4
5
6
7
8
interface Props extends React.HTMLAttributes<HTMLElement>{
}

const Layout: React.FunctionComponent<Props> = (props) => {
return (
...
)
}

解决className 覆盖问题

如果直接组件上...props 导致我们组件的className 被覆盖

1
2
3
4
5
6
7
8
9
const Layout: React.FunctionComponent<Props> = (props) => {
// rest 代表剩余的属性值,这样就不会把我们自己的 className 覆盖了
const {className, ...rest} = props;
return (
<div className={[sc(),className].join(' ')} {...rest}>
{props.children}
</div>
)
}

基本实现

layout.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import {scopedClassMaker} from '../classes';
import './layout.scss'

const sc = scopedClassMaker('fui-layout')

interface Props extends React.HTMLAttributes<HTMLElement>{
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
return (
<div className={sc('',{extra:className})} {...rest}>
{props.children}
</div>
)
}

export default Layout;

Temp.tsx 内容替换为 [header|footer|content|aside] 就是对应的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import {scopedClassMaker} from '../classes';

const sc = scopedClassMaker('fui-layout')

interface Props extends React.HTMLAttributes<HTMLElement>{
}

const Temp: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
return (
<div className={sc('temp',{extra:className})} {...rest}>
{props.children}
</div>
)
}

export default Temp;

layout.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@import "../helper";
.fui-layout{
border:1px solid red;
display: flex;
flex-direction: column;
&-content{
flex-grow:1;
border:1px solid green;
}
// layout 里的 layout
& &{
flex-grow: 1;
border:1px solid blue;
flex-direction: row;
}
}

遇到问题 就是 菜单单独在左侧,右侧上中下结构,我们的布局出新问题了

  • 尝试获取 layout 的 children 来改className 的方式。
  • 限制用户传递的 children 必须是元素 而不是字符串
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
interface Props extends React.HTMLAttributes<HTMLElement>{
// 限制我们的组件的 children 不能是一个字符串,必须是一个元素
children: ReactElement | Array<ReactElement>
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
// 报错 因为它不一定是数组
// if(props.children.length){}


// 只能通过断言 来绕过检查
// 但是这样就导致 容易写出垃圾代码 ,只是让你写的时候思考 该不该这样写
if((props.children as Array<ReactElement>).length){
// 此时还需要继续断言
(props.children as Array<ReactElement>).map(node => {
console.log(node);
})
}
return (
<div className={sc('',{extra:className})} {...rest}>
{props.children}
</div>
)
}

export default Layout;

通过判断 Layout 里是否有 Aside 来添加 hasAside

layout.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
import React, { ReactElement } from 'react';
import {scopedClassMaker} from '../classes';
import './layout.scss';
import Aside from './aside';

const sc = scopedClassMaker('fui-layout')

interface Props extends React.HTMLAttributes<HTMLElement>{
// 限制我们的组件的 children 不能是一个字符串,必须是一个元素
children: ReactElement | Array<ReactElement>
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;
// 报错 因为它不一定是数组
// if(props.children.length){}

// 只能通过断言 来绕过检查
// 但是这样就导致 容易写出垃圾代码 ,只是让你写的时候思考 该不该这样写
let hasAside = false;
if((props.children as Array<ReactElement>).length){
(props.children as Array<ReactElement>).map(node => {
console.log(node);
// 一旦发现有 aside 就附加一个类 hasAside
if(node.type === Aside){
hasAside = true;
}
})
}
return (
<div className={sc('',{extra:[className, hasAside && 'hasAside'].join(' ')})} {...rest}>
{props.children}
</div>
)
}

export default Layout;

layout.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@import "../helper";
.fui-layout{
border:1px solid red;
display: flex;
flex-direction: column;
/* 如果Layout 含有Aside 则它为row 同时它里面的 Layout 还是 column*/
&.hasAside{
flex-direction: row;
.fui-layout{
flex-direction: column;
}
}
&-content{
flex-grow:1;
border:1px solid green;
}
// layout 里的 layout
& &{
flex-grow: 1;
border:1px solid blue;
flex-direction: row;
}
}

消除代码里的 let 因为它违反函数式

  • 我们用一个boolean值 存这个 hasAside
1
2
3
4
5
6
7
8
9
10
11
if((props.children as Array<ReactElement>).length){
const hasAside =(props.children as Array<ReactElement>)
.reduce((result,node) => result || node.type === Aside,false);
}
return (
<div className={sc('',{extra:[className, hasAside && 'hasAside'].join(' ')})} {...rest}>
{props.children}
</div>
)

// 此时的问题是 hasAside 无法在 return 里用

改写完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Props extends React.HTMLAttributes<HTMLElement>{
// 限制我们的组件的 children 不能是一个字符串,必须是一个元素
children: ReactElement | Array<ReactElement>
}

const Layout: React.FunctionComponent<Props> = (props) => {
const {className, ...rest} = props;

const children = props.children as Array<ReactElement>
const hasAside = children.length &&
children.reduce((result,node) => result || node.type === Aside,false);
return (
<div className={sc('',{extra:[className, hasAside && 'hasAside'].join(' ')})} {...rest}>
{props.children}
</div>
);
}

export default Layout;

Object.entries()

1
2
3
4
5
6
7
Object.entries({a:1,c:2,b:3})
返回
[
["a",1],
["c":2],
["b":3]
]

重构 scopedClassMaker

  • classes.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
function classes(...names: (string | undefined)[]) {
return names.filter(Boolean).join(' ');
}

export default classes;

interface Options {
extra: string | undefined
}

/*
{
hasAside:true,
clearfix:false,
mt20:true
}
*/
interface ClassToggles {
[K: string]: boolean
}

function scopedClassMaker(prefix: string){
return function (name?: string | ClassToggles,options?: Options){
let name2;
let result;

if( typeof name === 'string' || name === undefined){
name2 = name;
result = [prefix,name2].filter(Boolean).join('-');
}else{
// ['hasAside','x']
name2 = Object.entries(name).filter(kv => kv[1]).map(kv => kv[0]);
result = name2.map( n =>{
return [prefix,n].filter(Boolean).join('-')
}).join(' ')
// ['fui-layout-hasAside', 'fui-layout-x']
}
if(options && options.extra){
return [ result, options && options.extra].filter(Boolean).join(' ');
}else{
return result;
}
}
}

export {scopedClassMaker};

再次重构它——函数式

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
interface ClassToggles {
[K: string]: boolean
}

function scopedClassMaker(prefix: string){

return function (name: string | ClassToggles , options?: Options){
const namesObject = (typeof name === 'string' || name === undefined) ?
{[name]:name} :
name;
const scoped = Object
.entries(namesObject)
.filter(kv => kv[1] !==false)
.map(kv => kv[0])
.map(name => [prefix, name]
.filter(Boolean)
.join('-')
).join(' ');
if(options && options.extra){
return [ scoped, options && options.extra].filter(Boolean).join(' ');
}else{
return scoped;
}
}
}

将函数式进行到底

1
2
3
4
5
6
7
8
9
10
11
const scopedClassMaker = (prefix: string) =>
(name: string | ClassToggles , options?: Options) =>
Object
.entries(name instanceof Object ? name : {[name]:name})
.filter(kv => kv[1] !==false)
.map(kv => kv[0])
.map(name => [prefix, name]
.filter(Boolean)
.join('-'))
.concat( options && options.extra || [])
.join(' ');

测试 scopedClassMaker

1
2
3
4
5
6
7
8
9
10
11
describe('scopedClassMaker', () => {
it('接收字符串 或 对象', () => {
const sc = scopedClassMaker('fui-layout')
expect(sc('')).toEqual('fui-layout')
expect(sc('x')).toEqual('fui-layout-x')
expect(sc({y:true,z:false})).toEqual('fui-layout-y')
expect(sc({y:true,z:true})).toEqual('fui-layout-y fui-layout-z')
expect(sc({y:true,z:true},{extra:'red'})).toEqual('fui-layout-y fui-layout-z red')
})

})

代码连接

react-gulu-test-4