React-page-maker, A drag-drop featured lib which will help you to build the page layout and generate the markup or JSON out of it. You can store this JSON at server side or anywhere based on your requirement and use it later for Preview.
Library is designed in such a way that you will get enough flexibility in terms of defining any type of elements/layout which you are going to use throughout the application.
If you are building custom layout builder/dashboard then probably you are in the right place! :)
npm install --save react-page-maker
type
property. Based on type we decide what component to load for that element/layout.
// Const.js
export const elements = {
TEXTBOX: 'TEXTBOX',
LAYOUT_GRID_1_2: 'LAYOUT_GRID_1_2'
};
Every Elements should have three versions defined.
In palette and canvas version element should have drag feature which can be achieved through Draggable
component.
// DraggableTextbox.js
import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup, Input, Label } from 'reactstrap';
import { Draggable } from 'react-page-maker'; // to give drag behaviour
const DraggableTextbox = (props) => {
// `showBasicContent` is default prop passed by `Palette` component
const { showBasicContent, showPreview, name } = props;
if (showBasicContent) { // palette version - content to be shown in palette list
return (
<Draggable {...props}>
<span>{name}</span>
</Draggable>
);
}
if (showPreview) { // preview version - content to be shown in preview mode - end result, no need of `Draggable`
return (
<FormGroup>
<Label>{name}</Label>
<Input type="text" />
</FormGroup>
);
}
return ( // canvas version - content to be shown in canvas
<Draggable {...props}>
<FormGroup>
<Label>{name}</Label>
<Input type="text" placeholder="Type here" />
</FormGroup>
</Draggable>
);
};
DraggableTextbox.propTypes = {
name: PropTypes.string,
showBasicContent: PropTypes.bool
};
export default DraggableTextbox;
Based on showBasicContent
prop you can manage what content to be shown in Canvas and Palette. This is default prop which will be available for all palette elements.
Note- For drag behaviour, wrap the element with Draggable
component and make sure all props gets passed to it.
Here the steps are very similar to element, after all layouts are also one type of element but the only difference is that they have some dropzone wherein you can drop a elements to form the page structure. To create such dropezon we have Dropzone
component. Layout can have one or many dropzones but make sure each dropzone has unique identifier.
// DraggableGrid_1_2.js
import { Draggable, Dropzone } from 'react-page-maker';
const DragItemGridLayout = (props) => {
// make sure you are passing `dropzoneProps` prop to dropzone
const { showBasicContent, showPreview, dropzoneProps, childNode, ...rest } = props;
if (showBasicContent) { // content to be shown in palette
return (
<Draggable {...props} >
<span>{ rest.name }</span>
</Draggable>
);
}
if (showPreview) { // content to be shown in preview mode - end result
return (
<Row className="row">
<Col sm="6">
{childNode['canvas-1-1'] && childNode['canvas-1-1'].map(e => e)}
</Col>
<Col sm="6">
{childNode['canvas-1-2'] && childNode['canvas-1-2'].map(e => e)}
</Col>
</Row>
)
}
return ( // content to be shown in canvas
<Draggable {...props} >
<span>{ rest.name }</span>
<div className="grid-layout">
<div className="row">
<div className="col-sm-6">
<Dropzone {...dropzoneProps} id="canvas-1-1" />
</div>
<div className="col-sm-6">
<Dropzone {...dropzoneProps} id="canvas-1-2" />
</div>
</div>
</div>
</Draggable>
);
};
Note -
id
and dropzoneProps
to every dropzonedropzoneProps
is by default available under propsimport { registerPaletteElements } from 'react-page-maker';
// pass array of elements which we want to use across
registerPaletteElements([{
type: elements.TEXTBOX, // import from const.js
component: DraggableTextbox // import from DraggableTextbox.js
}, {
type: elements.LAYOUT_GRID_1_2,
component: DragItemGridLayoutR1C2 // import from DraggableGrid_1_2.js
}]);
After this step, the application will be aware what type of elements we have and based on this types we can create as many fields required.
e.g. from elements.TEXTBOX
we can create any text field (First Name, Last Name, etc.)
Note - Call registerPaletteElements
function before you render palette. e.g. Inside constructor
or componentWillMount
Pass list of elements which we need to show inside palette (since not every time we will be using all elements).
import { Palette, Canvas } from 'react-page-maker';
class PageConfigurator extends Component {
constructor(props) {
super(props);
this.registerPaletteElements();
}
registerPaletteElements = () => {
registerPaletteElements([{ // <-- registered palette elements
type: elements.TEXTBOX,
component: DraggableTextbox
}, {
type: elements.LAYOUT_GRID_1_2,
component: DragItemGridLayoutR1C2
}]);
}
paletteElements = [{ // <-- palette elements to be shown
id: 'f1', // make sure ID is unique
name: 'Input Field',
type: elements.TEXTBOX
}, {
id: 'g1',
name: 'Two Dropzones',
type: elements.LAYOUT_GRID_1_2
}]
render() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-sm-8 canvas-wrapper">
<Canvas />
</div>
<div className="col-sm-4 palette-wrapper">
<Palette paletteElements={this.paletteElements} />
</div>
</div>
</div>
);
}
}
export default PageConfigurator;
Note - Make sure every palette elements has unique ID.
By now, you would be able to see Canvas and Palette (with those provided elements).
Prop | Type | Description |
---|---|---|
id | String | ID of an element |
name | String | Name of an element |
draggable | Boolean | Manage element drag behaviour, default is true |
type | String | Defines type of an element |
payload | Object | Any custom data that you want to pass |
Prop | Type | Description |
---|---|---|
paletteElements | Array | Array of element(Object) to be shown inside palette |
Prop | Type | Description |
---|---|---|
id | String | ID of a dropzone |
capacity | Number | Maximum number of elements dropzone can maintain |
allowHorizontal | Bool | Allow horizontal drop, default is vertical |
initialElements | Array | Array of element(Object) to be prepopulate inside dropzone, here format will be similar to palette elements |
placeholder | String | Text to be shown when dropzone is emepty. Default value is Drop Here. |
onDrop | Function | function gets triggered once element got dropped |
onElementMove | Function | Function get called when we try to move the element from one dropzone to another |
/*
* function gets triggered once element got dropped
* @param {Object} data - It holds element information
* @param {Function} cb - A callback function, which helps to decide whether to add element or not.
* if data is valid then call cb function to proceed else return false.
* @param {Number} dropIndex - Position where element getting dropped
* @param {Array} currentElements - Current elements which canvas holds
* @returns {cb/Boolean}
*/
_onDrop = (data, cb, { dropIndex, currentElements }) => {
//In order to mock user input I'm using `window.prompt`
// in actual scenario we can add some async call to fetch data
const name = window.prompt('Enter name of field');
const id = window.prompt('Enter id of field');
const result = cb({
...data,
name: name || data.name,
id: id || data.id
});
}
/**
* Function get called when we try to move the element from one dropzone to another
* @param {Object} elementMoved - Data of element which has been moved
* @returns {Boolean}
*/
_onElementMove = (elementMoved) => (true);
<Dropzone
id="d1"
capacity={4}
initialElements={[]}
placeholder="Drop Here"
onDrop={this._onDrop}
onElementMove={this._onElementMove}
{...dropzoneProps}
/>
Note - If is there any situation where you need to update the dropzone state manually then you can use dangerouslySetElements
function, this gives you direct access to dropzone state. May be you can refer state.updateElement|state.removeElement
API if that fulfills your requirement.
onSomeAction = () => {
// Note - make sure you are passing valid data else state service may break
// 1 - pass an array if you are just going to reset the dropzone
const current = this.currentDropzone.current;
current && current.dangerouslySetElements([element1, element2]);
// element type should be similar to what you are passing in palette
// or
// 2 - pass a function which will give you the current elements that dropzone holds, and then you can make necessary tweaks
const current = this.currentDropzone.current;
current && current.dangerouslySetElements((currentElements) => currentElements
.map(doSomeTweaks));
}
<Dropzone
ref={this.currentDropzone}
{...requiredProps}
/>
This is a top level dropzone element from where we actually starts drag-drop. Canvas
is extended version of Dropzone
with some default properties (e.g. ID
) defined.
You can use this component to have feature of trash/delete box. Once element dropped inside Trash then it gets removed from canvas and state.
Prop | Type | Description |
---|---|---|
onBeforeTrash | function | cb which invoke just before element being trashed |
onAfterTrash | function | cb which invoke after element is trashed |
Syntax
/**
* Function get trigger just before element getting trashed
* @param {Object} elementToBeTrashed - Data of element which is going to be trashed
* @returns {Boolean}
*/
_onBeforeTrash = (elementToBeTrashed) => {
return true;
}
/**
* Success function which gets triggered once element has been trashed
*/
_onAfterTrash = () => {
console.log('Updated state', state.getState());
}
<Trash onBeforeTrash={this._onBeforeTrash} onAfterTrash={this._onAfterTrash} />
Use this component to show the preview version of current state (End Output or markup). Make sure every draggable elements is returning something on showPreview
Syntax
/* how to use */
<Preview />
/* OR - for more layout flexibility */
<Preview>
{
({ children }) => (
<div>
Custom Layout
{children}
</div>
)
}
</Preview>
Note - Refer Create and Register elements/layouts section to know how we define preview version inside draggable element/layout.
It provide access to the state object which holds all the meta data.
Prop | Type | Description |
---|---|---|
getState | function | returns current state of canvas |
getStorableState | function | returns current state which can be stored at servers side for future use |
clearState | function | flush current state |
getElementParent | function | returns parent layout element |
getElement | function | returns details of element |
removeElement | function | remove element from tree |
updateElement | function | update specific element |
addEventListener | function | to add event, supported events change, flush, removeElement, updateElement |
removeEventListener | function | to remove event, pass proper event cb |
Syntax
import { state } from 'react-page-maker';
/* Function to get current state of the canvas */
state.getState();
/* Function to get current state of the canvas which can be parsed to string with help of `JSON.stringfy` and later we can store it at server side */
state.getStorableState();
/**
* function to return parent of element
* @param {string} dropzoneID - every elements gets dropzoneID under props
* @param {string} parentID - every elements gets parentID under props
* @returns {Object} layout element
*/
state.getElementParent(dropzoneID, parentID);
/**
* function to return element details
* @param {string} elementID - every elements gets id under props
* @param {string} dropzoneID - every elements gets dropzoneID under props
* @param {string} parentID - every elements gets parentID under props
* @returns {Object} element
*/
state.getElement(elementID, dropzoneID, parentID);
/**
* function to remove element from tree
* @param {string} elementID - every elements gets id under props
* @param {string} dropzoneID - every elements gets dropzoneID under props
* @param {string} parentID - every elements gets parentID under props
* @param {function} cb - success callback function
* @returns {Boolean}
*/
state.removeElement(elementID, dropzoneID, parentID, cb);
/**
* function to update an element details, you can update name, type, payload properties
* @param {string} elementID - every elements gets id under props
* @param {string} dropzoneID - every elements gets dropzoneID under props
* @param {string} parentID - every elements gets parentID under props
* @param {Object} newData - value that needs to be set, { name, type, payload }
* @param {function} cb - success callback function
* @returns {Boolean}
*/
state.updateElement(elementID, dropzoneID, parentID, newData, cb);
/**
* Function to flush canvas/current state
* @param {Function} cb - success call back, function gets triggered once canvas is flushed
*/
state.clearState(() => {
console.log('Canvas has been flushed successfully')
});
/**
* Function to add event
* @param {String} event - name of event, supported events are 'change', 'flush',
* 'removeElement', 'updateElement'
* @param {Function} cb - function gets called upon event trigger
* @param {Object} newState - new state
* @returns {Function} - instance of attached function
*/
const cb = state.addEventListener('change', (newState) => {
console.log('new state', newState);
});
/**
* Function to remove event
* @param {String} event - name of event, supported events are 'change', 'flush','removeElement', 'updateElement'
* @param {Function} oldEventInstance - instance of previously attached event
*/
state.removeEventListener('change', cb);
Please feel free to raise PR for any new feature or bug (if you find any).