Skip to content

Commit

Permalink
feat: improve performance of text inputs
Browse files Browse the repository at this point in the history
This makes inputs only commit changes when they loose focus, instead of
on every key press. This eliminates the janky feeling when typing in
huge records while on a relatively slow device.
  • Loading branch information
RiledUpCrow committed Aug 7, 2020
1 parent f187eac commit 82f6db4
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 208 deletions.
122 changes: 58 additions & 64 deletions src/frontend/components/property-type/default-type/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,75 @@
import React, { ReactNode } from 'react'
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { FC, useState, memo, useEffect } from 'react'
import Select from 'react-select'
import { withTheme, DefaultTheme } from 'styled-components'
import { Input, FormMessage, FormGroup, Label, selectStyles } from '@admin-bro/design-system'

import { EditPropertyProps } from '../base-property-props'
import { recordPropertyIsEqual } from '../record-property-is-equal'
import usePrevious from '../../../utils/usePrevious'

type CombinedProps = EditPropertyProps & {theme: DefaultTheme}

class Edit extends React.Component<CombinedProps> {
constructor(props) {
super(props)
this.handleInputChange = this.handleInputChange.bind(this)
this.handleSelectChange = this.handleSelectChange.bind(this)
}

shouldComponentUpdate(prevProps: CombinedProps): boolean {
return !recordPropertyIsEqual(prevProps, this.props)
}
const Edit: FC<CombinedProps> = (props) => {
const { property, record } = props
const error = record.errors?.[property.name]
return (
<FormGroup error={Boolean(error)}>
<Label
htmlFor={property.name}
required={property.isRequired}
>
{property.label}
</Label>
{property.availableValues ? <SelectEdit {...props} /> : <TextEdit {...props} />}
<FormMessage>{error && error.message}</FormMessage>
</FormGroup>
)
}

handleInputChange(event): void {
const { onChange, property } = this.props
onChange(property.name, event.target.value)
const SelectEdit: FC<CombinedProps> = (props) => {
const { theme, record, property, onChange } = props
if (!property.availableValues) {
return null
}
const propValue = record.params?.[property.name] ?? ''
const styles = selectStyles(theme)
const selected = property.availableValues.find(av => av.value === propValue)
return (
<Select
isClearable
styles={styles}
value={selected}
options={property.availableValues}
onChange={s => onChange(property.name, s?.value ?? '')}
isDisabled={property.isDisabled}
/>
)
}

handleSelectChange(selected): void {
const { onChange, property } = this.props
const value = selected ? selected.value : ''
onChange(property.name, value)
}
const TextEdit: FC<CombinedProps> = (props) => {
const { property, record, onChange } = props
const propValue = record.params?.[property.name] ?? ''
const [value, setValue] = useState(propValue)

renderInput(): ReactNode {
const { property, record, theme } = this.props
const value = (record.params && typeof record.params[property.name] !== 'undefined')
? record.params[property.name]
: ''
if (property.availableValues) {
const styles = selectStyles(theme)
const selected = property.availableValues.find(av => av.value === value)
return (
<Select
isClearable
styles={styles}
value={selected}
options={property.availableValues}
onChange={this.handleSelectChange}
isDisabled={property.isDisabled}
/>
)
const previous = usePrevious(propValue)
useEffect(() => {
// this means props updated
if (propValue !== previous) {

This comment has been minimized.

Copy link
@FelixGaebler

FelixGaebler Aug 26, 2020

isnt React.memo handing this by default?

setValue(propValue)
}
return (
<Input
id={property.name}
name={property.name}
onChange={this.handleInputChange}
value={value}
disabled={property.isDisabled}
/>
)
}
}, [])

render(): ReactNode {
const { property, record } = this.props
const error = record.errors && record.errors[property.name]
return (
<FormGroup error={!!error}>
<Label
htmlFor={property.name}
required={property.isRequired}
>
{property.label}
</Label>
{this.renderInput()}
<FormMessage>{error && error.message}</FormMessage>
</FormGroup>
)
}
return (
<Input
id={property.name}
name={property.name}
onChange={e => setValue(e.target.value)}
onBlur={() => onChange(property.name, value)}
value={value}
disabled={property.isDisabled}
/>
)
}

export default withTheme(Edit)
export default withTheme(memo(Edit, recordPropertyIsEqual))
21 changes: 16 additions & 5 deletions src/frontend/components/property-type/password/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import React, { useState, memo } from 'react'
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import React, { useState, memo, useEffect } from 'react'
import { Label, Input, FormGroup, InputGroup, FormMessage, Button, Icon } from '@admin-bro/design-system'

import { EditPropertyProps } from '../base-property-props'
import { recordPropertyIsEqual } from '../record-property-is-equal'
import usePrevious from '../../../utils/usePrevious'

const Edit: React.FC<EditPropertyProps> = (props) => {
const { property, record, onChange } = props
const value = record.params[property.name]
const propValue = record.params[property.name]
const [value, setValue] = useState(propValue)
const error = record.errors && record.errors[property.name]

const [isInput, setIsInput] = useState(false)

const previous = usePrevious(propValue)
useEffect(() => {
// this means props updated
if (propValue !== previous) {
setValue(propValue)
}
}, [])

return (
<FormGroup error={!!error}>
<Label
Expand All @@ -25,15 +35,16 @@ const Edit: React.FC<EditPropertyProps> = (props) => {
className="input"
id={property.name}
name={property.name}
onChange={(event): void => onChange(property.name, event.target.value)}
onChange={event => setValue(event.target.value)}
onBlur={() => onChange(property.name, value)}
value={value ?? ''}
disabled={property.isDisabled}
/>
<Button
variant={isInput ? 'primary' : 'text'}
type="button"
size="icon"
onClick={(): void => setIsInput(!isInput)}
onClick={() => setIsInput(!isInput)}
>
<Icon icon="View" />
</Button>
Expand Down
192 changes: 74 additions & 118 deletions src/frontend/components/property-type/richtext/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,79 @@
/* eslint-disable jsx-a11y/label-has-for */
import React, { ReactNode } from 'react'
import { findDOMNode } from 'react-dom'
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable no-unused-expressions */
import React, { useRef, useEffect, FC, useState, memo } from 'react'
import styled from 'styled-components'
import { FormGroup, Label, FormMessage } from '@admin-bro/design-system'

import styled from 'styled-components'
import { EditPropertyProps } from '../base-property-props'
import { recordPropertyIsEqual } from '../record-property-is-equal'
import loadQuill from '../../../utils/loadQuill'


const Edit: FC<EditPropertyProps> = (props) => {
const { property, record, onChange } = props
const value = record.params?.[property.name] ?? ''
const error = record.errors && record.errors[property.name]

const [quill, setQuill] = useState<Quill | null>(null)
const editorRef = useRef<HTMLDivElement>(null)

useEffect(() => {
let shouldLoad = true
loadQuill().then(() => {
if (!shouldLoad) {
return
}
const quillInstance = new (Quill as any)(editorRef.current, {
modules: { toolbar: toolbarOptions },
theme: 'snow',
})
setQuill(quillInstance)
})
return () => {
shouldLoad = false
}
}, [])

useEffect(() => {
if (!editorRef.current || !quill) {
return
}
if (value) {
quill.root.innerHTML = value
}
}, [value, quill])

useEffect(() => {
const editor = quill?.root
if (!editor) {
return undefined
}
const handler = () => {
const content = editor.innerHTML
onChange?.(property.name, content)
}
editor?.addEventListener('blur', handler)
return () => {
editor?.removeEventListener('blur', handler)
}
}, [onChange, property.name, quill])

return (
<FormGroup error={Boolean(error)}>
<Label
htmlFor={property.name}
required={property.isRequired}
>
{property.label}
</Label>
<Wrapper>
<div className="quill-editor" ref={editorRef} style={{ height: '400px' }} />
</Wrapper>
<FormMessage>{error?.message}</FormMessage>
</FormGroup>
)
}

const toolbarOptions = [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
Expand Down Expand Up @@ -41,117 +110,4 @@ const Wrapper = styled.div.attrs({
}
`

const loadQuill = () => new Promise((resolve) => {
const id = 'quill-script-tag'
if (window.document.getElementById(id)) {
// it could be a situation where id exists but quill hasn't been loaded. In this case
// we check if Quill global variable exists
const checkIfLoaded = () => {
if (typeof Quill === 'function') {
resolve()
}
}
checkIfLoaded()
setInterval(checkIfLoaded, 500)
return
}
const script = window.document.createElement('script')
script.src = 'https://cdn.quilljs.com/1.3.6/quill.js'
script.async = true
script.defer = true
script.id = id
script.addEventListener('load', () => {
resolve()
})

const style = window.document.createElement('link')
style.rel = 'stylesheet'
style.type = 'text/css'
style.href = 'https://cdn.quilljs.com/1.3.6/quill.snow.css'

window.document.body.appendChild(script)
window.document.body.appendChild(style)
})

export default class Edit extends React.Component<EditPropertyProps> {
private wysiwigRef: React.RefObject<any>

private quill: any

constructor(props: EditPropertyProps) {
super(props)
this.wysiwigRef = React.createRef()
}

componentDidMount(): void {
loadQuill().then(() => {
this.setupWysiwig()
})
}

shouldComponentUpdate(nextProps: EditPropertyProps): boolean {
const { record, property } = this.props
if (!nextProps) { return false }
const oldError = record.errors
&& record.errors[property.name]
&& record.errors[property.name].message
const newError = nextProps.record.errors
&& nextProps.record.errors[property.name]
&& nextProps.record.errors[property.name].message
return oldError !== newError
}

componentDidUpdate(): void {
this.setupWysiwig()
}

setupWysiwig(): void {
const { property, record } = this.props
const value = (record.params && record.params[property.name]) || ''
this.wysiwigRef.current.innerHTML = value
if (this.quill) {
delete this.quill
// eslint-disable-next-line react/no-find-dom-node
const thisNode = findDOMNode(this) as Element
const toolbars = thisNode.getElementsByClassName('ql-toolbar')
for (let index = 0; index < toolbars.length; index += 1) {
toolbars[index].remove()
}
}
this.quill = new Quill(this.wysiwigRef.current, {
modules: {
toolbar: toolbarOptions,
},
theme: 'snow',
...property.custom,
})

this.quill.on('text-change', () => {
this.handleChange(this.wysiwigRef.current.children[0].innerHTML)
})
}

handleChange(value: any): void {
const { onChange, property } = this.props
onChange(property.name, value)
}

render(): ReactNode {
const { property, record } = this.props
const error = record.errors && record.errors[property.name]
return (
<FormGroup error={!!error}>
<Label
htmlFor={property.name}
required={property.isRequired}
>
{property.label}
</Label>
<Wrapper>
<div className="quill-editor" ref={this.wysiwigRef} style={{ height: '400px' }} />
</Wrapper>
<FormMessage>{error && error.message}</FormMessage>
</FormGroup>
)
}
}
export default memo(Edit, recordPropertyIsEqual)

0 comments on commit 82f6db4

Please sign in to comment.