Form controller completed

main
Ashelyn Dawn 5 years ago
parent caa4d5e1f3
commit 82aa67b91c

@ -1,6 +1,8 @@
const router = require('express-promise-router')() const {Router} = require('express')
const pg = require('../db/pg') const pg = require('../db/pg')
const router = module.exports = new Router()
router.use((req, res, next)=>{ router.use((req, res, next)=>{
// Skip pretty-printing if in prod // Skip pretty-printing if in prod
if(req.app.locals.dev) if(req.app.locals.dev)
@ -12,6 +14,7 @@ router.use((req, res, next)=>{
next() next()
}) })
router.use('/auth', require('./auth'))
router.use('/users/', require('./users')) router.use('/users/', require('./users'))
router.use('/items/', require('./items')) router.use('/items/', require('./items'))
router.use('/images/', require('./images')) router.use('/images/', require('./images'))
@ -21,11 +24,17 @@ router.get('/', (req, res)=>{
res.json({test: true}) res.json({test: true})
}) })
router.use((req, res)=>{ router.use((req, res, next)=>{
res.status(404) const err = new Error('Not found')
err.status = 404
return next(err);
})
router.use((err, req, res, next)=> {
console.error(err.stack)
res.status(err.status || 500)
res.json({ res.json({
error: 'Not found' error: err.message
}) })
}) })
module.exports = router;

@ -0,0 +1,11 @@
import React from 'react'
import styles from './styles.module.css'
export default function Button({onClick, enabled, type, children}){
return (
<div className={styles.formElementContainer}>
<button disabled={!enabled} onClick={onClick} type={type} children={children}/>
</div>
)
}

@ -1,7 +1,9 @@
// TODO: Enable exportDefaultFrom in Babel syntax // TODO: Enable exportDefaultFrom in Babel syntax
import React, { useReducer } from 'react' import React, { useReducer } from 'react'
import styles from './styles.module.css'
export const Input = require('./input.js').default export const Input = require('./input.js').default
export const Button = require('./button.js').default
export const FormController = function FormController({children, onSubmit}){ export const FormController = function FormController({children, onSubmit}){
const initialState = { const initialState = {
@ -10,12 +12,25 @@ export const FormController = function FormController({children, onSubmit}){
const reducer = (state, action)=>{ const reducer = (state, action)=>{
let fields = {...state.fields} let fields = {...state.fields}
const prevField = state.fields[action.name]
// If no value, just mark touched
if(action.value === undefined)
return {
fields: {
...fields,
[action.name]: {
...prevField,
touched: true,
isValid: prevField.validate?prevField.validate(prevField.value, fields):true
}
}
}
// Update currently changing field // Update currently changing field
const prevField = state.fields[action.name]
const updatedField = { const updatedField = {
...prevField, ...prevField,
value: action.value, value: action.value !== undefined ? action.value : prevField.value,
isValid: prevField.validate?prevField.validate(action.value, fields):true isValid: prevField.validate?prevField.validate(action.value, fields):true
} }
fields[action.name] = updatedField fields[action.name] = updatedField
@ -44,32 +59,47 @@ export const FormController = function FormController({children, onSubmit}){
initialState.fields[child.props.name] = { initialState.fields[child.props.name] = {
name: child.props.name, name: child.props.name,
validate: child.props.validate, validate: child.props.validate,
value: child.props.initialValue, value: child.props.initialValue || "",
isValid: true isValid: false,
touched: false
} }
}) })
// Create reducer // Create reducer
const [state, dispatch] = useReducer(reducer, initialState) const [state, dispatch] = useReducer(reducer, initialState)
const submitForm = ev=>{
if(ev) ev.preventDefault();
const fields = {}
for(const name in state.fields){
const field = state.fields[name]
fields[field.name] = field.value
}
onSubmit(fields)
}
const _children = React.Children.map(children, child => { const _children = React.Children.map(children, child => {
if(child.type === Button && child.props.type.toLowerCase() === 'submit')
return React.cloneElement(child, {
enabled: !Object.values(state.fields).some(field=>!field.isValid),
onClick: submitForm
})
const {name} = child.props; const {name} = child.props;
if(!name) return child; if(!name) return child;
return React.cloneElement(child, {
const newProps = {
onChange: ev=>dispatch({name, value: ev.target.value}), onChange: ev=>dispatch({name, value: ev.target.value}),
onBlur: ev=>dispatch({name}),
value: state.fields[name].value, value: state.fields[name].value,
isValid: "" + state.fields[name].isValid isValid: state.fields[name].touched ? state.fields[name].isValid : true
} })
return React.cloneElement(child, newProps)
}) })
console.log(JSON.stringify(state.fields, null, 2))
return ( return (
<> <form autocomplete="off" onSubmit={onSubmit} className={styles.formContainer}>
{_children} {_children}
</> </form>
) )
} }

@ -2,10 +2,12 @@ import React from 'react'
import styles from './styles.module.css' import styles from './styles.module.css'
export default function Input({type, name, value, onChange, isValid}){ export default function Input({label, hint, type, name, value, onChange, onBlur, isValid}){
return ( return (
<div className={styles.formElementContainer}> <div className={styles.formElementContainer}>
<input type={type} name={name} value={value} onChange={onChange} /> <label htmlFor={name}>{label}:</label>
<input className={isValid?'':styles.invalid} type={type} name={name} value={value} onChange={onChange} onBlur={onBlur} />
<span className={styles.hint}>{hint}</span>
</div> </div>
) )
} }

@ -1,3 +1,69 @@
.formElementContainer { .formContainer > *:first-child {
margin-top: 20px;
}
.formContainer > * {
display: block;
align-items: center;
width: 400px;
max-width: 80vw;
margin: 10px auto;
margin-bottom: 20px;
}
.formElementContainer label {
width: 150px;
display: block;
font-weight: bold;
margin-bottom: 8px;
}
.formElementContainer input {
box-sizing: border-box;
width: 100%;
min-width: 0;
padding: 10px;
border: solid 1px rgba(0,0,0,.2);
border-radius: 2px;
transition-property: border-color,box-shadow;
transition: .2s ease-in-out;
}
.formElementContainer span.hint {
display: block; display: block;
margin-top: 8px;
color: red;
opacity: 0;
transition: .2s opacity;
user-select: none;
}
.formElementContainer input.invalid {
border: solid 1px red;
box-shadow: red;
}
.formElementContainer input.invalid + span.hint {
opacity: .5;
}
.formElementContainer button {
width: 100%;
min-height: 20px;
background: rgb(156, 39, 176);
color: white;
padding: 10px 0;
border: none;
box-shadow: none;
transition-property: opacity filter;
transition-duration: .2s;
}
.formElementContainer button[type="submit"] {
margin-top: 10px;
}
.formElementContainer button[disabled] {
opacity: .4;
filter: saturate(.2);
} }

@ -1,5 +1,6 @@
module.exports = { module.exports = {
user: require('./models/user'), user: require('./models/user'),
item: require('./models/item'), item: require('./models/item'),
category: require('./models/category') category: require('./models/category'),
user: require('./models/user')
} }

@ -55,18 +55,18 @@ user.register = async (email, password) => {
} }
user.login = async (email, password) => { user.login = async (email, password) => {
const user = await user.findByEmail(email) const _user = await user.findByEmail(email)
if(!user){ if(!_user){
// Avoid early exit timing difference // Avoid early exit timing difference
await bcrypt.hash(password) await bcrypt.hash(password, saltRounds)
throw new Error("User not found") throw new Error("User not found")
} }
const passwordCorrect = await bcrypt.compare(password, user.password_hash) const passwordCorrect = await bcrypt.compare(password, _user.password_hash)
if(!passwordCorrect) if(!passwordCorrect)
throw new Error("Password incorrect") throw new Error("Password incorrect")
return user return _user
} }

@ -30,6 +30,7 @@
"pg": "^7.18.1", "pg": "^7.18.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"sharp": "^0.24.1" "sharp": "^0.24.1",
"validator": "^12.2.0"
} }
} }

@ -1,16 +0,0 @@
import React from 'react'
import {FormController, Input} from '~/components/form'
const Index = () => (
<>
<h1>Form Controller Test</h1>
<FormController onSubmit={console.log}>
<Input type="password" name="password" validate={value=>(value.length >= 8)} initialValue="" />
</FormController>
</>
)
export default Index
// Help Almyki figure out how to do 4 spaces python, 2 spaces html
// also show her how to add a vertical bar

@ -1,8 +1,21 @@
import React from 'react' import React from 'react'
import isEmail from 'validator/lib/isEmail'
import axios from 'axios'
const Login = () => ( import {FormController, Input, Button} from '~/components/form'
<div>
<p>Login</p> export default function Login(){
</div> const submit = async (values)=>{
const {data} = await axios.post(`/api/auth`, values)
console.log(data)
}
return (
<FormController onSubmit={submit}>
<h1>Login</h1>
<Input label="Email" type="text" name="email" validate={value=>isEmail(value)} hint="Enter a valid email address" />
<Input label="Password" type="password" name="password" validate={value=>(value.length >= 8)} hint="Password must be at least 8 characters long" />
<Button type="submit">Submit</Button>
</FormController>
) )
export default Login }

Loading…
Cancel
Save