# Extending Schema Components In addition to the native html tags, developers can also adapt more custom components to enrich the Schema component library. Common methods used to extend components are - [connect](https://react.formilyjs.org/api/shared/connect) to access third-party components without intrusion, generally used to adapt field components, and [mapProps](https://react.formilyjs.org/api/shared/map-props)[, mapReadPretty](https://react.formilyjs.org/api/shared/map-read-pretty) are used with - [observer](https://react.formilyjs.org/api/shared/observer) when the component uses an observable object internally and you want the component to respond to changes to the observable object ## The simplest extension Register a ready-made React component directly into it. ```tsx /** * defaultShowCode: true */ import React from 'react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; const Hello = () => <h1>Hello, world!</h1>; const schema = { type: 'void', name: 'hello', 'x-component': 'Hello', }; export default () => { return ( <SchemaComponentProvider components={{ Hello }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` ## Access to third-party components via connect ```tsx /** * defaultShowCode: true */ import React from 'react'; import { Input } from 'antd' import { connect, mapProps, mapReadPretty } from '@formily/react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; const ReadPretty = (props) => { return <div>{props.value}</div> }; const SingleText = connect( Input, mapProps((props, field) => { return { ...props, suffix: '后缀', } }), mapReadPretty(ReadPretty), ); const schema = { type: 'object', properties: { t1: { type: 'string', default: 'hello t1', 'x-component': 'SingleText', }, t2: { type: 'string', default: 'hello t2', 'x-component': 'SingleText', 'x-pattern': 'readPretty', }, } }; export default () => { return ( <SchemaComponentProvider components={{ SingleText }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` ## Using observer response data ```tsx /** * defaultShowCode: true */ import React from 'react'; import { Input } from 'antd'; import { connect, observer, useForm } from '@formily/react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; const SingleText = connect(Input); const UsedObserver = observer((props) => { const form = useForm(); return <div>UsedObserver: {form.values.t1}</div> }); const NotUsedObserver = (props) => { const form = useForm(); return <div>NotUsedObserver: {form.values.t1}</div> }; const schema = { type: 'object', properties: { t1: { type: 'string', 'x-component': 'SingleText', }, t2: { type: 'string', 'x-component': 'UsedObserver', }, t3: { type: 'string', 'x-component': 'NotUsedObserver', }, } }; const components = { SingleText, UsedObserver, NotUsedObserver }; export default () => { return ( <SchemaComponentProvider components={components}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` ## Nested Schema - `props.children` nesting for void and object types properties, see [nesting of void and object-type schema](#void-and-object-type-schema-nesting) for examples - `<RecursionField />` custom nesting, for all types, see [nesting of array-type schema](#nesting-of-array-type-schema) Note: - `properties` of schema other than void and object types cannot be rendered directly by `props.children`, but nesting can be resolved using `<RecursionField />` - Only schema of type void and object can be used with onlyRenderProperties ```tsx | pure <RecursionField schema={schema} onlyRenderProperties /> ``` ### Nesting of void and object type schema The properties node can be adapted directly via props.children ```tsx /** * defaultShowCode: true */ import React from 'react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; // Hello 组件适配了 children,可以嵌套 properties 了 const Hello = (props) => <h1>Hello, {props.children}!</h1>; const World = () => <span>world</span>; const schema = { type: 'object', name: 'hello', 'x-component': 'Hello', properties: { world: { type: 'string', 'x-component': 'World', }, }, }; export default () => { return ( <SchemaComponentProvider components={{ Hello, World }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` Comparison of rendering results by property type ```tsx import React from 'react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; const Hello = (props) => <h1>Hello, {props.children}!</h1>; const World = () => <span>world</span>; const schema = { type: 'object', properties: { title1: { type: 'void', 'x-content': 'Void schema,渲染 properties', }, void: { type: 'void', name: 'hello', 'x-component': 'Hello', properties: { world: { type: 'void', 'x-component': 'World', }, }, }, title2: { type: 'void', 'x-content': 'Object schema,渲染 properties', }, object: { type: 'object', name: 'hello', 'x-component': 'Hello', properties: { world: { type: 'string', 'x-component': 'World', }, }, }, title3: { type: 'void', 'x-content': 'Array schema,不渲染 properties', }, array: { type: 'array', name: 'hello', 'x-component': 'Hello', properties: { world: { type: 'string', 'x-component': 'World', }, }, }, title4: { type: 'void', 'x-content': 'String schema,不渲染 properties', }, string: { type: 'string', name: 'hello', 'x-component': 'Hello', properties: { world: { type: 'string', 'x-component': 'World', }, }, }, } }; export default () => { return ( <SchemaComponentProvider components={{ Hello, World }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` ### Nesting of array type schema Custom nesting can be solved with `<RecursionField />` #### Array element is a string or number ```tsx import React from 'react'; import { useFieldSchema, Schema, RecursionField, useField, observer, connect } from '@formily/react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; const useValueSchema = () => { const schema = useFieldSchema(); return schema.reduceProperties((buf, s) => { if (s['x-component'] === 'Value') { return s; } return buf; }); }; const ArrayList = observer((props) => { const field = useField(); const schema = useValueSchema(); return ( <> String Array <ul> {field.value?.map((item, index) => { // Only one element return <RecursionField name={index} schema={schema} /> })} </ul> </> ); }); const Value = connect((props) => { return <li>value: {props.value}</li> }); const schema = { type: 'object', properties: { strArr: { type: 'array', default: [1, 2, 3], 'x-component': 'ArrayList', properties: { value: { type: 'number', 'x-component': 'Value', }, } }, } }; export default () => { return ( <SchemaComponentProvider components={{ ArrayList, Value }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` #### When the Array element is an Object ```tsx import React from 'react'; import { useFieldSchema, Schema, RecursionField, useField, observer, connect } from '@formily/react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; const ArrayList = observer((props) => { const field = useField(); const schema = useFieldSchema(); // The schema of array type cannot be onlyRenderProperties and needs to be converted to object type const objSchema = new Schema({ type: 'object', properties: schema.properties, }); return ( <ul> {field.value?.map((item, index) => { // When the Array element is an Object return ( <RecursionField name={index} schema={objSchema} onlyRenderProperties /> ) })} </ul> ); }); const Value = connect((props) => { return <li>value: {props.value}</li> }); const schema = { type: 'object', properties: { objArr: { type: 'array', default: [ { value: 't1' }, { value: 't2' }, { value: 't3' }, ], 'x-component': 'ArrayList', properties: { value: { type: 'number', 'x-component': 'Value', }, } } } }; export default () => { return ( <SchemaComponentProvider components={{ ArrayList, Value }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ``` #### Tree structure data ```tsx import { ArrayField } from '@formily/core'; import { connect, ISchema, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import { Table, TableColumnType } from 'antd'; import React from 'react'; const ArrayTable = observer((props: any) => { const { rowKey } = props; const field = useField<ArrayField>(); const schema = useFieldSchema(); const columnSchemas = schema.reduceProperties((buf, s) => { if (s['x-component'] === 'ArrayTable.Column') { buf.push(s); } return buf; }, []); const columns = columnSchemas.map((s) => { return { render: (value, record) => { return <RecursionField name={record.__path} schema={s} onlyRenderProperties />; }, } as TableColumnType<any>; }); return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />; }); const Value = connect((props) => { return <li>value: {props.value}</li>; }); const schema: ISchema = { type: 'object', properties: { objArr: { type: 'array', default: [ { __path: '0', id: 1, value: 't1' }, { __path: '1', id: 2, value: 't2', children: [ { __path: '1.children.0', id: 5, value: 't5', parentId: 2, }, ], }, { __path: '2', id: 3, value: 't3', children: [ { __path: '2.children.0', id: 4, value: 't4', parentId: 3, children: [ { __path: '2.children.0.children.0', id: 6, value: 't6', parentId: 4, }, { __path: '2.children.0.children.1', id: 7, value: 't7', parentId: 4, }, ], }, ], }, ], 'x-component': 'ArrayTable', 'x-component-props': { rowKey: 'id', }, properties: { c1: { type: 'void', 'x-component': 'ArrayTable.Column', properties: { value: { type: 'string', 'x-component': 'Value', }, }, }, }, }, }, }; export default () => { return ( <SchemaComponentProvider components={{ ArrayTable, Value }}> <SchemaComponent schema={schema} /> </SchemaComponentProvider> ); }; ```