Skip to content

Commit

Permalink
feat: hook up the provider with new observer implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Feb 14, 2020
1 parent 7ea8263 commit 4d18a65
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 276 deletions.
288 changes: 17 additions & 271 deletions packages/core/src/components/Observer.ts
@@ -1,277 +1,23 @@
import Vue, { VueConstructor, VNode } from 'vue';
import { values, findIndex, debounce, createFlags } from '../utils';
import { ValidationResult, VeeObserver, VNodeWithVeeContext, ValidationFlags, KnownKeys } from '../types';
import { ValidationProvider } from './Provider';
import { SetupContext, computed, provide } from 'vue';
import { normalizeChildren } from '../utils/vnode';
import { useForm } from '../useForm';

const FLAGS_STRATEGIES: [KnownKeys<ValidationFlags>, 'every' | 'some'][] = [
['pristine', 'every'],
['dirty', 'some'],
['touched', 'some'],
['untouched', 'every'],
['valid', 'every'],
['invalid', 'some'],
['pending', 'some'],
['validated', 'every'],
['changed', 'some'],
['passed', 'every'],
['failed', 'some']
];

type ProviderInstance = InstanceType<typeof ValidationProvider>;
type ObserverErrors = Record<string, string[]>;

interface ObserverField {
id: string;
name: string;
failedRules: Record<string, string>;
pristine: boolean;
dirty: boolean;
touched: boolean;
untouched: boolean;
valid: boolean;
invalid: boolean;
pending: boolean;
validated: boolean;
changed: boolean;
passed: boolean;
failed: boolean;
}

let OBSERVER_COUNTER = 0;

type withObserverNode = VueConstructor<
Vue & {
$_veeObserver: VeeObserver;
$vnode: VNodeWithVeeContext;
}
>;

function data() {
const refs: Record<string, ProviderInstance> = {};
const errors: ObserverErrors = {};
const flags: ValidationFlags = createObserverFlags();
const fields: Record<string, ObserverField> = {};
// FIXME: Not sure of this one can be typed, circular type reference.
const observers: any[] = [];

return {
id: '',
refs,
observers,
errors,
flags,
fields
};
}

function provideSelf(this: any) {
return {
$_veeObserver: this
};
}

export const ValidationObserver = (Vue as withObserverNode).extend({
export const ValidationObserver = {
name: 'ValidationObserver',
provide: provideSelf,
inject: {
$_veeObserver: {
from: '$_veeObserver',
default() {
if (!this.$vnode.context.$_veeObserver) {
return null;
}

return this.$vnode.context.$_veeObserver;
}
}
},
props: {
tag: {
type: String,
default: 'span'
},
vid: {
type: String,
default() {
return `obs_${OBSERVER_COUNTER++}`;
}
},
slim: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
data,
created() {
this.id = this.vid;
register(this);

const onChange = debounce(
({
errors,
flags,
fields
}: {
errors: ObserverErrors;
flags: ValidationFlags;
fields: Record<string, ObserverField>;
}) => {
this.errors = errors;
this.flags = flags;
this.fields = fields;
},
16
);

this.$watch(computeObserverState, onChange as any);
},
activated() {
register(this);
},
deactivated() {
unregister(this);
},
beforeDestroy() {
unregister(this);
},
render(h): VNode {
const children = normalizeChildren(this, prepareSlotProps(this));

return this.slim && children.length <= 1 ? children[0] : h(this.tag, { on: this.$listeners }, children);
},
methods: {
observe(subscriber: any, kind = 'provider') {
if (kind === 'observer') {
this.observers.push(subscriber);
return;
}

this.refs = { ...this.refs, ...{ [subscriber.id]: subscriber } };
},
unobserve(id: string, kind = 'provider') {
if (kind === 'provider') {
const provider = this.refs[id];
if (!provider) {
return;
}

this.$delete(this.refs, id);
return;
}

const idx = findIndex(this.observers, (o: any) => o.id === id);
if (idx !== -1) {
this.observers.splice(idx, 1);
}
},
async validate({ silent = false }: { silent?: boolean } = {}) {
const results = await Promise.all([
...values(this.refs)
.filter((r: any) => !r.disabled)
.map((ref: any) => ref[silent ? 'validateSilent' : 'validate']().then((r: ValidationResult) => r.valid)),
...this.observers.filter((o: any) => !o.disabled).map((obs: any) => obs.validate({ silent }))
]);

return results.every(r => r);
},
async handleSubmit(cb: Function) {
const isValid = await this.validate();
if (!isValid || !cb) {
return;
}

return cb();
},
reset() {
return [...values(this.refs), ...this.observers].forEach(ref => ref.reset());
},
setErrors(errors: Record<string, string[] | string>) {
Object.keys(errors).forEach(key => {
const provider = this.refs[key];
if (!provider) return;
let errorArr = errors[key] || [];
errorArr = typeof errorArr === 'string' ? [errorArr] : errorArr;

provider.setErrors(errorArr);
});

this.observers.forEach((observer: any) => {
observer.setErrors(errors);
});
}
}
});

type ObserverInstance = InstanceType<typeof ValidationObserver>;

function unregister(vm: ObserverInstance) {
if (vm.$_veeObserver) {
vm.$_veeObserver.unobserve(vm.id, 'observer');
}
}

function register(vm: ObserverInstance) {
if (vm.$_veeObserver) {
vm.$_veeObserver.observe(vm, 'observer');
}
}

function prepareSlotProps(vm: ObserverInstance) {
return {
...vm.flags,
errors: vm.errors,
fields: vm.fields,
validate: vm.validate,
passes: vm.handleSubmit,
handleSubmit: vm.handleSubmit,
reset: vm.reset
};
}

// Creates a modified version of validation flags
function createObserverFlags() {
return {
...createFlags(),
valid: true,
invalid: false
};
}

function computeObserverState(this: ObserverInstance) {
const vms = [...values(this.refs), ...this.observers];
let errors: ObserverErrors = {};
const flags: ValidationFlags = createObserverFlags();
let fields: Record<string, ObserverField> = {};

const length = vms.length;
for (let i = 0; i < length; i++) {
const vm = vms[i];

// validation provider
if (Array.isArray(vm.errors)) {
errors[vm.id] = vm.errors;
fields[vm.id] = {
id: vm.id,
name: vm.name,
failedRules: vm.failedRules,
...vm.flags
setup(_: any, ctx: SetupContext) {
const { form, errors, validate, handleSubmit, reset, ...flags } = useForm();
provide('$_veeObserver', form);

const slotProps = computed(() => {
return {
...flags,
errors: errors.value,
validate,
handleSubmit,
reset
};
continue;
}
});

// Nested observer, merge errors and fields
errors = { ...errors, ...vm.errors };
fields = { ...fields, ...vm.fields };
return () => normalizeChildren(ctx, slotProps.value);
}

FLAGS_STRATEGIES.forEach(([flag, method]) => {
flags[flag] = vms[method](vm => vm.flags[flag]);
});

return { errors, flags, fields };
}
};
11 changes: 8 additions & 3 deletions packages/core/src/components/Provider.ts
@@ -1,4 +1,4 @@
import { ref, computed, SetupContext, VNode } from 'vue';
import { ref, computed, SetupContext, VNode, inject } from 'vue';
import { modes, InteractionModeFactory } from '../modes';
import { normalizeRules } from '../utils/rules';
import {
Expand All @@ -12,7 +12,7 @@ import {
import { isCallable, isEqual, isNullOrUndefined } from '../utils';
import { getConfig } from '../config';
import { RuleContainer } from '../extend';
import { Flag, ValidationFlags } from '../types';
import { Flag, ValidationFlags, FormController } from '../types';
import { useField } from '../useField';

interface ProviderProps {
Expand Down Expand Up @@ -77,10 +77,15 @@ export const ValidationProvider = {
},
setup(props: ProviderProps, ctx: SetupContext) {
const fieldName = ref(props.name || '');
const $form = inject('$_veeObserver', undefined) as FormController | undefined;
const { errors, failedRules, value, validate: validateField, onInput, onBlur, reset, ...flags } = useField(
fieldName,
props.rules
props.rules,
{
form: $form
}
);

// let initialValue: any;
// eslint-disable-next-line prefer-const
let inputEvtName = '';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/index.ts
@@ -1,2 +1,2 @@
export { ValidationProvider } from './Provider';
// export { ValidationObserver } from './Observer';
export { ValidationObserver } from './Observer';
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Expand Up @@ -3,7 +3,7 @@ export { extend } from './extend';
// export { configure } from './config';
// export { setInteractionMode } from './modes';
// export { localize } from './localize';
export { ValidationProvider } from './components';
export { ValidationProvider, ValidationObserver } from './components';
export { normalizeRules } from './utils/rules';
export { useField } from './useField';
export { useForm } from './useForm';
Expand Down

0 comments on commit 4d18a65

Please sign in to comment.