分享日期: 2022-11-08
分享内容: 组件不是 React 特有的概念,但是 React 将组件化的思想发扬光大,可谓用到了极致。良好的组件设计会是良好的应用开发基础,这一讲就让我们谈一谈React工
程化开发实践中用到的一些UI组件库,并 结合企企项目中各位大佬们设计的组 件,更深入的了解组件设计思想,以便更好的在项目业务各类场景中应用。
分享主题由来:

一、常见React·UI库

如果您不想从头开始构建所有必要的 React UI 组件,您可以选择 React UI Library 来完成这项工作。所有这些都有一些基本的组件,比如按钮,下拉菜单,对话框和列表。有很多 UI 库可供 React 选择:

PC端UI库

移动端UI库

FAQ 常用的HTML标签有哪些? 行内元素和块级元素有什么区别?

二、企企React·UI库

/front-theory/packages/kaleido/packages/uikit/athena-ui (提供了57个UI库)

  • AdvanceTree
  • ag-grid
  • Alert
  • Athena
  • Button
  • Calendar
  • Check
  • Checkbox
  • DatePicker
  • Dialog
  • Drawer
  • FocusTrapZone
  • FocusZone
  • FormLayout
  • Grid
  • GridLayout
  • GroupedList
  • Image
  • Input
  • Label
  • LayoutGroup
  • List
  • ListView
  • Menu
  • MouseWheelListener
  • NavBar
  • NonIdealState
  • NxTree
  • Overlay
  • PageLayout
  • Popover
  • ProgressBar
  • QwertZone
  • Radio
  • react
  • react-cool-virtual
  • ReactOverflowList
  • ReactTree
  • ScrollablePane
  • Scrollbars
  • SearchBox
  • Select
  • Spinner
  • SplitterLayout
  • StatisticalReport
  • Sticky
  • SvgIcon
  • Switch
  • Tabs
  • Text
  • TimeInput
  • Toaster
  • Tooltip
  • TooltipElement
  • Tree
  • Viewer
  • WindowSizeListener
1. Alert 警告

Alerts notify users of important information and force them to acknowledge the alert content before continuing.

Although similar to dialogs, alerts are more restrictive and should only be used for important information. By default, the user can only exit the alert by clicking one of the confirmation buttons—clicking the overlay or pressing the esc key will not close the alert. These interactions can be enabled via props.

import * as React from 'react';
import { Alert } from '@athena-ui/components/Alert' constructor(props) {
super(props); this.state = {
canEscapeKeyCancel: false,
canOutsideClickCancel: false,
isOpen: false,
isOpenError: false,
}; this.handleEscapeKeyChange = handleBooleanChange(canEscapeKeyCancel => this.setState({ canEscapeKeyCancel }));
this.handleOutsideClickChange = handleBooleanChange(click => this.setState({ canOutsideClickCancel: click }));
this.handleErrorOpen = () => this.setState({ isOpenError: true });
this.handleErrorClose = () => this.setState({ isOpenError: false }); this.handleMoveOpen = () => this.setState({ isOpen: true });
this.handleMoveConfirm = () => {
this.setState({ isOpen: false });
};
this.handleMoveCancel = () => this.setState({ isOpen: false });
} render() {
const { isOpen, isOpenError, ...alertProps } = this.state;
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch
checked={this.state.canEscapeKeyCancel}
label="Can escape key cancel"
onChange={this.handleEscapeKeyChange}
/>
<Switch
checked={this.state.canOutsideClickCancel}
label="Can outside click cancel"
onChange={this.handleOutsideClickChange}
/>
</React.Fragment>
);
return (
<Example options={options} {...this.props} className="docs-example-frame-row">
<Button onClick={this.handleErrorOpen} text="Open file error alert" />
<Alert
{...alertProps}
confirmButtonText="Okay"
isOpen={isOpenError}
onClose={this.handleErrorClose}
>
<p>
Couldn't create the file because the containing folder doesn't exist anymore. You will be
redirected to your user folder.
</p>
</Alert> <Button onClick={this.handleMoveOpen} text="Open file deletion alert" />
<Alert
{...alertProps}
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={this.handleMoveCancel}
onConfirm={this.handleMoveConfirm}
>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</Example>
);
}
2. Button 按钮
2.1 基础用法

基础的按钮用法。

Button 组件默认提供7种主题,由color属性来定义,默认为default

render() {
return (
<React.Fragment>
<div className="at-row">
<Button intent="default">默认按钮</Button>
<Button intent="primary">主要按钮</Button>
<Button intent="success">成功按钮</Button>
<Button intent="info">信息按钮</Button>
<Button intent="warning">警告按钮</Button>
<Button intent="danger">危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" minimal>朴素按钮</Button>
<Button intent="primary" minimal>主要按钮</Button>
<Button intent="success" minimal>成功按钮</Button>
<Button intent="info" minimal>信息按钮</Button>
<Button intent="warning" minimal>警告按钮</Button>
<Button intent="danger" minimal>危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" outline>Outline按钮</Button>
<Button intent="primary" outline>主要按钮</Button>
<Button intent="success" outline>成功按钮</Button>
<Button intent="info" outline>信息按钮</Button>
<Button intent="warning" outline>警告按钮</Button>
<Button intent="danger" outline>危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" round>圆角按钮</Button>
<Button intent="primary" round>主要按钮</Button>
<Button intent="success" round>成功按钮</Button>
<Button intent="info" round>信息按钮</Button>
<Button intent="warning" round>警告按钮</Button>
<Button intent="danger" round>危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" icon="search" circle></Button>
<Button intent="primary" icon="edit" circle></Button>
<Button intent="success" icon="help" circle></Button>
<Button intent="info" icon="repeat" circle></Button>
<Button intent="warning" icon="star" circle></Button>
<Button intent="danger" icon="delete" circle></Button>
</div>
</React.Fragment>
)
}
2.2 禁用状态

按钮不可用状态。

你可以使用disabled属性来定义按钮是否可用,它接受一个Boolean值。

render() {
return (
<React.Fragment>
<div className="at-row">
<Button intent="default" disabled>默认按钮</Button>
<Button intent="primary" disabled>主要按钮</Button>
<Button intent="success" disabled>成功按钮</Button>
<Button intent="info" disabled>信息按钮</Button>
<Button intent="warning" disabled>警告按钮</Button>
<Button intent="danger" disabled>危险按钮</Button>
</div> <div className="at-row">
<Button intent="default" minimal disabled>朴素按钮</Button>
<Button intent="primary" minimal disabled>主要按钮</Button>
<Button intent="success" minimal disabled>成功按钮</Button>
<Button intent="info" minimal disabled>信息按钮</Button>
<Button intent="warning" minimal disabled>警告按钮</Button>
<Button intent="danger" minimal disabled>危险按钮</Button>
</div>
</React.Fragment>
)
}
2.3 文字按钮

没有边框和背景色的按钮。

render() {
return (
<div>
<Button intent="primary" link>文字按钮</Button>
<Button intent="success" link disabled>文字按钮</Button>
</div>
)
}
2.4 图标按钮

带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。

设置icon属性即可,也可以设置在文字右边的 icon。

render() {
return (
<div style={{display: "flex"}}>
<Button intent="primary" icon="edit"></Button>
<Button intent="primary" icon="share"></Button>
<Button intent="primary" icon="delete"></Button>
<Button intent="primary" icon="search">搜索</Button>
<Button intent="primary" rightIcon="upload">上传</Button>
</div>
)
}
2.5 按钮组

以按钮组的方式出现,常用于多项类似操作。

使用Button.Group标签来嵌套你的按钮。

render() {
return (
<div>
<Button.Group>
<Button intent="primary" icon="at-icon-arrow-left">上一页</Button>
<Button intent="primary">下一页<i className="el-icon-arrow-right el-icon-right"></i></Button>
</Button.Group> <Button.Group>
<Button intent="primary" icon="at-icon-edit"></Button>
<Button intent="primary" icon="at-icon-share"></Button>
<Button intent="primary" icon="at-icon-delete"></Button>
</Button.Group>
</div>
)
}
2.6 加载中

点击按钮后进行数据加载操作,在按钮上显示加载状态。

要设置为 loading 状态,只要设置loading属性为true即可。

render() {
return <Button intent="primary" loading={true}>加载中</Button>
}
2.7 不同尺寸

Button 组件提供除了默认值以外的三种尺寸,可以在不同场景下选择合适的按钮尺寸。

额外的尺寸:largesmallmini,通过设置size属性来配置它们。

render() {
return (
<div>
<div className="at-row">
<Button>默认按钮</Button>
<Button large>中等按钮</Button>
<Button small>小型按钮</Button>
</div>
<div className="at-row">
<Button round>默认按钮</Button>
<Button large round>中等按钮</Button>
<Button small round>小型按钮</Button>
</div>
</div>
)
}
2.8 组合按钮

CompoundButton 可以对按钮的功能提供一行描述信息。

通过设置 secondaryText 和 text。

render() {
return (
<div>
<div className="at-row">
<CompoundButton intent="info" secondaryText="You can create a new account here." text="Hi!" loading></CompoundButton>
</div>
</div>
)
}
2.9 Split Button

按钮包含下拉菜单

render() {
return (
<div>
<div className="at-row">
<Button
intent="primary"
text="新建"
menuProps={{
items: [
{
key: 'emailMessage',
text: 'Email message',
icon: 'envelope',
},
{
key: 'calendarEvent',
text: 'Calendar event',
icon: 'timeline-events',
}
],
directionalHintFixed: true
}}
></Button>
</div>
</div>
)
}
2.10 属性Attributes

参数

说明

类型

可选值

默认值

size

尺寸

string

large,small,mini

intent

类型

string

primary,success,warning,danger,info,text

minimal

是否朴素按钮

Boolean

true,false

false

loading

是否加载中状态

Boolean

false

disabled

禁用

boolean

true, false

false

icon

图标,已有的图标库中的图标名

string

nativeType

原生 type 属性

string

button,submit,reset

button

3. ButtonGroup 按钮组

Button groups arrange multiple buttons in a horizontal or vertical group.

3.1 基础用法
constructor(props) {
super(props); this.state = {
alignText: Alignment.CENTER,
fill: false,
iconOnly: false,
large: false,
minimal: false,
vertical: false,
}; this.handleFillChange = handleBooleanChange(fill => this.setState({ fill }));
this.handleIconOnlyChange = handleBooleanChange(iconOnly => this.setState({ iconOnly }));
this.handleLargeChange = handleBooleanChange(large => this.setState({ large }));
this.handleMinimalChange = handleBooleanChange(minimal => this.setState({ minimal }));
this.handleVerticalChange = handleBooleanChange(vertical => this.setState({ vertical }));
this.handleAlignChange = (alignText) => this.setState({ alignText });
}
render() {
const { iconOnly, ...bgProps } = this.state;
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch checked={this.state.fill} label="Fill" onChange={this.handleFillChange} />
<Switch checked={this.state.large} label="Large" onChange={this.handleLargeChange} />
<Switch checked={this.state.minimal} label="Minimal" onChange={this.handleMinimalChange} />
<Switch checked={this.state.vertical} label="Vertical" onChange={this.handleVerticalChange} />
<AlignmentSelect align={this.state.alignText} onChange={this.handleAlignChange} />
<H5>Example</H5>
<Switch checked={this.state.iconOnly} label="Icons only" onChange={this.handleIconOnlyChange} />
</React.Fragment>
); return (
<Example options={options} {...this.props}>
<ButtonGroup style={{ minWidth: 200 }} {...bgProps}>
<Button icon="database">{!iconOnly && "Queries"}</Button>
<Button icon="function">{!iconOnly && "Functions"}</Button>
<Button icon="cog" rightIcon="settings">
{!iconOnly && "Options"}
</Button>
<Button
intent="primary"
text={!iconOnly && "Create account"}
split
menuProps={{
items: [
{
key: 'emailMessage',
text: 'Email message',
},
{
key: 'calendarEvent',
text: 'Calendar event',
}
],
directionalHintFixed: true
}}
/>
</ButtonGroup>
</Example>
);
}
3.2 和popovers搭配使用

Buttons inside a ButtonGroup can trivially be wrapped with a Popover to create complex toolbars.

constructor(props) {
super(props); this.state = {
alignText: Alignment.CENTER,
large: false,
minimal: false,
vertical: false,
}; this.handleLargeChange = handleBooleanChange(large => this.setState({ large }));
this.handleMinimalChange = handleBooleanChange(minimal => this.setState({ minimal }));
this.handleVerticalChange = handleBooleanChange(vertical => this.setState({ vertical }));
this.handleAlignChange = (alignText) => this.setState({ alignText });
} render() {
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch label="Large" checked={this.state.large} onChange={this.handleLargeChange} />
<Switch label="Minimal" checked={this.state.minimal} onChange={this.handleMinimalChange} />
<Switch label="Vertical" checked={this.state.vertical} onChange={this.handleVerticalChange} />
<AlignmentSelect align={this.state.alignText} onChange={this.handleAlignChange} />
</React.Fragment>
);
return (
<Example options={options} {...this.props}>
<ButtonGroup {...this.state} style={{ minWidth: 120 }}>
{this.renderButton("File", "document")}
{this.renderButton("Edit", "edit")}
{this.renderButton("View", "eye-open")}
</ButtonGroup>
</Example>
);
} renderButton(text, iconName) {
const { vertical } = this.state;
const rightIconName: IconName = vertical ? "caret-right" : "caret-down";
const position = vertical ? Position.RIGHT_TOP : Position.BOTTOM_LEFT;
return (
<Popover content={<FileMenu />} position={position}>
<Button rightIcon={rightIconName} icon={iconName} text={text} />
</Popover>
);
}
4. Calendar 日历
4.1 default calendar
constructor(props) {
super(props); this.state = {
selectedDate: null,
}; this._onSelectDate = this._onSelectDate.bind(this);
} render() {
const divStyle = {
height: '340px'
}; const buttonStyle = {
margin: '17px 10px 0 0'
}; let dateRangeString = null; if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
} return (
<Example {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<Calendar
isMonthPickerVisible={false}
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
</div>
</Example>
);
} _onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
});
}
4.2 单击标题时带有重叠月份选择器内联日历

通过设置 showMonthPickerAsOverlaytrue 可以点击抬头区的月来选择月份。

constructor(props) {
super(props); this.state = {
selectedDate: null,
}; this._onSelectDate = this._onSelectDate.bind(this);
} render() {
const divStyle = {
height: '340px'
}; const buttonStyle = {
margin: '17px 10px 0 0'
}; let dateRangeString = null; if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
} return (
<Example {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<Calendar
isMonthPickerVisible={false}
showMonthPickerAsOverlay
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
</div>
</Example>
);
} _onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
});
}
4.3 带月份选择器的内联日历

通过设置 isMonthPickerVisibletrue 可以点击抬头区的月来选择月份。

constructor(props) {
super(props); this.state = {
selectedDate: null,
}; this._onSelectDate = this._onSelectDate.bind(this);
} render() {
const divStyle = {
height: '340px'
}; const buttonStyle = {
margin: '17px 10px 0 0'
}; let dateRangeString = null; if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
} return (
<Example {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<Calendar
isMonthPickerVisible
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
</div>
</Example>
);
} _onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
});
}
4.4 带周选择的内联日历

通过设置 dateRangeType 可以按特定类型选择一个区间的日期。dateRangeType 可以选择的类型有:Day, Week, Month, WorkWeek

constructor(props) {
super(props); this.state = {
selectedDate: null,
selectedDateRange: null,
dateRangeType: DateRangeType.Week,
}; this.DATE_RANGE_TYPES = [
{label: 'Day', value: DateRangeType.Day},
{label: 'Week', value: DateRangeType.Week},
{label: 'Month', value: DateRangeType.Month},
{label: 'WorkWeek', value: DateRangeType.WorkWeek},
] this._onSelectDate = this._onSelectDate.bind(this);
this._goNext = this._goNext.bind(this);
this._goPrevious = this._goPrevious.bind(this);
this._handleDateRangeTypeChange = this._handleDateRangeTypeChange.bind(this);
} render() {
const divStyle = {
height: '340px'
}; const buttonStyle = {
margin: '17px 10px 0 0'
}; let dateRangeString = null; if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
} return (
<Example options={this._renderOptions()} {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<div>
选择的日期范围:
<span> {!dateRangeString ? '无' : dateRangeString}</span>
</div>
<Calendar
isMonthPickerVisible
dateRangeType={this.state.dateRangeType}
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
<div>
<Button style={buttonStyle} onClick={this._goPrevious} text="上一区间" />
<Button style={buttonStyle} onClick={this._goNext} text="下一区间" />
</div>
</div>
</Example>
);
} _onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
selectedDateRange: dateRangeArray
});
} _goPrevious() {
this.setState((prevState) => {
const selectedDate = prevState.selectedDate || new Date();
const dateRangeArray = this.props.getDateRangeArray(selectedDate, this.state.dateRangeType, DayOfWeek.Sunday); let subtractFrom = dateRangeArray[0];
let daysToSubtract = dateRangeArray.length; if (this.state.dateRangeType === DateRangeType.Month) {
subtractFrom = new Date(subtractFrom.getFullYear(), subtractFrom.getMonth(), 1);
daysToSubtract = 1;
} const newSelectedDate = this.props.addDays(subtractFrom, -daysToSubtract); return {
selectedDate: newSelectedDate
};
});
} _goNext() {
this.setState((prevState) => {
const selectedDate = prevState.selectedDate || new Date();
const dateRangeArray = this.props.getDateRangeArray(selectedDate, this.state.dateRangeType, DayOfWeek.Sunday);
const newSelectedDate = this.props.addDays(dateRangeArray.pop(), 1); return {
selectedDate: newSelectedDate
};
});
} _handleDateRangeTypeChange(evt) {
this.setState({
dateRangeType: parseInt(evt.target.value)
})
} _renderOptions() {
const { dateRangeType } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<Label>
Position
<HTMLSelect value={dateRangeType} onChange={this._handleDateRangeTypeChange} options={this.DATE_RANGE_TYPES} />
</Label>
</React.Fragment>
);
}
5. Checkbox 多选框

一组备选项中进行多选。

5.1 基本使用方法

单独使用可以表示两种状态之间的切换,写在标签中的内容为 checkbox 按钮后的介绍。

render() {
return (
<Example alignLeft compact {...this.props}>
<div>
<Checkbox label="Gilad Gray" defaultIndeterminate={true} />
<Checkbox label="Jason Killian" />
<Checkbox label="Antoine Llorca" />
<Checkbox>
<SvgIcon use="#user" color="success" size={16} />
<span style={{marginRight: 8}}></span>
Gilad <strong>Gray</strong>
</Checkbox>
</div>
</Example>
)
}
5.2 禁用状态
render() {
return (
<Example alignLeft {...this.props}>
<div>
<Checkbox disabled>备选项</Checkbox>
<Checkbox checked={true} disabled>选中且禁用</Checkbox>
</div>
</Example>
)
}
5.3 多选框组

适用于在多个互斥的选项中选择的场景

constructor(props) {
super(props); this.state = {
mealTypes: ["one", "three"]
}
} handleMealChange(values) {
this.setState({ mealTypes: values });
}; render() {
return (
<Example alignLeft {...this.props}>
<div>
<CheckboxGroup
label="Meal Choice"
onValuesChange={this.handleMealChange.bind(this)}
selectedValues={this.state.mealTypes}
>
<Checkbox label="Soup" value="one" />
<Checkbox label="Salad" value="two" />
<Checkbox label="Sandwich" value="three" />
<Checkbox label="Franchy" value="four" disabled />
</CheckboxGroup>
</div>
</Example>
)
}
5.3 自定义状态图标
render() {
return (
<Example alignLeft {...this.props}>
<Checkbox checkedIcon="heart" unCheckedIcon="heart-broken">备选项</Checkbox>
</Example>
)
}
6. Collapse 折叠面板

The Collapse element shows and hides content with a built-in slide in/out animation. You might use this to create a panel of settings for your application, with sub-sections that can be expanded and collapsed.

constructor(props) {
super(props); this.state = {
isOpen: false,
keepChildrenMounted: false,
}; this.handleChildrenMountedChange = handleBooleanChange(keepChildrenMounted => {
this.setState({ keepChildrenMounted });
});
this.handleClick = () => this.setState({ isOpen: !this.state.isOpen });
} render() {
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch
checked={this.state.keepChildrenMounted}
label="Keep children mounted"
onChange={this.handleChildrenMountedChange}
/>
</React.Fragment>
); return (
<Example options={options} {...this.props}>
<div style={{ width: "100%" }}>
<Button onClick={this.handleClick}>{this.state.isOpen ? "Hide" : "Show"} build logs</Button>
<Collapse isOpen={this.state.isOpen} keepChildrenMounted={this.state.keepChildrenMounted}>
<Pre>
[11:53:30] Finished 'typescript-bundle-blueprint' after 769 ms<br />
[11:53:30] Starting 'typescript-typings-blueprint'...<br />
[11:53:30] Finished 'typescript-typings-blueprint' after 198 ms<br />
[11:53:30] write ./blueprint.css<br />
[11:53:30] Finished 'sass-compile-blueprint' after 2.84 s
</Pre>
</Collapse>
</div>
</Example>
);
}
7. DatePicker 日期输入框
7.1 基础用法
constructor(props) {
super(props); this.state = {
firstDayOfWeek: DayOfWeek.Sunday
};
} render() {
const { firstDayOfWeek } = this.state; return (
<Example {...this.props}>
<DatePicker
firstDayOfWeek={firstDayOfWeek}
placeholder="Select a date..."
onAfterMenuDismiss={() => console.log('onAfterMenuDismiss called')}
/>
</Example>
);
}
7.2 DatePicker允许输入日期字符串

Text input allowed by default when use keyboard navigation. Mouse click the TextField will popup DatePicker, click the TextField again will dismiss the DatePicker and allow text input.

constructor(props) {
super(props); this.state = {
};
this._onSelectDate = this._onSelectDate.bind(this);
this._onClick = this._onClick.bind(this);
} render() {
const { value } = this.state; return (
<Example {...this.props}>
<DatePicker
allowTextInput
placeholder="Select a date..."
value={value}
onSelectDate={this._onSelectDate}
/>
<Button onClick={this._onClick} text="清除" />
</Example>
);
} _onSelectDate(date) {
this.setState({ value: date });
} _onClick() {
this.setState({ value: null });
}
7.3 DatePicker允许格式化日期

Applications can customize how dates are formatted and parsed. Formatted dates can be ambiguous, so the control will avoid parsing the formatted strings of dates selected using the UI when text input is allowed. In this example, we are formatting and parsing dates as dd/MM/yy.

constructor(props) {
super(props); this.state = {
};
this._onSelectDate = this._onSelectDate.bind(this);
this._onClick = this._onClick.bind(this);
} render() {
const { value } = this.state; return (
<Example {...this.props}>
<DatePicker
allowTextInput
placeholder="Select a date..."
formatDate='DD/MM/YY'
value={value}
onSelectDate={this._onSelectDate}
/>
<Button onClick={this._onClick} text="清除" />
</Example>
);
} _onSelectDate(date) {
this.setState({ value: date });
} _onClick() {
this.setState({ value: null });
}
7.4 带日期边界的DatePicker(最小日期,最大日期)

When date boundaries are set (via minDate and maxDate props) the DatePicker will not allow out-of-bounds dates to be picked or entered. In this example, the allowed dates are 2018/6/30-2019/7/31

constructor(props) {
super(props);
} render() {
const today: Date = new Date(Date.now());
const minDate: Date = this.props.addMonths(today, -1);
const maxDate: Date = this.props.addYears(today, 1); return (
<Example {...this.props}>
<DatePicker
allowTextInput
placeholder="Select a date..."
minDate={minDate}
maxDate={maxDate}
/>
</Example>
);
}
8. Dialog 对话框
constructor(props) {
super(props);
this.handleOpen = this.handleOpen.bind(this);
this.handleClose = this.handleClose.bind(this); this.state = {
autoFocus: true,
canEscapeKeyClose: true,
canOutsideClickClose: true,
enforceFocus: true,
isOpen: false,
usePortal: true,
}
} handleOpen() {
this.setState({ isOpen: true });
}
handleClose() {
this.setState({ isOpen: false });
} render() {
return (
<Example options={this.renderOptions()}>
<Button onClick={this.handleOpen}>Show dialog</Button>
<Dialog
onClose={this.handleClose}
title="Palantir Foundry"
{...this.state}
>
<div className="bp3-dialog-body">
<p>
<strong>
Data integration is the seminal problem of the digital age. For over ten years, we’ve
helped the world’s premier organizations rise to the challenge.
</strong>
</p>
<p>
Palantir Foundry radically reimagines the way enterprises interact with data by amplifying
and extending the power of data integration. With Foundry, anyone can source, fuse, and
transform data into any shape they desire. Business analysts become data engineers — and
leaders in their organization’s data revolution.
</p>
<p>
Foundry’s back end includes a suite of best-in-class data integration capabilities: data
provenance, git-style versioning semantics, granular access controls, branching,
transformation authoring, and more. But these powers are not limited to the back-end IT
shop.
</p>
<p>
In Foundry, tables, applications, reports, presentations, and spreadsheets operate as data
integrations in their own right. Access controls, transformation logic, and data quality
flow from original data source to intermediate analysis to presentation in real time. Every
end product created in Foundry becomes a new data source that other users can build upon.
And the enterprise data foundation goes where the business drives it.
</p>
<p>Start the revolution. Unleash the power of data integration with Palantir Foundry.</p>
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
<Button onClick={this.handleClose}>Close</Button>
<Button
color='primary'
>
Visit the Foundry website
</Button>
</div>
</div>
</Dialog>
</Example>
)
} renderOptions() {
const { autoFocus, enforceFocus, canEscapeKeyClose, canOutsideClickClose, usePortal } = this.state; return (
<div>
<h5>Props</h5>
<Switch checked={autoFocus} label="Auto focus" onChange={(evt) => this.setState({autoFocus: evt.target.checked})} />
<Switch checked={enforceFocus} label="Enforce focus" onChange={(evt) => this.setState({enforceFocus: evt.target.checked})} />
<Switch checked={usePortal} onChange={(evt) => this.setState({usePortal: evt.target.checked})}>
Use <strong>Portal</strong>
</Switch>
<Switch
checked={canOutsideClickClose}
label="Click outside to close"
onChange={(evt) => this.setState({canOutsideClickClose: evt.target.checked})}
/>
<Switch checked={canEscapeKeyClose} label="Escape key to close" onChange={(evt) => this.setState({canEscapeKeyClose: evt.target.checked})} />
</div>
)
}
9. FocusZone 区域

FocusZones abstract arrow key navigation behaviors. Tabbable elements (buttons, anchors, and elements with data-is-focusable='true' attributes) are considered when pressing directional arrow keys and focus is moved appropriately. Tabbing to a zone sets focus only to the current "active" element, making it simple to use the tab key to transition from one zone to the next, rather than through every focusable element.

Using a FocusZone is simple. Just wrap a bunch of content inside of a FocusZone, and arrows and tabbling will be handled for you! See examples below.

constructor(props) {
super(props); this.PHOTOS = createArray(25, () => {
const randomWidth = 50 + Math.floor(Math.random() * 150); return {
url: `http://placehold.it/${randomWidth}x100`,
width: randomWidth,
height: 100
};
});
} render() {
const log = (): void => {
console.log('clicked');
}; return (
<FocusZone elementType="ul" className="at-FocusZoneExamples-photoList">
{this.PHOTOS.map((photo, index) => (
<li
key={index}
className="at-FocusZoneExamples-photoCell"
aria-posinset={index + 1}
aria-setsize={this.PHOTOS.length}
aria-label="Photo"
data-is-focusable={true}
onClick={log}
>
<Image src={photo.url} width={photo.width} height={photo.height} />
</li>
))}
</FocusZone>
)
}
10. Label 表单标签

表单字段的描述标签。

constructor(props) {
super(props); this.state = {
disabled: false,
helperText: false,
inline: false,
intent: Intent.NONE,
requiredLabel: true,
}; this.handleDisabledChange = handleBooleanChange(disabled => this.setState({ disabled }));
this.handleHelperTextChange = handleBooleanChange(helperText => this.setState({ helperText }));
this.handleInlineChange = handleBooleanChange(inline => this.setState({ inline }));
this.handleRequiredLabelChange = handleBooleanChange(requiredLabel => this.setState({ requiredLabel }));
this.handleIntentChange = handleStringChange((intent: Intent) => this.setState({ intent }));
} render() {
const { disabled, helperText, inline, intent, requiredLabel } = this.state; const options = (
<React.Fragment>
<H5>Props</H5>
<Switch label="Disabled" checked={disabled} onChange={this.handleDisabledChange} />
<Switch label="Inline" checked={inline} onChange={this.handleInlineChange} />
<Switch label="Show helper text" checked={helperText} onChange={this.handleHelperTextChange} />
<Switch label="Show label info" checked={requiredLabel} onChange={this.handleRequiredLabelChange} />
<IntentSelect intent={intent} onChange={this.handleIntentChange} />
</React.Fragment>
); return (
<Example options={options} {...this.props}>
<FormGroup
disabled={disabled}
helperText={helperText && "Helper text with details..."}
inline={inline}
intent={intent}
label="Label"
labelFor="text-input"
labelInfo={requiredLabel && "(required)"}
>
<Input id="text-input" placeholder="Placeholder text" disabled={disabled} intent={intent} />
</FormGroup>
<FormGroup
disabled={disabled}
helperText={helperText && "Helper text with details..."}
inline={inline}
intent={intent}
label="Label"
labelFor="text-input"
labelInfo={requiredLabel && "(required)"}
>
<Switch id="text-input" label="Engage the hyperdrive" disabled={disabled} />
<Switch id="text-input" label="Initiate thrusters" disabled={disabled} />
</FormGroup>
</Example>
);
}
11. Image 图像
11.1 ImageFit: Not specified
render() {
return (
<div>
<p>
With no imageFit property set, the width and height props control the size of the frame. Depending on which of
those props is used, the image may scale to fit the frame.
</p>
<p>
Without a width or height specified, the frame remains at its natural size and the image will not be scaled.
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and no height or width is specified."
/>
<br />
<p>
If only a width is provided, the frame will be set to that width. The image will scale proportionally to fill
the available width.
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and only width is specified."
width={600}
/>
<br />
<p>
If only a height is provided, the frame will be set to that height. The image will scale proportionally to
fill the available height.
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and only height is specified."
height={100}
/>
<br />
<p>
If both width and height are provided, the frame will be set to that width and height. The image will scale to
fill both the available width and height. <strong>This may result in a distorted image.</strong>
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and height or width is specified."
width={100}
height={100}
/>
</div>
);
}
11.2 ImageFit: None
render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500x250',
imageFit: ImageFit.none,
width: 350,
height: 150
}; return (
<div>
<p>
By setting the imageFit property to "none", the image will remain at its natural size, even if the frame is
made larger or smaller by setting the width and height props.
</p>
<p>
The image is larger than the frame, so it is cropped to fit. The image is positioned at the upper left of the
frame.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the none value on an image larger than the frame."
/>
<br />
<p>
The image is smaller than the frame, so there is empty space within the frame. The image is positioned at the
upper left of the frame.
</p>
<Image
{...imageProps}
src="http://placehold.it/100x100"
alt="Example implementation of the property image fit using the none value on an image smaller than the frame."
/>
</div>
);
}
11.3 ImageFit: Center
render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500x250',
imageFit: ImageFit.center,
width: 350,
height: 150
}; return (
<div>
<p>
Setting the imageFit property to "center" behaves the same as "none", while centering the image within the
frame.
</p>
<p>The image is larger than the frame, so all sides are cropped to center the image.</p>
<Image
{...imageProps}
src="http://placehold.it/800x300"
alt="Example implementation of the property image fit using the center value on an image larger than the frame."
/>
<br />
<p>
The image is smaller than the frame, so there is empty space within the frame. The image is centered in the
available space.
</p>
<Image
{...imageProps}
src="http://placehold.it/100x100"
alt="Example implementation of the property image fit using the center value on an image smaller than the frame."
/>
</div>
);
}
11.4 ImageFit: Contain
render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/700x300',
imageFit: ImageFit.contain,
}; return (
<div>
<p>
Setting the imageFit property to "contain" will scale the image up or down to fit the frame, while maintaining
its natural aspect ratio and without cropping the image.
</p>
<p>
The image has a wider aspect ratio (more landscape) than the frame, so the image is scaled to fit the width
and centered in the available vertical space.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the contain value on an image wider than the frame."
width={200}
height={200}
/>
<br />
<p>
The image has a taller aspect ratio (more portrait) than the frame, so the image is scaled to fit the height
and centered in the available horizontal space.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the contain value on an image taller than the frame."
width={300}
height={50}
/>
</div>
);
}
11.5 ImageFit: Cover
render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500X500',
imageFit: ImageFit.cover,
}; return (
<div>
<p>
Setting the imageFit property to "cover" will cause the image to scale up or down proportionally, while
cropping from either the top and bottom or sides to completely fill the frame.
</p>
<p>
The image has a wider aspect ratio (more landscape) than the frame, so the image is scaled to fit the height
and the sides are cropped evenly.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the cover value on an image wider than the frame."
width={150}
height={250}
/>
<br />
<p>
The image has a taller aspect ratio (more portrait) than the frame, so the image is scaled to fit the width
and the top and bottom are cropped evenly.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the cover value on an image taller than the frame."
width={250}
height={150}
/>
</div>
);
}
11.6 Maximizing the image frame
render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500x500',
imageFit: ImageFit.cover,
maximizeFrame: true
}; return (
<div>
<p>
Where the exact width and height of the image's frame is not known, such as when sizing an image as a
percentage of its parent, you can use the "maximizeFrame" prop to expand the frame to fill the parent element.
</p>
<p>The image is placed within a landscape container.</p>
<div style={{ width: '200px', height: '100px' }}>
<Image
{...imageProps}
alt="Example implementation of the property maximize frame with a landscape container."
/>
</div>
<br />
<p>The image is placed within a portrait container.</p>
<div style={{ width: '100px', height: '200px' }}>
<Image
{...imageProps}
alt="Example implementation of the property maximize frame with a portrait container"
/>
</div>
</div>
);
}
12. Input 输入框
12.1 基本使用方法
render() {
return [
<div className="at-row">
<Input placeholder="Text input"/>
<Input placeholder="Text input" disabled/>
<Input placeholder="Text input" readonly/>
</div>
,
<div className="at-row">
<Input placeholder="Text input" round/>
<Input placeholder="Text input" intent="primary"/>
<Input placeholder="Text input" intent="warning"/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="large and fill" large fill/>
</div>
]
}
12.2 带图标的输入框
render() {
return [
<div className="at-row">
<Input placeholder="Filter histogram..." leftElement="filter" />
<Input placeholder="Filter histogram..." leftElement="filter" disabled/>
<Input placeholder="Filter histogram..." leftElement="filter" round/>
</div>
,
<div className="at-row">
<Input placeholder="Enter your password..." rightElement="lock" intent="warning"/>
<Input placeholder="Enter your password..." rightElement="lock" disabled/>
<Input placeholder="Enter your password..." rightElement="lock" intent="warning" round/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="Filter histogram..." fill leftElement="filter" />
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="Enter your password..." fill rightElement="lock" intent="warning"/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="Search" fill leftElement="search" rightElement="arrow-right" intent="success"/>
</div>
]
}
12.3 多行文本输入框
render() {
return [
<div className="at-row">
<div>
<TextArea placeholder="Writing..." fill/>
</div>
<div>
<TextArea placeholder="Writing..." disabled fill/>
</div>
<div>
<TextArea placeholder="Writing..." intent="success" fill/>
</div>
</div>
,
<div className="at-row">
<div data-modifier=".pt-fill">
<TextArea placeholder="Writing..." rows={3} fill/>
</div>
</div>
,
<div className="at-row">
<div data-modifier=".pt-fill">
<TextArea placeholder="Writing..." fill autosize={{minRows: 2, maxRows: 8}}/>
</div>
</div>
]
}
13. List 列表
constructor(props) {
super(props); this.items = createArray(25, () => {
const randomWidth = 50 + Math.floor(Math.random() * 150); return {
url: `http://placehold.it/${randomWidth}x100`,
content: lorem(10),
width: randomWidth,
height: 100
};
});
} render() {
return (
<Example {...this.props}>
<div className="example-block is-scrollable">
<List
items={this.items}
onRenderCell={this._renderRow.bind(this)}
/>
</div>
</Example>
)
} _renderRow(item, index) {
return (
<div>
<p className="list-textContent">{item.content}</p>
<Image src={item.url} width={item.width} height={item.height} />
</div>
)
}
14. ListView 列表

ListViewList 的不同之处在于 ListView 是可交互的列表组件。

ListView 可以支持 Selection 进行单选或者多选,行可以有焦点。

ListView 组件同样是虚拟渲染方式,可以支持超大数据量的显示,同时也支持动态行高。

constructor(props) {
super(props); this.items = createArray(25, () => {
const randomWidth = 50 + Math.floor(Math.random() * 150); return {
url: `http://placehold.it/${randomWidth}x100`,
content: lorem(20),
width: randomWidth,
height: 100
};
});
} render() {
return (
<Example {...this.props}>
<div className="example-block is-scrollable">
<ListView
items={this.items}
onRenderItem={this._renderRow.bind(this)}
/>
</div>
</Example>
)
} _renderRow(item, index) {
return (
<div className="listItem">
<p className="list-textContent">{item.content}</p>
<Image src={item.url} width={item.width} height={item.height} />
</div>
)
}
15. Navbar 工具栏

由于选项默认可见,不宜过多,若选项过多,建议使用 Select 选择器。

render() {
return (
<div>
<Navbar>
<NavbarGroup align={Alignment.LEFT}>
<NavbarHeading>Athena</NavbarHeading>
<NavbarDivider />
<Button minimal icon="home" text="Home" />
<Button minimal icon="document" text="Files" />
</NavbarGroup>
</Navbar>
</div>
)
}
16. NumericInput 数字输入框
16.1 使用方法(Uncontrolled)
render() {
return [
<div className="at-row">
<NumericInput placeholder="Text input"/>
<NumericInput placeholder="Text input" disabled/>
<NumericInput placeholder="Text input" readonly/>
</div>
,
<div className="at-row">
<NumericInput placeholder="Text input" round/>
<NumericInput placeholder="Text input" intent="primary"/>
<NumericInput placeholder="Text input" intent="warning"/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<NumericInput placeholder="large and fill" large fill/>
</div>
]
}
16.2 使用方法(Controlled)
constructor(props) {
super(props);
this.state = {
value: 0,
}; this._onValueChanged = this._onValueChanged.bind(this);
} render() {
const { value } = this.state; return [
<div className="at-row">
<NumericInput placeholder="Text input" value={value} onValueChanged={this._onValueChanged}/>
</div>
]
} _onValueChanged(evt, value /* value is number type */) {
console.log(value);
this.setState({
value: value
})
}
17. Overlay 弹层
constructor(props) {
super(props); this.refHandlers = {
button: (ref) => (this.button = ref),
}; this.handleAutoFocusChange = handleBooleanChange(autoFocus => this.setState({ autoFocus }));
this.handleBackdropChange = handleBooleanChange(hasBackdrop => this.setState({ hasBackdrop }));
this.handleEnforceFocusChange = handleBooleanChange(enforceFocus => this.setState({ enforceFocus }));
this.handleEscapeKeyChange = handleBooleanChange(canEscapeKeyClose => this.setState({ canEscapeKeyClose }));
this.handleUsePortalChange = handleBooleanChange(usePortal => this.setState({ usePortal }));
this.handleOutsideClickChange = handleBooleanChange(val => this.setState({ canOutsideClickClose: val }));
this.handleOpen = () => this.setState({ isOpen: true });
this.handleClose = () => this.setState({ isOpen: false });
this.focusButton = () => this.button.focus(); this.state = {
autoFocus: true,
canEscapeKeyClose: true,
canOutsideClickClose: true,
enforceFocus: true,
hasBackdrop: true,
isOpen: false,
usePortal: true,
}; } render() {
const classes = classNames(Classes.CARD, Classes.ELEVATION_4, "docs-overlay-example-transition"); return (
<Example options={this.renderOptions()} {...this.props}>
<Button elementRef={this.refHandlers.button} onClick={this.handleOpen} text="Show overlay" />
<Overlay onClose={this.handleClose} className={Classes.OVERLAY_SCROLL_CONTAINER} {...this.state}>
<div className={classes}>
<H3>I'm an Overlay!</H3>
<p>
This is a simple container with some inline styles to position it on the screen. Its CSS
transitions are customized for this example only to demonstrate how easily custom
transitions can be implemented.
</p>
<p>
Click the right button below to transfer focus to the "Show overlay" trigger button outside
of this overlay. If persistent focus is enabled, focus will be constrained to the overlay.
Use the <Code>tab</Code> key to move to the next focusable element to illustrate this
effect.
</p>
<br />
<Button intent={Intent.DANGER} onClick={this.handleClose}>
Close
</Button>
<Button onClick={this.focusButton} style={{ float: "right" }}>
Focus button
</Button>
</div>
</Overlay>
</Example>
);
} renderOptions() {
const { autoFocus, enforceFocus, canEscapeKeyClose, canOutsideClickClose, hasBackdrop, usePortal } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<Switch checked={autoFocus} label="Auto focus" onChange={this.handleAutoFocusChange} />
<Switch checked={enforceFocus} label="Enforce focus" onChange={this.handleEnforceFocusChange} />
<Switch checked={usePortal} onChange={this.handleUsePortalChange}>
Use <Code>Portal</Code>
</Switch>
<Switch
checked={canOutsideClickClose}
label="Click outside to close"
onChange={this.handleOutsideClickChange}
/>
<Switch checked={canEscapeKeyClose} label="Escape key to close" onChange={this.handleEscapeKeyChange} />
<Switch checked={hasBackdrop} label="Has backdrop" onChange={this.handleBackdropChange} />
</React.Fragment>
);
}
18. Popover 弹出框
constructor(props) {
super(props); this.INTERACTION_KINDS = [
{ label: "Click", value: PopoverInteractionKind.CLICK.toString() },
{ label: "Click (target only)", value: PopoverInteractionKind.CLICK_TARGET_ONLY.toString() },
{ label: "Hover", value: PopoverInteractionKind.HOVER.toString() },
{ label: "Hover (target only)", value: PopoverInteractionKind.HOVER_TARGET_ONLY.toString() },
]; this.VALID_POSITIONS = [
"auto",
Position.TOP_LEFT,
Position.TOP,
Position.TOP_RIGHT,
Position.RIGHT_TOP,
Position.RIGHT,
Position.RIGHT_BOTTOM,
Position.BOTTOM_LEFT,
Position.BOTTOM,
Position.BOTTOM_RIGHT,
Position.LEFT_TOP,
Position.LEFT,
Position.LEFT_BOTTOM,
]; this.state = {
canEscapeKeyClose: true,
exampleIndex: 0,
hasBackdrop: false,
inheritDarkTheme: true,
interactionKind: PopoverInteractionKind.CLICK,
isOpen: false,
minimal: false,
modifiers: {
arrow: { enabled: true },
flip: { enabled: true },
keepTogether: { enabled: true },
preventOverflow: { enabled: true, boundariesElement: "scrollParent" },
},
position: "auto",
sliderValue: 5,
usePortal: true,
} this.getModifierChangeHandler = (name) => {
return (evt => {
const enabled = evt.target.checked;
this.setState({
modifiers: {
...this.state.modifiers,
[name]: { ...this.state.modifiers[name], enabled },
},
});
});
} this.handleSliderChange = (value) => this.setState({ sliderValue: value });
this.handleExampleIndexChange = handleNumberChange(exampleIndex => this.setState({ exampleIndex }));
this.handleInteractionChange = handleStringChange((interactionKind) => {
const hasBackdrop = this.state.hasBackdrop && interactionKind === PopoverInteractionKind.CLICK;
this.setState({ interactionKind, hasBackdrop });
});
this.handlePositionChange = handleStringChange((position) => this.setState({ position }));
this.handleBoundaryChange = handleStringChange((boundary) =>
this.setState({
modifiers: {
...this.state.modifiers,
preventOverflow: {
boundariesElement: boundary,
enabled: boundary.length > 0,
},
},
}),
);
this.toggleEscapeKey = handleBooleanChange(canEscapeKeyClose => this.setState({ canEscapeKeyClose }));
this.toggleIsOpen = handleBooleanChange(isOpen => this.setState({ isOpen }));
this.toggleMinimal = handleBooleanChange(minimal => this.setState({ minimal }));
this.toggleUsePortal = handleBooleanChange(usePortal => {
if (usePortal) {
this.setState({ hasBackdrop: false, inheritDarkTheme: false });
}
this.setState({ usePortal });
}); } render() {
const { exampleIndex, sliderValue, ...popoverProps } = this.state;
return (
<Example options={this.renderOptions()} {...this.props}>
<div className="docs-popover-example-scroll" ref={this.centerScroll}>
<Popover
popoverClassName={exampleIndex <= 2 ? Classes.POPOVER_CONTENT_SIZING : ""}
portalClassName="foo"
{...popoverProps}
enforceFocus={false}
isOpen={this.state.isOpen === true ? /* Controlled */ true : /* Uncontrolled */ undefined}
>
<Button intent={Intent.PRIMARY} text="Popover target" />
{this.getContents(exampleIndex)}
</Popover>
<p>
Scroll around this container to experiment<br />
with <Code>flip</Code> and <Code>preventOverflow</Code> modifiers.
</p>
</div>
</Example>
);
} renderOptions() {
const { arrow, flip, preventOverflow } = this.state.modifiers;
return (
<React.Fragment>
<H5>Appearance</H5>
<FormGroup
helperText="May be overridden to prevent overflow"
label="Position when opened"
labelFor="position"
>
<HTMLSelect
value={this.state.position}
onChange={this.handlePositionChange}
options={this.VALID_POSITIONS}
/>
</FormGroup>
<FormGroup label="Example content">
<HTMLSelect value={this.state.exampleIndex} onChange={this.handleExampleIndexChange}>
<option value="0">Text</option>
<option value="1">Input</option>
<option value="2">Slider</option>
<option value="3">Menu</option>
<option value="4">Empty</option>
</HTMLSelect>
</FormGroup>
<Switch checked={this.state.usePortal} onChange={this.toggleUsePortal}>
Use <Code>Portal</Code>
</Switch>
<Switch checked={this.state.minimal} label="Minimal appearance" onChange={this.toggleMinimal} />
<Switch checked={this.state.isOpen} label="Open (controlled mode)" onChange={this.toggleIsOpen} /> <H5>Interactions</H5>
<RadioGroup
label="Interaction kind"
selectedValue={this.state.interactionKind.toString()}
options={this.INTERACTION_KINDS}
onChange={this.handleInteractionChange}
/>
<Switch
checked={this.state.canEscapeKeyClose}
label="Can escape key close"
onChange={this.toggleEscapeKey}
/> <H5>Modifiers</H5>
<Switch checked={arrow.enabled} label="Arrow" onChange={this.getModifierChangeHandler("arrow")} />
<Switch checked={flip.enabled} label="Flip" onChange={this.getModifierChangeHandler("flip")} />
<Switch
checked={preventOverflow.enabled}
label="Prevent overflow"
onChange={this.getModifierChangeHandler("preventOverflow")}
>
<br />
<div style={{ marginTop: 5 }} />
<HTMLSelect
disabled={!preventOverflow.enabled}
value={preventOverflow.boundariesElement.toString()}
onChange={this.handleBoundaryChange}
>
<option value="scrollParent">scrollParent</option>
<option value="viewport">viewport</option>
<option value="window">window</option>
</HTMLSelect>
</Switch>
</React.Fragment>
); } getContents(index) {
return [
<div key="text">
<H5>Confirm deletion</H5>
<p>Are you sure you want to delete these items? You won't be able to recover them.</p>
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}>
<Button className={Classes.POPOVER_DISMISS} style={{ marginRight: 10 }}>
Cancel
</Button>
<Button intent={Intent.DANGER} className={Classes.POPOVER_DISMISS}>
Delete
</Button>
</div>
</div>,
<div key="input">
<label className={Classes.LABEL}>
Enter some text
<input autoFocus={true} className={Classes.INPUT} type="text" />
</label>
</div>,
<Slider key="slider" min={0} max={10} onChange={this.handleSliderChange} value={this.state.sliderValue} />,
<Menu key="menu">
<MenuDivider title="Edit" />
<MenuItem icon="cut" text="Cut" label="⌘X" />
<MenuItem icon="duplicate" text="Copy" label="⌘C" />
<MenuItem icon="clipboard" text="Paste" label="⌘V" disabled={true} />
<MenuDivider title="Text" />
<MenuItem icon="align-left" text="Alignment">
<MenuItem icon="align-left" text="Left" />
<MenuItem icon="align-center" text="Center" />
<MenuItem icon="align-right" text="Right" />
<MenuItem icon="align-justify" text="Justify" />
</MenuItem>
<MenuItem icon="style" text="Style">
<MenuItem icon="bold" text="Bold" />
<MenuItem icon="italic" text="Italic" />
<MenuItem icon="underline" text="Underline" />
</MenuItem>
</Menu>,
][index];
} centerScroll(div) {
if (div != null) {
// if we don't requestAnimationFrame, this function apparently executes
// before styles are applied to the page, so the centering is way off.
requestAnimationFrame(() => {
const container = div.parentElement;
container.scrollTop = div.clientHeight / 4;
container.scrollLeft = div.clientWidth / 4;
});
}
}
19. ProgressBar 进度条
constructor(props) {
super(props); this.state = {
hasValue: false,
value: 0.7,
}
} render() {
const { hasValue, intent, value } = this.state; return (
<Example options={this.renderOptions()}>
<ProgressBar intent={intent} value={hasValue ? value : null} />
</Example>
)
} renderOptions() {
const { hasValue, intent, value } = this.state; return (
<div>
<h5>Props</h5>
<IntentSelect intent={intent} onChange={(evt) => this.setState({ intent: evt.target.value})} />
<Switch checked={hasValue} label="Known value" onChange={(evt) => this.setState({ hasValue: evt.target.checked})} />
<Slider
disabled={!hasValue}
labelStepSize={1}
min={0}
max={1}
onChange={(value) => this.setState({ value: value})}
labelRenderer={this.renderLabel}
stepSize={0.1}
showTrackFill={false}
value={value}
/>
</div>
)
} renderLabel(value) {
return value.toFixed(1);
}
20. Radio 单选框

在一组备选项中进行单选。

20.1 基本使用方法

由于选项默认可见,不宜过多,若选项过多,建议使用 Select 选择器。

constructor(props) {
super(props); this.state = {
value: 1
}
} onChange(e) {
this.setState({ value: parseInt(e.target.value) });
} render() {
return (
<Example {...this.props}>
<div>
<Radio value="1" checked={this.state.value === 1} onChange={this.onChange.bind(this)}>备选项</Radio>
<Radio value="2" checked={this.state.value === 2} onChange={this.onChange.bind(this)}>备选项</Radio>
</div>
</Example>
)
}
20.2 禁用状态
render() {
return (
<Example {...this.props}>
<div>
<Radio value="1" disabled>备选项</Radio>
<Radio value="2" checked={true} disabled>选中且禁用</Radio>
</div>
</Example>
)
}
20.3 单选框组

适用于在多个互斥的选项中选择的场景。

constructor(props) {
super(props); this.state = {
mealType: "one"
}
} handleMealChange(e) {
this.setState({ mealType: e.target.value });
}; render() {
return (
<Example {...this.props}>
<div>
<RadioGroup
label="Meal Choice"
onChange={this.handleMealChange.bind(this)}
selectedValue={this.state.mealType}
>
<Radio label="Soup" value="one" />
<Radio label="Salad" value="two" />
<Radio label="Sandwich" value="three" />
</RadioGroup>
</div>
</Example>
)
}
21. Selection 选择
constructor(props) {
super(props); this._onSelectionChanged = this._onSelectionChanged.bind(this);
this._onToggleSelectAll = this._onToggleSelectAll.bind(this); this.state = {
items: this._createListItmes(),
selection: new Selection({onSelectionChanged: this._onSelectionChanged}),
selectionMode: SelectionMode.multiple
}; this.state.selection.setItems(this.state.items, false);
} render() {
const {selection, selectionMode, items} = this.state; return (
<div className="selection">
<div className="selection-item-check">
<Checkbox
onChange={this._onToggleSelectAll}
checked={selection.isAllSelected()}
indeterminate={selection.getSelectedCount() > 0 && !selection.isAllSelected()}
>全选</Checkbox>
</div>
<SelectionZone
selection={selection}
>
{items.map((item, index) => (
this._renderItem(item, index)
))}
</SelectionZone>
</div>
)
} _createListItmes() {
const colors = '赤橙黄绿青蓝紫'; return colors.split('').map((c, index) => ({
key: 'k' + index,
name: c,
}))
} _renderItem(item, index) {
const {selection} = this.state;
let isSelected = false; if (selection && index !== undefined) {
isSelected = selection.isIndexSelected(index);
} return (
<div
key={item.key}
className="selection-item"
data-is-focusable={true}
data-selection-toggle={true}
data-selection-index={index}
>
{selection &&
selection.canSelectItem(item) &&
selection.mode !== SelectionMode.none && (
<div className="selection-item-check" data-is-focusable={true}>
<Check checked={isSelected} />
</div>
)}
<span className="selection-item-name">{item.name}</span>
</div>
)
} _onSelectionChanged() {
console.log('onSelectionChanged')
this.forceUpdate();
} _onToggleSelectAll(evt) {
const { selection } = this.state;
selection.toggleAllSelected();
}
22. Spinner 加载

Spinners indicate progress in a circular fashion. They're great for ongoing operations and can also represent known progress.

22.1 基本用法
constructor(props) {
super(props); this.state = {
hasValue: false,
size: Spinner.SIZE_STANDARD,
value: 0.7,
}; this.handleIndeterminateChange = handleBooleanChange(hasValue => this.setState({ hasValue }));
this.handleModifierChange = handleStringChange((intent) => this.setState({ intent }));
this.renderLabel = (value) => value.toFixed(1);
this.handleValueChange = (value) => this.setState({ value });
this.handleSizeChange = (size) => this.setState({ size });
} render() {
const { size, hasValue, intent, value } = this.state;
return (
<Example options={this.renderOptions()} {...this.props}>
<Spinner intent={intent} size={size} value={hasValue ? value : null} />
</Example>
);
} renderOptions() {
const { size, hasValue, intent, value } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<IntentSelect intent={intent} onChange={this.handleModifierChange} />
<Label>Size</Label>
<Slider
labelStepSize={50}
min={0}
max={Spinner.SIZE_LARGE * 2}
showTrackFill={false}
stepSize={5}
value={size}
onChange={this.handleSizeChange}
/>
<Switch checked={hasValue} label="Known value" onChange={this.handleIndeterminateChange} />
<Slider
disabled={!hasValue}
labelStepSize={1}
min={0}
max={1}
onChange={this.handleValueChange}
labelRenderer={this.renderLabel}
stepSize={0.1}
showTrackFill={false}
value={value}
/>
</React.Fragment>
);
}
22.2 Props

Spinner is a simple stateless component that renders SVG markup. It can be used safely in DOM and SVG containers as it only renders SVG elements.

The value prop determines how much of the track is filled by the head. When this prop is defined, the spinner head will smoothly animate as value changes. Omitting value will result in an "indeterminate" spinner where the head spins indefinitely (this is the default appearance).

The size prop determines the pixel width/height of the spinner. Size constants are provided as static properties: Spinner.SIZE_SMALL, Spinner.SIZE_STANDARD, Spinner.SIZE_LARGE. Small and large sizes can be set by including Classes.SMALL or Classes.LARGE in className instead of the size prop (this prevents an API break when upgrading to 3.x).

23. Sticky
23.1 基本使用方法
render() {
const contentAreas = [];
for (let i = 0; i < 5; i++) {
contentAreas.push(this._createContentArea(i));
} return (
<Example data-example-id='StickyExample'>
<div
style={{
height: '600px',
width: '400px',
position: 'relative',
maxHeight: 'inherit'
}}
>
<ScrollablePane className="scrollablePaneDefaultExample">
{contentAreas.map(ele => {
return ele;
})}
</ScrollablePane>
</div>
</Example>
);
} _createContentArea(index) {
const colors = ['#eaeaea', '#dadada', '#d0d0d0', '#c8c8c8', '#a6a6a6', '#c7e0f4', '#71afe5', '#eff6fc', '#deecf9'];
const color = colors.splice(Math.floor(Math.random() * colors.length), 1)[0]; return (
<div
key={index}
style={{
backgroundColor: color
}}
>
<Sticky stickyPosition={StickyPositionType.Both}>
<div className="sticky">Sticky Component #{index + 1}</div>
</Sticky>
<div className="textContent">{this.props.lorem(100)}</div>
</div>
);
}
23.2 和 ListView 组合使用
constructor(props) {
super(props);
this.data = [];
this.state = {
desc: this.props.lorem(30),
expandedRow: undefined,
scrollToIndex: 0,
} for (let i=0; i<10000; i++) {
this.data.push(`[${i}] ${lorem(10 + Math.round(Math.random() * 50))}`);
}
} render() {
return (
<Example data-example-id='StickyGridExample'>
<div
style={{
height: '500px',
width: '420px',
position: 'relative',
maxHeight: 'inherit'
}}
>
<ScrollablePane className="scrollablePaneDefaultExample" style={{background: "#f3f3f3"}}>
<Sticky stickyPosition={StickyPositionType.Header}>
<div className="sticky">Search something ...</div>
</Sticky>
<div className="textContent">{this.state.desc}</div>
<Sticky stickyPosition={StickyPositionType.Header}>
<div style={{padding: 4}}>
<Input placeholder="Search" fill leftElement="search" rightElement="arrow-right" intent="success"/>
</div>
</Sticky>
<ListView
items={this.data}
checkboxVisibility={CheckboxVisibility.hidden}
onRenderItem={this._renderRow.bind(this)}
extra={{expandedRow: this.state.expandedRow}}
/>
</ScrollablePane>
</div>
</Example>
);
} _renderRow(item, index) {
return (
<div className="listItem">
<p className="list-textContent">{item}</p>
{this.state.expandedRow !== index &&
<Button intent="success" small onClick={() => this.setState({expandedRow: index, scrollToIndex: this.state.scrollToIndex + 20})}>展开</Button>
}
{this.state.expandedRow === index &&
<div>
<Button intent="success" small onClick={() => this.setState({expandedRow: undefined, scrollToIndex: 0})} >收起</Button>
<p className="list-textContent">哈哈,增加了一些内容!</p>
</div>
}
</div>
)
}
24. SvgIcon 图标

提供了一套常用的图标集合。

直接通过设置 use 图标名称来使用即可。例如:

render() {
return (
<div>
<SvgIcon intent="primary" use="#add" />
<SvgIcon intent="success" use="#search" />
<SvgIcon intent="warning" use="#caret-down" size={48} />
</div>
)
}
25. Tabs 控件
25.1 水平模式
constructor(props) {
super(props); this.state = {
focusIndex: 0
} this.onTabChange = (index: number) => {
this.setState({
focusIndex: index
})
} } render() {
return (
<Example {...this.props}>
<TabList
focusIndex={this.state.focusIndex}
onTabChange={this.onTabChange}
>
<Tab>Loki</Tab>
<Tab>Thor</Tab>
<Tab>Iron Man</Tab>
</TabList>
</Example>
)
}
25.2 垂直模式
constructor(props) {
super(props); this.state = {
focusIndex: 0
} this.onTabChange = (index: number) => {
this.setState({
focusIndex: index
})
} } render() {
return (
<Example {...this.props}>
<div style={{ display: "flex", width: 100, height: 200 }}>
<TabList
isVertical={true}
focusIndex={this.state.focusIndex}
onTabChange={this.onTabChange}
>
<Tab>Loki</Tab>
<Tab>Thor</Tab>
<Tab>Iron Man</Tab>
</TabList>
</div>
</Example>
)
}
26. Text 溢出提示文本

Text 在其内容溢出其容器时使用省略号截断,并且将完整的文本通过添加 title 属性显示。

constructor(props) {
super(props); this.state = {
textContent:
"You can change the text in the input below. Hover to see full text. " +
"If the text is long enough, then the content will overflow. This is done by setting " +
"ellipsize to true.",
}; this.onInputChange = handleStringChange((textContent) => this.setState({ textContent }));
} render() {
return (
<Example options={false} {...this.props}>
<Text ellipsize={true}>
{this.state.textContent}
&nbsp;
</Text>
<TextArea fill={true} onChange={this.onInputChange} value={this.state.textContent} />
</Example>
);
}
27. Toast 消息提示
constructor(props) {
super(props); this.refHandlers = {
toaster: (ref: Toaster) => (this.toaster = ref),
} this.POSITIONS = [
Position.TOP_LEFT,
Position.TOP,
Position.TOP_RIGHT,
Position.BOTTOM_LEFT,
Position.BOTTOM,
Position.BOTTOM_RIGHT,
]; this.TOAST_BUILDERS = [
{
action: {
href: "https://www.google.com/search?q=toast&source=lnms&tbm=isch",
target: "_blank",
text: <strong>Yum.</strong>,
},
button: "Procure toast",
intent: Intent.PRIMARY,
message: (
<React.Fragment>
One toast created. <em>Toasty.</em>
</React.Fragment>
),
},
{
action: {
onClick: () =>
this.addToast({
icon: "ban-circle",
intent: Intent.DANGER,
message: "You cannot undo the past.",
}),
text: "Undo",
},
button: "Move files",
icon: "tick",
intent: Intent.SUCCESS,
message: "Moved 6 files.",
},
{
action: {
onClick: () => this.addToast(this.TOAST_BUILDERS[2]),
text: "Retry",
},
button: "Delete root",
icon: "warning-sign",
intent: Intent.DANGER,
message:
"You do not have permissions to perform this action. \
Please contact your system administrator to request the appropriate access rights.",
},
{
action: {
onClick: () => this.addToast({ message: "Isn't parting just the sweetest sorrow?" }),
text: "Adieu",
},
button: "Log out",
icon: "hand",
intent: Intent.WARNING,
message: "Goodbye, old friend.",
},
]; this.handlePositionChange = handleStringChange((position) => this.setState({ position }));
this.toggleAutoFocus = handleBooleanChange(autoFocus => this.setState({ autoFocus }));
this.toggleEscapeKey = handleBooleanChange(canEscapeKeyClear => this.setState({ canEscapeKeyClear })); this.handleProgressToast = () => {
let progress = 0;
const key = this.toaster.show(this.renderProgress(0));
const interval = setInterval(() => {
if (this.toaster == null || progress > 100) {
clearInterval(interval);
} else {
progress += 10 + Math.random() * 20;
this.toaster.show(this.renderProgress(progress), key);
}
}, 1000);
}; this.state = {
autoFocus: false,
canEscapeKeyClear: true,
position: Position.TOP,
}
} render() {
return (
<Example options={this.renderOptions()} {...this.props}>
{this.TOAST_BUILDERS.map(this.renderToastDemo, this)}
<Button onClick={this.handleProgressToast} text="Upload file" />
<Toaster {...this.state} ref={this.refHandlers.toaster} />
</Example>
);
} renderOptions() {
const { autoFocus, canEscapeKeyClear, position } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<Label>
Position
<HTMLSelect value={position} onChange={this.handlePositionChange} options={this.POSITIONS} />
</Label>
<Switch label="Auto focus" checked={autoFocus} onChange={this.toggleAutoFocus} />
<Switch label="Can escape key clear" checked={canEscapeKeyClear} onChange={this.toggleEscapeKey} />
</React.Fragment>
);
} renderToastDemo(toast, index) {
// tslint:disable-next-line:jsx-no-lambda
return <Button intent={toast.intent} key={index} text={toast.button} onClick={() => this.addToast(toast)} />;
} renderProgress(amount) {
return {
icon: "cloud-upload",
message: (
<ProgressBar
className={classNames("docs-toast-progress", { [Classes.PROGRESS_NO_STRIPES]: amount >= 100 })}
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
value={amount / 100}
/>
),
timeout: amount < 100 ? 0 : 2000,
};
} addToast(toast) {
toast.timeout = 5000;
this.toaster.show(toast);
}
28. Tooltip 文字提示
constructor(props) {
super(props); this.toggleControlledTooltip = this.toggleControlledTooltip.bind(this); this.state = {
isOpen: false,
}
} render() {
// using JSX instead of strings for all content so the tooltips will re-render
// with every update for dark theme inheritance.
const lotsOfText = (
<span>
In facilisis scelerisque dui vel dignissim. Sed nunc orci, ultricies congue vehicula quis, facilisis a
orci.
</span>
);
const jsxContent = (
<em>
This tooltip contains an <strong>em</strong> tag.
</em>
); return (
<Example options={false} {...this.props}>
<div>
Inline text can have{" "}
<Tooltip className={Classes.TOOLTIP_INDICATOR} content={jsxContent}>
a tooltip.
</Tooltip>
</div>
<div>
<Tooltip content={lotsOfText}>Or, hover anywhere over this whole line.</Tooltip>
</div>
<div>
This line's tooltip{" "}
<Tooltip className={Classes.TOOLTIP_INDICATOR} content={<span>disabled</span>} disabled={true}>
is disabled.
</Tooltip>
</div>
<div>
This line's tooltip{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={<span>BRRAAAIINS</span>}
isOpen={this.state.isOpen}
>
is controlled by external state.
</Tooltip>
<Switch
checked={this.state.isOpen}
label="Open"
onChange={this.toggleControlledTooltip}
style={{ display: "inline-block", marginBottom: 0, marginLeft: 20 }}
/>
</div>
<div>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.PRIMARY"
intent='primary'
position={Position.LEFT}
usePortal={false}
>
Available
</Tooltip>{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.SUCCESS"
intent='success'
position={Position.TOP}
usePortal={false}
>
in the full
</Tooltip>{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.WARNING"
intent='warning'
position={Position.BOTTOM}
usePortal={false}
>
range of
</Tooltip>{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.DANGER"
intent='danger'
position={Position.RIGHT}
usePortal={false}
>
visual intents!
</Tooltip>
</div>
<br />
<Popover
content={<H1>Popover!</H1>}
position={Position.RIGHT}
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
>
<Tooltip
content={<span>This button also has a popover!</span>}
position={Position.RIGHT}
usePortal={false}
>
<Button intent='success' text="Hover and click me" />
</Tooltip>
</Popover>
</Example>
)
} toggleControlledTooltip() {
this.setState({ isOpen: !this.state.isOpen });
}
29. Tree 树形控件
constructor(props) {
super(props);
this.treeData = [
{ key: '0-0', title: 'parent 1', children:
[
{ key: '0-0-0', title: 'parent 1-1', children:
[
{ key: '0-0-0-0', title: 'parent 1-1-0' },
],
},
{ key: '0-0-1', title: 'parent 1-2', children:
[
{ key: '0-0-1-0', title: 'parent 1-2-0', disableCheckbox: true },
{ key: '0-0-1-1', title: 'parent 1-2-1' },
],
},
],
},
];
const keys = ['0-0-0-0'];
this.state = {
defaultExpandedKeys: keys,
defaultSelectedKeys: keys,
defaultCheckedKeys: keys,
}
} render() {
return (
<Example {...this.props}>
<Tree
showLine
className="myCls"
checkable
defaultExpandAll
defaultExpandedKeys={this.state.defaultExpandedKeys}
defaultSelectedKeys={this.state.defaultSelectedKeys}
defaultCheckedKeys={this.state.defaultCheckedKeys}
>
<TreeNode title="parent 1" key="0-0">
<TreeNode title="parent 1-0" key="0-0-0">
<TreeNode title="leaf" key="0-0-0-0" style={{ background: 'rgba(255, 0, 0, 0.1)' }} />
<TreeNode title="leaf" key="0-0-0-1" />
</TreeNode>
<TreeNode title="parent 1-1" key="0-0-1">
<TreeNode title="parent 1-1-0" key="0-0-1-0" />
<TreeNode title="parent 1-1-1" key="0-0-1-1" />
</TreeNode>
<TreeNode title="parent 1-2" key="0-0-2" disabled>
<TreeNode title="parent 1-2-0" key="0-0-2-0" disabled />
<TreeNode title="parent 1-2-1" key="0-0-2-1" />
</TreeNode>
</TreeNode>
</Tree>
</Example>
)
}
三、构建属于自己的React组件库
  • 单据 + 档案
  • 表单 + 查询列表
1. EasyBizForm.tsx

/src/main/components/easy-bizform/EasyBizForm.tsx

export class EasyBizForm extends BaseComponent<IEasyBizFormProps> implements ITabLifecycle {

  get presenter(): EasyBizFormPresenter<any> {
return this.props.presenter as any;
} .... render() {
return (
<BizFormPage
{...this.presenter.getBizFormOptions()}
>
{...this.presenter.renderTemplateSlot()}
</BizFormPage>
);
} ....
}
2. BizFormPage.tsx

/src/solutions/biz-form/page/BizFormPage.tsx

export class BizFormPage extends React.Component<IBizFormPageOptions & { tabApi?: ITabAPI }> {
render() {
const sections: any[] = [
<Section slot="header" key="header">
<BizFormHeader />
</Section>,
<Section slot="footer" key="footer">
<BizFormFooter />
</Section>,
<Section slot="fixed" key="fiexed">
<BizFormFixedContent />
</Section>,
]; .... const { entityName } = options;
const entity = metadata.getEntity(entityName); if (!entity) {
return <BizForm {...options}>{sections}</BizForm>;
} if (entity.isDocument) {
return <Archive sections={sections} {...options} />;
} if (entity.isVoucherBill) {
return <Bill sections={sections} {...options} />;
} return <BizForm {...options}>{sections}</BizForm>; ....
}
}

/src/solutions/biz-form/archive/form/Archive.tsx

interface IArchiveOptions extends IBizFormPageOptions {
sections: any[];
} export function Archive(props: IArchiveOptions) {
const { sections, displayOptions = {}, menuOptions, ...otherProps } = props; displayOptions.className = cx(displayOptions.className, 'bf-form-entity-archive'); const options = {
...otherProps,
displayOptions,
menuOptions,
}; return <BizForm {...options}>{sections}</BizForm>;
}

/src/solutions/biz-form/bill/form/Bill.tsx

interface IBillOptions extends IBizFormPageOptions {
sections: any[];
} export function Bill(props: IBillOptions) {
const { sections, displayOptions = {}, ...otherProps } = props; displayOptions.className = cx(displayOptions.className, 'bf-form-entity-bill'); const options = {
...otherProps,
displayOptions,
}; return <BizForm {...options}>{sections}</BizForm>;
}
3. BizForm.tsx

src/solutions/biz-form/core/components/BizForm.tsx

export class BizForm extends React.Component<IBizFormOptions & ContextProviderProps> {

  ...

  render() {
if (this.props.userScreenLoading) {
if (this.loadingStatus !== LoadingStatus.Complete) {
return null;
}
} let contentElement; if (this.props.userScreenLoading) {
contentElement = this.renderContent();
} else {
contentElement = (
<LoadingContainer
loadingStatus={this.loadingStatus}
className={this.loadingStyle.loadingClassName}
style={this.loadingStyle.loadingContainerStyle}
>
{() => this.renderContent()}
</LoadingContainer>
);
} return <PresenterProvider value={this.presenter}>{contentElement}</PresenterProvider>;
} private renderContent = () => {
if (this.props.onRenderContent) {
return this.props.onRenderContent({
presenter: this.presenter,
form: this.form,
renderFormContent: this.renderFormContent
});
}
return this.renderFormContent();
}; ... private renderFormContent = () => {
return (
<Observer>
{() => {
... const entityName = this.presenter.options.entityName; const className = cx(
displayOptions.className,
`bf-form-layout-${mode}`,
`bf-form-entity-${entityName}`,
); return (
<Page layout="BizFormLayout" className={className}>
{/* 内容区,根据 template 进行布局并显示主体内容 */}
<Section slot="content">
<Observer render={this.renderFormBody} />
</Section>
</Page>
);
}}
</Observer>
);
} ... private renderFormBody = () => {
const authController = this.presenter.getBean(BeanNames.AuthController);
if (!authController.hasAuthority) {
return this.renderAuthorizedFailed(this.presenter);
}
return <BizFormBody key={`${this.randomKey}`} />;
};
}
4. BizFormBody.tsx

src/solutions/biz-form/core/components/BizFormBody.tsx

export class BizFormBody extends React.Component<{presenter?: BizFormPresenter;tabApi?: ITabAPI;}> {

  ...

  render() {
return (
<Observer>
{() => (
<LoadingContainer loadingStatus={this.loadingStatus}>
{() => <QwertRegion circluar={false}>{this.renderZones()}</QwertRegion>}
</LoadingContainer>
)}
</Observer>
);
} renderZones() {
const zones = this.presenter.template.zones; const zonesList = zones.map((zone, index) => {
if (zone.type === BizFormZoneType.form) {
return <FormZone key={index} template={zone as any} index={index} />;
} else if (zone.type === BizFormZoneType.grid) {
return <GridZone key={index} template={zone as any} />;
}
}); const gridZones = zones.filter(zone => zone.type === BizFormZoneType.grid);
if (!gridZones.length && this.needRenderEmptyGrid) {
const template: any = {
// TODO 暂时这样 后面调整
layout: FormZoneLayoutType.flow,
type: BizFormZoneType.grid
};
zonesList.push(<GridZone key={zones.length} template={template} />);
} // 编辑态是否显示表尾区
const footerZone = zones.find(zone => zone.type === BizFormZoneType.footer);
if (footerZone) {
const displayFooterWhenEdit = !footerZone.hiddenInEdit;
if (displayFooterWhenEdit) {
zonesList.push(<FormZone key={zones.length} template={footerZone as any} />);
}
} return zonesList;
} ... }
5. FormZone.tsx

src/solutions/biz-form/core/components/form-zone/FormZone.tsx

export class FormZone extends React.Component<FormZoneProps & { presenter?: BizFormPresenter }> {
render() { ... const content = template.layout === FormZoneLayoutType.flow ?
<FormZoneInFlow key="flow" {...this.props} sections={normalSections} /> :
<FormZoneInTab key="tab" {...this.props} sections={normalSections} />; return (
<QwertElement>
{() => {
return <>
...
{content}
</>
}}
</QwertElement>
);
}
}
5.1 FormZoneInFlow.tsx

/src/solutions/biz-form/core/components/form-zone/FormZoneInFlow.tsx

export class FormZoneInFlow extends React.Component<FormZoneProps> {
render() {
return (
<QwertRegion>
<Observer>
{() => (
<div className="bf-zone bf-form-zone">
{(this.props.sections || [])
.filter((section, index) => {
if (!section.isCustomized) {
const { id } = section;
if(id){
return this.masterController.isSectionVisibleById(id);
}
return this.masterController.isSectionVisible(index);
}
return true;
})
.map((section, index) => {
if (section.isCustomized) {
return this.renderCustomizedSection(section as ICustomizedSection, index);
}
return this.renderSection(section as IFormZoneSection, index);
})}
</div>
)}
</Observer>
</QwertRegion>
);
} renderSection(section: IFormZoneSection, index: number) {
const cornerMark = {
name: section.cornerMark,
}; return (
<div key={`${index}`} className="bf-form-section">
<Section title={section.title} icon={'iconbiaotiqianzhui'} cornerMark={cornerMark}>
<MasterForm template={section} />
</Section>
</div>
);
} renderCustomizedSection(section: ICustomizedSection, index: number) {
return (
<div key={`${index}`} className={cx('bf-form-section', section.warpperClassName)} >
<Section title={section.title} icon="iconbiaotiqianzhui" className={section.className} rightElement={section.rightElement}>
{section.render({
section,
index,
type: FormZoneLayoutType.flow,
presenter: this.props.presenter,
})}
</Section>
</div>
);
}
}
5.2 Section.tsx

/src/components/section/Section.tsx

export class Section extends React.Component<ISectionProps> {
render() {
const {
titleClass,
contentClass,
className,
icon,
title,
children,
rightClass,
rightElement,
isEmpty = false,
dragPreviewRef,
cornerMark = {},
} = this.props;
const { name: cornerMarkName, position = 'top-right' } = cornerMark;
return (
<div className={cx('atx-section', styles.section, className)} onClick={this.props.onClick}>
{cornerMarkName && (
<div className={cx(styles['corner-mark'], styles[position])}>
<span>{cornerMarkName}</span>
</div>
)}
{title && (
<div ref={dragPreviewRef} className={cx('atx-section-header', styles.header, titleClass)}>
{icon && <SvgIcon className={styles.icon} use={`#${icon}`} />}
<div className={cx('atx-section-header-title', styles.title)}>{title}</div>
<div className={cx('atx-section-header-right', styles.right, rightClass)}>
{rightElement}
</div>
</div>
)}
<Observer>
{() =>
!isEmpty && (
<div className={cx('atx-section-content', styles.content, contentClass)}>
{children}
</div>
)
}
</Observer>
</div>
);
}
}
5.3 MasterForm.tsx

/src/solutions/biz-form/core/components/form-zone/MasterForm.tsx

export class MasterForm extends React.Component<MasterFormProps & {presenter?: BizFormPresenter; qwertElementParams?: IQwertElementParams }> {
... render() {
const { template, presenter } = this.props; ... return (
<QwertRegion>
<FormLayout columnSize={template.columnSize} disableError={this.disableError}>
<>
{template.fields
.filter((field) => field.visible)
.map((fieldTemplate, index) => {
// 读取字段的 模板信息
const { fieldName, isSlotField } = fieldTemplate; if (isSlotField) {
return this.renderSlotField(fieldTemplate);
} const fieldModel = presenter.model.master.fieldIndex[fieldName];
const isExtend = presenter.model.master.isExtendField(fieldName);
const extendModel =
presenter.model.master.extendFieldIndex[fieldName];
const props = {
path: fieldName,
form: formController.form,
template: fieldTemplate,
model: fieldModel,
index: index,
isExtend: isExtend,
extendModel: extendModel
};
return masterRenderController.renderField(props);
})}
</>
</FormLayout>
</QwertRegion>
);
} renderSlotField(template: IMasterField) {
const formController = this.props.presenter.getBean(BeanNames.FormController);
const { displayOptions = {} } = this.props.presenter.options;
if (displayOptions.masterSlot && displayOptions.masterSlot[template.fieldName]) {
return displayOptions.masterSlot[template.fieldName]({
form: formController.form,
path: template.fieldName
});
}
return <span>`这个插槽还没有被使用: ${template.fieldName}`</span>;
}
}
5.4 FormLayout.tsx

/packages/kaleido/packages/uikit/athena-ui/src/components/FormLayout/FormLayout.tsx

export enum Alignment {
Left = 'Left',
Right = 'Right',
Center = 'Center',
} export interface FormLayoutProps {
className?: string;
disableError?: boolean;
columnSize?: number;
// labelWidth?: number;
labelAlignment?: Alignment;
horizontalSpacing?: number;
verticalSpacing?: number;
layoutSize?: 'small' | 'normal' | 'large';
children: Array<JSX.Element> | JSX.Element;
} /**
* 前端组件
*/
export enum ComponentType {
CheckBox = 'CheckBox',
Text = 'Text',
Number = 'Number',
DatePicker = 'DatePicker',
Refer = 'Refer',
Enum = 'Enum',
List = 'List',
MultiLineText = 'MultiLineText',
TimeInput = 'TimeInput',
Unknown = 'Unknown',
} export class FormLayout extends React.PureComponent<FormLayoutProps> {
static defaultProps = {
columnSize: 1,
layoutSize: 'normal',
horizontalSpacing: 4,
verticalSpacing: 4,
}; private getClassNames() {
const {
columnSize,
className,
disableError,
layoutSize,
horizontalSpacing,
verticalSpacing,
} = this.props; const runtimeStyle = css`
.formElement{
width: ${100 / columnSize}%;
padding-right: ${horizontalSpacing}px; &.double{
width: ${(100 / columnSize) * 2}%;
} &.triple{
width: ${(100 / columnSize) * 3}%;
} &.quatary{
width: ${(100 / columnSize) * 4}%;
}
&.fivetimes{
width: ${(100 / columnSize) * 5}%;
}
&.sixtimes{
width: ${(100 / columnSize) * 6}%;
}
/* margin-bottom: ${verticalSpacing}px; */
}
`; return cx(
styles.root,
className,
{
[`at-FormLayout--disable-error`]: disableError,
[`${styles.root}-${layoutSize}`]: layoutSize,
},
runtimeStyle,
);
} render() {
... return (
<div className={this.getClassNames()}>
{childrens}
</div>
);
}
}

5.4.1 FormElement

export interface FormElementProps {
className?: string;
disableError?: boolean;
label?: string;
colspan?: number;
isRequired?: boolean;
suppressShowLabel?: boolean;
errorMessage?: string;
description?: string;
suffix?: any;
children?: any | ((options) => any);
showTooltip?: boolean; // 是否是显示 tooltip
labelRenderer?: () => JSX.Element;
contentRenderer?: () => JSX.Element;
componentType?: ComponentType;
} export class FormElement extends React.Component<FormElementProps> {
private labelRenderer = () => {
if (this.props.labelRenderer) {
return this.props.labelRenderer();
} const { label, isRequired, disableError, errorMessage } = this.props; return (
<FormElementLabel
label={label}
isRequired={isRequired}
disableError={disableError}
errorMessage={errorMessage}
/>
);
}; private contentRenderer = () => {
if (this.props.contentRenderer) {
return this.props.contentRenderer();
} const { disableError, errorMessage, children, suffix, description, ...otherProps } = this.props; return (
<FormElementContent
disableError={disableError}
errorMessage={errorMessage}
description={description}
children={children}
suffix={suffix}
{...otherProps}
/>
);
}; render() {
const { disableError, suppressShowLabel, colspan = 1, className, componentType, errorMessage } = this.props; return (
<FormElementSkeleton
classNames={cx(className, { 'formElement--disableError': disableError})}
colspan={colspan}
suppressShowLabel={suppressShowLabel}
labelRenderer={() => this.labelRenderer()}
contentRenderer={() => this.contentRenderer()}
componentType={componentType}
/>
);
}
}

5.4.2 FormElementSkeleton

/**
* 快捷 FormElement 布局组件
*/
export interface FormElementSkeletonProps {
classNames?: string;
colspan?: number;
suppressShowLabel?: boolean;
labelRenderer?: () => JSX.Element;
contentRenderer: () => JSX.Element;
componentType?: ComponentType;
} export class FormElementSkeleton extends React.PureComponent<FormElementSkeletonProps> {
private getClass = () => {
const { componentType } = this.props;
switch (componentType) {
case ComponentType.MultiLineText:
return 'multiLineTextElement';
}
return '';
}; render() {
const {
colspan = 1,
classNames = '',
suppressShowLabel,
labelRenderer,
contentRenderer,
} = this.props; const colspanArray = ['', 'double', 'triple', 'quatary', 'fivetimes', 'sixtimes']; const contentStyle = suppressShowLabel ? { paddingLeft: 0 } : null;
// 多行文本高度自适应 return (
<div className={cx(classNames, 'formElement ' + colspanArray[colspan - 1], this.getClass())}>
{!suppressShowLabel ? <div className="formLabel">{labelRenderer()}</div> : null}
<div style={contentStyle} className="formContent">
{contentRenderer()}
</div>
</div>
);
}
}

5.4.3 FormElementLabel

export interface FormElementLabelProps {
disableError?: boolean;
label: string;
isRequired?: boolean;
errorMessage?: string;
} /**
* 快捷 FormElementLabel 组件
*/
export class FormElementLabel extends React.PureComponent<FormElementLabelProps> {
render() {
const { label, isRequired, disableError, errorMessage } = this.props; return (
<React.Fragment>
{disableError && errorMessage && <ErrorTip error={errorMessage} />}
{isRequired && <Text className="required">*</Text>}
<Text ellipsize={true}>{label}</Text>
</React.Fragment>
);
}
}

5.4.5 FormElementContent

export interface FormElementContentProps {
disableError?: boolean;
errorMessage?: string;
description?: string;
suffix?: any;
children: any | ((options) => any);
showTooltip?: boolean; // 是否是显示 tooltip
} /**
* 快捷 FormElementContent 组件
*/
export class FormElementContent extends React.PureComponent<FormElementContentProps> {
private formInputRef: any; render() {
const { disableError, errorMessage, suffix, description } = this.props;
const children = this.renderChildren(); return (
<Observer>
{() => (
<React.Fragment>
<div className="formInput" ref={this.handleRef}>
{children}
{suffix}
</div>
{!disableError && errorMessage && <div className="formError" title={errorMessage}>{errorMessage}</div>}
{!errorMessage && description && <div className="formDescription">{description}</div>}
</React.Fragment>
)}
</Observer>
);
} renderChildren() {
const { children, showTooltip = false } = this.props; if (showTooltip) {
return (
<TooltipElement getMaxWidth={this.getMaxWidth}>
{children}
</TooltipElement>
);
} return children;
} handleRef = (ref) => {
if (ref) {
this.formInputRef = ref;
}
}; getMaxWidth = () => {
let maxWidth = 0;
if (this.formInputRef) {
maxWidth = this.formInputRef.clientWidth;
}
return maxWidth;
};
}
6. GridZone.tsx
export class GridZone extends React.Component<GridZoneProps & { presenter?: BizFormPresenter }> {
render() { ... return (
<QwertElement>
{() => {
if (template.layout === FormZoneLayoutType.flow) {
return <GridZoneInFlow {...this.props} />;
} else {
return <GridZoneInTab {...this.props} />;
}
}}
</QwertElement>
);
}
}
6.1 GridZoneInFlow.tsx
export interface GridZoneProps {
template: IGridZone;
} // 在 GridZone 中,一个 Grid 的定义
export interface IGridZoneSection {
// 子表字段
fieldName: string;
// 标题
title?: string;
// 图标
icon?: string;
// 表体字段
fields: Array<IDetailField>;
// 自定义渲染
customerRender?: (presenter: any) => JSX.Element;
// 自定义类名
className?: string;
// 右侧自定义渲染
rightElement?: (() => JSX.Element) | JSX.Element;
} export class GridZoneInFlow extends React.Component<GridZoneProps> {
render() {
return (
<div className="bf-zone bg-grid-zone-wrapper">
<div className="bf-zone bf-grid-zone">
{(this.props.sections || []).map((section, index) => {
return this.renderGridSection(section as IGridZoneSection, index);
})}
</div>
</div>
);
} renderGridSection(section: IGridZoneSection, index: number) {
const contentView = (
<ul className='atx-grid'>
{(section.fields || []).map((field, index) => {
const styleRules = { width: field.width }
return <li style={styleRules}>{field.title}</li>
})}
</ul>
);
return (
<Section title={section.title}>
{contentView}
</Section>
);
}
}
6.2 Section.tsx

/src/components/section/Section.tsx 同5.2

四、手写一个自己的Tree组件
1. 初始化项目
1.1 创建项目
mkdir customize_components
cd customize_components
cnpm init -y
touch .gitignore
1.2 安装依赖

@types开头的包都是typeScript的声明文件,可以进入node_modules/@types/XX/index.d.ts进行查看

npm i react @types/react react-dom @types/react-dom -S
npm i webpack webpack-cli webpack-dev-server -D
npm i typescript ts-loader source-map-loader style-loader css-loader less-loader less file-loader url-loader html-webpack-plugin -D
npm i axios express qs @types/qs -D

模块名

使用方式

react

React is a JavaScript library for creating user interfaces.

react-dom

This package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as react to npm.

webpack

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

webpack-cli

The official CLI of webpack

webpack-dev-server

Use webpack with a development server that provides live reloading. This should be used for development only.

typescript

TypeScript is a language for application-scale JavaScript.

ts-loader

This is the TypeScript loader for webpack.

source-map-loader

Extracts source maps from existing source files (from their sourceMappingURL).

style-loader

Inject CSS into the DOM.

css-loader

The css-loader interprets @import and url() like import/require() and will resolve them.

less-loader

A Less loader for webpack. Compiles Less to CSS.

less

This is the JavaScript, official, stable version of Less.

file-loader

The file-loader resolves import/require() on a file into a url and emits the file into the output directory.

url-loader

A loader for webpack which transforms files into base64 URIs.

html-webpack-plugin

Plugin that simplifies creation of HTML files to serve your bundles

1.3 支持typescript

首先需要生成一个tsconfig.json文件来告诉ts-loader如何编译代码TypeScript代码

tsc --init
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"outDir": "./dist",
"rootDir": "./src",
"noImplicitAny":true,
"esModuleInterop": true
},
"include": [
"./src/**/*",
"./typings/**/*"
]
}

参数

含义

target

转换成es5

module

代码规范

jsx

react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js

outDir

指定输出目录

rootDir

指定根目录

sourceMap

把 ts 文件编译成 js 文件的时候,同时生成对应的sourceMap文件

noImplicitAny

如果为true的话,TypeScript 编译器无法推断出类型时,它仍然会生成 JS文件,但是它也会报告一个错误

esModuleInterop

是否转译common.js模块

include

需要编译的目录

1.4 webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
entry: "./src/index.tsx",
output: {
path: path.join(__dirname, 'dist')
},
devtool: "source-map",
devServer: {
hot: true,
contentBase: path.join(__dirname, 'dist'),
historyApiFallback: {
index: './index.html'
}
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"]
}, module: {
rules: [{
test: /\.tsx?$/,
loader: "ts-loader"
},
{
enforce: "pre",
test: /\.tsx$/,
loader: "source-map-loader"
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(jpg|png|gif|svg)$/,
loader: "url-loader"
}
]
}, plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
],
};
1.5 package.json
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server",
}
2.创建和渲染树形菜单
2.1 src\index.html
<body>
<div id="root"></div>
</body>
2.2 src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import Tree from './components/tree';
import data from './data'; ReactDOM.render(, document.getElementById('root'));
2.3 src\typings.tsx
export interface TreeData {
name: string;
key: string;
type: string;
collapsed: boolean;
children?: Array<TreeData>;
parent?: TreeData;
checked?: boolean;
loading?: boolean;
}
2.4 src\data.tsx
import { TreeData } from './typings';

const data: TreeData = {
name: '父亲',
key: '1',
type: 'folder',
collapsed: false,
children: [
{
name: '儿子1',
key: '1-1',
type: 'folder',
collapsed: false,
children: [
{
name: '孙子1',
key: '1-1-1',
type: 'folder',
collapsed: false,
children: [
{
name: '重孙1',
key: '1-1-1-1',
type: 'file',
collapsed: false,
children: []
}
]
}
]
},
{
name: '儿子2',
key: '1-2',
type: 'folder',
collapsed: true
}
]
}
export default data;
2.5 src\components\tree.tsx
import React from 'react';
import './index.less';
import { TreeData } from '../typings';
import TreeNode from './tree-node';
import { getChildren } from '../api'; interface Props {
data: TreeData;
}
interface KeyToNodeMap {
[key: string]: TreeData
}
interface State {
data: TreeData;
fromNode?: TreeData;
}
class Tree extends React.Component<Props, State> {
data: TreeData;
keyToNodeMap: KeyToNodeMap;
constructor(props: Props) {
super(props);
this.state = { data: this.props.data };
this.data = props.data;
this.buildKeyMap();
}
buildKeyMap = () => {
let data = this.data;
this.keyToNodeMap = {};
this.keyToNodeMap[data.key] = data;
if (data.children && data.children.length > 0) {
this.walk(data.children, data);
}
this.setState({ data: this.state.data });
}
walk = (children: Array<TreeData>, parent: TreeData): void => {
children.map((item: TreeData) => {
item.parent = parent;
this.keyToNodeMap[item.key] = item;
if (item.children && item.children.length > 0) {
this.walk(item.children, item);
}
});
}
onCollapse = async (key: string) => {
let data = this.keyToNodeMap[key];
if (data) {
let { children } = data;
if (!children) {
data.loading = true;
this.setState({ data: this.state.data });
let result = await getChildren(data);
if (result.code == 0) {
data.children = result.data;
data.collapsed = false;
data.loading = false;
this.buildKeyMap();
} else {
alert('加载失败');
}
} else {
data.collapsed = !data.collapsed;
this.setState({ data: this.state.data });
}
}
}
onCheck = (key: string) => {
let data: TreeData = this.keyToNodeMap[key];
if (data) {
data.checked = !data.checked;
if (data.checked) {
this.checkChildren(data.children, true);
this.checkParentCheckAll(data.parent);
} else {
this.checkChildren(data.children, false);
this.checkParent(data.parent, false);
}
this.setState({ data: this.state.data });
}
}
checkParentCheckAll = (parent: TreeData) => {
while (parent) {
parent.checked = parent.children.every(item => item.checked);
parent = parent.parent;
}
}
checkParent = (parent: TreeData, checked: boolean) => {
while (parent) {
parent.checked = checked;
parent = parent.parent;
}
}
checkChildren = (children: Array<TreeData> = [], checked: boolean) => {
children.forEach((item: TreeData) => {
item.checked = checked;
this.checkChildren(item.children, checked);
});
}
setFromNode = (fromNode: TreeData) => {
this.setState({ ...this.state, fromNode });
}
onMove = (toNode: TreeData) => {
let fromNode = this.state.fromNode;
let fromChildren = fromNode.parent.children, toChildren = toNode.parent.children;
let fromIndex = fromChildren.findIndex((item: TreeData) => item === fromNode);
let toIndex = toChildren.findIndex(item => item === toNode);
fromChildren.splice(fromIndex, 1, toNode);
toChildren.splice(toIndex, 1, fromNode);
this.buildKeyMap();
}
render() {
return (
<div className="tree">
<div className="tree-nodes">
<TreeNode
data={this.props.data}
onCollapse={this.onCollapse}
onCheck={this.onCheck}
setFromNode={this.setFromNode}
onMove={this.onMove}
/>
</div>
</div>
)
}
}
export default Tree;
2.6 src\components\tree-node.tsx
import React from 'react';
import { TreeData } from '../typings';
import file from '../assets/file.png';
import closedFolder from '../assets/closed-folder.png';
import openedFolder from '../assets/opened-folder.png';
import loadingSrc from '../assets/loading.gif';
interface Props {
data: TreeData,
onCollapse: any,
onCheck: any;
setFromNode: any;
onMove: any
}
class TreeNode extends React.Component<Props> {
treeNodeRef: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
super(props);
this.treeNodeRef = React.createRef();
}
componentDidMount() {
this.treeNodeRef.current.addEventListener('dragstart', (event: DragEvent): void => {
this.props.setFromNode(this.props.data);
event.stopPropagation();
}, false);//useCapture=false
this.treeNodeRef.current.addEventListener('dragenter', (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
}, false);
this.treeNodeRef.current.addEventListener('dragover', (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
}, false);
this.treeNodeRef.current.addEventListener('drop', (event: DragEvent) => {
event.preventDefault();
this.props.onMove(this.props.data);
event.stopPropagation();
}, false);
}
render() {
let { data: { name, children, collapsed = false, key, checked = false, loading } } = this.props;
let caret, icon;
if (children) {
if (children.length > 0) {
caret = (
<span className={`collapse ${collapsed ? 'caret-right' : 'caret-down'}`}
onClick={() => this.props.onCollapse(key)}
/>
)
icon = collapsed ? closedFolder : openedFolder;
} else {
caret = null;
icon = file;
}
} else {
caret = (
loading ? <img className="collapse" src={loadingSrc} style={{ width: 14, top: '50%', marginTop: -7 }} /> : <span className={`collapse caret-right`}
onClick={() => this.props.onCollapse(key)}
/>
)
icon = closedFolder;
}
return (
<div className="tree-node" draggable={true} ref={this.treeNodeRef}>
<div className="inner">
{caret}
<span className="content">
<input type="checkbox" checked={checked} onChange={() => this.props.onCheck(key)} />
<img style={{ width: 20 }} src={icon} />
{name}
</span>
</div>
{
(children && children.length > 0 && !collapsed) && (
<div className="children">
{
children.map((item: TreeData) => (
<TreeNode
onCollapse={this.props.onCollapse}
onCheck={this.props.onCheck}
key={item.key}
setFromNode={this.props.setFromNode}
onMove={this.props.onMove}
data={item} />
))
}
</div>
)
}
</div>
)
}
}
export default TreeNode;
2.7 src\components\index.scss
.tree {
width: 80%;
overflow-x: hidden;
overflow-y: auto;
background-color: #fff; .tree-nodes {
position: relative;
overflow: hidden; .tree-node {
.inner {
color: #000;
font-size: 16px;
position: relative;
cursor: pointer;
padding-left: 10px; .collapse {
position: absolute;
left: 0;
cursor: pointer;
} .caret-right:before {
content: '\25B8';
} .caret-down:before {
content: '\25BE';
} .content {
display: inline-block;
width: 100%;
padding: 4px 5px;
}
} .children {
padding-left: 20px;
}
} }
}
2.8 src\typings\images.d.ts
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
2.9 src\api.tsx
import axios from 'axios';
import qs from 'qs';
axios.defaults.baseURL = 'http://localhost:3000';
export const getChildren = (data: any) => {
return axios.get(`/getChildren?${qs.stringify({ key: data.key, name: data.name })}`).then(res => res.data).catch(function (error) {
console.log(error);
});
}
2.10 api.js
let express = require('express');
let app = express();
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
app.get('/getChildren', (req, res) => {
let data = req.query;
setTimeout(function () {
res.json({
code: 0,
data: [
{
name: data.name',
key: `${data.key}-1`,
type: 'folder',
collapsed: true
},
{
name: data.name',
key: `${data.key}-2`,
type: 'folder',
collapsed: true
}
]
});
}, 2000) });
app.listen(3000, () => {
console.log(`接口服务器在${3000}上启动`);
});

React工程化实践之UI组件库的更多相关文章

  1. react_app 项目开发 (4)_ React UI 组件库 ant-design 的基本使用

    最流行的开源 React UI 组件库 material-ui 国外流行(安卓手机的界面效果)文档 ant-design 国内流行 (蚂蚁金服 设计,一套 PC.一套移动端的____下拉菜单.分页.. ...

  2. 加薪攻略之UI组件库实践—storybook

    目录 加薪攻略之UI组件库实践-storybook 一.业务背景 二.选用方案 三.引入分析 项目结构 项目效果 四.实现步骤 1.添加依赖 2.添加npm执行脚本 3.添加配置文件 4.添加必要的w ...

  3. 16款优秀的Vue UI组件库推荐

    16款优秀的Vue UI组件库推荐 Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基 ...

  4. [转载]前端——实用UI组件库

    https://www.cnblogs.com/xuepei/p/7920888.html Angular UI 组件 ngx-bootstrap 是一套Bootstrap 组件 官网:https:/ ...

  5. [转]VUE优秀UI组件库合集

    原文链接 随着SPA.前后端分离的技术架构在业界越来越流行,前端的业务复杂度也越来越高,导致前端开发者需要管理的内容,承担的职责越来越多,这一切,使得业界对前端开发方案的思考多了很多,以react.v ...

  6. 【转】前端——实用UI组件库

    Angular UI 组件 ngx-bootstrap 是一套Bootstrap 组件 官网:https://valor-software.com/ngx-bootstrap/#/ github: h ...

  7. 前端——实用UI组件库

    Angular UI 组件 ngx-bootstrap 是一套Bootstrap 组件 官网:https://valor-software.com/ngx-bootstrap/#/ github: h ...

  8. 强烈推荐优秀的Vue UI组件库

    Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基于Vue的UI组件框架开发,并投入正 ...

  9. 前端笔记之Vue(四)UI组件库&Vuex&虚拟服务器初识

    一.日历组件 new Date()的月份是从0开始的. 下面表达式是:2018年6月1日 new Date(2018, 5, 1); 下面表达式是:2018年5月1日 new Date(2018, 4 ...

  10. Teaset-React Native UI 组件库

    GitHub地址 https://github.com/rilyu/teaset/blob/master/docs/cn/README.md React Native UI 组件库, 超过 20 个纯 ...

随机推荐

  1. Python基础前言

    计算机内部存储数据的原理 """计算机内部只认识01二进制"""是因为计算机是基于电工作的,而电是有高低电频之分00000001   000 ...

  2. ECDSA签名验证

    using System; using System.IO; using System.Text; using Org.BouncyCastle.Crypto; using Org.BouncyCas ...

  3. 解决在宝塔面板IIS服务器上部署svg/woff/woff2字体的问题

    部署网站的字体和服务器IIS有什么关系?如果你的职责只限于一名前端开发,那么你可能很"幸福"地与这些问题擦肩而过,浑然不觉.可是本人一直都是孤军奋战,连开发环境都要自己搭建,这次又 ...

  4. ubuntu下ntp时间同步

    1. 首先安装ntp服务(ubuntu 16.02)在linux的root用户下执行以下命   sudo apt-get install  ntp (如果不是ubuntu系统则执行 yum insta ...

  5. 基于Antlr的Modelica3.5语言解析

    背景 Modelica语言是一种统一面向对象的系统建模语言 官方文档中明确写明了语法规范 在附录的第一章词法,第二章语法都完整的罗列的语言规范,对于Antlr适配特别好 只需要把[]修改为Antlr的 ...

  6. shell mv cp image in parallel 多线程解压parallel

    # apt install parallel # mkdir -p 1Kx1K/img # ls 1Kx1K/img_9*.jpg |parallel -j 80 mv {} 1Kx1K/img ht ...

  7. ls access.log.?.gz

    因为日志文件每天都会打包, 所以昨天的问题可能就在今天的access.log/error.log文件里找不到了.如何找出个位数的log文件呢? 这里就有两种不同的匹配符号, *匹配多个, ?匹配一个, ...

  8. mplfinance常用方法

    一.主题相关 查看可用预设主题 mpf.available_styles() 默认的主题包括:'binance','blueskies','brasil','charles','checkers',' ...

  9. 将freeswitch加入CentOS7的systemctl

    cd /usr/local/src/freeswitch/build cp freeswitch.service /usr/lib/systemd/system/ cp freeswitch.sysc ...

  10. bilibili经典面试题

    1. 如何向面试官解释什么是Redis,看看普通人和高手是如何回答的?_哔哩哔哩_bilibili 2.Java面试热点问题,synchronized原理剖析与优化_哔哩哔哩_bilibili 3.黑 ...