1
0
mirror of https://github.com/ascribe/onion.git synced 2025-01-21 02:01:56 +01:00
onion/js/components/ascribe_forms/property.js
Brett Sun fb917f1a09 Reset the Property value when its checkbox is unselected
The user doesn’t want to apply the property, so it should reset to
whatever it is by default.
2015-12-16 14:50:39 +01:00

343 lines
11 KiB
JavaScript

'use strict';
import React from 'react';
import ReactAddons from 'react/addons';
import Panel from 'react-bootstrap/lib/Panel';
import AppConstants from '../../constants/application_constants';
import { mergeOptions } from '../../utils/general_utils';
const { bool, element, string, oneOfType, func, object, arrayOf } = React.PropTypes;
const Property = React.createClass({
propTypes: {
editable: bool,
// If we want Form to have a different value for disabled as Property has one for
// editable, we need to set overrideForm to true, as it will then override Form's
// disabled value for individual Properties
overrideForm: bool,
label: string,
value: oneOfType([
string,
element
]),
footer: element,
handleChange: func,
ignoreFocus: bool,
name: string.isRequired,
className: string,
onClick: func,
onChange: func,
onBlur: func,
children: oneOfType([
arrayOf(element),
element
]),
style: object,
expanded: bool,
checkboxLabel: string,
autoFocus: bool
},
getDefaultProps() {
return {
editable: true,
expanded: true,
className: ''
};
},
getInitialState() {
const { expanded, ignoreFocus, checkboxLabel } = this.props;
return {
// We're mirroring expanded here as a state
// React's docs do NOT consider this an antipattern as long as it's
// not a "source of truth"-duplication
expanded,
// When a checkboxLabel is defined in the props, we want to set
// `ignoreFocus` to true
ignoreFocus: ignoreFocus || checkboxLabel,
// Please don't confuse initialValue with react's defaultValue.
// initialValue is set by us to ensure that a user can reset a specific
// property (after editing) to its initial value
initialValue: null,
value: null,
isFocused: false,
errors: null
};
},
componentDidMount() {
if(this.props.autoFocus) {
this.handleFocus();
}
},
componentWillReceiveProps(nextProps) {
let childInput = this.refs.input;
// For expanded there are actually three use cases:
//
// 1. Control its value from the outside completely (do not define `checkboxLabel`)
// 2. Let it be controlled from the inside (default value can be set though via `expanded`)
// 3. Let it be controlled from a child by using `setExpanded` (`expanded` must not be
// set from the outside as a prop then(!!!))
//
// This handles case 1. and 3.
if(nextProps.expanded !== this.props.expanded && nextProps.expanded !== this.state.expanded && !this.props.checkboxLabel) {
this.setState({ expanded: nextProps.expanded });
}
// In order to set this.state.value from another component
// the state of value should only be set if its not undefined and
// actually references something
if(childInput && typeof childInput.getDOMNode().value !== 'undefined') {
this.setState({
value: childInput.getDOMNode().value
});
// When implementing custom input components, their value isn't exposed like the one
// from native HTML elements.
// To enable developers to create input elements, they can expose a property called value
// in their state that will be picked up by property.js
} else if(childInput && childInput.state && typeof childInput.state.value !== 'undefined') {
this.setState({
value: childInput.state.value
});
}
if(!this.state.initialValue && childInput && childInput.props.defaultValue) {
this.setState({
initialValue: childInput.props.defaultValue
});
}
},
reset() {
let input = this.refs.input;
// maybe do reset by reload instead of front end state?
this.setState({value: this.state.initialValue});
if(input.state && input.state.value) {
// resets the value of a custom react component input
input.state.value = this.state.initialValue;
}
// For some reason, if we set the value of a non HTML element (but a custom input),
// after a reset, the value will be be propagated to this component.
//
// Therefore we have to make sure only to reset the initial value
// of HTML inputs (which we determine by checking if there 'type' attribute matches
// the ones included in AppConstants.possibleInputTypes).
let inputDOMNode = input.getDOMNode();
if(inputDOMNode.type && typeof inputDOMNode.type === 'string' &&
AppConstants.possibleInputTypes.indexOf(inputDOMNode.type.toLowerCase()) > -1) {
inputDOMNode.value = this.state.initialValue;
}
// For some inputs, reseting state.value is not enough to visually reset the
// component.
//
// So if the input actually needs a visual reset, it needs to implement
// a dedicated reset method.
if(typeof input.reset === 'function') {
input.reset();
}
},
handleChange(event) {
this.props.handleChange(event);
if (typeof this.props.onChange === 'function') {
this.props.onChange(event);
}
this.setState({value: event.target.value});
},
handleFocus() {
// if ignoreFocus (bool) is defined, then just ignore focusing on
// the property and input
if(this.state.ignoreFocus) {
return;
}
// if onClick is defined from the outside,
// just call it
if(typeof this.props.onClick === 'function') {
this.props.onClick();
}
// skip the focus of non-input elements
let nonInputHTMLElements = ['pre', 'div'];
if (this.refs.input &&
nonInputHTMLElements.indexOf(this.refs.input.getDOMNode().nodeName.toLowerCase()) > -1 ) {
return;
}
this.refs.input.getDOMNode().focus();
this.setState({
isFocused: true
});
},
handleBlur(event) {
this.setState({
isFocused: false
});
if(typeof this.props.onBlur === 'function') {
this.props.onBlur(event);
}
},
handleSuccess(){
this.setState({
isFocused: false,
errors: null,
// also update initialValue in case of the user updating and canceling its actions again
initialValue: this.refs.input.getDOMNode().value
});
},
setErrors(errors){
this.setState({
errors: errors.pop()
});
},
clearErrors(){
this.setState({errors: null});
},
getClassName() {
if(!this.state.expanded && !this.props.checkboxLabel){
return 'is-hidden';
}
if(!this.props.editable){
return 'is-fixed';
}
if (this.state.errors){
return 'is-error';
}
if(this.state.isFocused) {
return 'is-focused';
} else {
return '';
}
},
setExpanded(expanded) {
this.setState({ expanded });
},
handleCheckboxToggle() {
this.setExpanded(!this.state.expanded);
this.setState({
value: this.state.initialValue
});
},
renderChildren(style) {
// Input's props should only be cloned and propagated down the tree,
// if the component is actually being shown (!== 'expanded === false')
if((this.state.expanded && this.props.checkboxLabel) || !this.props.checkboxLabel) {
return ReactAddons.Children.map(this.props.children, (child) => {
// Since refs will be overriden by this functions return statement,
// we still want to be able to define refs for nested `Form` or `Property`
// children, which is why we're upfront simply invoking the callback-ref-
// function before overriding it.
if(typeof child.ref === 'function' && this.refs.input) {
child.ref(this.refs.input);
}
return React.cloneElement(child, {
style,
onChange: this.handleChange,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled: !this.props.editable,
ref: 'input',
name: this.props.name,
setExpanded: this.setExpanded
});
});
}
},
getLabelAndErrors() {
if(this.props.label || this.state.errors) {
return (
<p>
<span className="pull-left">{this.props.label}</span>
<span className="pull-right">{this.state.errors}</span>
</p>
);
} else {
return null;
}
},
getCheckbox() {
const { checkboxLabel } = this.props;
if(checkboxLabel) {
return (
<div
className="ascribe-property-collapsible-toggle"
onClick={this.handleCheckboxToggle}>
<input
onChange={this.handleCheckboxToggle}
type="checkbox"
checked={this.state.expanded}
ref="checkboxCollapsible"/>
<span className="checkbox">{' ' + checkboxLabel}</span>
</div>
);
} else {
return null;
}
},
render() {
let footer = null;
if(this.props.footer){
footer = (
<div className="ascribe-property-footer">
{this.props.footer}
</div>
);
}
return (
<div
className={'ascribe-property-wrapper ' + this.getClassName()}
onClick={this.handleFocus}
style={this.props.style}>
{this.getCheckbox()}
<Panel
collapsible
expanded={this.state.expanded}
className="bs-custom-panel">
<div className={'ascribe-property ' + this.props.className}>
{this.getLabelAndErrors()}
{this.renderChildren(this.props.style)}
{footer}
</div>
</Panel>
</div>
);
}
});
export default Property;