From a1476d9986a115a0c25afd77618a65880f3d1c86 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Thu, 10 Jun 2021 17:01:50 +0300 Subject: [PATCH] refactor: State management logic & tests --- src/Formulario.ts | 16 ++-- src/FormularioForm.vue | 28 ++++--- src/index.ts | 1 + src/utils/access.ts | 75 +++++++++++++------ src/utils/index.ts | 65 +--------------- storybook/stories/ExampleAddressList.tale.vue | 30 +++++++- storybook/stories/index.stories.js | 2 +- test/unit/FormularioForm.test.js | 6 +- test/unit/utils/access.test.js | 39 ++++++++-- 9 files changed, 143 insertions(+), 119 deletions(-) diff --git a/src/Formulario.ts b/src/Formulario.ts index 8a6d253..1e7f4c8 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -26,7 +26,7 @@ export default class Formulario { private readonly registry: Map - constructor (options?: FormularioOptions) { + public constructor (options?: FormularioOptions) { this.registry = new Map() this.validationRules = validationRules @@ -38,7 +38,7 @@ export default class Formulario { /** * Given a set of options, apply them to the pre-existing options. */ - extend (extendWith: FormularioOptions): Formulario { + public extend (extendWith: FormularioOptions): Formulario { if (typeof extendWith === 'object') { this.validationRules = merge(this.validationRules, extendWith.validationRules || {}) this.validationMessages = merge(this.validationMessages, extendWith.validationMessages || {}) @@ -47,7 +47,7 @@ export default class Formulario { throw new Error(`[Formulario]: Formulario.extend(): should be passed an object (was ${typeof extendWith})`) } - runValidation (id: string): Promise { + public runValidation (id: string): Promise { if (!this.registry.has(id)) { throw new Error(`[Formulario]: Formulario.runValidation(): no forms with id "${id}"`) } @@ -57,7 +57,7 @@ export default class Formulario { return form.runValidation() } - resetValidation (id: string): void { + public resetValidation (id: string): void { if (!this.registry.has(id)) { return } @@ -71,7 +71,7 @@ export default class Formulario { * Used by forms instances to add themselves into a registry * @internal */ - register (id: string, form: FormularioFormInterface): void { + public register (id: string, form: FormularioFormInterface): void { if (this.registry.has(id)) { throw new Error(`[Formulario]: Formulario.register(): id "${id}" is already in use`) } @@ -83,7 +83,7 @@ export default class Formulario { * Used by forms instances to remove themselves from a registry * @internal */ - unregister (id: string): void { + public unregister (id: string): void { if (this.registry.has(id)) { this.registry.delete(id) } @@ -93,7 +93,7 @@ export default class Formulario { * Get validation rules by merging any passed in with global rules. * @internal */ - getRules (extendWith: Record = {}): Record { + public getRules (extendWith: Record = {}): Record { return merge(this.validationRules, extendWith) } @@ -101,7 +101,7 @@ export default class Formulario { * Get validation messages by merging any passed in with global messages. * @internal */ - getMessages (vm: Vue, extendWith: Record): Record { + public getMessages (vm: Vue, extendWith: Record): Record { const raw = merge(this.validationMessages || {}, extendWith) const messages: Record = {} diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue index a07cb3a..05bc0dc 100644 --- a/src/FormularioForm.vue +++ b/src/FormularioForm.vue @@ -15,11 +15,12 @@ import { } from 'vue-property-decorator' import { clone, - getNested, + get, has, merge, - setNested, + set, shallowEquals, + unset, } from '@/utils' import PathRegistry from '@/PathRegistry' @@ -86,10 +87,15 @@ export default class FormularioForm extends Vue { private register (path: string, field: FormularioFieldInterface): void { this.registry.add(path, field) - const value = getNested(this.modelCopy, path) + const value = get(this.modelCopy, path) - if (!field.hasModel && this.modelIsDefined && value !== undefined) { - field.model = value + if (!field.hasModel && this.modelIsDefined) { + if (value !== undefined) { + field.model = value + } else { + this.setFieldValue(path, null) + this.emitInput() + } } else if (field.hasModel && !shallowEquals(field.proxy, value)) { this.setFieldValue(path, field.proxy) this.emitInput() @@ -116,13 +122,11 @@ export default class FormularioForm extends Vue { } @Provide('__FormularioForm_set') - private setFieldValue (field: string, value: unknown): void { + private setFieldValue (path: string, value: unknown): void { if (value === undefined) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [field]: value, ...proxy } = this.proxy - this.proxy = proxy + this.proxy = unset(this.proxy, path) as Record } else { - setNested(this.proxy, field, value) + this.proxy = set(this.proxy, path, value) as Record } } @@ -214,8 +218,8 @@ export default class FormularioForm extends Vue { } this.registry.getSubset(path).forEach((field, path) => { - const oldValue = getNested(this.proxy, path) - const newValue = getNested(state, path) + const oldValue = get(this.proxy, path, null) + const newValue = get(state, path, null) if (!shallowEquals(newValue, oldValue)) { this.setFieldValue(path, newValue) diff --git a/src/index.ts b/src/index.ts index 0c412e4..c6c7c6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import FormularioFieldGroup from '@/FormularioFieldGroup.vue' import FormularioForm from '@/FormularioForm.vue' export default { + Formulario, install (Vue: VueConstructor, options?: FormularioOptions): void { Vue.component('FormularioField', FormularioField) Vue.component('FormularioFieldGroup', FormularioFieldGroup) diff --git a/src/utils/access.ts b/src/utils/access.ts index a201876..628569c 100644 --- a/src/utils/access.ts +++ b/src/utils/access.ts @@ -7,10 +7,10 @@ const extractIntOrNaN = (value: string): number => { return numeric.toString() === value ? numeric : NaN } -const extractPath = (field: string): string[] => { +const extractPath = (raw: string): string[] => { const path = [] as string[] - field.split('.').forEach(key => { + raw.split('.').forEach(key => { if (/(.*)\[(\d+)]$/.test(key)) { path.push(...key.substr(0, key.length - 1).split('[').filter(k => k.length)) } else { @@ -21,18 +21,11 @@ const extractPath = (field: string): string[] => { return path } -const unsetInRecord = (record: Record, prop: string): Record => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [prop]: _, ...copy } = record - - return copy -} - -export function get (state: unknown, fieldOrPath: string|string[]): unknown { - const path = typeof fieldOrPath === 'string' ? extractPath(fieldOrPath) : fieldOrPath +export function get (state: unknown, rawOrPath: string|string[], fallback: unknown = undefined): unknown { + const path = typeof rawOrPath === 'string' ? extractPath(rawOrPath) : rawOrPath if (isScalar(state) || path.length === 0) { - return undefined + return fallback } const key = path.shift() as string @@ -40,7 +33,7 @@ export function get (state: unknown, fieldOrPath: string|string[]): unknown { if (!isNaN(index)) { if (Array.isArray(state) && index >= 0 && index < state.length) { - return path.length === 0 ? state[index] : get(state[index], path) + return path.length === 0 ? state[index] : get(state[index], path, fallback) } return undefined @@ -49,18 +42,54 @@ export function get (state: unknown, fieldOrPath: string|string[]): unknown { if (has(state as Record, key)) { const values = state as Record - return path.length === 0 ? values[key] : get(values[key], path) + return path.length === 0 ? values[key] : get(values[key], path, fallback) } return undefined } -export function unset (state: unknown, fieldOrPath: string|string[]): unknown { +export function set (state: unknown, rawOrPath: string|string[], value: unknown): unknown { + const path = typeof rawOrPath === 'string' ? extractPath(rawOrPath) : rawOrPath + + if (path.length === 0) { + return value + } + + const key = path.shift() as string + const index = extractIntOrNaN(key) + + if (!isRecordLike(state)) { + return set(!isNaN(index) ? [] : {}, [key, ...path], value) + } + + if (!isNaN(index) && Array.isArray(state)) { + const slice = [...state as unknown[]] + + slice[index] = path.length === 0 ? value : set(slice[index], path, value) + + return slice + } + + const slice = { ...state as Record } + + slice[key] = path.length === 0 ? value : set(slice[key], path, value) + + return slice +} + +const unsetInRecord = (record: Record, prop: string): Record => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [prop]: _, ...copy } = record + + return copy +} + +export function unset (state: unknown, rawOrPath: string|string[]): unknown { if (!isRecordLike(state)) { return state } - const path = typeof fieldOrPath === 'string' ? extractPath(fieldOrPath) : fieldOrPath + const path = typeof rawOrPath === 'string' ? extractPath(rawOrPath) : rawOrPath if (path.length === 0) { return state @@ -70,23 +99,23 @@ export function unset (state: unknown, fieldOrPath: string|string[]): unknown { const index = extractIntOrNaN(key) if (!isNaN(index) && Array.isArray(state) && index >= 0 && index < state.length) { - const values = (state as unknown[]).slice() + const slice = [...state as unknown[]] if (path.length === 0) { - values.splice(index, 1) + slice.splice(index, 1) } else { - values[index] = unset(values[index], path) + slice[index] = unset(slice[index], path) } - return values + return slice } if (has(state as Record, key)) { - const values = state as Record + const slice = { ...state as Record } return path.length === 0 - ? unsetInRecord(values, key) - : { ...values, [key]: unset(values[key], path) } + ? unsetInRecord(slice, key) + : { ...slice, [key]: unset(slice[key], path) } } return state diff --git a/src/utils/index.ts b/src/utils/index.ts index 0008ee5..7322c20 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,70 +1,7 @@ export { default as clone } from './clone' export { default as has } from './has' export { default as merge } from './merge' -export { get, unset } from './access' +export { get, set, unset } from './access' export { default as regexForFormat } from './regexForFormat' export { default as shallowEquals } from './shallowEquals' export { default as snakeToCamel } from './snakeToCamel' - -export function getNested (obj: Record, field: string): any { - const fieldParts = field.split('.') - - let result: Record = obj - - for (const key in fieldParts) { - const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/) - if (result === undefined) { - return null - } - if (matches) { - result = result[matches[1]] - - if (result === undefined) { - return null - } - result = result[matches[2]] - } else { - result = result[fieldParts[key]] - } - } - return result -} - -export function setNested (obj: Record, field: string, value: any): void { - const fieldParts = field.split('.') - - let subProxy: Record = obj - for (let i = 0; i < fieldParts.length; i++) { - const fieldPart = fieldParts[i] - const matches = fieldPart.match(/(.+)\[(\d+)\]$/) - - if (subProxy === undefined) { - break - } - - if (matches) { - if (subProxy[matches[1]] === undefined) { - subProxy[matches[1]] = [] - } - subProxy = subProxy[matches[1]] - - if (i === fieldParts.length - 1) { - subProxy[matches[2]] = value - break - } else { - subProxy = subProxy[matches[2]] - } - } else { - if (i === fieldParts.length - 1) { - subProxy[fieldPart] = value - break - } else { - // eslint-disable-next-line max-depth - if (subProxy[fieldPart] === undefined) { - subProxy[fieldPart] = {} - } - subProxy = subProxy[fieldPart] - } - } - } -} diff --git a/storybook/stories/ExampleAddressList.tale.vue b/storybook/stories/ExampleAddressList.tale.vue index b5708bc..34314cb 100644 --- a/storybook/stories/ExampleAddressList.tale.vue +++ b/storybook/stories/ExampleAddressList.tale.vue @@ -1,6 +1,30 @@