Vue3 json Schema form

Development log

Build the project

@ vue / cli 4.5.10

Custom: default+ts+unit test+no class-style+eslint+prettier+jest

Use prettier

Install Prettier-Code formatter vscode plugin

Create .prettierrc

{
    "semi": false,
    "singleQuote": true,
    "arrowParens": "always",
    "trailingComma": "all"
}

Configuration setting: Format On Save, it is recommended to save as workspace configuration

Some concepts

API in vue3
sfc’s new development method -0000

Please check the latest https://github.com/vuejs/rfcs/pull/227

<script setup>
  // imported components are also directly usable in template
  import Foo from './Foo.vue'
  import { ref } from 'vue'

  // write Composition API code just like in a normal setup()
  // but no need to manually return everything
  const count = ref(0)
  const inc = () => {
    count.value++
  }
</script>

<template>
  <Foo :count="count" @click="inc" />
</template>

Compiled Output

<script setup>
  import Foo from './Foo.vue'
  import { ref } from 'vue'
export default {
setup() {
const count = ref(1)
const inc = () => {
count.value++
}

   return  {
    Foo ,  // see note below
    count ,
    inc ,
   }
 } ,

}
</script>

<template>
<Foo :count="count" @click="inc" />
</template>

Note: the SFC compiler also extracts binding metadata from <script setup> and use it during template compilation. This is why the template can use Foo as a component here even though it’s returned from setup() instead of registered via components option.

Declaring Props and Emits

<script setup>
  import { defineProps, defineEmit } from 'vue'

  // expects props options
  const props = defineProps({
    foo: String,
  })
  // expects emits options
  const emit = defineEmits(['update', 'delete'])
</script>
component interface

####### defineConponent function

Mainly returns the type definition of the component, the specific implementation is very simple

// vue-next/package/runtime-core/src/apiDefineComponent.ts 
// various definitions
...
// implementation, close to no-op
export function defineComponent(options: unknown) {
  return isFunction(options) ? { setup: options, name: options.name } : options
}

Define props

...
<script lang="ts">
import { defineComponent, PropType } from "vue";

export  default  defineComponent ( { 
  name : "HelloWorld" , 
  props : { 
    // shorthand 
    msg : String , 
    // complete 
    name : { 
      type : String  as  PropType < string > , 
      required : true , 
    } , 
  } , 
} ) ; 
</ script >

The pit that require does not work when extracting common props definitions

...
<script lang="ts">
import { defineComponent, PropType } from "vue";

const  PropsType  =  { 
    msg : String , 
    name : { 
        type : String  as  PropType < string > , 
        // required 
        required : true , 
    } , 
// solution: add as const, manually tell ts that this object is read-only 
}  as  const

export  default  defineComponent ( { 
    name : "HelloWorld" , 
    props : PropsType , 
    mounted ( )  { 
        // The name prompt here may also be undefined 
        // Reason: {required: true} here is not tried correctly because ts does not know This object is read-only, and only read-only can allow {required: true} to be tested correctly by ts 
        // View the comment mentioned in the source defineComponent definition: 
        // the Readonly constraint allows TS to treat the type of { required: true} as constant instead of boolean. 
        // PropsOptions extends Readonly<ComponentPropsOptions>, 
        this . name 
    } 
} ) ; 
</ script >
h function

Equivalent to createElement in React

// main.ts
import { createApp, defineComponent, h } from "vue";
// import App from "./App.vue";

import  HelloWorld  from  './components 
/ 
HelloWorld.vue ' // ts verification does not pass the transposition of require's writing // import img from'./assets 
/logo.png 
' // eslint error: require is not allowed, cancel the current line Eslint check const  img  =  require ( './assets/logo.png' )  // eslint-disable-line

// Use the h function to simulate app files (sfc: single file component) to generate vue components. App files are almost compiled like this 
const  App  =  defineComponent ( { 
    render ( )  { 
        // h is equivalent to createElement in React for creating nodes 
        / / Native nodes use strings 
        // Parameters: node type, attributes, child nodes 
        return  h ( 'div' ,  { id : 'app' } ,  [ 
            h ( 'img' ,  { 
                alt : "Vue logo" , 
                // image It can’t be displayed, because the image address vue-loader in the template will be addressed. Here you need to import the image yourself 
                // src: "./assets/logo.png", 
                src : img , 
            }),
            h(HelloWorld, {
                msg: "Welcome to Your Vue.js + TypeScript App",
                name: "naxies",
            })
        ])
    }
})

createApp(App).mount("#app");
Source code

In fact, it is the package of createVNode. It is also possible to replace the following code h with createVNode

// vue-next/package/runtime-core/src/apiDefineComponent.ts 
// various definitions
...
// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // props without children
      return createVNode(type, propsOrChildren)
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

In addition to the three parameters like h, the createVNode parameters also have some optimized parameters, and vue-loader will make some optimizations through these

setup
// App.vue
 < template > 
  < img  alt =" Vue logo " src =" ./assets/logo.png " />
   < HelloWorld  msg =" Welcome to Your Vue.js + TypeScript App " name =" naxies " / >
   < h2 > {{state.age}}~ </ h2 > 
  <!-- It is not necessary. The value is because sfc will judge whether it is a ref currently --> 
  < h2 > ref {{ageRef}}~ </ h2 > 
  < h2 >computedAgeRef {{computedAgeRef}} ~ </ h2>
</template>

<script lang="ts">
import { defineComponent, reactive, ref, computed, watchEffect } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

export  default  defineComponent ( { 
  name : 'App' , 
  components : { 
    HelloWorld , 
  } , 
  mounted ( )  { 
    // The ref in this is a proxy or you don't need to write .value, it will automatically determine the 
    console . log ( this . ageRef ) 
  } , 
  // and the same data only once at initialization 
  Setup ( the props ,  { slots ,  attrs ,  EMIT } )  { 
    // returns: sfc returns the object 
    const  State  = reactive({
      age: 18
    })
    setInterval(() => {
      state.age += 1
    }, 1000)

    // Return {value: xxx} structure object, value is 
    const  ageRef  =  ref ( 18 ) 
    setInterval ( ( )  =>  { 
      ageRef . Value  +=  1 
    } ,  1000 )

    // computed
    const computedAgeRef = computed(() => {
      return ageRef.value + 2
    })

    // watchEffect: will be executed when all reactive function references and changes to ref 
    watchEffect ( ( )  =>  { 
      // assignment will be executed each time ageRef 
      Console . Log ( ageRef . Value ) ; 
    } )

    // Can't use {...state} to return, what is returned is not a responsive 
    return  { 
      state , 
      ageRef ,
      computedAgeRef
    }
  }
})
</script>

Setup returns the usage of render function

// main.ts
import { createApp, defineComponent, h, reactive, ref } from "vue";
// import App from "./App.vue";

import HelloWorld from './components/HelloWorld.vue'
const img = require('./assets/logo.png') // eslint-disable-line

// setup returns the writing of the render function ======================= 
const  App  =  defineComponent ( { 
    setup ( )  { 
        // You can also write other codes here 
        // Both reactive and ref are used to define reactive data. It is more recommended to define complex data types. ref is more recommended to define basic types. It can be simply understood that ref is a secondary packaging of reactive and access to data defined by ref One more time. value 
        const  state  =  reactive ( { 
            age : 18 
        } )

        const ageRef = ref(18)
        setInterval(() => {
            ageRef.value += 1
        }, 1000)

        // Return to the render function 
        return  ( )  =>  { 
            // Put this sentence outside the render function and the page display will not dynamically change according to the timer 
            // Because setup is only executed once, ageRefNum is always the initialized value 
            // reactive or The change of ref will re-execute this render function to form a dom tree, so the final value reading needs to be executed in render 
            const  ageRefNum  =  ageRef . value 
            return  h ( 'div' ,  { id : 'app' } ,  [ 
                h ( 'img' ,  { 
                    alt : "Vue logo" , 
                    src : img , 
                } ) , 
                h ( HelloWorld,  { 
                    msg : "Welcome to Your Vue.js + TypeScript App" , 
                    name : "naxies" , 
                } ) , 
                // You can get the value of the closure 
                h ( 'h2' ,  state . age ) , 
                h ( 'h2' ,  'ageRef '  +  ageRefNum ) 
            ] ) 
        } 
    } 
} )

createApp(App).mount("#app");
watch
// watching a getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// directly watching a ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// A watcher can also watch multiple sources at the same time using an array
const firstName = ref('');
const lastName = ref('');

watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues);
})

firstName.value = "John"; // logs: ["John",""] ["", ""]
lastName.value = "Smith"; // logs: ["John", "Smith"] ["John", ""]
jsx

https://github.com/vuejs/jsx-next

Install the plugin with:

npm install @vue/babel-plugin-jsx -D

Then add the plugin to .babelrc:

{
  "plugins": ["@vue/babel-plugin-jsx"]
}

Advantages: ts can be verified at compile time (ts cannot recognize the type exported in the vue file, it is a unified type), flexible to use (return and reuse html fragments through functions, etc.), and can use instructions

json-schema

Used to define json data, verify data, multi-terminal universal

Official draft: json-schema.org

ajv

js library of json-schema, https://ajv.js.org/

// or ESM/TypeScript import
// import Ajv from "ajv"
// Node.js require:
const Ajv = require("ajv").default
const addFormats = require("ajv-formats")
const localize = require('ajv-i18n');

// Simple 
let  schema  =  { 
    type : 'string' , 
    minLength : 10 , 
} 
// More complicated 
schema  =  { 
    type : 'object' , 
    properties : { 
        name : { 
            type : 'string' , 
            maxLength : 10 , 
        } , 
        age : { 
            type : 'number' , 
        } , 
        pets : { 
            type :'array' , 
            // The first way of defining 
            // items: { 
            // type:'string', 
            // }, 
            // The second kind of 
            items : [ 
                {  type : 'string' ,  } , 
                {  type : 'number ' ,  } 
            ] 
        } , 
        email : { 
            type : 'string' , 
            format : 'email' , 
        } , 
        testFormatProperty : { 
            type : 'string' , 
            format : 'testFormat' ,
        } , 
        testKeywordProperty : { 
            type : 'string' , 
            testKeyword : 'testKeyword' , 
            // provided by ajv-errors 
            // Any rule error will only display this error message 
            // errorMessage:'You are wrong again! ', 
            // Distinguish keyword setting error message 
            errorMessage : { 
                type : 'What you passed is not a string, don't you even recognize a string! ' , 
                testKeyword : 'Guess where you are wrong again! ' 
            } , 
        } 
    } , 
    required : [ 'name' ,  'age' ] , 
}

let data = 'naixes'
data = {
    name: 'naixes',
    age: 18,
    pets: ['egg core', 2],
    testFormatProperty: 'testFormat',
    testKeywordProperty: 'hello!',
}

// To use ajv-errors, you need to pass in {allErrors: true} 
const  ajv  =  new  Ajv ( { allErrors : true } )  // options can be passed, eg {allErrors: true} 
// version 7 and above formats need to install a plug-in separately 
addFormats ( ajv )

// 引入 ajv-errors 库
require ( 'ajv-errors' ) ( ajv )

// 定义 定义 format 
ajv . addFormat ( 'testFormat' ,  ( data )  =>  { 
    return  data  ===  'testFormat' 
} )

// Custom keyword 
ajv . addKeyword ( { 
    keyword : 'testKeyword' ,

    // Method 1: Call when validate 
    // validate: xx = (schema, data) => { 
    // console.log('schema', schema,'data', data); 
    // // Custom error message, You can also use the library to customize error information 
    // xx.errors = [ 
    // { 
    // keyword:'testKeyword', 
    // dataPath:'/testKeywordProperty', 
    // schemaPath:'#/properties/testKeywordProperty/testKeyword', 
    / / params: {}, 
    // message:'You are wrong! ' 
    //} 
    //] 
    // return data.length === 3 
    // },

    // Method 2: Call when compile 
    // compile: (sch, parentSchema) => { 
    // console.log('sch', sch,'parentSchema', parentSchema); 
    // // To return a function 
    // return () => true 
    // }, 
    // // The schema of the value received by this keyword 
    // metaSchema: (),

    // Method 3: Equivalent to combining multiple schema 
    macros : ( sch ,  parentSchema )  =>  { 
        // console.log('sch', sch,'parentSchema', parentSchema); 
        // To return a schema 
        return  { 
            minLength : 10 , 
        } 
    } , 
    metaSchema : { } ,

    // Will overwrite the custom error message in Method 1 
    errors : false , 
} )

const validate = ajv.compile(schema)
const valid = validate(data)
if (!valid) {
    localize.zh(validate.errors)
    console.log(validate.errors)
}

installation:npm i ajv

####### Formats

format: some commonly used validation rules, only for string and number types

installation:npm i ajv-formats

From version 7 Ajv does not include formats defined by JSON Schema specification - these and several other formats are provided by ajv-formats plugin.

To add all formats from this plugin:

import Ajv from "ajv"
import addFormats from "ajv-formats"

const  ajv  =  new  Ajv ( ) 
addFormats ( ajv )
  • date: full-date according to RFC3339.
  • time: time with optional time-zone.
  • date-time: date-time from the same source (time-zone is mandatory).
  • duration: duration from RFC3339
  • uri : full URI.
  • uri-reference: URI reference, including full and relative URIs.
  • uri-template: URI template according to RFC6570
  • url (deprecated): URL record.
  • email: email address.
  • hostname: host name according to RFC1034.
  • ipv4: IP address v4.
  • ipv6: IP address v6.
  • regex: tests whether a string is a valid regular expression by passing it to RegExp constructor.
  • uuid: Universally Unique IDentifier according to RFC4122.
  • json-pointer: JSON-pointer according to RFC6901.
  • relative-json-pointer: relative JSON-pointer according to this draft.

Custom format

ajv.addFormat (name: string, format: Format): Ajv

type Format =
  | true // to ignore this format (and pass validation)
  | string // will be converted to RegExp
  | RegExp
  | (data: string) => boolean
  | Object // format definition (see below and in types)

Add format to validate strings or numbers.

If object is passed it should have properties validatecompare and async:

interface FormatDefinition { // actual type definition is more precise - see types.ts
  validate: string | RegExp | (data: number | string) => boolean | Promise<boolean>
  compare: (data1: string, data2: string): number // an optional function that accepts two strings
    // and compares them according to the format meaning.
    // This function is used with keywords `formatMaximum`/`formatMinimum`
    // (defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package).
    // It should return `1` if the first value is bigger than the second value,
    // `-1` if it is smaller and `0` if it is equal.
  async?: true // if `validate` is an asynchronous function
  type?: "string" | "number" // "string" is default. If data type is different, the validation will pass.
}

Formats can be also added via formats option.

####### keywords

Four ways to customize keywords

ajv.addKeyword({
  keyword: "constant",
  validate: (schema, data) =>
    typeof schema == "object" && schema !== null ? deepEqual(schema, data) : schema === data,
  errors: false,
})

const schema = {
  constant: 2,
}
const validate = ajv.compile(schema)
console.log(validate(2)) // true
console.log(validate(3)) // false

const schema = {
  constant: {foo: "bar"},
}
const validate = ajv.compile(schema)
console.log(validate({foo: "bar"})) // true
console.log(validate({foo: "baz"})) // false
ajv.addKeyword({
  keyword: "range",
  type: "number",
  compile([min, max], parentSchema) {
    return parentSchema.exclusiveRange === true
      ? (data) => data > min && data < max
      : (data) => data >= min && data <= max
  },
  errors: false,
  metaSchema: {
    // schema to validate keyword value
    type: "array",
    items: [{type: "number"}, {type: "number"}],
    minItems: 2,
    additionalItems: false,
  },
})

const schema = {
  range: [2, 4],
  exclusiveRange: true,
}
const validate = ajv.compile(schema)
console.log(validate(2.01)) // true
console.log(validate(3.99)) // true
console.log(validate(2) )  // false 
console . log ( validate ( 4 ) )  // false
ajv.addKeyword({
  keyword: "range",
  type: "number",
  macro: ([minimum, maximum]) => ({minimum, maximum}), // schema with keywords minimum and maximum
  // metaSchema: the same as in the example above
})

####### Error message

Display Chinese error message:

Install ajv-i18n

var Ajv = require('ajv'); // version >= 2.0.0
var localize = require('ajv-i18n');

// option `i18n` is required for this package to work
var ajv = Ajv({ allErrors: true });
var validate = ajv.compile(schema);
var valid = validate(data);

if (!valid) {
    // ru for Russian
    localize.ru(validate.errors);
    // string with all errors and data paths
    console.log(ajv.errorsText(validate.errors, { separator: '\n' }));
}

Custom error message

...
 // Custom keyword 
ajv . AddKeyword ( { 
    keyword : 'testKeyword' ,

    // Method 1: When calls validate 
    validate : XX  =  ( Schema ,  Data )  =>  { 
        Console . Log ( 'Schema' ,  Schema ,  'Data' ,  Data ) ; 
        // custom error message 
        XX . Errors  =  [ 
            { 
                keyword : 'testKeyword' , 
                dataPath : '/testKeywordProperty' , 
                schemaPath : '#/properties/testKeywordProperty/testKeyword' , 
                params : { }, 
                message : 'You are wrong! ' 
            } 
        ] 
        return  data . length  ===  3 
    } ,

    // overrides custom error messages 
    // errors: false, 
} ) 
. . .

: : Ajv-errors

const Ajv = require("ajv").default
const ajv = new Ajv({allErrors: true})
// Ajv option allErrors is required
require("ajv-errors")(ajv /*, {singleError: true} */)

const schema = {
  type: "object",
  required: ["foo"],
  properties: {
    foo: {type: "integer"},
  },
  additionalProperties: false,
  errorMessage: "should be an object with an integer property foo only",
}

const validate = ajv.compile(schema)
console.log(validate({foo: "a", bar: 2})) // false
console.log(validate.errors) // processed errors

Component definition and interface

Props
<JsonSchemaForm
	schema={schema}   
    value={value}
    locale={locale}
    onChange={handleChange}
    contextRef={someRef}
    uiSchema={uiSchema}
></JsonSchemaForm>
schema

json schema object, used to define the data, but also the basis for us to define the form

value

The data result of the form, you can change the value from the outside. When the form is edited, the onChangevalue will be revealed through

It should be noted that because Vue uses variable data, if we change valuethe object address every time the data changes , the entire form will need to be re-rendered, which will cause performance degradation. From a practical point of view, the internal modification of the value of the field of the object we passed in will basically not have any side effects, so we will use this method to implement it. In other words, if it valueis an object, then JsonSchemaFormthe value modified from the inside will not change the valueobject itself. We will still trigger onChange, because the user may need to perform some actions after the form changes.

onChange

The callback method will be triggered when there is any change in the form value, and the new value will be returned

locale

Language, use the language of the ajv-i18nspecified error message

contextRef

You need to pass in a vue3 Refobject, we will mount the doValidatemethod on this object , you can pass

const yourRef = ref({})

onMounted(() => {
  yourRef.value.doValidate()
})

<JsonSchemaForm contextRef={yourRef} />

In this way, the form is actively verified.

uiSchema

Make some customizations to the display of the form, the types are as follows:

export interface VueJsonSchemaConfig {
  title?: string
  descrription?: string
  component?: string
  additionProps?: {
    [key: string]: any
  }
  withFormItem?: boolean
  widget?: 'checkbox' | 'textarea' | 'select' | 'radio' | 'range' | string
  items?: UISchema | UISchema[]
}
export interface UISchema extends VueJsonSchemaConfig {
  properties?: {
    [property: string]: UISchema
  }
}
vue-jss

The css in js library is based on the secondary development of jss

npm i vue-jss jss jss-preset-default

monaco-editor

Code editor

/* eslint no-use-before-define: 0 */

import { defineComponent, ref, onMounted, watch, onBeforeUnmount, shallowReadonly, shallowRef } from 'vue'

import * as Monaco from 'monaco-editor'

import type { PropType, Ref } from 'vue'
import { createUseStyles } from 'vue-jss'

// 返回一个方法
const useStyles = createUseStyles({
  container: {
    border: '1px solid #eee',
    display: 'flex',
    flexDirection: 'column',
    borderRadius: 5
  },
  title: {
    backgroundColor: '#eee',
    padding: '10px 0',
    paddingLeft: 20,
  },
  code: {
    flexGrow: 1
  }
})

export default defineComponent({
  props: {
    code: {
      type: String as PropType<string>,
      required: true
    },
    onChange: {
      type: Function as PropType<(value: string, event: Monaco.editor.IModelContentChangedEvent) => void>,
      required: true
    },
    title: {
      type: String as PropType<string>,
      required: true
    }
  },
  setup(props) {
    // must be shallowRef, if not, editor.getValue() won't work
    const editorRef = shallowRef()

    const containerRef = ref()

    let _subscription: Monaco.IDisposable | undefined
    let __prevent_trigger_change_event = false // eslint-disable-line

    onMounted(() => {
      const editor = editorRef.value = Monaco.editor.create(containerRef.value, {
        value: props.code,
        language: 'json',
        formatOnPaste: true,
        tabSize: 2,
        minimap: {
          enabled: false,
        },
      })

      _subscription = editor.onDidChangeModelContent((event) => {
        console.log('--------->', __prevent_trigger_change_event) // eslint-disable-line
        if (!__prevent_trigger_change_event) { // eslint-disable-line
          props.onChange(editor.getValue(), event);
        }
      });
    })

    onBeforeUnmount(() => {
      if (_subscription)
        _subscription.dispose()
    })

    watch(() => props.code, (v) => {
      const editor = editorRef.value
      const model = editor.getModel()
      if (v !== model.getValue()) {
        editor.pushUndoStop();
        __prevent_trigger_change_event = true // eslint-disable-line
        // pushEditOperations says it expects a cursorComputer, but doesn't seem to need one.
        model.pushEditOperations(
          [],
          [
            {
              range: model.getFullModelRange(),
              text: v,
            },
          ]
        );
        editor.pushUndoStop();
        __prevent_trigger_change_event = false // eslint-disable-line
      }
      // if (v !== editorRef.value.getValue()) {
      //   editorRef.value.setValue(v)
      // }
    })

    const classesRef = useStyles()

    return () => {

      const classes = classesRef.value

      return (
        <div class={classes.container}>
          <div class={classes.title}><span>{props.title}</span></div>
          <div class={classes.code} ref={containerRef}></div>
        </div>
      )
    }
  }
})

Plug-in: monaco-editor-webpack-plugin

  const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')

module.exports = {
  chainWebpack(config) {
      config.plugin('monaco').use(new MonacoWebpackPlugin())
  },
}

Display item

app.tsx

import { defineComponent, reactive, ref, Ref, watchEffect } from "vue";
import {createUseStyles} from 'vue-jss'

import MonacoEditor from './components/MonacoEditor'
import demos from './demos'
import SchemaForm from '../lib'

// TODO:在lib中export
type Schema = any
type UISchema = any

function jsonToString(data: any) {
    return JSON.stringify(data, null, 2)
}

// 样式
const useStyles = createUseStyles({
    ...
})

export  default  defineComponent ( { 
    setup ( )  { 
        // Record the currently selected demo 
        const  selectedRef : Ref < number >  =  ref ( 0 )

        const  demo : { 
            schema : Schema | null , 
            data : any , 
            uiSchema : UISchema | null , 
            schemaCode : string , 
            dataCode : string , 
            uiSchemaCode : string , 
        }  =  reactive ( { 
            schema : null , 
            data : { } , 
            uiSchema : { } , 
            schemaCode: '' , 
            dataCode : '' , 
            uiSchemaCode : '' , 
        } )

        watchEffect(() => {
          const index = selectedRef.value
          const d = demos[index]
          demo.schema = d.schema
          demo.data = d.default
          demo.uiSchema = d.uiSchema
          demo.schemaCode = jsonToString(d.schema)
          demo.dataCode = jsonToString(d.default)
          demo.uiSchemaCode = jsonToString(d.uiSchema)
        })

        const handleChange = (v: any) => {
          demo.data = v
          demo.dataCode = jsonToString(v)
        }

        // 工厂函数
        const handleCodeChange = (
          field: 'schema' | 'data' | 'uiSchema',
          value: string
        ) => {
            let json: any
            try {
                json = JSON.parse(value)
                demo[field] = json
                ;(demo as any)[`${field}Code`] = value
            } catch (err){
                console.log(err);
            }
        }
        const handleSchemaChange = (v: string) => handleCodeChange('schema', v)
        const handleDataChange = (v: string) => handleCodeChange('data', v)
        const handleUISchemaChange = (v: string) => handleCodeChange('uiSchema', v)

        const classesRef = useStyles()

        return () => {
            const classes = classesRef.value
            const selected = selectedRef.value

            return (        // <StyleThemeProvider>
              // <VJSFThemeProvider theme={theme as any}>
              <div class={classes.container}>
                <div class={classes.menu}>
                  <h1>Vue3 JsonSchema Form</h1>
                  <div>
                    {demos.map((demo, index) => (
                      <button
                        class={{
                          [classes.menuButton]: true,
                          [classes.menuSelected]: index === selected,
                        }}
                        onClick={() => (selectedRef.value = index)}
                      >
                        {demo.name}
                      </button>
                    ))}
                  </div>
                </div>
                <div class={classes.content}>
                  <div class={classes.code}>
                    <MonacoEditor
                      code={demo.schemaCode}
                      class={classes.codePanel}
                      onChange={handleSchemaChange}
                      title="Schema"
                    />
                    <div class={classes.uiAndValue}>
                      <MonacoEditor
                        code={demo.uiSchemaCode}
                        class={classes.codePanel}
                        onChange={handleUISchemaChange}
                        title="UISchema"
                      />
                      <MonacoEditor
                        code={demo.dataCode}
                        class={classes.codePanel}
                        onChange={handleDataChange}
                        title="Value"
                      />
                    </div>
                  </div>
                  <div class={classes.form}>
                    <SchemaForm
                      schema={demo.schema}
                      onChange={handleChange}
                      value={demo.data}
                    />
                    {/* <SchemaForm
                      schema={demo.schema!}
                      uiSchema={demo.uiSchema!}
                      onChange={handleChange}
                      contextRef={methodRef}
                      value={demo.data}
                    /> */}
                  </div>
                </div>
              </div>
              // </VJSFThemeProvider>
              // </StyleThemeProvider>
            )
        }
    }
})

Component development

SchemaItem: Distribute different types of schemas

fields/xxxField: specific implementation

Simple node rendering
<template>
    <input type="text" :value="value" @input="handleChange" />
</template>

<script lang='ts' setup>
import {defineProps} from 'vue'

import {FieldPropsDefine, Schema} from '../types'

// Using FieldPropsDefine directly will report an error 
const props =  defineProps ({ ... FieldPropsDefine })

const handleChange = (e: any) => {
    props.onChange(e.target.value)
}
</script>
Complex node rendering
Object

Do not refer to components circularly, it is difficult to find errors, use plug-ins to remindnpm i -D circular-dependency-plugin

Use provide to solve the problem of circular references

Source code: apiInject.ts

// /packages/runtime-core/src/apiInject.ts
import { isFunction } from '@vue/shared'
import { currentInstance } from './component'
import { currentRenderingInstance } from './componentRenderUtils'
import { warn } from './warning'

export interface InjectionKey<T> extends Symbol {}

export  function  provide < T > ( key : InjectionKey < T > | string | number ,  value : T )  { 
	// Determine whether it is in the component rendering process, that is, you can only use 
    if  ( ! currentInstance )  { 
        if  ( __DEV__ )  { 
            warn ( `provide() can only be used inside setup().` ) 
        } 
    }  else  { 
        let  provides  =  currentInstance .provides 
        // by default an instance inherits its parent's provides object 
        // but when it needs to provide values ​​of its own, it creates its 
        // own provides object using parent provides object as prototype. 
        // this way in `inject` we can simply look up injections from direct 
        // parent and let the prototype chain do the work. 
        // By default, the instance inherits the Provides object of its parent object 
        // But when it needs to provide its own value, it will use the parent provided object as prototype to create objects 
        // in this way, the `inject`, we can look directly from the parent object directly injected into the query so that the prototype chain works 
        const  parentProvides  = 
              the currentInstance of . parent  &&  the currentInstance of . parent . the Provides 
        IF  ( parentProvides  == = provides) {
            provides = currentInstance.provides = Object.create(parentProvides)
        }
        // TS doesn't allow symbol as index type
        provides[key as string] = value
    }
}

...
export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // fallback to `currentRenderingInstance` so that this can be called in
  // a functional component
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the intance is at root
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    if (provides && (key as string | symbol) in provides) {
      // TS doesn't allow symbol as index type
      return provides[key as string]
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue()
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

Optimization: Encapsulate common logic

Array
/** 
* Three cases 
* 
* Single-type arrays, with no length limitation by default, are all the same type 
* { 
* items: {type:'string'} 
*} 
* Fixed length, which types are respectively 
* { 
* items: [ 
* {type:'string'}, 
* {type:'numer'} 
*] 
*} 
* enum means optional 
items * { 
* items: {type:'string', enum: ['1', '2']} 
*} 
*/

Fixed-length array rendering

Single type array rendering

Contains optional array rendering

Download Details:

Author: Naixes

Source Code: https://github.com/Naixes/vue3-json-schema-form

#vue #vuejs #javascript

Vue3 json Schema form
17.75 GEEK