UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

139 lines (120 loc) 3.99 kB
import React, {Component} from 'react' import PropTypes from 'prop-types' import get from 'lodash/get' import isEqual from 'lodash/isEqual' import pick from 'lodash/pick' // TODO: Kill this HOC and use composition instead. This thing is a nightmare to maintain... /** * Higher Order Component used in Edit views to pass generic helper functions */ export default function withEditTools(WrappedComponent) { class Wrapper extends Component { static displayName = `${this.name}(${WrappedComponent.displayName})` // [jsimon]: I originally tried to switch these PropTypes.object calls to WrappedComponent.propTypes.<name> but // these props don't exist on every edit component. To make matters worse, their shape isn't consistent so we can't // make these more specific. static propTypes = { changeItemState: PropTypes.func.isRequired, errors: PropTypes.object, errorsAreShowing: PropTypes.bool, interactionData: PropTypes.object, itemBody: PropTypes.string, properties: PropTypes.object, scoringData: PropTypes.object, children: PropTypes.node, } static defaultProps = { errors: void 0, errorsAreShowing: void 0, interactionData: void 0, itemBody: void 0, properties: void 0, scoringData: void 0, children: void 0, } node = null // If the props passed to the component on mount result in validation // errors, call the changeItemState callback with the validation errors to // notify the parent component componentDidMount() { const itemState = this.getItemState() if (Object.keys(itemState.errors).length > 0) { this.props.changeItemState(itemState) } } componentDidUpdate(prevProps) { const itemState = this.getItemState() if (!isEqual(itemState.errors, prevProps.errors)) { this.props.changeItemState(itemState) } } getItem(props = this.props) { return pick(props, [ 'calculatorType', 'interactionData', 'itemBody', 'properties', 'scoringAlgorithm', 'scoringData', ]) } getItemErrors(item) { return WrappedComponent.interactionType.getErrors(item) } getItemState(props = this.props) { const item = this.getItem(props) const errors = this.getItemErrors(item) return {...item, errors} } /** * default handling of stem changes */ handleDescriptionChange = itemBody => this.props.changeItemState({itemBody}) /** * helper function that gives out errors in instui format */ getErrors = (path, fallback = []) => { const errors = get(this.props.errors, path) if (errors != null && this.props.errorsAreShowing) { return formatErrors(errors) } return fallback } getWrappedComponent = () => this.node handleRef = node => { this.node = node } /** * render the decorated component, passing helpers through props */ render() { return ( <WrappedComponent ref={this.handleRef} onDescriptionChange={this.handleDescriptionChange} getErrors={this.getErrors} {...this.props} /> ) } } Wrapper.WrappedComponent = WrappedComponent return Wrapper } // These can be included by components that use withEditTools, to provide // propType validation without including these private, internal props in the // auto-generated documentation. withEditTools.injectedProps = { getErrors: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired, } function formatErrors(node) { if (typeof node === 'string') { return {text: node, type: 'error'} } else if (Array.isArray(node)) { return node.map(formatErrors) } else if (typeof node === 'object') { return Object.keys(node).reduce((obj, key) => ({...obj, [key]: formatErrors(node[key])}), {}) } return node }