Skip to content

Commit 37c3947

Browse files
committed
Merge pull request react-bootstrap#427 from react-bootstrap/collapsable-updates
[changed] CollapsableMixin state tracking
2 parents b2e86f6 + befed83 commit 37c3947

10 files changed

+449
-95
lines changed

docs/examples/CollapsableParagraph.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
var CollapsableParagraph = React.createClass({
2+
mixins: [CollapsableMixin],
3+
4+
getCollapsableDOMNode: function(){
5+
return this.refs.panel.getDOMNode();
6+
},
7+
8+
getCollapsableDimensionValue: function(){
9+
return this.refs.panel.getDOMNode().scrollHeight;
10+
},
11+
12+
onHandleToggle: function(e){
13+
e.preventDefault();
14+
this.setState({expanded:!this.state.expanded});
15+
},
16+
17+
render: function(){
18+
var styles = this.getCollapsableClassSet();
19+
var text = this.isExpanded() ? 'Hide' : 'Show';
20+
return (
21+
<div>
22+
<Button onClick={this.onHandleToggle}>{text} Content</Button>
23+
<div ref="panel" className={classSet(styles)}>
24+
{this.props.children}
25+
</div>
26+
</div>
27+
);
28+
}
29+
});
30+
31+
var panelInstance = (
32+
<CollapsableParagraph>
33+
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
34+
</CollapsableParagraph>
35+
);
36+
37+
React.render(panelInstance, mountNode);

docs/src/ComponentsPage.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ var ComponentsPage = React.createClass({
207207
<h3 id="panels-accordion">Accordions</h3>
208208
<p><code>&lt;Accordion /&gt;</code> aliases <code>&lt;PanelGroup accordion /&gt;</code>.</p>
209209
<ReactPlayground codeText={fs.readFileSync(__dirname + '/../examples/PanelGroupAccordion.js', 'utf8')} />
210+
211+
<h3 id="panels-collapsable">Collapsable Mixin</h3>
212+
<p><code>CollapsableMixin</code> can be used to create your own components with collapse functionality.</p>
213+
<ReactPlayground codeText={fs.readFileSync(__dirname + '/../examples/CollapsableParagraph.js', 'utf8')} />
210214
</div>
211215

212216
<div className="bs-docs-section">

docs/src/ReactPlayground.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var Badge = require('../../lib/Badge');
88
var Button = require('../../lib/Button');
99
var ButtonGroup = require('../../lib/ButtonGroup');
1010
var ButtonToolbar = require('../../lib/ButtonToolbar');
11+
var CollapsableMixin = require('../../lib/CollapsableMixin');
1112
var Carousel = require('../../lib/Carousel');
1213
var CarouselItem = require('../../lib/CarouselItem');
1314
var Col = require('../../lib/Col');

src/BootstrapMixin.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ var BootstrapMixin = {
2929
}
3030

3131
return classes;
32+
},
33+
34+
prefixClass: function(subClass) {
35+
return constants.CLASSES[this.props.bsClass] + '-' + subClass;
3236
}
3337
};
3438

35-
module.exports = BootstrapMixin;
39+
module.exports = BootstrapMixin;

src/CollapsableMixin.js

Lines changed: 107 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,149 @@
11
var React = require('react');
2-
var TransitionEvents = require('./utils/TransitionEvents');
2+
var TransitionEvents = require('react/lib/ReactTransitionEvents');
33

44
var CollapsableMixin = {
55

66
propTypes: {
7-
collapsable: React.PropTypes.bool,
87
defaultExpanded: React.PropTypes.bool,
98
expanded: React.PropTypes.bool
109
},
1110

12-
getInitialState: function () {
11+
getInitialState: function(){
12+
var defaultExpanded = this.props.defaultExpanded != null ?
13+
this.props.defaultExpanded :
14+
this.props.expanded != null ?
15+
this.props.expanded :
16+
false;
17+
1318
return {
14-
expanded: this.props.defaultExpanded != null ? this.props.defaultExpanded : null,
19+
expanded: defaultExpanded,
1520
collapsing: false
1621
};
1722
},
1823

19-
handleTransitionEnd: function () {
20-
this._collapseEnd = true;
21-
this.setState({
22-
collapsing: false
23-
});
24-
},
25-
26-
componentWillReceiveProps: function (newProps) {
27-
if (this.props.collapsable && newProps.expanded !== this.props.expanded) {
28-
this._collapseEnd = false;
29-
this.setState({
30-
collapsing: true
31-
});
24+
componentWillUpdate: function(nextProps, nextState){
25+
var willExpanded = nextProps.expanded != null ? nextProps.expanded : nextState.expanded;
26+
if (willExpanded === this.isExpanded()) {
27+
return;
3228
}
33-
},
3429

35-
_addEndTransitionListener: function () {
30+
// if the expanded state is being toggled, ensure node has a dimension value
31+
// this is needed for the animation to work and needs to be set before
32+
// the collapsing class is applied (after collapsing is applied the in class
33+
// is removed and the node's dimension will be wrong)
34+
3635
var node = this.getCollapsableDOMNode();
36+
var dimension = this.dimension();
37+
var value = '0';
3738

38-
if (node) {
39-
TransitionEvents.addEndEventListener(
40-
node,
41-
this.handleTransitionEnd
42-
);
39+
if(!willExpanded){
40+
value = this.getCollapsableDimensionValue();
4341
}
42+
43+
node.style[dimension] = value + 'px';
44+
45+
this._afterWillUpdate();
4446
},
4547

46-
_removeEndTransitionListener: function () {
47-
var node = this.getCollapsableDOMNode();
48+
componentDidUpdate: function(prevProps, prevState){
49+
// check if expanded is being toggled; if so, set collapsing
50+
this._checkToggleCollapsing(prevProps, prevState);
4851

49-
if (node) {
50-
TransitionEvents.removeEndEventListener(
51-
node,
52-
this.handleTransitionEnd
53-
);
54-
}
52+
// check if collapsing was turned on; if so, start animation
53+
this._checkStartAnimation();
54+
},
55+
56+
// helps enable test stubs
57+
_afterWillUpdate: function(){
5558
},
5659

57-
componentDidMount: function () {
58-
this._afterRender();
60+
_checkStartAnimation: function(){
61+
if(!this.state.collapsing) {
62+
return;
63+
}
64+
65+
var node = this.getCollapsableDOMNode();
66+
var dimension = this.dimension();
67+
var value = this.getCollapsableDimensionValue();
68+
69+
// setting the dimension here starts the transition animation
70+
var result;
71+
if(this.isExpanded()) {
72+
result = value + 'px';
73+
} else {
74+
result = '0px';
75+
}
76+
node.style[dimension] = result;
5977
},
6078

61-
componentWillUnmount: function () {
62-
this._removeEndTransitionListener();
79+
_checkToggleCollapsing: function(prevProps, prevState){
80+
var wasExpanded = prevProps.expanded != null ? prevProps.expanded : prevState.expanded;
81+
var isExpanded = this.isExpanded();
82+
if(wasExpanded !== isExpanded){
83+
if(wasExpanded) {
84+
this._handleCollapse();
85+
} else {
86+
this._handleExpand();
87+
}
88+
}
6389
},
6490

65-
componentWillUpdate: function (nextProps) {
66-
var dimension = (typeof this.getCollapsableDimension === 'function') ?
67-
this.getCollapsableDimension() : 'height';
91+
_handleExpand: function(){
6892
var node = this.getCollapsableDOMNode();
93+
var dimension = this.dimension();
94+
95+
var complete = (function (){
96+
this._removeEndEventListener(node, complete);
97+
// remove dimension value - this ensures the collapsable item can grow
98+
// in dimension after initial display (such as an image loading)
99+
node.style[dimension] = '';
100+
this.setState({
101+
collapsing:false
102+
});
103+
}).bind(this);
104+
105+
this._addEndEventListener(node, complete);
69106

70-
this._removeEndTransitionListener();
107+
this.setState({
108+
collapsing: true
109+
});
71110
},
72111

73-
componentDidUpdate: function (prevProps, prevState) {
74-
this._afterRender();
112+
_handleCollapse: function(){
113+
var node = this.getCollapsableDOMNode();
114+
115+
var complete = (function (){
116+
this._removeEndEventListener(node, complete);
117+
this.setState({
118+
collapsing: false
119+
});
120+
}).bind(this);
121+
122+
this._addEndEventListener(node, complete);
123+
124+
this.setState({
125+
collapsing: true
126+
});
75127
},
76128

77-
_afterRender: function () {
78-
if (!this.props.collapsable) {
79-
return;
80-
}
129+
// helps enable test stubs
130+
_addEndEventListener: function(node, complete){
131+
TransitionEvents.addEndEventListener(node, complete);
132+
},
81133

82-
this._addEndTransitionListener();
83-
setTimeout(this._updateDimensionAfterRender, 0);
134+
// helps enable test stubs
135+
_removeEndEventListener: function(node, complete){
136+
TransitionEvents.removeEndEventListener(node, complete);
84137
},
85138

86-
_updateDimensionAfterRender: function () {
87-
var node = this.getCollapsableDOMNode();
88-
if (node) {
89-
var dimension = (typeof this.getCollapsableDimension === 'function') ?
90-
this.getCollapsableDimension() : 'height';
91-
node.style[dimension] = this.isExpanded() ?
92-
this.getCollapsableDimensionValue() + 'px' : '0px';
93-
}
139+
dimension: function(){
140+
return (typeof this.getCollapsableDimension === 'function') ?
141+
this.getCollapsableDimension() :
142+
'height';
94143
},
95144

96-
isExpanded: function () {
97-
return (this.props.expanded != null) ?
98-
this.props.expanded : this.state.expanded;
145+
isExpanded: function(){
146+
return this.props.expanded != null ? this.props.expanded : this.state.expanded;
99147
},
100148

101149
getCollapsableClassSet: function (className) {

0 commit comments

Comments
 (0)