React工程化实践之UI组件库
分享日期: 2022-11-08
分享内容: 组件不是 React 特有的概念,但是 React 将组件化的思想发扬光大,可谓用到了极致。良好的组件设计会是良好的应用开发基础,这一讲就让我们谈一谈React工
程化开发实践中用到的一些UI组件库,并 结合企企项目中各位大佬们设计的组 件,更深入的了解组件设计思想,以便更好的在项目业务各类场景中应用。
分享主题由来:
如果您不想从头开始构建所有必要的 React UI 组件,您可以选择 React UI Library 来完成这项工作。所有这些都有一些基本的组件,比如按钮,下拉菜单,对话框和列表。有很多 UI 库可供 React 选择:
PC端UI库
- Ant Design https://ant.design/index-cn
- Chakra UI https://chakra-ui.com
- Tailwind UI https://tailwindui.com
- Semantic UI https://semantic-ui.com/
- Material UI https://v0.mui.com/
- React Bootstrap http://react.tgwoo.com/components.html
移动端UI库
- Taro UI for React https://taro-ui.jd.com/#/docs/introduction
- Ant Design Mobile of React https://mobile.ant.design/zh/components/button
- TDesign React Mobile https://tdesign.tencent.com/react/overview
- NutUI for React https://nutui.jd.com/react/#/zh-CN/component/button
- Material-UI https://mui.com/zh/material-ui/api/accordion/
- React WeUI https://weui.github.io/react-weui/docs/#/react-weui/docs/page/1/articles/0
- Zarm Design React https://zarm.design/#/components/button
FAQ 常用的HTML标签有哪些? 行内元素和块级元素有什么区别?
/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
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>
);
}
基础的按钮用法。
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>
)
}
按钮不可用状态。
你可以使用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>
)
}
没有边框和背景色的按钮。
render() {
return (
<div>
<Button intent="primary" link>文字按钮</Button>
<Button intent="success" link disabled>文字按钮</Button>
</div>
)
}
带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。
设置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>
)
}
以按钮组的方式出现,常用于多项类似操作。
使用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>
)
}
点击按钮后进行数据加载操作,在按钮上显示加载状态。
要设置为 loading 状态,只要设置loading
属性为true
即可。
render() {
return <Button intent="primary" loading={true}>加载中</Button>
}
Button 组件提供除了默认值以外的三种尺寸,可以在不同场景下选择合适的按钮尺寸。
额外的尺寸:large
、small
、mini
,通过设置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>
)
}
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>
)
}
按钮包含下拉菜单
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>
)
}
参数 |
说明 |
类型 |
可选值 |
默认值 |
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 |
Button groups arrange multiple buttons in a horizontal or vertical group.
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>
);
}
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>
);
}
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,
});
}
通过设置 showMonthPickerAsOverlay
为 true
可以点击抬头区的月来选择月份。
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,
});
}
通过设置 isMonthPickerVisible
为 true
可以点击抬头区的月来选择月份。
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,
});
}
通过设置 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>
);
}
一组备选项中进行多选。
单独使用可以表示两种状态之间的切换,写在标签中的内容为 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>
)
}
render() {
return (
<Example alignLeft {...this.props}>
<div>
<Checkbox disabled>备选项</Checkbox>
<Checkbox checked={true} disabled>选中且禁用</Checkbox>
</div>
</Example>
)
}
适用于在多个互斥的选项中选择的场景
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>
)
}
render() {
return (
<Example alignLeft {...this.props}>
<Checkbox checkedIcon="heart" unCheckedIcon="heart-broken">备选项</Checkbox>
</Example>
)
}
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>
);
}
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>
);
}
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 });
}
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 });
}
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>
);
}
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>
)
}
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>
)
}
表单字段的描述标签。
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
]
}
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>
]
}
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>
]
}
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>
)
}
ListView
和 List
的不同之处在于 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>
)
}
由于选项默认可见,不宜过多,若选项过多,建议使用 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>
)
}
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>
]
}
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
})
}
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>
);
}
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;
});
}
}
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);
}
在一组备选项中进行单选。
由于选项默认可见,不宜过多,若选项过多,建议使用 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>
)
}
render() {
return (
<Example {...this.props}>
<div>
<Radio value="1" disabled>备选项</Radio>
<Radio value="2" checked={true} disabled>选中且禁用</Radio>
</div>
</Example>
)
}
适用于在多个互斥的选项中选择的场景。
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>
)
}
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();
}
Spinners indicate progress in a circular fashion. They're great for ongoing operations and can also represent known progress.
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>
);
}
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).
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>
);
}
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>
)
}
提供了一套常用的图标集合。
直接通过设置 use
图标名称来使用即可。例如:
render() {
return (
<div>
<SvgIcon intent="primary" use="#add" />
<SvgIcon intent="success" use="#search" />
<SvgIcon intent="warning" use="#caret-down" size={48} />
</div>
)
}
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>
)
}
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>
)
}
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}
</Text>
<TextArea fill={true} onChange={this.onInputChange} value={this.state.textContent} />
</Example>
);
}
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);
}
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 });
}
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>
)
}
- 单据 + 档案
- 表单 + 查询列表
/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>
);
} ....
}
/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>;
}
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}`} />;
};
}
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;
} ... }
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>
);
}
}
/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>
);
}
}
/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>
);
}
}
/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>;
}
}
/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;
};
}
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>
);
}
}
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>
);
}
}
/src/components/section/Section.tsx 同5.2
mkdir customize_components
cd customize_components
cnpm init -y
touch .gitignore
@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 |
首先需要生成一个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 |
需要编译的目录 |
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()
],
};
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server",
}
<body>
<div id="root"></div>
</body>
import React from 'react';
import ReactDOM from 'react-dom';
import Tree from './components/tree';
import data from './data'; ReactDOM.render(, document.getElementById('root'));
export interface TreeData {
name: string;
key: string;
type: string;
collapsed: boolean;
children?: Array<TreeData>;
parent?: TreeData;
checked?: boolean;
loading?: boolean;
}
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;
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;
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;
.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;
}
} }
}
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
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);
});
}
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组件库的更多相关文章
- react_app 项目开发 (4)_ React UI 组件库 ant-design 的基本使用
最流行的开源 React UI 组件库 material-ui 国外流行(安卓手机的界面效果)文档 ant-design 国内流行 (蚂蚁金服 设计,一套 PC.一套移动端的____下拉菜单.分页.. ...
- 加薪攻略之UI组件库实践—storybook
目录 加薪攻略之UI组件库实践-storybook 一.业务背景 二.选用方案 三.引入分析 项目结构 项目效果 四.实现步骤 1.添加依赖 2.添加npm执行脚本 3.添加配置文件 4.添加必要的w ...
- 16款优秀的Vue UI组件库推荐
16款优秀的Vue UI组件库推荐 Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基 ...
- [转载]前端——实用UI组件库
https://www.cnblogs.com/xuepei/p/7920888.html Angular UI 组件 ngx-bootstrap 是一套Bootstrap 组件 官网:https:/ ...
- [转]VUE优秀UI组件库合集
原文链接 随着SPA.前后端分离的技术架构在业界越来越流行,前端的业务复杂度也越来越高,导致前端开发者需要管理的内容,承担的职责越来越多,这一切,使得业界对前端开发方案的思考多了很多,以react.v ...
- 【转】前端——实用UI组件库
Angular UI 组件 ngx-bootstrap 是一套Bootstrap 组件 官网:https://valor-software.com/ngx-bootstrap/#/ github: h ...
- 前端——实用UI组件库
Angular UI 组件 ngx-bootstrap 是一套Bootstrap 组件 官网:https://valor-software.com/ngx-bootstrap/#/ github: h ...
- 强烈推荐优秀的Vue UI组件库
Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基于Vue的UI组件框架开发,并投入正 ...
- 前端笔记之Vue(四)UI组件库&Vuex&虚拟服务器初识
一.日历组件 new Date()的月份是从0开始的. 下面表达式是:2018年6月1日 new Date(2018, 5, 1); 下面表达式是:2018年5月1日 new Date(2018, 4 ...
- Teaset-React Native UI 组件库
GitHub地址 https://github.com/rilyu/teaset/blob/master/docs/cn/README.md React Native UI 组件库, 超过 20 个纯 ...
随机推荐
- Python基础前言
计算机内部存储数据的原理 """计算机内部只认识01二进制"""是因为计算机是基于电工作的,而电是有高低电频之分00000001 000 ...
- ECDSA签名验证
using System; using System.IO; using System.Text; using Org.BouncyCastle.Crypto; using Org.BouncyCas ...
- 解决在宝塔面板IIS服务器上部署svg/woff/woff2字体的问题
部署网站的字体和服务器IIS有什么关系?如果你的职责只限于一名前端开发,那么你可能很"幸福"地与这些问题擦肩而过,浑然不觉.可是本人一直都是孤军奋战,连开发环境都要自己搭建,这次又 ...
- ubuntu下ntp时间同步
1. 首先安装ntp服务(ubuntu 16.02)在linux的root用户下执行以下命 sudo apt-get install ntp (如果不是ubuntu系统则执行 yum insta ...
- 基于Antlr的Modelica3.5语言解析
背景 Modelica语言是一种统一面向对象的系统建模语言 官方文档中明确写明了语法规范 在附录的第一章词法,第二章语法都完整的罗列的语言规范,对于Antlr适配特别好 只需要把[]修改为Antlr的 ...
- 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 ...
- ls access.log.?.gz
因为日志文件每天都会打包, 所以昨天的问题可能就在今天的access.log/error.log文件里找不到了.如何找出个位数的log文件呢? 这里就有两种不同的匹配符号, *匹配多个, ?匹配一个, ...
- mplfinance常用方法
一.主题相关 查看可用预设主题 mpf.available_styles() 默认的主题包括:'binance','blueskies','brasil','charles','checkers',' ...
- 将freeswitch加入CentOS7的systemctl
cd /usr/local/src/freeswitch/build cp freeswitch.service /usr/lib/systemd/system/ cp freeswitch.sysc ...
- bilibili经典面试题
1. 如何向面试官解释什么是Redis,看看普通人和高手是如何回答的?_哔哩哔哩_bilibili 2.Java面试热点问题,synchronized原理剖析与优化_哔哩哔哩_bilibili 3.黑 ...