Skip to content

Commit

Permalink
fix: support nested arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
Hein Jeong authored and hein-j committed Nov 4, 2022
1 parent 3921f71 commit c4ab231
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,9 @@ export default function NestedJson(props) {
setFirstName1(initialValues[\\"first Name\\"]);
setErrors({});
};
const [currentBioFavoritetreesValue, setCurrentBioFavoritetreesValue] =
React.useState(undefined);
const bioFavoritetreesRef = React.createRef();
const [currentNicknames1Value, setCurrentNicknames1Value] =
React.useState(undefined);
const Nicknames1Ref = React.createRef();
Expand All @@ -1939,6 +1942,7 @@ export default function NestedJson(props) {
lastName: [],
\\"bio.favorite Quote\\": [],
\\"bio.favorite-Animal\\": [],
\\"bio.favorite-trees\\": [],
Nicknames1: [],
\\"nick-names2\\": [],
\\"first Name\\": [],
Expand Down Expand Up @@ -2105,6 +2109,54 @@ export default function NestedJson(props) {
hasError={errors[\\"bio.favorite-Animal\\"]?.hasError}
{...getOverrideProps(overrides, \\"bio.favorite-Animal\\")}
></TextField>
<ArrayField
onChange={async (items) => {
let values = items;
if (onChange) {
const modelFields = {
\\"first-Name\\": firstName,
lastName,
bio: { ...bio, [\\"favorite-trees\\"]: values },
Nicknames1,
\\"nick-names2\\": nicknames,
\\"first Name\\": firstName1,
};
const result = onChange(modelFields);
values = result?.bio?.[\\"favorite-trees\\"] ?? values;
}
setBio({ ...bio, [\\"favorite-trees\\"]: values });
setCurrentBioFavoritetreesValue(undefined);
}}
currentFieldValue={currentBioFavoritetreesValue}
label={\\"favorite trees\\"}
items={bio.favorite - trees ?? []}
hasError={errors?.[\\"bio.favorite-trees\\"]?.hasError}
setFieldValue={setCurrentBioFavoritetreesValue}
inputFieldRef={bio.favorite - treesRef}
defaultFieldValue={undefined}
>
<TextField
label=\\"favorite trees\\"
value={currentBioFavoritetreesValue}
onChange={(e) => {
let { value } = e.target;
if (errors[\\"bio.favorite-trees\\"]?.hasError) {
runValidationTasks(\\"bio.favorite-trees\\", value);
}
setCurrentBioFavoritetreesValue(value);
}}
onBlur={() =>
runValidationTasks(
\\"bio.favorite-trees\\",
currentBioFavoritetreesValue
)
}
errorMessage={errors[\\"bio.favorite-trees\\"]?.errorMessage}
hasError={errors[\\"bio.favorite-trees\\"]?.hasError}
ref={bioFavoritetreesRef}
{...getOverrideProps(overrides, \\"bio.favorite-trees\\")}
></TextField>
</ArrayField>
<ArrayField
onChange={async (items) => {
let values = items;
Expand Down Expand Up @@ -2273,6 +2325,7 @@ export declare type NestedJsonInputValues = {
bio?: {
\\"favorite Quote\\"?: string;
\\"favorite-Animal\\"?: string;
\\"favorite-trees\\"?: string[];
};
};
export declare type NestedJsonValidationValues = {
Expand All @@ -2284,6 +2337,7 @@ export declare type NestedJsonValidationValues = {
bio?: {
\\"favorite Quote\\"?: ValidationFunction<string>;
\\"favorite-Animal\\"?: ValidationFunction<string>;
\\"favorite-trees\\"?: ValidationFunction<string>;
};
};
export declare type FormProps<T> = Partial<T> & React.DOMAttributes<HTMLDivElement>;
Expand All @@ -2294,6 +2348,7 @@ export declare type NestedJsonOverridesProps = {
bio?: FormProps<HeadingProps>;
\\"bio.favorite Quote\\"?: FormProps<TextFieldProps>;
\\"bio.favorite-Animal\\"?: FormProps<TextFieldProps>;
\\"bio.favorite-trees\\"?: FormProps<TextFieldProps>;
Nicknames1?: FormProps<TextFieldProps>;
\\"nick-names2\\"?: FormProps<TextFieldProps>;
\\"first Name\\"?: FormProps<TextFieldProps>;
Expand Down
3 changes: 2 additions & 1 deletion packages/codegen-ui-react/lib/forms/form-renderer-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
buildAccessChain,
buildNestedStateSet,
capitalizeFirstLetter,
getArrayChildRefName,
getCurrentValueIdentifier,
getCurrentValueName,
getSetNameIdentifier,
Expand Down Expand Up @@ -222,7 +223,7 @@ export const addFormAttributes = (component: StudioComponent | StudioComponentCh
attributes.push(
factory.createJsxAttribute(
factory.createIdentifier('ref'),
factory.createJsxExpression(undefined, factory.createIdentifier(`${renderedVariableName}Ref`)),
factory.createJsxExpression(undefined, factory.createIdentifier(getArrayChildRefName(renderedVariableName))),
),
);
}
Expand Down
36 changes: 28 additions & 8 deletions packages/codegen-ui-react/lib/forms/form-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,21 @@ import {
PropertyName,
} from 'typescript';

export const getCurrentValueName = (fieldName: string) => `current${capitalizeFirstLetter(fieldName)}Value`;
// used just to sanitize nested array field names
// when rendering currentValue state and ref
const getVariableName = (input: string[]) =>
input.length > 1 ? input.join('').replace(/[^a-zA-Z0-9_$]/g, '') : input.join('');

export const getArrayChildRefName = (fieldName: string) => {
const paths = fieldName.split('.').map((path, i) => (i === 0 ? path : capitalizeFirstLetter(path)));
return `${getVariableName(paths)}Ref`;
};

// value of the child of an ArrayField
export const getCurrentValueName = (fieldName: string) => {
const paths = fieldName.split('.').map((path) => capitalizeFirstLetter(path));
return `current${getVariableName(paths)}Value`;
};

export const getCurrentValueIdentifier = (fieldName: string) =>
factory.createIdentifier(getCurrentValueName(fieldName));
Expand Down Expand Up @@ -100,6 +114,7 @@ export const getDefaultValueExpression = (
name: string,
componentType: string,
dataType?: DataFieldDataType,
isArray?: boolean,
): Expression => {
const componentTypeToDefaultValueMap: { [key: string]: Expression } = {
ToggleButton: factory.createFalse(),
Expand All @@ -109,14 +124,14 @@ export const getDefaultValueExpression = (
CheckboxField: factory.createFalse(),
};

if (isArray) {
return factory.createArrayLiteralExpression([], false);
}

// it's a nonModel or relationship object
if (dataType && typeof dataType === 'object' && !('enum' in dataType)) {
return factory.createObjectLiteralExpression();
}
// the name itself is a nested json object
if (name.split('.').length > 1) {
return factory.createObjectLiteralExpression();
}

if (componentType in componentTypeToDefaultValueMap) {
return componentTypeToDefaultValueMap[componentType];
Expand All @@ -132,16 +147,21 @@ export const getInitialValues = (fieldConfigs: Record<string, FieldConfigMetadat
const stateNames = new Set<string>();
const propertyAssignments = Object.entries(fieldConfigs).reduce<PropertyAssignment[]>(
(acc, [name, { dataType, componentType, isArray }]) => {
const isNested = name.includes('.');
// we are only setting top-level keys
const stateName = name.split('.')[0];
let initialValue = getDefaultValueExpression(name, componentType, dataType, isArray);
if (isNested) {
// if nested, just set up an empty object for the top-level key
initialValue = factory.createObjectLiteralExpression();
}
if (!stateNames.has(stateName)) {
acc.push(
factory.createPropertyAssignment(
isValidVariableName(stateName)
? factory.createIdentifier(stateName)
: factory.createStringLiteral(stateName),
isArray
? factory.createArrayLiteralExpression([], false)
: getDefaultValueExpression(name, componentType, dataType),
initialValue,
),
);
stateNames.add(stateName);
Expand Down
3 changes: 2 additions & 1 deletion packages/codegen-ui-react/lib/forms/react-form-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
} from './form-renderer-helper';
import {
buildUseStateExpression,
getArrayChildRefName,
getCurrentValueName,
getDefaultValueExpression,
getInitialValues,
Expand Down Expand Up @@ -469,7 +470,7 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer<
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
factory.createIdentifier(`${renderedName}Ref`),
factory.createIdentifier(getArrayChildRefName(renderedName)),
undefined,
undefined,
factory.createCallExpression(
Expand Down
24 changes: 15 additions & 9 deletions packages/codegen-ui-react/lib/utils/forms/array-field-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import { FieldConfigMetadata } from '@aws-amplify/codegen-ui/lib/types';
import { isValidVariableName } from '@aws-amplify/codegen-ui';
import { factory, JsxChild, JsxTagNamePropertyAccess, NodeFlags, SyntaxKind } from 'typescript';
import {
capitalizeFirstLetter,
getCurrentValueIdentifier,
getCurrentValueName,
getDefaultValueExpression,
getSetNameIdentifier,
setFieldState,
} from '../../forms/form-state';
import { buildOverrideOnChangeStatement } from '../../forms/form-renderer-helper';
import { addUseEffectWrapper } from '../generate-react-hooks';
Expand Down Expand Up @@ -1014,6 +1014,7 @@ export const renderArrayFieldComponent = (
const setStateName = getSetNameIdentifier(getCurrentValueName(renderedFieldName));
const valuesListName = factory.createIdentifier('values');
const onChangeArgName = factory.createIdentifier('items');

return factory.createJsxElement(
factory.createJsxOpeningElement(
factory.createIdentifier('ArrayField'),
Expand Down Expand Up @@ -1048,13 +1049,8 @@ export const renderArrayFieldComponent = (
),
),
buildOverrideOnChangeStatement(fieldName, fieldConfigs, valuesListName),
factory.createExpressionStatement(
factory.createCallExpression(
factory.createIdentifier(`set${capitalizeFirstLetter(renderedFieldName)}`),
undefined,
[valuesListName],
),
),
factory.createExpressionStatement(setFieldState(renderedFieldName, valuesListName)),

factory.createExpressionStatement(
factory.createCallExpression(setStateName, undefined, [
getDefaultValueExpression(fieldName, componentType, dataType),
Expand All @@ -1076,7 +1072,17 @@ export const renderArrayFieldComponent = (
),
factory.createJsxAttribute(
factory.createIdentifier('items'),
factory.createJsxExpression(undefined, factory.createIdentifier(renderedFieldName)),
factory.createJsxExpression(
undefined,
// render `?? []` if nested.
renderedFieldName.includes('.')
? factory.createBinaryExpression(
factory.createIdentifier(renderedFieldName),
factory.createToken(SyntaxKind.QuestionQuestionToken),
factory.createArrayLiteralExpression([], false),
)
: factory.createIdentifier(renderedFieldName),
),
),
factory.createJsxAttribute(
factory.createIdentifier('hasError'),
Expand Down
10 changes: 10 additions & 0 deletions packages/codegen-ui/example-schemas/forms/bio-nested-create.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
"position": {
"below": "bio.favorite Quote"
}
},
"bio.favorite-trees": {
"inputType": {
"type": "TextField",
"isArray": true
},
"label": "favorite trees",
"position": {
"below": "bio.favorite-Animal"
}
}
},
"formActionType": "create",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ describe('Forms', () => {
cy.visit('http://localhost:3000/form-tests');
});

const getInputByLabel = (label) => {
return cy.contains('label', label).parent().find('input');
};

describe('CustomFormCreateDog', () => {
it('should validate, clear, and submit', () => {
const ErrorMessageMap = {
Expand Down Expand Up @@ -85,10 +89,6 @@ describe('Forms', () => {
describe('DataStoreFormCreateAllSupportedFormFields', () => {
it('should save to DataStore', () => {
cy.get('#dataStoreFormCreateAllSupportedFormFields').within(() => {
const getInputByLabel = (label) => {
return cy.contains('label', label).parent().find('input');
};

getInputByLabel('String').type('MyString');

// String array
Expand Down Expand Up @@ -131,4 +131,17 @@ describe('Forms', () => {
});
});
});

describe('CustomFormCreateNestedJson', () => {
it('should have a working ArrayField', () => {
cy.get('#customFormCreateNestedJson').within(() => {
cy.contains('Add item').click();
getInputByLabel('Animals').type('String1');
cy.contains('Add').click();
cy.contains('Add item').click();
getInputByLabel('Animals').type('String2');
cy.contains('String1').should('exist');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const EXPECTED_SUCCESSFUL_CASES = new Set([
'ListingExpanderWithComponentSlot',
'CustomFormCreateDog',
'DataStoreFormCreateAllSupportedFormFields',
'CustomFormCreateNestedJson',
'ComponentWithDataBindingWithPredicate',
'ComponentWithDataBindingWithoutPredicate',
'ComponentWithSimplePropertyBinding',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import '@aws-amplify/ui-react/styles.css';
import { AmplifyProvider, View, Heading, Divider, Text } from '@aws-amplify/ui-react';
import { useState, useEffect, useRef } from 'react';
import { DataStore } from '@aws-amplify/datastore';
import { CustomFormCreateDog, DataStoreFormCreateAllSupportedFormFields } from './ui-components'; // eslint-disable-line import/extensions, max-len
import {
CustomFormCreateDog,
DataStoreFormCreateAllSupportedFormFields,
CustomFormCreateNestedJson,
} from './ui-components'; // eslint-disable-line import/extensions, max-len
import { AllSupportedFormFields } from './models';

export default function FormTests() {
Expand Down Expand Up @@ -83,6 +87,11 @@ export default function FormTests() {
/>
<Text>{dataStoreFormCreateAllSupportedFormFieldsRecord}</Text>
</View>
<Divider />
<Heading>Custom Form - CreateNestedJson</Heading>
<View id="customFormCreateNestedJson">
<CustomFormCreateNestedJson onSubmit={() => undefined} />
</View>
</AmplifyProvider>
);
}

0 comments on commit c4ab231

Please sign in to comment.