@contember/react-form 2.0.0-alpha.9 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/development/index.cjs +6 -2
  2. package/dist/development/index.cjs.map +1 -1
  3. package/dist/development/index.js +5 -1
  4. package/dist/development/index.js.map +1 -1
  5. package/dist/development/src/components/FormCheckbox.cjs +9 -6
  6. package/dist/development/src/components/FormCheckbox.cjs.map +1 -1
  7. package/dist/development/src/components/FormCheckbox.js +10 -7
  8. package/dist/development/src/components/FormCheckbox.js.map +1 -1
  9. package/dist/development/src/components/FormError.cjs +5 -5
  10. package/dist/development/src/components/FormError.cjs.map +1 -1
  11. package/dist/development/src/components/FormError.js +6 -6
  12. package/dist/development/src/components/FormError.js.map +1 -1
  13. package/dist/development/src/components/FormFieldScope.cjs +24 -14
  14. package/dist/development/src/components/FormFieldScope.cjs.map +1 -1
  15. package/dist/development/src/components/FormFieldScope.js +25 -15
  16. package/dist/development/src/components/FormFieldScope.js.map +1 -1
  17. package/dist/development/src/components/FormFieldStateProvider.cjs +56 -0
  18. package/dist/development/src/components/FormFieldStateProvider.cjs.map +1 -0
  19. package/dist/development/src/components/FormFieldStateProvider.js +56 -0
  20. package/dist/development/src/components/FormFieldStateProvider.js.map +1 -0
  21. package/dist/development/src/components/FormHasManyRelationScope.cjs +25 -15
  22. package/dist/development/src/components/FormHasManyRelationScope.cjs.map +1 -1
  23. package/dist/development/src/components/FormHasManyRelationScope.js +26 -16
  24. package/dist/development/src/components/FormHasManyRelationScope.js.map +1 -1
  25. package/dist/development/src/components/FormHasOneRelationScope.cjs +25 -15
  26. package/dist/development/src/components/FormHasOneRelationScope.cjs.map +1 -1
  27. package/dist/development/src/components/FormHasOneRelationScope.js +25 -15
  28. package/dist/development/src/components/FormHasOneRelationScope.js.map +1 -1
  29. package/dist/development/src/components/FormInput.cjs +11 -6
  30. package/dist/development/src/components/FormInput.cjs.map +1 -1
  31. package/dist/development/src/components/FormInput.js +12 -7
  32. package/dist/development/src/components/FormInput.js.map +1 -1
  33. package/dist/development/src/components/FormLabel.cjs +7 -5
  34. package/dist/development/src/components/FormLabel.cjs.map +1 -1
  35. package/dist/development/src/components/FormLabel.js +8 -6
  36. package/dist/development/src/components/FormLabel.js.map +1 -1
  37. package/dist/development/src/components/FormRadioInput.cjs +17 -13
  38. package/dist/development/src/components/FormRadioInput.cjs.map +1 -1
  39. package/dist/development/src/components/FormRadioInput.js +18 -14
  40. package/dist/development/src/components/FormRadioInput.js.map +1 -1
  41. package/dist/development/src/contexts.cjs +7 -8
  42. package/dist/development/src/contexts.cjs.map +1 -1
  43. package/dist/development/src/contexts.js +8 -9
  44. package/dist/development/src/contexts.js.map +1 -1
  45. package/dist/development/src/hooks/useFormInputValidationHandler.cjs +12 -3
  46. package/dist/development/src/hooks/useFormInputValidationHandler.cjs.map +1 -1
  47. package/dist/development/src/hooks/useFormInputValidationHandler.js +12 -3
  48. package/dist/development/src/hooks/useFormInputValidationHandler.js.map +1 -1
  49. package/dist/development/src/internal/useFormInputHandler.cjs.map +1 -1
  50. package/dist/development/src/internal/useFormInputHandler.js.map +1 -1
  51. package/dist/production/index.cjs +6 -2
  52. package/dist/production/index.cjs.map +1 -1
  53. package/dist/production/index.js +5 -1
  54. package/dist/production/index.js.map +1 -1
  55. package/dist/production/src/components/FormCheckbox.cjs +7 -4
  56. package/dist/production/src/components/FormCheckbox.cjs.map +1 -1
  57. package/dist/production/src/components/FormCheckbox.js +8 -5
  58. package/dist/production/src/components/FormCheckbox.js.map +1 -1
  59. package/dist/production/src/components/FormError.cjs +5 -5
  60. package/dist/production/src/components/FormError.cjs.map +1 -1
  61. package/dist/production/src/components/FormError.js +6 -6
  62. package/dist/production/src/components/FormError.js.map +1 -1
  63. package/dist/production/src/components/FormFieldScope.cjs +16 -6
  64. package/dist/production/src/components/FormFieldScope.cjs.map +1 -1
  65. package/dist/production/src/components/FormFieldScope.js +17 -7
  66. package/dist/production/src/components/FormFieldScope.js.map +1 -1
  67. package/dist/production/src/components/FormFieldStateProvider.cjs +34 -0
  68. package/dist/production/src/components/FormFieldStateProvider.cjs.map +1 -0
  69. package/dist/production/src/components/FormFieldStateProvider.js +34 -0
  70. package/dist/production/src/components/FormFieldStateProvider.js.map +1 -0
  71. package/dist/production/src/components/FormHasManyRelationScope.cjs +17 -7
  72. package/dist/production/src/components/FormHasManyRelationScope.cjs.map +1 -1
  73. package/dist/production/src/components/FormHasManyRelationScope.js +18 -8
  74. package/dist/production/src/components/FormHasManyRelationScope.js.map +1 -1
  75. package/dist/production/src/components/FormHasOneRelationScope.cjs +17 -7
  76. package/dist/production/src/components/FormHasOneRelationScope.cjs.map +1 -1
  77. package/dist/production/src/components/FormHasOneRelationScope.js +17 -7
  78. package/dist/production/src/components/FormHasOneRelationScope.js.map +1 -1
  79. package/dist/production/src/components/FormInput.cjs +9 -4
  80. package/dist/production/src/components/FormInput.cjs.map +1 -1
  81. package/dist/production/src/components/FormInput.js +10 -5
  82. package/dist/production/src/components/FormInput.js.map +1 -1
  83. package/dist/production/src/components/FormLabel.cjs +7 -5
  84. package/dist/production/src/components/FormLabel.cjs.map +1 -1
  85. package/dist/production/src/components/FormLabel.js +8 -6
  86. package/dist/production/src/components/FormLabel.js.map +1 -1
  87. package/dist/production/src/components/FormRadioInput.cjs +15 -11
  88. package/dist/production/src/components/FormRadioInput.cjs.map +1 -1
  89. package/dist/production/src/components/FormRadioInput.js +16 -12
  90. package/dist/production/src/components/FormRadioInput.js.map +1 -1
  91. package/dist/production/src/contexts.cjs +7 -8
  92. package/dist/production/src/contexts.cjs.map +1 -1
  93. package/dist/production/src/contexts.js +8 -9
  94. package/dist/production/src/contexts.js.map +1 -1
  95. package/dist/production/src/hooks/useFormInputValidationHandler.cjs +12 -3
  96. package/dist/production/src/hooks/useFormInputValidationHandler.cjs.map +1 -1
  97. package/dist/production/src/hooks/useFormInputValidationHandler.js +12 -3
  98. package/dist/production/src/hooks/useFormInputValidationHandler.js.map +1 -1
  99. package/dist/production/src/internal/useFormInputHandler.cjs.map +1 -1
  100. package/dist/production/src/internal/useFormInputHandler.js.map +1 -1
  101. package/dist/types/components/FormCheckbox.d.ts.map +1 -1
  102. package/dist/types/components/FormFieldScope.d.ts +2 -1
  103. package/dist/types/components/FormFieldScope.d.ts.map +1 -1
  104. package/dist/types/components/FormFieldStateProvider.d.ts +36 -0
  105. package/dist/types/components/FormFieldStateProvider.d.ts.map +1 -0
  106. package/dist/types/components/FormHasManyRelationScope.d.ts +5 -1
  107. package/dist/types/components/FormHasManyRelationScope.d.ts.map +1 -1
  108. package/dist/types/components/FormHasOneRelationScope.d.ts +2 -1
  109. package/dist/types/components/FormHasOneRelationScope.d.ts.map +1 -1
  110. package/dist/types/components/FormInput.d.ts.map +1 -1
  111. package/dist/types/components/FormLabel.d.ts.map +1 -1
  112. package/dist/types/components/FormRadioInput.d.ts +1 -1
  113. package/dist/types/components/FormRadioInput.d.ts.map +1 -1
  114. package/dist/types/components/index.d.ts +1 -0
  115. package/dist/types/components/index.d.ts.map +1 -1
  116. package/dist/types/contexts.d.ts +10 -6
  117. package/dist/types/contexts.d.ts.map +1 -1
  118. package/dist/types/hooks/useFormInputValidationHandler.d.ts.map +1 -1
  119. package/dist/types/tsconfig.tsbuildinfo +1 -1
  120. package/dist/types/types/index.d.ts +12 -0
  121. package/dist/types/types/index.d.ts.map +1 -1
  122. package/package.json +7 -6
  123. package/src/components/FormCheckbox.tsx +17 -6
  124. package/src/components/FormError.tsx +6 -6
  125. package/src/components/FormFieldScope.tsx +17 -12
  126. package/src/components/FormFieldStateProvider.tsx +71 -0
  127. package/src/components/FormHasManyRelationScope.tsx +22 -14
  128. package/src/components/FormHasOneRelationScope.tsx +17 -13
  129. package/src/components/FormInput.tsx +12 -5
  130. package/src/components/FormLabel.tsx +8 -6
  131. package/src/components/FormRadioInput.tsx +22 -15
  132. package/src/components/index.ts +1 -0
  133. package/src/contexts.tsx +18 -0
  134. package/src/hooks/useFormInputValidationHandler.tsx +13 -3
  135. package/src/tsconfig.json +5 -2
  136. package/src/types/index.ts +13 -0
  137. package/src/contexts.ts +0 -12
@@ -1,7 +1,19 @@
1
+ import { ErrorAccessor } from '@contember/react-binding';
1
2
  import * as React from 'react';
2
3
  export type FormInputHandler = {
3
4
  parseValue: (value: string) => any;
4
5
  formatValue: (value: any) => string;
5
6
  defaultInputProps?: React.InputHTMLAttributes<HTMLInputElement>;
6
7
  };
8
+ export type FormFieldState = {
9
+ htmlId: string;
10
+ errors: ErrorAccessor.Error[];
11
+ required: boolean;
12
+ dirty: boolean;
13
+ field?: {
14
+ entityName: string;
15
+ fieldName: string;
16
+ enumName?: string;
17
+ };
18
+ };
7
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,MAAM,MAAM,gBAAgB,GAAG;IAC9B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,CAAA;IAClC,WAAW,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAA;IACnC,iBAAiB,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,CAAA;CAC/D,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,MAAM,MAAM,gBAAgB,GAAG;IAC9B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,CAAA;IAClC,WAAW,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAA;IACnC,iBAAiB,CAAC,EAAE,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,CAAA;CAC/D,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,aAAa,CAAC,KAAK,EAAE,CAAA;IAC7B,QAAQ,EAAE,OAAO,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE;QACP,UAAU,EAAE,MAAM,CAAA;QAClB,SAAS,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;CACD,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@contember/react-form",
3
3
  "license": "Apache-2.0",
4
- "version": "2.0.0-alpha.9",
4
+ "version": "2.0.0-beta.2",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/production/index.js",
@@ -11,6 +11,7 @@
11
11
  "types": "./dist/types/index.d.ts",
12
12
  "development": "./dist/development/index.js",
13
13
  "production": "./dist/production/index.js",
14
+ "typescript": "./src/index.ts",
14
15
  "default": "./dist/production/index.js"
15
16
  },
16
17
  "require": {
@@ -33,10 +34,10 @@
33
34
  "directory": "packages/react-form"
34
35
  },
35
36
  "dependencies": {
36
- "@contember/react-binding": "2.0.0-alpha.9",
37
- "@contember/react-utils": "2.0.0-alpha.9",
38
- "@contember/utilities": "2.0.0-alpha.9",
39
- "@radix-ui/react-slot": "^1.0.2"
37
+ "@contember/react-binding": "2.0.0-beta.2",
38
+ "@contember/react-utils": "2.0.0-beta.2",
39
+ "@contember/utilities": "2.0.0-beta.2",
40
+ "@radix-ui/react-slot": "^1.1.0"
40
41
  },
41
42
  "peerDependencies": {
42
43
  "react": "^18 || ^19",
@@ -46,4 +47,4 @@
46
47
  "react": "^18.3.1",
47
48
  "react-dom": "^18.3.1"
48
49
  }
49
- }
50
+ }
@@ -1,7 +1,14 @@
1
1
  import * as React from 'react'
2
2
  import { ChangeEventHandler, ReactElement, useCallback, useEffect } from 'react'
3
- import { Component, Field, OptionallyVariableFieldValue, SugaredRelativeSingleField, TreeNodeEnvironmentFactory, useField } from '@contember/react-binding'
4
- import { useFormError, useFormFieldId } from '../contexts'
3
+ import {
4
+ Component,
5
+ Field,
6
+ OptionallyVariableFieldValue,
7
+ SugaredRelativeSingleField,
8
+ TreeNodeEnvironmentFactory,
9
+ useField,
10
+ } from '@contember/react-binding'
11
+ import { useFormFieldState } from '../contexts'
5
12
  import { dataAttribute } from '@contember/utilities'
6
13
  import { useFormInputValidationHandler } from '../hooks'
7
14
  import { Slot } from '@radix-ui/react-slot'
@@ -16,10 +23,13 @@ export interface FormCheckboxProps {
16
23
  }
17
24
 
18
25
  export const FormCheckbox = Component<FormCheckboxProps>(({ field, isNonbearing, defaultValue, ...props }) => {
19
- const id = useFormFieldId()
20
26
  const accessor = useField<boolean>(field)
27
+
28
+ const formState = useFormFieldState()
29
+ const id = formState?.htmlId
30
+ const hasErrors = (formState?.errors.length ?? accessor.errors?.errors?.length ?? 0) > 0
31
+ const dirty = formState?.dirty ?? accessor.hasUnpersistedChanges
21
32
  const value = accessor.value
22
- const errors = useFormError() ?? accessor.errors?.errors ?? []
23
33
  const accessorGetter = accessor.getAccessor
24
34
  const { ref, onFocus, onBlur } = useFormInputValidationHandler(accessor)
25
35
  const [checkboxRef, setCheckboxRef] = React.useState<HTMLInputElement | null>(null)
@@ -40,8 +50,9 @@ export const FormCheckbox = Component<FormCheckboxProps>(({ field, isNonbearing,
40
50
  type="checkbox"
41
51
  checked={accessor.value === true}
42
52
  data-state={value === null ? 'indeterminate' : (value ? 'checked' : 'unchecked')}
43
- data-invalid={dataAttribute(errors.length > 0)}
44
- id={`${id}-input`}
53
+ data-invalid={dataAttribute(hasErrors)}
54
+ data-dirty={dataAttribute(dirty)}
55
+ id={id ? `${id}-input` : undefined}
45
56
  onFocus={onFocus}
46
57
  onBlur={onBlur}
47
58
  onChange={useCallback<ChangeEventHandler<HTMLInputElement>>(e => {
@@ -1,21 +1,21 @@
1
1
  import { cloneElement, ReactElement, ReactNode, useMemo } from 'react'
2
- import { useFormError, useFormFieldId } from '../contexts'
2
+ import { useFormFieldState } from '../contexts'
3
3
  import { BindingError, ErrorAccessor } from '@contember/react-binding'
4
4
 
5
5
  export const FormError = ({ children, formatter }: {
6
6
  formatter: (errors: ErrorAccessor.Error[]) => ReactNode[]
7
7
  children: ReactElement
8
8
  }) => {
9
- const errors = useFormError()
10
- const id = useFormFieldId()
11
- const formatted = useMemo(() => formatter(errors ?? []), [errors, formatter])
12
- if (errors === undefined || id === undefined) {
9
+ const formState = useFormFieldState()
10
+ if (!formState) {
13
11
  throw new BindingError('FormError must be used inside a FormField')
14
12
  }
13
+ const { errors, htmlId } = formState
14
+ const formatted = useMemo(() => formatter(errors ?? []).filter((it, index, arr) => arr.indexOf(it) === index), [errors, formatter])
15
15
  return formatted.map((it, index) => {
16
16
  return cloneElement(children, {
17
17
  key: index,
18
- ...{ id: `${id}-error-${index}` },
18
+ ...(htmlId ? { id: `${htmlId}-error-${index}` } : {}),
19
19
  children: it,
20
20
  })
21
21
  })
@@ -1,24 +1,29 @@
1
1
  import * as React from 'react'
2
- import { useId } from 'react'
3
- import { FormErrorContext, FormFieldIdContext } from '../contexts'
2
+ import { useMemo } from 'react'
4
3
  import { SugaredRelativeSingleField, useField } from '@contember/react-binding'
5
-
6
- const emptyArr: [] = []
4
+ import { FormFieldStateProvider } from './FormFieldStateProvider'
7
5
 
8
6
  export type FormFieldScopeProps = {
9
7
  field: SugaredRelativeSingleField['field']
10
8
  children: React.ReactNode
9
+ required?: boolean
11
10
  }
12
11
 
13
- export const FormFieldScope = ({ field, children }: FormFieldScopeProps) => {
14
- const id = useId()
12
+ export const FormFieldScope = ({ field, children, required }: FormFieldScopeProps) => {
15
13
  const fieldAccessor = useField({ field })
16
- const errors = fieldAccessor.errors?.errors
14
+ const entityName = fieldAccessor.getParent().name
15
+ const fieldName = fieldAccessor.fieldName
16
+ const enumName = fieldAccessor.schema.enumName ?? undefined
17
+ const fieldInfo = useMemo(() => ({ entityName, fieldName, enumName }), [entityName, fieldName, enumName])
18
+
17
19
  return (
18
- <FormFieldIdContext.Provider value={id}>
19
- <FormErrorContext.Provider value={errors ?? emptyArr}>
20
- {children}
21
- </FormErrorContext.Provider>
22
- </FormFieldIdContext.Provider>
20
+ <FormFieldStateProvider
21
+ errors={fieldAccessor.errors?.errors}
22
+ required={required ?? !fieldAccessor.schema.nullable}
23
+ dirty={fieldAccessor.hasUnpersistedChanges}
24
+ field={fieldInfo}
25
+ >
26
+ {children}
27
+ </FormFieldStateProvider>
23
28
  )
24
29
  }
@@ -0,0 +1,71 @@
1
+ import { ErrorAccessor } from '@contember/react-binding'
2
+ import { useEffect, useId, useMemo } from 'react'
3
+ import { FormFieldStateContext, useFormFieldState } from '../contexts'
4
+ import { FormFieldState } from '../types'
5
+
6
+ const emptyArr: [] = []
7
+
8
+ export type FormFieldStateProvider =
9
+ & Partial<FormFieldState>
10
+ & {
11
+ children: React.ReactNode
12
+ }
13
+
14
+ export const FormFieldStateProvider = ({ children, required = false, errors = emptyArr, dirty = false, htmlId, field }: FormFieldStateProvider) => {
15
+ const generatedId = useId()
16
+ htmlId ??= generatedId
17
+ const value = useMemo(() => ({ htmlId, required, errors, dirty, field }), [htmlId, required, errors, dirty, field])
18
+ return <FormFieldStateContext.Provider value={value}>{children}</FormFieldStateContext.Provider>
19
+ }
20
+
21
+ /**
22
+ * BC for
23
+ * <FormFieldIdContext.Provider value={id}>
24
+ * <FormErrorContext.Provider value={[]}>
25
+ */
26
+
27
+ /**
28
+ * @deprecated use `FormFieldStateProvider` instead
29
+ */
30
+ export const FormFieldIdContext = {
31
+ /**
32
+ * @deprecated use `FormFieldStateProvider` instead
33
+ */
34
+ Provider: ({ children, value }: { children: React.ReactNode; value: string }) => {
35
+ const state = useFormFieldState()
36
+ if (import.meta.env.DEV) {
37
+ // eslint-disable-next-line react-hooks/rules-of-hooks
38
+ useEffect(() => {
39
+ console.warn('FormFieldIdContext.Provider is deprecated, use FormFieldStateProvider instead')
40
+ }, [])
41
+ }
42
+ return (
43
+ <FormFieldStateProvider {...state} htmlId={value}>
44
+ {children}
45
+ </FormFieldStateProvider>
46
+ )
47
+ },
48
+ }
49
+
50
+ /**
51
+ * @deprecated use `FormFieldStateProvider` instead
52
+ */
53
+ export const FormErrorContext = {
54
+ /**
55
+ * @deprecated use `FormFieldStateProvider` instead
56
+ */
57
+ Provider: ({ children, value }: { children: React.ReactNode; value: ErrorAccessor.Error[] }) => {
58
+ const state = useFormFieldState()
59
+ if (import.meta.env.DEV) {
60
+ // eslint-disable-next-line react-hooks/rules-of-hooks
61
+ useEffect(() => {
62
+ console.warn('FormErrorContext.Provider is deprecated, use FormFieldStateProvider instead')
63
+ }, [])
64
+ }
65
+ return (
66
+ <FormFieldStateProvider {...state} errors={value}>
67
+ {children}
68
+ </FormFieldStateProvider>
69
+ )
70
+ },
71
+ }
@@ -1,24 +1,32 @@
1
- import { SugaredRelativeEntityList, useEntityList } from '@contember/react-binding'
1
+ import { HasManyRelationMarker, SugaredRelativeEntityList, useEntityList } from '@contember/react-binding'
2
2
  import * as React from 'react'
3
- import { useId } from 'react'
4
- import { FormErrorContext, FormFieldIdContext } from '../contexts'
5
-
6
- const emptyArr: [] = []
3
+ import { FormFieldStateProvider } from './FormFieldStateProvider'
4
+ import { useMemo } from 'react'
7
5
 
8
6
  export type FormHasManyRelationScopeProps = {
9
7
  field: SugaredRelativeEntityList['field']
8
+ orderBy?: SugaredRelativeEntityList['orderBy']
9
+ limit?: SugaredRelativeEntityList['limit']
10
+ offset?: SugaredRelativeEntityList['offset']
10
11
  children: React.ReactNode
12
+ required?: boolean
11
13
  }
12
14
 
13
- export const FormHasManyRelationScope = ({ field, children }: FormHasManyRelationScopeProps) => {
14
- const id = useId()
15
- const entityRelation = useEntityList({ field })
16
- const errors = entityRelation.errors?.errors
15
+ export const FormHasManyRelationScope = ({ children, required, ...props }: FormHasManyRelationScopeProps) => {
16
+ const entityRelation = useEntityList(props)
17
+ const entityName = entityRelation.getParent()!.name
18
+ const marker = entityRelation.getMarker() as HasManyRelationMarker
19
+ const fieldName = marker.parameters.field
20
+ const fieldInfo = useMemo(() => ({ entityName, fieldName }), [entityName, fieldName])
21
+
17
22
  return (
18
- <FormFieldIdContext.Provider value={id}>
19
- <FormErrorContext.Provider value={errors ?? emptyArr}>
20
- {children}
21
- </FormErrorContext.Provider>
22
- </FormFieldIdContext.Provider>
23
+ <FormFieldStateProvider
24
+ errors={entityRelation.errors?.errors}
25
+ dirty={entityRelation.hasUnpersistedChanges}
26
+ required={required}
27
+ field={fieldInfo}
28
+ >
29
+ {children}
30
+ </FormFieldStateProvider>
23
31
  )
24
32
  }
@@ -1,24 +1,28 @@
1
1
  import * as React from 'react'
2
- import { useId } from 'react'
3
- import { FormErrorContext, FormFieldIdContext } from '../contexts'
4
- import { SugaredRelativeSingleEntity, useEntity } from '@contember/react-binding'
5
-
6
- const emptyArr: [] = []
2
+ import { HasOneRelationMarker, SugaredRelativeSingleEntity, useEntity } from '@contember/react-binding'
3
+ import { FormFieldStateProvider } from './FormFieldStateProvider'
4
+ import { useMemo } from 'react'
7
5
 
8
6
  export type FormHasOneRelationScopeProps = {
9
7
  field: SugaredRelativeSingleEntity['field']
10
8
  children: React.ReactNode
9
+ required?: boolean
11
10
  }
12
11
 
13
- export const FormHasOneRelationScope = ({ field, children }: FormHasOneRelationScopeProps) => {
14
- const id = useId()
12
+ export const FormHasOneRelationScope = ({ field, children, required }: FormHasOneRelationScopeProps) => {
15
13
  const entityRelation = useEntity({ field })
16
- const errors = entityRelation.errors?.errors
14
+ const marker = entityRelation.getMarker() as HasOneRelationMarker
15
+ const fieldName = marker.parameters.field
16
+ const entityName = entityRelation.getParent()!.name
17
+ const fieldInfo = useMemo(() => ({ entityName, fieldName }), [entityName, fieldName])
17
18
  return (
18
- <FormFieldIdContext.Provider value={id}>
19
- <FormErrorContext.Provider value={errors ?? emptyArr}>
20
- {children}
21
- </FormErrorContext.Provider>
22
- </FormFieldIdContext.Provider>
19
+ <FormFieldStateProvider
20
+ errors={entityRelation.errors?.errors}
21
+ dirty={entityRelation.hasUnpersistedChanges}
22
+ required={required}
23
+ field={fieldInfo}
24
+ >
25
+ {children}
26
+ </FormFieldStateProvider>
23
27
  )
24
28
  }
@@ -4,7 +4,7 @@ import { Slot } from '@radix-ui/react-slot'
4
4
  import { Component, Field, OptionallyVariableFieldValue, SugaredRelativeSingleField, useField } from '@contember/react-binding'
5
5
  import { dataAttribute } from '@contember/utilities'
6
6
  import { useFormInputHandler } from '../internal/useFormInputHandler'
7
- import { useFormError, useFormFieldId } from '../contexts'
7
+ import { useFormFieldState } from '../contexts'
8
8
  import { useFormInputValidationHandler } from '../hooks/useFormInputValidationHandler'
9
9
  import { FormInputHandler } from '../types'
10
10
 
@@ -21,9 +21,14 @@ export interface FormInputProps {
21
21
  }
22
22
 
23
23
  export const FormInput = Component<FormInputProps>(({ field, isNonbearing, defaultValue, formatValue: formatValueIn, parseValue: parseValueIn, ...props }) => {
24
- const id = useFormFieldId()
25
24
  const accessor = useField(field)
26
- const errors = useFormError() ?? accessor.errors?.errors ?? []
25
+
26
+ const formState = useFormFieldState()
27
+ const id = formState?.htmlId
28
+ const hasErrors = (formState?.errors.length ?? accessor.errors?.errors?.length ?? 0) > 0
29
+ const dirty = formState?.dirty ?? accessor.hasUnpersistedChanges
30
+ const required = formState?.required ?? !accessor.schema.nullable
31
+
27
32
  const { parseValue, formatValue, defaultInputProps } = useFormInputHandler(accessor, { formatValue: formatValueIn, parseValue: parseValueIn })
28
33
  const accessorGetter = accessor.getAccessor
29
34
  const { ref, onFocus, onBlur } = useFormInputValidationHandler(accessor)
@@ -32,7 +37,9 @@ export const FormInput = Component<FormInputProps>(({ field, isNonbearing, defau
32
37
  <SlotInput
33
38
  ref={ref}
34
39
  value={formatValue(accessor.value)}
35
- data-invalid={dataAttribute(errors.length > 0)}
40
+ data-invalid={dataAttribute(hasErrors)}
41
+ data-dirty={dataAttribute(dirty)}
42
+ data-required={dataAttribute(required)}
36
43
  onFocus={onFocus}
37
44
  onBlur={onBlur}
38
45
  onChange={useCallback<ChangeEventHandler<HTMLInputElement>>(e => {
@@ -40,7 +47,7 @@ export const FormInput = Component<FormInputProps>(({ field, isNonbearing, defau
40
47
  }, [accessorGetter, parseValue])}
41
48
  {...defaultInputProps}
42
49
  id={id ? `${id}-input` : undefined}
43
- required={!accessor.schema.nullable}
50
+ required={required}
44
51
  {...props}
45
52
  />
46
53
  )
@@ -1,21 +1,23 @@
1
1
  import * as React from 'react'
2
2
  import { Slot } from '@radix-ui/react-slot'
3
3
  import { dataAttribute } from '@contember/utilities'
4
- import { useFormError, useFormFieldId } from '../contexts'
4
+ import { useFormFieldState } from '../contexts'
5
5
  import { BindingError } from '@contember/react-binding'
6
6
 
7
7
  export const FormLabel = (props: {
8
8
  children: React.ReactElement
9
9
  }) => {
10
- const id = useFormFieldId()
11
- const errors = useFormError()
12
- if (errors === undefined || id === undefined) {
13
- throw new BindingError('FormLabel must be used inside a FormField')
10
+ const formState = useFormFieldState()
11
+ if (!formState) {
12
+ throw new BindingError('FormError must be used inside a FormField')
14
13
  }
14
+ const { errors, htmlId, dirty, required } = formState
15
15
  return (
16
16
  <Slot
17
17
  data-invalid={dataAttribute(errors.length > 0)}
18
- {...{ htmlFor: `${id}-input` }}
18
+ data-dirty={dataAttribute(dirty)}
19
+ data-required={dataAttribute(required)}
20
+ {...(htmlId ? { htmlFor: `${htmlId}-input` } : {})}
19
21
  {...props}
20
22
  />
21
23
  )
@@ -1,45 +1,52 @@
1
1
  import { Component, Field, OptionallyVariableFieldValue, SugaredRelativeSingleField, useField } from '@contember/react-binding'
2
+ import { dataAttribute } from '@contember/utilities'
3
+ import { Slot } from '@radix-ui/react-slot'
2
4
  import * as React from 'react'
3
5
  import { ChangeEventHandler, useCallback } from 'react'
4
- import { Slot } from '@radix-ui/react-slot'
5
- import { useFormError, useFormFieldId } from '../contexts'
6
+ import { useFormFieldState } from '../contexts'
6
7
  import { useFormInputValidationHandler } from '../hooks'
7
- import { dataAttribute } from '@contember/utilities'
8
8
 
9
9
  const SlotInput = Slot as React.ForwardRefExoticComponent<React.RefAttributes<HTMLInputElement> & React.InputHTMLAttributes<HTMLInputElement>>
10
10
 
11
11
  export interface FormRadioItemProps {
12
12
  field: SugaredRelativeSingleField['field']
13
- value: string
13
+ value: string | null | number | boolean
14
14
  isNonbearing?: boolean
15
15
  defaultValue?: OptionallyVariableFieldValue
16
16
  children: React.ReactNode
17
17
  }
18
18
 
19
19
  export const FormRadioInput = Component<FormRadioItemProps>(({ field, value, defaultValue, isNonbearing, ...props }) => {
20
- const id = useFormFieldId()
21
- const accessor = useField<string>(field)
22
- const errors = useFormError() ?? accessor.errors?.errors ?? []
20
+ const accessor = useField(field)
21
+
22
+ const formState = useFormFieldState()
23
+ const id = formState?.htmlId
24
+ const hasErrors = (formState?.errors.length ?? accessor.errors?.errors?.length ?? 0) > 0
25
+ const dirty = formState?.dirty ?? accessor.hasUnpersistedChanges
26
+
23
27
  const accessorGetter = accessor.getAccessor
24
28
  const { ref, onFocus, onBlur } = useFormInputValidationHandler(accessor)
25
29
 
30
+ const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(e => {
31
+ accessorGetter().updateValue(value)
32
+ }, [accessorGetter, value])
33
+
26
34
  return (
27
35
  <SlotInput
28
36
  ref={ref}
29
37
  type="radio"
30
- value={value}
38
+ value={typeof value === 'string' ? value : undefined}
31
39
  checked={accessor.value === value}
32
- data-invalid={dataAttribute(errors.length > 0)}
33
- name={id + '-input'}
34
- id={`${id}-input-${value}`}
40
+ data-invalid={dataAttribute(hasErrors)}
41
+ data-dirty={dataAttribute(dirty)}
42
+ name={id ? id + '-input' : undefined}
43
+ id={id ? `${id}-input` : undefined}
35
44
  onFocus={onFocus}
36
45
  onBlur={onBlur}
37
- onChange={useCallback<ChangeEventHandler<HTMLInputElement>>(e => {
38
- accessorGetter().updateValue(value)
39
- }, [accessorGetter, value])}
46
+ onChange={handleChange}
40
47
  {...props}
41
48
  />
42
49
  )
43
50
  }, ({ field, isNonbearing, defaultValue }) => {
44
- return <Field field={field} isNonbearing={isNonbearing} defaultValue={defaultValue} />
51
+ return <Field field={field} isNonbearing={isNonbearing} defaultValue={defaultValue}/>
45
52
  })
@@ -1,6 +1,7 @@
1
1
  export * from './FormError'
2
2
  export * from './FormCheckbox'
3
3
  export * from './FormFieldScope'
4
+ export * from './FormFieldStateProvider'
4
5
  export * from './FormHasOneRelationScope'
5
6
  export * from './FormHasManyRelationScope'
6
7
  export * from './FormInput'
@@ -0,0 +1,18 @@
1
+ import { createContext } from '@contember/react-utils'
2
+ import { FormFieldState } from './types'
3
+
4
+
5
+ const FormFieldStateContext_ = createContext<FormFieldState | undefined>('FormFieldStateContext', undefined)
6
+ export const FormFieldStateContext = FormFieldStateContext_[0]
7
+ export const useFormFieldState = FormFieldStateContext_[1]
8
+
9
+
10
+ /**
11
+ * @deprecated use `useFormState` instead
12
+ */
13
+ export const useFormFieldId = () => useFormFieldState()?.htmlId
14
+
15
+ /**
16
+ * @deprecated use `useFormState` instead
17
+ */
18
+ export const useFormError = () => useFormFieldState()?.errors
@@ -25,9 +25,12 @@ export const useFormInputValidationHandler = (field: FieldAccessor<any>) => {
25
25
  const previousMessage = validationMessage.current
26
26
  validationMessage.current = message
27
27
 
28
+ const accessor = accessorGetter()
28
29
  // if there is no message, we want to clear the error
29
30
  if (!message) {
30
- accessorGetter().clearErrors()
31
+ if (previousMessage) {
32
+ clearSpecificError(accessor, previousMessage)
33
+ }
31
34
  return
32
35
  }
33
36
  if (message === previousMessage) {
@@ -41,8 +44,8 @@ export const useFormInputValidationHandler = (field: FieldAccessor<any>) => {
41
44
  // if the input is not focused, we want to show the error message
42
45
  // also, even the input is focused, we want to replace the previous message
43
46
  if (!focus || !!previousMessage) {
44
- accessorGetter().clearErrors()
45
- accessorGetter().addError(message)
47
+ accessor.clearErrors()
48
+ accessor.addError(message)
46
49
  }
47
50
  })
48
51
 
@@ -60,3 +63,10 @@ export const useFormInputValidationHandler = (field: FieldAccessor<any>) => {
60
63
  }, [accessorGetter]),
61
64
  }
62
65
  }
66
+
67
+ const clearSpecificError = (field: FieldAccessor<any>, message: string) => {
68
+ const errors = field.errors?.errors ?? []
69
+ const error = errors.filter(e => !(e.type === 'validation' && e.message === message))
70
+ field.clearErrors()
71
+ error.forEach(e => field.addError(e))
72
+ }
package/src/tsconfig.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "extends": "../../../tsconfig.settings.json",
3
3
  "compilerOptions": {
4
- "outDir": "../dist/types"
4
+ "outDir": "../dist/types",
5
+ "types": [
6
+ "vite/client"
7
+ ]
5
8
  },
6
9
  "references": [
7
10
  { "path": "../../react-binding/src" },
8
11
  { "path": "../../react-utils/src" },
9
- { "path": "../../utilities/src" },
12
+ { "path": "../../utilities/src" }
10
13
  ]
11
14
  }
@@ -1,3 +1,4 @@
1
+ import { ErrorAccessor } from '@contember/react-binding'
1
2
  import * as React from 'react'
2
3
 
3
4
  export type FormInputHandler = {
@@ -5,3 +6,15 @@ export type FormInputHandler = {
5
6
  formatValue: (value: any) => string
6
7
  defaultInputProps?: React.InputHTMLAttributes<HTMLInputElement>
7
8
  }
9
+
10
+ export type FormFieldState = {
11
+ htmlId: string
12
+ errors: ErrorAccessor.Error[]
13
+ required: boolean
14
+ dirty: boolean
15
+ field?: {
16
+ entityName: string
17
+ fieldName: string
18
+ enumName?: string
19
+ }
20
+ }
package/src/contexts.ts DELETED
@@ -1,12 +0,0 @@
1
- import { createContext } from '@contember/react-utils'
2
- import { ErrorAccessor } from '@contember/react-binding'
3
-
4
- const FormFieldIdContext_ = createContext<string | undefined>('FormFieldIdContext', undefined)
5
- /** @internal */
6
- export const FormFieldIdContext = FormFieldIdContext_[0]
7
- export const useFormFieldId = FormFieldIdContext_[1]
8
-
9
- const FormErrorContext_ = createContext<ErrorAccessor.Error[] | undefined>('FormErrorContext', undefined)
10
- /** @internal */
11
- export const FormErrorContext = FormErrorContext_[0]
12
- export const useFormError = FormErrorContext_[1]