@mollycule/mason
v1.1.3
Published
A utlity for dynamically creating the UI components from a config
Downloads
24
Maintainers
Readme
Table of Contents
- About the Project
- Getting Started
- Usage
- Mason Configuration Semantics
- Contributing
- License
- Contact
- Acknowledgements
About The Project
Mason is a utility built for dynamically rendering UI components, currently for React environment. All it needs to render a complete UI is a JSON describing the type, id and various other parameters like validation clauses, display clauses and event handling by using a configurative DSL.
It also needs a map of component type vs React component factories to render the corresponding components mentioned in the config.
Built With
Getting Started
Prerequisites
Following Peer Dependencies are required for using mason package:
- react: "^16.8.6"
Installation
npm i @mollycule/mason -S
or
yarn add @mollycule/mason
Usage
- Build the mason config for rendering the UI as per your needs.
{
"page": "HOME",
"config": {
"id": "HomePageLayout",
"type": "PAGE_LAYOUT",
"children": [
{
"id": "firstSearchInput",
"type": "SEARCH_INPUT",
"meta": {
"disabled": "false",
"maxLength": 50,
"minLength": 5,
"placeholder": "Type here to search"
},
"validations": [
{
"type": "REQUIRED"
},
{
"type": "REGEX",
"meta": {
"pattern": "^\\w{3}\\d{0,2}$"
}
},
{
"type": "CUSTOM",
"meta": {
"functionName": "validateSearch"
}
}
],
"events": {
"change": [
{
"type": "AJAX_CALL",
"meta": {
"endpoint": "/data/chart-data/${0}/foo/${1}",
"urlParams": ["firstSearchInput", "<fieldIdHere>"],
"queryParams": {
"foo": "<FieldIdHere>",
"bar": "<FieldIdHere>"
},
"fieldId": "<FieldId of the field whose data we want to set>"
}
}
],
"click": {
"type": "SET_DATA",
"meta": {
"fieldId": "<FieldId>",
"valueField": "<FieldId>",
"value": "abc"
}
}
}
},
{
"id": "myListBox",
"type": "LIST_GROUP",
"meta": {
"groupHeading": "Lorem Ipsum",
"groupSubHeading": "A foxtrot above my head"
},
"data": {
"type": "AJAX_CALL",
"meta": {
"endpoint": "/data/chart-data/${0}/foo/${1}",
"queryParams": {
"foo": "<FieldIdHere>",
"bar": "hardcodedText"
}
}
}
}
]
}
}
- Make use of
ReactRenderer
to build the UI based on the config you created in previous step as
import { ReactConfigRenderer } from "@mollycule/mason";
const homePageRenderer = new ReactConfigRenderer(
config,
{
PAGE_LAYOUT: HomePageLayout,
SEARCH_INPUT: Search,
RECIPES_LIST_GROUP: Recipes
},
{
initialValues: { email: "[email protected]", password: "password"},
dataProcessors: {
recipeDataSourceProcessor(dataSource) {
return dataSource.hits.map(h => h.recipe);
}
}
}
);
const HomePage = homePageRenderer.render();
const App = () => <main><HomePage /></main>;
ReactDOM.render(<App/>, document.getElementById("root"));
Mason Configuration
Below is the detailed configuration options that can be done using Mason.
1. Root Node
{
"page": "RECIPES",
"config": {
"id": "HomePageLayout",
"type": "PAGE_LAYOUT",
"style": {
"display": "grid"
},
"children": [...]
}
The root node has a little different structure than rest of the nodes. It has two properties as page
and config
which basically represents the page or route of your web app you're trying to configure the UI for. That said, mason is not limited to page levels only. You can use it to created nested UI as well.
Note: The config
property can be an array of nodes as well striaght-away if you don't want to wrap them in an uber level layout element.
2. Component Node
{
"id": "<Unique Component Id>",
"type": "<Component Type>",
"meta": {
"disabled": false,
"maxLength": 50,
"placeholder": "Type here to search",
},
"show": true,
"validations": [],
"events": {},
"data": {},
"style": {
"display": "grid"
},
"children": [...]
}
id
field represents the element id used to uniquely identify the current state and updations for a given element. Make sure to keep it unique since this drives any updations during prop change of a given componenttype
field will help render the corresponding component during the render phase. It'll get matched against the components mapping that's passed while creating the renderer during usage.const homePageRenderer = new ReactConfigRenderer( config, { PAGE_LAYOUT: HomePageLayout, SEARCH_INPUT: SearchInput, RECIPES_LIST_GROUP: RecipesListGroup, FOO: () => <p>foo</p> } );
meta
field can be used to pass any properties to an UI Component that'll be added as inline props to the component being rendered. You can pass component props or normal HTML element props in it. Note: Setting upvalue
property inmeta
is helpful when you want to make your component controllable to avoid React warningsshow
property will completely unmount or not render the component in the first place if it evaluated to falsy. It can accept either a boolean or a boolean confguration as{ "operator": "!=", "leftOperand": "<%statesDropdown%>", "rightOperand": "[]", "type": "ATOMIC" }
Here, it'll evaluate the boolean operation as
<Value of statesDropdown> != []
and determine the value ofshow
.The detailed sturcture of
BooleanConfig
can be understood as the TS type below:type BooleanConfig = { type: OperationType; operator: ComparisonOperators | CompoundOperators; leftOperand: string | BooleanConfig; rightOperand: string | BooleanConfig; };
ComparisonOperators
andCompoundOperators
are discussed in detail further.style
property simply represents inline styles object passed in the Component.data
property will work as onMount fetching of data. You can specify a remote data source to fetch data asynchronously or set it statically.AJAX_CALL
: The most common scenario is of loading data on mount of a given component.type
attribute set toAJAX_CALL
is meant to handle the same. On successful ajax call, it'll set thedatasource
prop on the component. Also, when the Ajax call starts, aloading
prop is set totrue
on the component and set asfalse
on promise settling (either resolved or caught).{ "type": "AJAX_CALL", "meta": { "endpoint": "https://api.edamam.com/search", "queryParams": { "q": "egg", "app_id": "<app id>", "app_key": "<app key>", "foo": "<%fieldId%>", }, "credentials": "include", "fieldId": "recipesListComponent", "dataProcessor": "recipeDataSourceProcessor" } }
- In the
meta
config, you can pass things like theendpoint
and dynamic query params can be passed as the key value pairs to it. queryParams
property can incorporate query params value to be set from another component value by enclosing the desiredcomponentId
orfieldId
in special syntax as<%fieldId%>
. It'll dynamically take the current value of that component and interpolate it here to make the Ajax Call.credentials
property can be set to control sending same or cross origin cookiesfieldId
property can be used to tell which componentdatasource
prop you want to set.dataProcessor
: It might be the case that you want to massage or prune the ajax data before applying it to your component. So, you can tailor the data received as per your component needs by passing a function name here that can be referred from the paramters passed by the caller of the mason renderer as:
It's like a resolver function that'll be called before applying the datasource prop to the component.dataProcessors: { recipeDataSourceProcessor(dataSource) { return dataSource.hits.map(h => h.recipe); } }
fieldIds
is an object that contains key value pairs of fieldIds and their corresponding dataProcessors. It's useful in the cases where in the root level you wanna make an ajax call and set the children's data based on extracting properties from it.
- In the
SET_DATASOURCE
: To statically set the datasource of the current component or some other component by specifying the optionalfieldId
.{ "type": "SET_DATASOURCE", "meta": { "data": ["foo", "bar"], "fieldId": "<fieldIdToSetDataSourceFor>" } }
SET_VALUE
: In case you want to set the value of the current component or some other component on mount of current component.{ "type": "SET_VALUE", "meta": { "value": "foo", "fieldId": "<fieldIdToSetValueFor>" } }
CUSTOM
: When you want to execute a custom function on mount of the component.{ "type": "CUSTOM", "meta": { "name": "<nameOfTheCustomFunction>" } }
The Custom function needs to be passed in the
resolvers
map while creating the renderer. It'll be called with one parameter as({ event, value, id })
from which the values can be destructured during invocation.
events
property can either accept a map of event name vs their handlers configuration. Each event could have multiple handlers, hence both the array config and object config is supported. Below is the TypeScript typing for the same.events?: { [eventName: string]: Array<IEventsConfig> | IEventsConfig; }
Eg:
"events": { "onChange": [ { "type": "SET_VALUE" }, { "type": "AJAX_CALL", "meta": { "endpoint": "https://api.edamam.com/search", "queryParams": { "q": "<%SELF%>", "app_id": "<APP_ID>" }, "dataProcessor": "recipeDataSourceProcessor", "fieldId": "recipesListGroup" }, "when": { "operator": "=", "leftOperand": "<%secondSearchInput%>", "rightOperand": "", "type": "ATOMIC" } } ], "onFocus": { "type": "SET_VALUE", "meta": { "value": [], "fieldId": "<fieldIdToSetValueFor>" } }, "onClick": [ { "type": "SET_DATASOURCE", "meta": { "data": [], "fieldId": "<fieldIdToSetDataSourceFor>" } }, { "type": "CUSTOM", "meta": { "name": "<customFunctionNameHere>" } } ] }
Here, the
onChange
event of the component will execute two event handlers viz set value of itself (required for controlled components - same as setState the value), and the other one makes an ajax call to a remote api to fetch data of a listing component based on the query text entered in the current/self component. Note how the query param is able to take<%SELF%>
as it's value. It's a special value to get the current value of the current component in consideration. You could pass the id of the self component here as well, but it'll give a stale value since this is the event handler which will change it in the end. Thewhen
clause is a conditional clause which is discussed in detail further.Next, the
onFocus
event is executing a single handler to set the datasource property of the givenfieldId
. No need to pass an array form here.And, the
onClick
event is useful in case of a button component wherein you want to execute a custom event handler by matching it's name against what was passed in thedataProcessors
property while creating the Renderer.validations
can be used to configure the form validations on a given component. It's very useful in case of rendering forms. Here's a sample configuration for a simple Login Form.{ "page": "FORM", "config": [ { "id": "email", "type": "TEXTFIELD", "meta": { "placeholder": "Enter your email", "type": "email", "value": "", "autoComplete": "off" }, "validations": [ { "type": "REQUIRED" }, { "type": "LENGTH", "meta": { "min": 3, "max": 50 } }, { "type": "REGEX", "meta": { "pattern": "^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$" } } ], "events": { "onChange": [ { "type": "SET_VALUE" } ] }, "style": { "marginBottom": 10 } }, { "id": "password", "type": "TEXTFIELD", "meta": { "value": "Password@123", "type": "password", "placeholder": "Enter your password" }, "events": { "onChange": { "type": "SET_VALUE" } }, "style": { "marginBottom": 10 }, "validations": [ { "type": "REGEX", "meta": { "pattern": "^(?=.*\\d)(?=.*[!@#$%^&*])(?=.*[a-z])(?=.*[A-Z]).{8,}$" } } ] } ] }
The
validations
property expects an array of validation configuration to be supplied. Each configuration has atype
field which can take either of the following values.REQUIRED
: It'll guard against any value which is a blank string''
,undefined
ornull
(and not blank array or objects).{ "type": "REQUIRED" }
REGEX
: You can specify a Regular Expression for pattern matching the current value of the component against it.{ "type": "REGEX", "meta": { "pattern": "^(?=.*\\d)(?=.*[!@#$%^&*])(?=.*[a-z])(?=.*[A-Z]).{8,}$" } }
RANGE
: To check if the value falls in the numeric range betweenmin
andmax
inmeta
.{ "type": "RANGE", "meta": { "min": 3, "max": 10 } }
LENGTH
: Can work on both string and array values by checkingvalue.length
property between themin
andmax
values specified inmeta
.{ "type": "LENGTH", "meta": { "min": 3, "max": 10 } }
JSON
: To validate the JSON value fed to a component. It'll try to parse it and set a validation error if the parsing fails.{ "type": "JSON" }
CUSTOM
: In order to write a custom validator for custom needs.{ "type": "CUSTOM", "meta": { "name": "myCustomValidator" } }
The corresponding validator function has to be specified in the caller environment by passing a
validators
object containing these custom validator functions asvalidators: { myCustomEmailValidator(value: string) { return !value.includes("swiggy") ? "Not a valid swiggy email" : undefined; } }
A validator function takes value as input and returns either a string in case of an invalid value or
undefined
otherwise.undefined
means there was no error and the value was valid.
disabled
clause can be use to passdisabled
property to the component. It can either take a boolean straight-forwardly or a Conditional config which has the following structure.type ConditionalConfig = { type: OperationType; operator: ComparisonOperators | CompoundOperators; leftOperand: string | ConditionalConfig; rightOperand: string | ConditionalConfig; };
The
OperationType
can beATOMIC
: It's like a one level condition. A single expression likex === true
COMPOUND
: Compound condition can have multiple expressions joined via Compound operators like&&
or||
The
operators
could be either compound operators as mentioned above or comparison operators like=
,!=
,<
,<=
,>
,>=
Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
License
Distributed under the MIT License. See LICENSE
for more information.
Contact
Param Singh - @paramsinghvc - [email protected]
Project Link: https://github.com/paramsinghvc/mason