Skip to content

Commit

Permalink
Make InputLiteral a functional component
Browse files Browse the repository at this point in the history
  • Loading branch information
jcoyne committed Aug 2, 2019
1 parent 415bb2c commit cd504ce
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 127 deletions.
73 changes: 30 additions & 43 deletions __tests__/components/editor/property/InputLiteral.test.js
Expand Up @@ -4,7 +4,12 @@ import 'jsdom-global/register'
import React from 'react'
import { shallow } from 'enzyme'
import shortid from 'shortid'
import { InputLiteral } from 'components/editor/property/InputLiteral'
import InputLiteral from 'components/editor/property/InputLiteral'

const reduxPath = [
'resourceTemplate:bf2:Monograph:Instance',
'http://id.loc.gov/ontologies/bibframe/instanceOf',
]

const plProps = {
propertyTemplate: {
Expand All @@ -14,7 +19,8 @@ const plProps = {
mandatory: '',
repeatable: '',
},
reduxPath: [],
reduxPath,
items: {},
}

const valConstraintProps = {
Expand All @@ -29,7 +35,7 @@ const valConstraintProps = {
}

describe('<InputLiteral />', () => {
const wrapper = shallow(<InputLiteral {...plProps} />)
const wrapper = shallow(<InputLiteral.WrappedComponent {...plProps} />)

it('contains a placeholder of "Instance of"', () => {
expect(wrapper.find('input').props().placeholder).toBe('Instance of')
Expand All @@ -40,45 +46,31 @@ describe('<InputLiteral />', () => {
})

it('contains required="true" attribute on input tag when mandatory is true', () => {
wrapper.instance().props.propertyTemplate.mandatory = 'true'
wrapper.instance().forceUpdate() /** Update plProps with mandatory: "true" * */
const propertyTemplate = { propertyTemplate: { ...plProps.propertyTemplate, mandatory: 'true' } }
wrapper.setProps({ ...plProps, ...propertyTemplate })
expect(wrapper.find('input').prop('required')).toBeTruthy()
expect(wrapper.find('label > RequiredSuperscript')).toBeTruthy()
})

it('contains required="false" attribute on input tag when mandatory is false', () => {
wrapper.instance().props.propertyTemplate.mandatory = 'false'
wrapper.instance().forceUpdate()
const propertyTemplate = { propertyTemplate: { ...plProps.propertyTemplate, mandatory: 'false' } }
wrapper.setProps({ ...plProps, ...propertyTemplate })
expect(wrapper.find('input').prop('required')).toBeFalsy()
})
})

describe('checkMandatoryRepeatable', () => {
const wrapper = shallow(<InputLiteral {...plProps} />)

it('is true when the field is mandatory and nothing has been filled in', () => {
wrapper.instance().props.propertyTemplate.mandatory = 'true'
wrapper.instance().forceUpdate()
expect(wrapper.find('input').prop('required')).toBeTruthy()
})
})

describe('When the user enters input into field', () => {
// Our mockItemsChange function to replace the one provided by mapDispatchToProps
let mockItemsChange
let mockWrapper
const reduxPath = [
'resourceTemplate:bf2:Monograph:Instance',
'http://id.loc.gov/ontologies/bibframe/instanceOf',
]

shortid.generate = jest.fn().mockReturnValue(0)

beforeEach(() => {
mockItemsChange = jest.fn()

mockWrapper = shallow(<InputLiteral {...plProps}
reduxPath={reduxPath}
handleMyItemsChange={mockItemsChange} />)
mockWrapper = shallow(<InputLiteral.WrappedComponent {...plProps}
handleMyItemsChange={mockItemsChange} />)
})

it('calls the change function when enter is pressed', () => {
Expand Down Expand Up @@ -122,8 +114,8 @@ describe('When the user enters input into field', () => {
})

it('property template contains repeatable "true", allowed to add more than one item into myItems array', () => {
mockWrapper.instance().props.propertyTemplate.repeatable = 'true'
mockWrapper.instance().forceUpdate()
const propertyTemplate = { propertyTemplate: { ...plProps.propertyTemplate, repeatable: 'true' } }
mockWrapper.setProps({ ...plProps, ...propertyTemplate })
mockWrapper.find('input').simulate('change', { target: { value: 'fooby' } })
mockWrapper.find('input').simulate('keypress', { key: 'Enter', preventDefault: () => {} })
mockWrapper.find('input').simulate('change', { target: { value: 'bar' } })
Expand All @@ -144,12 +136,6 @@ describe('When the user enters input into field', () => {
mockItemsChange.mock.calls = [] // Reset the redux store to empty
})

it('required is true if mandatory', () => {
mockWrapper.instance().props.propertyTemplate.mandatory = 'true'
mockWrapper.instance().forceUpdate()
expect(mockWrapper.find('input').prop('required')).toBeTruthy()
})

it('item appears when there are items', () => {
const plProps = {
propertyTemplate: {
Expand All @@ -169,8 +155,8 @@ describe('When the user enters input into field', () => {
},
reduxPath: ['basePath'],
}
const wrapper = shallow(<InputLiteral {...plProps}
handleMyItemsChange={mockItemsChange} />)
const wrapper = shallow(<InputLiteral.WrappedComponent {...plProps}
handleMyItemsChange={mockItemsChange} />)

expect(wrapper.find('Connect(InputValue)').prop('reduxPath')).toEqual(['basePath', 'items', 'abc123'])
})
Expand All @@ -194,19 +180,19 @@ describe('when there is a default literal value in the property template', () =>

it('input has disabled attribute when there are items', () => {
const nonrepeatWrapper = shallow(
<InputLiteral {...nrProps}
handleMyItemsChange={mockMyItemsChange}
items={{ 0: { content: 'fooby', lang: 'en' } }}/>,
<InputLiteral.WrappedComponent {...nrProps}
handleMyItemsChange={mockMyItemsChange}
items={{ 0: { content: 'fooby', lang: 'en' } }}/>,
)

expect(nonrepeatWrapper.find('input').prop('disabled')).toBe(true)
})

it('input does not have disabled attribute when there are no items', () => {
const nonrepeatWrapper = shallow(
<InputLiteral {...nrProps}
handleMyItemsChange={mockMyItemsChange}
items={{}}/>,
<InputLiteral.WrappedComponent {...nrProps}
handleMyItemsChange={mockMyItemsChange}
items={{}}/>,
)
expect(nonrepeatWrapper.find('input').prop('disabled')).toBe(false)
})
Expand All @@ -226,11 +212,12 @@ describe('When a user enters non-roman text in a work title', () => {
repeatable: 'true',
},
reduxPath: ['basePath'],
items: {},
}

const workTitleWrapper = shallow(
<InputLiteral {...workTitleProps}
handleMyItemsChange={mockDataFn} />,
<InputLiteral.WrappedComponent {...workTitleProps}
handleMyItemsChange={mockDataFn} />,
)

it('allows user to enter Chinese characters', () => {
Expand All @@ -250,7 +237,7 @@ describe('When a user enters non-roman text in a work title', () => {

describe('Errors', () => {
const errors = ['Required']
const wrapper = shallow(<InputLiteral displayValidations={true} errors={errors} {...plProps}/>)
const wrapper = shallow(<InputLiteral.WrappedComponent displayValidations={true} errors={errors} {...plProps}/>)

it('displays the errors', () => {
expect(wrapper.find('span.help-block').text()).toEqual('Required')
Expand Down
140 changes: 56 additions & 84 deletions src/components/editor/property/InputLiteral.jsx
@@ -1,6 +1,6 @@
// Copyright 2018, 2019 Stanford University see LICENSE for license

import React, { Component } from 'react'
import React, { useRef, useState } from 'react'
import PropTypes from 'prop-types'
import SinopiaPropTypes from 'SinopiaPropTypes'
import { connect } from 'react-redux'
Expand All @@ -13,107 +13,79 @@ import InputValue from './InputValue'
import { booleanPropertyFromTemplate, defaultLanguageId } from 'Utilities'
import _ from 'lodash'

// Redux recommends exporting the unconnected component for unit tests.
export class InputLiteral extends Component {
constructor(props) {
super(props)

this.state = {
content_add: '',
}
this.inputLiteralRef = React.createRef()
const InputLiteral = (props) => {
// Don't render if don't have property templates yet.
if (!props.propertyTemplate) {
return null
}

disabled = () => !booleanPropertyFromTemplate(this.props.propertyTemplate, 'repeatable', true)
&& Object.keys(this.props.items).length > 0
const inputLiteralRef = useRef(Math.floor(100 * Math.random()))
const [content, setContent] = useState('')

handleChange = (event) => {
const userInput = event.target.value
const disabled = !booleanPropertyFromTemplate(props.propertyTemplate, 'repeatable', true)
&& Object.keys(props.items).length > 0

this.setState({ content_add: userInput })
}

addUserInput = (input, currentcontent) => {
input[shortid.generate()] = {
content: currentcontent,
lang: defaultLanguageId,
const addItem = () => {
const currentcontent = content.trim()
if (!currentcontent) {
return
}
}

handleKeypress = (event) => {
if (event.key !== 'Enter') {
return
const userInput = {
reduxPath: props.reduxPath,
items: {
[shortid.generate()]: { content: currentcontent, lang: defaultLanguageId },
},
}
this.addItem()
event.preventDefault()
props.handleMyItemsChange(userInput)
setContent('')
}

addItem = () => {
const currentcontent = this.state.content_add.trim()
if (!currentcontent) {
return
}
const userInput = {}
this.addUserInput(userInput, currentcontent)
const payload = {
reduxPath: this.props.reduxPath,
items: userInput,
const handleKeypress = (event) => {
if (event.key === 'Enter') {
addItem()
event.preventDefault()
}
this.props.handleMyItemsChange(payload)
this.setState({
content_add: '',
})
}

handleEdit = (content) => {
this.setState({ content_add: content })
this.inputLiteralRef.current.focus()
const handleEdit = (content) => {
setContent(content)
inputLiteralRef.current.focus()
}

makeAddedList = () => {
if (this.props.items === undefined) {
return
}
const itemKeys = Object.keys(props.items)
const addedList = itemKeys.map(itemId => (<InputValue key={itemId}
handleEdit={handleEdit}
reduxPath={[...props.reduxPath, 'items', itemId]} />))

const itemKeys = Object.keys(this.props.items)
return itemKeys.map(itemId => (<InputValue key={itemId}
handleEdit={this.handleEdit}
reduxPath={[...this.props.reduxPath, 'items', itemId]} />))
}

render() {
// Don't render if don't have property templates yet.
if (!this.props.propertyTemplate) {
return null
}

const required = booleanPropertyFromTemplate(this.props.propertyTemplate, 'mandatory', false)
let error
let groupClasses = 'form-group'
const required = booleanPropertyFromTemplate(props.propertyTemplate, 'mandatory', false)

if (this.props.displayValidations && !_.isEmpty(this.props.errors)) {
groupClasses += ' has-error'
error = this.props.errors.join(',')
}
let error
let groupClasses = 'form-group'

return (
<div className={groupClasses}>
<input
required={required}
className="form-control"
placeholder={this.props.propertyTemplate.propertyLabel}
onChange={this.handleChange}
onKeyPress={this.handleKeypress}
onBlur={this.addItem}
value={this.state.content_add}
disabled={this.disabled()}
ref={this.inputLiteralRef}
/>
{error && <span className="help-block">{error}</span>}
{this.makeAddedList()}
</div>
)
if (props.displayValidations && !_.isEmpty(props.errors)) {
groupClasses += ' has-error'
error = props.errors.join(',')
}

return (
<div className={groupClasses}>
<input
required={required}
className="form-control"
placeholder={props.propertyTemplate.propertyLabel}
onChange={event => setContent(event.target.value)}
onKeyPress={handleKeypress}
onBlur={addItem}
value={content}
disabled={disabled}
ref={inputLiteralRef}
/>
{error && <span className="help-block">{error}</span>}
{addedList}
</div>
)
}

InputLiteral.propTypes = {
Expand All @@ -133,7 +105,7 @@ const mapStateToProps = (state, ownProps) => {
const formData = findNode(state.selectorReducer, reduxPath)
const errors = findErrors(state.selectorReducer, reduxPath)
// items has to be its own prop or rerendering won't occur when one is removed
const items = formData.items
const items = formData.items || {}
const propertyTemplate = getPropertyTemplate(state, resourceTemplateId, propertyURI)

return {
Expand Down

0 comments on commit cd504ce

Please sign in to comment.