Skip to content

Commit 935ed28

Browse files
committed
[React Hooks] Add useForm hook to manage form submit
1 parent 2dd01a4 commit 935ed28

File tree

8 files changed

+136
-90
lines changed

8 files changed

+136
-90
lines changed

src/actions/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const fetchSyntaxes = dispatch => (
4747
.then(json => dispatch(setSyntaxes(json)))
4848
)
4949

50-
export const postSnippet = (snippet, onSuccess) => dispatch => (
50+
export const postSnippet = (snippet, onSuccess, onError = () => {}) => dispatch => (
5151
fetch(getApiUri('snippets'), {
5252
method: 'POST',
5353
headers: {
@@ -61,4 +61,5 @@ export const postSnippet = (snippet, onSuccess) => dispatch => (
6161
dispatch(setSnippet(json))
6262
onSuccess(json)
6363
})
64+
.catch(onError)
6465
)

src/components/ListBoxWithSearch.jsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import React, { useState } from 'react'
1+
import React, { Fragment, useState, useCallback } from 'react'
22

33
import ListBox from './ListBox'
44

55
import { regExpEscape } from '../misc/reqExp'
66

7-
const ListBoxWithSearch = props => {
8-
let { items } = props
9-
7+
const ListBoxWithSearch = ({ items, onClick }) => {
108
const [ searchQuery, setSearchQuery ] = useState(null)
119

12-
const onSearch = e => {
10+
const onSearch = useCallback(e => {
1311
setSearchQuery(e.target.value.trim())
14-
}
12+
})
1513

1614
// Normalize items arrays so each item is always an object.
1715
items = items.map((item) => {
@@ -30,17 +28,17 @@ const ListBoxWithSearch = props => {
3028
}
3129

3230
return (
33-
[
31+
<Fragment>
3432
<div className="new-snippet-lang-header" key="Syntax input">
3533
<input className="input" placeholder="Type to search..." onChange={onSearch} />
36-
</div>,
34+
</div>
3735
<div className="new-snippet-lang-list-wrapper" key="Syntax list">
3836
<ListBox
3937
items={items}
40-
onClick={props.onClick}
38+
onClick={onClick}
4139
/>
42-
</div>,
43-
]
40+
</div>
41+
</Fragment>
4442
)
4543
}
4644

src/components/NewSnippet.jsx

Lines changed: 41 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react'
1+
import React, { useEffect, useRef } from 'react'
22
import { connect } from 'react-redux'
33
import AceEditor from 'react-ace'
44
import { WithContext as Tags } from 'react-tag-input'
@@ -17,25 +17,18 @@ import { validateSnippet } from '../entries/snippetValidation'
1717
import { delimeterKeys } from '../entries/keyboardKeys'
1818
import { defaultOptions } from '../entries/aceEditorOptions'
1919

20-
import '../styles/NewSnippet.styl'
21-
22-
const recalcLangHeaderHeight = () => {
23-
const mainHeader = 'new-snippet-code-header'
24-
const langHeader = 'new-snippet-lang-header'
25-
26-
const height = document.getElementsByClassName(mainHeader)[0].offsetHeight
27-
28-
document.getElementsByClassName(langHeader)[0].setAttribute('style', `height:${height}px`)
29-
}
20+
import useForm from '../hooks/useForm'
3021

31-
const NewSnippet = props => {
32-
const { dispatch, history } = props
22+
import '../styles/NewSnippet.styl'
3323

34-
const [ syntax, setSyntax ] = useState('')
35-
const [ content, setContent ] = useState('')
36-
const [ title, setTitle ] = useState('')
37-
const [ tags, setTags ] = useState([])
38-
const [ validationError, setValidationError ] = useState(null)
24+
const NewSnippet = ({ dispatch, history, syntaxes }) => {
25+
const snippetHeader = useRef()
26+
const {
27+
values: { title = '', syntax = '', content = '', tags = [] },
28+
error,
29+
handleChange,
30+
handleSubmit,
31+
} = useForm(post, validate)
3932

4033
useEffect(() => {
4134
dispatch(fetchSyntaxes)
@@ -45,82 +38,72 @@ const NewSnippet = props => {
4538
recalcLangHeaderHeight()
4639
}, [tags])
4740

48-
const onTagAdded = tag => {
49-
if (tag && tag.text) {
50-
setTags([...tags, tag])
51-
}
41+
function validate() {
42+
return validateSnippet({ content: content.trim() })
5243
}
5344

54-
const onTagRemoved = i => {
55-
setTags(tags.filter((tag, index) => index !== i))
45+
function post() {
46+
dispatch(postSnippet({
47+
content, title, tags: tags.map(tag => tag.text), syntax,
48+
}, json => history.push(`/${json.id}`)))
5649
}
5750

58-
const onTagBlur = tag => {
59-
onTagAdded({ id: tag, text: tag })
60-
}
51+
const recalcLangHeaderHeight = () => {
52+
const height = snippetHeader.current.offsetHeight
6153

62-
const onSyntaxClick = syntax => {
63-
setSyntax(syntax)
54+
document.getElementsByClassName('new-snippet-lang-header')[0]
55+
.setAttribute('style', `height:${height}px`)
6456
}
6557

66-
const onTitleChange = e => {
67-
const { value } = e.target
58+
const onTagBlur = tag => onTagAdded({ id: tag, text: tag })
6859

69-
setTitle(value)
60+
const onTagAdded = tag => {
61+
if (tag && tag.text) {
62+
return { tags: [...tags, tag] }
63+
}
7064
}
7165

72-
const validate = () => validateSnippet({ content: content.trim() })
73-
74-
const post = e => {
75-
e.preventDefault()
76-
const { error } = validate()
77-
78-
setValidationError(error)
79-
80-
if (!error) {
81-
dispatch(postSnippet({
82-
content, title, tags: tags.map(tag => tag.text), syntax,
83-
}, json => history.push(`/${json.id}`)))
84-
}
66+
const onTagRemoved = i => {
67+
return { tags: tags.filter((tag, index) => index !== i) }
8568
}
8669

70+
const handleSyntax = syntax => ({ syntax })
71+
const handleContent = content => ({ content })
72+
8773
const getSyntaxes = () => {
8874
const { modesByName } = getModesByName()
8975

90-
return props.syntaxes.map(item => ({
76+
return syntaxes.map(item => ({
9177
name: modesByName[item].caption,
9278
value: item,
9379
}))
9480
}
9581

96-
const renderValidationError = () => (validationError && <Notification
97-
message="Content is required :("
98-
show={!!validationError}
99-
/>)
82+
const renderValidationError = () => (error && <Notification message={error} />)
10083

10184
return (
10285
<form
10386
className="new-snippet"
10487
key="New Snippet"
105-
onSubmit={post}
88+
onSubmit={handleSubmit}
10689
role="presentation"
10790
>
10891
<div className="new-snippet-code-wrapper">
109-
<div className="new-snippet-code-header">
92+
<div className="new-snippet-code-header" ref={snippetHeader}>
11093
<input
11194
className="input"
11295
placeholder="Title"
11396
name="title"
11497
type="text"
11598
value={title}
116-
onChange={onTitleChange}
99+
onChange={handleChange}
117100
/>
118101
<Tags
119102
placeholder="Tags"
120103
tags={tags}
121-
handleDelete={onTagRemoved}
122-
handleAddition={onTagAdded}
123-
handleInputBlur={onTagBlur}
104+
handleDelete={(value) => handleChange(value, onTagRemoved)}
105+
handleAddition={(value) => handleChange(value, onTagAdded)}
106+
handleInputBlur={(value) => handleChange(value, onTagBlur)}
124107
delimiters={delimeterKeys}
125108
/>
126109
</div>
@@ -135,7 +118,7 @@ const NewSnippet = props => {
135118
setOptions={defaultOptions}
136119
editorProps={{ $blockScrolling: Infinity }}
137120
value={content}
138-
onChange={(value) => setContent(value)}
121+
onChange={(value) => handleChange(value, handleContent)}
139122
/>
140123
<div className="new-snippet-code-bottom-bar">
141124
{renderValidationError()}
@@ -146,7 +129,7 @@ const NewSnippet = props => {
146129
<div className="new-snippet-lang-wrapper">
147130
<ListBoxWithSearch
148131
items={getSyntaxes()}
149-
onClick={onSyntaxClick}
132+
onClick={(syntax) => handleChange(syntax, handleSyntax)}
150133
/>
151134
</div>
152135
</form>

src/components/RecentSnippets.jsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@ const scrollTop = () => {
1111
window.scroll({ top: 0, behavior: 'smooth' })
1212
}
1313

14-
const RecentSnippets = props => {
15-
const { dispatch, pagination, snippets, recent } = props
16-
14+
const RecentSnippets = ({ dispatch, pagination, snippets, recent }) => {
1715
const older = pagination.get('next')
1816
const newer = pagination.get('prev')
1917

2018
useEffect(() => {
21-
const { dispatch, recent, pagination } = props
2219
let marker = null
2320

2421
if (pagination.get('prev')) {

src/components/Snippet.jsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react'
1+
import React, { useEffect, useState, useRef } from 'react'
22
import { connect } from 'react-redux'
33
import AceEditor from 'react-ace'
44

@@ -18,25 +18,24 @@ import { existingSnippetOptions } from '../entries/aceEditorOptions'
1818

1919
import '../styles/Snippet.styl'
2020

21-
const copyToClipboard = (e, id = 'embedded') => {
22-
document.getElementById(id).select()
23-
document.execCommand('copy')
24-
e.target.focus()
25-
}
26-
2721
const Snippet = props => {
2822
const { snippet, dispatch } = props
29-
30-
if (!snippet) return <Spinner />
31-
3223
const [ isShowEmbed, setIsShowEmbed ] = useState(false)
24+
const embeddedRef = useRef()
3325

3426
useEffect(() => {
3527
const { id } = props.match.params
3628

3729
dispatch(fetchSnippet(Number(id)))
3830
}, [])
3931

32+
if (!snippet) return <Spinner />
33+
34+
const copyToClipboard = () => {
35+
embeddedRef.current.select()
36+
document.execCommand('copy')
37+
}
38+
4039
const download = () => {
4140
downloadSnippet(props.snippet)
4241
}
@@ -58,7 +57,7 @@ const Snippet = props => {
5857
simply copy and paste code provided below:
5958
</p>
6059
<input
61-
id="embedded"
60+
ref={embeddedRef}
6261
className="input"
6362
type="text"
6463
defaultValue={`<script src='http://xsnippet.org/${snippet.get('id')}/embed/'></script>`}

src/components/common/Notification.jsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ import PropTypes from 'prop-types'
33

44
import '../../styles/common/Notification.styl'
55

6-
const Notification = ({ show, message }) => (
7-
<div className={`notification ${show && 'has-error'}`}>{message}</div>
6+
const Notification = ({ message }) => (
7+
<div className="notification has-error">{message}</div>
88
)
99

1010
Notification.propTypes = {
11-
show: PropTypes.bool,
1211
message: PropTypes.string,
1312
}
1413

1514
Notification.defaultProps = {
16-
show: false,
1715
message: '',
1816
}
1917

src/entries/snippetValidation.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,32 @@ const schema = Joi.object().keys({
44
content: Joi.string().required(),
55
})
66

7-
export const validateSnippet = value => Joi.validate(value, schema)
7+
export const validateSnippet = value => {
8+
const result = Joi.validate(value, schema)
9+
10+
if (result.error) {
11+
return reasons(result.error.details)
12+
}
13+
14+
return null
15+
}
16+
17+
const messages = new Map([
18+
['content.any.empty', 'Content is required :('],
19+
['default', 'Something went wrong, please check all fields'],
20+
])
21+
22+
const reasons = details => {
23+
const errors = []
24+
25+
details.forEach(element => {
26+
const reason = `${element.path[0]}.${element.type}`
27+
28+
errors.push(messages.has(reason)
29+
? messages.get(reason)
30+
: messages.get('default')
31+
)
32+
})
33+
34+
return errors[0]
35+
}

src/hooks/useForm.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useState, useEffect, useCallback } from 'react'
2+
3+
const useForm = (cb, validate) => {
4+
const [values, setValues] = useState({})
5+
const [error, setError] = useState('')
6+
const [isSubmitting, setIsSubmitting] = useState(false)
7+
8+
useEffect(() => {
9+
if (!error && isSubmitting) {
10+
cb()
11+
}
12+
13+
return () => {
14+
setIsSubmitting(false)
15+
}
16+
}, [error, isSubmitting])
17+
18+
const handleSubmit = useCallback(e => {
19+
e.preventDefault()
20+
21+
setError(validate(values))
22+
setIsSubmitting(true)
23+
})
24+
25+
const handleChange = useCallback((e, setter) => {
26+
if (e.target) {
27+
e.persist()
28+
setValues(values => ({ ...values, [e.target.name]: e.target.value }))
29+
} else {
30+
setValues(values => ({ ...values, ...setter(e) }))
31+
}
32+
})
33+
34+
return {
35+
handleChange,
36+
handleSubmit,
37+
values,
38+
error,
39+
}
40+
}
41+
42+
export default useForm

0 commit comments

Comments
 (0)