refactor: Deep cloning of state, deep equal checker
This commit is contained in:
parent
7c2a9e8110
commit
b37040d2d3
@ -7,10 +7,10 @@ import {
|
|||||||
ValidationRuleFn,
|
ValidationRuleFn,
|
||||||
ValidationMessageFn,
|
ValidationMessageFn,
|
||||||
ValidationMessageI18NFn,
|
ValidationMessageI18NFn,
|
||||||
ViolationsRecord,
|
Violation,
|
||||||
} from '@/validation/validator'
|
} from '@/validation/validator'
|
||||||
|
|
||||||
import { FormularioFormInterface } from '@/types'
|
import { FormularioForm } from '@/types'
|
||||||
|
|
||||||
export interface FormularioOptions {
|
export interface FormularioOptions {
|
||||||
validationRules?: Record<string, ValidationRuleFn>;
|
validationRules?: Record<string, ValidationRuleFn>;
|
||||||
@ -24,7 +24,7 @@ export default class Formulario {
|
|||||||
public validationRules: Record<string, ValidationRuleFn> = {}
|
public validationRules: Record<string, ValidationRuleFn> = {}
|
||||||
public validationMessages: Record<string, ValidationMessageI18NFn|string> = {}
|
public validationMessages: Record<string, ValidationMessageI18NFn|string> = {}
|
||||||
|
|
||||||
private readonly registry: Map<string, FormularioFormInterface>
|
private readonly registry: Map<string, FormularioForm>
|
||||||
|
|
||||||
public constructor (options?: FormularioOptions) {
|
public constructor (options?: FormularioOptions) {
|
||||||
this.registry = new Map()
|
this.registry = new Map()
|
||||||
@ -47,12 +47,12 @@ export default class Formulario {
|
|||||||
throw new Error(`[Formulario]: Formulario.extend(): should be passed an object (was ${typeof extendWith})`)
|
throw new Error(`[Formulario]: Formulario.extend(): should be passed an object (was ${typeof extendWith})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
public runValidation (id: string): Promise<ViolationsRecord> {
|
public runValidation (id: string): Promise<Record<string, Violation[]>> {
|
||||||
if (!this.registry.has(id)) {
|
if (!this.registry.has(id)) {
|
||||||
throw new Error(`[Formulario]: Formulario.runValidation(): no forms with id "${id}"`)
|
throw new Error(`[Formulario]: Formulario.runValidation(): no forms with id "${id}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = this.registry.get(id) as FormularioFormInterface
|
const form = this.registry.get(id) as FormularioForm
|
||||||
|
|
||||||
return form.runValidation()
|
return form.runValidation()
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ export default class Formulario {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = this.registry.get(id) as FormularioFormInterface
|
const form = this.registry.get(id) as FormularioForm
|
||||||
|
|
||||||
form.resetValidation()
|
form.resetValidation()
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ export default class Formulario {
|
|||||||
* Used by forms instances to add themselves into a registry
|
* Used by forms instances to add themselves into a registry
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public register (id: string, form: FormularioFormInterface): void {
|
public register (id: string, form: FormularioForm): void {
|
||||||
if (this.registry.has(id)) {
|
if (this.registry.has(id)) {
|
||||||
throw new Error(`[Formulario]: Formulario.register(): id "${id}" is already in use`)
|
throw new Error(`[Formulario]: Formulario.register(): id "${id}" is already in use`)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
Prop,
|
Prop,
|
||||||
Watch,
|
Watch,
|
||||||
} from 'vue-property-decorator'
|
} from 'vue-property-decorator'
|
||||||
import { has, shallowEquals, snakeToCamel } from './utils'
|
import { deepEquals, has, snakeToCamel } from './utils'
|
||||||
import {
|
import {
|
||||||
processConstraints,
|
processConstraints,
|
||||||
validate,
|
validate,
|
||||||
@ -87,35 +87,18 @@ export default class FormularioField extends Vue {
|
|||||||
return has(this.$options.propsData || {}, 'value')
|
return has(this.$options.propsData || {}, 'value')
|
||||||
}
|
}
|
||||||
|
|
||||||
public get model (): unknown {
|
|
||||||
return this.modelGetConverter(this.hasModel ? this.value : this.proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
public set model (value: unknown) {
|
|
||||||
value = this.modelSetConverter(value, this.proxy)
|
|
||||||
|
|
||||||
if (!shallowEquals(value, this.proxy)) {
|
|
||||||
this.proxy = value
|
|
||||||
this.$emit('input', value)
|
|
||||||
|
|
||||||
if (typeof this.__FormularioForm_set === 'function') {
|
|
||||||
this.__FormularioForm_set(this.fullPath, value)
|
|
||||||
this.__FormularioForm_emitInput()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get context (): FormularioFieldContext<unknown> {
|
private get context (): FormularioFieldContext<unknown> {
|
||||||
return Object.defineProperty({
|
return Object.defineProperty({
|
||||||
name: this.fullPath,
|
name: this.fullPath,
|
||||||
|
path: this.fullPath,
|
||||||
runValidation: this.runValidation.bind(this),
|
runValidation: this.runValidation.bind(this),
|
||||||
violations: this.violations,
|
violations: this.violations,
|
||||||
errors: this.localErrors,
|
errors: this.localErrors,
|
||||||
allErrors: [...this.localErrors, ...this.violations.map(v => v.message)],
|
allErrors: [...this.localErrors, ...this.violations.map(v => v.message)],
|
||||||
}, 'model', {
|
}, 'model', {
|
||||||
get: () => this.model,
|
get: () => this.modelGetConverter(this.proxy),
|
||||||
set: (value: unknown) => {
|
set: (value: unknown): void => {
|
||||||
this.model = value
|
this.syncProxy(this.modelSetConverter(value, this.proxy))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -137,18 +120,12 @@ export default class FormularioField extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Watch('value')
|
@Watch('value')
|
||||||
private onValueChange (newValue: unknown, oldValue: unknown): void {
|
private onValueChange (): void {
|
||||||
if (this.hasModel && !shallowEquals(newValue, oldValue)) {
|
this.syncProxy(this.value)
|
||||||
this.model = newValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('proxy')
|
@Watch('proxy')
|
||||||
private onProxyChange (newValue: unknown, oldValue: unknown): void {
|
private onProxyChange (): void {
|
||||||
if (!this.hasModel && !shallowEquals(newValue, oldValue)) {
|
|
||||||
this.model = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
|
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
|
||||||
this.runValidation()
|
this.runValidation()
|
||||||
} else {
|
} else {
|
||||||
@ -160,10 +137,6 @@ export default class FormularioField extends Vue {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public created (): void {
|
public created (): void {
|
||||||
if (!shallowEquals(this.model, this.proxy)) {
|
|
||||||
this.model = this.proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this.__FormularioForm_register === 'function') {
|
if (typeof this.__FormularioForm_register === 'function') {
|
||||||
this.__FormularioForm_register(this.fullPath, this)
|
this.__FormularioForm_register(this.fullPath, this)
|
||||||
}
|
}
|
||||||
@ -182,13 +155,22 @@ export default class FormularioField extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public runValidation (): Promise<Violation[]> {
|
private syncProxy (value: unknown): void {
|
||||||
this.validationRun = this.validate().then(violations => {
|
if (!deepEquals(value, this.proxy)) {
|
||||||
if (!shallowEquals(this.violations, violations)) {
|
this.proxy = value
|
||||||
this.emitValidation(this.fullPath, violations)
|
this.$emit('input', value)
|
||||||
|
|
||||||
|
if (typeof this.__FormularioForm_set === 'function') {
|
||||||
|
this.__FormularioForm_set(this.fullPath, value)
|
||||||
|
this.__FormularioForm_emitInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public runValidation (): Promise<Violation[]> {
|
||||||
|
this.validationRun = this.validate().then(violations => {
|
||||||
this.violations = violations
|
this.violations = violations
|
||||||
|
this.emitValidation(this.fullPath, violations)
|
||||||
|
|
||||||
return this.violations
|
return this.violations
|
||||||
})
|
})
|
||||||
@ -202,8 +184,8 @@ export default class FormularioField extends Vue {
|
|||||||
this.$formulario.getRules(this.normalizedValidationRules),
|
this.$formulario.getRules(this.normalizedValidationRules),
|
||||||
this.$formulario.getMessages(this, this.normalizedValidationMessages),
|
this.$formulario.getMessages(this, this.normalizedValidationMessages),
|
||||||
), {
|
), {
|
||||||
value: this.context.model,
|
value: this.proxy,
|
||||||
name: this.context.name,
|
name: this.fullPath,
|
||||||
formValues: this.__FormularioForm_getState(),
|
formValues: this.__FormularioForm_getState(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -22,17 +22,17 @@ export default class FormularioFieldGroup extends Vue {
|
|||||||
|
|
||||||
@Provide('__Formulario_path')
|
@Provide('__Formulario_path')
|
||||||
get fullPath (): string {
|
get fullPath (): string {
|
||||||
const name = `${this.name}`
|
const path = `${this.name}`
|
||||||
|
|
||||||
if (parseInt(name).toString() === name) {
|
if (parseInt(path).toString() === path) {
|
||||||
return `${this.__Formulario_path}[${name}]`
|
return `${this.__Formulario_path}[${path}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.__Formulario_path === '') {
|
if (this.__Formulario_path === '') {
|
||||||
return name
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${this.__Formulario_path}.${name}`
|
return `${this.__Formulario_path}.${path}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,67 +14,51 @@ import {
|
|||||||
Watch,
|
Watch,
|
||||||
} from 'vue-property-decorator'
|
} from 'vue-property-decorator'
|
||||||
import {
|
import {
|
||||||
|
id,
|
||||||
clone,
|
clone,
|
||||||
|
deepEquals,
|
||||||
get,
|
get,
|
||||||
has,
|
has,
|
||||||
merge,
|
merge,
|
||||||
set,
|
set,
|
||||||
shallowEquals,
|
|
||||||
unset,
|
unset,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
|
||||||
import PathRegistry from '@/PathRegistry'
|
import { FormularioField } from '@/types'
|
||||||
|
import { Violation } from '@/validation/validator'
|
||||||
import { FormularioFieldInterface } from '@/types'
|
|
||||||
import {
|
|
||||||
Violation,
|
|
||||||
ViolationsRecord,
|
|
||||||
} from '@/validation/validator'
|
|
||||||
|
|
||||||
type ErrorsRecord = Record<string, string[]>
|
|
||||||
|
|
||||||
type ValidationEventPayload = {
|
type ValidationEventPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
violations: Violation[];
|
violations: Violation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let counter = 0
|
const update = (state: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return unset(state, path) as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
return set(state, path, value) as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
@Component({ name: 'FormularioForm' })
|
@Component({ name: 'FormularioForm' })
|
||||||
export default class FormularioForm extends Vue {
|
export default class FormularioForm extends Vue {
|
||||||
@Model('input', { default: () => ({}) })
|
@Model('input', { default: () => ({}) })
|
||||||
public readonly state!: Record<string, unknown>
|
public readonly state!: Record<string, unknown>
|
||||||
|
|
||||||
@Prop({ default: () => `formulario-form-${++counter}` })
|
@Prop({ default: () => id('formulario-form') })
|
||||||
public readonly id!: string
|
public readonly id!: string
|
||||||
|
|
||||||
// Describes validation errors of whole form
|
// Describes validation errors of whole form
|
||||||
@Prop({ default: () => ({}) }) readonly fieldsErrors!: ErrorsRecord
|
@Prop({ default: () => ({}) }) readonly fieldsErrors!: Record<string, string[]>
|
||||||
// Only used on FormularioForm default slot
|
// Only used on FormularioForm default slot
|
||||||
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
@Prop({ default: () => ([]) }) readonly formErrors!: string[]
|
||||||
|
|
||||||
private proxy: Record<string, unknown> = {}
|
private proxy: Record<string, unknown> = {}
|
||||||
private registry: PathRegistry<FormularioFieldInterface> = new PathRegistry()
|
private registry: Map<string, FormularioField> = new Map()
|
||||||
// Local error messages are temporal, they wiped each resetValidation call
|
// Local error messages are temporal, they wiped each resetValidation call
|
||||||
private localFieldsErrors: ErrorsRecord = {}
|
private localFieldsErrors: Record<string, string[]> = {}
|
||||||
private localFormErrors: string[] = []
|
private localFormErrors: string[] = []
|
||||||
|
|
||||||
private get hasModel (): boolean {
|
|
||||||
return has(this.$options.propsData || {}, 'state')
|
|
||||||
}
|
|
||||||
|
|
||||||
private get modelIsDefined (): boolean {
|
|
||||||
return this.state && typeof this.state === 'object'
|
|
||||||
}
|
|
||||||
|
|
||||||
private get modelCopy (): Record<string, unknown> {
|
|
||||||
if (this.hasModel && typeof this.state === 'object') {
|
|
||||||
return { ...this.state } // @todo - use a deep clone to detach reference types
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private get fieldsErrorsComputed (): Record<string, string[]> {
|
private get fieldsErrorsComputed (): Record<string, string[]> {
|
||||||
return merge(this.fieldsErrors || {}, this.localFieldsErrors)
|
return merge(this.fieldsErrors || {}, this.localFieldsErrors)
|
||||||
}
|
}
|
||||||
@ -84,19 +68,21 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provide('__FormularioForm_register')
|
@Provide('__FormularioForm_register')
|
||||||
private register (path: string, field: FormularioFieldInterface): void {
|
private register (path: string, field: FormularioField): void {
|
||||||
this.registry.add(path, field)
|
if (!this.registry.has(path)) {
|
||||||
|
this.registry.set(path, field)
|
||||||
|
}
|
||||||
|
|
||||||
const value = get(this.modelCopy, path)
|
const value = get(this.proxy, path)
|
||||||
|
|
||||||
if (!field.hasModel && this.modelIsDefined) {
|
if (!field.hasModel) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
field.model = value
|
field.proxy = value
|
||||||
} else {
|
} else {
|
||||||
this.setFieldValue(path, null)
|
this.setFieldValue(path, null)
|
||||||
this.emitInput()
|
this.emitInput()
|
||||||
}
|
}
|
||||||
} else if (field.hasModel && !shallowEquals(field.proxy, value)) {
|
} else if (!deepEquals(field.proxy, value)) {
|
||||||
this.setFieldValue(path, field.proxy)
|
this.setFieldValue(path, field.proxy)
|
||||||
this.emitInput()
|
this.emitInput()
|
||||||
}
|
}
|
||||||
@ -109,10 +95,9 @@ export default class FormularioForm extends Vue {
|
|||||||
@Provide('__FormularioForm_unregister')
|
@Provide('__FormularioForm_unregister')
|
||||||
private unregister (path: string): void {
|
private unregister (path: string): void {
|
||||||
if (this.registry.has(path)) {
|
if (this.registry.has(path)) {
|
||||||
this.registry.remove(path)
|
this.registry.delete(path)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
this.proxy = unset(this.proxy, path) as Record<string, unknown>
|
||||||
const { [path]: _, ...newProxy } = this.proxy
|
this.emitInput()
|
||||||
this.proxy = newProxy
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,16 +108,12 @@ export default class FormularioForm extends Vue {
|
|||||||
|
|
||||||
@Provide('__FormularioForm_set')
|
@Provide('__FormularioForm_set')
|
||||||
private setFieldValue (path: string, value: unknown): void {
|
private setFieldValue (path: string, value: unknown): void {
|
||||||
if (value === undefined) {
|
this.proxy = update(this.proxy, path, value)
|
||||||
this.proxy = unset(this.proxy, path) as Record<string, unknown>
|
|
||||||
} else {
|
|
||||||
this.proxy = set(this.proxy, path, value) as Record<string, unknown>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provide('__FormularioForm_emitInput')
|
@Provide('__FormularioForm_emitInput')
|
||||||
private emitInput (): void {
|
private emitInput (): void {
|
||||||
this.$emit('input', { ...this.proxy })
|
this.$emit('input', clone(this.proxy))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provide('__FormularioForm_emitValidation')
|
@Provide('__FormularioForm_emitValidation')
|
||||||
@ -141,9 +122,29 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Watch('state', { deep: true })
|
@Watch('state', { deep: true })
|
||||||
private onStateChange (state: Record<string, unknown>): void {
|
private onStateChange (newState: Record<string, unknown>): void {
|
||||||
if (this.hasModel && state && typeof state === 'object') {
|
const newProxy = clone(newState)
|
||||||
this.loadState(state)
|
const oldProxy = this.proxy
|
||||||
|
|
||||||
|
let proxyHasChanges = false
|
||||||
|
|
||||||
|
this.registry.forEach((field, path) => {
|
||||||
|
const newValue = get(newState, path, null)
|
||||||
|
const oldValue = get(oldProxy, path, null)
|
||||||
|
|
||||||
|
field.proxy = newValue
|
||||||
|
|
||||||
|
if (!deepEquals(newValue, oldValue)) {
|
||||||
|
field.$emit('input', newValue)
|
||||||
|
update(newProxy, path, newValue)
|
||||||
|
proxyHasChanges = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.proxy = newProxy
|
||||||
|
|
||||||
|
if (proxyHasChanges) {
|
||||||
|
this.emitInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,18 +156,22 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public created (): void {
|
public created (): void {
|
||||||
this.syncProxy()
|
|
||||||
this.$formulario.register(this.id, this)
|
this.$formulario.register(this.id, this)
|
||||||
|
if (typeof this.state === 'object') {
|
||||||
|
this.proxy = clone(this.state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public beforeDestroy (): void {
|
public beforeDestroy (): void {
|
||||||
this.$formulario.unregister(this.id)
|
this.$formulario.unregister(this.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public runValidation (): Promise<ViolationsRecord> {
|
public runValidation (): Promise<Record<string, Violation[]>> {
|
||||||
const violations: ViolationsRecord = {}
|
const runs: Promise<void>[] = []
|
||||||
const runs = this.registry.map((field: FormularioFieldInterface, path: string) => {
|
const violations: Record<string, Violation[]> = {}
|
||||||
return field.runValidation().then(v => { violations[path] = v })
|
|
||||||
|
this.registry.forEach((field, path) => {
|
||||||
|
runs.push(field.runValidation().then(v => { violations[path] = v }))
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.all(runs).then(() => violations)
|
return Promise.all(runs).then(() => violations)
|
||||||
@ -178,7 +183,10 @@ export default class FormularioForm extends Vue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public setErrors ({ fieldsErrors, formErrors }: { fieldsErrors?: ErrorsRecord; formErrors?: string[] }): void {
|
public setErrors ({ fieldsErrors, formErrors }: {
|
||||||
|
fieldsErrors?: Record<string, string[]>;
|
||||||
|
formErrors?: string[];
|
||||||
|
}): void {
|
||||||
this.localFieldsErrors = fieldsErrors || {}
|
this.localFieldsErrors = fieldsErrors || {}
|
||||||
this.localFormErrors = formErrors || []
|
this.localFormErrors = formErrors || []
|
||||||
}
|
}
|
||||||
@ -186,14 +194,13 @@ export default class FormularioForm extends Vue {
|
|||||||
public resetValidation (): void {
|
public resetValidation (): void {
|
||||||
this.localFieldsErrors = {}
|
this.localFieldsErrors = {}
|
||||||
this.localFormErrors = []
|
this.localFormErrors = []
|
||||||
this.registry.forEach((field: FormularioFieldInterface) => {
|
this.registry.forEach((field: FormularioField) => {
|
||||||
field.resetValidation()
|
field.resetValidation()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSubmit (): Promise<void> {
|
private onSubmit (): Promise<void> {
|
||||||
return this.runValidation()
|
return this.runValidation().then(violations => {
|
||||||
.then(violations => {
|
|
||||||
const hasErrors = Object.keys(violations).some(path => violations[path].length > 0)
|
const hasErrors = Object.keys(violations).some(path => violations[path].length > 0)
|
||||||
|
|
||||||
if (!hasErrors) {
|
if (!hasErrors) {
|
||||||
@ -203,46 +210,5 @@ export default class FormularioForm extends Vue {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadState (state: Record<string, unknown>): void {
|
|
||||||
const paths = Array.from(new Set([
|
|
||||||
...Object.keys(state),
|
|
||||||
...Object.keys(this.proxy),
|
|
||||||
]))
|
|
||||||
|
|
||||||
let proxyHasChanges = false
|
|
||||||
|
|
||||||
paths.forEach(path => {
|
|
||||||
if (!this.registry.hasSubset(path)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.registry.getSubset(path).forEach((field, path) => {
|
|
||||||
const oldValue = get(this.proxy, path, null)
|
|
||||||
const newValue = get(state, path, null)
|
|
||||||
|
|
||||||
if (!shallowEquals(newValue, oldValue)) {
|
|
||||||
this.setFieldValue(path, newValue)
|
|
||||||
proxyHasChanges = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shallowEquals(newValue, field.proxy)) {
|
|
||||||
field.model = newValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.syncProxy()
|
|
||||||
|
|
||||||
if (proxyHasChanges) {
|
|
||||||
this.$emit('input', { ...this.proxy })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncProxy (): void {
|
|
||||||
if (this.modelIsDefined) {
|
|
||||||
this.proxy = this.modelCopy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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()
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach (callback: (field: T, path: string) => void): void {
|
|
||||||
this.registry.forEach((field, path) => {
|
|
||||||
callback(field, path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
map<U> (mapper: (item: T, path: string) => U): U[] {
|
|
||||||
return Array.from(this.registry.keys()).map(path => mapper(this.get(path) as T, path))
|
|
||||||
}
|
|
||||||
}
|
|
64
src/types.ts
64
src/types.ts
@ -1,13 +1,13 @@
|
|||||||
import { Violation, ViolationsRecord } from '@/validation/validator'
|
import Vue from 'vue'
|
||||||
|
import { Violation } from '@/validation/validator'
|
||||||
|
|
||||||
export interface FormularioFormInterface {
|
export interface FormularioForm extends Vue {
|
||||||
runValidation(): Promise<ViolationsRecord>;
|
runValidation(): Promise<Record<string, Violation[]>>;
|
||||||
resetValidation(): void;
|
resetValidation(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FormularioFieldInterface {
|
export interface FormularioField extends Vue {
|
||||||
hasModel: boolean;
|
hasModel: boolean;
|
||||||
model: unknown;
|
|
||||||
proxy: unknown;
|
proxy: unknown;
|
||||||
setErrors(errors: string[]): void;
|
setErrors(errors: string[]): void;
|
||||||
runValidation(): Promise<Violation[]>;
|
runValidation(): Promise<Violation[]>;
|
||||||
@ -33,10 +33,58 @@ export interface FormularioFieldModelSetConverter {
|
|||||||
|
|
||||||
export type Empty = undefined | null
|
export type Empty = undefined | null
|
||||||
|
|
||||||
export type RecordKey = string | number
|
export enum TYPE {
|
||||||
export type RecordLike<T> = T[] | Record<RecordKey, T>
|
ARRAY = 'ARRAY',
|
||||||
|
BIGINT = 'BIGINT',
|
||||||
|
BOOLEAN = 'BOOLEAN',
|
||||||
|
DATE = 'DATE',
|
||||||
|
FUNCTION = 'FUNCTION',
|
||||||
|
NUMBER = 'NUMBER',
|
||||||
|
RECORD = 'RECORD',
|
||||||
|
STRING = 'STRING',
|
||||||
|
SYMBOL = 'SYMBOL',
|
||||||
|
UNDEFINED = 'UNDEFINED',
|
||||||
|
NULL = 'NULL',
|
||||||
|
}
|
||||||
|
|
||||||
export type Scalar = boolean | number | string | symbol | Empty
|
export function typeOf (value: unknown): string {
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'bigint':
|
||||||
|
return TYPE.BIGINT
|
||||||
|
case 'boolean':
|
||||||
|
return TYPE.BOOLEAN
|
||||||
|
case 'function':
|
||||||
|
return TYPE.FUNCTION
|
||||||
|
case 'number':
|
||||||
|
return TYPE.NUMBER
|
||||||
|
case 'string':
|
||||||
|
return TYPE.STRING
|
||||||
|
case 'symbol':
|
||||||
|
return TYPE.SYMBOL
|
||||||
|
case 'undefined':
|
||||||
|
return TYPE.UNDEFINED
|
||||||
|
case 'object':
|
||||||
|
if (value === null) {
|
||||||
|
return TYPE.NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return TYPE.DATE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return TYPE.ARRAY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.constructor.name === 'Object') {
|
||||||
|
return TYPE.RECORD
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'InstanceOf<' + value.constructor.name + '>'
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
|
||||||
export function isRecordLike (value: unknown): boolean {
|
export function isRecordLike (value: unknown): boolean {
|
||||||
return typeof value === 'object' && value !== null && ['Array', 'Object'].includes(value.constructor.name)
|
return typeof value === 'object' && value !== null && ['Array', 'Object'].includes(value.constructor.name)
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import has from '@/utils/has'
|
import { isRecordLike, isScalar } from '@/types'
|
||||||
import {
|
|
||||||
RecordLike,
|
|
||||||
Scalar,
|
|
||||||
isRecordLike,
|
|
||||||
isScalar,
|
|
||||||
} from '@/types'
|
|
||||||
|
|
||||||
const cloneInstance = <T>(original: T): T => {
|
const cloneInstance = <T>(original: T): T => {
|
||||||
return Object.assign(Object.create(Object.getPrototypeOf(original)), original)
|
return Object.assign(Object.create(Object.getPrototypeOf(original)), original)
|
||||||
@ -14,27 +8,27 @@ const cloneInstance = <T>(original: T): T => {
|
|||||||
* A simple (somewhat non-comprehensive) clone function, valid for our use
|
* A simple (somewhat non-comprehensive) clone function, valid for our use
|
||||||
* case of needing to unbind reactive watchers.
|
* case of needing to unbind reactive watchers.
|
||||||
*/
|
*/
|
||||||
export default function clone (value: unknown): unknown {
|
export default function clone<T = unknown> (value: T): T {
|
||||||
if (isScalar(value)) {
|
if (isScalar(value)) {
|
||||||
return value as Scalar
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return new Date(value)
|
return new Date(value) as unknown as T
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isRecordLike(value)) {
|
if (!isRecordLike(value)) {
|
||||||
return cloneInstance(value)
|
return cloneInstance(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const source: RecordLike<unknown> = value as RecordLike<unknown>
|
if (Array.isArray(value)) {
|
||||||
const copy: RecordLike<unknown> = Array.isArray(source) ? [] : {}
|
return value.slice().map(clone) as unknown as T
|
||||||
|
|
||||||
for (const key in source) {
|
|
||||||
if (has(source, key)) {
|
|
||||||
copy[key] = clone(source[key])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy
|
const source: Record<string, unknown> = value as Record<string, unknown>
|
||||||
|
|
||||||
|
return Object.keys(source).reduce((copy, key) => ({
|
||||||
|
...copy,
|
||||||
|
[key]: clone(source[key])
|
||||||
|
}), {}) as unknown as T
|
||||||
}
|
}
|
||||||
|
82
src/utils/compare.ts
Normal file
82
src/utils/compare.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import has from './has'
|
||||||
|
import { typeOf, TYPE } from '@/types'
|
||||||
|
|
||||||
|
export interface EqualPredicate {
|
||||||
|
(a: unknown, b: unknown): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datesEquals (a: Date, b: Date): boolean {
|
||||||
|
return a.getTime() === b.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arraysEquals (
|
||||||
|
a: unknown[],
|
||||||
|
b: unknown[],
|
||||||
|
predicate: EqualPredicate
|
||||||
|
): boolean {
|
||||||
|
if (a.length !== b.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
if (!predicate(a[i], b[i])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordsEquals (
|
||||||
|
a: Record<string, unknown>,
|
||||||
|
b: Record<string, unknown>,
|
||||||
|
predicate: EqualPredicate
|
||||||
|
): boolean {
|
||||||
|
if (Object.keys(a).length !== Object.keys(b).length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop in a as object) {
|
||||||
|
if (!has(b, prop) || !predicate(a[prop], b[prop])) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function strictEquals (a: unknown, b: unknown): boolean {
|
||||||
|
return a === b
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equals (a: unknown, b: unknown, predicate: EqualPredicate): boolean {
|
||||||
|
const typeOfA = typeOf(a)
|
||||||
|
const typeOfB = typeOf(b)
|
||||||
|
|
||||||
|
return typeOfA === typeOfB && (
|
||||||
|
(typeOfA === TYPE.ARRAY && arraysEquals(
|
||||||
|
a as unknown[],
|
||||||
|
b as unknown[],
|
||||||
|
predicate
|
||||||
|
)) ||
|
||||||
|
(typeOfA === TYPE.DATE && datesEquals(a as Date, b as Date)) ||
|
||||||
|
(typeOfA === TYPE.RECORD && recordsEquals(
|
||||||
|
a as Record<string, unknown>,
|
||||||
|
b as Record<string, unknown>,
|
||||||
|
predicate
|
||||||
|
)) ||
|
||||||
|
(typeOfA.includes('InstanceOf') && equals(
|
||||||
|
Object.entries(a as object),
|
||||||
|
Object.entries(b as object),
|
||||||
|
predicate,
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deepEquals (a: unknown, b: unknown): boolean {
|
||||||
|
return a === b || equals(a, b, deepEquals)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shallowEquals (a: unknown, b: unknown): boolean {
|
||||||
|
return a === b || equals(a, b, strictEquals)
|
||||||
|
}
|
10
src/utils/id.ts
Normal file
10
src/utils/id.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const registry: Map<string, number> = new Map()
|
||||||
|
|
||||||
|
export default (prefix: string): string => {
|
||||||
|
const current = registry.get(prefix) || 0
|
||||||
|
const next = current + 1
|
||||||
|
|
||||||
|
registry.set(prefix, next)
|
||||||
|
|
||||||
|
return `${prefix}-${next}`
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
|
export { default as id } from './id'
|
||||||
export { default as clone } from './clone'
|
export { default as clone } from './clone'
|
||||||
export { default as has } from './has'
|
export { default as has } from './has'
|
||||||
export { default as merge } from './merge'
|
export { default as merge } from './merge'
|
||||||
export { get, set, unset } from './access'
|
export { get, set, unset } from './access'
|
||||||
export { default as regexForFormat } from './regexForFormat'
|
export { default as regexForFormat } from './regexForFormat'
|
||||||
export { default as shallowEquals } from './shallowEquals'
|
export { deepEquals, shallowEquals } from './compare'
|
||||||
export { default as snakeToCamel } from './snakeToCamel'
|
export { default as snakeToCamel } from './snakeToCamel'
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
export function equalsDates (a: Date, b: Date): boolean {
|
|
||||||
return a.getTime() === b.getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shallowEqualsRecords (
|
|
||||||
a: Record<string, unknown>,
|
|
||||||
b: Record<string, unknown>
|
|
||||||
): boolean {
|
|
||||||
const aKeys = Object.keys(a)
|
|
||||||
const bKeys = Object.keys(b)
|
|
||||||
|
|
||||||
if (aKeys.length !== bKeys.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aKeys.length === 0) {
|
|
||||||
return a === b
|
|
||||||
}
|
|
||||||
|
|
||||||
return aKeys.reduce((equals: boolean, key: string): boolean => {
|
|
||||||
return equals && a[key] === b[key]
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function shallowEquals (a: unknown, b: unknown): boolean {
|
|
||||||
if (a === b) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!a || !b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a instanceof Date && b instanceof Date) {
|
|
||||||
return equalsDates(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return shallowEqualsRecords(
|
|
||||||
a as Record<string, unknown>,
|
|
||||||
b as Record<string, unknown>
|
|
||||||
)
|
|
||||||
}
|
|
@ -130,7 +130,7 @@ const rules: Record<string, ValidationRuleFn> = {
|
|||||||
* Rule: Value is in an array (stack).
|
* Rule: Value is in an array (stack).
|
||||||
*/
|
*/
|
||||||
in ({ value }: ValidationContext, ...stack: any[]): boolean {
|
in ({ value }: ValidationContext, ...stack: any[]): boolean {
|
||||||
return stack.some(item => typeof item === 'object' ? shallowEquals(item, value) : item === value)
|
return stack.some(item => shallowEquals(item, value))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,7 +198,7 @@ const rules: Record<string, ValidationRuleFn> = {
|
|||||||
* Rule: Value is not in stack.
|
* Rule: Value is not in stack.
|
||||||
*/
|
*/
|
||||||
not ({ value }: ValidationContext, ...stack: any[]): boolean {
|
not ({ value }: ValidationContext, ...stack: any[]): boolean {
|
||||||
return !stack.some(item => typeof item === 'object' ? shallowEquals(item, value) : item === value)
|
return !stack.some(item => shallowEquals(item, value))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,8 +11,6 @@ export interface Violation {
|
|||||||
context: ValidationContext|null;
|
context: ValidationContext|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViolationsRecord = Record<string, Violation[]>
|
|
||||||
|
|
||||||
export interface ValidationRuleFn {
|
export interface ValidationRuleFn {
|
||||||
(context: ValidationContext, ...args: any[]): Promise<boolean>|boolean;
|
(context: ValidationContext, ...args: any[]): Promise<boolean>|boolean;
|
||||||
}
|
}
|
||||||
|
@ -283,7 +283,6 @@ describe('FormularioField', () => {
|
|||||||
const form = wrapper.findComponent(FormularioForm)
|
const form = wrapper.findComponent(FormularioForm)
|
||||||
|
|
||||||
expect(form.emitted('input')).toEqual([
|
expect(form.emitted('input')).toEqual([
|
||||||
[{}],
|
|
||||||
[{ date: new Date('2001-05-12') }],
|
[{ date: new Date('2001-05-12') }],
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
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',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
125
test/unit/utils/compare.test.js
Normal file
125
test/unit/utils/compare.test.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { deepEquals, shallowEquals } from '@/utils/compare'
|
||||||
|
|
||||||
|
class Sample {
|
||||||
|
constructor() {
|
||||||
|
this.fieldA = 'fieldA'
|
||||||
|
this.fieldB = 'fieldB'
|
||||||
|
}
|
||||||
|
|
||||||
|
doSomething () {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('compare', () => {
|
||||||
|
describe('deepEquals', () => {
|
||||||
|
test.each`
|
||||||
|
type | a
|
||||||
|
${'booleans'} | ${false}
|
||||||
|
${'numbers'} | ${123}
|
||||||
|
${'strings'} | ${'hello'}
|
||||||
|
${'symbols'} | ${Symbol(123)}
|
||||||
|
${'undefined'} | ${undefined}
|
||||||
|
${'null'} | ${null}
|
||||||
|
${'array'} | ${[1, 2, 3]}
|
||||||
|
${'pojo'} | ${{ a: 1, b: 2 }}
|
||||||
|
${'empty array'} | ${[]}
|
||||||
|
${'empty pojo'} | ${{}}
|
||||||
|
${'date'} | ${new Date()}
|
||||||
|
`('A=A check on $type', ({ a }) => {
|
||||||
|
expect(deepEquals(a, a)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each`
|
||||||
|
a | b | expected
|
||||||
|
${[]} | ${[]} | ${true}
|
||||||
|
${[1, 2, 3]} | ${[1, 2, 3]} | ${true}
|
||||||
|
${[1, 2, { a: 1 }]} | ${[1, 2, { a: 1 }]} | ${true}
|
||||||
|
${[1, 2, { a: 1 }]} | ${[1, 2, { a: 2 }]} | ${false}
|
||||||
|
${[1, 2, 3]} | ${[1, 2, 4]} | ${false}
|
||||||
|
${[1, 2, 3]} | ${[1, 2]} | ${false}
|
||||||
|
${[]} | ${[1, 2]} | ${false}
|
||||||
|
${{}} | ${{}} | ${true}
|
||||||
|
${{ a: 1, b: 2 }} | ${{ a: 1, b: 2 }} | ${true}
|
||||||
|
${{ a: 1, b: 2 }} | ${{ a: 1 }} | ${false}
|
||||||
|
${{ a: {} }} | ${{ a: {} }} | ${true}
|
||||||
|
${{ a: { b: 1 } }} | ${{ a: { b: 1 } }} | ${true}
|
||||||
|
${{ a: { b: 1 } }} | ${{ a: { b: 2 } }} | ${false}
|
||||||
|
${new Date()} | ${new Date()} | ${true}
|
||||||
|
`('A=B & B=A check: A=$a, B=$b, expected: $expected', ({ a, b, expected }) => {
|
||||||
|
expect(deepEquals(a, b)).toBe(expected)
|
||||||
|
expect(deepEquals(b, a)).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('A=B & B=A check for instances', () => {
|
||||||
|
const a = new Sample()
|
||||||
|
const b = new Sample()
|
||||||
|
|
||||||
|
expect(deepEquals(a, b)).toBe(true)
|
||||||
|
expect(deepEquals(b, a)).toBe(true)
|
||||||
|
|
||||||
|
b.fieldA += '~'
|
||||||
|
|
||||||
|
expect(deepEquals(a, b)).toBe(false)
|
||||||
|
expect(deepEquals(b, a)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('A=B & B=A check for instances with nesting', () => {
|
||||||
|
const a = new Sample()
|
||||||
|
const b = new Sample()
|
||||||
|
|
||||||
|
a.fieldA = new Sample()
|
||||||
|
b.fieldA = new Sample()
|
||||||
|
|
||||||
|
expect(deepEquals(a, b)).toBe(true)
|
||||||
|
expect(deepEquals(b, a)).toBe(true)
|
||||||
|
|
||||||
|
b.fieldA.fieldA += '~'
|
||||||
|
|
||||||
|
expect(deepEquals(a, b)).toBe(false)
|
||||||
|
expect(deepEquals(b, a)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shallowEquals', () => {
|
||||||
|
test.each`
|
||||||
|
type | a
|
||||||
|
${'booleans'} | ${false}
|
||||||
|
${'numbers'} | ${123}
|
||||||
|
${'strings'} | ${'hello'}
|
||||||
|
${'symbols'} | ${Symbol(123)}
|
||||||
|
${'undefined'} | ${undefined}
|
||||||
|
${'null'} | ${null}
|
||||||
|
${'array'} | ${[1, 2, 3]}
|
||||||
|
${'pojo'} | ${{ a: 1, b: 2 }}
|
||||||
|
${'empty array'} | ${[]}
|
||||||
|
${'empty pojo'} | ${{}}
|
||||||
|
${'date'} | ${new Date()}
|
||||||
|
`('A=A check on $type', ({ a }) => {
|
||||||
|
expect(shallowEquals(a, a)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test.each`
|
||||||
|
a | b | expected
|
||||||
|
${[]} | ${[]} | ${true}
|
||||||
|
${[1, 2, 3]} | ${[1, 2, 3]} | ${true}
|
||||||
|
${[1, 2, 3]} | ${[1, 2, 4]} | ${false}
|
||||||
|
${[1, 2, 3]} | ${[1, 2]} | ${false}
|
||||||
|
${[]} | ${[1, 2]} | ${false}
|
||||||
|
${{}} | ${{}} | ${true}
|
||||||
|
${{ a: 1, b: 2 }} | ${{ a: 1, b: 2 }} | ${true}
|
||||||
|
${{ a: 1, b: 2 }} | ${{ a: 1 }} | ${false}
|
||||||
|
${{ a: {} }} | ${{ a: {} }} | ${false}
|
||||||
|
${new Date()} | ${new Date()} | ${true}
|
||||||
|
`('A=B & B=A check: A=$a, B=$b, expected: $expected', ({ a, b, expected }) => {
|
||||||
|
expect(shallowEquals(a, b)).toBe(expected)
|
||||||
|
expect(shallowEquals(b, a)).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('A=B & B=A check for instances', () => {
|
||||||
|
const a = new Sample()
|
||||||
|
const b = new Sample()
|
||||||
|
|
||||||
|
expect(shallowEquals(a, b)).toBe(false)
|
||||||
|
expect(shallowEquals(b, a)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user