Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ 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'

const store = createFlowerStore({
reducer: {
external: (state = { externalMessage: '' }) => state
}
})

function AppWithExternalStore() {
return (
<FlowerProvider store={store}>
{/* i tuoi flow */}
</FlowerProvider>
)
}
```
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.

## Full Documentation

For more info [flowerjs.it/](https://flowerjs.it/).
Expand Down
34 changes: 28 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions packages/flower-core/src/CoreUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -190,10 +191,15 @@ export const CoreUtils: CoreUtilitiesFunctions = {
}

if (idValue.indexOf('^') === 0) {
const [flowNameFromPath, ...rest] =
CoreUtils.cleanPath(idValue).split('.')
const [rootName, ...rest] = CoreUtils.cleanPath(idValue).split('.')
if (isExternalReducer(rootName)) {
return {
path: [],
externalPath: [rootName, ...rest].filter(Boolean)
}
}
return {
flowNameFromPath,
flowNameFromPath: rootName,
path: rest
}
}
Expand Down
16 changes: 16 additions & 0 deletions packages/flower-core/src/FlowerCoreStateSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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])
}
}
10 changes: 6 additions & 4 deletions packages/flower-core/src/FlowerCoreStateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down
15 changes: 15 additions & 0 deletions packages/flower-core/src/__tests__/coreUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -566,6 +575,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', () => {
Expand Down
48 changes: 48 additions & 0 deletions packages/flower-core/src/__tests__/fieldPaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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')

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()
})
})
99 changes: 74 additions & 25 deletions packages/flower-core/src/__tests__/flowerCoreSelector.test.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,60 @@
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

const TEST_FLOW_NAME = 'test_flow'

const state: { flower: { [x: string]: Flower<Record<string, any>> } } = {
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<Record<string, any>> } = {
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<Record<string, any>> }
external: Record<string, any>
} = {
flower: flowerSlice,
external: {
externalMessage: 'external value',
nested: {
flag: true
}
}
}

describe('FlowerCoreSelectors', () => {
describe('SelectGlobal', () => {
it('should return the flower object in state.flower', () => {
Expand Down Expand Up @@ -224,6 +244,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'
Expand Down
18 changes: 18 additions & 0 deletions packages/flower-core/src/externalReducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const externalReducerNames = new Set<string>()

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()
}

export const getExternalReducerNames = () => Array.from(externalReducerNames)
Loading