Skip to content

Instantly share code, notes, and snippets.

@syllog1sm
Created January 13, 2014 15:09
Show Gist options
  • Save syllog1sm/8401949 to your computer and use it in GitHub Desktop.
Save syllog1sm/8401949 to your computer and use it in GitHub Desktop.
Draft proposals for wrapping Bootstrap components with React.js
/** @jsx React.DOM */
'use strict';
// Components wrapping Twitter Bootstrap stuff
//
var BSNames = {
// This isn't exhaustive. Need to think through what should go here. Should
// be exclusive.
bsClass: {'column': 'col', 'button': 'btn', 'btn-group': 'btn-group', 'label': 'label',
'alert': 'alert', 'input-group': 'input-group', 'form': 'form'},
bsStyle: {'default': 'default', 'primary': 'primary', 'success': 'success',
'info': 'info', 'warning': 'warning', 'danger': 'danger',
'link': 'link', 'inline': 'inline', undefined: ''},
bsSize: {'large': 'lg', 'medium': 'md', 'small': 'sm', 'xsmall': 'xs', undefined: ''},
}
var Large = {getDefaultProps: function() { return {bsSize: 'large'};}};
var Medium = {getDefaultProps: function() { return {bsSize: 'medium'};}};
var Small = {getDefaultProps: function() { return {bsSize: 'small'};}};
var Default = {getDefaultProps: function() { return {bsStyle: 'default'};}};
var Primary = {getDefaultProps: function() { return {bsStyle: 'primary'};}};
var Success = {getDefaultProps: function() { return {bsStyle: 'success'};}};
var Info = {getDefaultProps: function() { return {bsStyle: 'info'};}};
var Warning = {getDefaultProps: function() { return {bsStyle: 'warning'};}};
var Danger = {getDefaultProps: function() { return {bsStyle: 'danger'};}};
var Link = {getDefaultProps: function() { return {bsStyle: 'link'};}};
var INPUT_TYPES = ['text', 'password', 'datetime', 'datetime-local', 'date',
'month', 'time', 'week', 'number', 'email', 'url', 'search', 'tel', 'color'];
var BootStrapMixin = {
propTypes: {
bsClass: React.PropTypes.oneOf(Object.keys(BSNames.bsClass)),
//bsStyle: React.PropTypes.oneOf(Object.keys(BSNames.bsStyle)),
bsSize: React.PropTypes.oneOf(Object.keys(BSNames.bsSize))
},
extendClassName: function() {
var classes = {};
if(this.props.className) {
this.props.className.split(' ').map(
function(klass){
classes[klass] = true;
}
);
}
if (this.props.clickover)
classes.hasPopover = true;
// Map into the Bootstrap names, from whatever aliases we've defined
var bsClass = BSNames.bsClass[this.props.bsClass];
var bsStyle = BSNames.bsStyle[this.props.bsStyle];
var bsSize = BSNames.bsSize[this.props.bsSize];
if (!bsClass)
return React.addons.classSet(classes);
var prefix = bsClass + '-';
// Additional classes we introduce
// For columns etc
if (this.props.bsNum) {
classes[prefix + this.props.bsNum] = true;
classes[prefix + bsSize + '-' + this.props.bsNum] = true;
}
// TODO: Check there are no conflicting values in className
classes[bsClass] = true;
if(bsStyle)
classes[prefix + bsStyle] = true;
if(bsSize)
classes[prefix + bsSize] = true;
// Merge with previous classes
// This produces a ' ' delineated string
// from all members of classes that evaluate as truthy
return React.addons.classSet(classes);
},
};
var Col = React.createClass({
mixins: [BootStrapMixin],
getDefaultProps: function() {return {bsClass: 'column'};},
render: function() {
return this.transferPropsTo(
<div className={this.extendClassName()}>
{this.props.children}
</div>
);
}
});
var Row = React.createClass({
render: function() {
return (<div className="row-fluid">{this.props.children}</div>)
}
});
var Button = React.createClass({
mixins: [BootStrapMixin],
getDefaultProps: function() {return {bsClass: 'button'};},
render: function() {
return this.transferPropsTo(
<button className={this.extendClassName()}>
{this.props.children}
</button>
);
},
componentDidMount: function() {
if (this.props.clickover) {
this.props.clickover.html = true;
this.props.clickover.trigger = 'click';
$(this.getDOMNode()).popover(this.props.clickover);
}
}
});
var ButtonGroup = React.createClass({
getDefaultProps: function() { return {bsClass: 'btn-group'};},
mixins: [BootStrapMixin],
render: function() {
return this.transferPropsTo(
<div className={this.extendClassName()}>
{this.props.children}
</div>
);
}
});
var InfoButton = React.createClass({
getDefaultProps: function() {return {hasPopover: true};},
mixins: [PopoverMixin, Success, Medium, Button],
render: function(){
return this.transferPropsTo(<Button>{this.props.children}</Button>);
},
});
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() {
$(this.getDOMNode()).modal({background: true, keyboard: true, show: false});
},
componentWillUnmount: function() {
$(this.getDOMNode()).off('hidden', this.handleHidden);
},
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" data-modal={this.props.modalID}>
<div className="modal-dialog">
<div className="modal-content">
<Body className="modal-header"/>
</div>
</div>
</div>
);
}
});
var PopoverMixin = {
};
var Clearfix = React.createClass({
render: function() {
return (<div className="clearfix"/>);
}
});
var Input = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
type: React.PropTypes.oneOf(INPUT_TYPES).isRequired,
placeholder: React.PropTypes.string,
label: React.PropTypes.string,
required: React.PropTypes.bool,
oneOf: React.PropTypes.array,
minLength: React.PropTypes.int
},
getInitialState: function() { return {}; },
getValue: function() {
return this.refs.input.getDOMNode().value;
},
renderInput: function(){
var className = "form-control input-md";
return <input type={this.props.type} className={className}
placeholder={this.props.placeholder} ref="input"/>;
},
renderLabel: function(){
return <label>{this.props.label}</label> ? this.props.label : undefined;
},
render: function(){
var className = "form-group";
if (this.state.error)
className += ' has-error';
return (
<div className={className} onBlur={this.onBlur} onFocus={this.onFocus}>
{this.renderInput()}
{this.renderLabel()}
</div>
);
},
onBlur: function(e){
var value = this.getValue();
var error;
if (this.props.required && !value)
error = 'required';
else if (this.props.oneOf && !(value in this.props.oneOf))
error = 'oneOf';
else if (this.props.minLength && value.length < this.props.minLength)
error = 'minLength';
this.setState({error: error});
},
onFocus: function(e) {
this.setState({error: false});
e.stopPropagation();
}
});
var Form = React.createClass({
mixins: [BootStrapMixin],
propTypes: {
callback: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
bsClass: "form"
};
},
onSubmit: function(e) {
e.preventDefault();
if (e.target.type == 'submit')
this.props.callback(this.getValues());
},
render: function(){
var className = this.extendClassName();
return this.transferPropsTo(
<form onClick={this.onSubmit} className={className} role="form" action="#">
{this.props.children}
</form>
);
},
getValues: function() {
var values= {errors: {}};
var err;
jQuery.each(this.props.children, function(idx, child){
if (child && child.props.name) {
values[child.props.name] = child.getValue();
err = child.state.error;
if (err)
values.errors[child.props.name] = err;
}
});
return values;
},
});
var TabbedArea = React.createClass({
propTypes: {
// Array of POJO objects for panes
paneModels: React.PropTypes.array.isRequired,
activeTab: React.PropTypes.number.isRequired,
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} ref={"tab" + idx} name={panePojo.tabName}
// This bubbles up to the router (above the Controller).
onClick={this.handleClick.bind(this, idx)}
isActive={idx === this.props.activeTab}
/>
);
}.bind(this));
},
renderPanes: function() {
return this.props.paneModels.map(function(panePojo, idx) {
panePojo.classes['tab-pane'] = true;
panePojo.classes.active = idx === this.props.activeTab;
return (<div key={idx} ref={"pane" + idx}
className={React.addons.classSet(panePojo.classes)}>
{panePojo.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>);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment