@appscode/ui-builder
Version:
## Motivation
292 lines (223 loc) • 10.3 kB
Markdown
# Modularity in UI Builder
Modularity in UI Builder is achieved by introducing a new type of _form element_ called `reusable-element`.
## Reusable Element
The _Reusable Element_ is basically a _Singel Step Form_ type Form elemnt. **It supports all the properties of _Single Step Form_ along with some special ones**. The json exaple for this element is given below:
```json
{
"type": "reusable-element",
"alias": "module_alias",
"dataContext": {
"studentName": {
"$ref": "schema#/properties/name"
}
},
"functionCallbacks": {
"updateName": {
"$ref": "functions#/updateName"
}
},
"chart": {
"name": "chart_name",
"version": "chart_version"
},
"schema": {
"$ref": "schema#/properties/school"
},
"moduleResolver": "fetchSchoolJsons"
}
```
Let's explain each of the properties one by one.
### Type [REQUIRED]
The type property is a common _form element_ property. It's used to distinguish between different types for form elements. For _reusable elements_ we have to specify the `type` property as `"reusable-element"`.
### Alias [REQUIRED]
Alias is a special property for _reusable form element_. It is used by the UI Builder to differentiate between different reusable elements in same `ui.json`.
Alias is used to scope the associated `language.json` and `function.js` inside the reusable component and makes sure those does not interfere or colflict with the `language.json` and `function.js` of the parent/caller.
So, make sure to give an unique and suitable _alias_ to your _reusable element_.
### Schema [REQUIRED]
The Schema is a required property. Just like any other _form element_, the schema property consists of a `$ref` attribute which points to the path in the `schema.json` for which form will be fetched and generated by the _reusable element_.
### Module Resolver [REQUIRED]
The Module Resolver property referes to a function name from `function.js` file. This function is responsible for fetching the module's `ui.json`, `language.json` and `function.js`.
This is a user defined function. Just like any other function inside `function.js`, it will have access to the `context` object. The return value of this function has to be of the following format:
```js
{
ui: {...},
language: {...},
functions: {...}
}
```
here,
- `ui` property should contain the `ui.json` for the module
- `language` - property should contain the `language.json` for the module
- `functions` - property should contain the functions from the module's `function.js`.
An example of a `moduleResolver` is given below:
```js
async function fetchSchoolJsons({ loadLocalFile, loadLocalJsModule }) {
let ui = {};
let language = {};
let functions = {};
try {
ui = await loadLocalFile("/school-json/ui.json");
language = await loadLocalFile("/school-json/language.json");
functions = await loadLocalJsModule("/school-json/function.js");
} catch (e) {
console.log(e);
}
return {
ui,
language,
functions,
};
}
```
### Chart
The Chart property is used to define properties for fetching reusable `Moduler JSONs` and `function.js` via api call that can be passed to the _reusable element_ from outside.
```json
{
"chart": {
"name": "chart_name",
"version": "chart_version"
}
}
```
- `name` - You can pass the chart name to _reusable element_.
- `version` - You can pass the chart version to _reusable element_.
### Data Context
The Data Context property is another form element property that is unique to the _reusable form element_. Normally, the function from the module's `function.js` does not have access to `model` properties outside of it's scope.
The Data Context property is used to define properties that can be passed to the _reusable element_ from outside. Example:
```json
{
"dataContext": {
"schoolType": {
"$ref": "discriminator#/properties/schoolType"
},
"schoolName": {
"$ref": "schema#/properties/name"
},
"studentName": {
"$ref": "data#/properties/studentName"
}
}
}
```
Each property defined inside the Data Context must have a `$ref` attribute which will contain the reference to the value which we want to pass. You can reference values from three different sources:
- `model` - You can reference any value from the model to which the caller of the _reusable element_ has access to. eg: `"schema#/properties/name"`.
- `discriminator` - You can reference any value from the discriminator object of the caller element. The discriminator object should be defined on the _single step form_ of which the caller element is part of. eg: `"discriminator#/properties/schoolType"`
- `parentDataContext` - If the caller/parent element happens to be inside another reusable element itself, you can refer to any value inside it's `dataContext` also. This means that, we can chain `dataContext` for nested reusable elements to pass a value from root to a leaf.
### Function Callbacks
The Function Callbacks property is used to pass functions to the reusable elements which can be called from functions inside the reusable element's `function.js`.
This property is used to pass functions from parent `function.js` to the reusable element. The reusable element can pass functions to it's child reusable element from it's own `function.js` or from it's `functionCallbacks`. This, like `dataContext`, can be chained from reusable element to element. Example:
```json
{
"functionCallbacks": {
"updateName": {
"$ref": "functions#/updateName"
}
}
}
```
Each property defined inside the Function Callbacks must have a `$ref` attribute which will denote the reference to the function which we want to pass. You can reference functions from two different sources:
- `function.js` - You can reference any function from the caller's `function.js`. eg: `"functions#/updateName"`.
- `functionCallbacks` - You can reference any function from the caller's `functionCallbacks` if the caller happens to be a _reusable element_ itself. The caller's `functionCallbacks` ultimately contains passed functions from it's parents. This means that, we can chain `functionCallbacks` for nested reusable elements to pass a function from root to a leaf. To pass the functions from parents to it's child, reference will be `parentFunctions` instead of `functions`. eg. `"parentFunctions#/updateName"`
## Accessing and Modifying values outside of module's scope
Normally, a _Reusable element_ cannot access nor modify values outside of it's scope. It's scope is kept confined inside the part of the model that it's schema refers to.
Generally, we want the moduler elements to be independent from outside influence. But, what if we need to access and modify values from outside of the scope? Luckily, we have `dataContext` and `functionCallbacks` properties. We can use these properties to access and modify values from outside.
`dataContext` allows the parent to grant access to values in it's scope, to it's child reusable element.
`functionCallbacks` allows the parent to pass functions which have access to values in it's scope, to it's child reusable element. The child reusable element can call these functions from it's own funciton.js. The parent's function has access to the parent's model, so it can modify them. And as it is passed to the child reusable element, it actually allows the child element to call the function and modify the parent's model.
An example of a function in the parent element that allows data to be modified from the child reusable element is given below:
```js
// parent function js
function updateName({ commit }, value) {
commit("wizard/model$update", {
path: "/name",
value,
force: true,
});
}
```
This `updateName` function allows the name to be modified which is in the parent's scope.
This update function is passed to the child reusable element via `functionContext`
```json
{
"type": "reusable-element",
...
...
"functionCallbacks": {
"updateStudentName": {
"$ref": "functions#/updateName"
}
},
...
...
}
```
Now, in the child reusable element, this function named `updateStudentName`, can the called from a function inside `function.js`.
```js
// child's (reusable-element) function js
function onSubjectsChange({ reusableElementCtx, model, getValue }) {
const { functionCallbacks } = reusableElementCtx || {};
const { updateStudentName } = functionCallbacks || {};
const subjects = getValue(model, "/");
updateStudentName(subjects.pop()); // <=== here is the function call
}
```
You can see from the above example, that the child element is able to modify the `name` property from outside of it's context via `updateStudentName` function.
## Moduler JSONs
Each module contains `ui.json`, `language.json` and `function.js` files.
The schema and format for the `language.json` and `function.js` files are same as before. The schema for the `ui.json` file is slightly different. There is no `multi-step-form` here.
### `ui.json`
The `ui.json` file can contain either an Array of form elemnts or an Object describing a single form element.
If it's an array, each item of the array should be a valid _form elemnt_ type object. eg:
```json
[
{
"type": "radio",
"schema": {
"$ref": "schema#/properties/schoolType"
},
...
...
},
{
"type": "input",
...
...
},
{
"type": "reusable-element",
"schema": {
"$ref": "schema#/properties/grade"
},
...
...
}
]
```
If it's an object, the object must describe a valid _form element_. eg:
```json
{
"type": "single-step-form",
"schema": {
"$ref": "schema#/"
},
"discriminator": {
"schoolType": {
"type": "string"
}
},
"elements": [
{
"type": "radio",
...
...
},
...
...
]
}
```
### `functions.js`
The functions in the module's `function.js` has access to an extra context property which is `reusableElementCtx`.
This `reusableElementCtx` has three properties
- `alias` - It is the alias of the module.
- `dataContext` - This Object contains values that are passed from the parent element of the module.
- `functionCallbacks` - This object contains callback functions that are passed from the parent element element of the module.