Skip to content

Commit

Permalink
feat(viewer): load dynamic input data
Browse files Browse the repository at this point in the history
Closes #197
  • Loading branch information
Skaiir authored and fake-join[bot] committed Jul 18, 2022
1 parent f7d8ca5 commit daf867b
Show file tree
Hide file tree
Showing 13 changed files with 646 additions and 568 deletions.
524 changes: 32 additions & 492 deletions package-lock.json

Large diffs are not rendered by default.

32 changes: 22 additions & 10 deletions packages/form-js-viewer/src/import/Importer.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,32 @@ export default class Importer {
const {
defaultValue,
_path,
type
type,
valuesKey
} = formField;

if (!_path) {
return importedData;
// get values defined via valuesKey

if (valuesKey) {
importedData = {
...importedData,
[ valuesKey ]: get(data, [ valuesKey ])
};
}

// (1) try to get value from data
// (2) try to get default value from form field
// (3) get empty value from form field
return {
...importedData,
[ _path[ 0 ] ]: get(data, _path, isUndefined(defaultValue) ? this._formFields.get(type).emptyValue : defaultValue)
};
// try to get value from data
// if unavailable - try to get default value from form field
// if unavailable - get empty value from form field

if (_path) {
importedData = {
...importedData,
[_path[0]]: get(data, _path, isUndefined(defaultValue) ? this._formFields.get(type).emptyValue : defaultValue),
};
}

return importedData;

}, {});
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/form-js-viewer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export { FormFieldRegistry } from './core';
export * from './render';
export * from './util';

const schemaVersion = 4;
const schemaVersion = 5;

export {
Form,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useContext } from 'preact/hooks';
import useValuesAsync, { LOAD_STATES } from '../../hooks/useValuesAsync';

import { FormContext } from '../../context';

Expand All @@ -25,8 +26,7 @@ export default function Checklist(props) {
const {
description,
id,
label,
values
label
} = field;

const toggleCheckbox = (v) => {
Expand All @@ -45,13 +45,18 @@ export default function Checklist(props) {
});
};

const {
state: loadState,
values: options
} = useValuesAsync(field);

const { formId } = useContext(FormContext);

return <div class={ formFieldClasses(type, errors) }>
<Label
label={ label } />
{
values.map((v, index) => {
loadState == LOAD_STATES.LOADED && options.map((v, index) => {
return (
<Label
id={ prefixId(`${id}-${index}`, formId) }
Expand All @@ -75,6 +80,9 @@ export default function Checklist(props) {
}

Checklist.create = function(options = {}) {

if (options.valuesKey) return options;

return {
values: [
{
Expand Down
20 changes: 14 additions & 6 deletions packages/form-js-viewer/src/render/components/form-fields/Radio.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useContext } from 'preact/hooks';
import useValuesAsync, { LOAD_STATES } from '../../hooks/useValuesAsync';

import { FormContext } from '../../context';

Expand Down Expand Up @@ -26,8 +27,7 @@ export default function Radio(props) {
description,
id,
label,
validate = {},
values
validate = {}
} = field;

const { required } = validate;
Expand All @@ -39,27 +39,32 @@ export default function Radio(props) {
});
};

const {
state: loadState,
values: options
} = useValuesAsync(field);

const { formId } = useContext(FormContext);

return <div class={ formFieldClasses(type, errors) }>
<Label
label={ label }
required={ required } />
{
values.map((v, index) => {
loadState == LOAD_STATES.LOADED && options.map((option, index) => {
return (
<Label
id={ prefixId(`${ id }-${ index }`, formId) }
key={ `${ id }-${ index }` }
label={ v.label }
label={ option.label }
required={ false }>
<input
checked={ v.value === value }
checked={ option.value === value }
class="fjs-input"
disabled={ disabled }
id={ prefixId(`${ id }-${ index }`, formId) }
type="radio"
onClick={ () => onChange(v.value) } />
onClick={ () => onChange(option.value) } />
</Label>
);
})
Expand All @@ -70,6 +75,9 @@ export default function Radio(props) {
}

Radio.create = function(options = {}) {

if (options.valuesKey) return options;

return {
values: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useContext } from 'preact/hooks';
import useOptionsAsync, { LOAD_STATES } from '../../hooks/useValuesAsync';

import { FormContext } from '../../context';

Expand All @@ -25,8 +26,7 @@ export default function Select(props) {
description,
id,
label,
validate = {},
values
validate = {}
} = field;

const { required } = validate;
Expand All @@ -38,6 +38,11 @@ export default function Select(props) {
});
};

const {
state: loadState,
values: options
} = useOptionsAsync(field);

const { formId } = useContext(FormContext);

return <div class={ formFieldClasses(type, errors) }>
Expand All @@ -53,12 +58,12 @@ export default function Select(props) {
value={ value || '' }>
<option value=""></option>
{
values.map((v, index) => {
loadState == LOAD_STATES.LOADED && options.map((option, index) => {
return (
<option
key={ `${ id }-${ index }` }
value={ v.value }>
{ v.label }
value={ option.value }>
{ option.label }
</option>
);
})
Expand All @@ -71,6 +76,8 @@ export default function Select(props) {

Select.create = function(options = {}) {

if (options.valuesKey) return options;

return {
values: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useContext, useEffect, useRef, useState } from 'preact/hooks';
import useValuesAsync, { LOAD_STATES } from '../../hooks/useValuesAsync';

import { FormContext } from '../../context';

Expand Down Expand Up @@ -31,8 +32,7 @@ export default function Taglist(props) {
const {
description,
id,
label,
values : options
label
} = field;

const { formId } = useContext(FormContext);
Expand All @@ -41,26 +41,41 @@ export default function Taglist(props) {
const [ filteredValues, setFilteredValues ] = useState([]);
const [ isDropdownExpanded, setIsDropdownExpanded ] = useState(false);
const [ hasValuesLeft, setHasValuesLeft ] = useState(true);
const [ escapeClose, setEscapeClose ] = useState(false);
const [ isEscapeClosed, setIsEscapeClose ] = useState(false);
const searchbarRef = useRef();

const {
state: loadState,
values: options
} = useValuesAsync(field);


// Usage of stringify is necessary here because we want this effect to only trigger when there is a value change to the array
useEffect(() => {
const selectedValues = values.map(v => options.find(o => o.value === v)).filter(v => v !== undefined);
setSelectedValues(selectedValues);
}, [ JSON.stringify(values), options ]);
if (loadState === LOAD_STATES.LOADED) {
const selectedValues = values.map(v => options.find(o => o.value === v)).filter(v => v !== undefined);
setSelectedValues(selectedValues);
}
else {
setSelectedValues([]);
}
}, [ JSON.stringify(values), options, loadState ]);

useEffect(() => {
setFilteredValues(options.filter((o) => o.label && (o.label.toLowerCase().includes(filter.toLowerCase())) && !values.includes(o.value)));
if (loadState === LOAD_STATES.LOADED) {
setFilteredValues(options.filter((o) => o.label && (o.label.toLowerCase().includes(filter.toLowerCase())) && !values.includes(o.value)));
}
else {
setFilteredValues([]);
}
}, [ filter, JSON.stringify(values), options ]);

useEffect(() => {
setHasValuesLeft(selectedValues.length < options.length);
}, [ selectedValues.length, options.length ]);

const onFilterChange = ({ target }) => {
setEscapeClose(false);
setIsEscapeClose(false);
setFilter(target.value);
};

Expand Down Expand Up @@ -88,11 +103,11 @@ export default function Taglist(props) {
}
break;
case 'Escape':
setEscapeClose(true);
setIsEscapeClose(true);
break;
case 'Enter':
if (escapeClose) {
setEscapeClose(false);
if (isEscapeClosed) {
setIsEscapeClose(false);
}
break;
}
Expand All @@ -103,7 +118,7 @@ export default function Taglist(props) {
label={ label }
id={ prefixId(id, formId) } />
<div class={ classNames('fjs-taglist', { 'disabled': disabled }) }>
{!disabled &&
{!disabled && loadState === LOAD_STATES.LOADED &&
selectedValues.map((sv) => {
return (
<div class="fjs-taglist-tag" onMouseDown={ (e) => e.preventDefault() }>
Expand All @@ -126,12 +141,12 @@ export default function Taglist(props) {
placeholder={ 'Search' }
autoComplete="off"
onKeyDown={ (e) => onInputKeyDown(e) }
onMouseDown={ () => setEscapeClose(false) }
onMouseDown={ () => setIsEscapeClose(false) }
onFocus={ () => setIsDropdownExpanded(true) }
onBlur={ () => { setIsDropdownExpanded(false); setFilter(''); } } />
</div>
<div class="fjs-taglist-anchor">
{!disabled && isDropdownExpanded && !escapeClose && <DropdownList
{!disabled && loadState === LOAD_STATES.LOADED && isDropdownExpanded && !isEscapeClosed && <DropdownList
values={ filteredValues }
getLabel={ (v) => v.label }
onValueSelected={ (v) => selectValue(v) }
Expand All @@ -144,6 +159,9 @@ export default function Taglist(props) {
}

Taglist.create = function(options = {}) {

if (options.valuesKey) return options;

return {
values: [
{
Expand Down
63 changes: 63 additions & 0 deletions packages/form-js-viewer/src/render/hooks/useValuesAsync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useState } from 'preact/hooks';
import useService from './useService';

/**
* @enum { String }
*/
export const LOAD_STATES = {
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error'
};

/**
* @typedef {Object} ValuesGetter
* @property {Object[]} values - The values data
* @property {(LOAD_STATES)} state - The values data's loading state, to use for conditional rendering
*/

/**
* A hook to load values for single and multiselect components.
*
* @param {Object} field - The form field to handle values for
* @return {ValuesGetter} valuesGetter - A values getter object providing loading state and values
*/
export default function(field) {
const {
valuesKey,
values: staticValues
} = field;

const [ valuesGetter, setValuesGetter ] = useState({ values: [], error: undefined, state: LOAD_STATES.LOADING });
const initialData = useService('form')._getState().initialData;

useEffect(() => {

let values = [];

if (valuesKey !== undefined) {

const keyedValues = (initialData || {})[ valuesKey ];

if (keyedValues && Array.isArray(keyedValues)) {
values = keyedValues;
}
}
else if (staticValues !== undefined) {
values = Array.isArray(staticValues) ? staticValues : [];
}
else {
setValuesGetter(getErrorState('No values source defined in the form definition'));
return;
}

setValuesGetter(buildLoadedState(values));

}, [ valuesKey, staticValues, initialData ]);

return valuesGetter;
}

const getErrorState = (error) => ({ values: [], error, state: LOAD_STATES.ERROR });

const buildLoadedState = (values) => ({ values, error: undefined, state: LOAD_STATES.LOADED });
Loading

0 comments on commit daf867b

Please sign in to comment.