@mollycule/mason
v1.1.3
Published
A utlity for dynamically creating the UI components from a config
Downloads
11
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 -Sor
yarn add @mollycule/masonUsage
- 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
ReactRendererto 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": [...]
}idfield 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 componenttypefield 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> } );metafield 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 upvalueproperty inmetais helpful when you want to make your component controllable to avoid React warningsshowproperty 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
BooleanConfigcan be understood as the TS type below:type BooleanConfig = { type: OperationType; operator: ComparisonOperators | CompoundOperators; leftOperand: string | BooleanConfig; rightOperand: string | BooleanConfig; };ComparisonOperatorsandCompoundOperatorsare discussed in detail further.styleproperty simply represents inline styles object passed in the Component.dataproperty 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.typeattribute set toAJAX_CALLis meant to handle the same. On successful ajax call, it'll set thedatasourceprop on the component. Also, when the Ajax call starts, aloadingprop is set totrueon the component and set asfalseon 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
metaconfig, you can pass things like theendpointand dynamic query params can be passed as the key value pairs to it. queryParamsproperty can incorporate query params value to be set from another component value by enclosing the desiredcomponentIdorfieldIdin special syntax as<%fieldId%>. It'll dynamically take the current value of that component and interpolate it here to make the Ajax Call.credentialsproperty can be set to control sending same or cross origin cookiesfieldIdproperty can be used to tell which componentdatasourceprop 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); } }fieldIdsis 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
resolversmap while creating the renderer. It'll be called with one parameter as({ event, value, id })from which the values can be destructured during invocation.
eventsproperty 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
onChangeevent 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. Thewhenclause is a conditional clause which is discussed in detail further.Next, the
onFocusevent is executing a single handler to set the datasource property of the givenfieldId. No need to pass an array form here.And, the
onClickevent 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 thedataProcessorsproperty while creating the Renderer.validationscan 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
validationsproperty expects an array of validation configuration to be supplied. Each configuration has atypefield which can take either of the following values.REQUIRED: It'll guard against any value which is a blank string'',undefinedornull(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 betweenminandmaxinmeta.{ "type": "RANGE", "meta": { "min": 3, "max": 10 } }LENGTH: Can work on both string and array values by checkingvalue.lengthproperty between theminandmaxvalues 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
validatorsobject 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
undefinedotherwise.undefinedmeans there was no error and the value was valid.
disabledclause can be use to passdisabledproperty 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
OperationTypecan beATOMIC: It's like a one level condition. A single expression likex === trueCOMPOUND: Compound condition can have multiple expressions joined via Compound operators like&&or||
The
operatorscould 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

