1
0
Fork 0
mirror of synced 2025-04-10 04:10:54 +00:00

Compare commits

..

No commits in common. "master" and "v0.6.3" have entirely different histories.

34 changed files with 4321 additions and 2718 deletions

View file

@ -1,27 +1,30 @@
module.exports = {
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: {
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: {
'@typescript-eslint/camelcase': ['error', {
allow: ['^__Formulario'],
}],
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off', // @TODO
'@typescript-eslint/no-unused-vars': ['error'], // @TODO

11
.gitignore vendored
View file

@ -1,16 +1,11 @@
node_modules
coverage
.cache
.DS_Store
.idea
.vscode
coverage
dist/
node_modules
*.sublime-project
*.sublime-workspace
*.dev.tale.vue
*.dev.stories.js
storybook-static
.npmrc
storybook-static

View file

@ -2,73 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.8.3](https://github.com/retailcrm/vue-formulario/compare/v0.8.2...v0.8.3) (2024-12-06)
### Features
* Updated package.json manifest ([cbe80e3](https://github.com/retailcrm/vue-formulario/commit/cbe80e3de498732af8db34f0846cc92d977d9420))
### [0.8.2](https://github.com/retailcrm/vue-formulario/compare/v0.8.1...v0.8.2) (2023-12-28)
### Features
* Added web-types declaration ([eda7a1d](https://github.com/retailcrm/vue-formulario/commit/eda7a1d79d98e4ccd76e67bc93c1bf8dc5249e67))
### [0.8.1](https://github.com/retailcrm/vue-formulario/compare/v0.8.0...v0.8.1) (2023-12-28)
### Fixes
* build ([92a3f8c](https://github.com/retailcrm/vue-formulario/commit/92a3f8cc60fdfb1f7588b281febcc833b17d3521))
## [0.8.0](https://github.com/retailcrm/vue-formulario/compare/v0.7.3...v0.8.0) (2023-12-28)
### ⚠ BREAKING CHANGES
* Old names of FormularioField & FormularioFieldGroup no longer available
### Features
* Components conststructors are exposed as external API, added TypeScript declarations ([dfc6557](https://github.com/retailcrm/vue-formulario/commit/dfc6557bc632489daeb8f4ae44ad208f6c6a9997))
* Old names of FormularioField & FormularioFieldGroup no longer available ([b83a429](https://github.com/retailcrm/vue-formulario/commit/b83a42911749272c9c8bd35c4ea10f687c5d8821))
### [0.7.3](https://github.com/retailcrm/vue-formulario/compare/v0.7.2...v0.7.3) (2021-11-11)
### Fixes
* Fixed toString calls in validators ([da56b04](https://github.com/retailcrm/vue-formulario/commit/da56b04213b6ebc3d001a273b26a350a59e0382b))
### [0.7.2](https://github.com/retailcrm/vue-formulario/compare/v0.7.1...v0.7.2) (2021-10-21)
### Fixes
* Blob objects are no longer cloned ([67dba98](https://github.com/retailcrm/vue-formulario/commit/67dba981a15b04a84512de277f633d0f7d19d543))
### [0.7.1](https://github.com/retailcrm/vue-formulario/compare/v0.7.0...v0.7.1) (2021-09-30)
### Fixes
* Build ([8e36a9f](https://github.com/retailcrm/vue-formulario/commit/8e36a9f59dc21d0efc4f3dfe97fe992c204ee3e0))
## [0.7.0](https://github.com/retailcrm/vue-formulario/compare/v0.6.3...v0.7.0) (2021-09-30)
### ⚠ BREAKING CHANGES
* Added property "unregisterBehavior" to FormularioField to control value unset behavior on field component removal
### Features
* Added property "unregisterBehavior" to FormularioField to control value unset behavior on field component removal ([d39ca17](https://github.com/retailcrm/vue-formulario/commit/d39ca17e45cb5957bd9b9916b6e904993e660bc5))
### [0.6.3](https://github.com/retailcrm/vue-formulario/compare/v0.6.2...v0.6.3) (2021-09-29)

View file

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

View file

@ -11,8 +11,10 @@ export default {
input: 'src/index.ts',
output: {
name: 'VueFormulario',
exports: 'default',
format: 'iife',
globals: {
'is-plain-object': 'isPlainObject',
'is-url': 'isUrl',
vue: 'Vue',
'vue-property-decorator': 'vuePropertyDecorator',
@ -28,7 +30,7 @@ export default {
vue({ css: true, compileTemplate: true }),
alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }),
commonjs(),
internal(['is-url', 'vue-property-decorator']),
internal(['is-plain-object', 'is-url', 'vue-property-decorator']),
terser(),
]
}

1531
dist/formulario.esm.js vendored Normal file

File diff suppressed because it is too large Load diff

34
dist/formulario.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1538
dist/formulario.umd.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
version: '3.6'
services:
node:
image: node:16-alpine
image: node:12-alpine
user: node
volumes:
- ./:/var/www/vue-formulario

94
index.d.ts vendored
View file

@ -1,94 +0,0 @@
// 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

@ -1,6 +1,6 @@
{
"name": "@retailcrm/vue-formulario",
"version": "0.8.3",
"version": "0.6.3",
"license": "MIT",
"author": "RetailDriverLLC <integration@retailcrm.ru>",
"main": "dist/formulario.umd.js",
@ -9,22 +9,12 @@
"./sfc": "src/index.ts"
},
"unpkg": "dist/formulario.min.js",
"web-types": "web-types.json",
"files": [
"dist",
"types",
"CHANGELOG.md",
"LICENSE.txt",
"README.md"
],
"dependencies": {
"is-plain-object": "^3.0.0",
"is-url": "^1.2.4",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^8.4.2"
},
"peerDependencies": {
"vue": "^2.6"
},
"bugs": {
"url": "https://github.com/retailcrm/vue-formulario/issues"
},
@ -34,7 +24,7 @@
"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:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
"lint": "eslint --ext .js,.mjs,.ts,.vue",
"lint": "vue-cli-service lint",
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:patch": "standard-version --release-as patch",
@ -62,28 +52,32 @@
"@storybook/vue": "^6.0.26",
"@types/is-url": "^1.2.28",
"@types/jest": "^26.0.14",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@vue/cli-plugin-babel": "^4.3.1",
"@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-plugin-typescript": "^4.5.7",
"@vue/cli-service": "^4.5.4",
"@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",
"autoprefixer": "^9.7.6",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^25.5.1",
"bootstrap-scss": "^4.5.2",
"eslint": "^8.56.0",
"eslint-config-standard": "^17.1.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.19.2",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.3",
"flush-promises": "^1.0.2",
"jest": "^26.5.2",
"jest-vue-preprocessor": "^1.7.1",
"node-sass": "^9.0.0",
"node-sass": "^4.14.1",
"rollup": "^1.32.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-internal": "^1.0.4",
@ -94,7 +88,7 @@
"sass-loader": "^10.0.3",
"standard-version": "^9.3.0",
"ts-jest": "^26.4.1",
"typescript": "^4.9.5",
"typescript": "~3.9.3",
"vue": "^2.6.11",
"vue-cli-plugin-storybook": "^1.3.0",
"vue-jest": "^3.0.5",

View file

@ -1,26 +1,33 @@
import type { FormularioFormConstructor } from '../types/form'
import merge from '@/utils/merge'
import validationRules from '@/validation/rules'
import validationMessages from '@/validation/messages'
import type {
import {
ValidationContext,
ValidationRuleFn,
ValidationMessageFn,
ValidationMessageI18NFn,
Violation,
} from '../types/validation'
} from '@/validation/validator'
import type { Options } from '../types/plugin'
import { FormularioForm } from '@/types'
import validationRules from '@/validation/rules'
import validationMessages from '@/validation/messages'
export interface FormularioOptions {
validationRules?: Record<string, ValidationRuleFn>;
validationMessages?: Record<string, ValidationMessageI18NFn|string>;
}
/**
* The base formulario library.
*/
export default class Formulario {
public validationRules: Record<string, ValidationRuleFn> = {}
public validationMessages: Record<string, ValidationMessageI18NFn|string> = {}
private readonly _registry: Map<string, InstanceType<FormularioFormConstructor>>
private readonly registry: Map<string, FormularioForm>
public constructor (options?: Options) {
this._registry = new Map()
public constructor (options?: FormularioOptions) {
this.registry = new Map()
this.validationRules = validationRules
this.validationMessages = validationMessages
@ -31,31 +38,31 @@ export default class Formulario {
/**
* Given a set of options, apply them to the pre-existing options.
*/
public extend (extendWith: Options): Formulario {
public extend (extendWith: FormularioOptions): Formulario {
if (typeof extendWith === 'object') {
this.validationRules = { ...this.validationRules, ...(extendWith.validationRules || {}) }
this.validationMessages = { ...this.validationMessages, ...(extendWith.validationMessages || {}) }
this.validationRules = merge(this.validationRules, extendWith.validationRules || {})
this.validationMessages = merge(this.validationMessages, extendWith.validationMessages || {})
return this
}
throw new Error(`[Formulario]: Formulario.extend(): should be passed an object (was ${typeof extendWith})`)
}
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}"`)
}
const form = this._registry.get(id) as InstanceType<FormularioFormConstructor>
const form = this.registry.get(id) as FormularioForm
return form.runValidation()
}
public resetValidation (id: string): void {
if (!this._registry.has(id)) {
if (!this.registry.has(id)) {
return
}
const form = this._registry.get(id) as InstanceType<FormularioFormConstructor>
const form = this.registry.get(id) as FormularioForm
form.resetValidation()
}
@ -64,12 +71,12 @@ export default class Formulario {
* Used by forms instances to add themselves into a registry
* @internal
*/
public register (id: string, form: InstanceType<FormularioFormConstructor>): void {
if (this._registry.has(id)) {
public register (id: string, form: FormularioForm): void {
if (this.registry.has(id)) {
throw new Error(`[Formulario]: Formulario.register(): id "${id}" is already in use`)
}
this._registry.set(id, form)
this.registry.set(id, form)
}
/**
@ -77,8 +84,8 @@ export default class Formulario {
* @internal
*/
public unregister (id: string): void {
if (this._registry.has(id)) {
this._registry.delete(id)
if (this.registry.has(id)) {
this.registry.delete(id)
}
}
@ -87,7 +94,7 @@ export default class Formulario {
* @internal
*/
public getRules (extendWith: Record<string, ValidationRuleFn> = {}): Record<string, ValidationRuleFn> {
return { ...this.validationRules, ...extendWith }
return merge(this.validationRules, extendWith)
}
/**
@ -95,14 +102,12 @@ export default class Formulario {
* @internal
*/
public getMessages (vm: Vue, extendWith: Record<string, ValidationMessageI18NFn|string>): Record<string, ValidationMessageFn> {
const raw = { ...this.validationMessages, ...extendWith }
const raw = merge(this.validationMessages || {}, extendWith)
const messages: Record<string, ValidationMessageFn> = {}
for (const name in raw) {
messages[name] = (context: ValidationContext, ...args: unknown[]): string => {
const fn = raw[name]
return typeof fn === 'string' ? fn : fn(vm, context, ...args)
messages[name] = (context: ValidationContext, ...args: any[]): string => {
return typeof raw[name] === 'string' ? raw[name] : raw[name](vm, context, ...args)
}
}

View file

@ -8,21 +8,6 @@
</template>
<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 {
Component,
@ -31,22 +16,39 @@ import {
Prop,
Watch,
} from 'vue-property-decorator'
import { processConstraints, validate } from '@/validation/validator'
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'
const VALIDATION_BEHAVIOR = {
DEMAND: 'demand',
LIVE: 'live',
SUBMIT: 'submit',
}
@Component({ name: 'FormularioField', inheritAttrs: false })
export default class FormularioField extends Vue {
@Inject({ default: '' }) __Formulario_path!: string
@Inject({ default: undefined }) __FormularioForm_set!: ((path: string, value: unknown) => void)|undefined
@Inject({ default: () => (): void => {} }) __FormularioForm_emitInput!: () => void
@Inject({ default: () => (): void => {} }) __FormularioForm_emitValidation!: (path: string, violations: Violation[]) => void
@Inject({ default: undefined }) __FormularioForm_register!: ((path: string, field: FormularioField) => void)|undefined
@Inject({ default: undefined }) __FormularioForm_unregister!: ((path: string, behavior: UnregisterBehaviour) => void)|undefined
@Inject({ default: undefined }) __FormularioForm_set!: Function|undefined
@Inject({ default: () => (): void => {} }) __FormularioForm_emitInput!: Function
@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_getState!: () => Record<string, unknown>
__FormularioForm_getState!: () => Record<string, unknown>
@Model('input', { default: '' }) value!: unknown
@ -59,9 +61,9 @@ export default class FormularioField extends Vue {
@Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRuleFn>
@Prop({ default: () => ({}) }) validationMessages!: Record<string, ValidationMessageI18NFn|string>
@Prop({
default: 'demand',
validator: (behavior: string) => ['demand', 'live', 'submit'].includes(behavior)
}) validationBehavior!: ValidationBehaviour
default: VALIDATION_BEHAVIOR.DEMAND,
validator: behavior => Object.values(VALIDATION_BEHAVIOR).includes(behavior)
}) validationBehavior!: string
// Affects only setting of local errors
@Prop({ default: false }) errorsDisabled!: boolean
@ -70,7 +72,6 @@ export default class FormularioField extends Vue {
@Prop({ default: () => <T, U>(value: U|T): U|T => value }) modelSetConverter!: ModelSetConverter
@Prop({ default: 'div' }) tag!: string
@Prop({ default: 'none' }) unregisterBehavior!: UnregisterBehaviour
public proxy: unknown = this.hasModel ? this.value : ''
@ -84,12 +85,14 @@ export default class FormularioField extends Vue {
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 {
return has(this.$options.propsData || {}, 'value')
}
private get context (): Context<unknown> {
private get context (): FormularioFieldContext<unknown> {
return Object.defineProperty({
name: this.fullPath,
path: this.fullPath,
@ -97,15 +100,15 @@ export default class FormularioField extends Vue {
violations: this.violations,
errors: this.localErrors,
allErrors: [...this.localErrors, ...this.violations.map(v => v.message)],
} as Context<unknown>, 'model', {
}, 'model', {
get: () => this.modelGetConverter(this.proxy),
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> = {}
Object.keys(this.validationRules).forEach(key => {
rules[snakeToCamel(key)] = this.validationRules[key]
@ -113,7 +116,7 @@ export default class FormularioField extends Vue {
return rules
}
private 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]
@ -122,70 +125,42 @@ export default class FormularioField extends Vue {
}
@Watch('value')
private _onValueChange (): void {
this._syncProxy(this.value)
private onValueChange (): void {
this.syncProxy(this.value)
}
@Watch('proxy')
private _onProxyChange (): void {
if (this.validationBehavior === 'live') {
private onProxyChange (): void {
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
this.runValidation()
} else {
this.resetValidation()
}
}
/** @internal */
/**
* @internal
*/
public created (): void {
if (typeof this.__FormularioForm_register === 'function') {
this.__FormularioForm_register(this.fullPath, this)
}
if (this.validationBehavior === 'live') {
if (this.validationBehavior === VALIDATION_BEHAVIOR.LIVE) {
this.runValidation()
}
}
/** @internal */
/**
* @internal
*/
public beforeDestroy (): void {
if (typeof this.__FormularioForm_unregister === 'function') {
this.__FormularioForm_unregister(this.fullPath, this.unregisterBehavior)
this.__FormularioForm_unregister(this.fullPath)
}
}
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 {
private syncProxy (value: unknown): void {
if (!deepEquals(value, this.proxy)) {
this.proxy = value
this.$emit('input', value)
@ -197,11 +172,22 @@ export default class FormularioField extends Vue {
}
}
private _validate (): Promise<Violation[]> {
public runValidation (): 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(
this.validation,
this.$formulario.getRules(this._normalizedValidationRules),
this.$formulario.getMessages(this, this._normalizedValidationMessages),
this.$formulario.getRules(this.normalizedValidationRules),
this.$formulario.getMessages(this, this.normalizedValidationMessages),
), {
value: this.proxy,
name: this.fullPath,
@ -209,11 +195,36 @@ 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 })
if (typeof this.__FormularioForm_emitValidation === 'function') {
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>

View file

@ -5,11 +5,7 @@
</template>
<script lang="ts">
import type { UnregisterBehaviour } from '../types/field'
import type { Violation } from '../types/validation'
import Vue from 'vue'
import {
Component,
Model,
@ -17,18 +13,19 @@ import {
Provide,
Watch,
} from 'vue-property-decorator'
import {
id,
clone,
deepEquals,
get,
has,
merge,
set,
unset,
} from '@/utils'
import { FormularioField } from '@/types'
import { Violation } from '@/validation/validator'
const update = (state: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
if (value === undefined) {
@ -58,7 +55,7 @@ export default class FormularioForm extends Vue {
private localFormErrors: string[] = []
private get fieldsErrorsComputed (): Record<string, string[]> {
return { ...this.fieldsErrors, ...this.localFieldsErrors }
return merge(this.fieldsErrors || {}, this.localFieldsErrors)
}
private get formErrorsComputed (): string[] {
@ -91,14 +88,11 @@ export default class FormularioForm extends Vue {
}
@Provide('__FormularioForm_unregister')
private unregister (path: string, behavior: UnregisterBehaviour): void {
private unregister (path: string): void {
if (this.registry.has(path)) {
this.registry.delete(path)
if (behavior === 'unset') {
this.proxy = unset(this.proxy, path) as Record<string, unknown>
this.emitInput()
}
this.proxy = unset(this.proxy, path) as Record<string, unknown>
this.emitInput()
}
}
@ -184,17 +178,17 @@ export default class FormularioForm extends Vue {
})
}
public setErrors ({ formErrors, fieldsErrors }: {
formErrors?: string[];
public setErrors ({ fieldsErrors, formErrors }: {
fieldsErrors?: Record<string, string[]>;
formErrors?: string[];
}): void {
this.localFormErrors = formErrors || []
this.localFieldsErrors = fieldsErrors || {}
this.localFormErrors = formErrors || []
}
public resetValidation (): void {
this.localFormErrors = []
this.localFieldsErrors = {}
this.localFormErrors = []
this.registry.forEach((field: FormularioField) => {
field.resetValidation()
})

View file

@ -1,26 +1,23 @@
import type { VueConstructor } from 'vue'
import type { Options } from '../types/plugin'
import { VueConstructor } from 'vue'
import Formulario from '@/Formulario'
import Formulario, { FormularioOptions } from '@/Formulario.ts'
import FormularioField from '@/FormularioField.vue'
import FormularioFieldGroup from '@/FormularioFieldGroup.vue'
import FormularioForm from '@/FormularioForm.vue'
export {
Formulario,
FormularioField,
FormularioFieldGroup,
FormularioForm,
}
export default {
Formulario,
install (Vue: VueConstructor, options?: Options): void {
install (Vue: VueConstructor, options?: FormularioOptions): void {
Vue.component('FormularioField', FormularioField)
Vue.component('FormularioFieldGroup', FormularioFieldGroup)
Vue.component('FormularioForm', FormularioForm)
// @deprecated Use FormularioField instead
Vue.component('FormularioInput', FormularioField)
// @deprecated Use FormularioFieldGroup instead
Vue.component('FormularioGrouping', FormularioFieldGroup)
Vue.mixin({
beforeCreate () {
const o = this.$options as Record<string, any>

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

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

View file

@ -1,6 +1,10 @@
import type { Violation } from '../types/validation'
import Vue from 'vue'
import { Violation } from '@/validation/validator'
export interface FormularioForm extends Vue {
runValidation(): Promise<Record<string, Violation[]>>;
resetValidation(): void;
}
export interface FormularioField extends Vue {
hasModel: boolean;
@ -10,6 +14,25 @@ export interface FormularioField extends Vue {
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 {
ARRAY = 'ARRAY',
BIGINT = 'BIGINT',
@ -72,7 +95,7 @@ export function typeOf (value: unknown): string {
return 'InstanceOf<' + (constructorOf(value) as { name?: string }).name + '>'
}
throw new Error('[Formulario] typeOf - unknown type detected')
throw new Error()
}
export function isScalar (value: unknown): boolean {

View file

@ -9,8 +9,7 @@ const cloneInstance = <T>(original: T): T => {
* case of needing to unbind reactive watchers.
*/
export default function clone<T = unknown> (value: T): T {
// scalars & immutables
if (isScalar(value) || value instanceof Blob) {
if (isScalar(value)) {
return value
}

View file

@ -1,6 +1,7 @@
export { default as id } from './id'
export { default as clone } from './clone'
export { default as has } from './has'
export { default as merge } from './merge'
export { get, set, unset } from './access'
export { default as regexForFormat } from './regexForFormat'
export { deepEquals, shallowEquals } from './compare'

40
src/utils/merge.ts Normal file
View file

@ -0,0 +1,40 @@
import isPlainObject from 'is-plain-object'
import has from '@/utils/has.ts'
/**
* Create a new object by copying properties of base and mergeWith.
* Note: arrays don't overwrite - they push
*
* @param {Object} a
* @param {Object} b
* @param {boolean} concatArrays
*/
export default function merge (
a: Record<string, any>,
b: Record<string, any>,
concatArrays = true
): Record<string, any> {
const merged: Record<string, any> = {}
for (const key in a) {
if (has(b, key)) {
if (isPlainObject(b[key]) && isPlainObject(a[key])) {
merged[key] = merge(a[key], b[key], concatArrays)
} else if (concatArrays && Array.isArray(a[key]) && Array.isArray(b[key])) {
merged[key] = a[key].concat(b[key])
} else {
merged[key] = b[key]
}
} else {
merged[key] = a[key]
}
}
for (const prop in b) {
if (!has(merged, prop)) {
merged[prop] = b[prop]
}
}
return merged
}

View file

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

View file

@ -1,10 +1,9 @@
import type {
ValidationContext,
ValidationRuleFn,
} from '../../types/validation'
import isUrl from 'is-url'
import { has, regexForFormat, shallowEquals } from '@/utils'
import {
ValidationContext,
ValidationRuleFn,
} from '@/validation/validator'
const rules: Record<string, ValidationRuleFn> = {
/**
@ -166,7 +165,7 @@ const rules: Record<string, ValidationRuleFn> = {
}
if (typeof value === 'string' || (force === 'length')) {
value = !isNaN(value) ? String(value) : value
value = !isNaN(value) ? value.toString() : value
return value.length <= maximum
}
@ -188,7 +187,7 @@ const rules: Record<string, ValidationRuleFn> = {
}
if (typeof value === 'string' || (force === 'length')) {
value = !isNaN(value) ? String(value) : value
value = !isNaN(value) ? value.toString() : value
return value.length >= minimum
}

View file

@ -1,14 +1,42 @@
import type {
ValidationContext,
ValidationMessageFn,
ValidationRuleFn,
Validator,
ValidatorGroup,
Violation,
} from '../../types/validation'
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 (
ruleFn: ValidationRuleFn,
ruleName: string|null,

View file

@ -286,37 +286,4 @@ describe('FormularioField', () => {
[{ date: new Date('2001-05-12') }],
])
})
test('unregister behavior', async () => {
const wrapper = mount({
props: {
formExists: {
type: Boolean,
default: true,
}
},
data: () => ({ state: { fieldA: '', fieldB: '' } }),
watch: {
state () {
this.$emit('updated', this.state)
},
},
template: `
<div>
<FormularioForm v-if="formExists" v-model="state">
<FormularioField name="fieldA" />
<FormularioField name="fieldB" unregister-behavior="unset" />
</FormularioForm>
</div>
`,
})
await wrapper.vm.$nextTick()
wrapper.setProps({ formExists: false })
await wrapper.vm.$nextTick()
expect(wrapper.emitted('updated')).toEqual([[{ fieldA: '' }]])
})
})

View file

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

View file

@ -76,11 +76,4 @@ describe('clone', () => {
expect(copy.sample.doSomething).toBeTruthy()
expect(copy.sample.doSomething).not.toThrow()
})
test('does not create a copy of a blob', () => {
const blob = new Blob(['{"fieldA": "fieldA"}'], { type : 'application/json' })
const copy = clone(blob)
expect(blob === copy).toBeTruthy()
})
})

View file

@ -0,0 +1,56 @@
import merge from '@/utils/merge.ts'
describe('merge', () => {
it('Can merge simple object', () => {
expect(merge({
optionA: true,
optionB: '1234',
}, {
optionA: false,
})).toEqual({
optionA: false,
optionB: '1234',
})
})
it('Can add to simple array', () => {
expect(merge({
optionA: true,
optionB: ['first', 'second']
}, {
optionB: ['third']
}, true)).toEqual({
optionA: true,
optionB: ['first', 'second', 'third']
})
})
it('Can merge recursively', () => {
expect(merge({
optionA: true,
optionC: {
first: '123',
third: {
a: 'b',
},
},
optionB: '1234',
}, {
optionB: '567',
optionC: {
first: '1234',
second: '789',
}
})).toEqual({
optionA: true,
optionC: {
first: '1234',
third: {
a: 'b',
},
second: '789',
},
optionB: '567',
})
})
})

34
types/field.d.ts vendored
View file

@ -1,34 +0,0 @@
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
View file

@ -1,17 +0,0 @@
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
View file

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

38
types/validation.d.ts vendored
View file

@ -1,38 +0,0 @@
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
View file

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

View file

@ -1,207 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
"framework": "vue",
"name": "@omnica/accordion-vue2",
"version": "0.24.22",
"js-types-syntax": "typescript",
"description-markup": "markdown",
"contributions": {
"html": {
"vue-components": [
{
"name": "FormularioForm",
"description": "Form root",
"source": {
"module": "@retailcrm/vue-formulario",
"symbol": "FormularioForm"
},
"props": [
{
"name": "state",
"type": "object",
"default": "{}",
"description": ""
},
{
"name": "id",
"type": "string",
"default": "id('formulario-form')",
"description": ""
},
{
"name": "fieldsErrors",
"type": "object",
"default": "{}",
"description": "Describes validation errors of concrete fields"
},
{
"name": "formErrors",
"type": "array",
"default": "[]",
"description": "Describes validation errors of entire state"
}
],
"events": [
{
"name": "input",
"description": "Occurs on state change",
"arguments": [
{
"name": "...",
"type": {
"name": "any"
}
}
]
},
{
"name": "validation",
"description": "Occurs at the end of a validation run",
"arguments": [
{
"name": "...",
"type": {
"name": "any"
}
}
]
}
],
"slots": [
{
"name": "default",
"description": "Form content",
"scoped": true
}
]
},
{
"name": "FormularioField",
"description": "Form field",
"source": {
"module": "@retailcrm/vue-formulario",
"symbol": "FormularioField"
},
"props": [
{
"name": "value",
"type": "any",
"description": "The field's value, if not set explicitly, will be extracted from the state using 'path'"
},
{
"name": "name",
"type": "string",
"description": "Path to field's value in the state",
"required": true
},
{
"name": "validation",
"type": "string|array",
"description": ""
},
{
"name": "validationRules",
"type": "object",
"description": "Validation rules override/extension opportunity",
"default": "{}"
},
{
"name": "validationMessages",
"type": "object",
"description": "Validation messages override/extension opportunity",
"default": "{}"
},
{
"name": "validationBehavior",
"type": "string",
"description": "",
"default": "'demand'"
},
{
"name": "errorsDisabled",
"type": "boolean",
"description": "Disables passing errors to the field from FormularioForm's fieldsErrors",
"default": "false"
},
{
"name": "modelGetConverter",
"type": "function",
"description": "Simple middleware that provides opportunity to transform value before passing it to a template"
},
{
"name": "modelSetConverter",
"type": "function",
"description": "Simple middleware that provides opportunity to transform new value before assigning"
},
{
"name": "tag",
"type": "string",
"description": "Root element's tagName in lowercase"
},
{
"name": "unregisterBehavior",
"type": "string",
"description": "Possible values: 'none', 'unset'",
"default": "'none'"
}
],
"events": [
{
"name": "input",
"description": "Occurs on value change",
"arguments": [
{
"name": "...",
"type": {
"name": "any"
}
}
]
},
{
"name": "validation",
"description": "Occurs at the end of a validation run",
"arguments": [
{
"name": "...",
"type": {
"name": "any"
}
}
]
}
],
"slots": [
{
"name": "default",
"description": "Field content",
"scoped": true
}
]
},
{
"name": "FormularioFieldGroup",
"description": "Field group",
"source": {
"module": "@retailcrm/vue-formulario",
"symbol": "FormularioFieldGroup"
},
"props": [
{
"name": "name",
"type": "string",
"description": "Path to a nested state",
"required": true
}
],
"slots": [
{
"name": "default",
"description": "Group content"
}
]
}
]
}
}
}

2856
yarn.lock

File diff suppressed because it is too large Load diff