Skip to content

Commit 796df37

Browse files
authored
fix(ui): awaits form state before rendering conditional fields (#9933)
When a condition exists on a field and it resolves to `false`, it currently "blinks" in and out when rendered within an array or block row. This is because when add rows to form state, we iterate over the _fields_ of that row and render their respective components. Then when conditions are checked for that field, we're expecting `passesCondition` to be explicitly `false`, ultimately _rendering_ the field for a brief moment before form state returns with evaluated conditions. The fix is to set these fields into local form state with a new `isLoading: true` prop, then display a loader within the row until form state returns with its proper conditions.
1 parent 9c8cdea commit 796df37

File tree

14 files changed

+157
-63
lines changed

14 files changed

+157
-63
lines changed

packages/payload/src/admin/forms/Form.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type Row = {
1414
blockType?: string
1515
collapsed?: boolean
1616
id: string
17+
isLoading?: boolean
1718
}
1819

1920
export type FilterOptionsResult = {

packages/ui/src/fields/Array/ArrayRow.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import type { UseDraggableSortableReturn } from '../../elements/DraggableSortabl
99
import { ArrayAction } from '../../elements/ArrayAction/index.js'
1010
import { Collapsible } from '../../elements/Collapsible/index.js'
1111
import { ErrorPill } from '../../elements/ErrorPill/index.js'
12+
import { ShimmerEffect } from '../../elements/ShimmerEffect/index.js'
1213
import { useFormSubmitted } from '../../forms/Form/context.js'
1314
import { RenderFields } from '../../forms/RenderFields/index.js'
1415
import { RowLabel } from '../../forms/RowLabel/index.js'
15-
import { useTranslation } from '../../providers/Translation/index.js'
16+
import { useThrottledValue } from '../../hooks/useThrottledValue.js'
1617
import './index.scss'
18+
import { useTranslation } from '../../providers/Translation/index.js'
1719

1820
const baseClass = 'array-field'
1921

@@ -25,6 +27,7 @@ type ArrayRowProps = {
2527
readonly fields: ClientField[]
2628
readonly forceRender?: boolean
2729
readonly hasMaxRows?: boolean
30+
readonly isLoading?: boolean
2831
readonly isSortable?: boolean
2932
readonly labels: Partial<ArrayField['labels']>
3033
readonly moveRow: (fromIndex: number, toIndex: number) => void
@@ -50,6 +53,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
5053
forceRender = false,
5154
hasMaxRows,
5255
isDragging,
56+
isLoading: isLoadingFromProps,
5357
isSortable,
5458
labels,
5559
listeners,
@@ -68,6 +72,8 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
6872
transform,
6973
transition,
7074
}) => {
75+
const isLoading = useThrottledValue(isLoadingFromProps, 500)
76+
7177
const { i18n } = useTranslation()
7278
const hasSubmitted = useFormSubmitted()
7379

@@ -136,17 +142,21 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
136142
isCollapsed={row.collapsed}
137143
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
138144
>
139-
<RenderFields
140-
className={`${baseClass}__fields`}
141-
fields={fields}
142-
forceRender={forceRender}
143-
margins="small"
144-
parentIndexPath=""
145-
parentPath={path}
146-
parentSchemaPath={schemaPath}
147-
permissions={permissions === true ? permissions : permissions?.fields}
148-
readOnly={readOnly}
149-
/>
145+
{isLoading ? (
146+
<ShimmerEffect />
147+
) : (
148+
<RenderFields
149+
className={`${baseClass}__fields`}
150+
fields={fields}
151+
forceRender={forceRender}
152+
margins="small"
153+
parentIndexPath=""
154+
parentPath={path}
155+
parentSchemaPath={schemaPath}
156+
permissions={permissions === true ? permissions : permissions?.fields}
157+
readOnly={readOnly}
158+
/>
159+
)}
150160
</Collapsible>
151161
</div>
152162
)

packages/ui/src/fields/Array/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
276276
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
277277
>
278278
{rowsData.map((rowData, i) => {
279-
const { id: rowID } = rowData
279+
const { id: rowID, isLoading } = rowData
280280

281281
const rowPath = `${path}.${i}`
282282

@@ -296,6 +296,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
296296
fields={fields}
297297
forceRender={forceRender}
298298
hasMaxRows={hasMaxRows}
299+
isLoading={isLoading}
299300
isSortable={isSortable}
300301
labels={labels}
301302
moveRow={moveRow}

packages/ui/src/fields/Blocks/BlockRow.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
'use client'
2-
import type {
3-
ClientBlock,
4-
ClientField,
5-
Labels,
6-
Row,
7-
SanitizedFieldPermissions,
8-
SanitizedFieldsPermissions,
9-
} from 'payload'
2+
import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload'
103

114
import { getTranslation } from '@payloadcms/translations'
125
import React from 'react'
@@ -17,8 +10,10 @@ import type { RenderFieldsProps } from '../../forms/RenderFields/types.js'
1710
import { Collapsible } from '../../elements/Collapsible/index.js'
1811
import { ErrorPill } from '../../elements/ErrorPill/index.js'
1912
import { Pill } from '../../elements/Pill/index.js'
13+
import { ShimmerEffect } from '../../elements/ShimmerEffect/index.js'
2014
import { useFormSubmitted } from '../../forms/Form/context.js'
2115
import { RenderFields } from '../../forms/RenderFields/index.js'
16+
import { useThrottledValue } from '../../hooks/useThrottledValue.js'
2217
import { useTranslation } from '../../providers/Translation/index.js'
2318
import { RowActions } from './RowActions.js'
2419
import { SectionTitle } from './SectionTitle/index.js'
@@ -33,6 +28,7 @@ type BlocksFieldProps = {
3328
errorCount: number
3429
fields: ClientField[]
3530
hasMaxRows?: boolean
31+
isLoading?: boolean
3632
isSortable?: boolean
3733
Label?: React.ReactNode
3834
labels: Labels
@@ -58,6 +54,7 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
5854
errorCount,
5955
fields,
6056
hasMaxRows,
57+
isLoading: isLoadingFromProps,
6158
isSortable,
6259
Label,
6360
labels,
@@ -76,6 +73,8 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
7673
setNodeRef,
7774
transform,
7875
}) => {
76+
const isLoading = useThrottledValue(isLoadingFromProps, 500)
77+
7978
const { i18n } = useTranslation()
8079
const hasSubmitted = useFormSubmitted()
8180

@@ -161,16 +160,20 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
161160
key={row.id}
162161
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
163162
>
164-
<RenderFields
165-
className={`${baseClass}__fields`}
166-
fields={fields}
167-
margins="small"
168-
parentIndexPath=""
169-
parentPath={path}
170-
parentSchemaPath={schemaPath}
171-
permissions={blockPermissions}
172-
readOnly={readOnly}
173-
/>
163+
{isLoading ? (
164+
<ShimmerEffect />
165+
) : (
166+
<RenderFields
167+
className={`${baseClass}__fields`}
168+
fields={fields}
169+
margins="small"
170+
parentIndexPath=""
171+
parentPath={path}
172+
parentSchemaPath={schemaPath}
173+
permissions={blockPermissions}
174+
readOnly={readOnly}
175+
/>
176+
)}
174177
</Collapsible>
175178
</div>
176179
)

packages/ui/src/fields/Blocks/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
259259
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
260260
>
261261
{rows.map((row, i) => {
262-
const { blockType } = row
262+
const { blockType, isLoading } = row
263263
const blockConfig = blocks.find((block) => block.slug === blockType)
264264

265265
if (blockConfig) {
@@ -281,6 +281,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
281281
errorCount={rowErrorCount}
282282
fields={blockConfig.fields}
283283
hasMaxRows={hasMaxRows}
284+
isLoading={isLoading}
284285
isSortable={isSortable}
285286
Label={Label}
286287
labels={labels}

packages/ui/src/forms/Form/fieldReducer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
2929
id: (subFieldState?.id?.value as string) || new ObjectId().toHexString(),
3030
blockType: blockType || undefined,
3131
collapsed: false,
32+
isLoading: true,
3233
}
3334

3435
withNewRow.splice(rowIndex, 0, newRow)
@@ -43,6 +44,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
4344

4445
// add new row to array _field state_
4546
const { remainingFields, rows: siblingRows } = separateRows(path, state)
47+
4648
siblingRows.splice(rowIndex, 0, subFieldState)
4749

4850
const newState: FormState = {

packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
142142

143143
let fieldPermissions: SanitizedFieldPermissions = true
144144

145+
const fieldState: FormFieldWithoutComponents = {
146+
errorPaths: [],
147+
fieldSchema: includeSchema ? field : undefined,
148+
initialValue: undefined,
149+
isSidebar: fieldIsSidebar(field),
150+
passesCondition,
151+
valid: true,
152+
value: undefined,
153+
}
154+
145155
if (fieldAffectsData(field) && !fieldIsHiddenOrDisabled(field)) {
146156
fieldPermissions =
147157
parentPermissions === true
@@ -163,16 +173,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
163173

164174
const validate = field.validate
165175

166-
const fieldState: FormFieldWithoutComponents = {
167-
errorPaths: [],
168-
fieldSchema: includeSchema ? field : undefined,
169-
initialValue: undefined,
170-
isSidebar: fieldIsSidebar(field),
171-
passesCondition,
172-
valid: true,
173-
value: undefined,
174-
}
175-
176176
let validationResult: string | true = true
177177

178178
if (typeof validate === 'function' && !skipValidation && passesCondition) {
@@ -672,7 +672,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
672672
})
673673

674674
let childPermissions: SanitizedFieldsPermissions = undefined
675-
if (tabHasName(tab)) {
675+
676+
if (isNamedTab) {
676677
if (parentPermissions === true) {
677678
childPermissions = true
678679
} else {
@@ -721,16 +722,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
721722
await Promise.all(promises)
722723
} else if (field.type === 'ui') {
723724
if (!filter || filter(args)) {
724-
state[path] = {
725-
disableFormData: true,
726-
errorPaths: [],
727-
fieldSchema: includeSchema ? field : undefined,
728-
initialValue: undefined,
729-
isSidebar: fieldIsSidebar(field),
730-
passesCondition,
731-
valid: true,
732-
value: undefined,
733-
}
725+
state[path] = fieldState
726+
state[path].disableFormData = true
734727
}
735728
}
736729

packages/ui/src/forms/fieldSchemasToFormState/iterateFields.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const iterateFields = async ({
102102

103103
fields.forEach((field, fieldIndex) => {
104104
let passesCondition = true
105+
105106
if (!skipConditionChecks) {
106107
passesCondition = Boolean(
107108
(field?.admin?.condition

packages/ui/src/forms/useField/index.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,7 @@ export const useField = <TValue,>(options: Options): FieldType<TValue> => {
118118
value,
119119
}),
120120
[
121-
field?.errorMessage,
122-
field?.rows,
123-
field?.valid,
124-
field?.errorPaths,
121+
field,
125122
processing,
126123
setValue,
127124
showError,
@@ -131,7 +128,6 @@ export const useField = <TValue,>(options: Options): FieldType<TValue> => {
131128
path,
132129
filterOptions,
133130
initializing,
134-
field?.customComponents,
135131
],
136132
)
137133

packages/ui/src/forms/withCondition/WatchCondition.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import React, { Fragment } from 'react'
3+
import type React from 'react'
44

55
import { useFormFields } from '../Form/context.js'
66

@@ -19,5 +19,5 @@ export const WatchCondition: React.FC<{
1919
return null
2020
}
2121

22-
return <Fragment>{children}</Fragment>
22+
return children
2323
}

0 commit comments

Comments
 (0)