refactor: Simplified logic of field components registry, moved value set logic from registry to form component
This commit is contained in:
parent
8144c27c69
commit
4c3274e621
@ -73,7 +73,7 @@ export default class FormularioField extends Vue {
|
||||
validator: behavior => Object.values(VALIDATION_BEHAVIOR).includes(behavior)
|
||||
}) validationBehavior!: string
|
||||
|
||||
// Affects only observing & setting of local errors
|
||||
// Affects only setting of local errors
|
||||
@Prop({ default: false }) errorsDisabled!: boolean
|
||||
|
||||
@Prop({ default: () => <U, T>(value: U|Empty): U|T|Empty => value }) modelGetConverter!: ModelGetConverter
|
||||
@ -87,10 +87,17 @@ export default class FormularioField extends Vue {
|
||||
|
||||
private validationRun: Promise<Violation[]> = Promise.resolve([])
|
||||
|
||||
public get fullQualifiedName (): string {
|
||||
public get fullPath (): string {
|
||||
return this.__Formulario_path !== '' ? `${this.__Formulario_path}.${this.name}` : this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this formulario element is v-modeled or not.
|
||||
*/
|
||||
get hasModel (): boolean {
|
||||
return has(this.$options.propsData || {}, 'value')
|
||||
}
|
||||
|
||||
private get model (): unknown {
|
||||
const model = this.hasModel ? 'value' : 'proxy'
|
||||
return this.modelGetConverter(this[model])
|
||||
@ -112,7 +119,7 @@ export default class FormularioField extends Vue {
|
||||
|
||||
private get context (): FormularioFieldContext<unknown> {
|
||||
return Object.defineProperty({
|
||||
name: this.fullQualifiedName,
|
||||
name: this.fullPath,
|
||||
runValidation: this.runValidation.bind(this),
|
||||
violations: this.violations,
|
||||
errors: this.localErrors,
|
||||
@ -141,13 +148,6 @@ export default class FormularioField extends Vue {
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this formulario element is v-modeled or not.
|
||||
*/
|
||||
private get hasModel (): boolean {
|
||||
return has(this.$options.propsData || {}, 'value')
|
||||
}
|
||||
|
||||
@Watch('proxy')
|
||||
private onProxyChange (newValue: unknown, oldValue: unknown): void {
|
||||
if (!this.hasModel && !shallowEquals(newValue, oldValue)) {
|
||||
@ -170,7 +170,7 @@ export default class FormularioField extends Vue {
|
||||
created (): void {
|
||||
this.initProxy()
|
||||
if (typeof this.__FormularioForm_register === 'function') {
|
||||
this.__FormularioForm_register(this.fullQualifiedName, this)
|
||||
this.__FormularioForm_register(this.fullPath, this)
|
||||
}
|
||||
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
|
||||
this.runValidation()
|
||||
@ -179,7 +179,7 @@ export default class FormularioField extends Vue {
|
||||
|
||||
beforeDestroy (): void {
|
||||
if (typeof this.__FormularioForm_unregister === 'function') {
|
||||
this.__FormularioForm_unregister(this.fullQualifiedName)
|
||||
this.__FormularioForm_unregister(this.fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ export default class FormularioFieldGroup extends Vue {
|
||||
readonly isArrayItem!: boolean
|
||||
|
||||
@Provide('__Formulario_path')
|
||||
get groupPath (): string {
|
||||
get fullPath (): string {
|
||||
if (this.isArrayItem) {
|
||||
return `${this.__Formulario_path}[${this.name}]`
|
||||
}
|
||||
@ -33,7 +33,7 @@ export default class FormularioFieldGroup extends Vue {
|
||||
return this.name
|
||||
}
|
||||
|
||||
return `${this.path}.${this.name}`
|
||||
return `${this.__Formulario_path}.${this.name}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
shallowEquals,
|
||||
} from '@/utils'
|
||||
|
||||
import FormularioFormRegistry from '@/FormularioFormRegistry'
|
||||
import PathRegistry from '@/PathRegistry'
|
||||
|
||||
import FormularioField from '@/FormularioField.vue'
|
||||
|
||||
@ -45,7 +45,7 @@ export default class FormularioForm extends Vue {
|
||||
|
||||
public proxy: Record<string, unknown> = {}
|
||||
|
||||
private registry: FormularioFormRegistry = new FormularioFormRegistry(this)
|
||||
private registry: PathRegistry<FormularioField> = new PathRegistry()
|
||||
|
||||
// Local error messages are temporal, they wiped each resetValidation call
|
||||
private localFormErrors: string[] = []
|
||||
@ -85,13 +85,13 @@ export default class FormularioForm extends Vue {
|
||||
|
||||
@Watch('mergedFieldErrors', { deep: true, immediate: true })
|
||||
onMergedFieldErrorsChange (errors: Record<string, string[]>): void {
|
||||
this.registry.forEach((vm, path) => {
|
||||
vm.setErrors(errors[path] || [])
|
||||
this.registry.forEach((field, path) => {
|
||||
field.setErrors(errors[path] || [])
|
||||
})
|
||||
}
|
||||
|
||||
created (): void {
|
||||
this.initProxy()
|
||||
this.syncProxy()
|
||||
}
|
||||
|
||||
@Provide('__FormularioForm_getValue')
|
||||
@ -112,58 +112,77 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
|
||||
@Provide('__FormularioForm_emitValidation')
|
||||
onFormularioFieldValidation (payload: ValidationEventPayload): void {
|
||||
private emitValidation (payload: ValidationEventPayload): void {
|
||||
this.$emit('validation', payload)
|
||||
}
|
||||
|
||||
@Provide('__FormularioForm_register')
|
||||
private register (field: string, vm: FormularioField): void {
|
||||
this.registry.add(field, vm)
|
||||
private register (path: string, field: FormularioField): void {
|
||||
this.registry.add(path, field)
|
||||
|
||||
if (has(this.mergedFieldErrors, field)) {
|
||||
vm.setErrors(this.mergedFieldErrors[field] || [])
|
||||
const value = getNested(this.initialValues, path)
|
||||
|
||||
if (!field.hasModel && this.hasInitialValue && value !== undefined) {
|
||||
// In the case that the form is carrying an initial value and the
|
||||
// element is not, set it directly.
|
||||
// @ts-ignore
|
||||
field.context.model = value
|
||||
} else if (field.hasModel && !shallowEquals(field.proxy, value)) {
|
||||
// In this case, the field is v-modeled or has an initial value and the
|
||||
// form has no value or a different value, so use the field value
|
||||
this.setFieldValueAndEmit(path, field.proxy)
|
||||
}
|
||||
|
||||
if (has(this.mergedFieldErrors, path)) {
|
||||
field.setErrors(this.mergedFieldErrors[path] || [])
|
||||
}
|
||||
}
|
||||
|
||||
@Provide('__FormularioForm_unregister')
|
||||
private unregister (field: string): void {
|
||||
if (this.registry.has(field)) {
|
||||
this.registry.remove(field)
|
||||
private unregister (path: string): void {
|
||||
if (this.registry.has(path)) {
|
||||
this.registry.remove(path)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [path]: _, ...newProxy } = this.proxy
|
||||
this.proxy = newProxy
|
||||
}
|
||||
}
|
||||
|
||||
initProxy (): void {
|
||||
syncProxy (): void {
|
||||
if (this.hasInitialValue) {
|
||||
this.proxy = this.initialValues
|
||||
}
|
||||
}
|
||||
|
||||
setValues (values: Record<string, unknown>): void {
|
||||
const keys = Array.from(new Set([...Object.keys(values), ...Object.keys(this.proxy)]))
|
||||
setValues (state: Record<string, unknown>): void {
|
||||
const paths = Array.from(new Set([
|
||||
...Object.keys(state),
|
||||
...Object.keys(this.proxy),
|
||||
]))
|
||||
|
||||
let proxyHasChanges = false
|
||||
keys.forEach(field => {
|
||||
if (!this.registry.hasNested(field)) {
|
||||
|
||||
paths.forEach(path => {
|
||||
if (!this.registry.hasSubset(path)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.registry.getNested(field).forEach((_, fqn) => {
|
||||
const $field = this.registry.get(fqn) as FormularioField
|
||||
|
||||
const oldValue = getNested(this.proxy, fqn)
|
||||
const newValue = getNested(values, fqn)
|
||||
this.registry.getSubset(path).forEach((field, path) => {
|
||||
const oldValue = getNested(this.proxy, path)
|
||||
const newValue = getNested(state, path)
|
||||
|
||||
if (!shallowEquals(newValue, oldValue)) {
|
||||
this.setFieldValue(fqn, newValue)
|
||||
this.setFieldValue(path, newValue)
|
||||
proxyHasChanges = true
|
||||
}
|
||||
|
||||
if (!shallowEquals(newValue, $field.proxy)) {
|
||||
$field.context.model = newValue
|
||||
if (!shallowEquals(newValue, field.proxy)) {
|
||||
field.context.model = newValue
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.initProxy()
|
||||
this.syncProxy()
|
||||
|
||||
if (proxyHasChanges) {
|
||||
this.$emit('input', { ...this.proxy })
|
||||
|
@ -1,142 +0,0 @@
|
||||
import { getNested, has, shallowEquals } from '@/utils'
|
||||
|
||||
import FormularioField from '@/FormularioField.vue'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
|
||||
/**
|
||||
* Component registry with inherent depth to handle complex nesting. This is
|
||||
* important for features such as grouped fields.
|
||||
*/
|
||||
export default class FormularioFormRegistry {
|
||||
private ctx: FormularioForm
|
||||
private registry: Map<string, FormularioField>
|
||||
|
||||
/**
|
||||
* Create a new registry of components.
|
||||
* @param {FormularioForm} ctx The host vm context of the registry.
|
||||
*/
|
||||
constructor (ctx: FormularioForm) {
|
||||
this.registry = new Map()
|
||||
this.ctx = ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully register a component.
|
||||
* @param {string} field name of the field.
|
||||
* @param {FormularioForm} component the actual component instance.
|
||||
*/
|
||||
add (field: string, component: FormularioField): void {
|
||||
if (this.registry.has(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.registry.set(field, component)
|
||||
|
||||
// @ts-ignore
|
||||
const value = getNested(this.ctx.initialValues, field)
|
||||
const hasModel = has(component.$options.propsData || {}, 'value')
|
||||
|
||||
// @ts-ignore
|
||||
if (!hasModel && this.ctx.hasInitialValue && value !== undefined) {
|
||||
// In the case that the form is carrying an initial value and the
|
||||
// element is not, set it directly.
|
||||
// @ts-ignore
|
||||
component.context.model = value
|
||||
// @ts-ignore
|
||||
} else if (hasModel && !shallowEquals(component.proxy, value)) {
|
||||
// In this case, the field is v-modeled or has an initial value and the
|
||||
// form has no value or a different value, so use the field value
|
||||
// @ts-ignore
|
||||
this.ctx.setFieldValueAndEmit(field, component.proxy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the registry.
|
||||
*/
|
||||
remove (name: string): void {
|
||||
this.registry.delete(name)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [name]: value, ...newProxy } = this.ctx.proxy
|
||||
// @ts-ignore
|
||||
this.ctx.proxy = newProxy
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the registry has the given key.
|
||||
*/
|
||||
has (key: string): boolean {
|
||||
return this.registry.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the registry has elements, that equals or nested given key
|
||||
*/
|
||||
hasNested (key: string): boolean {
|
||||
for (const i of this.registry.keys()) {
|
||||
if (i === key || i.includes(key + '.')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a particular registry value.
|
||||
*/
|
||||
get (key: string): FormularioField | undefined {
|
||||
return this.registry.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry value for key or nested to given key
|
||||
*/
|
||||
getNested (key: string): Map<string, FormularioField> {
|
||||
const result = new Map()
|
||||
|
||||
for (const i of this.registry.keys()) {
|
||||
const objectKey = key + '.'
|
||||
const arrayKey = key + '['
|
||||
|
||||
if (
|
||||
i === key ||
|
||||
i.substring(0, objectKey.length) === objectKey ||
|
||||
i.substring(0, arrayKey.length) === arrayKey
|
||||
) {
|
||||
result.set(i, this.registry.get(i))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the registry.
|
||||
*/
|
||||
forEach (callback: (component: FormularioField, field: string) => void): void {
|
||||
this.registry.forEach((component, field) => {
|
||||
callback(component, field)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys of the registry.
|
||||
*/
|
||||
keys (): string[] {
|
||||
return Array.from(this.registry.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the registry.
|
||||
* @param {function} callback
|
||||
* @param accumulator
|
||||
*/
|
||||
reduce<U> (callback: Function, accumulator: U): U {
|
||||
this.registry.forEach((component, field) => {
|
||||
accumulator = callback(accumulator, component, field)
|
||||
})
|
||||
return accumulator
|
||||
}
|
||||
}
|
82
src/PathRegistry.ts
Normal file
82
src/PathRegistry.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export default class PathRegistry<T> {
|
||||
private registry: Map<string, T>
|
||||
|
||||
constructor () {
|
||||
this.registry = new Map()
|
||||
}
|
||||
|
||||
has (path: string): boolean {
|
||||
return this.registry.has(path)
|
||||
}
|
||||
|
||||
hasSubset (path: string): boolean {
|
||||
for (const itemPath of this.registry.keys()) {
|
||||
if (itemPath === path || itemPath.includes(path + '.')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
get (path: string): T | undefined {
|
||||
return this.registry.get(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns registry subset by given path - field & descendants
|
||||
*/
|
||||
getSubset (path: string): PathRegistry<T> {
|
||||
const subset: PathRegistry<T> = new PathRegistry()
|
||||
|
||||
for (const itemPath of this.registry.keys()) {
|
||||
if (
|
||||
itemPath === path ||
|
||||
itemPath.startsWith(path + '.') ||
|
||||
itemPath.startsWith(path + '[')
|
||||
) {
|
||||
subset.add(itemPath, this.registry.get(itemPath) as T)
|
||||
}
|
||||
}
|
||||
|
||||
return subset
|
||||
}
|
||||
|
||||
add (path: string, item: T): void {
|
||||
if (!this.registry.has(path)) {
|
||||
this.registry.set(path, item)
|
||||
}
|
||||
}
|
||||
|
||||
remove (path: string): void {
|
||||
this.registry.delete(path)
|
||||
}
|
||||
|
||||
paths (): IterableIterator<string> {
|
||||
return this.registry.keys()
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the registry.
|
||||
*/
|
||||
forEach (callback: (field: T, path: string) => void): void {
|
||||
this.registry.forEach((field, path) => {
|
||||
callback(field, path)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the registry.
|
||||
* @param {function} callback
|
||||
* @param accumulator
|
||||
*/
|
||||
reduce<U> (callback: (accumulator: U, item: T, path: string) => U, accumulator: U): U {
|
||||
this.registry.forEach((item, path) => {
|
||||
accumulator = callback(accumulator, item, path)
|
||||
})
|
||||
return accumulator
|
||||
}
|
||||
}
|
@ -38,63 +38,6 @@ describe('FormularioForm', () => {
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Adds subcomponents to the registry', () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { state: {} },
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioField name="sub1" />
|
||||
<FormularioField name="sub2" />
|
||||
`
|
||||
}
|
||||
})
|
||||
expect(wrapper.vm['registry'].keys()).toEqual(['sub1', 'sub2'])
|
||||
})
|
||||
|
||||
it('Removes subcomponents from the registry', async () => {
|
||||
const wrapper = mount({
|
||||
data: () => ({ active: true }),
|
||||
template: `
|
||||
<FormularioForm>
|
||||
<FormularioField v-if="active" name="sub1" />
|
||||
<FormularioField name="sub2" />
|
||||
</FormularioForm>
|
||||
`
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent(FormularioForm).vm['registry'].keys()).toEqual(['sub1', 'sub2'])
|
||||
wrapper.setData({ active: false })
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent(FormularioForm).vm['registry'].keys()).toEqual(['sub2'])
|
||||
})
|
||||
|
||||
it('Getting nested fields from registry', async () => {
|
||||
const wrapper = mount({
|
||||
data: () => ({ active: true, nested: { groups: { value: 'value' } }, groups: [{ name: 'group1' }, { name: 'group2' }] }),
|
||||
template: `
|
||||
<FormularioForm>
|
||||
<FormularioField name="sub1" />
|
||||
<FormularioField name="sub2" />
|
||||
<FormularioField name="nested.groups.value" />
|
||||
<FormularioField name="groups">
|
||||
<FormularioFieldGroup :name="'groups[' + index + ']'" v-for="(_, index) in groups" :key="index">
|
||||
<FormularioField name="name" />
|
||||
</FormularioFieldGroup>
|
||||
</FormularioField>
|
||||
</FormularioForm>
|
||||
`
|
||||
})
|
||||
await flushPromises()
|
||||
expect(Array.from(wrapper.findComponent(FormularioForm).vm.registry.getNested('sub1').keys())).toEqual(['sub1'])
|
||||
expect(Array.from(wrapper.findComponent(FormularioForm).vm.registry.getNested('groups').keys()))
|
||||
.toEqual(['groups', 'groups[0].name', 'groups[1].name'])
|
||||
|
||||
wrapper.setData({ active: true, groups: [{ name: 'group1' }] })
|
||||
await flushPromises()
|
||||
expect(Array.from(wrapper.findComponent(FormularioForm).vm.registry.getNested('groups').keys()))
|
||||
.toEqual(['groups', 'groups[0].name'])
|
||||
})
|
||||
|
||||
it('Can set a field’s initial value', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { state: { test: 'Has initial value' } },
|
||||
|
50
test/unit/PathRegistry.test.js
Normal file
50
test/unit/PathRegistry.test.js
Normal file
@ -0,0 +1,50 @@
|
||||
import PathRegistry from '@/PathRegistry'
|
||||
|
||||
describe('PathRegistry', () => {
|
||||
test ('subset structure', () => {
|
||||
const registry = new PathRegistry()
|
||||
|
||||
const paths = path => Array.from(registry.getSubset(path).paths())
|
||||
|
||||
registry.add('name', null)
|
||||
registry.add('address', [])
|
||||
registry.add('address[0]', {})
|
||||
registry.add('address[0].street', 'Baker Street')
|
||||
registry.add('address[0].building', '221b')
|
||||
registry.add('address[1]', {})
|
||||
registry.add('address[1].street', '')
|
||||
registry.add('address[1].building', '')
|
||||
|
||||
expect(paths('name')).toEqual(['name'])
|
||||
expect(paths('address')).toEqual([
|
||||
'address',
|
||||
'address[0]',
|
||||
'address[0].street',
|
||||
'address[0].building',
|
||||
'address[1]',
|
||||
'address[1].street',
|
||||
'address[1].building',
|
||||
])
|
||||
expect(paths('address[1]')).toEqual([
|
||||
'address[1]',
|
||||
'address[1].street',
|
||||
'address[1].building',
|
||||
])
|
||||
|
||||
registry.remove('address[1]')
|
||||
|
||||
expect(paths('address')).toEqual([
|
||||
'address',
|
||||
'address[0]',
|
||||
'address[0].street',
|
||||
'address[0].building',
|
||||
'address[1].street',
|
||||
'address[1].building',
|
||||
])
|
||||
|
||||
expect(paths('address[1]')).toEqual([
|
||||
'address[1].street',
|
||||
'address[1].building',
|
||||
])
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user