'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() { const expanded = !this.state.expanded; this.setExpanded(expanded); // Reset the value to be the initial value when the checkbox is unticked since the // user doesn't want to specify their own value. if (!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 (

{this.props.label} {this.state.errors}

); } else { return null; } }, getCheckbox() { const { checkboxLabel, name } = this.props; if (checkboxLabel) { return (
{' ' + checkboxLabel}
); } else { return null; } }, render() { let footer = null; if(this.props.footer){ footer = (
{this.props.footer}
); } return (
{this.getCheckbox()}
{this.getLabelAndErrors()} {this.renderChildren(this.props.style)} {footer}
); } }); export default Property;