Bootstrap TabbedArea component with React.js

This post is another small note about designing a component library for Twitter Bootstrap using React.js.

Here is how tabbed content regions are created using Bootstrap:

<div>
  <ul class="nav nav-tabs" id="myTab">
    <li class="active"><a href="#home" data-toggle="tab">Home</a></li>
    <li><a href="#profile" data-toggle="tab">Profile</a></li>
    <li><a href="#messages" data-toggle="tab">Messages</a></li>
    <li><a href="#settings" data-toggle="tab">Settings</a></li>
  </ul>

  <div class="tab-content">
    <div class="tab-pane active" id="home">...</div>
    <div class="tab-pane" id="profile">...</div>
    <div class="tab-pane" id="messages">...</div>
    <div class="tab-pane" id="settings">...</div>
  </div>

<script>
  $(function () {
    $('#myTab a:last').tab('show')
  })
</script>

React makes it possible to use the Bootstrap css and supply a much better api:

var MyTabbed = React.createComponent({
  getInitialState: function () {return {activeTab: 0}},
  
  switchTab: function (idx) { this.setState({activeTab: idx}); },

  paneModels: [
    {tabName: "Home", children: [...]},
    {tabName: "Profile", children: [...]},
    {tabName: "Messages", children: [...]},
    {tabName: "Settings", children: [...]}
  ],

  render: function() {
    return (
       <TabbedArea
           paneModels={this.paneModels}
           activeTab={this.state.activeTab}
           switchTab={this.switchTab}
       />);
    }
});

The component library will contain the TabbedArea component, which the parent element will pass an array of objects describing the pane names, their contents, and any other properties we might want to attach. Instead of telling the tabs how to toggle the panes, we maintain a state variable, and supply a callback that manipulates it. It’s taken me a while to get used to this way of designing things, but once it clicks and you use React as it’s intended, the code can be very simple.

Bootstrap has over 60,000 users according to GitHub, and I think the React-wrapped components will be significantly better. So I’m quite excited about this project. The advantage of the React version is not just in the lines of code. The parallel tabs and panes lists in the Bootstrap markup will be much more difficult to manipulate programmatically, and the code that does so will live in some random part of the javascript, separated from the objects it is acting upon, even though the two are entirely coupled. The React version is simply better code.

Here is how I’ve implemented TabbedArea. There are still various styling classes that need to be exposed as parameters, like the option to justify the header tabs, or to use tab styling instead of pills. And maybe there’s another design, which allows us to ship a better api still. I don’t see why not. Any ideas?



var TabbedArea = React.createClass({
    propTypes: {
        // Array of POJO objects for panes
        // Each model should have:
        // classes: object, tabName: string, children: array of objects
        paneModels: React.PropTypes.array.isRequired,
        // The index of the currently active tab
        activeTab: React.PropTypes.number.isRequired,
        // Callback for when a tab is switched.
        // Expects one argument: index of new tab.
        switchTab: React.PropTypes.func.isRequired
    },

    handleClick: function(idx, e) {
        e.preventDefault();
        this.props.switchTab(idx);
    },

    render: function() {
        return this.transferPropsTo(
            <div>
                <ul className="nav nav-pills nav-justified">
                    {this.renderTabs()}
                </ul>
                <div className="tab-content">
                    {this.renderPanes()}
                </div>
            </div>
        );
    },

    renderTabs: function() {
        return this.props.paneModels.map(function(panePojo, idx) {
            return (
                <Tab key={idx} name={panePojo.tabName} 
                    // Note that React handles onClick itself. This isn't
                    // a raw, DOM-attached event handler.
                    onClick={this.handleClick.bind(this, idx)}
                    isActive={idx === this.props.activeTab}
                />
            );
        }.bind(this));
    },

    renderPanes: function() {
        return this.props.paneModels.map(function(paneModel, idx) {
            paneModel.classes['tab-pane'] = true;
            paneModel.classes.active = idx === this.props.activeTab;
            // Build classname string out of all truthy classes
            return (<div key={idx} className={React.addons.classSet(paneModel.classes)}>
                        {paneModel.children}
                    </div>);
        }.bind(this));
    },
});


var Tab = React.createClass({
    propTypes: {
        isActive: React.PropTypes.bool.isRequired,
        onClick: React.PropTypes.func.isRequired
    },

    render: function() {
        var className = React.addons.classSet({active: this.props.isActive})
        return (<li className={className} onClick={this.props.onClick}>
                    <a href="#">{this.props.name}</a>
                </li>);
    }
});

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s