@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
139 lines (120 loc) • 3.99 kB
JavaScript
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
}