Skip to content
This repository has been archived by the owner on Jan 5, 2022. It is now read-only.

Component Implementation

Eric Jackson edited this page Jun 5, 2015 · 20 revisions

Implementing Client-Side Display Components

We'll use some simple, but concrete component examples to document how display components are to be implemented.

SimpleCard

The SimpleCard component is extremely simple. It takes a single card and displays its title and body.

Here is the JSX code for SimpleCard in full:

import React from 'react';

var cardStore = require('../stores/CardStore');

var SimpleCard = React.createClass({

    propTypes: {
        site: React.PropTypes.object.isRequired,
        componentData: React.PropTypes.object.isRequired,
        componentProps: React.PropTypes.object.isRequired,
        storeId: React.PropTypes.number.isRequired
    },


    render: function() {
        var card = cardStore.getCard(this.props.componentData['mycard'].ids[0]);

        var renderTitle = function(tagLevel, card) {
            if (tagLevel && tagLevel > 0) {
                switch (tagLevel) {
                    case 1:
                        return <h1>{card.title}</h1>
                        break;
                    case 2:
                        return <h2>{card.title}</h2>
                        break;
                    case 3:
                        return <h3>{card.title}</h3>
                        break;
                    case 4:
                        return <h4>{card.title}</h4>
                        break;
                    case 5:
                        return <h5>{card.title}</h5>
                        break;
                }
            }
        };

        if (card == undefined) {
            return <div >SimpleCard loading ... </div>
        }
        else {
            return (
                <div className="row">
                    <div className="col-md-12">
                        {renderTitle(Number(this.props.componentProps.headerTag), card)}

                        <span dangerouslySetInnerHTML={{__html: card.body}} />
                    </div>
                </div>
            );
        }
    }
});

export default SimpleCard;

The propTypes property here is standard for every GBE display component. It contains four items: site, componentData, componentProps, and storeId. Since SimpleCard doesn't actually make use of site or storeId, we'll leave discussion of them for later.

The componentProps property is a simple name-value hash of properties that have been set in the component definition or by the user in configuring the component.

The componentData property is a hash. The hash keys are the tags specified in the component definition file (see [here] (Component-Registration)). The associated object contains information the component needs to access the data elements it will use (cards, cardsets, datasets and multidatasets). In the case of a card or cardset, this.props.componentData['tag'].ids is an array of IDs of the cards it needs. If the component has requested a cardset, the array will be in the order established on the server. Here, of course, the array has just a single ID.

Cards are accessed from the CardStore, as shown in the code. At this point, all cards are downloaded at the same time as the page itself, so cardStore.getCard() should never actually return undefined, but this may change in the future. It is important that the React component always have a reasonable behavior for the render method when no data is available.

Note that we call the dangerouslySetInnerHTML React workaround because the card body is converted Markdown. Not sure we'll keep doing it this way - it depends on what components need from cards.

SlideShow

The SlideShow component is very similar to SimpleCard. It receives a card set, rather than a single card, but this doesn't actually change the interface to the data (view the source code here).

The SlideShow component is, however, a useful example of a React component built on top of functionality provided by an outside library (here a jQuery plugin called FlexSlider). Note the need to call flexslider() on the component both on initial mount and after every update.

HistoryTable

NOTE: The rest of the documentation below is basically correct, but I am in the process of simplifying things before the June CityCampNC Hackathon. I will update this documentation to reflect that as soon as I am done - @ejaxon

The HistoryTable component illustrates two more advanced key aspects of the front-end framework: the DataModel interface and externalization of state. The code can be viewed here.

HistoryTable creates a simple table with all the rows of data associated with a multi-year dataset, aggregated over a sub-set of categories, restricted to just Expense and Revenue accounts, and thresholded to include only those line items totaling at least $0.01 in some year. In addition to the table, the component presents the user with a drop-down to toggle between revenue and expense data.

Properties

Properties here fall into two groups: the ones that are standard for all display components (componentData, and storeId) and those that are specific to this component, which are set locally.

There are two new properties specific to this component:

  • accountTypes - these are used in the select element for selecting revenue or expense data
  • dataInitialization - the initial configuration (or command) to pass into the DataModel that determines the base data configuration that will be used (more below).

IMPORTANT NOTE (5/9/2015): There will obviously be cases where component-specific properties will need to be set on the server. Component JSON definition files now have a new (required) 'props' property (in addition to 'data'). The 'props' property lets the component designer add configuration parameters that can be set by the user when adding to a page (or can simply expose them, without allowing them to be changed). The property is a simple object containing name-value pairs. The names are the names of the properties, the values are objects that have two required properties: 'value' and 'configurable'. The value property is, of course, the value that will be passed in to the component, by default. The 'configurable' property, if true, adds to the configuration dialog for the page component (i.e., in addition to selecting data, the user can set property values). Right now only a 'select' type of configurable property is supported - see the new BarchartExplorer.json component definition file for usage. The value configured by the user overrides that set by default.

Data Initialization

Another difference from the previous components is the need for data initialization. This applies to any component that uses dataset data.

In componentWillMount, the component uses the ApiActions module to fire off requests to the server for the data it will need (note that the IDs are accessed in props via the alldata tag in exactly the same way as for card data above). Rather than handle receipt and processing of the datasets directly, however, the component creates a DataModel to take care of this. It passes in the IDs of the datasets to be managed (in this case there are more than one), along with the initialization to be used.

The initialization has three specifications:

  • hierarchy, which specifies that only the Fund, Department, and Division categories are to be used and in that order. This causes the DataModel to filter out all other account data and to aggregate all any additional categories (e.g., Account, Cost Center, etc.) under the Division category.
  • accountTypes, which specifies that only revenue and expense accounts are to be retained.
  • amountThreshold, which causes line items that have no (absolute) value greater than $0.01 in any year to be discarded.

The ID of the data model created is stored in the external state. In earlier versions, it was stored locally, but that turned out to be a mistake. Switching away from a page and then back causes a new instance of the component to be created. In order to ensure that it accesses the same datamodel, it must uses its storeId to get information on the model. The first time it looks, it doesn't find it and so creates it. Subsequent calls just return the ide.

The last line of componentWillMount leads us to the next important topic.

Handling Component State

The MultiYearTable component has state that affects what data is displayed and changes in that state are linked to a select element. In most tutorials on React, this would be handled with a local state variable that would provide the current select value and be modified by the select element's onChange event handler.

In the GBE framework, however, intern component state should only be used when necessary to optimize interactive response. Otherwise, as here, all state changes pass through the dispatcher and the state itself is accessed during rendering through the StateStore.

During initialization of the app, every component is assigned a unique ID that it can use to access the state store. For the MultiYearTable, there is only one state value, namely which account type is selected. We register this state value in componentWillMount with the call

        stateStore.setComponentState(this.props.stateId, {selectedItem: AccountTypes.REVENUE});

and then access it in the render() function with:

        var selectedItem = stateStore.getComponentStateValue(this.props.stateId, 'selectedItem');

The significant point here though is the method for registering a change as a result of user interaction. We set onChange={this.onSelectChange} in the select element, where the onSelectChange method is defined as follows:

    onSelectChange: function(e) {
        dispatcher.dispatch({
            actionType: ActionTypes.COMPONENT_STATE_CHANGE,
            payload: {
                id: this.props.stateId,
                name: 'selectedItem',
                value: Number(e.target.value)
            }
        });
    }

There are a couple obvious advantages to handling state this way. First, because all state changes flow through a single dispatcher, it is trivial to implement something like logging. We may also save out the state of the entire application and recreate it at some other time, something that would be impossible if state were stored internally by each component.

Testing Components

When creating a new component, it is recommended that you write automated tests to ensure its behavior. We have set up a client side testing framework using Karma and Jasmine. Tests can be executed by running the command npm test. This will start the Karma test runner in continuous mode. You can keep this process running in the background and the tests will re-run every time you change any of the relevant files. If you are running the tests inside of the vagrant development environment they will run entirely in the background. If you are running them on your host machine, Karma will boot up an instance of the chrome browser to run the tests inside. If you do not have chrome installed, the tests will not run.

If you want to step into your tests and manually debug them, you can do so using the Chrome developer tools. If running inside of vagrant, boot up Chrome on the host and browse to port 9876 on the virtual server (192.168.33.27) and open up developer tools. If you are running the tests on your host machine simply open developer tools on the instance of Chrome automatically started when you ran the tests.

Tests should be stored in a folder named __tests__ next to the module they are actual testings. (i.e. resources/js/SimpleCard.js has a test at resources/js/__tests__/SimpleCard-test.js). Test filenames must always end in -test.js

Guidelines for writing tests

Component tests should be unit tests, so any dependencies that your component relies on should be mocked. In order to support mocking we use proxyquireify, which allows us to stub out any dependencies a component calls in via require. This is particularly important to ensure we do not have to fully set up store(s) that a component relies on.

Tests are written using the standard Jasmine describe it syntax. Your top level describe should just be the component name. If you have a number of different scenarios you are testing based on different component props, you should break them into sub describe blocks.

Much of your testing code will rely on methods defined in React.TestUtils, provided by the React framework. This namespace has methods to render components in your test DOM, inspect the current DOM contents, and otherwise validate component behavior. More details on these methods are available in the React documentation here.

Helper Methods

Along side proxyquireify, the testing framework also includes some custom helper methods to assist in test writing. These are listed below with explanation

given

The given method allows you to define lazily evaluated variables for a given set of tests. For those from a ruby background it's similar to rspec/minitests let() method. It helps remove boilerplate/duplicate code by allowing you to set up scenarios for a set of common tests and then override them to properly set up each individual test. See this gist for an overview of why you might want this sort of setup, and for an idea of how we implemented this method.

A simple example is provided for SimpleCard.js.

Optimizing Out Unnecessary Re-Rendering

Any change to the state store results in re-rendering of the entire app. In some cases this may be expensive, particularly around data-driven visualizations, etc. The MultiYearTable component illustrates one way to avoid recomputing things if there's been no change.

After a DataModel has been initialized, the output depends only on the commands passed into getData. So in shouldComponentUpdate we can check whether either the data has changed (perhaps because an additional dataset has been received from the server and processed into the model) or the command that we intend to send is different from the last one that the data model processed. In this case, the component need not track anything at all - it simply needs to know what the current request is and how to render given current state.

Gotchas

Invariant Violation: findComponentRoot

There are a number of things that can cause this, including inappropriate nesting of elements, skipping

tags, etc. The one that's gotten me a couple times is leaving spaces around the content of an option, e.g.,
   return <option value={x}> {x} </option>

This ends up generating

   <option value="x"><span> </span>{x}<span> </span></option>

but the spans are removed by the browser, causing the error.

Final Thoughts

This is a first iteration on both this documentation and on the framework. Please reach out (Twitter: @ejaxon) and let me know how to improve.