feat: InputRange组件min,max,step支持变量

This commit is contained in:
lurunze1226 2023-07-07 18:07:23 +08:00
parent b159a650e3
commit 13d9d6838e
4 changed files with 465 additions and 26 deletions

View File

@ -56,7 +56,7 @@ order: 38
## 控制调整的粒度
使用 `step` 可以控制调整粒度,默认是 1。
使用 `step` 可以控制调整粒度,默认是 1。`3.3.0`版本后支持使用变量。
```schema: scope="body"
{
@ -255,14 +255,14 @@ order: 38
当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| className | `string` | | css 类名 |
| value | `number` or `string` or `{min: number, max: number}` or `[number, number]` | | |
| min | `number` | `0` | 最小值 |
| max | `number` | `100` | 最大 |
| min | `number \| string` | `0` | 最小值,支持变量 | `3.3.0`后支持变量 |
| max | `number \| string` | `100` | 最大 支持变量值 | `3.3.0`后支持变量 |
| disabled | `boolean` | `false` | 是否禁用 |
| step | `number` | `1` | 步长 |
| step | `number \| string` | `1` | 步长,支持变量 | `3.3.0`后支持变量 |
| showSteps | `boolean` | `false` | 是否显示步长 |
| parts | `number` or `number[]` | `1` | 分割的块数<br/>主持数组传入分块的节点 |
| marks | <code>{ [number &#124; string]: ReactNode }</code> or <code>{ [number &#124; string]: { style: CSSProperties, label: ReactNode } }</code> | | 刻度标记<br/>- 支持自定义样式<br/>- 设置百分比 |

View File

@ -1,5 +1,306 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renderer: range with min & max & step by variables 1`] = `
<div>
<div
class="cxd-Page"
>
<div
class="cxd-Page-content"
>
<div
class="cxd-Page-main"
>
<div
class="cxd-Page-body"
role="page-body"
>
<div
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
style="position: relative;"
>
<div
class="cxd-Panel-heading"
>
<h3
class="cxd-Panel-title"
>
<span
class="cxd-TplField"
>
<span>
表单
</span>
</span>
</h3>
</div>
<div
class="cxd-Panel-body"
>
<form
class="cxd-Form cxd-Form--normal"
novalidate=""
>
<input
style="display: none;"
type="submit"
/>
<div
class="cxd-Form-item cxd-Form-item--normal"
data-role="form-item"
>
<label
class="cxd-Form-label"
>
<span>
<span
class="cxd-TplField"
>
<span>
滑块
</span>
</span>
</span>
</label>
<div
class="cxd-RangeControl cxd-InputRange cxd-Form-control"
>
<div
class="cxd-InputRange-wrap"
>
<div
class="cxd-InputRange-track cxd-InputRange-track--background"
>
<div
class="cxd-InputRange-track-active"
style="width: 10%; left: 0%;"
/>
<div
class="cxd-InputRange-handle"
style="left: 10%; z-index: 1; position: relative;"
>
<div
class="cxd-InputRange-handle-icon"
>
<icon-mock
classname="icon icon-slider-handle"
icon="slider-handle"
/>
</div>
<div
class="cxd-InputRange-label pos-top"
style="position: relative;"
>
<span>
12
</span>
<div
class="resize-sensor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
class="resize-sensor-expand"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
/>
</div>
<div
class="resize-sensor-shrink"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
/>
</div>
<div
class="resize-sensor-appear"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
/>
</div>
</div>
<div
class="resize-sensor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
class="resize-sensor-expand"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
/>
</div>
<div
class="resize-sensor-shrink"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
/>
</div>
<div
class="resize-sensor-appear"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
/>
</div>
</div>
</div>
</div>
<div
class="cxd-InputRange-input"
>
<div
class="cxd-Number cxd-Number--borderFull"
>
<div
class="cxd-Number-handler-wrap"
>
<span
aria-disabled="false"
aria-label="Increase Value"
class="cxd-Number-handler cxd-Number-handler-up"
role="button"
unselectable="on"
>
<span
class="cxd-Number-handler-up-inner"
unselectable="on"
/>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="cxd-Number-handler cxd-Number-handler-down"
role="button"
unselectable="on"
>
<span
class="cxd-Number-handler-down-inner"
unselectable="on"
/>
</span>
</div>
<div
class="cxd-Number-input-wrap"
>
<input
aria-valuemax="66"
aria-valuemin="6"
aria-valuenow="12"
autocomplete="off"
class="cxd-Number-input"
role="spinbutton"
step="2"
value="12"
/>
</div>
</div>
</div>
<a
class="cxd-InputRange-clear is-active"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
</div>
</form>
</div>
<div
class="cxd-Panel-footerWrap"
>
<div
class="cxd-Panel-btnToolbar cxd-Panel-footer"
>
<button
class="cxd-Button cxd-Button--primary cxd-Button--size-default"
type="submit"
>
<span>
Submit
</span>
</button>
</div>
</div>
<div
class="resize-sensor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
class="resize-sensor-expand"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
/>
</div>
<div
class="resize-sensor-shrink"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
/>
</div>
<div
class="resize-sensor-appear"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Renderer:range with marks 1`] = `
<div>
<div

View File

@ -1,3 +1,15 @@
/**
* InputRange
*
* 01. showInput
* 02. multiple & clearable & delimiter
* 03. showSteps
* 04. marks
* 05. tooltipVisible & tooltipPlacement
* 06. min & max & step & joinValues
* 07. min & max & step
*/
import React = require('react');
import {render, fireEvent, screen, waitFor} from '@testing-library/react';
import '../../../src';
@ -300,3 +312,84 @@ test('Renderer:range with min & max & step & joinValues', async () => {
expect(container).toMatchSnapshot();
});
test('Renderer: range with min & max & step by variables', async () => {
const onSubmit = jest.fn();
const submitBtnText = 'Submit';
const {container} = render(
amisRender(
{
type: 'page',
data: {
min: '6',
max: '66',
step: '2'
},
body: {
type: 'form',
submitText: submitBtnText,
api: '/api/mock2/form/saveForm',
body: [
{
type: 'input-range',
label: '滑块',
name: 'range',
min: '${min}',
max: '${max}',
step: '${step}',
showInput: true
}
]
}
},
{onSubmit},
makeEnv({})
)
);
const inputs = container.querySelectorAll('.cxd-InputRange-input input');
const inputEl = inputs[0];
const calculatePercentage = (value: number) => ((value - 6) / (66 - 6)) * 100;
/** min & max 正常解析 */
fireEvent.change(inputEl, {target: {value: 88}});
fireEvent.blur(inputEl);
await wait(200);
expect(
(
container.querySelector('.cxd-InputRange-track-active') as Element
).getAttribute('style')
).toContain(`width: 100%; left: 0%;`);
await wait(200);
fireEvent.change(inputEl, {target: {value: 0}});
fireEvent.blur(inputEl);
await wait(200);
expect(
(
container.querySelector('.cxd-InputRange-track-active') as Element
).getAttribute('style')
).toContain(`width: 0%; left: 0%;`);
/** step正常解析 */
fireEvent.change(inputEl, {target: {value: 12}});
fireEvent.blur(inputEl);
await wait(200);
expect(
(
container.querySelector('.cxd-InputRange-track-active') as Element
).getAttribute('style')
).toContain(`width: ${calculatePercentage(12)}%; left: 0%;`);
/** 提交参数 */
await wait(500);
const submitBtn = screen.getByRole('button', {name: submitBtnText});
await waitFor(() => {
expect(submitBtn).toBeInTheDocument();
});
fireEvent.click(submitBtn);
await wait(500);
const formData = onSubmit.mock.calls[0][0];
expect(onSubmit).toHaveBeenCalled();
expect(formData).toEqual({
range: 12
});
expect(container).toMatchSnapshot();
});

View File

@ -12,12 +12,16 @@ import {
stripNumber,
filter,
ActionObject,
isMobile
isMobile,
isPureVariable,
resolveVariableAndFilter
} from 'amis-core';
import {Range as InputRange, NumberInput, Icon} from 'amis-ui';
import {FormBaseControlSchema, SchemaObject} from '../../Schema';
import {supportStatic} from './StaticHoc';
import type {SchemaTokenizeableString} from '../../Schema';
/**
* Range
* https://aisuda.bce.baidu.com/amis/zh-CN/components/form/range
@ -39,17 +43,17 @@ export interface RangeControlSchema extends FormBaseControlSchema {
/**
*
*/
max?: number;
max?: number | SchemaTokenizeableString;
/**
*
*/
min?: number;
min?: number | SchemaTokenizeableString;
/**
*
*/
step?: number;
step?: number | SchemaTokenizeableString;
/**
*
@ -131,17 +135,17 @@ export interface RangeProps extends FormControlProps {
/**
*
*/
min: number;
min: number | SchemaTokenizeableString;
/**
*
*/
max: number;
max: number | SchemaTokenizeableString;
/**
*
*/
step: number;
step: number | SchemaTokenizeableString;
/**
*
@ -241,7 +245,11 @@ export interface DefaultProps {
tooltipPlacement: TooltipPosType;
}
export interface RangeItemProps extends RangeProps {
export interface RangeItemProps
extends Omit<RangeProps, 'min' | 'max' | 'step'> {
min: number;
max: number;
step: number;
value: FormatValue;
onChange: (value: Value) => void;
onAfterChange: () => void;
@ -251,6 +259,29 @@ export interface RangeState {
value: FormatValue;
}
const resolveNumVariable = (
value: number | string | undefined,
data: Record<string, any> = {},
fallback: number
) => {
if (typeof value === 'string') {
value = isPureVariable(value)
? resolveVariableAndFilter(value, data)
: value;
if (typeof value === 'string') {
const result = parseFloat(value);
return isNaN(result) ? fallback : result;
} else if (typeof value === 'number') {
return value;
}
} else if (typeof value === 'number') {
return value;
}
return value ?? fallback;
};
/**
* value值
* @param value value值 Value
@ -336,7 +367,8 @@ export class Input extends React.Component<RangeItemProps, any> {
* @returns
*/
getStepPrecision() {
const {step} = this.props;
const {step: rawStep, data} = this.props;
const step = resolveNumVariable(rawStep, data, 1);
const stepIsDecimal = /^\d+\.\d+$/.test(step.toString());
return !stepIsDecimal || step < 0
? 0
@ -350,7 +382,8 @@ export class Input extends React.Component<RangeItemProps, any> {
* @returns
*/
getValue(value: string | number, type?: string) {
const {max, min, step, value: stateValue} = this.props as RangeItemProps;
const {min, max, step, value: stateValue} = this.props as RangeItemProps;
// value为null、undefined时取对应的min/max
value = value ?? (type === 'min' ? min : max);
// 校正value为step的倍数
@ -488,12 +521,13 @@ export default class RangeControl extends React.PureComponent<
constructor(props: RangeProps) {
super(props);
const {value: propsValue, multiple, delimiter, min, max} = this.props;
const {value: propsValue, multiple, delimiter, min, max, data} = this.props;
const value = formatValue(propsValue, {
multiple,
delimiter,
min,
max
min: resolveNumVariable(min, data, 0),
max: resolveNumVariable(max, data, 0)
});
this.state = {
@ -502,26 +536,31 @@ export default class RangeControl extends React.PureComponent<
}
componentDidUpdate(prevProps: RangeProps) {
const {value, min, max} = prevProps;
const {value, min, max, data: prevData} = prevProps;
const {
value: nextPropsValue,
multiple,
delimiter,
min: nextPropsMin,
max: nextPropsMax,
data,
onChange
} = this.props;
const prevMin = resolveNumVariable(min, prevData, 0);
const prevMax = resolveNumVariable(max, prevData, 100);
const nextMin = resolveNumVariable(nextPropsMin, data, 0);
const nextMax = resolveNumVariable(nextPropsMax, data, 100);
if (
value !== nextPropsValue ||
min !== nextPropsMin ||
max !== nextPropsMax
prevMin !== nextMin ||
prevMax !== nextMax
) {
const value = formatValue(nextPropsValue, {
multiple,
delimiter,
min: nextPropsMin,
max: nextPropsMax
min: nextMin,
max: nextMax
});
this.setState({
value: this.getValue(value)
@ -531,7 +570,6 @@ export default class RangeControl extends React.PureComponent<
doAction(action: ActionObject, data: object, throwErrors: boolean) {
const actionType = action?.actionType as string;
const {multiple, min, max} = this.props;
if (!!~['clear', 'reset'].indexOf(actionType)) {
this.clearValue(actionType);
@ -540,7 +578,10 @@ export default class RangeControl extends React.PureComponent<
@autobind
clearValue(type: string = 'clear') {
const {multiple, min, max, onChange} = this.props;
const {multiple, min: rawMin, max: rawMax, data, onChange} = this.props;
const min = resolveNumVariable(rawMin, data, 0);
const max = resolveNumVariable(rawMax, data, 100);
let resetValue = this.props.resetValue;
if (type === 'clear') {
@ -623,6 +664,10 @@ export default class RangeControl extends React.PureComponent<
const {value} = this.state;
const props: RangeItemProps = {
...this.props,
/** 解析变量,下面组件透传属性时使用 props 即可 */
min: resolveNumVariable(this.props.min, this.props.data, 0),
max: resolveNumVariable(this.props.max, this.props.data, 0),
step: resolveNumVariable(this.props.step, this.props.data, 1),
value,
onChange: this.handleChange,
onAfterChange: this.onAfterChange