Skip to content

Commit

Permalink
Improve expense adding form validation
Browse files Browse the repository at this point in the history
This should prevent form submission and give sensible feedback to the users when entering an amount that can't be parsed as a number
Also checks that at least one participant is involved
  • Loading branch information
ssimono committed Mar 19, 2020
1 parent be8dbd5 commit 9720945
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 16 deletions.
6 changes: 4 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ <h4>How to balance?</h4>

<section path="/add_expense">
<h2>Add an expense</h2>
<json-form class="add-expense" name="add_expense">
<add-expense-form class="add-expense" name="add_expense">
<label for="add_expense_title">Title</label>
<input type="text" id="add_expense_title" name="title" autocomplete="off" required/>

Expand All @@ -109,9 +109,11 @@ <h2>Add an expense</h2>
<input type="date" id="add_expense_date" name="date" required/>
<input type="hidden" name="currency" value="EUR" />

<p class="errors"></p>

<input type="submit" name="submit" value="Add" />
<button type="button" title="Cancel" to="/trip/expenses" class="cancel">Cancel</button>
</json-form>
</add-expense-form>
</section>

<script type="module">
Expand Down
19 changes: 19 additions & 0 deletions js/components/AddExpenseForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import JsonForm from './JsonForm.js'

export default class AddExpenseForm extends JsonForm {
validate(newExpense) {
if (Number.isNaN(Number(newExpense.amount))) {
return [{name: 'amount', error: 'Invalid amount. Please enter a valid number'}]
}

if (!newExpense.participants || !newExpense.participants.length) {
return [{error: 'Please choose who this expense applies to'}]
}

return []
}

format(newExpense) {
return Object.assign({}, newExpense, {amount: Number(newExpense.amount)})
}
}
88 changes: 75 additions & 13 deletions js/components/JsonForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {dispatch, html} from '../lib.js'
export default class JsonForm extends HTMLElement {
constructor() {
super()
// Use a nested form element to circument lack of customized built-in elements in Safari
const form = html`<form name="${this.getAttribute('name')}">`
// Use a nested form element to circumvent lack of customized built-in elements in Safari
const form = html`<form data-name="${this.getAttribute('name')}">`

while(this.firstChild) {
form.appendChild(this.firstChild.cloneNode(true))
Expand All @@ -15,26 +15,88 @@ export default class JsonForm extends HTMLElement {
}

connectedCallback() {
this.addEventListener('submit', onSubmit)
this.addEventListener('submit', submitHandler(
this.validate.bind(this),
this.format.bind(this),
))
}

validate(payload) {
return []
}

format(payload) {
return payload
}
}

function onSubmit(event) {
event.preventDefault()
const formData = new FormData(event.target)
function submitHandler(validator, formatter) {
return event => {
event.preventDefault()
const form = event.target
const payload = getFormData(form)
const errors = validator(payload)

if (errors.length) {
showErrors(form, errors)
} else {
dispatch(
form,
`app:submit_${event.target.dataset.name}`,
formatter(payload)
)
}
}
}

function showErrors(form, errors) {
const generic = errors.filter(e => !e.name)
const specific = errors.filter(e => !!e.name)

for(let err of specific) {
let inputs = form.querySelectorAll(`[name="${err.name}"]`)
if (!inputs.length) {
generic.push(err)
continue
}

for(let input of inputs) {
input.setCustomValidity(err.error)
input.addEventListener(
'change',
() => clearErrors(input.form, input.getAttribute('name')),
{once: true}
)
}
}

const errorContainer = form.querySelector('.errors')
if (errorContainer && generic.length) {
errorContainer.innerText = generic.map(e => e.error).join(' — ')
form.addEventListener('change', () => errorContainer.innerText = '', {once: true})
}
}

function clearErrors(form, name) {
for(let input of form.querySelectorAll(`[name="${name}"]`)) {
input.setCustomValidity('')
}
}

function getFormData(form) {
const formData = new FormData(form)
const map = Object.create(null)
for(let [k, v] of formData.entries()) {

return Array.from(formData.entries()).reduce((map, [k, v]) => {
if (k.substr(-2) === '[]') {
const key = k.substr(0, k.length - 2)
if (key in map) {
map[key].push(v)
return Object.assign(map, {[key]: [...map[key], v]})
} else {
map[key] = [v]
return Object.assign(map, {[key]: [v]})
}
} else {
map[k] = v
return Object.assign(map, {[k]: v})
}
}

dispatch(event.target, `app:submit_${event.target.getAttribute('name')}`, map)
}, Object.create(null))
}
4 changes: 3 additions & 1 deletion js/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {attachRoutes, dispatch, generateId, goTo} from './lib.js'
import AddExpenseForm from '/js/components/AddExpenseForm.js'
import JsonForm from '/js/components/JsonForm.js'
import InputList from '/js/components/InputList.js'

Expand Down Expand Up @@ -47,8 +48,9 @@ export default function main() {
const localCommandsKey = `${boxId}_commands`
const client = new Client(boxId)

customElements.define('json-form', JsonForm)
customElements.define('input-list', InputList)
customElements.define('json-form', JsonForm)
customElements.define('add-expense-form', AddExpenseForm)

attachRoutes(routes, document.body)

Expand Down
9 changes: 9 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ form .cancel, form .remove {
background-color: var(--tertiary);
}

form .errors {
color: var(--tertiary);
text-align: center;
}

form .errors:empty {
display: none;
}

/* ===== Footer */
footer {
background-color: rgba(0,0,0,0.6);
Expand Down

0 comments on commit 9720945

Please sign in to comment.