Better Bootstrap modals and popover with React.js

Updated snippet at the end of the post, via a tip from Steve Hunt.

I’m pretty new to frontend development (I do NLP research), but the way Bootstrap worked always bugged me. I think React offers a way to make things a lot cleaner, but I’m not sure whether the solutions I’m coming up with are crazy. This post is a quick note to get feedback from the React community.

Bootstrap modals

Let’s consider a basic modal use-case. You want to have a button that triggers a modal.

In Twitter Bootstrap, you’ll write two elements:

  1. The payload: a div with the modal window, which will be hidden at first;
  2. The trigger: a link to activate the modal, like a button.
  3. The trigger can be linked to the payload by setting a data attribute on the trigger element, with a css selector identifying it. Alternatively, you can do it via event handling in javascript, catching the onClick method, fetching the payload element, and calling payload.modal(‘show’).

    You also need to do some initialization, either by setting data attributes on the payload element, or by calling a javascript function and passing in parameters.

    What I hate about this

    My beef with this set-up is that I need to manage the linkage between the trigger and the payload. Usually, this means adding an id to the payload element, and referencing this id in the trigger’s data-target attribute.

    React.js components

    Facebook’s React.js lets you define custom components, which encapsulate html, javascript and css properties in one place. This is a big win over traditional templating imo, because often javascript behaviours and html structures are inherently coupled, so they ought to be defined in the same place. An example component for the payload div, written in jsx syntax:

    
    var ModalPayload = React.createClass({
        componentDidMount: function() {
            // These can be configured via options; this is just a demo
            $(this.getDOMNode()).modal({background: true, keyboard: true, show: false});
        },
    
        componentWillUnmount: function() {
            $(this.getDOMNode()).off('hidden', this.handleHidden);
        },
        render: getDefaultProps() {
            return {Header: React.DOM.div, Body: React.DOM.div, Footer: React.DOM.div};
        }
        render: function() {
            var Header = this.props.header;
            var Body = this.props.body;
            var Footer = this.props.footer;
            return (
                <div className="modal fade" role="dialog" aria-hidden="true" data-modalID={this.props.modalID}>
                    <div className="modal-dialog">
                        <div className="modal-content">
                            <Header className="modal-header"/>
                            <Body className="modal-body"/>
                            <Footer className="modal-footer"/>
                        </div>
                    </div>
                </div>
            );
        }
    });
    

    The idea is to provide a reusable wrapper around Twitter’s modals, handling their quirky mark-up structures and invocation calls. Client code would define their modal’s header, body and footer content as once-off assets, and use them as arguments to the ModalPayload component, which wraps them. Example client usage:

    <ModalPayload header={MyHeaderAsset} body={MyBodyAsset}/>
    

    Here the footer will be filled by an empty div. So far so good.

    But, how do we write our trigger? The obvious solution is to have a ModalTrigger component, which we pass in a matching modalID string, which would communicate with the payload via the DOM. That doesn’t really seem like how React is expecting me to do business, though, and I hate writing matching ID strings like that.

    Linking via the DOM

    It would please my black heart if I could write a single component which would own both the trigger and the payload:

        <ModalTrigger trigger={MyTriggerButton} header={MyHeader} body={MyBody} footer={MyFooter}/>
    

    The problem is that funny things happen if the payload element is a sibling of the trigger in the DOM. The standard practice is to put the payloads at the end of the body, to be unhidden and re-positioned when the trigger is fired.

    So: I want to write a component which “owns” an element that won’t be written as a child of it in the DOM. It turns out I had to fight React pretty hard to make this happen, which suggests it’s not a good idea. I did come up with a solution, though. My black heart is not yet fully pleased, but I’d say I’m about 90%.

    What I do is pass in a callback that tells the ModalTrigger component where to the payload:

    
    var ModalTrigger = React.createClass({
        handleClick: function(e) {
            $('[data-modal=' + this.props.modalID + ']').modal();
        },
        render: function() {
            var Trigger = this.props.trigger;
            return (<div onClick={this.handleClick}><Trigger/></div>);
        },
        componentDidMount: function() {
            this.props.renderTo(<Modal modalID={this.props.modalID} body={this.props.body}></Modal>);
        }
    });
    

    The docs indicate that render() should have no side-effects, and that this logic should be handled in componentDidMount.

    We still communicate between the trigger and the payload via the DOM, but at least now the arbitrary ID string is only defined once. I tried using refs, but this doesn’t seem to work for elements that don’t have parent/child relations.

    The renderTo callback is managed by the top-level component, which is my app’s controller:

    var Controller = React.createComponent({
        renderHidden: function(component) {
            var hidden = this.refs.hiddenNodes;
            hidden.setState({
                childNodes: hidden.state.childNodes.concat(hidden.childNodes, [component])
            });
        }
    
       render: function() {
         <div className="container-fluid">
            <HiddenNodes ref="hiddenNodes">
            </HiddenNodes>
            <div>
               <h1>Hello possums</h1>
               <div>
                  <ModalTrigger trigger={MyTriggerAsset} body={MyBodyAsset} modalID={"arbitraryString"} renderTo={this.renderHidden}/>
               </div>
            </div>
         </div>
       }
    });
    
    var HiddenNodes = React.createClass({
        getInitialState: function() {
            return {childNodes: []};
        },
        
        render: function() {
          return (<div>{this.state.childNodes}</div>);
        }
    });
    

    I think a mixin could handle this boiler-plate for the controller.

    So. There are surely better ways. What should I do? Just, “don’t want this”? It’s a lot of trouble to go to avoid passing around ID strings. But the component should be quite reusable. Getting rid of that for good is worth some deviousness, in my opinion.

    No matter what the eventual solution looks like, I think publishing nice React components for Twitter Bootstrap modals, popovers and tooltips is a good idea. These receive a high volume of questions on stackoverflow, and involve a lot of fiddly boilerplate. I’ve only been playing with React for a day or so now, but it seems clear that it offers a better way.

    UPDATE: After talking to Steve Hunt on IRC (#reactjs on freenode is very responsive — you should join!), I was led to a much much simpler solution. Steve asked me what the problem was with having the payload as a sibling of the trigger on the DOM, so I took another look and found a very simple fix — it was just that the click handler on the modal would bubble events back up to the trigger. Easy: e.stopPropagation.

    So this works pretty well:

    
    var ModalTrigger = React.createClass({
        handleClick: function(e) {
            $(this.refs.payload.getDOMNode()).modal();
        },
        render: function() {
            var Trigger = this.props.trigger;
            return (<div onClick={this.handleClick}>
                <Trigger/>
                <Modal ref="payload"
                    header={this.props.header}
                    body={this.props.body}
                    footer={this.props.footer}>
                </Modal>;
            </div>);
        },
    });
    
    var Modal = React.createClass({
        componentDidMount: function() {
            // Initialize the modal, once we have the DOM node
            // TODO: Pass these in via props
            $(this.getDOMNode()).modal({background: true, keyboard: true, show: false});
        },
        componentWillUnmount: function() {
            $(this.getDOMNode()).off('hidden');
        },
        // This was the key fix --- stop events from bubbling
        handleClick: function(e) {
            e.stopPropagation();
        },
        render: function() {
            var Header = this.props.header;
            var Body = this.props.body;
            var Footer = this.props.footer;
            return (
                <div onClick={this.handleClick} className="modal fade" role="dialog" aria-hidden="true">
                    <div className="modal-dialog">
                        <div className="modal-content">
                            <Body className="modal-header"/>
                        </div>
                    </div>
                </div>
            );
        }
    });
    

    Black heart, pleased.

    Advertisement

2 thoughts on “Better Bootstrap modals and popover with React.js

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 )

Connecting to %s