Skip to content

Commit

Permalink
feat: implement viewer debounce
Browse files Browse the repository at this point in the history
Closes #958
  • Loading branch information
Skaiir committed Jan 2, 2024
1 parent a5a2bc7 commit 6e23317
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 28 deletions.
11 changes: 6 additions & 5 deletions packages/form-js-editor/src/FormEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,21 +248,22 @@ export default class FormEditor {
*/
_createInjector(options, container) {
const {
additionalModules = [],
modules = this._getModules(),
renderer = {}
additionalModules = [],
renderer = {},
...config
} = options;

const config = {
...options,
const enrichedConfig = {
...config,
renderer: {
...renderer,
container
}
};

return createInjector([
{ config: [ 'value', config ] },
{ config: [ 'value', enrichedConfig ] },
{ formEditor: [ 'value', this ] },
core,
...modules,
Expand Down
8 changes: 5 additions & 3 deletions packages/form-js-viewer/src/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,18 +335,20 @@ export default class Form {
*/
_createInjector(options, container) {
const {
modules = this._getModules(),
additionalModules = [],
modules = this._getModules()
...config
} = options;

const config = {
const enrichedConfig = {
...config,
renderer: {
container
}
};

return createInjector([
{ config: [ 'value', config ] },
{ config: [ 'value', enrichedConfig ] },
{ form: [ 'value', this ] },
core,
...modules,
Expand Down
4 changes: 2 additions & 2 deletions packages/form-js-viewer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export function createForm(options) {
const {
data,
schema,
...rest
...formOptions
} = options;

const form = new Form(rest);
const form = new Form(formOptions);

return form.importSchema(schema, data).then(function() {
return form;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Big from 'big.js';
import classNames from 'classnames';

import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
import useFlushDebounce from '../../hooks/useFlushDebounce';

import Description from '../Description';
import Errors from '../Errors';
Expand Down Expand Up @@ -32,8 +34,7 @@ export default function Numberfield(props) {
onFocus,
field,
value,
readonly,
onChange
readonly
} = props;

const {
Expand All @@ -57,6 +58,19 @@ export default function Numberfield(props) {

const [ stringValueCache, setStringValueCache ] = useState('');

const [ onChangeDebounced, flushOnChange ] = useFlushDebounce((params) => {
props.onChange(params);
}, [ props.onChange ]);

const onInputBlur = () => {
flushOnChange && flushOnChange();
onBlur && onBlur();
};

const onInputFocus = () => {
onFocus && onFocus();
};

// checks whether the value currently in the form data is practically different from the one in the input field cache
// this allows us to guarantee the field always displays valid form data, but without auto-simplifying values like 1.000 to 1
const cacheValueMatchesState = useMemo(() => Numberfield.config.sanitizeValue({ value, formField: field }) === Numberfield.config.sanitizeValue({ value: stringValueCache, formField: field }), [ stringValueCache, value, field ]);
Expand All @@ -82,7 +96,7 @@ export default function Numberfield(props) {

if (isNullEquivalentValue(stringValue)) {
setStringValueCache('');
onChange({ field, value: null });
onChangeDebounced({ field, value: null });
return;
}

Expand All @@ -96,14 +110,14 @@ export default function Numberfield(props) {

if (isNaN(Number(stringValue))) {
setStringValueCache('NaN');
onChange({ field, value: 'NaN' });
onChangeDebounced({ field, value: 'NaN' });
return;
}

setStringValueCache(stringValue);
onChange({ field, value: serializeToString ? stringValue : Number(stringValue) });
onChangeDebounced({ field, value: serializeToString ? stringValue : Number(stringValue) });

}, [ field, onChange, serializeToString ]);
}, [ field, onChangeDebounced, serializeToString ]);

const increment = () => {
if (readonly) {
Expand Down Expand Up @@ -187,8 +201,8 @@ export default function Numberfield(props) {
id={ domId }
onKeyDown={ onKeyDown }
onKeyPress={ onKeyPress }
onBlur={ () => onBlur && onBlur() }
onFocus={ () => onFocus && onFocus() }
onBlur={ onInputBlur }
onFocus={ onInputFocus }

// @ts-ignore
onInput={ (e) => setValue(e.target.value) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { isArray, isObject, isNil } from 'min-dash';

import { useEffect, useLayoutEffect, useRef } from 'preact/hooks';
import useFlushDebounce from '../../hooks/useFlushDebounce';

import { formFieldClasses } from '../Util';

import Description from '../Description';
Expand Down Expand Up @@ -28,14 +31,22 @@ export default function Textarea(props) {
} = field;

const { required } = validate;

const textareaRef = useRef();

const onInput = ({ target }) => {
const [ onInputChange, flushOnChange ] = useFlushDebounce(({ target }) => {
props.onChange({
field,
value: target.value
});
}, [ props.onChange ]);

const onInputBlur = () => {
flushOnChange && flushOnChange();
onBlur && onBlur();
};

const onInputFocus = () => {
onFocus && onFocus();
};

useLayoutEffect(() => {
Expand All @@ -55,9 +66,9 @@ export default function Textarea(props) {
disabled={ disabled }
readonly={ readonly }
id={ domId }
onInput={ onInput }
onBlur={ () => onBlur && onBlur() }
onFocus={ () => onFocus && onFocus() }
onInput={ onInputChange }
onBlur={ onInputBlur }
onFocus={ onInputFocus }
value={ value }
ref={ textareaRef }
aria-describedby={ errorMessageId } />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Errors from '../Errors';
import Label from '../Label';
import InputAdorner from './parts/TemplatedInputAdorner';

import useFlushDebounce from '../../hooks/useFlushDebounce';

const type = 'textfield';

export default function Textfield(props) {
Expand Down Expand Up @@ -35,11 +37,20 @@ export default function Textfield(props) {

const { required } = validate;

const onChange = ({ target }) => {
const [ onInputChange, flushOnChange ] = useFlushDebounce(({ target }) => {
props.onChange({
field,
value: target.value
});
}, [ props.onChange ]);

const onInputBlur = () => {
flushOnChange && flushOnChange();
onBlur && onBlur();
};

const onInputFocus = () => {
onFocus && onFocus();
};

return <div class={ formFieldClasses(type, { errors, disabled, readonly }) }>
Expand All @@ -53,9 +64,9 @@ export default function Textfield(props) {
disabled={ disabled }
readOnly={ readonly }
id={ domId }
onInput={ onChange }
onBlur={ () => onBlur && onBlur() }
onFocus={ () => onFocus && onFocus() }
onInput={ onInputChange }
onBlur={ onInputBlur }
onFocus={ onInputFocus }
type="text"
value={ value }
aria-describedby={ errorMessageId } />
Expand Down
50 changes: 50 additions & 0 deletions packages/form-js-viewer/src/render/hooks/useFlushDebounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useCallback, useRef } from 'preact/hooks';
import useService from './useService';

function useFlushDebounce(func, additionalDeps = []) {

const timeoutRef = useRef(null);
const lastArgsRef = useRef(null);

const config = useService('config', false);
const debounce = config && config.debounce;
const shouldDebounce = debounce !== false && debounce !== 0;
const delay = typeof debounce === 'number' ? debounce : 300;

const debounceFunc = useCallback((...args) => {

if (!shouldDebounce) {
func(...args);
return;
}

lastArgsRef.current = args;

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
func(...lastArgsRef.current);
lastArgsRef.current = null;
}, delay);

}, [ func, delay, shouldDebounce, ...additionalDeps ]);

const flushFunc = useCallback(() => {

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
if (lastArgsRef.current !== null) {
func(...lastArgsRef.current);
lastArgsRef.current = null;
}
timeoutRef.current = null;
}

}, [ func, ...additionalDeps ]);

return [ debounceFunc, flushFunc ];
}

export default useFlushDebounce;
2 changes: 1 addition & 1 deletion packages/form-js-viewer/test/spec/Form.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('Form', function() {

const bootstrapForm = ({ bootstrapExecute = () => {}, ...options }) => {
return act(async () => {
form = await createForm(options);
form = await createForm({ debounce: false, ...options });
bootstrapExecute(form);
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export function createMockInjector(services = {}, options = {}) {
return injector;
}

const VIEWER_CONFIG = {
debounce: false
};

function _createMockModule(services, options) {

return {
Expand All @@ -21,6 +25,8 @@ function _createMockModule(services, options) {
formFieldRegistry: [ 'value', services.formFieldRegistry || new FormFieldRegistryMock(options) ],
pathRegistry: [ 'value', services.pathRegistry || new PathRegistryMock(options) ],
eventBus: [ 'value', services.eventBus || new EventBusMock(options) ],
debounce: [ 'value', services.debounce || (fn => fn) ],
config: [ 'value', services.config || VIEWER_CONFIG ],

// using actual implementations in testing
formFields: services.formFields ? [ 'value', services.formFields ] : [ 'type', FormFields ],
Expand Down

0 comments on commit 6e23317

Please sign in to comment.