In a previous post I went through a simple example of using AngularJS to render API data on an APEX page. This led me to think about a ReactJS implementation.

Whereas AngularJS is a fully fledged MVC-like framework, ReactJS is about building self contained re-usable components. For example, it has no routing or built in HTTP object for making requests (you can use jQuery for that).

React is all about building reusable components. In fact, with React the only thing you do is build components. Since they're so encapsulated, components make code reuse, testing, and separation of concerns easy.

Have a look at the ReactJS tutorial to get a feel for how to build a component.

Initially when I started to write this post, the idea was to create a ReactJS enabled page template and then re-use this template on different pages with different data sets being to demonstrate it's flexibility. Then I started thinking about plug-ins.

I tend not to write many plug-ins in APEX, mostly because I enjoy working with other languages more when it comes to rendering HTML, but it dawned on me that writing a plug-in based on a ReactJS component would give me a nice separation of concerns between my data code and my rendering code. More crucially, it opens up a huge library of ReactJS components to APEX.

Let's revisit the whiteboard idea from the AngularJS article. Thinking about the UI, we can break things up into separate components.

  • The whiteboard.
  • The status column.
  • The card.

This is the initial skeleton for my ReactJS code with some static data. I have defined three components and instructed ReactJS to render the Whiteboard. The way I have structured my classes resembles how they appear in the UI. The Whiteboard is made up of WhiteboardColumns, which in turn is made up of Cards.

var Whiteboard = React.createClass({
   render: function() {          
      return (              
         <div>
            <WhiteboardColumn status="New"/>
            <WhiteboardColumn status="In Progress"/>
            <WhiteboardColumn status="Done"/>
         </div>
      );
   }      
});

var WhiteboardColumn = React.createClass({
   render: function() {
      return (
        <div className="col-md-3">
           <div className="panel panel-default">                          
              <div className="panel-heading">{this.props.status}</div>
              <div className="panel-body">
                 <Card />
              </div>
           </div>
        </div>
      );
   }
});

var Card = React.createClass({
   render: function() {
      return (
         <div className="panel panel-info">
            <div className="panel-heading">Some Heading</div>
            <div className="panel-body">Some Description</div>
         </div>
      );
   }   
});

ReactDOM.render(
   <Whiteboard />,
   document.getElementById('content')
);

I've put this code into a file called react_whiteboard_plugin.js which I am referencing in my plug-in render function. Notice that my plug-in script type is text/babel when I reference my ReactJS code. This is because ReactJS provides the option of using JSX templates.

plus in render

I'm pulling in the react libs that I need using the JavaScript file URL properties.

JS:

https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js
https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js
https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js

CSS:

https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css

Here is how it looks with this static data. I have 3 columns. The WhiteboardColumn element is reused nicely by passing in the desired column status as a property.

React Simple

Now lets plug this into an apex.server.process call in order to get my story card data. I've added a couple of functions to the WhiteboardColumn declaration. getInitialState is called by ReactJS to initialise the default state for the component. componentDidMount is fired once the component is rendered. The apex.server.process call passes in this.props.status which is passed down from the parent component (Whiteboard), so we are filtering at query level in the application process. The resulting data array is set as the component state and this is used by the render function to build up an array of cards to be rendered.

var WhiteboardColumn = React.createClass({
     render: function() {
        var Cards = this.state.data.map(function (card, idx) {
           return <Card key={idx} title={card.TITLE} desc={card.DESCRIPTION}/>
        });

        return (
           <div className="col-md-3">
              <div className="panel panel-default">
                 <div className="panel-heading">{this.props.status}</div>
                 <div className="panel-body">
                    {Cards}
                 </div>
              </div>
           </div>
        );
     },
     getInitialState: function()
     {
        return ({
           data: []
        });
     },
     componentDidMount: function() {
        var self = this;
        apex.server.process('get_cards', {x01: this.props.status}, {type: "GET"})
          .done(function (cards) {
             self.setState({data: cards});
          });
     }
  });

We can add in some further abstraction in the Whiteboard component. Lets pass an array of status values in rather than having them hard coded.

var Whiteboard = React.createClass({
     render: function() {
        var WhiteboardColumns = this.props.statusArray.map(function (status, idx) {
           return <WhiteboardColumn key={idx} status={status}/>
        });

        return (
           <div>
              {WhiteboardColumns}
           </div>
        );
     }
  });

Now our ReactDOM.render call looks like this.

ReactDOM.render(
    <Whiteboard statusArray={['New', 'In Progress', 'Done']}/>,
    document.getElementById('content')
);

Lets see how it looks at runtime.

react data 1

Nice! We have a working whiteboard. Now it's time to abstract things further.

This is the final version of my ReactJS code that I've added to the react_whiteboard_plugin.js file. I've made the apex.server.process name a configurable property too and now I'm referencing a variable for both this, the region ID and the statusArray. If you refer back to my render function, you will see that I am setting up these variables based on plug-in parameters.

  var Whiteboard = React.createClass({
     render: function() {
        var self = this;
        var WhiteboardColumns = this.props.statusArray.map(function (status, idx) {
           return <WhiteboardColumn key={idx} status={status} serverProcess={self.props.serverProcess}/>
        });

        return (
           <div>
              {WhiteboardColumns}
           </div>
        );
     }
  });

  var WhiteboardColumn = React.createClass({
     render: function() {
        var Cards = this.state.data.map(function (card, idx) {
           return <Card key={idx} title={card.TITLE} desc={card.DESCRIPTION}/>
        });

        return (
           <div className="col-md-3">
              <div className="panel panel-default">
                 <div className="panel-heading">{this.props.status}</div>
                 <div className="panel-body">
                    {Cards}
                 </div>
              </div>
           </div>
        );
     },
     getInitialState: function()
     {
        return ({
           data: []
        });
     },
     componentDidMount: function() {
        var self = this;
        apex.server.process(this.props.serverProcess, {x01: this.props.status}, {type: "GET"})
          .done(function (cards) {
             self.setState({data: cards});
          });
     }
  });

  var Card = React.createClass({
     render: function() {
        return (
           <div className="panel panel-info">
              <div className="panel-heading">{this.props.title}</div>
              <div className="panel-body">{this.props.desc}</div>
           </div>
        );
     }
  });

  ReactDOM.render(
     <Whiteboard statusArray={pageStatusList} serverProcess={pageServerProcess}/>,
     document.getElementById('content')
  );

Now what we have is a a Whiteboard component based plug-in that can be tied to any application process that follows the expected query format. For example, in my ReactJS Whiteboard page, I want to display a Whiteboard for movies.

All I need to do is add the plug-in to my page and set the status columns and application process I would like to use.

Now my page displays movie cards.

How about if I just want to see classics.

I'll be spending more time playing around with existing ReactJS components and seeing if I can get them to play nice with APEX plug-ins.