refactor: moved isScalar to new file, improved typehinting
This commit is contained in:
parent
013931fbfc
commit
e6271bb069
@ -52,20 +52,20 @@ type Empty = null | undefined
|
||||
|
||||
@Component({ name: 'FormularioField', inheritAttrs: false })
|
||||
export default class FormularioField extends Vue {
|
||||
@Inject({ default: undefined }) formularioSetter!: Function|undefined
|
||||
@Inject({ default: () => (): void => {} }) onFormularioFieldValidation!: Function
|
||||
@Inject({ default: undefined }) formularioRegister!: Function|undefined
|
||||
@Inject({ default: undefined }) formularioDeregister!: Function|undefined
|
||||
@Inject({ default: () => (): Record<string, any> => ({}) }) getFormValues!: Function
|
||||
@Inject({ default: undefined }) addErrorObserver!: Function|undefined
|
||||
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined
|
||||
@Inject({ default: undefined }) __FormularioForm_set!: Function|undefined
|
||||
@Inject({ default: () => (): void => {} }) __FormularioForm_emitValidation!: Function
|
||||
@Inject({ default: undefined }) __FormularioForm_register!: Function|undefined
|
||||
@Inject({ default: undefined }) __FormularioForm_unregister!: Function|undefined
|
||||
@Inject({ default: () => (): Record<string, unknown> => ({}) }) __FormularioForm_getValue!: () => Record<string, unknown>
|
||||
@Inject({ default: undefined }) __FormularioForm_addErrorObserver!: Function|undefined
|
||||
@Inject({ default: undefined }) __FormularioForm_removeErrorObserver!: Function|undefined
|
||||
@Inject({ default: '' }) path!: string
|
||||
|
||||
@Model('input', { default: '' }) value!: any
|
||||
@Model('input', { default: '' }) value!: unknown
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
validator: (name: any): boolean => typeof name === 'string' && name.length > 0,
|
||||
validator: (name: unknown): boolean => typeof name === 'string' && name.length > 0,
|
||||
}) name!: string
|
||||
|
||||
@Prop({ default: '' }) validation!: string|any[]
|
||||
@ -82,22 +82,24 @@ export default class FormularioField extends Vue {
|
||||
@Prop({ default: () => <U, T>(value: U|Empty): U|T|Empty => value }) modelGetConverter!: ModelGetConverter
|
||||
@Prop({ default: () => <T, U>(value: U|T): U|T => value }) modelSetConverter!: ModelSetConverter
|
||||
|
||||
public proxy: any = this.getInitialValue()
|
||||
public proxy: unknown = this.getInitialValue()
|
||||
|
||||
private localErrors: string[] = []
|
||||
private violations: Violation[] = []
|
||||
private validationRun: Promise<any> = Promise.resolve()
|
||||
|
||||
get fullQualifiedName (): string {
|
||||
private violations: Violation[] = []
|
||||
|
||||
private validationRun: Promise<Violation[]> = Promise.resolve([])
|
||||
|
||||
private get fullQualifiedName (): string {
|
||||
return this.path !== '' ? `${this.path}.${this.name}` : this.name
|
||||
}
|
||||
|
||||
get model (): any {
|
||||
private get model (): unknown {
|
||||
const model = this.hasModel ? 'value' : 'proxy'
|
||||
return this.modelGetConverter(this[model])
|
||||
}
|
||||
|
||||
set model (value: any) {
|
||||
private set model (value: unknown) {
|
||||
value = this.modelSetConverter(value, this.proxy)
|
||||
|
||||
if (!shallowEqualObjects(value, this.proxy)) {
|
||||
@ -106,12 +108,12 @@ export default class FormularioField extends Vue {
|
||||
|
||||
this.$emit('input', value)
|
||||
|
||||
if (typeof this.formularioSetter === 'function') {
|
||||
this.formularioSetter(this.context.name, value)
|
||||
if (typeof this.__FormularioForm_set === 'function') {
|
||||
this.__FormularioForm_set(this.context.name, value)
|
||||
}
|
||||
}
|
||||
|
||||
get context (): Context<any> {
|
||||
private get context (): Context<unknown> {
|
||||
return Object.defineProperty({
|
||||
name: this.fullQualifiedName,
|
||||
runValidation: this.runValidation.bind(this),
|
||||
@ -120,13 +122,13 @@ export default class FormularioField extends Vue {
|
||||
allErrors: [...this.localErrors, ...this.violations.map(v => v.message)],
|
||||
}, 'model', {
|
||||
get: () => this.model,
|
||||
set: (value: any) => {
|
||||
set: (value: unknown) => {
|
||||
this.model = value
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
get normalizedValidationRules (): Record<string, ValidationRuleFn> {
|
||||
private get normalizedValidationRules (): Record<string, ValidationRuleFn> {
|
||||
const rules: Record<string, ValidationRuleFn> = {}
|
||||
Object.keys(this.validationRules).forEach(key => {
|
||||
rules[snakeToCamel(key)] = this.validationRules[key]
|
||||
@ -134,7 +136,7 @@ export default class FormularioField extends Vue {
|
||||
return rules
|
||||
}
|
||||
|
||||
get normalizedValidationMessages (): Record<string, ValidationMessageI18NFn|string> {
|
||||
private get normalizedValidationMessages (): Record<string, ValidationMessageI18NFn|string> {
|
||||
const messages: Record<string, ValidationMessageI18NFn|string> = {}
|
||||
Object.keys(this.validationMessages).forEach(key => {
|
||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||
@ -145,12 +147,12 @@ export default class FormularioField extends Vue {
|
||||
/**
|
||||
* Determines if this formulario element is v-modeled or not.
|
||||
*/
|
||||
get hasModel (): boolean {
|
||||
private get hasModel (): boolean {
|
||||
return has(this.$options.propsData || {}, 'value')
|
||||
}
|
||||
|
||||
@Watch('proxy')
|
||||
onProxyChanged (newValue: any, oldValue: any): void {
|
||||
private onProxyChange (newValue: unknown, oldValue: unknown): void {
|
||||
if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
@ -162,7 +164,7 @@ export default class FormularioField extends Vue {
|
||||
}
|
||||
|
||||
@Watch('value')
|
||||
onValueChanged (newValue: any, oldValue: any): void {
|
||||
private onValueChange (newValue: unknown, oldValue: unknown): void {
|
||||
if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
@ -170,32 +172,31 @@ export default class FormularioField extends Vue {
|
||||
|
||||
created (): void {
|
||||
this.initProxy()
|
||||
if (typeof this.formularioRegister === 'function') {
|
||||
this.formularioRegister(this.fullQualifiedName, this)
|
||||
if (typeof this.__FormularioForm_register === 'function') {
|
||||
this.__FormularioForm_register(this.fullQualifiedName, this)
|
||||
}
|
||||
if (typeof this.addErrorObserver === 'function' && !this.errorsDisabled) {
|
||||
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.fullQualifiedName })
|
||||
if (typeof this.__FormularioForm_addErrorObserver === 'function' && !this.errorsDisabled) {
|
||||
this.__FormularioForm_addErrorObserver({ callback: this.setErrors, type: 'field', field: this.fullQualifiedName })
|
||||
}
|
||||
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
|
||||
this.runValidation()
|
||||
}
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
beforeDestroy (): void {
|
||||
if (!this.errorsDisabled && typeof this.removeErrorObserver === 'function') {
|
||||
this.removeErrorObserver(this.setErrors)
|
||||
if (!this.errorsDisabled && typeof this.__FormularioForm_removeErrorObserver === 'function') {
|
||||
this.__FormularioForm_removeErrorObserver(this.setErrors)
|
||||
}
|
||||
if (typeof this.formularioDeregister === 'function') {
|
||||
this.formularioDeregister(this.fullQualifiedName)
|
||||
if (typeof this.__FormularioForm_unregister === 'function') {
|
||||
this.__FormularioForm_unregister(this.fullQualifiedName)
|
||||
}
|
||||
}
|
||||
|
||||
getInitialValue (): any {
|
||||
private getInitialValue (): unknown {
|
||||
return has(this.$options.propsData || {}, 'value') ? this.value : ''
|
||||
}
|
||||
|
||||
initProxy (): void {
|
||||
private initProxy (): void {
|
||||
// This should only be run immediately on created and ensures that the
|
||||
// proxy and the model are both the same before any additional registration.
|
||||
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
||||
@ -213,8 +214,8 @@ export default class FormularioField extends Vue {
|
||||
violations: this.violations,
|
||||
}
|
||||
this.$emit('validation', payload)
|
||||
if (typeof this.onFormularioFieldValidation === 'function') {
|
||||
this.onFormularioFieldValidation(payload)
|
||||
if (typeof this.__FormularioForm_emitValidation === 'function') {
|
||||
this.__FormularioForm_emitValidation(payload)
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,7 +232,7 @@ export default class FormularioField extends Vue {
|
||||
), {
|
||||
value: this.context.model,
|
||||
name: this.context.name,
|
||||
formValues: this.getFormValues(),
|
||||
formValues: this.__FormularioForm_getValue(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,24 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { Component, Model, Prop, Provide, Watch } from 'vue-property-decorator'
|
||||
import { clone, getNested, has, merge, setNested, shallowEqualObjects } from '@/utils'
|
||||
import {
|
||||
Component,
|
||||
Model,
|
||||
Prop,
|
||||
Provide,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import {
|
||||
clone,
|
||||
getNested,
|
||||
has,
|
||||
merge,
|
||||
setNested,
|
||||
shallowEqualObjects,
|
||||
} from '@/utils'
|
||||
|
||||
import Registry from '@/form/registry'
|
||||
|
||||
import FormularioField from '@/FormularioField.vue'
|
||||
|
||||
import {
|
||||
@ -19,20 +34,25 @@ import {
|
||||
|
||||
import { Violation } from '@/validation/validator'
|
||||
|
||||
type ValidationEventPayload = {
|
||||
name: string;
|
||||
violations: Violation[];
|
||||
}
|
||||
|
||||
@Component({ name: 'FormularioForm' })
|
||||
export default class FormularioForm extends Vue {
|
||||
@Model('input', { default: () => ({}) })
|
||||
public readonly formularioValue!: Record<string, any>
|
||||
public readonly formularioValue!: Record<string, unknown>
|
||||
|
||||
// Errors record, describing state validation errors of whole form
|
||||
@Prop({ default: () => ({}) }) readonly errors!: Record<string, any>
|
||||
@Prop({ default: () => ({}) }) readonly errors!: Record<string, string[]>
|
||||
// Form errors only used on FormularioForm default slot
|
||||
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
||||
|
||||
@Provide()
|
||||
public path = ''
|
||||
|
||||
public proxy: Record<string, any> = {}
|
||||
public proxy: Record<string, unknown> = {}
|
||||
|
||||
private registry: Registry = new Registry(this)
|
||||
|
||||
@ -41,7 +61,7 @@ export default class FormularioForm extends Vue {
|
||||
private localFormErrors: string[] = []
|
||||
private localFieldErrors: Record<string, string[]> = {}
|
||||
|
||||
get initialValues (): Record<string, any> {
|
||||
get initialValues (): Record<string, unknown> {
|
||||
if (this.hasModel && typeof this.formularioValue === 'object') {
|
||||
// If there is a v-model on the form/group, use those values as first priority
|
||||
return { ...this.formularioValue } // @todo - use a deep clone to detach reference types
|
||||
@ -67,7 +87,7 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
|
||||
@Watch('formularioValue', { deep: true })
|
||||
onFormularioValueChanged (values: Record<string, any>): void {
|
||||
onFormularioValueChanged (values: Record<string, unknown>): void {
|
||||
if (this.hasModel && values && typeof values === 'object') {
|
||||
this.setValues(values)
|
||||
}
|
||||
@ -80,7 +100,7 @@ export default class FormularioForm extends Vue {
|
||||
|
||||
@Watch('mergedFieldErrors', { deep: true, immediate: true })
|
||||
onMergedFieldErrorsChanged (errors: Record<string, string[]>): void {
|
||||
this.errorObserverRegistry.filter(o => o.type === 'input').observe(errors)
|
||||
this.errorObserverRegistry.filter(o => o.type === 'field').observe(errors)
|
||||
}
|
||||
|
||||
created (): void {
|
||||
@ -88,7 +108,7 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
|
||||
@Provide()
|
||||
getFormValues (): Record<string, any> {
|
||||
getFormValues (): Record<string, unknown> {
|
||||
return this.proxy
|
||||
}
|
||||
|
||||
@ -104,12 +124,12 @@ export default class FormularioForm extends Vue {
|
||||
})
|
||||
}
|
||||
|
||||
@Provide()
|
||||
onFormularioFieldValidation (payload: { name: string; violations: Violation[]}): void {
|
||||
@Provide('__FormularioForm_emitValidation')
|
||||
onFormularioFieldValidation (payload: ValidationEventPayload): void {
|
||||
this.$emit('validation', payload)
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Provide('__FormularioForm_addErrorObserver')
|
||||
addErrorObserver (observer: ErrorObserver): void {
|
||||
this.errorObserverRegistry.add(observer)
|
||||
if (observer.type === 'form') {
|
||||
@ -119,18 +139,18 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Provide('__FormularioForm_removeErrorObserver')
|
||||
removeErrorObserver (observer: ErrorHandler): void {
|
||||
this.errorObserverRegistry.remove(observer)
|
||||
}
|
||||
|
||||
@Provide('formularioRegister')
|
||||
register (field: string, component: FormularioField): void {
|
||||
@Provide('__FormularioForm_register')
|
||||
private register (field: string, component: FormularioField): void {
|
||||
this.registry.add(field, component)
|
||||
}
|
||||
|
||||
@Provide('formularioDeregister')
|
||||
deregister (field: string): void {
|
||||
@Provide('__FormularioForm_unregister')
|
||||
private unregister (field: string): void {
|
||||
this.registry.remove(field)
|
||||
}
|
||||
|
||||
@ -140,7 +160,7 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
setValues (values: Record<string, any>): void {
|
||||
setValues (values: Record<string, unknown>): void {
|
||||
const keys = Array.from(new Set([...Object.keys(values), ...Object.keys(this.proxy)]))
|
||||
let proxyHasChanges = false
|
||||
keys.forEach(field => {
|
||||
@ -148,18 +168,19 @@ export default class FormularioForm extends Vue {
|
||||
return
|
||||
}
|
||||
|
||||
this.registry.getNested(field).forEach((registryField, registryKey) => {
|
||||
const $input = this.registry.get(registryKey) as FormularioField
|
||||
const oldValue = getNested(this.proxy, registryKey)
|
||||
const newValue = getNested(values, registryKey)
|
||||
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)
|
||||
|
||||
if (!shallowEqualObjects(newValue, oldValue)) {
|
||||
this.setFieldValue(registryKey, newValue)
|
||||
this.setFieldValue(fqn, newValue)
|
||||
proxyHasChanges = true
|
||||
}
|
||||
|
||||
if (!shallowEqualObjects(newValue, $input.proxy)) {
|
||||
$input.context.model = newValue
|
||||
if (!shallowEqualObjects(newValue, $field.proxy)) {
|
||||
$field.context.model = newValue
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -171,7 +192,7 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
setFieldValue (field: string, value: any): void {
|
||||
setFieldValue (field: string, value: unknown): void {
|
||||
if (value === undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [field]: value, ...proxy } = this.proxy
|
||||
@ -181,8 +202,8 @@ export default class FormularioForm extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Provide('formularioSetter')
|
||||
setFieldValueAndEmit (field: string, value: any): void {
|
||||
@Provide('__FormularioForm_set')
|
||||
setFieldValueAndEmit (field: string, value: unknown): void {
|
||||
this.setFieldValue(field, value)
|
||||
this.$emit('input', { ...this.proxy })
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
* If given parameter is not string, object ot array, result will be an empty array.
|
||||
* @param {*} item
|
||||
*/
|
||||
export default function arrayify (item: any): any[] {
|
||||
export default function arrayify (item: unknown): unknown[] {
|
||||
if (!item) {
|
||||
return []
|
||||
}
|
||||
@ -14,7 +14,7 @@ export default function arrayify (item: any): any[] {
|
||||
return item
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.values(item)
|
||||
return Object.values(item as Record<string, unknown>)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
@ -1,25 +1,32 @@
|
||||
import isScalar from '@/utils/isScalar'
|
||||
import has from '@/utils/has'
|
||||
import { RecordLike, Scalar, isScalar } from '@/utils/types'
|
||||
|
||||
type Cloneable = Scalar|Date|RecordLike<unknown>
|
||||
|
||||
/**
|
||||
* A simple (somewhat non-comprehensive) clone function, valid for our use
|
||||
* case of needing to unbind reactive watchers.
|
||||
*/
|
||||
export default function clone (value: any): any {
|
||||
if (typeof value !== 'object') {
|
||||
return value
|
||||
export default function clone (value: Cloneable): Cloneable {
|
||||
if (isScalar(value)) {
|
||||
return value as Scalar
|
||||
}
|
||||
|
||||
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
|
||||
if (value instanceof Date) {
|
||||
return new Date(value)
|
||||
}
|
||||
|
||||
for (const key in value) {
|
||||
if (has(value, key)) {
|
||||
if (isScalar(value[key])) {
|
||||
copy[key] = value[key]
|
||||
} else if (value instanceof Date) {
|
||||
copy[key] = new Date(copy[key])
|
||||
const source: RecordLike<unknown> = value as RecordLike<unknown>
|
||||
const copy: RecordLike<unknown> = Array.isArray(source) ? [] : {}
|
||||
|
||||
for (const key in source) {
|
||||
if (has(source, key)) {
|
||||
if (isScalar(source[key])) {
|
||||
copy[key] = source[key]
|
||||
} else if (source[key] instanceof Date) {
|
||||
copy[key] = new Date(source[key] as Date)
|
||||
} else {
|
||||
copy[key] = clone(value[key])
|
||||
copy[key] = clone(source[key] as Cloneable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
export { default as arrayify } from './arrayify'
|
||||
export { default as clone } from './clone'
|
||||
export { default as has } from './has'
|
||||
export { default as isScalar } from './isScalar'
|
||||
export { isScalar } from './types'
|
||||
export { default as merge } from './merge'
|
||||
export { default as regexForFormat } from './regexForFormat'
|
||||
export { default as shallowEqualObjects } from './shallowEqualObjects'
|
||||
|
@ -1,12 +0,0 @@
|
||||
export default function isScalar (data: any): boolean {
|
||||
switch (typeof data) {
|
||||
case 'symbol':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
return data === null
|
||||
}
|
||||
}
|
17
src/utils/types.ts
Normal file
17
src/utils/types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export type RecordKey = string | number
|
||||
export type RecordLike<T> = T[] | Record<RecordKey, T>
|
||||
|
||||
export type Scalar = boolean | number | string | symbol | undefined | null
|
||||
|
||||
export function isScalar (value: unknown): boolean {
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'symbol':
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
return value === null
|
||||
}
|
||||
}
|
@ -6,10 +6,12 @@ export interface ErrorHandler {
|
||||
|
||||
export interface ErrorObserver {
|
||||
callback: ErrorHandler;
|
||||
type: 'form' | 'field';
|
||||
type: ErrorObserverType;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export type ErrorObserverType = 'form' | 'field'
|
||||
|
||||
export interface ErrorObserverPredicate {
|
||||
(value: ErrorObserver, index: number, array: ErrorObserver[]): unknown;
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
import isScalar from '@/utils/isScalar'
|
||||
|
||||
describe('isScalar', () => {
|
||||
it('Passes on strings', () => expect(isScalar('hello')).toBe(true))
|
||||
|
||||
it('Passes on numbers', () => expect(isScalar(123)).toBe(true))
|
||||
|
||||
it('Passes on booleans', () => expect(isScalar(false)).toBe(true))
|
||||
|
||||
it('Passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
|
||||
|
||||
it('Passes on null', () => expect(isScalar(null)).toBe(true))
|
||||
|
||||
it('Passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||
|
||||
it('Fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||
})
|
14
test/unit/utils/types.test.js
Normal file
14
test/unit/utils/types.test.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { isScalar } from '@/utils/types'
|
||||
|
||||
describe('isScalar', () => {
|
||||
const expectIsScalar = value => expect(isScalar(value)).toBe(true)
|
||||
|
||||
test('passes on booleans', () => expectIsScalar(false))
|
||||
test('passes on numbers', () => expectIsScalar(123))
|
||||
test('passes on strings', () => expectIsScalar('hello'))
|
||||
test('passes on symbols', () => expectIsScalar(Symbol(123)))
|
||||
test('passes on undefined', () => expectIsScalar(undefined))
|
||||
test('passes on null', () => expectIsScalar(null))
|
||||
|
||||
test('fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||
})
|
Loading…
Reference in New Issue
Block a user