From 8d42f98e674c4d80da7d41c4eaa66aaee57052cd Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Wed, 17 Dec 2025 11:58:53 +0100 Subject: [PATCH 1/7] feat: add rules in destroy value --- packages/flower-demo/src/App.tsx | 3 +- .../flower-demo/src/Examples/Example12.tsx | 152 ++++++++++++++++++ .../flower-react/src/__tests__/form.test.tsx | 77 ++++++++- .../src/components/FlowerField.tsx | 68 +++++++- .../src/components/types/FlowerField.ts | 2 +- 5 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 packages/flower-demo/src/Examples/Example12.tsx diff --git a/packages/flower-demo/src/App.tsx b/packages/flower-demo/src/App.tsx index 99ba75a..f77d6b8 100644 --- a/packages/flower-demo/src/App.tsx +++ b/packages/flower-demo/src/App.tsx @@ -12,8 +12,9 @@ import { Example8 } from './Examples/Example8' // Simple example with FlowerRule import { Example9 } from './Examples/Example9' // Simple example form import { Example10 } from './Examples/Example10' // Simple example form import { Example11 } from './Examples/Example11' // Form async error and hidden +import { Example12 } from './Examples/Example12' function AppLogin() { - return + return } export default AppLogin diff --git a/packages/flower-demo/src/Examples/Example12.tsx b/packages/flower-demo/src/Examples/Example12.tsx new file mode 100644 index 0000000..3b768d8 --- /dev/null +++ b/packages/flower-demo/src/Examples/Example12.tsx @@ -0,0 +1,152 @@ +import { + FlowerNavigate, + FlowerNode, + FlowerField, + FlowerAction, + useFlower, + useFlowerForm, + Flower +} from '@flowerforce/flower-react' +import { useEffect } from 'react' +import './styles.css' + +/** + * Here we are using restart action to reset all not initial datas, form state and returns to first node + */ + +export function Example12() { + const { getFormStatus } = useFlowerForm({ flowName: 'example11' }) + const status = getFormStatus('step1') + const dirties = status?.dirty ? Object.keys(status?.dirty) : [] + const touches = status?.touches ? Object.keys(status?.touches) : [] + + return ( + + {/** + * step 1 + */} + +
+ 1 +
+ + + + {({ onChange, onBlur, value }) => ( + onChange(e.target.checked)} + onBlur={onBlur} + /> + )} + + + + {({ onChange, value = '', errors, onBlur }) => ( +
+ onChange(e.target.value)} + /> + + {errors &&
{errors.join(', ')}
} +
+ )} +
+
+ + {touches &&
touches: {touches.join(', ')}
} + {dirties &&
dirty: {dirties.join(', ')}
} + +
+ + {({ onClick, hidden }) => ( + + )} + +
+
+
+ + {/** + * step 2 + */} + +
+ +
+
+ + {/** + * step 3 + */} + +
+ Success + + + +
+
+ + {/** + * step 4 + */} + +
+ Error + + + +
+
+
+ ) +} + +const ComponentAction = () => { + const { next } = useFlower() + const { getData } = useFlowerForm() + + useEffect(() => { + // get form data + const formData = getData() + + try { + // * do your staff here - api call etc ** + // example setTimout to simulate delay api call + setTimeout(() => { + // navigate to success step + next('onSuccess') + }, 500) + } catch (error) { + // navigate to error step + next('onError') + } + }, [next, getData]) + + return +} diff --git a/packages/flower-react/src/__tests__/form.test.tsx b/packages/flower-react/src/__tests__/form.test.tsx index f5a4e11..65d2587 100644 --- a/packages/flower-react/src/__tests__/form.test.tsx +++ b/packages/flower-react/src/__tests__/form.test.tsx @@ -7,7 +7,7 @@ import React, { useEffect } from 'react' // import react-testing methods -import { render, fireEvent, screen } from '@testing-library/react' +import { render, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' // add custom jest matchers from jest-dom @@ -912,6 +912,81 @@ describe('Test Form', () => { expect(screen.getByTestId('h1')).toHaveTextContent('EMPTY') }) + it('Test form destroy field with rules', async () => { + const user = userEvent.setup() + + render( + + + + + + + + + + + {({ value }) => ( +

{value || 'EMPTY'}

+ )} +
+ +
+ + + +
+
+ ) + + await user.type(screen.getByTestId('description'), 'persistent') + expect(screen.getByTestId('description-value')).toHaveTextContent('persistent') + + await user.type(screen.getByTestId('mode'), 'reset') + await waitFor(() => { + expect(screen.getByTestId('description-value')).toHaveTextContent('EMPTY') + expect(screen.getByTestId('description')).toHaveAttribute('value', '') + }) + }) + + it('Test form destroy field with rules does not destroy on mount when already satisfied', async () => { + const user = userEvent.setup() + + render( + + + + + + + + + + + {({ value }) => ( +

{value || 'EMPTY'}

+ )} +
+
+
+
+ ) + + await user.type(screen.getByTestId('description'), 'stable') + expect(screen.getByTestId('description-value')).toHaveTextContent('stable') + expect(screen.getByTestId('description')).toHaveAttribute('value', 'stable') + }) + it('Test form replace field whit replaceData', async () => { const user = userEvent.setup() diff --git a/packages/flower-react/src/components/FlowerField.tsx b/packages/flower-react/src/components/FlowerField.tsx index eef3bca..2f7dcae 100644 --- a/packages/flower-react/src/components/FlowerField.tsx +++ b/packages/flower-react/src/components/FlowerField.tsx @@ -13,7 +13,8 @@ import { makeSelectNodeFieldDirty, makeSelectNodeFieldFocused, makeSelectNodeFieldTouched, - makeSelectNodeFormSubmitted + makeSelectNodeFormSubmitted, + selectorRulesDisabled } from '../selectors' import { context } from '../context' import FlowerRule from './FlowerRule' @@ -79,7 +80,33 @@ function Wrapper({ makeSelectNodeFieldFocused(flowName, currentNode, id) ) + const destroyRules = useMemo(() => { + if (!destroyValue || typeof destroyValue === 'boolean') return undefined + return destroyValue + }, [destroyValue]) + + const destroyRulesKeys = useMemo( + () => + destroyRules + ? MatchRules.utils.getKeys(destroyRules, { prefix: flowName }) ?? [] + : [], + [destroyRules, flowName] + ) + + const destroyRulesDisabled = useSelector( + selectorRulesDisabled( + id ?? '', + destroyRules, + destroyRulesKeys, + flowName ?? '', + value, + currentNode ?? '' + ) + ) + const refValue = useRef>() + const destroyRulesTriggered = useRef(false) + const prevDestroyRulesDisabled = useRef() const isSubmitted = useSelector( makeSelectNodeFormSubmitted(flowName, currentNode) @@ -246,10 +273,47 @@ function Wrapper({ }) },[currentNode, id, flowName]) + useEffect(() => { + if (!destroyRules) { + destroyRulesTriggered.current = false + prevDestroyRulesDisabled.current = undefined + return + } + + const wasDisabled = prevDestroyRulesDisabled.current + prevDestroyRulesDisabled.current = destroyRulesDisabled + + const shouldDestroy = + wasDisabled !== undefined && wasDisabled && !destroyRulesDisabled + + if (!shouldDestroy) { + if (destroyRulesTriggered.current && destroyRulesDisabled) { + destroyRulesTriggered.current = false + } + return + } + + if (!destroyRulesTriggered.current) { + destroyRulesTriggered.current = true + dispatch({ + type: `flower/unsetData`, + payload: { flowName: flowNameFromPath, id: path } + }) + resetField() + } + }, [ + destroyRules, + destroyRulesDisabled, + flowNameFromPath, + path, + resetField, + dispatch + ]) + useEffect(() => { // destroy return () => { - if (destroyValue) { + if (typeof destroyValue === 'boolean' && destroyValue === true) { dispatch({ type: `flower/unsetData`, payload: { flowName: flowNameFromPath, id: path } diff --git a/packages/flower-react/src/components/types/FlowerField.ts b/packages/flower-react/src/components/types/FlowerField.ts index b51587f..51ba394 100644 --- a/packages/flower-react/src/components/types/FlowerField.ts +++ b/packages/flower-react/src/components/types/FlowerField.ts @@ -114,7 +114,7 @@ export type FlowerFieldProps< /** Initial value field */ defaultValue?: unknown /** Remove value from data on destroy element */ - destroyValue?: boolean + destroyValue?: boolean | RulesObject | FunctionRule /** Remove value from data on hide element */ destroyOnHide?: boolean value?: any From 9dc0a807b4d6e130f962f8321605585e112c0dc4 Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Tue, 30 Dec 2025 15:23:01 +0100 Subject: [PATCH 2/7] feat: WIP add external redux --- README.md | 26 ++++ package-lock.json | 34 +++++- packages/flower-core/src/CoreUtils.ts | 8 ++ .../src/FlowerCoreStateSelectors.ts | 16 +++ .../src/__tests__/coreUtils.test.ts | 6 + .../src/__tests__/fieldPaths.test.ts | 36 ++++++ .../src/__tests__/flowerCoreSelector.test.ts | 90 ++++++++++---- packages/flower-core/src/index.ts | 1 + .../src/interfaces/CoreInterface.ts | 1 + .../src/interfaces/SelectorsInterface.ts | 9 ++ .../flower-core/src/rules-matcher/utils.ts | 4 + packages/flower-core/src/utils/fieldPaths.ts | 32 +++++ packages/flower-demo/src/App.tsx | 7 +- .../flower-demo/src/Examples/Example11.tsx | 2 +- .../src/Examples/ExampleExternalStore.tsx | 113 ++++++++++++++++++ packages/flower-demo/src/Examples/styles.css | 4 + packages/flower-react/README.md | 19 +++ packages/flower-react/package.json | 3 +- .../src/components/FlowerField.tsx | 85 ++++++++++--- .../src/components/FlowerValue.tsx | 14 ++- .../src/components/types/FlowerProvider.ts | 35 ++---- .../src/components/useFlowerForm.ts | 45 +++++-- .../flower-react/src/createFlowerStore.ts | 53 ++++++++ packages/flower-react/src/externalState.ts | 17 +++ packages/flower-react/src/index.ts | 7 +- packages/flower-react/src/provider.tsx | 44 ++++--- packages/flower-react/src/reducer.ts | 71 +++++++++-- packages/flower-react/src/selectors.ts | 16 ++- 28 files changed, 683 insertions(+), 115 deletions(-) create mode 100644 packages/flower-core/src/__tests__/fieldPaths.test.ts create mode 100644 packages/flower-core/src/utils/fieldPaths.ts create mode 100644 packages/flower-demo/src/Examples/ExampleExternalStore.tsx create mode 100644 packages/flower-react/src/createFlowerStore.ts create mode 100644 packages/flower-react/src/externalState.ts diff --git a/README.md b/README.md index c1c044f..3d1c151 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,32 @@ Flower is currently available for React only. --> - **Form Management**: Flower has a powerfull built-in Form Manager that allows to create sets of rules to know if a form is valid. - **Render Benefits**: Flower optimally manages rerenders, ensuring top-notch performance. +## External Redux Store Support + +Flower può convivere con uno store già esistente semplicemente usando `createFlowerStore`, lo stesso helper che `FlowerProvider` usa internamente: la funzione accetta la stessa configurazione di `configureStore` ma aggiunge un reducer che intercetta gli id `#external.*` e aggiorna il rispettivo path alla radice dello stato. I reducer esterni non devono contenere logiche custom per Flower, basta combinarli normalmente. + +```tsx +import { createFlowerStore } from '@flowerforce/flower-react' +import { reducerFlower } from '@flowerforce/flower-react' + +const store = createFlowerStore({ + reducer: { + flower: reducerFlower, + external: (state = { externalMessage: '' }) => state + } +}) + +function AppWithExternalStore() { + return ( + + {/* i tuoi flow */} + + ) +} +``` + +`FlowerField` e le regole di navigazione possono continuare a usare `#external.*`, e ogni scrittura viene automaticamente applicata al path specificato (es. `state.external.externalMessage`). I reducer esterni vedono i dati aggiornati senza dover ascoltare azioni Flower-specifiche. + ## Full Documentation For more info [flowerjs.it/](https://flowerjs.it/). diff --git a/package-lock.json b/package-lock.json index 764088f..9a093d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4586,17 +4586,19 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.6.tgz", - "integrity": "sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "dependencies": { - "immer": "^10.0.3", + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { @@ -4608,6 +4610,15 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", @@ -5035,6 +5046,16 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -23964,7 +23985,8 @@ "license": "MIT", "dependencies": { "@flowerforce/flower-core": "3.5.2", - "@reduxjs/toolkit": "^2.2.4", + "@reduxjs/toolkit": "^2.11.2", + "immer": "^10.1.1", "lodash": "^4.17.21", "react-redux": "^9.1.2", "reselect": "^5.1.0" diff --git a/packages/flower-core/src/CoreUtils.ts b/packages/flower-core/src/CoreUtils.ts index 5ded544..4719bc3 100644 --- a/packages/flower-core/src/CoreUtils.ts +++ b/packages/flower-core/src/CoreUtils.ts @@ -198,6 +198,14 @@ export const CoreUtils: CoreUtilitiesFunctions = { } } + if (idValue.indexOf('#') === 0) { + const externalPath = CoreUtils.cleanPath(idValue, '#').split('.') + return { + path: [], + externalPath + } + } + return { path: idValue.split('.') } diff --git a/packages/flower-core/src/FlowerCoreStateSelectors.ts b/packages/flower-core/src/FlowerCoreStateSelectors.ts index dae4823..88286ac 100644 --- a/packages/flower-core/src/FlowerCoreStateSelectors.ts +++ b/packages/flower-core/src/FlowerCoreStateSelectors.ts @@ -4,6 +4,10 @@ import { CoreUtils } from './CoreUtils' import { MatchRules } from './RulesMatcher' import { unflatten } from 'flat' import { createFormData } from './FlowerCoreStateUtils' +import { + readExternalValue, + resolveFieldPath +} from './utils/fieldPaths' export const FlowerCoreStateSelectors: ISelectors = { selectGlobal: (state) => state && state.flower, @@ -97,5 +101,17 @@ export const FlowerCoreStateSelectors: ISelectors = { ) return disabled + }, + makeSelectFieldValue: (flowName, id) => (state) => { + const { flowNameFromPath, path, externalPath, isExternal } = + resolveFieldPath(id, flowName) + + if (isExternal && externalPath) { + return readExternalValue(state ?? {}, externalPath) + } + + const targetFlow = flowNameFromPath ?? flowName + const dataPath = Array.isArray(path) ? path : [path] + return _get(state, ['flower', targetFlow, 'data', ...dataPath]) } } diff --git a/packages/flower-core/src/__tests__/coreUtils.test.ts b/packages/flower-core/src/__tests__/coreUtils.test.ts index da707d3..f72a82a 100644 --- a/packages/flower-core/src/__tests__/coreUtils.test.ts +++ b/packages/flower-core/src/__tests__/coreUtils.test.ts @@ -566,6 +566,12 @@ describe('CoreUtils object', () => { const emptyPath = CoreUtils.getPath() expect(emptyPath).toEqual({ path: [] }) + + const externalPath = CoreUtils.getPath('#external.externalMessage') + expect(externalPath).toEqual({ + path: [], + externalPath: ['external', 'externalMessage'] + }) }) test('allEqual match', () => { diff --git a/packages/flower-core/src/__tests__/fieldPaths.test.ts b/packages/flower-core/src/__tests__/fieldPaths.test.ts new file mode 100644 index 0000000..8738f47 --- /dev/null +++ b/packages/flower-core/src/__tests__/fieldPaths.test.ts @@ -0,0 +1,36 @@ +import { readExternalValue, resolveFieldPath } from '../utils/fieldPaths' + +describe('fieldPaths utils', () => { + it('resolves regular field paths with flowName fallback', () => { + const result = resolveFieldPath('form.field', 'defaultFlow') + + expect(result.path).toEqual(['form', 'field']) + expect(result.flowNameFromPath).toEqual('defaultFlow') + expect(result.isExternal).toBe(false) + expect(result.externalPath).toBeUndefined() + }) + + it('resolves external paths starting with # and marks them as external', () => { + const result = resolveFieldPath('#external.values.message', 'defaultFlow') + + expect(result.path).toEqual([]) + expect(result.externalPath).toEqual(['external', 'values', 'message']) + expect(result.isExternal).toBe(true) + expect(result.flowNameFromPath).toEqual('defaultFlow') + }) + + it('reads deeply nested external values', () => { + const state = { + external: { + nested: { + message: 'hello' + } + } + } + + expect(readExternalValue(state, ['external', 'nested', 'message'])).toBe( + 'hello' + ) + expect(readExternalValue(state, undefined)).toBeUndefined() + }) +}) diff --git a/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts b/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts index 58fd266..28e989a 100644 --- a/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts +++ b/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts @@ -5,36 +5,47 @@ import { Flower } from '../interfaces/Store' const TEST_FLOW_NAME = 'test_flow' -const state: { flower: { [x: string]: Flower> } } = { - flower: { - test_flow: { - persist: false, - startId: 'Start', - current: 'Node1', - history: ['start', 'Node1'], - nodes: { - Start: { nodeId: 'start', nodeType: 'FlowerRoute' }, - Node1: { nodeId: 'Node1', nodeType: 'FlowerNode' }, - Node2: { nodeId: 'Node2', nodeType: 'FlowerNode', retain: true } - }, - nextRules: { - Start: [{ nodeId: 'Node1', rules: null }] - }, - data: { - name: 'UserName', - test_getDataFromState: { value: 'test' } - }, - form: { - Start: { - isSubmitted: true, - errors: {}, - isValidating: false - } +const flowerSlice: { [x: string]: Flower> } = { + test_flow: { + persist: false, + startId: 'Start', + current: 'Node1', + history: ['start', 'Node1'], + nodes: { + Start: { nodeId: 'start', nodeType: 'FlowerRoute' }, + Node1: { nodeId: 'Node1', nodeType: 'FlowerNode' }, + Node2: { nodeId: 'Node2', nodeType: 'FlowerNode', retain: true } + }, + nextRules: { + Start: [{ nodeId: 'Node1', rules: null }] + }, + data: { + name: 'UserName', + test_getDataFromState: { value: 'test' } + }, + form: { + Start: { + isSubmitted: true, + errors: {}, + isValidating: false } } } } +const state: { + flower: { [x: string]: Flower> } + external: Record +} = { + flower: flowerSlice, + external: { + externalMessage: 'external value', + nested: { + flag: true + } + } +} + describe('FlowerCoreSelectors', () => { describe('SelectGlobal', () => { it('should return the flower object in state.flower', () => { @@ -224,6 +235,35 @@ describe('FlowerCoreSelectors', () => { }) }) + describe('makeSelectFieldValue', () => { + it('returns flow data for regular field ids', () => { + const selectFieldValue = FlowerCoreStateSelectors.makeSelectFieldValue( + TEST_FLOW_NAME, + 'test_getDataFromState.value' + ) + + expect(selectFieldValue(state)).toEqual('test') + }) + + it('returns values from external paths', () => { + const selectFieldValue = FlowerCoreStateSelectors.makeSelectFieldValue( + TEST_FLOW_NAME, + '#external.externalMessage' + ) + + expect(selectFieldValue(state)).toEqual('external value') + }) + + it('returns undefined for missing external paths', () => { + const selectFieldValue = FlowerCoreStateSelectors.makeSelectFieldValue( + TEST_FLOW_NAME, + '#external.unknownValue' + ) + + expect(selectFieldValue(state)).toBeUndefined() + }) + }) + describe('makeSelectFieldError', () => { it('should return an empty array if validate is false', () => { const name = 'UserName' diff --git a/packages/flower-core/src/index.ts b/packages/flower-core/src/index.ts index db50141..af6fe17 100644 --- a/packages/flower-core/src/index.ts +++ b/packages/flower-core/src/index.ts @@ -6,3 +6,4 @@ export { CoreUtils } from './CoreUtils' export { MatchRules } from './RulesMatcher' export { devtoolState } from './devtoolState' export * from './interfaces' +export { readExternalValue, resolveFieldPath } from './utils/fieldPaths' diff --git a/packages/flower-core/src/interfaces/CoreInterface.ts b/packages/flower-core/src/interfaces/CoreInterface.ts index 5b93d63..d6ed9f8 100644 --- a/packages/flower-core/src/interfaces/CoreInterface.ts +++ b/packages/flower-core/src/interfaces/CoreInterface.ts @@ -87,6 +87,7 @@ export type CleanPath = (name: string, char?: string) => string export type GetPath = (idValue?: string) => { path: string | string[] flowNameFromPath?: string + externalPath?: string[] } export type AllEqual = (...args: Array[]) => boolean diff --git a/packages/flower-core/src/interfaces/SelectorsInterface.ts b/packages/flower-core/src/interfaces/SelectorsInterface.ts index 772d11a..d5ede4a 100644 --- a/packages/flower-core/src/interfaces/SelectorsInterface.ts +++ b/packages/flower-core/src/interfaces/SelectorsInterface.ts @@ -139,6 +139,15 @@ export interface ISelectors { id: string, validate: { rules?: RulesObject; message?: string }[] | null ): (data: T | undefined, form: Form) => Array + /** + * @param name + * @param id + * @returns + */ + makeSelectFieldValue>( + name: string, + id: string + ): (state: Record) => any /** * @param id * @param rules diff --git a/packages/flower-core/src/rules-matcher/utils.ts b/packages/flower-core/src/rules-matcher/utils.ts index 11b2f0e..8c99e6a 100644 --- a/packages/flower-core/src/rules-matcher/utils.ts +++ b/packages/flower-core/src/rules-matcher/utils.ts @@ -174,6 +174,10 @@ const rulesMatcherUtils: RulesMatcherUtils = { return path } + if (path.indexOf('#') === 0) { + return _trimStart(path, '#') + } + return prefix ? `${prefix}.${path}` : path }, // TODO BUG NUMERI CON LETTERE 1asdas o solo diff --git a/packages/flower-core/src/utils/fieldPaths.ts b/packages/flower-core/src/utils/fieldPaths.ts new file mode 100644 index 0000000..a89b37c --- /dev/null +++ b/packages/flower-core/src/utils/fieldPaths.ts @@ -0,0 +1,32 @@ +import _get from 'lodash/get' +import { CoreUtils } from '../CoreUtils' + +export type FieldPathInfo = { + flowNameFromPath?: string + path: string | string[] + externalPath?: string[] + isExternal: boolean +} + +export const resolveFieldPath = ( + id?: string, + flowName?: string +): FieldPathInfo => { + const { path, flowNameFromPath, externalPath } = CoreUtils.getPath(id) + return { + flowNameFromPath: flowNameFromPath ?? flowName, + path, + externalPath, + isExternal: Array.isArray(externalPath) && externalPath.length > 0 + } +} + +export const readExternalValue = ( + state: Record, + externalPath?: string[] +) => { + if (!externalPath || !externalPath.length) { + return undefined + } + return _get(state, externalPath, undefined) +} diff --git a/packages/flower-demo/src/App.tsx b/packages/flower-demo/src/App.tsx index 99ba75a..0aeefe6 100644 --- a/packages/flower-demo/src/App.tsx +++ b/packages/flower-demo/src/App.tsx @@ -12,8 +12,13 @@ import { Example8 } from './Examples/Example8' // Simple example with FlowerRule import { Example9 } from './Examples/Example9' // Simple example form import { Example10 } from './Examples/Example10' // Simple example form import { Example11 } from './Examples/Example11' // Form async error and hidden +import { ExampleExternalStore } from './Examples/ExampleExternalStore' function AppLogin() { - return + return ( + <> + + + ) } export default AppLogin diff --git a/packages/flower-demo/src/Examples/Example11.tsx b/packages/flower-demo/src/Examples/Example11.tsx index 8af6f1b..2e63e2d 100644 --- a/packages/flower-demo/src/Examples/Example11.tsx +++ b/packages/flower-demo/src/Examples/Example11.tsx @@ -75,7 +75,7 @@ export function Example11() { ['Async field error']} + asyncValidate={(v) => v?.includes('a') ? []: ['Async field error']} asyncInitialError="Async initial error" asyncWaitingError="Async waiting error" asyncDebounce={500} diff --git a/packages/flower-demo/src/Examples/ExampleExternalStore.tsx b/packages/flower-demo/src/Examples/ExampleExternalStore.tsx new file mode 100644 index 0000000..d2e03c1 --- /dev/null +++ b/packages/flower-demo/src/Examples/ExampleExternalStore.tsx @@ -0,0 +1,113 @@ +import { + Flower, + FlowerField, + FlowerNavigate, + FlowerNode, + FlowerProvider, + FlowerValue, + createFlowerStore, + useSelector +} from '@flowerforce/flower-react' +import './styles.css' + +type ExternalState = { + externalMessage: string +} + +const externalFlowerStore = createFlowerStore({ + reducer: { + external: (state: ExternalState = { externalMessage: '' }) => state + }, + devTools: { name: 'flower-external-store' } +}) + +const ExternalControls = () => { + const message = useSelector((state: any) => state.external?.externalMessage ?? '') + + return ( +
+ External reducer value:{' '} + {message || 'empty'} +
+ ) +} + +export function ExampleExternalStore() { + return ( +
+ + + + + + +
+ External Store +

Navigation uses the same reducers but a custom store instance.

+ + {({ value = '', onChange, errors }) => ( + <> + onChange(event.target.value)} + /> + {errors &&
{errors.join(', ')}
} + + )} +
+ + + +
+
+ + +
+ Success + + + +
+
+ + +
+ Info + + {({ value }) => value} + + + + +
+
+ +
+
+
+ ) +} diff --git a/packages/flower-demo/src/Examples/styles.css b/packages/flower-demo/src/Examples/styles.css index a05ce87..88dd6d9 100644 --- a/packages/flower-demo/src/Examples/styles.css +++ b/packages/flower-demo/src/Examples/styles.css @@ -149,3 +149,7 @@ button.error { font-size: 12px; font-weight: bold; } + +.external-store{ + background-color: #ccc; +} \ No newline at end of file diff --git a/packages/flower-react/README.md b/packages/flower-react/README.md index 17a5cd6..ba879db 100644 --- a/packages/flower-react/README.md +++ b/packages/flower-react/README.md @@ -61,6 +61,25 @@ function Root() { ``` > You can pass the prop `enableReduxDevtool` to the `FlowerProvider` to show the Flower Store data inside the redux devtool of your browser. +### External store support + +Flower può condividere lo store Redux che già usi. Usa `createFlowerStore` da `@flowerforce/flower-react` al posto di `configureStore`: l’helper accetta la stessa configurazione ma aggiunge un reducer che intercetta gli id `#external.*` e applica il nuovo valore direttamente al path specificato. In questo modo i reducer esterni non devono conoscere Flower e il codice dello store rimane invariato. + +```tsx +import { reducerFlower, createFlowerStore } from '@flowerforce/flower-react' + +const store = createFlowerStore({ + reducer: { + flower: reducerFlower, + external: (state = { externalMessage: '' }) => state + } +}) +``` + +`FlowerField` e le regole di navigazione possono continuare a usare `#external.*` e lo stato esterno viene aggiornato automaticamente in `state.external.*` senza che il reducer debba ascoltare azioni Flower-specifiche. + +`FlowerField` e le regole di navigazione possono mantenere gli stessi `#external.*` path, ma i dati vengono scritti solo da chi gestisce la callback: la tua slice non ha bisogno di sapere che Flower sta passando l’aggiornamento. + ## How to use ### Simple Example diff --git a/packages/flower-react/package.json b/packages/flower-react/package.json index 4104ecb..96ff7fa 100644 --- a/packages/flower-react/package.json +++ b/packages/flower-react/package.json @@ -35,7 +35,8 @@ }, "dependencies": { "@flowerforce/flower-core": "3.5.2", - "@reduxjs/toolkit": "^2.2.4", + "@reduxjs/toolkit": "^2.11.2", + "immer": "^10.1.1", "lodash": "^4.17.21", "react-redux": "^9.1.2", "reselect": "^5.1.0" diff --git a/packages/flower-react/src/components/FlowerField.tsx b/packages/flower-react/src/components/FlowerField.tsx index eef3bca..0dbd6d7 100644 --- a/packages/flower-react/src/components/FlowerField.tsx +++ b/packages/flower-react/src/components/FlowerField.tsx @@ -17,7 +17,7 @@ import { } from '../selectors' import { context } from '../context' import FlowerRule from './FlowerRule' -import { store, useDispatch, useSelector } from '../provider' +import { useDispatch, useSelector, useStore } from '../provider' import debounce from 'lodash/debounce' import { MatchRules, @@ -26,6 +26,8 @@ import { } from '@flowerforce/flower-core' import { FlowerFieldProps } from './types/FlowerField' import isEqual from 'lodash/isEqual' +import { readExternalValue, resolveFieldPath } from '@flowerforce/flower-core' +import { setExternalValue } from '../externalState' function isIntrinsicElement(x: unknown): x is keyof JSX.IntrinsicElements { return typeof x === 'string' } @@ -51,6 +53,7 @@ function Wrapper({ ...props }: any) { const dispatch = useDispatch() + const reduxStore = useStore() const [customAsyncErrors, setCustomAsyncErrors] = useState( asyncValidate && asyncInitialError && [asyncInitialError] @@ -59,12 +62,18 @@ function Wrapper({ undefined ) - const { flowNameFromPath = flowName, path } = useMemo( - () => CoreUtils.getPath(id), - [id] + const { + flowNameFromPath = flowName, + path, + externalPath, + isExternal + } = useMemo(() => resolveFieldPath(id, flowName), [id, flowName]) + + const value = useSelector( + isExternal && externalPath + ? (state: Record) => readExternalValue(state, externalPath) + : getDataFromState(flowNameFromPath, path) ) - - const value = useSelector(getDataFromState(flowNameFromPath, path)) const errors = useSelector( makeSelectFieldError(flowName, id, validate), CoreUtils.allEqual @@ -120,7 +129,9 @@ function Wrapper({ setCustomAsyncErrors([asyncWaitingError]) } setIsValidating(true) - const state = FlowerStateUtils.getAllData(store) + const state = FlowerStateUtils.getAllData( + reduxStore.getState ? reduxStore.getState() : reduxStore + ) const res = await asyncValidate(value, state, errors) setIsValidating(false) setCustomAsyncErrors(res) @@ -137,6 +148,10 @@ function Wrapper({ if (asyncValidate && asyncWaitingError) { setCustomAsyncErrors([asyncWaitingError]) } + if (isExternal && externalPath) { + dispatch(setExternalValue(externalPath, val)) + return + } dispatch({ type: `flower/addDataByPath`, payload: { @@ -147,7 +162,17 @@ function Wrapper({ } }) }, - [flowNameFromPath, id, dispatch, setCustomAsyncErrors, asyncValidate, asyncWaitingError] + [ + flowNameFromPath, + id, + dispatch, + setCustomAsyncErrors, + asyncValidate, + asyncWaitingError, + isExternal, + externalPath, + defaultValue + ] ) const onBlurInternal = useCallback( @@ -247,29 +272,53 @@ function Wrapper({ },[currentNode, id, flowName]) useEffect(() => { - // destroy return () => { if (destroyValue) { - dispatch({ - type: `flower/unsetData`, - payload: { flowName: flowNameFromPath, id: path } - }) + if (isExternal && externalPath) { + dispatch(setExternalValue(externalPath, undefined)) + } else { + dispatch({ + type: `flower/unsetData`, + payload: { flowName: flowNameFromPath, id: path } + }) + } } resetField() } - }, [destroyValue, id, flowNameFromPath, path, resetField]) + }, [ + destroyValue, + flowNameFromPath, + path, + resetField, + isExternal, + externalPath, + dispatch + ]) useEffect(() => { - if(hidden){ - if (destroyOnHide) { + if (hidden) { + if (destroyOnHide) { + if (isExternal && externalPath) { + dispatch(setExternalValue(externalPath, undefined)) + } else { dispatch({ type: `flower/unsetData`, payload: { flowName: flowNameFromPath, id: path } }) - resetField() } + resetField() } - }, [destroyOnHide, hidden, flowNameFromPath, path, resetField]) + } + }, [ + destroyOnHide, + hidden, + flowNameFromPath, + path, + resetField, + isExternal, + externalPath, + dispatch + ]) useEffect(() => { if (defaultValue && !dirty && !isEqual(value, defaultValue)) { diff --git a/packages/flower-react/src/components/FlowerValue.tsx b/packages/flower-react/src/components/FlowerValue.tsx index 05e8210..bc6419d 100644 --- a/packages/flower-react/src/components/FlowerValue.tsx +++ b/packages/flower-react/src/components/FlowerValue.tsx @@ -1,11 +1,11 @@ /* eslint-disable */ -import React, { useContext, useEffect, useMemo } from 'react'; -import { CoreUtils } from '@flowerforce/flower-core'; +import React, { useContext, useEffect, useMemo } from 'react' import { useSelector } from '../provider'; import { getDataFromState } from '../selectors'; import { context } from '../context'; import FlowerRule from './FlowerRule'; import { FlowerValueProps } from './types/FlowerValue'; +import { readExternalValue, resolveFieldPath } from '@flowerforce/flower-core'; //TODO make types for wrapper function function Wrapper({ @@ -17,11 +17,13 @@ function Wrapper({ onUpdate, ...props }: any) { - const { flowNameFromPath = flowName, path } = useMemo( - () => CoreUtils.getPath(id), - [id] + const { flowNameFromPath = flowName, path, externalPath, isExternal } = + useMemo(() => resolveFieldPath(id, flowName), [id, flowName]); + const value = useSelector( + isExternal && externalPath + ? (state: Record) => readExternalValue(state, externalPath) + : getDataFromState(flowNameFromPath, path) ); - const value = useSelector(getDataFromState(flowNameFromPath, path)); const values = spreadValue && typeof value === 'object' && !Array.isArray(value) ? value diff --git a/packages/flower-react/src/components/types/FlowerProvider.ts b/packages/flower-react/src/components/types/FlowerProvider.ts index 2dd6f18..04b5037 100644 --- a/packages/flower-react/src/components/types/FlowerProvider.ts +++ b/packages/flower-react/src/components/types/FlowerProvider.ts @@ -1,26 +1,15 @@ -import { ThunkMiddleware, Tuple, configureStore } from '@reduxjs/toolkit' -import { Flower } from '@flowerforce/flower-core' -import { UnknownAction } from 'redux' +import type { EnhancedStore } from '@reduxjs/toolkit' +import type { Flower } from '@flowerforce/flower-core' -export interface FlowerProviderInterface { - render(): JSX.Element +export type FlowerProviderState = { + flower: Record> } -export type FlowerProviderProps = ReturnType< - typeof configureStore< - { - flower: Record> - }, - UnknownAction, - Tuple< - [ - ThunkMiddleware< - { - flower: Record> - }, - UnknownAction - > - ] - > - > -> +export type FlowerProviderStore = EnhancedStore + +export type FlowerProviderProps = FlowerProviderStore + +export interface FlowerProviderOptions { + enableReduxDevtool?: boolean + store?: FlowerProviderStore +} diff --git a/packages/flower-react/src/components/useFlowerForm.ts b/packages/flower-react/src/components/useFlowerForm.ts index b85aae8..be2ef82 100644 --- a/packages/flower-react/src/components/useFlowerForm.ts +++ b/packages/flower-react/src/components/useFlowerForm.ts @@ -1,10 +1,11 @@ import { useCallback, useContext } from 'react' -import { CoreUtils } from '@flowerforce/flower-core' import get from 'lodash/get' import { context } from '../context' import { makeSelectCurrentNodeId, makeSelectNodeErrors } from '../selectors' import { actions } from '../reducer' import { useDispatch, useSelector, useStore } from '../provider' +import { setExternalValue } from '../externalState' +import { resolveFieldPath } from '@flowerforce/flower-core' import { UseFlowerForm } from './types/FlowerHooks' /** This hook allows you to manage and retrieve information about Forms. @@ -52,8 +53,16 @@ const useFlowerForm: UseFlowerForm = ({ const getData = useCallback( (path?: string) => { - const { flowNameFromPath = flowName, path: newpath } = - CoreUtils.getPath(path) + const { + flowNameFromPath = flowName, + path: newpath, + externalPath + } = resolveFieldPath(path, flowName) + + if (externalPath?.length) { + return get(store.getState(), externalPath) + } + return get(store.getState(), [ 'flower', flowNameFromPath, @@ -66,8 +75,10 @@ const useFlowerForm: UseFlowerForm = ({ const getFormStatus = useCallback( (path?: string) => { - const { flowNameFromPath = flowName, path: newpath } = - CoreUtils.getPath(path) + const { flowNameFromPath = flowName, path: newpath } = resolveFieldPath( + path, + flowName + ) return get(store.getState(), [ 'flower', flowNameFromPath, @@ -80,7 +91,16 @@ const useFlowerForm: UseFlowerForm = ({ const setDataField = useCallback( (id: string, val: any, dirty = true) => { - const { flowNameFromPath = flowName } = CoreUtils.getPath(id) + const { + flowNameFromPath = flowName, + externalPath + } = resolveFieldPath(id, flowName) + + if (externalPath?.length) { + dispatch(setExternalValue(externalPath, val)) + return + } + dispatch( actions.addDataByPath({ flowName: flowNameFromPath, @@ -107,8 +127,17 @@ const useFlowerForm: UseFlowerForm = ({ const unsetData = useCallback( (path: string) => { - const { flowNameFromPath = flowName, path: newpath } = - CoreUtils.getPath(path) + const { + flowNameFromPath = flowName, + path: newpath, + externalPath + } = resolveFieldPath(path, flowName) + + if (externalPath?.length) { + dispatch(setExternalValue(externalPath, undefined)) + return + } + dispatch(actions.unsetData({ flowName: flowNameFromPath, id: newpath })) }, [flowName, dispatch] diff --git a/packages/flower-react/src/createFlowerStore.ts b/packages/flower-react/src/createFlowerStore.ts new file mode 100644 index 0000000..49253de --- /dev/null +++ b/packages/flower-react/src/createFlowerStore.ts @@ -0,0 +1,53 @@ +import { + combineReducers, + configureStore as configureReduxStore, + UnknownAction, + type ConfigureStoreOptions, + type Reducer, + type ReducersMapObject +} from '@reduxjs/toolkit' +import { produce } from 'immer' +import set from 'lodash/set' +import { FLOWER_EXTERNAL_UPDATE, FlowerExternalPayload } from './externalState' +import flowerReducer from './reducer' + +const applyExternalUpdate = (state: any, action: UnknownAction) => { + const { path, value } = (action.payload as FlowerExternalPayload) ?? {} + if (!Array.isArray(path) || path.length === 0) { + return state + } + return produce(state, (draft: any) => { + set(draft, path, value) + }) +} + +const wrapReducerWithExternal = (reducer: Reducer) => ( + state: any, + action: UnknownAction +) => { + if (action.type === FLOWER_EXTERNAL_UPDATE) { + const patchedState = applyExternalUpdate(state, action) + const nextState = reducer(patchedState, action) + return applyExternalUpdate(nextState, action) + } + return reducer(state, action) +} + +export const createFlowerStore = ( + options: ConfigureStoreOptions +) => { + const { reducer, ...rest } = options + const reducerMap = reducer as ReducersMapObject + + const rootReducerMap: ReducersMapObject = { + flower: flowerReducer, + ...reducerMap + } + + const rootReducer = combineReducers(rootReducerMap) as Reducer + + return configureReduxStore({ + ...rest, + reducer: wrapReducerWithExternal(rootReducer) + }) +} diff --git a/packages/flower-react/src/externalState.ts b/packages/flower-react/src/externalState.ts new file mode 100644 index 0000000..27830fb --- /dev/null +++ b/packages/flower-react/src/externalState.ts @@ -0,0 +1,17 @@ +export const FLOWER_EXTERNAL_UPDATE = 'flower/external/update' + +export interface FlowerExternalPayload { + path: string[] + value: any +} + +export const setExternalValue = ( + path: string[], + value: any +): { + type: string + payload: FlowerExternalPayload +} => ({ + type: FLOWER_EXTERNAL_UPDATE, + payload: { path, value } +}) diff --git a/packages/flower-react/src/index.ts b/packages/flower-react/src/index.ts index 26f6ed4..ef874b1 100644 --- a/packages/flower-react/src/index.ts +++ b/packages/flower-react/src/index.ts @@ -16,6 +16,8 @@ export { default as FlowerComponent } from './components/FlowerComponent' export { default as useFlower } from './components/useFlower' export { default as useFlowerForm } from './components/useFlowerForm' export { default as FlowerProvider } from './provider' +export { createFlowerStore } from './createFlowerStore' +export { actions, reducerFlower } from './reducer' export { getDataByFlow } from './selectors' export { useSelector, useDispatch, useStore } from './provider' export type { FlowerContext as FlowerContextProps } from './context' @@ -38,7 +40,10 @@ export type { FlowerNavigateActionsProps } from './components/types/FlowerNavigate' export type { FlowerNodeProps } from './components/types/FlowerNode' -export type { FlowerProviderProps } from './components/types/FlowerProvider' +export type { + FlowerProviderOptions, + FlowerProviderProps +} from './components/types/FlowerProvider' export type { FlowerRouteProps } from './components/types/FlowerRoute' export type { FlowerRuleProps } from './components/types/FlowerRule' export type { FlowerServerProps } from './components/types/FlowerServer' diff --git a/packages/flower-react/src/provider.tsx b/packages/flower-react/src/provider.tsx index a630433..7b4e24d 100644 --- a/packages/flower-react/src/provider.tsx +++ b/packages/flower-react/src/provider.tsx @@ -6,9 +6,13 @@ import { createStoreHook, ReactReduxContextValue } from 'react-redux' -import { Action, configureStore } from '@reduxjs/toolkit' +import { Action } from '@reduxjs/toolkit' import { reducerFlower } from './reducer' -import { FlowerProviderProps } from './components/types/FlowerProvider' +import { createFlowerStore } from './createFlowerStore' +import { + FlowerProviderOptions, + FlowerProviderStore +} from './components/types/FlowerProvider' //TODO check reduxContext type due to remove all any types @@ -20,30 +24,40 @@ export const useDispatch = createDispatchHook(reduxContext) // exported export const useSelector = createSelectorHook(reduxContext) export const useStore = createStoreHook(reduxContext) -export const store = ({ enableDevtool }: { enableDevtool?: boolean }) => - configureStore({ +export const store = ({ + enableDevtool +}: { + enableDevtool?: boolean +}): FlowerProviderStore => { + return createFlowerStore({ reducer: reducerFlower, devTools: enableDevtool ? { name: 'flower' } : false }) +} -class FlowerProvider extends PureComponent< - PropsWithChildren<{ enableReduxDevtool?: boolean }>, - FlowerProviderProps -> { - private store: FlowerProviderProps - constructor(props: PropsWithChildren<{ enableReduxDevtool?: boolean }>) { - super(props) - this.store = store({ enableDevtool: props.enableReduxDevtool }) - } +class FlowerProvider extends PureComponent> { + private storeInstance?: FlowerProviderStore render() { - const { children } = this.props + const { children, store: providedStore } = this.props + const currentStore = + providedStore ?? this.storeInstance ?? this.getStore() + return ( - + {children} ) } + + private getStore(): FlowerProviderStore { + if (!this.storeInstance) { + this.storeInstance = store({ + enableDevtool: this.props.enableReduxDevtool + }) + } + return this.storeInstance + } } export default FlowerProvider diff --git a/packages/flower-react/src/reducer.ts b/packages/flower-react/src/reducer.ts index d9a440e..8383dfd 100644 --- a/packages/flower-react/src/reducer.ts +++ b/packages/flower-react/src/reducer.ts @@ -1,16 +1,69 @@ -import { createSlice } from '@reduxjs/toolkit' -import { FlowerCoreReducers, Flower } from '@flowerforce/flower-core' +import { produce } from 'immer' +import type { UnknownAction } from '@reduxjs/toolkit' +import { + Flower, + FlowerCoreReducers, + type ActionWithPayload +} from '@flowerforce/flower-core' -const flowerReducer = createSlice({ - name: 'flower', - initialState: {} as Record>>, - reducers: FlowerCoreReducers -}) +const FLOWER_SLICE_NAME = 'flower' -export const { actions } = flowerReducer +export type FlowerState = Record>> + +const initialState: FlowerState = {} + +type FlowerReducerCases = keyof typeof FlowerCoreReducers +type FlowerActionPayload = Parameters< + typeof FlowerCoreReducers[Key] +>[1]['payload'] + +type FlowerActionCreators = { + [Key in FlowerReducerCases]: ( + payload: FlowerActionPayload + ) => { + type: string + payload: FlowerActionPayload + } +} + +export const actions = {} as FlowerActionCreators + +const assignActionCreator = (key: Key) => { + actions[key] = ((payload: FlowerActionPayload) => ({ + type: `${FLOWER_SLICE_NAME}/${key}`, + payload + })) as FlowerActionCreators[Key] +} + +;(Object.keys(FlowerCoreReducers) as FlowerReducerCases[]).forEach((key) => + assignActionCreator(key) +) + +const getReducerKey = (actionType: string): FlowerReducerCases | undefined => { + if (!actionType.startsWith(`${FLOWER_SLICE_NAME}/`)) return undefined + const key = actionType.slice(FLOWER_SLICE_NAME.length + 1) + if (key in FlowerCoreReducers) { + return key as FlowerReducerCases + } + return undefined +} + +const flowerReducer = ( + state: FlowerState = initialState, + action: UnknownAction +): FlowerState => { + const reducerKey = getReducerKey(action.type) + if (!reducerKey) return state + return produce(state, (draft) => { + FlowerCoreReducers[reducerKey]( + draft, + action as ActionWithPayload + ) + }) +} export const reducerFlower = { - flower: flowerReducer.reducer + flower: flowerReducer } export default flowerReducer diff --git a/packages/flower-react/src/selectors.ts b/packages/flower-react/src/selectors.ts index 35bf7e3..5e6e39f 100644 --- a/packages/flower-react/src/selectors.ts +++ b/packages/flower-react/src/selectors.ts @@ -10,6 +10,7 @@ import _get from 'lodash/get' const { getAllData: mapData } = FlowerStateUtils const { selectGlobal } = Selectors +const selectRootState = (state: Record) => state ?? {} const selectFlower = (name: string) => createSelector(selectGlobal, Selectors.selectFlower(name)) @@ -98,7 +99,17 @@ const makeSelectNodeFormSubmitted = (name: string, currentNodeId: string) => Selectors.makeSelectNodeFormSubmitted ) -const getAllData = createSelector(selectGlobal, mapData) +const getAllData = createSelector( + selectRootState, + selectGlobal, + (rootState, flowerState) => { + const { flower: _flower, ...rest } = rootState ?? {} + return { + ...rest, + ...mapData(flowerState) + } + } +) const selectFlowerFormCurrentNode = (name: string) => createSelector( @@ -116,6 +127,8 @@ const makeSelectFieldError = (name: string, id: string, validate: any) => Selectors.makeSelectFieldError(name, id, validate) ) +const makeSelectFieldValue = Selectors.makeSelectFieldValue + export const selectorRulesDisabled = ( id: string, rules: RulesObject | FunctionRule, @@ -144,6 +157,7 @@ export { makeSelectNodeFieldFocused, makeSelectNodeFieldDirty, makeSelectFieldError, + makeSelectFieldValue, makeSelectNodeFormSubmitted, makeSelectPrevNodeRetain } From 40178c48971bb1542f8efc8d181831c34ba6ffb2 Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Tue, 30 Dec 2025 15:32:57 +0100 Subject: [PATCH 3/7] fix: rules to with external reducer --- .../flower-core/src/FlowerCoreStateUtils.ts | 10 ++++++---- .../flower-react/src/createFlowerStore.ts | 20 ++++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/flower-core/src/FlowerCoreStateUtils.ts b/packages/flower-core/src/FlowerCoreStateUtils.ts index a661d8f..4eeffd8 100644 --- a/packages/flower-core/src/FlowerCoreStateUtils.ts +++ b/packages/flower-core/src/FlowerCoreStateUtils.ts @@ -4,10 +4,12 @@ import { CoreStateUtils } from './interfaces/UtilsInterface' export const FlowerStateUtils: CoreStateUtils = { getAllData: (state) => state && - Object.entries(state ?? {}).reduce( - (acc, [k, v]) => ({ ...acc, [k]: v.data }), - {} - ), + Object.entries(state ?? {}).reduce((acc, [k, v]) => { + if (k === '__external') { + return { ...acc, ...v } + } + return { ...acc, [k]: v.data } + }, {}), selectFlowerFormNode: (name, id) => (state) => _get(state, [name, 'form', id]), diff --git a/packages/flower-react/src/createFlowerStore.ts b/packages/flower-react/src/createFlowerStore.ts index 49253de..0a22fb3 100644 --- a/packages/flower-react/src/createFlowerStore.ts +++ b/packages/flower-react/src/createFlowerStore.ts @@ -21,16 +21,30 @@ const applyExternalUpdate = (state: any, action: UnknownAction) => { }) } +const attachExternalSnapshot = (state: any) => { + if (!state) return state + const { flower, ...rest } = state + return { + ...state, + flower: { + ...flower, + __external: rest + } + } +} + const wrapReducerWithExternal = (reducer: Reducer) => ( state: any, action: UnknownAction ) => { + const stateWithSnapshot = attachExternalSnapshot(state) if (action.type === FLOWER_EXTERNAL_UPDATE) { - const patchedState = applyExternalUpdate(state, action) + const patchedState = applyExternalUpdate(stateWithSnapshot, action) const nextState = reducer(patchedState, action) - return applyExternalUpdate(nextState, action) + return attachExternalSnapshot(nextState) } - return reducer(state, action) + const nextState = reducer(stateWithSnapshot, action) + return attachExternalSnapshot(nextState) } export const createFlowerStore = ( From c7fa061de10a97e2fd953d6c94995f4759002c40 Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Tue, 30 Dec 2025 19:18:01 +0100 Subject: [PATCH 4/7] feat: change # in ^ --- README.md | 7 +++---- packages/flower-core/src/CoreUtils.ts | 20 +++++++++---------- .../src/__tests__/coreUtils.test.ts | 11 +++++++++- .../src/__tests__/fieldPaths.test.ts | 18 ++++++++++++++--- .../src/__tests__/flowerCoreSelector.test.ts | 13 ++++++++++-- packages/flower-core/src/externalReducers.ts | 16 +++++++++++++++ packages/flower-core/src/index.ts | 5 +++++ .../src/Examples/ExampleExternalStore.tsx | 8 ++++---- packages/flower-react/README.md | 10 +++++----- .../flower-react/src/createFlowerStore.ts | 19 ++++++++++++++---- packages/flower-react/src/provider.tsx | 2 -- 11 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 packages/flower-core/src/externalReducers.ts diff --git a/README.md b/README.md index 3d1c151..ff41f8d 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,13 @@ Flower is currently available for React only. --> ## External Redux Store Support -Flower può convivere con uno store già esistente semplicemente usando `createFlowerStore`, lo stesso helper che `FlowerProvider` usa internamente: la funzione accetta la stessa configurazione di `configureStore` ma aggiunge un reducer che intercetta gli id `#external.*` e aggiorna il rispettivo path alla radice dello stato. I reducer esterni non devono contenere logiche custom per Flower, basta combinarli normalmente. +Flower può convivere con uno store già esistente semplicemente usando `createFlowerStore`, lo stesso helper che `FlowerProvider` usa internamente: la funzione accetta la stessa configurazione di `configureStore` ma aggiunge un reducer che intercetta gli id `^external.*` e aggiorna il rispettivo path alla radice dello stato. I reducer esterni non devono contenere logiche custom per Flower, basta combinarli normalmente. ```tsx import { createFlowerStore } from '@flowerforce/flower-react' -import { reducerFlower } from '@flowerforce/flower-react' const store = createFlowerStore({ reducer: { - flower: reducerFlower, external: (state = { externalMessage: '' }) => state } }) @@ -39,8 +37,9 @@ function AppWithExternalStore() { ) } ``` +L'helper `createFlowerStore` registra automaticamente il reducer interno di Flower, quindi ti basta dichiarare i reducer esterni da integrare. -`FlowerField` e le regole di navigazione possono continuare a usare `#external.*`, e ogni scrittura viene automaticamente applicata al path specificato (es. `state.external.externalMessage`). I reducer esterni vedono i dati aggiornati senza dover ascoltare azioni Flower-specifiche. +`FlowerField` e le regole di navigazione possono continuare a usare `^external.*`, e ogni scrittura viene automaticamente applicata al path specificato (es. `state.external.externalMessage`). I reducer esterni vedono i dati aggiornati senza dover ascoltare azioni Flower-specifiche. ## Full Documentation diff --git a/packages/flower-core/src/CoreUtils.ts b/packages/flower-core/src/CoreUtils.ts index 4719bc3..04b23e7 100644 --- a/packages/flower-core/src/CoreUtils.ts +++ b/packages/flower-core/src/CoreUtils.ts @@ -10,6 +10,7 @@ import mapKeys from 'lodash/mapKeys' import mapValues from 'lodash/mapValues' import trimStart from 'lodash/trimStart' import { MatchRules } from './RulesMatcher' +import { isExternalReducer } from './externalReducers' import { CoreUtilitiesFunctions, GetRulesExists @@ -190,19 +191,16 @@ export const CoreUtils: CoreUtilitiesFunctions = { } if (idValue.indexOf('^') === 0) { - const [flowNameFromPath, ...rest] = - CoreUtils.cleanPath(idValue).split('.') - return { - flowNameFromPath, - path: rest + const [rootName, ...rest] = CoreUtils.cleanPath(idValue).split('.') + if (isExternalReducer(rootName)) { + return { + path: [], + externalPath: [rootName, ...rest].filter(Boolean) + } } - } - - if (idValue.indexOf('#') === 0) { - const externalPath = CoreUtils.cleanPath(idValue, '#').split('.') return { - path: [], - externalPath + flowNameFromPath: rootName, + path: rest } } diff --git a/packages/flower-core/src/__tests__/coreUtils.test.ts b/packages/flower-core/src/__tests__/coreUtils.test.ts index f72a82a..8c4ff85 100644 --- a/packages/flower-core/src/__tests__/coreUtils.test.ts +++ b/packages/flower-core/src/__tests__/coreUtils.test.ts @@ -3,6 +3,15 @@ import { flattenRules // searchEmptyKeyRecursively, } from '../CoreUtils' +import { + clearExternalReducers, + registerExternalReducers +} from '../externalReducers' + +beforeEach(() => { + clearExternalReducers() + registerExternalReducers(['external']) +}) describe('flattenRules function', () => { test('should flatten nested object into a single-level object', () => { @@ -567,7 +576,7 @@ describe('CoreUtils object', () => { const emptyPath = CoreUtils.getPath() expect(emptyPath).toEqual({ path: [] }) - const externalPath = CoreUtils.getPath('#external.externalMessage') + const externalPath = CoreUtils.getPath('^external.externalMessage') expect(externalPath).toEqual({ path: [], externalPath: ['external', 'externalMessage'] diff --git a/packages/flower-core/src/__tests__/fieldPaths.test.ts b/packages/flower-core/src/__tests__/fieldPaths.test.ts index 8738f47..89efbed 100644 --- a/packages/flower-core/src/__tests__/fieldPaths.test.ts +++ b/packages/flower-core/src/__tests__/fieldPaths.test.ts @@ -1,6 +1,18 @@ -import { readExternalValue, resolveFieldPath } from '../utils/fieldPaths' +import { + readExternalValue, + resolveFieldPath +} from '../utils/fieldPaths' +import { + clearExternalReducers, + registerExternalReducers +} from '../externalReducers' describe('fieldPaths utils', () => { + beforeEach(() => { + clearExternalReducers() + registerExternalReducers(['external']) + }) + it('resolves regular field paths with flowName fallback', () => { const result = resolveFieldPath('form.field', 'defaultFlow') @@ -10,8 +22,8 @@ describe('fieldPaths utils', () => { expect(result.externalPath).toBeUndefined() }) - it('resolves external paths starting with # and marks them as external', () => { - const result = resolveFieldPath('#external.values.message', 'defaultFlow') + it('resolves external paths starting with ^ and marks them as external', () => { + const result = resolveFieldPath('^external.values.message', 'defaultFlow') expect(result.path).toEqual([]) expect(result.externalPath).toEqual(['external', 'values', 'message']) diff --git a/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts b/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts index 28e989a..bc00f58 100644 --- a/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts +++ b/packages/flower-core/src/__tests__/flowerCoreSelector.test.ts @@ -1,5 +1,14 @@ import { FlowerCoreStateSelectors } from '../FlowerCoreStateSelectors' import { Flower } from '../interfaces/Store' +import { + clearExternalReducers, + registerExternalReducers +} from '../externalReducers' + +beforeEach(() => { + clearExternalReducers() + registerExternalReducers(['external']) +}) //todo: double check if tests are ok @@ -248,7 +257,7 @@ describe('FlowerCoreSelectors', () => { it('returns values from external paths', () => { const selectFieldValue = FlowerCoreStateSelectors.makeSelectFieldValue( TEST_FLOW_NAME, - '#external.externalMessage' + '^external.externalMessage' ) expect(selectFieldValue(state)).toEqual('external value') @@ -257,7 +266,7 @@ describe('FlowerCoreSelectors', () => { it('returns undefined for missing external paths', () => { const selectFieldValue = FlowerCoreStateSelectors.makeSelectFieldValue( TEST_FLOW_NAME, - '#external.unknownValue' + '^external.unknownValue' ) expect(selectFieldValue(state)).toBeUndefined() diff --git a/packages/flower-core/src/externalReducers.ts b/packages/flower-core/src/externalReducers.ts new file mode 100644 index 0000000..5ee44a9 --- /dev/null +++ b/packages/flower-core/src/externalReducers.ts @@ -0,0 +1,16 @@ +const externalReducerNames = new Set() + +export const registerExternalReducers = (names: string[] = []) => { + names.forEach((name) => { + if (name && name !== 'flower') { + externalReducerNames.add(name) + } + }) +} + +export const isExternalReducer = (name?: string) => + !!name && externalReducerNames.has(name) + +export const clearExternalReducers = () => { + externalReducerNames.clear() +} diff --git a/packages/flower-core/src/index.ts b/packages/flower-core/src/index.ts index af6fe17..3832b76 100644 --- a/packages/flower-core/src/index.ts +++ b/packages/flower-core/src/index.ts @@ -7,3 +7,8 @@ export { MatchRules } from './RulesMatcher' export { devtoolState } from './devtoolState' export * from './interfaces' export { readExternalValue, resolveFieldPath } from './utils/fieldPaths' +export { + registerExternalReducers, + isExternalReducer, + clearExternalReducers +} from './externalReducers' diff --git a/packages/flower-demo/src/Examples/ExampleExternalStore.tsx b/packages/flower-demo/src/Examples/ExampleExternalStore.tsx index d2e03c1..6a66334 100644 --- a/packages/flower-demo/src/Examples/ExampleExternalStore.tsx +++ b/packages/flower-demo/src/Examples/ExampleExternalStore.tsx @@ -44,7 +44,7 @@ export function ExampleExternalStore() { success: null, info: { rules: { - "#external.externalMessage": { + "^external.externalMessage": { $eq: "asd" } } @@ -53,7 +53,7 @@ export function ExampleExternalStore() {
External Store

Navigation uses the same reducers but a custom store instance.

-
Info - + {({ value }) => value} diff --git a/packages/flower-react/README.md b/packages/flower-react/README.md index ba879db..d1866c2 100644 --- a/packages/flower-react/README.md +++ b/packages/flower-react/README.md @@ -63,22 +63,22 @@ function Root() { ### External store support -Flower può condividere lo store Redux che già usi. Usa `createFlowerStore` da `@flowerforce/flower-react` al posto di `configureStore`: l’helper accetta la stessa configurazione ma aggiunge un reducer che intercetta gli id `#external.*` e applica il nuovo valore direttamente al path specificato. In questo modo i reducer esterni non devono conoscere Flower e il codice dello store rimane invariato. +Flower può condividere lo store Redux che già usi. Usa `createFlowerStore` da `@flowerforce/flower-react` al posto di `configureStore`: l’helper accetta la stessa configurazione ma aggiunge un reducer che intercetta gli id `^external.*` e applica il nuovo valore direttamente al path specificato. In questo modo i reducer esterni non devono conoscere Flower e il codice dello store rimane invariato. ```tsx -import { reducerFlower, createFlowerStore } from '@flowerforce/flower-react' +import { createFlowerStore } from '@flowerforce/flower-react' const store = createFlowerStore({ reducer: { - flower: reducerFlower, external: (state = { externalMessage: '' }) => state } }) ``` +`createFlowerStore` registra automaticamente il reducer di Flower, quindi non serve passare manualmente `reducerFlower`. -`FlowerField` e le regole di navigazione possono continuare a usare `#external.*` e lo stato esterno viene aggiornato automaticamente in `state.external.*` senza che il reducer debba ascoltare azioni Flower-specifiche. +`FlowerField` e le regole di navigazione possono continuare a usare `^external.*` e lo stato esterno viene aggiornato automaticamente in `state.external.*` senza che il reducer debba ascoltare azioni Flower-specifiche. -`FlowerField` e le regole di navigazione possono mantenere gli stessi `#external.*` path, ma i dati vengono scritti solo da chi gestisce la callback: la tua slice non ha bisogno di sapere che Flower sta passando l’aggiornamento. +`FlowerField` e le regole di navigazione possono mantenere gli stessi `^external.*` path, ma i dati vengono scritti solo da chi gestisce la callback: la tua slice non ha bisogno di sapere che Flower sta passando l’aggiornamento. ## How to use diff --git a/packages/flower-react/src/createFlowerStore.ts b/packages/flower-react/src/createFlowerStore.ts index 0a22fb3..c5e8ac4 100644 --- a/packages/flower-react/src/createFlowerStore.ts +++ b/packages/flower-react/src/createFlowerStore.ts @@ -10,6 +10,7 @@ import { produce } from 'immer' import set from 'lodash/set' import { FLOWER_EXTERNAL_UPDATE, FlowerExternalPayload } from './externalState' import flowerReducer from './reducer' +import { registerExternalReducers } from '@flowerforce/flower-core' const applyExternalUpdate = (state: any, action: UnknownAction) => { const { path, value } = (action.payload as FlowerExternalPayload) ?? {} @@ -47,11 +48,21 @@ const wrapReducerWithExternal = (reducer: Reducer) => ( return attachExternalSnapshot(nextState) } -export const createFlowerStore = ( - options: ConfigureStoreOptions -) => { +type FlowerStoreOptions = Omit< + ConfigureStoreOptions, + 'reducer' +> & { + reducer?: ReducersMapObject +} + +export const createFlowerStore = (options: FlowerStoreOptions) => { const { reducer, ...rest } = options - const reducerMap = reducer as ReducersMapObject + const reducerMap = + (reducer as ReducersMapObject) ?? {} + + registerExternalReducers( + Object.keys(reducerMap).filter((name) => name !== 'flower') + ) const rootReducerMap: ReducersMapObject = { flower: flowerReducer, diff --git a/packages/flower-react/src/provider.tsx b/packages/flower-react/src/provider.tsx index 7b4e24d..7c6e81e 100644 --- a/packages/flower-react/src/provider.tsx +++ b/packages/flower-react/src/provider.tsx @@ -7,7 +7,6 @@ import { ReactReduxContextValue } from 'react-redux' import { Action } from '@reduxjs/toolkit' -import { reducerFlower } from './reducer' import { createFlowerStore } from './createFlowerStore' import { FlowerProviderOptions, @@ -30,7 +29,6 @@ export const store = ({ enableDevtool?: boolean }): FlowerProviderStore => { return createFlowerStore({ - reducer: reducerFlower, devTools: enableDevtool ? { name: 'flower' } : false }) } From 48cb991c9fb45afc2742197be13039292aef5a74 Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Tue, 30 Dec 2025 19:22:26 +0100 Subject: [PATCH 5/7] fieat: check reducer conflict --- packages/flower-core/src/externalReducers.ts | 2 ++ packages/flower-core/src/index.ts | 3 ++- packages/flower-react/src/components/Flower.tsx | 13 ++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/flower-core/src/externalReducers.ts b/packages/flower-core/src/externalReducers.ts index 5ee44a9..4fa0973 100644 --- a/packages/flower-core/src/externalReducers.ts +++ b/packages/flower-core/src/externalReducers.ts @@ -14,3 +14,5 @@ export const isExternalReducer = (name?: string) => export const clearExternalReducers = () => { externalReducerNames.clear() } + +export const getExternalReducerNames = () => Array.from(externalReducerNames) diff --git a/packages/flower-core/src/index.ts b/packages/flower-core/src/index.ts index 3832b76..6c20a8a 100644 --- a/packages/flower-core/src/index.ts +++ b/packages/flower-core/src/index.ts @@ -10,5 +10,6 @@ export { readExternalValue, resolveFieldPath } from './utils/fieldPaths' export { registerExternalReducers, isExternalReducer, - clearExternalReducers + clearExternalReducers, + getExternalReducerNames } from './externalReducers' diff --git a/packages/flower-react/src/components/Flower.tsx b/packages/flower-react/src/components/Flower.tsx index 8bd2591..0c9a812 100644 --- a/packages/flower-react/src/components/Flower.tsx +++ b/packages/flower-react/src/components/Flower.tsx @@ -10,7 +10,7 @@ import React, { PropsWithChildren } from 'react' import _keyBy from 'lodash/keyBy' -import { Emitter, devtoolState } from '@flowerforce/flower-core' +import { Emitter, devtoolState, getExternalReducerNames } from '@flowerforce/flower-core' import { Provider } from '../context' import _get from 'lodash/get' import { convertElements } from '../utils' @@ -51,6 +51,17 @@ const FlowerClient = ({ }: FlowerClientProps) => { const flowName = name + useEffect(() => { + if (!flowName) return + const externalNames = getExternalReducerNames() + if (externalNames.includes(flowName)) { + // eslint-disable-next-line no-console + console.warn( + `Flower flow name "${flowName}" matches an external reducer. This may cause confusing path resolution; consider renaming the flow or the reducer.` + ) + } + }, [flowName]) + const dispatch = useDispatch() const one = useRef(false) const [wsDevtools, setWsDevtools] = useState( From b8833554c4b04a8c882c0c2d91d3b3001f658cc1 Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Tue, 30 Dec 2025 19:41:59 +0100 Subject: [PATCH 6/7] feat: expose setExternalValue --- .../src/Examples/ExampleExternalStore.tsx | 70 +++++++++++++++++++ packages/flower-react/src/index.ts | 1 + 2 files changed, 71 insertions(+) diff --git a/packages/flower-demo/src/Examples/ExampleExternalStore.tsx b/packages/flower-demo/src/Examples/ExampleExternalStore.tsx index 6a66334..e182d4a 100644 --- a/packages/flower-demo/src/Examples/ExampleExternalStore.tsx +++ b/packages/flower-demo/src/Examples/ExampleExternalStore.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { Flower, FlowerField, @@ -6,8 +7,14 @@ import { FlowerProvider, FlowerValue, createFlowerStore, + setExternalValue, + useDispatch, useSelector } from '@flowerforce/flower-react' +import { + Provider as ReduxProvider, + useDispatch as useReduxDispatch +} from 'react-redux' import './styles.css' type ExternalState = { @@ -32,11 +39,74 @@ const ExternalControls = () => { ) } +const ExternalDispatcher = () => { + const dispatch = useDispatch() + const [externalText, setExternalText] = useState('message from outside') + + const updateExternalMessage = () => { + dispatch(setExternalValue(['external', 'externalMessage'], externalText)) + } + + const updateWithTimestamp = () => { + dispatch( + setExternalValue( + ['external', 'externalMessage'], + `updated @ ${new Date().toLocaleTimeString()}` + ) + ) + } + + return ( +
+ +
+ + +
+
+ ) +} + +const ExternalDispatcherRedux = () => { + const dispatch = useReduxDispatch() + const [text, setText] = useState('dispatched da redux') + + const dispatchText = () => + dispatch(setExternalValue(['external', 'externalMessage'], text)) + + return ( +
+ +
+ +
+
+ ) +} + export function ExampleExternalStore() { return (
+ + + + diff --git a/packages/flower-react/src/index.ts b/packages/flower-react/src/index.ts index ef874b1..cadcbe0 100644 --- a/packages/flower-react/src/index.ts +++ b/packages/flower-react/src/index.ts @@ -20,6 +20,7 @@ export { createFlowerStore } from './createFlowerStore' export { actions, reducerFlower } from './reducer' export { getDataByFlow } from './selectors' export { useSelector, useDispatch, useStore } from './provider' +export { setExternalValue } from './externalState' export type { FlowerContext as FlowerContextProps } from './context' export type { FlowerNodeDefaultProps } from './components/types/DefaultNode' export type { FlowerComponentProps } from './components/types/FlowerComponent' From a502cd3dddf679ffc0569530d9ec6d81d4ebe90b Mon Sep 17 00:00:00 2001 From: Andrea Zucca Date: Tue, 30 Dec 2025 19:48:41 +0100 Subject: [PATCH 7/7] chore: update docs --- packages/flower-react/README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/flower-react/README.md b/packages/flower-react/README.md index d1866c2..3216609 100644 --- a/packages/flower-react/README.md +++ b/packages/flower-react/README.md @@ -63,22 +63,27 @@ function Root() { ### External store support -Flower può condividere lo store Redux che già usi. Usa `createFlowerStore` da `@flowerforce/flower-react` al posto di `configureStore`: l’helper accetta la stessa configurazione ma aggiunge un reducer che intercetta gli id `^external.*` e applica il nuovo valore direttamente al path specificato. In questo modo i reducer esterni non devono conoscere Flower e il codice dello store rimane invariato. +Flower può riutilizzare uno store Redux esistente sovrascrivendo il `configureStore` con `createFlowerStore`. La funzione: + +- arricchisce la configurazione standard con il reducer Flower interno; +- registra automaticamente i reducer aggiuntivi (tranne `flower`) come sorgenti esterne, in modo che le stringhe `^reducerName.path` sappiano a quale ramo dello stato scrivere o leggere; +- espone `setExternalValue(path, value)` per aggiornare il ramo esterno senza passare da `FlowerField`. ```tsx import { createFlowerStore } from '@flowerforce/flower-react' const store = createFlowerStore({ reducer: { - external: (state = { externalMessage: '' }) => state - } + external: (state = { externalMessage: '' }) => state, + anotherSlice: anotherReducer + }, + devTools: { name: 'flower-external-store' } }) ``` -`createFlowerStore` registra automaticamente il reducer di Flower, quindi non serve passare manualmente `reducerFlower`. -`FlowerField` e le regole di navigazione possono continuare a usare `^external.*` e lo stato esterno viene aggiornato automaticamente in `state.external.*` senza che il reducer debba ascoltare azioni Flower-specifiche. +Quando in un flow leggi o scrivi `^external.externalMessage`, Flower scrive il dato direttamente dentro `state.external.externalMessage` e non emette azioni Flower-specifiche verso i reducer esterni. Gli aggiornamenti manuali si ottengono dispatchando `setExternalValue(['external', 'externalMessage'], value)` con `useDispatch` (o con `react-redux` direttamente, come mostra la demo). -`FlowerField` e le regole di navigazione possono mantenere gli stessi `^external.*` path, ma i dati vengono scritti solo da chi gestisce la callback: la tua slice non ha bisogno di sapere che Flower sta passando l’aggiornamento. +Per evitare confusione tra flow e reducer, Flower segnala con una warning console quando il `flowName` coincide con il nome di un reducer esterno registrato. ## How to use