diff --git a/packages/devtools/src/helpers.ts b/packages/devtools/src/helpers.ts index d21388d6..12320ab0 100644 --- a/packages/devtools/src/helpers.ts +++ b/packages/devtools/src/helpers.ts @@ -3,22 +3,20 @@ import { DevtoolsField, DevtoolsForm, EncodedNode, + FieldState, fieldToState, + FormState, formToState, - NODE_TYPE, NodeState, PathState, } from './types'; -import type { FormReturns, FormField } from '@core/index'; -import { ComponentInternalInstance, toValue } from 'vue'; +import { toValue } from 'vue'; import { getPluginColors } from './constants'; import { brandMessage, buildFormTree } from './utils'; import { setInPath } from '@core/utils/path'; import { getField, getForm } from './registry'; -export function buildFieldState( - state: Pick, -): CustomInspectorState { +export function buildFieldState(state: FieldState | PathState): CustomInspectorState { return { 'Field state': [ { key: 'errors', value: state.errors }, @@ -42,68 +40,56 @@ export function buildFieldState( }; } -export function buildFormState(form: FormReturns): CustomInspectorState { - const { isSubmitting, isTouched, isDirty, isValid, submitAttemptsCount, values, getErrors } = form; - +export function buildFormState(form: FormState): CustomInspectorState { return { 'Form state': [ { key: 'submitCount', - value: submitAttemptsCount.value, + value: form.submitCount, }, { key: 'isSubmitting', - value: isSubmitting.value, + value: form.isSubmitting, }, { key: 'touched', - value: isTouched(), + value: form.touched, }, { key: 'dirty', - value: isDirty(), + value: form.dirty, }, { key: 'valid', - value: isValid(), + value: form.valid, }, { key: 'currentValues', - value: values, + value: form.value, }, { key: 'errors', - value: getErrors(), + value: form.errors, }, ], }; } export function encodeNodeId(nodeState?: NodeState): string { - const type = (() => { - if (!nodeState) { - return 'unknown'; + const ff = (() => { + if (nodeState?.type !== 'field') { + return ''; } - if ('id' in nodeState) { - return 'form'; - } else if ('path' in nodeState) { - return 'field'; - } else { - return 'pathState'; - } + return nodeState.path; })(); - const ff = (() => { - if (!nodeState) { + const fp = (() => { + if (nodeState?.type !== 'path') { return ''; } - if ('path' in nodeState) { - return nodeState.path; - } else { - return ''; - } + return nodeState.path; })(); const form = (() => { @@ -111,64 +97,77 @@ export function encodeNodeId(nodeState?: NodeState): string { return ''; } - if ('id' in nodeState) { + if (nodeState.type === 'form') { return nodeState.id; } - if ('formId' in nodeState && nodeState.formId) { - return nodeState.formId; + if (nodeState.type === 'field' || nodeState.type === 'path') { + return nodeState.formId ?? ''; } return ''; })(); - const idObject = { f: form, ff, type } satisfies EncodedNode; + const idObject = { f: form, ff, fp, type: nodeState?.type ?? 'unknown' } satisfies EncodedNode; return btoa(encodeURIComponent(JSON.stringify(idObject))); } -export function decodeNodeId(nodeId: string): { - field?: FormField & { _vm?: ComponentInternalInstance | null }; - form?: FormReturns & { _vm?: ComponentInternalInstance | null }; - state?: PathState; - type?: NODE_TYPE; -} { +export function decodeNode(nodeId: string): NodeState | null { try { const idObject = JSON.parse(decodeURIComponent(atob(nodeId))) as EncodedNode; - const form = getForm(idObject.f); - // standalone field - if (!form && idObject.ff) { - const field = getField(idObject.ff); + if (idObject.type === 'field') { + if (!idObject.ff) { + return null; + } + + const field = getField(idObject.ff, idObject.f); if (!field) { - return {}; + return null; + } + + return fieldToState(field, idObject.f); + } + + if (idObject.type === 'path') { + if (!idObject.fp) { + return null; + } + + const form = getForm(idObject.f); + + // Should not happen, path should always be relative to a form + if (!form || '_isRoot' in form) { + return null; } return { - type: idObject.type, - field, + type: 'path', + path: idObject.fp, + formId: idObject.f, + touched: form.isTouched(idObject.fp), + dirty: form.isDirty(idObject.fp), + valid: form.isValid(idObject.fp), + errors: form.getErrors(idObject.fp), + value: form.getValue(idObject.fp), }; } - if (!form || '_isRoot' in form) { - return {}; - } + if (idObject.type === 'form') { + const form = getForm(idObject.f); - const field = form.fields.get(idObject.ff); - const state = formToState(form); - - return { - type: idObject.type, - form, - state, - field, - }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (err) { + if (!form || '_isRoot' in form) { + return null; + } + + return formToState(form); + } + } catch { console.error(brandMessage(`Failed to parse node id ${nodeId}`)); } - return {}; + return null; } /** @@ -218,7 +217,7 @@ export function mapFormForDevtoolsInspector(form: DevtoolsForm, filter?: string) setInPath(formTreeNodes, toValue(state.getPath() ?? ''), mapFieldForDevtoolsInspector(state, form)); }); - const { children } = buildFormTree(formTreeNodes); + const { children } = buildFormTree(formTreeNodes, [], form); return { id: encodeNodeId(formState), diff --git a/packages/devtools/src/init.ts b/packages/devtools/src/init.ts index f367cd8b..3ad76cbb 100644 --- a/packages/devtools/src/init.ts +++ b/packages/devtools/src/init.ts @@ -6,12 +6,19 @@ import { PathState } from './types'; import { buildFieldState, buildFormState, - decodeNodeId, + decodeNode, mapFieldForDevtoolsInspector, mapFormForDevtoolsInspector, } from './helpers'; import { getInspectorId } from './constants'; -import { getAllForms, getRootFields, registerField as _registerField, registerForm as _registerForm } from './registry'; +import { + getAllForms, + getRootFields, + registerField as _registerField, + registerForm as _registerForm, + getForm, + getField, +} from './registry'; import { brandMessage } from './utils'; let SELECTED_NODE: @@ -19,9 +26,8 @@ let SELECTED_NODE: | { type: 'field'; field: FormField } | null | { - type: 'pathState'; + type: 'path'; state: PathState; - form: FormReturns; } = null; /** @@ -120,33 +126,40 @@ async function installDevtoolsPlugin(app: App) { return; } - const { form, field, state, type } = decodeNodeId(payload.nodeId); - + const node = decodeNode(payload.nodeId); api.unhighlightElement(); + if (!node) { + return; + } + + if (node.type === 'form') { + payload.state = buildFormState(node); + const form = getForm(node.id); + + if (form && '_vm' in form) { + SELECTED_NODE = { type: 'form', form }; + api.highlightElement(form._vm); + } - if (form && type === 'form') { - payload.state = buildFormState(form); - SELECTED_NODE = { type: 'form', form }; - api.highlightElement(form._vm); return; } - if (state && type === 'pathState' && form) { - payload.state = buildFieldState(state); - SELECTED_NODE = { type: 'pathState', state, form }; + if (node.type === 'field') { + payload.state = buildFieldState(node); + const field = getField(node.path, node.formId); + + if (field) { + SELECTED_NODE = { type: 'field', field }; + api.highlightElement(field._vm); + } + return; } - if (field && type === 'field') { - payload.state = buildFieldState({ - errors: field.errors.value, - dirty: field.isDirty.value, - valid: field.isValid.value, - touched: field.isTouched.value, - value: field.fieldValue.value, - }); - SELECTED_NODE = { field, type: 'field' }; - api.highlightElement(field._vm); + if (node.type === 'path') { + payload.state = buildFieldState(node); + SELECTED_NODE = { type: 'path', state: node }; + return; } diff --git a/packages/devtools/src/types.ts b/packages/devtools/src/types.ts index c02c7028..b4c9ba05 100644 --- a/packages/devtools/src/types.ts +++ b/packages/devtools/src/types.ts @@ -1,8 +1,7 @@ import type { FormField, FormReturns } from '@core/index'; import { ComponentInternalInstance } from 'vue'; -// Base interface for state -export interface PathState { +interface BaseState { touched: boolean; dirty: boolean; valid: boolean; @@ -10,16 +9,27 @@ export interface PathState { value: TValue; } +// Base interface for state +export interface PathState extends BaseState { + type: 'path'; + path: string; + formId?: string; +} + // Form state extending base state -export interface FormState extends PathState { +export interface FormState extends BaseState { id: string; + isSubmitting: boolean; + submitCount: number; + type: 'form'; } // Field state extending base state -export interface FieldState extends PathState { +export interface FieldState extends BaseState { path: string; name: string; formId?: string; + type: 'field'; } // Union type for node state @@ -39,40 +49,30 @@ export type DevtoolsRootForm = { _isRoot: true; }; -// Devtools field type - -// Node types -export const NODE_TYPES = { - form: 'form', - field: 'field', - pathState: 'pathState', - unknown: 'unknown', -} as const; - -export type NODE_TYPE = keyof typeof NODE_TYPES; - // Encoded node type export type EncodedNode = { - type: NODE_TYPE; - ff: string; // form field path + type: NodeState['type'] | 'unknown'; + ff: string; // form field f: string; // form id + fp: string; // form path }; // Functions to convert form and field to state -export const formToState = (form: FormReturns): FormState => { - const errors = form.getErrors(); - +export function formToState(form: FormReturns): FormState { return { id: form.context.id, touched: form.isTouched(), dirty: form.isDirty(), - valid: errors.length === 0, + isSubmitting: form.isSubmitting.value, + submitCount: form.submitAttemptsCount.value, + valid: form.isValid(), value: form.values, - errors, + errors: form.getErrors(), + type: 'form', }; -}; +} -export const fieldToState = (field: FormField, formId?: string): FieldState => { +export function fieldToState(field: FormField, formId?: string): FieldState { return { path: field.getPath() ?? '', name: field.getName() ?? '', @@ -82,5 +82,6 @@ export const fieldToState = (field: FormField, formId?: string): FieldS value: field.fieldValue, errors: field.errors.value.map(error => error?.[0]), formId, + type: 'field', }; -}; +} diff --git a/packages/devtools/src/utils.ts b/packages/devtools/src/utils.ts index 62f65eb6..0323ba16 100644 --- a/packages/devtools/src/utils.ts +++ b/packages/devtools/src/utils.ts @@ -1,6 +1,8 @@ import { CustomInspectorNode } from '@vue/devtools-kit'; +import type { FormReturns } from '@core/index'; import { PathState } from './types'; import { isObject } from 'packages/shared/src/utils'; +import { encodeNodeId } from './helpers'; /** * A typed version of Object.keys @@ -14,7 +16,11 @@ export function isPathState(value: any): value is PathState { return value && 'path' in value && 'value' in value; } -export function buildFormTree(tree: any[] | Record, path: string[] = []): CustomInspectorNode { +export function buildFormTree( + tree: any[] | Record, + path: string[] = [], + form: FormReturns, +): CustomInspectorNode { const key = [...path].pop(); if ('id' in tree) { return { @@ -23,19 +29,32 @@ export function buildFormTree(tree: any[] | Record, path: string[] } as CustomInspectorNode; } + const fullPath = path.join('.'); + + const nodeState: PathState = { + formId: form.context.id, + dirty: form.isDirty(fullPath), + valid: form.isValid(fullPath), + errors: form.getErrors(fullPath), + value: form.getValue(fullPath), + touched: form.isTouched(fullPath), + type: 'path', + path: fullPath, + }; + if (isObject(tree)) { return { - id: `${path.join('.')}`, + id: encodeNodeId(nodeState), label: key || '', - children: Object.keys(tree).map(key => buildFormTree(tree[key] as any, [...path, key])), + children: Object.keys(tree).map(key => buildFormTree(tree[key] as any, [...path, key], form)), }; } if (Array.isArray(tree)) { return { - id: `${path.join('.')}`, + id: encodeNodeId(nodeState), label: `${key}[]`, - children: tree.map((c, idx) => buildFormTree(c, [...path, String(idx)])), + children: tree.map((c, idx) => buildFormTree(c, [...path, String(idx)], form)), }; } diff --git a/packages/playground/src/components/AllForm.vue b/packages/playground/src/components/AllForm.vue index 4e23f001..14181a98 100644 --- a/packages/playground/src/components/AllForm.vue +++ b/packages/playground/src/components/AllForm.vue @@ -34,16 +34,16 @@ const onSubmit = handleSubmit(payload => {

Fields Inside Form

- + - - - + + +