1
0
mirror of synced 2024-11-24 06:16:25 +03:00

feat: Components conststructors are exposed as external API, added TypeScript declarations

This commit is contained in:
Kirill Zaytsev 2023-12-28 12:37:43 +04:00 committed by Zaytsev Kirill
parent 6224b40e02
commit dfc6557bc6
23 changed files with 1565 additions and 822 deletions

View File

@ -1,30 +1,27 @@
module.exports = { module.exports = {
root: true, root: true,
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'standard',
'@vue/standard',
'@vue/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:vue/recommended',
],
env: { env: {
browser: true, browser: true,
}, },
parserOptions: {
parser: '@typescript-eslint/parser',
},
plugins: [
'@typescript-eslint',
'vue',
],
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/recommended',
],
rules: { rules: {
'@typescript-eslint/camelcase': ['error', {
allow: ['^__Formulario'],
}],
'@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off', // @TODO '@typescript-eslint/no-explicit-any': 'off', // @TODO
'@typescript-eslint/no-unused-vars': ['error'], // @TODO '@typescript-eslint/no-unused-vars': ['error'], // @TODO

View File

@ -9,7 +9,6 @@ export default {
input: 'src/index.ts', input: 'src/index.ts',
output: [{ output: [{
name: 'Formulario', name: 'Formulario',
exports: 'default',
globals: { globals: {
'is-plain-object': 'isPlainObject', 'is-plain-object': 'isPlainObject',
'is-url': 'isUrl', 'is-url': 'isUrl',

View File

@ -11,7 +11,6 @@ export default {
input: 'src/index.ts', input: 'src/index.ts',
output: { output: {
name: 'VueFormulario', name: 'VueFormulario',
exports: 'default',
format: 'iife', format: 'iife',
globals: { globals: {
'is-plain-object': 'isPlainObject', 'is-plain-object': 'isPlainObject',

94
index.d.ts vendored Normal file
View File

@ -0,0 +1,94 @@
// noinspection JSUnusedGlobalSymbols
import type { Vue } from 'vue/types/vue'
import type { PluginObject } from 'vue/types/plugin'
import type { DefineComponent } from './types/vue'
import type { FormularioFormConstructor } from './types/form'
import type {
ModelGetConverter,
ModelSetConverter,
ValidationBehaviour,
UnregisterBehaviour,
} from './types/field'
import type {
ValidationMessageFn,
ValidationMessageI18NFn,
ValidationRuleFn,
Violation,
} from './types/validation'
import type { Options } from './types/plugin'
declare const FormularioForm: FormularioFormConstructor
export { FormularioForm }
declare const FormularioField: DefineComponent<{
name: string;
value?: unknown;
validation?: string|any[];
/** Defaults to 'demand' */
validationBehavior?: ValidationBehaviour;
validationRules?: Record<string, ValidationRuleFn>;
validationMessages?: Record<string, ValidationMessageI18NFn|string>;
errorsDisabled?: boolean;
modelGetConverter?: ModelGetConverter;
modelSetConverter?: ModelSetConverter;
/** Defaults to 'none' */
unregisterBehavior?: UnregisterBehaviour;
tag?: string;
}, {
runValidation(): Promise<Violation[]>;
hasValidationErrors (): Promise<boolean>;
/** @internal */
resetValidation(): void;
}>
export { FormularioField }
declare class Formulario {
public validationRules: Record<string, ValidationRuleFn>;
public validationMessages: Record<string, ValidationMessageI18NFn|string>;
constructor (options?: Options);
/** Given a set of options, apply them to the pre-existing options. */
public extend (extendWith: Options): Formulario;
public runValidation (id: string): Promise<Record<string, Violation[]>>;
public resetValidation (id: string): void;
/** Used by forms instances to add themselves into a registry */
public register (id: string, form: InstanceType<FormularioFormConstructor>): void;
/** Used by forms instances to remove themselves from a registry */
public unregister (id: string): void;
/** Get validation rules by merging any passed in with global rules. */
public getRules (extendWith: Record<string, ValidationRuleFn> = {}): Record<string, ValidationRuleFn>;
/** Get validation messages by merging any passed in with global messages. */
public getMessages (
vm: Vue,
extendWith: Record<string, ValidationMessageI18NFn|string>
): Record<string, ValidationMessageFn>;
}
export { Formulario }
declare module 'vue/types/vue' {
interface Vue {
readonly $formulario: Formulario;
}
}
declare const VueFormulario: PluginObject<Options> & {
Formulario: Formulario,
}
export default VueFormulario

View File

@ -24,7 +24,7 @@
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js", "build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js",
"build:size": "gzip -c dist/formulario.esm.js | wc -c", "build:size": "gzip -c dist/formulario.esm.js | wc -c",
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js", "build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
"lint": "vue-cli-service lint", "lint": "eslint --ext .js,.mjs,.ts,.vue",
"release": "standard-version", "release": "standard-version",
"release:minor": "standard-version --release-as minor", "release:minor": "standard-version --release-as minor",
"release:patch": "standard-version --release-as patch", "release:patch": "standard-version --release-as patch",
@ -52,28 +52,24 @@
"@storybook/vue": "^6.0.26", "@storybook/vue": "^6.0.26",
"@types/is-url": "^1.2.28", "@types/is-url": "^1.2.28",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.14",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^2.26.0", "@typescript-eslint/parser": "^6.16.0",
"@vue/cli-plugin-babel": "^4.3.1", "@vue/cli-plugin-babel": "^4.3.1",
"@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-plugin-typescript": "^4.5.7", "@vue/cli-plugin-typescript": "^4.5.7",
"@vue/cli-service": "^4.5.4", "@vue/cli-service": "^4.5.4",
"@vue/component-compiler-utils": "^3.1.2", "@vue/component-compiler-utils": "^3.1.2",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^1.0.2", "@vue/test-utils": "^1.0.2",
"autoprefixer": "^9.7.6", "autoprefixer": "^9.7.6",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^25.5.1", "babel-jest": "^25.5.1",
"bootstrap-scss": "^4.5.2", "bootstrap-scss": "^4.5.2",
"eslint": "^5.16.0", "eslint": "^8.56.0",
"eslint-config-standard": "^12.0.0", "eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.20.1", "eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-node": "^8.0.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-promise": "^4.1.1", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^5.2.3", "eslint-plugin-vue": "^9.19.2",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"jest": "^26.5.2", "jest": "^26.5.2",
"jest-vue-preprocessor": "^1.7.1", "jest-vue-preprocessor": "^1.7.1",
@ -88,7 +84,7 @@
"sass-loader": "^10.0.3", "sass-loader": "^10.0.3",
"standard-version": "^9.3.0", "standard-version": "^9.3.0",
"ts-jest": "^26.4.1", "ts-jest": "^26.4.1",
"typescript": "~3.9.3", "typescript": "^4.9.5",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-cli-plugin-storybook": "^1.3.0", "vue-cli-plugin-storybook": "^1.3.0",
"vue-jest": "^3.0.5", "vue-jest": "^3.0.5",

View File

@ -1,33 +1,28 @@
import merge from '@/utils/merge' import type { FormularioFormConstructor } from '../types/form'
import validationRules from '@/validation/rules'
import validationMessages from '@/validation/messages'
import { import type {
ValidationContext, ValidationContext,
ValidationRuleFn, ValidationRuleFn,
ValidationMessageFn, ValidationMessageFn,
ValidationMessageI18NFn, ValidationMessageI18NFn,
Violation, Violation,
} from '@/validation/validator' } from '../types/validation'
import { FormularioForm } from '@/types' import type { Options } from '../types/plugin'
export interface FormularioOptions { import merge from '@/utils/merge'
validationRules?: Record<string, ValidationRuleFn>;
validationMessages?: Record<string, ValidationMessageI18NFn|string>; import validationRules from '@/validation/rules'
} import validationMessages from '@/validation/messages'
/**
* The base formulario library.
*/
export default class Formulario { 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, FormularioForm> private readonly _registry: Map<string, InstanceType<FormularioFormConstructor>>
public constructor (options?: FormularioOptions) { public constructor (options?: Options) {
this.registry = new Map() this._registry = new Map()
this.validationRules = validationRules this.validationRules = validationRules
this.validationMessages = validationMessages this.validationMessages = validationMessages
@ -38,7 +33,7 @@ export default class Formulario {
/** /**
* Given a set of options, apply them to the pre-existing options. * Given a set of options, apply them to the pre-existing options.
*/ */
public extend (extendWith: FormularioOptions): Formulario { public extend (extendWith: Options): Formulario {
if (typeof extendWith === 'object') { if (typeof extendWith === 'object') {
this.validationRules = merge(this.validationRules, extendWith.validationRules || {}) this.validationRules = merge(this.validationRules, extendWith.validationRules || {})
this.validationMessages = merge(this.validationMessages, extendWith.validationMessages || {}) this.validationMessages = merge(this.validationMessages, extendWith.validationMessages || {})
@ -48,21 +43,21 @@ export default class Formulario {
} }
public runValidation (id: string): Promise<Record<string, Violation[]>> { 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 FormularioForm const form = this._registry.get(id) as InstanceType<FormularioFormConstructor>
return form.runValidation() return form.runValidation()
} }
public resetValidation (id: string): void { public resetValidation (id: string): void {
if (!this.registry.has(id)) { if (!this._registry.has(id)) {
return return
} }
const form = this.registry.get(id) as FormularioForm const form = this._registry.get(id) as InstanceType<FormularioFormConstructor>
form.resetValidation() form.resetValidation()
} }
@ -71,12 +66,12 @@ 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: FormularioForm): void { public register (id: string, form: InstanceType<FormularioFormConstructor>): 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`)
} }
this.registry.set(id, form) this._registry.set(id, form)
} }
/** /**
@ -84,8 +79,8 @@ export default class Formulario {
* @internal * @internal
*/ */
public unregister (id: string): void { public unregister (id: string): void {
if (this.registry.has(id)) { if (this._registry.has(id)) {
this.registry.delete(id) this._registry.delete(id)
} }
} }

View File

@ -8,6 +8,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type {
Context,
Empty,
ModelGetConverter,
ModelSetConverter,
ValidationBehaviour,
UnregisterBehaviour,
} from '../types/field'
import type {
ValidationRuleFn,
ValidationMessageI18NFn,
Violation,
} from '../types/validation'
import Vue from 'vue' import Vue from 'vue'
import { import {
Component, Component,
@ -16,38 +31,19 @@ import {
Prop, Prop,
Watch, Watch,
} from 'vue-property-decorator' } from 'vue-property-decorator'
import { processConstraints, validate } from '@/validation/validator'
import { deepEquals, has, snakeToCamel } from './utils' import { deepEquals, has, snakeToCamel } from './utils'
import {
processConstraints,
validate,
ValidationRuleFn,
ValidationMessageI18NFn,
Violation,
} from '@/validation/validator'
import {
FormularioFieldContext,
FormularioFieldModelGetConverter as ModelGetConverter,
FormularioFieldModelSetConverter as ModelSetConverter,
Empty,
} from '@/types'
import { UNREGISTER_BEHAVIOR } from '@/enum'
const VALIDATION_BEHAVIOR = {
DEMAND: 'demand',
LIVE: 'live',
SUBMIT: 'submit',
}
@Component({ name: 'FormularioField', inheritAttrs: false }) @Component({ name: 'FormularioField', inheritAttrs: false })
export default class FormularioField extends Vue { export default class FormularioField extends Vue {
@Inject({ default: '' }) __Formulario_path!: string @Inject({ default: '' }) __Formulario_path!: string
@Inject({ default: undefined }) __FormularioForm_set!: Function|undefined @Inject({ default: undefined }) __FormularioForm_set!: ((path: string, value: unknown) => void)|undefined
@Inject({ default: () => (): void => {} }) __FormularioForm_emitInput!: Function @Inject({ default: () => (): void => {} }) __FormularioForm_emitInput!: () => void
@Inject({ default: () => (): void => {} }) __FormularioForm_emitValidation!: Function @Inject({ default: () => (): void => {} }) __FormularioForm_emitValidation!: (path: string, violations: Violation[]) => void
@Inject({ default: undefined }) __FormularioForm_register!: Function|undefined @Inject({ default: undefined }) __FormularioForm_register!: ((path: string, field: FormularioField) => void)|undefined
@Inject({ default: undefined }) __FormularioForm_unregister!: Function|undefined @Inject({ default: undefined }) __FormularioForm_unregister!: ((path: string, behavior: UnregisterBehaviour) => void)|undefined
@Inject({ default: () => (): Record<string, unknown> => ({}) }) @Inject({ default: () => (): Record<string, unknown> => ({}) })
__FormularioForm_getState!: () => Record<string, unknown> __FormularioForm_getState!: () => Record<string, unknown>
@ -63,9 +59,9 @@ export default class FormularioField extends Vue {
@Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRuleFn> @Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRuleFn>
@Prop({ default: () => ({}) }) validationMessages!: Record<string, ValidationMessageI18NFn|string> @Prop({ default: () => ({}) }) validationMessages!: Record<string, ValidationMessageI18NFn|string>
@Prop({ @Prop({
default: VALIDATION_BEHAVIOR.DEMAND, default: 'demand',
validator: behavior => Object.values(VALIDATION_BEHAVIOR).includes(behavior) validator: (behavior: string) => ['demand', 'live', 'submit'].includes(behavior)
}) validationBehavior!: string }) validationBehavior!: ValidationBehaviour
// Affects only setting of local errors // Affects only setting of local errors
@Prop({ default: false }) errorsDisabled!: boolean @Prop({ default: false }) errorsDisabled!: boolean
@ -74,7 +70,7 @@ export default class FormularioField extends Vue {
@Prop({ default: () => <T, U>(value: U|T): U|T => value }) modelSetConverter!: ModelSetConverter @Prop({ default: () => <T, U>(value: U|T): U|T => value }) modelSetConverter!: ModelSetConverter
@Prop({ default: 'div' }) tag!: string @Prop({ default: 'div' }) tag!: string
@Prop({ default: UNREGISTER_BEHAVIOR.NONE }) unregisterBehavior!: string @Prop({ default: 'none' }) unregisterBehavior!: UnregisterBehaviour
public proxy: unknown = this.hasModel ? this.value : '' public proxy: unknown = this.hasModel ? this.value : ''
@ -88,14 +84,12 @@ export default class FormularioField extends Vue {
return this.__Formulario_path !== '' ? `${this.__Formulario_path}.${this.name}` : this.name return this.__Formulario_path !== '' ? `${this.__Formulario_path}.${this.name}` : this.name
} }
/** /** Determines if this formulario element is v-modeled or not. */
* Determines if this formulario element is v-modeled or not.
*/
public get hasModel (): boolean { public get hasModel (): boolean {
return has(this.$options.propsData || {}, 'value') return has(this.$options.propsData || {}, 'value')
} }
private get context (): FormularioFieldContext<unknown> { private get context (): Context<unknown> {
return Object.defineProperty({ return Object.defineProperty({
name: this.fullPath, name: this.fullPath,
path: this.fullPath, path: this.fullPath,
@ -103,15 +97,15 @@ export default class FormularioField extends Vue {
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', { } as Context<unknown>, 'model', {
get: () => this.modelGetConverter(this.proxy), get: () => this.modelGetConverter(this.proxy),
set: (value: unknown): void => { set: (value: unknown): void => {
this.syncProxy(this.modelSetConverter(value, this.proxy)) this._syncProxy(this.modelSetConverter(value, this.proxy))
}, },
}) })
} }
private get normalizedValidationRules (): Record<string, ValidationRuleFn> { private get _normalizedValidationRules (): Record<string, ValidationRuleFn> {
const rules: Record<string, ValidationRuleFn> = {} const rules: Record<string, ValidationRuleFn> = {}
Object.keys(this.validationRules).forEach(key => { Object.keys(this.validationRules).forEach(key => {
rules[snakeToCamel(key)] = this.validationRules[key] rules[snakeToCamel(key)] = this.validationRules[key]
@ -119,7 +113,7 @@ export default class FormularioField extends Vue {
return rules return rules
} }
private get normalizedValidationMessages (): Record<string, ValidationMessageI18NFn|string> { private get _normalizedValidationMessages (): Record<string, ValidationMessageI18NFn|string> {
const messages: Record<string, ValidationMessageI18NFn|string> = {} const messages: Record<string, ValidationMessageI18NFn|string> = {}
Object.keys(this.validationMessages).forEach(key => { Object.keys(this.validationMessages).forEach(key => {
messages[snakeToCamel(key)] = this.validationMessages[key] messages[snakeToCamel(key)] = this.validationMessages[key]
@ -128,42 +122,70 @@ export default class FormularioField extends Vue {
} }
@Watch('value') @Watch('value')
private onValueChange (): void { private _onValueChange (): void {
this.syncProxy(this.value) this._syncProxy(this.value)
} }
@Watch('proxy') @Watch('proxy')
private onProxyChange (): void { private _onProxyChange (): void {
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) { if (this.validationBehavior === 'live') {
this.runValidation() this.runValidation()
} else { } else {
this.resetValidation() this.resetValidation()
} }
} }
/** /** @internal */
* @internal
*/
public created (): void { public created (): void {
if (typeof this.__FormularioForm_register === 'function') { if (typeof this.__FormularioForm_register === 'function') {
this.__FormularioForm_register(this.fullPath, this) this.__FormularioForm_register(this.fullPath, this)
} }
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) { if (this.validationBehavior === 'live') {
this.runValidation() this.runValidation()
} }
} }
/** /** @internal */
* @internal
*/
public beforeDestroy (): void { public beforeDestroy (): void {
if (typeof this.__FormularioForm_unregister === 'function') { if (typeof this.__FormularioForm_unregister === 'function') {
this.__FormularioForm_unregister(this.fullPath, this.unregisterBehavior) this.__FormularioForm_unregister(this.fullPath, this.unregisterBehavior)
} }
} }
private syncProxy (value: unknown): void { public runValidation (): Promise<Violation[]> {
this.validationRun = this._validate().then(violations => {
this.violations = violations
this._emitValidation(this.fullPath, violations)
return this.violations
})
return this.validationRun
}
public hasValidationErrors (): Promise<boolean> {
return new Promise(resolve => {
this.$nextTick(() => {
this.validationRun.then(() => resolve(this.violations.length > 0))
})
})
}
/** @internal */
public setErrors (errors: string[]): void {
if (!this.errorsDisabled) {
this.localErrors = errors
}
}
/** @internal */
public resetValidation (): void {
this.localErrors = []
this.violations = []
}
private _syncProxy (value: unknown): void {
if (!deepEquals(value, this.proxy)) { if (!deepEquals(value, this.proxy)) {
this.proxy = value this.proxy = value
this.$emit('input', value) this.$emit('input', value)
@ -175,22 +197,11 @@ export default class FormularioField extends Vue {
} }
} }
public runValidation (): Promise<Violation[]> { private _validate (): Promise<Violation[]> {
this.validationRun = this.validate().then(violations => {
this.violations = violations
this.emitValidation(this.fullPath, violations)
return this.violations
})
return this.validationRun
}
private validate (): Promise<Violation[]> {
return validate(processConstraints( return validate(processConstraints(
this.validation, this.validation,
this.$formulario.getRules(this.normalizedValidationRules), this.$formulario.getRules(this._normalizedValidationRules),
this.$formulario.getMessages(this, this.normalizedValidationMessages), this.$formulario.getMessages(this, this._normalizedValidationMessages),
), { ), {
value: this.proxy, value: this.proxy,
name: this.fullPath, name: this.fullPath,
@ -198,36 +209,11 @@ export default class FormularioField extends Vue {
}) })
} }
private emitValidation (path: string, violations: Violation[]): void { private _emitValidation (path: string, violations: Violation[]): void {
this.$emit('validation', { path, violations }) this.$emit('validation', { path, violations })
if (typeof this.__FormularioForm_emitValidation === 'function') { if (typeof this.__FormularioForm_emitValidation === 'function') {
this.__FormularioForm_emitValidation(path, violations) this.__FormularioForm_emitValidation(path, violations)
} }
} }
public hasValidationErrors (): Promise<boolean> {
return new Promise(resolve => {
this.$nextTick(() => {
this.validationRun.then(() => resolve(this.violations.length > 0))
})
})
}
/**
* @internal
*/
public setErrors (errors: string[]): void {
if (!this.errorsDisabled) {
this.localErrors = errors
}
}
/**
* @internal
*/
public resetValidation (): void {
this.localErrors = []
this.violations = []
}
} }
</script> </script>

View File

@ -5,7 +5,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { UnregisterBehaviour } from '../types/field'
import type { Violation } from '../types/validation'
import Vue from 'vue' import Vue from 'vue'
import { import {
Component, Component,
Model, Model,
@ -13,6 +17,7 @@ import {
Provide, Provide,
Watch, Watch,
} from 'vue-property-decorator' } from 'vue-property-decorator'
import { import {
id, id,
clone, clone,
@ -25,9 +30,6 @@ import {
} from '@/utils' } from '@/utils'
import { FormularioField } from '@/types' import { FormularioField } from '@/types'
import { Violation } from '@/validation/validator'
import { UNREGISTER_BEHAVIOR } from '@/enum'
const update = (state: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => { const update = (state: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
if (value === undefined) { if (value === undefined) {
@ -90,11 +92,11 @@ export default class FormularioForm extends Vue {
} }
@Provide('__FormularioForm_unregister') @Provide('__FormularioForm_unregister')
private unregister (path: string, behavior: string): void { private unregister (path: string, behavior: UnregisterBehaviour): void {
if (this.registry.has(path)) { if (this.registry.has(path)) {
this.registry.delete(path) this.registry.delete(path)
if (behavior === UNREGISTER_BEHAVIOR.UNSET) { if (behavior === 'unset') {
this.proxy = unset(this.proxy, path) as Record<string, unknown> this.proxy = unset(this.proxy, path) as Record<string, unknown>
this.emitInput() this.emitInput()
} }
@ -183,17 +185,17 @@ export default class FormularioForm extends Vue {
}) })
} }
public setErrors ({ fieldsErrors, formErrors }: { public setErrors ({ formErrors, fieldsErrors }: {
fieldsErrors?: Record<string, string[]>;
formErrors?: string[]; formErrors?: string[];
fieldsErrors?: Record<string, string[]>;
}): void { }): void {
this.localFieldsErrors = fieldsErrors || {}
this.localFormErrors = formErrors || [] this.localFormErrors = formErrors || []
this.localFieldsErrors = fieldsErrors || {}
} }
public resetValidation (): void { public resetValidation (): void {
this.localFieldsErrors = {}
this.localFormErrors = [] this.localFormErrors = []
this.localFieldsErrors = {}
this.registry.forEach((field: FormularioField) => { this.registry.forEach((field: FormularioField) => {
field.resetValidation() field.resetValidation()
}) })

View File

@ -1,4 +0,0 @@
export const UNREGISTER_BEHAVIOR = {
NONE: 'none',
UNSET: 'unset',
}

View File

@ -1,14 +1,22 @@
import { VueConstructor } from 'vue' import type { VueConstructor } from 'vue'
import type { Options } from '../types/plugin'
import Formulario, { FormularioOptions } from '@/Formulario.ts' import Formulario from '@/Formulario'
import FormularioField from '@/FormularioField.vue' import FormularioField from '@/FormularioField.vue'
import FormularioFieldGroup from '@/FormularioFieldGroup.vue' import FormularioFieldGroup from '@/FormularioFieldGroup.vue'
import FormularioForm from '@/FormularioForm.vue' import FormularioForm from '@/FormularioForm.vue'
export {
Formulario,
FormularioField,
FormularioFieldGroup,
FormularioForm,
}
export default { export default {
Formulario, Formulario,
install (Vue: VueConstructor, options?: FormularioOptions): void { install (Vue: VueConstructor, options?: Options): void {
Vue.component('FormularioField', FormularioField) Vue.component('FormularioField', FormularioField)
Vue.component('FormularioFieldGroup', FormularioFieldGroup) Vue.component('FormularioFieldGroup', FormularioFieldGroup)
Vue.component('FormularioForm', FormularioForm) Vue.component('FormularioForm', FormularioForm)

9
src/shims-ext.d.ts vendored
View File

@ -3,12 +3,7 @@ import Formulario from '@/Formulario'
declare module 'vue/types/vue' { declare module 'vue/types/vue' {
interface Vue { interface Vue {
$formulario: Formulario; $formulario: Formulario;
$route: VueRoute; $t: any;
$t: Function; $tc: any;
$tc: Function;
}
interface VueRoute {
path: string;
} }
} }

View File

@ -1,10 +1,6 @@
import Vue from 'vue' import type { Violation } from '../types/validation'
import { Violation } from '@/validation/validator'
export interface FormularioForm extends Vue { import Vue from 'vue'
runValidation(): Promise<Record<string, Violation[]>>;
resetValidation(): void;
}
export interface FormularioField extends Vue { export interface FormularioField extends Vue {
hasModel: boolean; hasModel: boolean;
@ -14,25 +10,6 @@ export interface FormularioField extends Vue {
resetValidation(): void; resetValidation(): void;
} }
export type FormularioFieldContext<T> = {
model: T;
name: string;
runValidation(): Promise<Violation[]>;
violations: Violation[];
errors: string[];
allErrors: string[];
}
export interface FormularioFieldModelGetConverter {
<U, T>(value: U|Empty): U|T|Empty;
}
export interface FormularioFieldModelSetConverter {
<T, U>(curr: U|T, prev: U|Empty): U|T;
}
export type Empty = undefined | null
export enum TYPE { export enum TYPE {
ARRAY = 'ARRAY', ARRAY = 'ARRAY',
BIGINT = 'BIGINT', BIGINT = 'BIGINT',

View File

@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import has from '@/utils/has.ts' import has from '@/utils/has'
/** /**
* Create a new object by copying properties of base and mergeWith. * Create a new object by copying properties of base and mergeWith.

View File

@ -1,7 +1,7 @@
import { import type {
ValidationContext, ValidationContext,
ValidationMessageI18NFn, ValidationMessageI18NFn,
} from '@/validation/validator' } from '../../types/validation'
/** /**
* Message builders, names match rules names, see @/validation/rules * Message builders, names match rules names, see @/validation/rules

View File

@ -1,9 +1,10 @@
import isUrl from 'is-url' import type {
import { has, regexForFormat, shallowEquals } from '@/utils'
import {
ValidationContext, ValidationContext,
ValidationRuleFn, ValidationRuleFn,
} from '@/validation/validator' } from '../../types/validation'
import isUrl from 'is-url'
import { has, regexForFormat, shallowEquals } from '@/utils'
const rules: Record<string, ValidationRuleFn> = { const rules: Record<string, ValidationRuleFn> = {
/** /**

View File

@ -1,42 +1,14 @@
import type {
ValidationContext,
ValidationMessageFn,
ValidationRuleFn,
Validator,
ValidatorGroup,
Violation,
} from '../../types/validation'
import { has, snakeToCamel } from '@/utils' import { has, snakeToCamel } from '@/utils'
export interface Validator {
(context: ValidationContext): Promise<Violation|null>;
}
export interface Violation {
message: string;
rule: string|null;
args: any[];
context: ValidationContext|null;
}
export interface ValidationRuleFn {
(context: ValidationContext, ...args: any[]): Promise<boolean>|boolean;
}
export interface ValidationMessageFn {
(context: ValidationContext, ...args: any[]): string;
}
export interface ValidationMessageI18NFn {
(vm: Vue, context: ValidationContext, ...args: any[]): string;
}
export interface ValidationContext {
// The value of the field (do not mutate!),
value: any;
// If wrapped in a FormulateForm, the value of other form fields.
formValues: Record<string, any>;
// The validation name to be used
name: string;
}
export type ValidatorGroup = {
validators: Validator[];
bail: boolean;
}
export function createValidator ( export function createValidator (
ruleFn: ValidationRuleFn, ruleFn: ValidationRuleFn,
ruleName: string|null, ruleName: string|null,

View File

@ -4,7 +4,6 @@ import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises' import flushPromises from 'flush-promises'
import Formulario from '@/index.ts' import Formulario from '@/index.ts'
import FormularioFieldGroup from '@/FormularioFieldGroup.vue'
import FormularioForm from '@/FormularioForm.vue' import FormularioForm from '@/FormularioForm.vue'
Vue.use(Formulario) Vue.use(Formulario)

34
types/field.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
import type { Violation } from './validation'
export type Empty = undefined | null
export type Context<T> = {
model: T;
name: string;
path: string;
violations: Violation[];
errors: string[];
allErrors: string[];
runValidation(): Promise<Violation[]>;
}
export interface ModelGetConverter {
<U, T>(value: U|Empty): U|T|Empty;
}
export interface ModelSetConverter {
<T, U>(curr: U|T, prev: U|Empty): U|T;
}
/**
* - 'demand': triggers validation on manual call
* - 'live': triggers validation on any changes
* - 'submit': triggers validation on form submit event
*/
export type ValidationBehaviour = 'demand' | 'live' | 'submit'
/**
* - 'none': no any specific effects
* - 'unset': the value under field's path will be unset and path will be removed from the state
*/
export type UnregisterBehaviour = 'none' | 'unset'

17
types/form.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import type { DefineComponent } from './vue'
import type { Violation } from './validation'
export type FormularioFormConstructor = DefineComponent<{
id?: string;
state?: Record<string, unknown>;
fieldsErrors?: Record<string, string[]>;
formErrors?: string[];
}, {
setErrors ({ formErrors, fieldsErrors }: {
formErrors?: string[];
fieldsErrors?: Record<string, string[]>;
}): void;
runValidation(): Promise<Record<string, Violation[]>>;
hasValidationErrors (): Promise<boolean>;
resetValidation(): void;
}>

9
types/plugin.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import type {
ValidationMessageI18NFn,
ValidationRuleFn
} from './validation'
export interface Options {
validationRules?: Record<string, ValidationRuleFn>;
validationMessages?: Record<string, ValidationMessageI18NFn|string>;
}

38
types/validation.d.ts vendored Normal file
View File

@ -0,0 +1,38 @@
import { Vue } from 'vue/types/vue'
export interface ValidationContext {
// The value of the field (do not mutate!),
value: any;
// If wrapped in a FormulateForm, the value of other form fields.
formValues: Record<string, any>;
// The validation name to be used
name: string;
}
export interface Violation {
message: string;
rule: string|null;
args: any[];
context: ValidationContext|null;
}
export interface Validator {
(context: ValidationContext): Promise<Violation|null>;
}
export type ValidatorGroup = {
validators: Validator[];
bail: boolean;
}
export interface ValidationRuleFn {
(context: ValidationContext, ...args: any[]): Promise<boolean>|boolean;
}
export interface ValidationMessageFn {
(context: ValidationContext, ...args: any[]): string;
}
export interface ValidationMessageI18NFn {
(vm: Vue, context: ValidationContext, ...args: any[]): string;
}

3
types/vue.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import type { Vue, VueConstructor } from 'vue/types/vue'
export type DefineComponent<Props, Methods> = VueConstructor<Vue & Required<Props> & Methods>

1776
yarn.lock

File diff suppressed because it is too large Load Diff