= (
+ path: P,
+ fn: (value: PathType) => unknown
+) => void
+
+type UseDataReturn = {
+ data: Data
+ update: UseDataReturnUpdate
+}
+
+export default function useData(
+ id: string,
+ data: Data = undefined
+): UseDataReturn {
+ const initialDataRef = useRef(data)
+ const sharedState = useSharedState(id, data)
+
+ const updatePath = useCallback>(
+ (path, fn) => {
+ const existingData = sharedState.data || ({} as Data)
+ const existingValue = pointer.has(existingData, path)
+ ? pointer.get(existingData, path)
+ : undefined
+
+ // get new value
+ const newValue = fn(existingValue)
+
+ // update existing data
+ pointer.set(existingData, path, newValue)
+
+ // update provider
+ sharedState.update?.(existingData)
+ },
+ [sharedState]
+ )
+
+ // when initial data changes, update the shared state
+ useEffect(() => {
+ if (data && data !== initialDataRef.current) {
+ initialDataRef.current = data
+ sharedState.update?.(data)
+ }
+ }, [data, sharedState])
+
+ return { data: sharedState.data, update: updatePath }
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
index a706062ed18..44895516761 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
@@ -5,3 +5,4 @@ export { default as ButtonRow } from './ButtonRow'
export { default as MainHeading } from './MainHeading'
export { default as SubHeading } from './SubHeading'
export { default as Visibility } from './Visibility'
+export { default as useData } from './hooks/useData'
diff --git a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx
index a21553267d4..611898594b0 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/Iterate/Array/Array.tsx
@@ -36,8 +36,6 @@ function ArrayComponent(props: Props) {
layout = 'vertical',
placeholder,
label,
- labelDescription,
- labelSecondary,
path,
value: arrayValue,
info,
@@ -81,8 +79,6 @@ function ArrayComponent(props: Props) {
className={classnames('dnb-forms-field-number', className)}
layout={layout}
label={label}
- labelDescription={labelDescription}
- labelSecondary={labelSecondary}
info={info}
warning={warning}
error={error}
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx
index 85dcf15b809..4c58eddf018 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/__tests__/useDataValue.test.tsx
@@ -2,8 +2,7 @@ import React from 'react'
import { act, renderHook, waitFor } from '@testing-library/react'
import useDataValue from '../useDataValue'
import { Provider } from '../../DataContext'
-import { JSONSchema7 } from 'json-schema'
-import { FieldBlock, FormError } from '../../Forms'
+import { FieldBlock, FormError, JSONSchema } from '../../Forms'
describe('useDataValue', () => {
it('should call external onChange based change callbacks', () => {
@@ -171,7 +170,7 @@ describe('useDataValue', () => {
})
it('should validate schema', async () => {
- const schema: JSONSchema7 = {
+ const schema: JSONSchema = {
type: 'object',
properties: {
txt: {
@@ -214,7 +213,7 @@ describe('useDataValue', () => {
})
it('should have correct validation order', async () => {
- const schema: JSONSchema7 = {
+ const schema: JSONSchema = {
type: 'string',
pattern: '^(throw-on-validator)$',
}
diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts
index ef24e7f5a45..f2f718b37fc 100644
--- a/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useDataValue.ts
@@ -155,7 +155,6 @@ export default function useDataValue<
return undefined
}, [
props.value,
- props.capitalize,
inIterate,
itemPath,
dataContext.data,
@@ -369,7 +368,7 @@ export default function useDataValue<
}, [dataContext.showAllErrors, showError])
useEffect(() => {
- if (path && props.value) {
+ if (path && typeof props.value !== 'undefined') {
const hasValue = pointer.has(dataContext.data, path)
const value = hasValue
? pointer.get(dataContext.data, path)
diff --git a/packages/dnb-eufemia/src/extensions/forms/types.ts b/packages/dnb-eufemia/src/extensions/forms/types.ts
index 6c62e20a82d..09277e2e6bb 100644
--- a/packages/dnb-eufemia/src/extensions/forms/types.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/types.ts
@@ -1,5 +1,7 @@
-import { JSONSchema7 } from 'json-schema'
-import { SpacingProps } from '../../components/space/types'
+import type { SpacingProps } from '../../components/space/types'
+import type { JSONSchema7 as JSONSchema } from 'json-schema'
+
+export type { JSONSchema }
type ValidationRule = string | string[]
type MessageValues = Record
@@ -163,10 +165,6 @@ export interface FieldProps<
layout?: 'horizontal' | 'vertical'
/** Main label text */
label?: React.ReactNode
- /** A more discreet text displayed beside the label (i.e for "(optional)") */
- labelDescription?: React.ReactNode
- /** Secondary information displayed at the end of the label line (i.e character counter) */
- labelSecondary?: React.ReactNode
/** Text showing in place of the value if no value is given */
placeholder?: string
autoComplete?:
@@ -181,7 +179,7 @@ export interface FieldProps<
trim?: boolean
// Validation
required?: boolean
- schema?: JSONSchema7
+ schema?: JSONSchema
validator?: (
value: Value | EmptyValue,
errorMessages?: ErrorMessages
diff --git a/packages/dnb-eufemia/src/shared/helpers/EventEmitter.ts b/packages/dnb-eufemia/src/shared/helpers/EventEmitter.ts
index 7a928682adc..c24f5bed2b4 100644
--- a/packages/dnb-eufemia/src/shared/helpers/EventEmitter.ts
+++ b/packages/dnb-eufemia/src/shared/helpers/EventEmitter.ts
@@ -38,6 +38,11 @@ export type EventEmitterScopeObject = {
data: EventEmitterData
}
+/**
+ * Deprecated
+ *
+ * @deprecated Please use useSharedState instead for new code
+ */
class EventEmitter {
static createInstance(id: EventEmitterId) {
return new EventEmitter(id)
diff --git a/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
new file mode 100644
index 00000000000..234e089f97e
--- /dev/null
+++ b/packages/dnb-eufemia/src/shared/helpers/__tests__/useSharedState.test.ts
@@ -0,0 +1,84 @@
+import { renderHook, act } from '@testing-library/react'
+import { useSharedState, getOrCreateSharedState } from '../useSharedState'
+
+describe('useSharedState', () => {
+ it('should create a new shared state if one does not exist', () => {
+ const { result } = renderHook(() =>
+ useSharedState('testId', { test: 'initial' })
+ )
+ expect(result.current.data).toEqual({ test: 'initial' })
+ })
+
+ it('should use an existing shared state if one exists', () => {
+ getOrCreateSharedState('existingId', { test: 'existing' })
+ const { result } = renderHook(() =>
+ useSharedState('existingId', { test: 'initial' })
+ )
+ expect(result.current.data).toEqual({ test: 'existing' })
+ })
+
+ it('should update the shared state', () => {
+ const { result } = renderHook(() =>
+ useSharedState('updateId', { test: 'initial' })
+ )
+ act(() => {
+ result.current.update({ test: 'updated' })
+ })
+ expect(result.current.data).toEqual({ test: 'updated' })
+ })
+
+ it('should update the component when the shared state changes', () => {
+ const { result } = renderHook(() =>
+ useSharedState('changeId', { test: 'initial' })
+ )
+ const sharedState = getOrCreateSharedState('changeId', {
+ test: 'initial',
+ })
+ act(() => {
+ sharedState.updateSharedState({ test: 'changed' })
+ })
+ expect(result.current.data).toEqual({ test: 'changed' })
+ })
+
+ it('should unsubscribe from the shared state when the component unmounts', () => {
+ const { result, unmount } = renderHook(() =>
+ useSharedState('unmountId', { test: 'initial' })
+ )
+ const sharedState = getOrCreateSharedState('unmountId', {
+ test: 'initial',
+ })
+ unmount()
+ act(() => {
+ sharedState.updateSharedState({ test: 'unmounted' })
+ })
+ expect(result.current.data).toEqual({ test: 'initial' })
+ })
+
+ it('should return undefined data when no ID is given', () => {
+ const { result } = renderHook(() =>
+ useSharedState(null, { test: 'initial' })
+ )
+ expect(result.current.data).toBeUndefined()
+ })
+
+ it('should not update the data when no ID is given', () => {
+ const { result } = renderHook(() =>
+ useSharedState(null, { test: 'initial' })
+ )
+ act(() => {
+ result.current.update({ test: 'updated' })
+ })
+ expect(result.current.data).toBeUndefined()
+ })
+
+ it('should not subscribe to the shared state when no ID is given', () => {
+ const { result, unmount } = renderHook(() =>
+ useSharedState(null, { test: 'initial' })
+ )
+ unmount()
+ act(() => {
+ result.current.update({ test: 'unmounted' })
+ })
+ expect(result.current.data).toBeUndefined()
+ })
+})
diff --git a/packages/dnb-eufemia/src/shared/helpers/useEventEmitter.tsx b/packages/dnb-eufemia/src/shared/helpers/useEventEmitter.tsx
index 793f8db72bf..012f2bdb3d8 100644
--- a/packages/dnb-eufemia/src/shared/helpers/useEventEmitter.tsx
+++ b/packages/dnb-eufemia/src/shared/helpers/useEventEmitter.tsx
@@ -9,6 +9,8 @@ import EventEmitter, {
*
* @param {string} id unique id, same as used in the "lined place"
* @returns React Hook { data, update }
+ *
+ * @deprecated Please use useSharedState instead for new code
*/
export const useEventEmitter = (id: EventEmitterId = null) => {
const [, updateState] = React.useState(null)
diff --git a/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
new file mode 100644
index 00000000000..1fcc89dff1f
--- /dev/null
+++ b/packages/dnb-eufemia/src/shared/helpers/useSharedState.tsx
@@ -0,0 +1,94 @@
+import { useCallback, useEffect, useMemo, useState } from 'react'
+
+type SharedStateId = string
+type Subscriber = () => void
+
+interface SharedStateInstance {
+ data: Data
+ getSharedState: () => Data
+ updateSharedState: (newData: Partial) => void
+ subscribeToSharedState: (subscriber: Subscriber) => void
+ unsubscribeFromSharedState: (subscriber: Subscriber) => void
+}
+
+const sharedStates: Record> = {}
+
+export function getOrCreateSharedState(
+ id: SharedStateId,
+ initialData: Data
+): SharedStateInstance {
+ if (!sharedStates[id]) {
+ let subscribers: Subscriber[] = []
+
+ const getSharedState = () => sharedStates[id].data
+
+ const updateSharedState = (newData: Partial) => {
+ sharedStates[id].data = { ...sharedStates[id].data, ...newData }
+ subscribers.forEach((subscriber) => subscriber())
+ }
+
+ const subscribeToSharedState = (subscriber: Subscriber) => {
+ subscribers.push(subscriber)
+ }
+
+ const unsubscribeFromSharedState = (subscriber: Subscriber) => {
+ subscribers = subscribers.filter((sub) => sub !== subscriber)
+ }
+
+ sharedStates[id] = {
+ data: initialData ? { ...initialData } : undefined,
+ getSharedState,
+ updateSharedState,
+ subscribeToSharedState,
+ unsubscribeFromSharedState,
+ } as SharedStateInstance
+ } else if (
+ sharedStates[id].data === undefined &&
+ initialData !== undefined
+ ) {
+ sharedStates[id].data = { ...initialData }
+ }
+
+ return sharedStates[id]
+}
+
+export function useSharedState(
+ id: SharedStateId,
+ initialData: Data
+) {
+ const sharedState = useMemo(
+ () => id && getOrCreateSharedState(id, initialData),
+ [id, initialData]
+ )
+ const [data, setData] = useState(sharedState?.getSharedState?.())
+
+ const update = useCallback(
+ (newData: Data) => {
+ if (!id) {
+ return
+ }
+
+ sharedState?.updateSharedState?.(newData)
+ },
+ [id, sharedState]
+ )
+
+ useEffect(() => {
+ if (!id) {
+ return
+ }
+
+ const updateState = () => {
+ const existingData = sharedState.getSharedState()
+ setData(existingData)
+ }
+
+ sharedState.subscribeToSharedState(updateState)
+
+ return () => {
+ sharedState.unsubscribeFromSharedState(updateState)
+ }
+ }, [id, sharedState])
+
+ return { data, update }
+}