From 1dcda8390aab1539f607474885a98526763828a9 Mon Sep 17 00:00:00 2001 From: Scott Young Date: Tue, 13 Dec 2022 09:39:50 -0800 Subject: [PATCH] fix: make textfield a controlled component (#830) * fix: make textfield a controlled component * fix: update cypress form test * fix: cypress form test * fix: update timestamp assertion * chore: reset test data Co-authored-by: Scott Young --- ...studio-ui-codegen-react-forms.test.ts.snap | 406 +++++++++++------- .../__tests__/forms/component-helper.test.ts | 42 ++ .../lib/__tests__/forms/form-state.test.ts | 14 +- .../studio-ui-codegen-react-forms.test.ts | 11 +- .../lib/forms/component-helper.ts | 58 ++- .../lib/forms/form-renderer-helper.ts | 45 +- .../codegen-ui-react/lib/forms/form-state.ts | 1 + .../lib/forms/react-form-renderer.ts | 16 +- .../helpers/form-field.test.ts | 3 +- .../cypress/e2e/form-spec.cy.ts | 2 + 10 files changed, 419 insertions(+), 179 deletions(-) create mode 100644 packages/codegen-ui-react/lib/__tests__/forms/component-helper.test.ts diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap index a93dea864..626a195c4 100644 --- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap +++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-forms.test.ts.snap @@ -19,12 +19,12 @@ export default function CustomDataForm(props) { const { onSubmit, onCancel, onValidate, onChange, overrides, ...rest } = props; const initialValues = { - name: undefined, - email: undefined, + name: \\"\\", + email: \\"\\", city: undefined, category: undefined, pages: 0, - phone: undefined, + phone: \\"\\", }; const [name, setName] = React.useState(initialValues.name); const [email, setEmail] = React.useState(initialValues.email); @@ -103,6 +103,7 @@ export default function CustomDataForm(props) { label=\\"name\\" isRequired={true} defaultValue=\\"John Doe\\" + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -131,6 +132,7 @@ export default function CustomDataForm(props) { label=\\"E-mail\\" isRequired={true} defaultValue=\\"johndoe@amplify.com\\" + value={email} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -276,6 +278,7 @@ export default function CustomDataForm(props) { isRequired={true} defaultValue=\\"+1-401-152-6995\\" type=\\"tel\\" + value={phone} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -307,7 +310,10 @@ export default function CustomDataForm(props) { { let { value } = e.target; if (onChange) { @@ -517,6 +524,7 @@ export default function CustomDataForm(props) { label=\\"E-mail\\" isRequired={true} defaultValue=\\"johndoe@amplify.com\\" + value={email} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -662,6 +670,7 @@ export default function CustomDataForm(props) { isRequired={true} defaultValue=\\"+1-401-152-6995\\" type=\\"tel\\" + value={phone} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -693,7 +702,10 @@ export default function CustomDataForm(props) { { setName(initialValues.name); setEmail(initialValues.email); - setCurrentEmailValue(undefined); + setCurrentEmailValue(\\"\\"); setPhone(initialValues.phone); - setCurrentPhoneValue(undefined); + setCurrentPhoneValue(\\"\\"); setErrors({}); }; - const [currentEmailValue, setCurrentEmailValue] = React.useState(undefined); + const [currentEmailValue, setCurrentEmailValue] = React.useState(\\"\\"); const emailRef = React.createRef(); - const [currentPhoneValue, setCurrentPhoneValue] = React.useState(undefined); + const [currentPhoneValue, setCurrentPhoneValue] = React.useState(\\"\\"); const phoneRef = React.createRef(); const validations = { name: [{ type: \\"Required\\" }], @@ -997,6 +1009,7 @@ export default function CustomDataForm(props) { label=\\"name\\" isRequired={true} defaultValue=\\"John Doe\\" + value={name} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -1031,7 +1044,7 @@ export default function CustomDataForm(props) { values = result?.email ?? values; } setEmail(values); - setCurrentEmailValue(undefined); + setCurrentEmailValue(\\"\\"); }} currentFieldValue={currentEmailValue} label={\\"E-mail\\"} @@ -1039,7 +1052,7 @@ export default function CustomDataForm(props) { hasError={errors.email?.hasError} setFieldValue={setCurrentEmailValue} inputFieldRef={emailRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { + event.preventDefault(); + resetStateValues(); + }} {...getOverrideProps(overrides, \\"ClearButton\\")} > { let { value } = e.target; if (onChange) { @@ -1332,7 +1348,7 @@ export default function CustomDataForm(props) { label=\\"E-mail\\" isRequired={true} defaultValue=\\"johndoe@amplify.com\\" - defaultValue={email} + value={email} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -1509,7 +1525,10 @@ export default function CustomDataForm(props) { { + event.preventDefault(); + resetStateValues(); + }} {...getOverrideProps(overrides, \\"ClearButton\\")} > { let { value } = e.target; if (onChange) { @@ -1931,6 +1954,7 @@ export default function MyPostForm(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -1972,7 +1996,7 @@ export default function MyPostForm(props) { values = result?.Customtags ?? values; } setCustomtags(values); - setCurrentCustomtagsValue(undefined); + setCurrentCustomtagsValue(\\"\\"); }} currentFieldValue={currentCustomtagsValue} label={\\"Tags\\"} @@ -1980,7 +2004,7 @@ export default function MyPostForm(props) { hasError={errors.Customtags?.hasError} setFieldValue={setCurrentCustomtagsValue} inputFieldRef={CustomtagsRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -2062,6 +2087,7 @@ export default function MyPostForm(props) { label=\\"Profile url\\" isRequired={false} isReadOnly={false} + value={profile_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -2143,7 +2169,7 @@ export default function MyPostForm(props: MyPostFormProps): React.ReactElement; " `; -exports[`amplify form renderer tests custom form tests should render nested json fields 1`] = ` +exports[`amplify form renderer tests custom form tests should render nested json fields for create form 1`] = ` "/* eslint-disable */ import * as React from \\"react\\"; import { fetchByPath, validateField } from \\"./utils\\"; @@ -2298,12 +2324,12 @@ export default function NestedJson(props) { props; const { tokens } = useTheme(); const initialValues = { - \\"first-Name\\": undefined, - lastName: undefined, + \\"first-Name\\": \\"\\", + lastName: \\"\\", bio: {}, Nicknames1: [], \\"nick-names2\\": [], - \\"first Name\\": undefined, + \\"first Name\\": \\"\\", options: {}, }; const [firstName, setFirstName] = React.useState(initialValues[\\"first-Name\\"]); @@ -2323,21 +2349,20 @@ export default function NestedJson(props) { setLastName(initialValues.lastName); setBio(initialValues.bio); setNicknames1(initialValues.Nicknames1); - setCurrentNicknames1Value(undefined); + setCurrentNicknames1Value(\\"\\"); setNicknames(initialValues[\\"nick-names2\\"]); - setCurrentNicknamesValue(undefined); + setCurrentNicknamesValue(\\"\\"); setFirstName1(initialValues[\\"first Name\\"]); setOptions(initialValues.options); setErrors({}); }; const [currentBioFavoritetreesValue, setCurrentBioFavoritetreesValue] = - React.useState(undefined); + React.useState(\\"\\"); const bioFavoritetreesRef = React.createRef(); const [currentNicknames1Value, setCurrentNicknames1Value] = - React.useState(undefined); + React.useState(\\"\\"); const Nicknames1Ref = React.createRef(); - const [currentNicknamesValue, setCurrentNicknamesValue] = - React.useState(undefined); + const [currentNicknamesValue, setCurrentNicknamesValue] = React.useState(\\"\\"); const nicknamesRef = React.createRef(); const validations = { \\"first-Name\\": [], @@ -2402,6 +2427,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2429,6 +2455,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2461,6 +2488,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2490,6 +2518,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -2534,7 +2563,7 @@ export default function NestedJson(props) { values = result?.bio?.[\\"favorite-trees\\"] ?? values; } setBio({ ...bio, [\\"favorite-trees\\"]: values }); - setCurrentBioFavoritetreesValue(undefined); + setCurrentBioFavoritetreesValue(\\"\\"); }} currentFieldValue={currentBioFavoritetreesValue} label={\\"favorite trees\\"} @@ -2542,11 +2571,11 @@ export default function NestedJson(props) { hasError={errors?.[\\"bio.favorite-trees\\"]?.hasError} setFieldValue={setCurrentBioFavoritetreesValue} inputFieldRef={bioFavoritetreesRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (errors[\\"bio.favorite-trees\\"]?.hasError) { @@ -2588,7 +2617,7 @@ export default function NestedJson(props) { values = result?.Nicknames1 ?? values; } setNicknames1(values); - setCurrentNicknames1Value(undefined); + setCurrentNicknames1Value(\\"\\"); }} currentFieldValue={currentNicknames1Value} label={\\"Nick Names1\\"} @@ -2596,7 +2625,7 @@ export default function NestedJson(props) { hasError={errors.Nicknames1?.hasError} setFieldValue={setCurrentNicknames1Value} inputFieldRef={Nicknames1Ref} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -2726,7 +2756,10 @@ export default function NestedJson(props) { { let { value } = e.target; if (onChange) { @@ -3133,7 +3165,7 @@ export default function NestedJson(props) { values = result?.lastName ?? values; } setLastName1(values); - setCurrentLastName1Value(undefined); + setCurrentLastName1Value(\\"\\"); }} currentFieldValue={currentLastName1Value} label={\\"lastName\\"} @@ -3141,7 +3173,7 @@ export default function NestedJson(props) { hasError={errors.lastName?.hasError} setFieldValue={setCurrentLastName1Value} inputFieldRef={lastName1Ref} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -3194,7 +3226,7 @@ export default function NestedJson(props) { > { let { value } = e.target; if (onChange) { @@ -3226,7 +3258,10 @@ export default function NestedJson(props) { { let { value } = e.target; if (onChange) { @@ -3420,7 +3456,10 @@ export default function CustomWithSectionalElements(props) { { + event.preventDefault(); + resetStateValues(); + }} {...getOverrideProps(overrides, \\"ClearButton\\")} > { let { value } = e.target; if (onChange) { @@ -3668,6 +3711,7 @@ export default function MyPostForm(props) { label=\\"Username\\" isRequired={false} isReadOnly={false} + value={username} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -3695,6 +3739,7 @@ export default function MyPostForm(props) { label=\\"Post url\\" isRequired={false} isReadOnly={false} + value={post_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -3749,6 +3794,7 @@ export default function MyPostForm(props) { label=\\"Profile url\\" isRequired={false} isReadOnly={false} + value={profile_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -3854,10 +3900,10 @@ export default function MyPostForm(props) { } = props; const initialValues = { TextAreaFieldbbd63464: undefined, - caption: undefined, - username: undefined, - profile_url: undefined, - post_url: undefined, + caption: \\"\\", + username: \\"\\", + profile_url: \\"\\", + post_url: \\"\\", metadata: undefined, }; const [TextAreaFieldbbd63464, setTextAreaFieldbbd63464] = React.useState( @@ -3976,7 +4022,10 @@ export default function MyPostForm(props) { @@ -4036,7 +4085,7 @@ export default function MyPostForm(props) { label=\\"Caption\\" isRequired={false} isReadOnly={false} - defaultValue={caption} + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4065,7 +4114,7 @@ export default function MyPostForm(props) { label=\\"Username\\" isRequired={false} isReadOnly={false} - defaultValue={username} + value={username} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4094,7 +4143,7 @@ export default function MyPostForm(props) { label=\\"Profile url\\" isRequired={false} isReadOnly={false} - defaultValue={profile_url} + value={profile_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4123,7 +4172,7 @@ export default function MyPostForm(props) { label=\\"Post url\\" isRequired={false} isReadOnly={false} - defaultValue={post_url} + value={post_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4184,7 +4233,10 @@ export default function MyPostForm(props) { @@ -4432,11 +4484,11 @@ export default function MyFlexCreateForm(props) { ...rest } = props; const initialValues = { - username: undefined, - caption: undefined, + username: \\"\\", + caption: \\"\\", Customtags: [], tags: [], - profile_url: undefined, + profile_url: \\"\\", }; const [username, setUsername] = React.useState(initialValues.username); const [caption, setCaption] = React.useState(initialValues.caption); @@ -4450,16 +4502,16 @@ export default function MyFlexCreateForm(props) { setUsername(initialValues.username); setCaption(initialValues.caption); setCustomtags(initialValues.Customtags); - setCurrentCustomtagsValue(undefined); + setCurrentCustomtagsValue(\\"\\"); setTags(initialValues.tags); - setCurrentTagsValue(undefined); + setCurrentTagsValue(\\"\\"); setProfile_url(initialValues.profile_url); setErrors({}); }; const [currentCustomtagsValue, setCurrentCustomtagsValue] = - React.useState(undefined); + React.useState(\\"\\"); const CustomtagsRef = React.createRef(); - const [currentTagsValue, setCurrentTagsValue] = React.useState(undefined); + const [currentTagsValue, setCurrentTagsValue] = React.useState(\\"\\"); const tagsRef = React.createRef(); const validations = { username: [ @@ -4544,7 +4596,10 @@ export default function MyFlexCreateForm(props) { { let { value } = e.target; if (onChange) { @@ -4607,6 +4663,7 @@ export default function MyFlexCreateForm(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -4646,7 +4703,7 @@ export default function MyFlexCreateForm(props) { values = result?.Customtags ?? values; } setCustomtags(values); - setCurrentCustomtagsValue(undefined); + setCurrentCustomtagsValue(\\"\\"); }} currentFieldValue={currentCustomtagsValue} label={\\"Tags\\"} @@ -4654,7 +4711,7 @@ export default function MyFlexCreateForm(props) { hasError={errors.Customtags?.hasError} setFieldValue={setCurrentCustomtagsValue} inputFieldRef={CustomtagsRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -4965,10 +5023,10 @@ export default function BlogCreateForm(props) { ...rest } = props; const initialValues = { - title: undefined, - content: undefined, + title: \\"\\", + content: \\"\\", switch: false, - published: undefined, + published: \\"\\", editedAt: [], }; const [title, setTitle] = React.useState(initialValues.title); @@ -4983,11 +5041,10 @@ export default function BlogCreateForm(props) { setSwitch1(initialValues.switch); setPublished(initialValues.published); setEditedAt(initialValues.editedAt); - setCurrentEditedAtValue(undefined); + setCurrentEditedAtValue(\\"\\"); setErrors({}); }; - const [currentEditedAtValue, setCurrentEditedAtValue] = - React.useState(undefined); + const [currentEditedAtValue, setCurrentEditedAtValue] = React.useState(\\"\\"); const editedAtRef = React.createRef(); const validations = { title: [], @@ -5086,6 +5143,7 @@ export default function BlogCreateForm(props) { label=\\"Title\\" isRequired={false} isReadOnly={false} + value={title} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -5113,6 +5171,7 @@ export default function BlogCreateForm(props) { label=\\"Content\\" isRequired={false} isReadOnly={false} + value={content} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -5169,6 +5228,7 @@ export default function BlogCreateForm(props) { isRequired={false} isReadOnly={false} type=\\"datetime-local\\" + value={published && convertToLocal(new Date(published))} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -5207,7 +5267,7 @@ export default function BlogCreateForm(props) { values = result?.editedAt ?? values; } setEditedAt(values); - setCurrentEditedAtValue(undefined); + setCurrentEditedAtValue(\\"\\"); }} currentFieldValue={currentEditedAtValue} label={\\"Edited at\\"} @@ -5215,7 +5275,7 @@ export default function BlogCreateForm(props) { hasError={errors.editedAt?.hasError} setFieldValue={setCurrentEditedAtValue} inputFieldRef={editedAtRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { + event.preventDefault(); + resetStateValues(); + }} {...getOverrideProps(overrides, \\"ClearButton\\")} > ({ ...errors, [fieldName]: validationResponse })); return validationResponse; }; + const convertTimeStampToDate = (ts) => { + if (Math.abs(Date.now() - ts) < Math.abs(Date.now() - ts * 1000)) { + return new Date(ts); + } + return new Date(ts * 1000); + }; + const convertToLocal = (date) => { + const df = new Intl.DateTimeFormat(\\"default\\", { + year: \\"numeric\\", + month: \\"2-digit\\", + day: \\"2-digit\\", + hour: \\"2-digit\\", + minute: \\"2-digit\\", + calendar: \\"iso8601\\", + numberingSystem: \\"latn\\", + hour12: false, + }); + const parts = df.formatToParts(date).reduce((acc, part) => { + acc[part.type] = part.value; + return acc; + }, {}); + return \`\${parts.year}-\${parts.month}-\${parts.day}T\${parts.hour}:\${parts.minute}\`; + }; return ( { let value = parseInt(e.target.value); if (isNaN(value)) { @@ -5682,6 +5769,7 @@ export default function InputGalleryCreateForm(props) { isReadOnly={false} type=\\"number\\" step=\\"any\\" + value={rootbeer} onChange={(e) => { let value = Number(e.target.value); if (isNaN(value)) { @@ -5853,7 +5941,7 @@ export default function InputGalleryCreateForm(props) { values = result?.arrayTypeField ?? values; } setArrayTypeField(values); - setCurrentArrayTypeFieldValue(undefined); + setCurrentArrayTypeFieldValue(\\"\\"); }} currentFieldValue={currentArrayTypeFieldValue} label={\\"Array type field\\"} @@ -5861,7 +5949,7 @@ export default function InputGalleryCreateForm(props) { hasError={errors.arrayTypeField?.hasError} setFieldValue={setCurrentArrayTypeFieldValue} inputFieldRef={arrayTypeFieldRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { const date = new Date(e.target.value); if (!(date instanceof Date && !isNaN(date))) { @@ -6013,6 +6102,7 @@ export default function InputGalleryCreateForm(props) { label=\\"Ippy\\" isRequired={false} isReadOnly={false} + value={ippy} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -6047,6 +6137,7 @@ export default function InputGalleryCreateForm(props) { isRequired={false} isReadOnly={false} type=\\"time\\" + value={timeisnow} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -6083,7 +6174,10 @@ export default function InputGalleryCreateForm(props) { { let value = parseInt(e.target.value); if (isNaN(value)) { @@ -6571,7 +6665,7 @@ export default function InputGalleryUpdateForm(props) { isReadOnly={false} type=\\"number\\" step=\\"any\\" - defaultValue={rootbeer} + value={rootbeer} onChange={(e) => { let value = Number(e.target.value); if (isNaN(value)) { @@ -6745,7 +6839,7 @@ export default function InputGalleryUpdateForm(props) { values = result?.arrayTypeField ?? values; } setArrayTypeField(values); - setCurrentArrayTypeFieldValue(undefined); + setCurrentArrayTypeFieldValue(\\"\\"); }} currentFieldValue={currentArrayTypeFieldValue} label={\\"Array type field\\"} @@ -6753,7 +6847,7 @@ export default function InputGalleryUpdateForm(props) { hasError={errors.arrayTypeField?.hasError} setFieldValue={setCurrentArrayTypeFieldValue} inputFieldRef={arrayTypeFieldRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { const date = new Date(e.target.value); if (!(date instanceof Date && !isNaN(date))) { @@ -6909,7 +7001,7 @@ export default function InputGalleryUpdateForm(props) { label=\\"Ippy\\" isRequired={false} isReadOnly={false} - defaultValue={ippy} + value={ippy} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -6944,7 +7036,7 @@ export default function InputGalleryUpdateForm(props) { isRequired={false} isReadOnly={false} type=\\"time\\" - defaultValue={timeisnow} + value={timeisnow} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -6981,7 +7073,10 @@ export default function InputGalleryUpdateForm(props) { @@ -7015,7 +7110,7 @@ export default function InputGalleryUpdateForm(props) { " `; -exports[`amplify form renderer tests datastore form tests should render a form with multiple date types 4`] = ` +exports[`amplify form renderer tests datastore form tests should render a form with multiple date types on update form 2`] = ` "import * as React from \\"react\\"; import { InputGallery } from \\"../models\\"; import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\"; @@ -7245,11 +7340,11 @@ export default function MyFlexCreateForm(props) { ...rest } = props; const initialValues = { - username: undefined, - caption: undefined, + username: \\"\\", + caption: \\"\\", Customtags: [], tags: [], - profile_url: undefined, + profile_url: \\"\\", }; const [username, setUsername] = React.useState(initialValues.username); const [caption, setCaption] = React.useState(initialValues.caption); @@ -7263,16 +7358,16 @@ export default function MyFlexCreateForm(props) { setUsername(initialValues.username); setCaption(initialValues.caption); setCustomtags(initialValues.Customtags); - setCurrentCustomtagsValue(undefined); + setCurrentCustomtagsValue(\\"\\"); setTags(initialValues.tags); - setCurrentTagsValue(undefined); + setCurrentTagsValue(\\"\\"); setProfile_url(initialValues.profile_url); setErrors({}); }; const [currentCustomtagsValue, setCurrentCustomtagsValue] = - React.useState(undefined); + React.useState(\\"\\"); const CustomtagsRef = React.createRef(); - const [currentTagsValue, setCurrentTagsValue] = React.useState(undefined); + const [currentTagsValue, setCurrentTagsValue] = React.useState(\\"\\"); const tagsRef = React.createRef(); const validations = { username: [ @@ -7357,7 +7452,10 @@ export default function MyFlexCreateForm(props) { { let { value } = e.target; if (onChange) { @@ -7420,6 +7519,7 @@ export default function MyFlexCreateForm(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -7459,7 +7559,7 @@ export default function MyFlexCreateForm(props) { values = result?.Customtags ?? values; } setCustomtags(values); - setCurrentCustomtagsValue(undefined); + setCurrentCustomtagsValue(\\"\\"); }} currentFieldValue={currentCustomtagsValue} label={\\"Tags\\"} @@ -7467,7 +7567,7 @@ export default function MyFlexCreateForm(props) { hasError={errors.Customtags?.hasError} setFieldValue={setCurrentCustomtagsValue} inputFieldRef={CustomtagsRef} - defaultFieldValue={undefined} + defaultFieldValue={\\"\\"} > { let { value } = e.target; if (onChange) { @@ -7642,10 +7743,10 @@ export default function PostCreateFormRow(props) { ...rest } = props; const initialValues = { - username: undefined, - caption: undefined, - post_url: undefined, - profile_url: undefined, + username: \\"\\", + caption: \\"\\", + post_url: \\"\\", + profile_url: \\"\\", status: undefined, metadata: undefined, }; @@ -7758,6 +7859,7 @@ export default function PostCreateFormRow(props) { isRequired={false} isReadOnly={false} placeholder=\\"john\\" + value={username} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -7787,6 +7889,7 @@ export default function PostCreateFormRow(props) { isRequired={false} isReadOnly={false} placeholder=\\"i love code\\" + value={caption} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -7817,6 +7920,7 @@ export default function PostCreateFormRow(props) { descriptiveText=\\"post url to use for the component\\" isRequired={false} isReadOnly={false} + value={post_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -7846,6 +7950,7 @@ export default function PostCreateFormRow(props) { descriptiveText=\\"profile image url\\" isRequired={false} isReadOnly={false} + value={profile_url} onChange={(e) => { let { value } = e.target; if (onChange) { @@ -7933,7 +8038,10 @@ export default function PostCreateFormRow(props) { { + it('should render TextField with value attribute', () => { + const attr = renderValueAttribute({ + fieldConfig: { + validationRules: [], + componentType: 'TextField', + }, + componentName: 'animalName', + }); + + expect(attr).toHaveProperty('initializer.expression.escapedText', 'animalName'); + }); +}); + +describe('render default value attribute', () => { + it('should render TextField with the value attribute', () => { + const attr = renderDefaultValueAttribute( + 'buildingName', + { validationRules: [], componentType: 'TextField' }, + 'TextField', + ); + + expect(attr).toHaveProperty('initializer.expression.escapedText', 'buildingName'); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts b/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts index 7f8896234..932daa2d7 100644 --- a/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/forms/form-state.test.ts @@ -14,7 +14,7 @@ limitations under the License. */ import { factory } from 'typescript'; -import { buildNestedStateSet, setFieldState } from '../../forms/form-state'; +import { buildNestedStateSet, setFieldState, getDefaultValueExpression } from '../../forms/form-state'; import { genericPrinter } from '../__utils__'; describe('nested state', () => { @@ -57,3 +57,15 @@ describe('set field state', () => { expect(response).toMatchSnapshot(); }); }); + +describe('get default values', () => { + it('should generate the proper default value for a TextField', () => { + const expression = getDefaultValueExpression('name', 'TextField'); + expect(expression).toMatchObject({ text: '' }); + }); + + it('should generate the proper default value for a SliderField', () => { + const expression = getDefaultValueExpression('name', 'SliderField'); + expect(expression).toMatchObject({ text: '0' }); + }); +}); diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts index c63c9ddff..2298d8dd8 100644 --- a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-forms.test.ts @@ -23,6 +23,7 @@ describe('amplify form renderer tests', () => { 'datastore/post', ); expect(componentText).toContain('DataStore.save'); + expect(componentText).toContain('resetStateValues();'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); }); @@ -57,22 +58,24 @@ describe('amplify form renderer tests', () => { expect(declaration).toMatchSnapshot(); }); - it('should render a form with multiple date types', () => { + it('should render a form with multiple date types on create form', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer( 'forms/input-gallery-create', 'datastore/input-gallery', ); expect(componentText).toContain('DataStore.save'); + expect(componentText).toContain('const convertToLocal'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); }); - it('should render a form with multiple date types', () => { + it('should render a form with multiple date types on update form', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer( 'forms/input-gallery-update', 'datastore/input-gallery', ); expect(componentText).toContain('DataStore.save'); + expect(componentText).toContain('const convertToLocal'); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); }); @@ -132,13 +135,13 @@ describe('amplify form renderer tests', () => { expect(declaration).toMatchSnapshot(); }); - it('should render nested json fields', () => { + it('should render nested json fields for create form', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/bio-nested-create', undefined); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); }); - it('should render nested json fields', () => { + it('should render nested json fields for update form', () => { const { componentText, declaration } = generateWithAmplifyFormRenderer('forms/bio-nested-update', undefined); expect(componentText).toMatchSnapshot(); expect(declaration).toMatchSnapshot(); diff --git a/packages/codegen-ui-react/lib/forms/component-helper.ts b/packages/codegen-ui-react/lib/forms/component-helper.ts index 2103158ff..a2ba254a2 100644 --- a/packages/codegen-ui-react/lib/forms/component-helper.ts +++ b/packages/codegen-ui-react/lib/forms/component-helper.ts @@ -15,11 +15,18 @@ */ import { FieldConfigMetadata, StudioDataSourceType, StudioFormActionType } from '@aws-amplify/codegen-ui'; -import { BinaryExpression, factory, Identifier, JsxAttribute, SyntaxKind } from 'typescript'; -import { resetValuesName } from './form-state'; +import { BinaryExpression, factory, Identifier, JsxAttribute, SyntaxKind, ElementAccessExpression } from 'typescript'; +import { getCurrentValueName, resetValuesName } from './form-state'; import { FIELD_TYPE_TO_TYPESCRIPT_MAP } from './typescript-type-map'; -export const ControlledComponents = ['StepperField', 'SliderField', 'SelectField', 'ToggleButton', 'SwitchField']; +export const ControlledComponents = [ + 'StepperField', + 'SliderField', + 'SelectField', + 'ToggleButton', + 'SwitchField', + 'TextField', +]; /** * given the component returns true if the component is a controlled component @@ -54,7 +61,11 @@ export const convertedValueAttributeMap: Record { +export const renderDefaultValueAttribute = ( + stateName: string, + { dataType }: FieldConfigMetadata, + componentType: string, +) => { const identifier = factory.createIdentifier(stateName); let expression = factory.createJsxExpression(undefined, identifier); @@ -62,7 +73,10 @@ export const renderDefaultValueAttribute = (stateName: string, { dataType }: Fie expression = factory.createJsxExpression(undefined, convertedValueAttributeMap[dataType](identifier)); } - return factory.createJsxAttribute(factory.createIdentifier('defaultValue'), expression); + return factory.createJsxAttribute( + componentType === 'TextField' ? factory.createIdentifier('value') : factory.createIdentifier('defaultValue'), + expression, + ); }; export const renderValueAttribute = ({ @@ -75,10 +89,33 @@ export const renderValueAttribute = ({ currentValueIdentifier?: Identifier; }): JsxAttribute | undefined => { const componentType = fieldConfig.studioFormComponentType ?? fieldConfig.componentType; + const { dataType } = fieldConfig; const shouldGetForUncontrolled = fieldConfig.isArray; const valueIdentifier = currentValueIdentifier || getValueIdentifier(componentName, componentType); + let renderedFieldName = fieldConfig.sanitizedFieldName || componentName; + if (fieldConfig.isArray) { + renderedFieldName = getCurrentValueName(renderedFieldName); + } + let fieldNameIdentifier: Identifier | ElementAccessExpression = factory.createIdentifier(renderedFieldName); + if (componentName.includes('.')) { + const [parent, child] = componentName.split('.'); + fieldNameIdentifier = factory.createElementAccessExpression( + factory.createIdentifier(parent), + factory.createStringLiteral(child), + ); + } + + let controlledExpression = factory.createJsxExpression(undefined, fieldNameIdentifier); + + if (dataType && typeof dataType !== 'object' && convertedValueAttributeMap[dataType]) { + controlledExpression = factory.createJsxExpression( + undefined, + convertedValueAttributeMap[dataType](valueIdentifier), + ); + } + const controlledComponentToAttributesMap: { [key: string]: JsxAttribute } = { ToggleButton: factory.createJsxAttribute( factory.createIdentifier('isPressed'), @@ -104,6 +141,15 @@ export const renderValueAttribute = ({ factory.createIdentifier('checked'), factory.createJsxExpression(undefined, valueIdentifier), ), + TextField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + DateTimeField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + IPAddressField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + DateField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + TimeField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + NumberField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + URLField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + PhoneNumberField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), + EmailField: factory.createJsxAttribute(factory.createIdentifier('value'), controlledExpression), }; if (controlledComponentToAttributesMap[componentType]) { @@ -112,8 +158,6 @@ export const renderValueAttribute = ({ // TODO: all components should be controlled once conversions are solid if (shouldGetForUncontrolled) { - const { dataType } = fieldConfig; - let expression = factory.createJsxExpression(undefined, valueIdentifier); if (dataType && typeof dataType !== 'object' && convertedValueAttributeMap[dataType]) { diff --git a/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts b/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts index ee31beb52..5241af75c 100644 --- a/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts +++ b/packages/codegen-ui-react/lib/forms/form-renderer-helper.ts @@ -195,7 +195,7 @@ export const addFormAttributes = (component: StudioComponent | StudioComponentCh } if (formMetadata.formActionType === 'update' && !fieldConfig.isArray && !isControlledComponent(componentType)) { - attributes.push(renderDefaultValueAttribute(renderedVariableName, fieldConfig)); + attributes.push(renderDefaultValueAttribute(renderedVariableName, fieldConfig, componentType)); } attributes.push(buildOnChangeStatement(component, formMetadata.fieldConfigs)); attributes.push(buildOnBlurStatement(componentName, fieldConfig)); @@ -238,11 +238,52 @@ export const addFormAttributes = (component: StudioComponent | StudioComponentCh attributes.push(flexGapAttribute); } } + /* + onClick={(event) => { + event.preventDefault(); + resetStateValues(); + }} + */ if (componentName === 'ClearButton' || componentName === 'ResetButton') { attributes.push( factory.createJsxAttribute( factory.createIdentifier('onClick'), - factory.createJsxExpression(undefined, resetValuesName), + factory.createJsxExpression( + undefined, + factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('event'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('SyntheticEvent'), undefined), + undefined, + ), + ], + undefined, + factory.createToken(SyntaxKind.EqualsGreaterThanToken), + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createCallExpression( + factory.createPropertyAccessExpression( + factory.createIdentifier('event'), + factory.createIdentifier('preventDefault'), + ), + undefined, + [], + ), + ), + factory.createExpressionStatement(factory.createCallExpression(resetValuesName, undefined, [])), + ], + true, + ), + ), + ), ), ); if (formMetadata.formActionType === 'update' && formMetadata.dataType.dataSourceType === 'DataStore') { diff --git a/packages/codegen-ui-react/lib/forms/form-state.ts b/packages/codegen-ui-react/lib/forms/form-state.ts index 33e3244f3..576f0ba4a 100644 --- a/packages/codegen-ui-react/lib/forms/form-state.ts +++ b/packages/codegen-ui-react/lib/forms/form-state.ts @@ -125,6 +125,7 @@ export const getDefaultValueExpression = ( StepperField: factory.createNumericLiteral(0), SliderField: factory.createNumericLiteral(0), CheckboxField: factory.createFalse(), + TextField: factory.createStringLiteral(''), }; if (isArray) { diff --git a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts index 0ec7f025d..9a9d760bb 100644 --- a/packages/codegen-ui-react/lib/forms/react-form-renderer.ts +++ b/packages/codegen-ui-react/lib/forms/react-form-renderer.ts @@ -528,23 +528,11 @@ export abstract class ReactFormTemplateRenderer extends StudioTemplateRenderer< // timestamp type takes precedence over datetime as it includes formatter for datetime // we include both the timestamp conversion and local date formatter if (dataTypesMap.AWSTimestamp) { - // helper needed if update form - // or, if create form and the field is an array and therefore controlled - if ( - formActionType === 'update' || - dataTypesMap.AWSTimestamp.some((fieldName) => formMetadata.fieldConfigs[fieldName].isArray) - ) { - statements.push(convertTimeStampToDateAST, convertToLocalAST); - } + statements.push(convertTimeStampToDateAST, convertToLocalAST); } // if we only have date time then we only need the local conversion else if (dataTypesMap.AWSDateTime) { - if ( - formActionType === 'update' || - dataTypesMap.AWSDateTime.some((fieldName) => formMetadata.fieldConfigs[fieldName].isArray) - ) { - statements.push(convertToLocalAST); - } + statements.push(convertToLocalAST); } return statements; diff --git a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts index c590c017a..18f902bf4 100644 --- a/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts +++ b/packages/codegen-ui/lib/__tests__/generate-form-definition/helpers/form-field.test.ts @@ -104,7 +104,7 @@ describe('getFormDefinitionInputElement', () => { isRequired: true, isReadOnly: false, placeholder: 'MyPlaceholder', - defaultValue: 'MyDefaultValue', + value: 'MyDefaultValue', }, }; @@ -114,7 +114,6 @@ describe('getFormDefinitionInputElement', () => { label: 'MyLabel', descriptiveText: 'MyDescriptiveText', placeholder: 'MyPlaceholder', - defaultValue: 'MyDefaultValue', }, studioFormComponentType: 'TextField', }); diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts index 8f39201c2..afde498e1 100644 --- a/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts +++ b/packages/test-generator/integration-test-templates/cypress/e2e/form-spec.cy.ts @@ -101,6 +101,7 @@ describe('Forms', () => { getInputByLabel('Aws date').type('2022-10-12'); getInputByLabel('Aws time').type('10:12'); getInputByLabel('Aws date time').type('2017-06-01T08:30'); + getInputByLabel('Aws timestamp').type('2022-12-01T10:30'); getInputByLabel('Aws email').type('myemail@yahoo.com'); getInputByLabel('Aws url').type('https://amazon.com'); getInputByLabel('Aws ip address').type('192.0.2.146'); @@ -120,6 +121,7 @@ describe('Forms', () => { expect(record.awsDate).to.equal('2022-10-12'); expect(record.awsTime).to.equal('10:12'); expect(record.awsDateTime).to.equal('2017-06-01T08:30:00.000Z'); + expect(record.awsTimestamp).to.equal(1669890600000); expect(record.awsEmail).to.equal('myemail@yahoo.com'); expect(record.awsUrl).to.equal('https://amazon.com'); expect(record.awsIPAddress).to.equal('192.0.2.146');