Skip to content

Commit b063d60

Browse files
committed
add test persistence components to @plotly/dash-test-components
1 parent 1586634 commit b063d60

File tree

3 files changed

+599
-1
lines changed

3 files changed

+599
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import React, {PureComponent} from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
// simple function to substitute is-numeric for our use case
5+
const isNumber = (n) => {
6+
return !isNaN(parseFloat(n)) && isFinite(n);
7+
}
8+
9+
// eslint-disable-next-line no-implicit-coercion
10+
const convert = val => (isNumber(val) ? +val : NaN);
11+
12+
const isEquivalent = (v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2));
13+
14+
// using these inline functions instead of ramda
15+
const isNil = val => val == null
16+
17+
const omit = (key, obj) => {
18+
const { [key]: omitted, ...rest } = obj;
19+
return rest;
20+
}
21+
22+
/**
23+
* A basic HTML input control for entering text, numbers, or passwords.
24+
*
25+
* Note that checkbox and radio types are supported through
26+
* the Checklist and RadioItems component. Dates, times, and file uploads
27+
* are also supported through separate components.
28+
*/
29+
export default class MyPersistedComponent extends PureComponent {
30+
constructor(props) {
31+
super(props);
32+
33+
this.input = React.createRef();
34+
35+
this.onChange = this.onChange.bind(this);
36+
this.onEvent = this.onEvent.bind(this);
37+
this.onKeyPress = this.onKeyPress.bind(this);
38+
this.setInputValue = this.setInputValue.bind(this);
39+
this.setPropValue = this.setPropValue.bind(this);
40+
}
41+
42+
UNSAFE_componentWillReceiveProps(nextProps) {
43+
const {value} = this.input.current;
44+
const valueAsNumber = convert(value);
45+
this.setInputValue(
46+
isNil(valueAsNumber) ? value : valueAsNumber,
47+
nextProps.value
48+
);
49+
if (this.props.type !== 'number') {
50+
this.setState({value: nextProps.value});
51+
}
52+
}
53+
54+
componentDidMount() {
55+
const {value} = this.input.current;
56+
const valueAsNumber = convert(value);
57+
this.setInputValue(
58+
isNil(valueAsNumber) ? value : valueAsNumber,
59+
this.props.value
60+
);
61+
}
62+
63+
UNSAFE_componentWillMount() {
64+
if (this.props.type !== 'number') {
65+
this.setState({value: this.props.value});
66+
}
67+
}
68+
69+
render() {
70+
const valprops =
71+
this.props.type === 'number' ? {} : {value: this.state.value};
72+
const {loading_state} = this.props;
73+
return (
74+
<input
75+
data-dash-is-loading={
76+
(loading_state && loading_state.is_loading) || undefined
77+
}
78+
ref={this.input}
79+
onChange={this.onChange}
80+
onKeyPress={this.onKeyPress}
81+
{...valprops}
82+
{...omit(
83+
[
84+
'debounce',
85+
'value',
86+
'n_submit',
87+
'n_submit_timestamp',
88+
'selectionDirection',
89+
'selectionEnd',
90+
'selectionStart',
91+
'setProps',
92+
'loading_state',
93+
],
94+
this.props
95+
)}
96+
/>
97+
);
98+
}
99+
100+
setInputValue(base, value) {
101+
const __value = value;
102+
base = this.input.current.checkValidity() ? convert(base) : NaN;
103+
value = convert(value);
104+
105+
if (!isEquivalent(base, value)) {
106+
this.input.current.value = isNumber(value) ? value : __value;
107+
}
108+
}
109+
110+
setPropValue(base, value) {
111+
base = convert(base);
112+
value = this.input.current.checkValidity() ? convert(value) : NaN;
113+
114+
if (!isEquivalent(base, value)) {
115+
this.props.setProps({value});
116+
}
117+
}
118+
119+
onEvent() {
120+
const {value} = this.input.current;
121+
const valueAsNumber = convert(value);
122+
if (this.props.type === 'number') {
123+
this.setPropValue(
124+
this.props.value,
125+
isNil(valueAsNumber) ? value : valueAsNumber
126+
);
127+
} else {
128+
this.props.setProps({value});
129+
}
130+
}
131+
132+
onKeyPress(e) {
133+
if (e.key === 'Enter') {
134+
this.props.setProps({
135+
n_submit: this.props.n_submit + 1,
136+
n_submit_timestamp: Date.now(),
137+
});
138+
this.input.current.checkValidity();
139+
}
140+
return this.props.debounce && e.key === 'Enter' && this.onEvent();
141+
}
142+
143+
onChange() {
144+
if (!this.props.debounce) {
145+
this.onEvent();
146+
} else if (this.props.type !== 'number') {
147+
this.setState({value: this.input.current.value});
148+
}
149+
}
150+
}
151+
152+
MyPersistedComponent.defaultProps = {
153+
type: 'text',
154+
n_submit: 0,
155+
n_submit_timestamp: -1,
156+
debounce: false,
157+
persisted_props: ['value'],
158+
persistence_type: 'local',
159+
};
160+
161+
MyPersistedComponent.propTypes = {
162+
/**
163+
* The ID of this component, used to identify dash components
164+
* in callbacks. The ID needs to be unique across all of the
165+
* components in an app.
166+
*/
167+
id: PropTypes.string,
168+
169+
/**
170+
* The value of the input
171+
*/
172+
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
173+
174+
/**
175+
* The input's inline styles
176+
*/
177+
style: PropTypes.object,
178+
179+
/**
180+
* The class of the input element
181+
*/
182+
className: PropTypes.string,
183+
184+
/**
185+
* If true, changes to input will be sent back to the Dash server only on enter or when losing focus.
186+
* If it's false, it will sent the value back on every change.
187+
*/
188+
debounce: PropTypes.bool,
189+
190+
/**
191+
* The type of control to render.
192+
*/
193+
type: PropTypes.oneOf([
194+
// Only allowing the input types with wide browser compatibility
195+
'text',
196+
'number',
197+
'password',
198+
'email',
199+
'range',
200+
'search',
201+
'tel',
202+
'url',
203+
'hidden',
204+
]),
205+
206+
/**
207+
* The name of the control, which is submitted with the form data.
208+
*/
209+
name: PropTypes.string,
210+
211+
/**
212+
* A regular expression that the control's value is checked against. The pattern must match the entire value, not just some subset. Use the title attribute to describe the pattern to help the user. This attribute applies when the value of the type attribute is text, search, tel, url, email, or password, otherwise it is ignored. The regular expression language is the same as JavaScript RegExp algorithm, with the 'u' parameter that makes it treat the pattern as a sequence of unicode code points. The pattern is not surrounded by forward slashes.
213+
*/
214+
pattern: PropTypes.string,
215+
216+
/**
217+
* A hint to the user of what can be entered in the control . The placeholder text must not contain carriage returns or line-feeds. Note: Do not use the placeholder attribute instead of a <label> element, their purposes are different. The <label> attribute describes the role of the form element (i.e. it indicates what kind of information is expected), and the placeholder attribute is a hint about the format that the content should take. There are cases in which the placeholder attribute is never displayed to the user, so the form must be understandable without it.
218+
*/
219+
placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
220+
221+
/**
222+
* Number of times the `Enter` key was pressed while the input had focus.
223+
*/
224+
n_submit: PropTypes.number,
225+
/**
226+
* Last time that `Enter` was pressed.
227+
*/
228+
n_submit_timestamp: PropTypes.number,
229+
230+
/**
231+
* Dash-assigned callback that gets fired when the value changes.
232+
*/
233+
setProps: PropTypes.func,
234+
235+
/**
236+
* Object that holds the loading state object coming from dash-renderer
237+
*/
238+
loading_state: PropTypes.shape({
239+
/**
240+
* Determines if the component is loading or not
241+
*/
242+
is_loading: PropTypes.bool,
243+
/**
244+
* Holds which property is loading
245+
*/
246+
prop_name: PropTypes.string,
247+
/**
248+
* Holds the name of the component that is loading
249+
*/
250+
component_name: PropTypes.string,
251+
}),
252+
253+
/**
254+
* Used to allow user interactions in this component to be persisted when
255+
* the component - or the page - is refreshed. If `persisted` is truthy and
256+
* hasn't changed from its previous value, a `value` that the user has
257+
* changed while using the app will keep that change, as long as
258+
* the new `value` also matches what was given originally.
259+
* Used in conjunction with `persistence_type`.
260+
*/
261+
persistence: PropTypes.oneOfType([
262+
PropTypes.bool,
263+
PropTypes.string,
264+
PropTypes.number,
265+
]),
266+
267+
/**
268+
* Properties whose user interactions will persist after refreshing the
269+
* component or the page. Since only `value` is allowed this prop can
270+
* normally be ignored.
271+
*/
272+
persisted_props: PropTypes.arrayOf(PropTypes.oneOf(['value'])),
273+
274+
/**
275+
* Where persisted user changes will be stored:
276+
* memory: only kept in memory, reset on page refresh.
277+
* local: window.localStorage, data is kept after the browser quit.
278+
* session: window.sessionStorage, data is cleared once the browser quit.
279+
*/
280+
persistence_type: PropTypes.oneOf(['local', 'session', 'memory']),
281+
};
282+
283+
MyPersistedComponent.persistenceTransforms = {
284+
value: {
285+
286+
extract: propValue => {
287+
if (!(propValue === null || propValue === undefined)) {
288+
return propValue.toUpperCase();
289+
}
290+
return propValue;
291+
},
292+
apply: storedValue => storedValue,
293+
294+
},
295+
};

0 commit comments

Comments
 (0)