Skip to content

Commit

Permalink
Rewrite PathBuilder using hooks (badges#3721)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmelnikow authored Jul 18, 2019
1 parent 5d3d78b commit d4e17d4
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 108 deletions.
183 changes: 77 additions & 106 deletions frontend/components/customizer/path-builder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import styled, { css } from 'styled-components'
import pathToRegexp from 'path-to-regexp'
Expand Down Expand Up @@ -68,124 +68,93 @@ const NamedParamCaption = styled(BuilderCaption)`
text-align: center;
`

export default class PathBuilder extends React.Component {
static propTypes = {
pattern: PropTypes.string.isRequired,
exampleParams: objectOfKeyValuesPropType,
onChange: PropTypes.func,
isPrefilled: PropTypes.bool,
}

constructor(props) {
super(props)

const { pattern } = props
const tokens = pathToRegexp.parse(pattern)

let namedParams
if (this.props.isPrefilled) {
namedParams = this.props.exampleParams
} else {
namedParams = {}
// `pathToRegexp.parse()` returns a mixed array of strings for literals
// and objects for parameters. Filter out the literals and work with the
// objects.
tokens
.filter(t => typeof t !== 'string')
.forEach(({ name }) => {
namedParams[name] = ''
})
}
this.state = {
tokens,
namedParams,
}
}

static constructPath({ tokens, namedParams }) {
let isComplete = true
const path = tokens
.map(token => {
if (typeof token === 'string') {
return token
export function constructPath({ tokens, namedParams }) {
let isComplete = true
const path = tokens
.map(token => {
if (typeof token === 'string') {
return token
} else {
const { delimiter, name, optional } = token
const value = namedParams[name]
if (value) {
return `${delimiter}${value}`
} else if (optional) {
return ''
} else {
const { delimiter, name, optional } = token
const value = namedParams[name]
if (value) {
return `${delimiter}${value}`
} else if (optional) {
return ''
} else {
isComplete = false
return `${delimiter}:${name}`
}
isComplete = false
return `${delimiter}:${name}`
}
})
.join('')
return { path, isComplete }
}
}
})
.join('')
return { path, isComplete }
}

notePathChanged({ tokens, namedParams }) {
const { onChange } = this.props
export default function PathBuilder({
pattern,
exampleParams,
onChange,
isPrefilled,
}) {
const [tokens] = useState(() => pathToRegexp.parse(pattern))
const [namedParams, setNamedParams] = useState(() =>
isPrefilled
? exampleParams
: // `pathToRegexp.parse()` returns a mixed array of strings for literals
// and objects for parameters. Filter out the literals and work with the
// objects.
tokens
.filter(t => typeof t !== 'string')
.reduce((accum, { name }) => {
accum[name] = ''
return accum
}, {})
)

useEffect(() => {
// Ensure the default style is applied right away.
if (onChange) {
const { path, isComplete } = this.constructor.constructPath({
tokens,
namedParams,
})
const { path, isComplete } = constructPath({ tokens, namedParams })
onChange({ path, isComplete })
}
}

componentDidMount() {
// Ensure the default style is applied right away.
const { tokens, namedParams } = this.state
this.notePathChanged({ tokens, namedParams })
}
}, [tokens, namedParams, onChange])

handleTokenChange = evt => {
function handleTokenChange(evt) {
const { name, value } = evt.target
const { tokens, namedParams: oldNamedParams } = this.state

const namedParams = {
...oldNamedParams,
setNamedParams({
...namedParams,
[name]: value,
}

this.setState({ namedParams })
this.notePathChanged({ tokens, namedParams })
})
}

renderLiteral(literal, tokenIndex) {
function renderLiteral(literal, tokenIndex) {
return (
<PathBuilderColumn key={`${tokenIndex}-${literal}`}>
<PathLiteral isFirstToken={tokenIndex === 0}>{literal}</PathLiteral>
</PathBuilderColumn>
)
}

renderNamedParamInput(token) {
function renderNamedParamInput(token) {
const { name, pattern } = token
const options = patternToOptions(pattern)

const { namedParams } = this.state
const value = namedParams[name]

if (options) {
return (
<NamedParamSelect
name={name}
onChange={this.handleTokenChange}
onChange={handleTokenChange}
value={value}
>
<option disabled={this.props.isPrefilled} key="empty" value="">
<option disabled={isPrefilled} key="empty" value="">
{' '}
</option>
{options.map(option => (
<option
disabled={this.props.isPrefilled}
key={option}
value={option}
>
<option disabled={isPrefilled} key={option} value={option}>
{option}
</option>
))}
Expand All @@ -194,9 +163,9 @@ export default class PathBuilder extends React.Component {
} else {
return (
<NamedParamInput
disabled={this.props.isPrefilled}
disabled={isPrefilled}
name={name}
onChange={this.handleTokenChange}
onChange={handleTokenChange}
type="text"
value={value}
{...noAutocorrect}
Expand All @@ -205,22 +174,21 @@ export default class PathBuilder extends React.Component {
}
}

renderNamedParam(token, tokenIndex, namedParamIndex) {
function renderNamedParam(token, tokenIndex, namedParamIndex) {
const { delimiter, name, optional } = token

const { exampleParams } = this.props
const exampleValue = exampleParams[name] || '(not set)'

return (
<React.Fragment key={token.name}>
{this.renderLiteral(delimiter, tokenIndex)}
{renderLiteral(delimiter, tokenIndex)}
<PathBuilderColumn withHorizPadding>
<NamedParamLabelContainer>
<BuilderLabel htmlFor={name}>{humanizeString(name)}</BuilderLabel>
{optional ? <BuilderLabel>(optional)</BuilderLabel> : null}
</NamedParamLabelContainer>
{this.renderNamedParamInput(token)}
{!this.props.isPrefilled && (
{renderNamedParamInput(token)}
{!isPrefilled && (
<NamedParamCaption>
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
</NamedParamCaption>
Expand All @@ -230,17 +198,20 @@ export default class PathBuilder extends React.Component {
)
}

render() {
const { tokens } = this.state
let namedParamIndex = 0
return (
<BuilderContainer>
{tokens.map((token, tokenIndex) =>
typeof token === 'string'
? this.renderLiteral(token, tokenIndex)
: this.renderNamedParam(token, tokenIndex, namedParamIndex++)
)}
</BuilderContainer>
)
}
let namedParamIndex = 0
return (
<BuilderContainer>
{tokens.map((token, tokenIndex) =>
typeof token === 'string'
? renderLiteral(token, tokenIndex)
: renderNamedParam(token, tokenIndex, namedParamIndex++)
)}
</BuilderContainer>
)
}
PathBuilder.propTypes = {
pattern: PropTypes.string.isRequired,
exampleParams: objectOfKeyValuesPropType,
onChange: PropTypes.func,
isPrefilled: PropTypes.bool,
}
4 changes: 2 additions & 2 deletions frontend/components/customizer/path-builder.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { test, given } from 'sazerac'
import pathToRegexp from 'path-to-regexp'
import PathBuilder from './path-builder'
import { constructPath } from './path-builder'

describe('<PathBuilder />', function() {
const tokens = pathToRegexp.parse('github/license/:user/:repo')
test(PathBuilder.constructPath, () => {
test(constructPath, () => {
given({
tokens,
namedParams: {
Expand Down

0 comments on commit d4e17d4

Please sign in to comment.