• Build a Drag and Drop layout Builder with React and ImmutableJS
  • Originally by Chris Kitson
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: fireairforce
  • Proofreader: Eternaldeath, Portandbridge

Build a drag-and-drop (DnD) layout builder using React and ImmutableJS

There is a huge demand for “drag and drop” activities such as building web sites (Wix) or interactive applications (Trello). There is no doubt that this type of interaction makes for a very cool user experience. If we add some of the latest UI technology, we can create some really good software.

What is the ultimate goal of this article?

I wanted to build a drag-and-drop layout builder that would allow users to build layouts using a set of customizable UI components, eventually building a website or web application.

Which libraries will we use?

  1. React
  2. ImmutableJS

Let’s take a moment to explain their role in building this project.

React

React is declarative programming, which means it renders according to state. The State is really just a JSON object with properties that tell React how to render (style and functionality). Unlike libraries that manipulate the DOM (such as jQuery), we don’t change the DOM directly. Instead, we change the state and let React take care of the DOM (more on DOM later).

In this project, we assume that there is a parent component that holds the state of the layout (JSON objects), and that this state will be passed to other components that are stateless components in React.

The purpose of these components is to get the state from the parent component and then render itself based on its properties.

Here is a simple example of a state with three Link objects:

{
  links:  [{
    name: "Link 1".url: "http://link.one".selected: false
  }, {
    name: "Link 2".url: "http://link.two".selected: true
  }, {
    name: "Link 3".url: "http://link.three".selected: false}}]Copy the code

Using the above example, we can create a stateless component for each element by iterating through the Links array:

interface ILink {
  name: string;
  url: string;
  selected: boolean;
}

const LinkComponent = ({ name, url, selected }: ILink) =>
<a href={url} className={selected ? 'selected': ''}>{name}</a>;
Copy the code

You can see how we apply the CSS class “Selected” to the Links array component based on the selected properties saved in the state. Here is what is rendered to the browser:

<a href="http://link.two" class="selected">Link 2</a>
Copy the code

ImmutableJS

We’ve seen the importance of state in our project; it’s the only real source of data that makes the React component render. States in React are stored in immutable data structures.

In short, this means that once a data object has been created, it cannot be modified directly. Unless we create a new object with changed state.

Let’s use another simple example to illustrate immutability:

interface ILink {
  name: string;
  url: string;
  selected: boolean;
}

const link: ILink = {
    name: "Link 1",
    url: "http://link.one",
    selected: false
}
Copy the code

In traditional JavaScript, you can update a Link object by:

link.name = 'New name';
Copy the code

This cannot be done if our state is immutable, and we must create a new object whose name property has been changed.

link = {... link,name: 'New name' };
Copy the code

Note: To support immutability, React provides us with a methodthis.setState()We can use it to tell the component that the state has changed and that the component needs to re-render if any state changes.

The above is just a basic example, but what do you do if you want to change multi-layer nested properties in a complex JSON state structure?

ECMA Script 6 provides us with some convenient operators and methods to change objects, but they are not suitable for complex data structures, which is why we need ImmutableJS to simplify tasks.

We’ll use ImmutableJS later, but for now all you need to know is that it gives us an easy way to change complex states.

HTML5 Drag and drop (DnD)

So we know that our state is an immutable JSON object, and React handles the components, but we need a fun user interaction experience, right?

Thanks to HTML5, this is actually quite simple, as it provides methods we can use to detect when we drag components and where we put them. Because React exposes native HTML elements to the browser, we can use native event methods to make our implementation simpler.

Note: I understand that there may be some problems with DnD implemented in HTML5 but if nothing else, this is probably an exploratory course and we can replace it later if we find problems.

In this project, we have components that users can drag (HTML Divs), which I call dragable components.

We also have areas that allow users to place components, which I call deployable components.

With native HTML5 events such as onDragStart, onDragOver, and onDragDrop, we should also have what we need to change the state of our application based on DnD interactions.

Here is an example of a draggable component:

export interface IDraggableComponent { name: string; type: string; draggable? : boolean; onDragStart: (ev: React.DragEvent<HTMLDivElement>, name: string, type: string) => void; } export const DraggableComponent = ({ name, type, onDragStart, draggable = true }: IDraggableComponent) => <div className='draggable-component' draggable={draggable} onDragStart={(ev) => onDragStart(ev, name, type)}>{name}</div>;Copy the code

In the code snippet above, we render a React component that uses the onDragStart event to tell the parent that we are starting to drag the component. We can also toggle the ability to drag it by passing the draggable property.

Here is an example of a drop-able component:

export interface IDroppableComponent { name: string; onDragOver: (ev: React.DragEvent<HTMLDivElement>) => void; onDrop: (ev: React.DragEvent<HTMLDivElement>, componentName: string) => void; } export const DroppableComponent = ({ name, onDragOver, onDrop }: IDroppableComponent) => <div className='droppable-component' onDragOver={(ev: React.DragEvent<HTMLDivElement>) => onDragOver(ev)} onDrop={(ev: React.DragEvent<HTMLDivElement>) => onDrop(ev, name)}> <span>Drop components here! </span> </div>;Copy the code

In the above component, we are listening for an onDrop event, so we can update the state based on the new component that we put into the placeable component.

Ok, time for a quick review and then put them all together:

We’ll render the entire layout using a small number of decoupled stateless components based on state objects in React. User interactions will be handled by HTML5 DnD events, and time will use ImmutableJS to trigger changes to the state object.

Drag the whole

Now that we have a good idea of what to do and how to deal with it, let’s consider some of the most important pieces of the puzzle:

  1. Layout of the state
  2. Drag and drop builder components
  3. Render nested components within the grid

1. Layout status

In order for our components to represent infinite layout combinations, state needs to be flexible and extensible. We also need to keep in mind that if you want to represent a DOM tree for any given layout, that means a lot of pleasant recursive support for nested structures!

Our state needs to store a large number of components, which can be represented through the following interface:

If you’re not familiar with JavaScript interfaces, you should check them outTypeScript– You can probably tell I’m a fan. It works well with React.

export interface IComponent {
  name: string;
  type: string; renderProps? : { size? :1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}; children? : IComponent[]; }Copy the code

I’ll minimize the definition of the component, but you can expand it as needed. I defined an object here in the renderProps, so we can give the component state to tell it how to render, and the children property gives us recursion.

For higher levels, I’ll create an array of objects to hold components that will appear at the root of the state.

To illustrate this, we recommend the following as a valid layout for tags in HTML:

<div class="content-panel-1">
  <div class="component">
    Component 1
  </div>
  <div class="component">
    Component 2
  </div>
</div>
<div class="content-panel-2">
  <div class="component">
    Component 3
  </div>
</div>
Copy the code

To represent this in the state, we can define an interface for the content panel as follows:

export interface IContent {
  id: string;
  cssClass: string;
  components: IComponent[];
}
Copy the code

Then our state will become an IContent array like the following:

const state: IContent[] = [
  {
    id: 'content-panel-1',
    cssClass: 'content-panel-1',
    components: [{
      type: 'component1',
      renderProps: {},
      children: []
    },
    {
      type: 'component2',
      renderProps: {},
      children: []
    }]
  },
  {
    id: 'content-panel-2',
    cssClass: 'content-panel-2',
    components: [{
      type: 'component3',
      renderProps: {},
      children: []
    }]
  }
];
Copy the code

By pushing other components in the Children array property, we can define other components to create nested DOM-like tree structures:

[0]
  components:
    [0]
      children:
        [0]
          children:
            [0]
               ...
Copy the code

2. Drag and drop layout builder

The layout builder component performs a number of functions, such as:

  • Maintain and update component state
  • Renders draggable components and deployable components
  • Render the nested layout structure
  • Trigger the DnD HTML5 event

The code looks something like this:

export class BuilderLayout extends React.Component {

  public state: IBuilderState = {
    dashboardState: []
  };

  constructor(props: {}) {
    super(props);

    this.onDragStart = this.onDragStart.bind(this);
    this.onDragDrop = this.onDragDrop.bind(this);
  }

  public render() {

  }

  private onDragStart(event: React.DragEvent <HTMLDivElement>, name: string.type: string) :void {
    event.dataTransfer.setData('id', name);
    event.dataTransfer.setData('type'.type);
  }

  private onDragOver(event: React.DragEvent<HTMLDivElement>): void {
    event.preventDefault();
  }

  private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string) :void{}}Copy the code

Let’s forget about the render() function for a moment, but we’ll see it again soon.

We have three events that we are going to bind to our Dragable component and our placable component.

Here we set up some details about the components in the event object, namely name and type.

OnDragOver () — we won’t do anything with this event right now, in fact we disable the browser’s default behavior with the.preventDefault() function.

This leaves the onDragDrop() event, which is the magic of modifying immutable state. To change the state, we need several pieces of information:

  • The name of the component to place —nameeventObject SettingsonDragStart().
  • The type of component to place —typeeventObject SettingsonDragStart().
  • Where the component is placed —containerIdPass in this method from a deployable component.

In containerId you have to tell us exactly where the new component is going to be in the state. There may be a cleaner way to do this, but to describe the location, I’ll use an underlined index list.

Review our state model:

[index]
  components:
    [index]
      children:
        [index]
          children:
            [index]
               ...
Copy the code

It is expressed as cb_index_index_index_index in string format.

The number of indexes here describes the level of depth in the nested structure where the component should be removed.

Now we need to call the powerful features in immutableJS to help us change the state of the application. We’ll do this in the onDragDrop() method, which might look like this:

private onDragDrop(event: React.DragEvent <HTMLDivElement>, containerId: string) {
  const name = event.dataTransfer.getData('id');
  const type = event.dataTransfer.getData('type');

  const newComponent: IComponent =  this.generateComponent(name, type);

  const containerArray: string[] = containerId.split('_');
  containerArray.shift(); // Ignore the first argument, which is a string prefix

  const componentsPath: Array<number | string> = []   containerArray.forEach((id: string, index: number) = > {
  componentsPath.push(parseInt(id, INT_LENGTH));
  componentsPath.push(index === 0 ? 'components' : 'children');
});

  const { dashboardState } = this.state;
  let componentState = fromJS(dashboardState);

  componentState = componentState.setIn(componentsPath,       componentState.getIn(componentsPath).push(newComponent));

  this.setState({ dashboardState: componentState.toJS() });

}
Copy the code

The functionality here comes from the.setin () and.getin () methods ImmutableJS provides us.

They take a set of strings/values to determine where to get or set values in the nested state model. This fits well with the way we generate our deployable ids. Pretty cool, huh?

The fromJS() and toJS() methods convert JSON objects to ImmutableJS objects and back again.

There’s a lot of stuff out there about ImmutableJS, and I’ll probably write a dedicated post about it in the future. I’m sorry, this is just a brief introduction!

3. Render nested components within the grid

Finally, let’s take a quick look at the rendering methods mentioned earlier. I would like to support a CSS grid system similar to the Material Responsive Grid to make our layout more flexible. It uses a 12-column grid to specify the HTML layout, as follows:

<div class="mdc-layout-grid">
  <div class="mdc-layout-grid__inner">
    <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6">
      Left column
    </div>
    <div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-6">
      Right column
    </div>
  </div>
</div>
Copy the code

Combine this with the nested structure that our state represents, and you get a very powerful layout builder.

For now, I just fixed the size of the grid to a two-column layout (that is, the recursion that two columns in a single deployable component have).

To achieve this, we have a grid of draggable components that will contain two deployable (one for each column).

Here’s one I created earlier:

Up here I have a Grid with a Card in the first column and a Heading in the second column.

Now I have another Grid in the first column with a Heading in the first column and a Card in the second column.

Do you understand?

Here’s an example of how React pseudocode can be used to do this:

  1. Iterate over the items (we state the root) and rendering a ContentBuilderDraggableComponent and a DroppableComponent.

  2. To determine whether components for the Grid type, and then apply colours to a drawing ContentBuilderGridComponent, or rendering a regular DraggableComponent.

  3. Rendering is the Grid components of X is the project tag, each component has a ContentBuilderDraggableComponent and a DroppableComponent.

class ContentBuilderComponent... { render() { return ( <ContentComponent> components.map(...) { <ContentBuilderDraggableComponent... /> } <DroppableComponent... /> </ContentComponent> ) } } class ContentBuilderDraggableComponent... { render() { if (type === GRID) { return <ContentBuilderGridComponent... /> } else { return <DraggableComponent ... /> } } } class ContentBuilderGridComponent... { render() { <GridComponent... > children.map(...) { <GridItemComponent... > gridItemChildren.map(...) { <ContentBuilderDraggableComponent... /> <DroppableComponent... /> } </GridItemComponent> } </GridComponent> } }Copy the code

What’s next?

We’ve finished this article, but I’ll expand on it a bit in the future. Here are some ideas:

  • Configure the render items for the component
  • Make grid components configurable
  • Generate HTML layouts from saved state objects using server-side rendering

I hope you can follow me. If you don’t, this is a working example of me on GitHub. I hope you can appreciate it. Chriskitson/React Drag-and-drop Layout Builder uses react and ImmutableJS drag-and-drop (DnD) UI layout builder – Chriskitson/React drag-and-drop layout builder github.com

Thank you for taking the time to read this.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.