У меня есть сложный компонент React, который работает аналогично HTML5 input
. Это должно позволять следующее:
- Разрешить ввод частичного номера, например
5.
, 5.0
или 0.
. 5.0
является частичным, потому что я хочу разрешить два десятичных знака.
- Когда номер подтвержден, отправьте его родителю через
this.props
.
- Поддержка нескольких типов, таких как
int
, percent
, float
, переключаемых через некоторые действия. Каждый тип имеет свою валидацию.
- Разрешить специальные действия клавиатуры, такие как
UP
увеличение значения или DOWN
уменьшение его.
Я могу настроить наблюдаемый поток, который объединяет поток типов и поток ввода:
const rawInput = fromEvent<React.FormEvent<HTMLInputElement>>(this.input, "input").pipe(
map(v => v.currentTarget.value)
);
this.typeSubject.pipe(
switchMap(t => this.parseRawValue(rawInput, t))
).subscribe(
v => {
this.props.setValue(v);
this.setState({ isValid: true });
});
Я не знаю, как также объединить поток клавиатуры:
const pressedKey = fromEvent<React.KeyboardEvent<HTMLInputElement>>(this.input, "keydown").pipe(
map(k => k.keyCode),
filter(k => this.inputKeyCodes.indexOf(k) > -1)
);
Учитывая приведенный ниже пример кода большего размера, как мне объединить эти три потока, чтобы сохранить функциональность набора и проверки?
Я пробовал комбинации combineAll
, combineLatest
, merge
, mergeAll
и withLatestFrom
, но не смог создать правильный поток, не добавив значительно больше кода в parseRawValue
ниже.
Здесь определяется текущий компонент: TransformedImagesElement / src / components / editor / input / NumberInput.tsx
Вот большой пример кода с комментариями, чтобы помочь. Основной вопрос в пределах componentDidMount
:
import * as React from 'react';
import { fromEvent, Observable, Subject, BehaviorSubject } from "rxjs";
import { map, filter, switchMap } from 'rxjs/operators';
enum NumberInputExampleType {
int = "###",
percent = "%"
}
interface INumberInputExampleProps {
type: NumberInputExampleType;
value: number | null;
setValue(value: number): void;
max: number;
min: number;
}
interface INumberInputExampleState {
type?: NumberInputExampleType | null;
rawValue: string;
isValid: boolean;
}
export class NumberInputExample extends React.PureComponent<INumberInputExampleProps, INumberInputExampleState> {
state: INumberInputExampleState = {
type: null,
rawValue: "",
isValid: true,
}
private inputKeyCodes = [
38, // UP key
40 // DOWN key
];
// Reference to the input
private input: HTMLInputElement | null;
// Get type from the props, if the state is not yet set, or from state
private get type() {
return this.state.type || this.props.type;
}
// Subject for the type
private typeSubject: Subject<NumberInputExampleType> = new BehaviorSubject(this.type);
// This is called upon some action in the rendered elements
private switchType(newType: NumberInputExampleType): void {
this.typeSubject.next(newType);
this.setState({
type: newType,
rawValue: ""
});
}
private parseValue(): string {
// Return the raw value if it is not valid
if (this.state.rawValue !== "") {
return this.state.rawValue;
}
else if (this.props.value !== null) {
return this.getValue(this.props.value);
}
return "";
}
private getValue(value: number): string {
switch (this.type) {
case NumberInputExampleType.int:
//return(value * this.props.max).toString() // Return the internal value, a float, converted to the presentation value, a string, using this.props.max
case NumberInputExampleType.percent:
// Similar logic to the above, except converting to a valid percentage
}
}
componentDidMount() {
if (this.input) {
const rawInput = fromEvent<React.FormEvent<HTMLInputElement>>(this.input, "input").pipe(
map(v => v.currentTarget.value)
);
const pressedKey = fromEvent<React.KeyboardEvent<HTMLInputElement>>(this.input, "keydown").pipe(
map(k => k.keyCode),
filter(k => this.inputKeyCodes.indexOf(k) > -1)
);
// If pressedKey is UP, increment the value. If it is DOWN, decrement the value.
// How to combine rawInput and pressedKey while keeping the functionality of this.parseRawValue?
//
this.typeSubject.pipe(
switchMap(t => this.parseRawValue(rawInput, t))
).subscribe(
v => {
this.props.setValue(v);
this.setState({ isValid: true });
});
}
}
private parseRawValue(rawInput: Observable<string>, type: NumberInputExampleType): Observable<number> {
switch (type) {
case NumberInputExampleType.int:
return rawInput.pipe(
//filter(v => ... ), // Filter invalid input, allowing only numbers or numbers-while-typing, such as '5.'
//map(v => ... ), // Ensure the "number" is between this.props.max and this.props.min
map(this.storeValueInState),
//map(v / this.props.max) // Finally, convert the represented value to the internal value, a float
)
case NumberInputExampleType.percent:
// Similar logic to the above, except converting to a valid percentage. Filter out partial numbers
}
}
private storeValueInState(value: string): string {
this.setState({
rawValue: value,
isValid: false
});
return value;
};
render() {
return <input ref={e => this.input = e} value={this.parseValue()} />;
}
}
Я ожидаю, что при наборе текста должно произойти следующее, например, для NumberInputExampleType.percent
:
action | rawValue | state.isValid | this.props.value
-------+----------+---------------+-----------------
init | | true | null
type 1 | 1 | true | 1
type . | 1. | false | 1
type 0 | 1.0 | false | 1
type w | 1.0 | false | 1
type 1 | 1.01 | true | 1.01
key UP | 1.02 | true | 1.02
erase | | true | null
type 9 | 9 | true | 9
type 9 | 99 | true | 99
type . | 99. | false | 99
type 9 | 99.9 | false | 99
type 9 | 99.99 | true | 99.99
key UP | 100 | true | 100
key UP | 100 | true | 100