Skip to content

Commit

Permalink
Merge pull request #4 from scriptoLLC/validation
Browse files Browse the repository at this point in the history
Provide hooks for form validation
  • Loading branch information
toddself authored Jun 19, 2017
2 parents a7bb402 + 84345af commit f868a57
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 13 deletions.
101 changes: 94 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,110 @@

A collection of form elements.

## Usage
```js
const els = require('@scriptoll/form-elements')
const html = require('bel')

function form (state, emit) {
const err = els.error()

return html`<form onsubmit=${submit}>
${err.render()}
${els.labeledInput('Email', 'email', update, state.email, {validate: true, errorDisplay: err})}
${els.button('submit!', submit)}
</form>`

function input (evt) {
state.email = evt.currentTarget.value
}

function submit (evt) {
evt.preventDefault() // so the browser doesn't submit this thing
emit('form:submit')
}
}
```

Results in:

```html
<form>
<div></div>
<label>Email<br><input type="email" value=""></label>
<button>submit!</button>
</form>
```

In this example, the `<input type='email'>` field will be validated using the
built-in browser's validation framework for email addresses. When the user
`blur`s from the field, it'll attempt to validate the data inside, and if
it isn't valid, it'll display the error in the `<div>` located at the top
of the form.


## Options

#### `elementOptions:object(key:string,value:any)`
All the elements accept an `elementOptions` hash as the final argument which controls any additional element attributes you may wish to set, in addition to the following:

* `classes:array(string)` - an array of class names to apply, in addition to the default ones.
* `style:string` - a string containing inline style information

If you wish to override all the classes attached to the input, you can do so by providing the `class` key on this options list.

Event handlers should be provided using the `on[event]` key, e.g. `oninput` or `onblur`.

Each element type will have specific attributes that can be set in addition to the [default attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes) provided by the DOM spec.

## API

#### `error(opts:elementOptions):ErrorDisplay`
Create a new error display object

`ErrorDisplay`:
* `#render():HTMLDivElement` - return a `<div>` element, using the `opts` provided by the constructor
* `#displayError(key:string, text:string):undefined` - Add a new error to the display
* `#removeError(key:string):undefined` - Remove an error from the display
* `#clear():undefined` - Remove all errors

## Elements

#### `input(label:string, type:string, inputHandler:function(evt:HTMLInputEvent, value:(string|number):HTMLLabelElement`
Create a form input wrapped in a label tag:
#### `input(label:string, type:string, inputHandler:function(evt:HTMLInputEvent, value:(string|number), opts:elementOptions):HTMLInputElement`
Create a form input with various options. `opts` accepts any of the [HTMLInputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes) attributes, the usual default attributes, in addition to several custom fields:

* `validate:boolean` - should validation be attempted when the `blur` event has been fired
* `validator:function($el:HTMLInputElement):boolean` - a custom validation method that returns true/false. If you need to set a custom error message, you should use `$el.setCustomValidity`, but don't forget to set to an empty string when the error has been resolved!
* `errorDisplay:ErrorElement` - the error element that validation errors should be placed into. Must match the API provided by the included `error` object


```html
<label>[label text]
<input type=[type] value=[value] oninput=[inputHandler]>
</label>
<input type=[type] value=[value] oninput=[inputHandler]>
```
---

#### `button(text:string, clickHandler:function(evt:HTMLClickEvent):HTMLButtonElement`
Create a button with a click handler:
#### `button(text:string, clickHandler:function(evt:HTMLClickEvent), opts:elementOptions):HTMLButtonElement`
Create a button with a click handler.

```html
<button onclick=[clickHandler]>[text]</button>
```
---

#### `label(text:string, opts:elementOptions):HTMLLabellement`
Create a label

```html
<label>[text]</label>
```
---

#### `labeledInput(label:string, type:string, inputHandler:function(evt:HTMLInputEvent, value:(string|number), labelOpts:elementOptions, inputOpts:elementOptions)`
Create a form input element wrapped by a label

```html
<label>[text]<br><input type=[type] value=[value] oninput=[inputHandler]></label>
```

## License
Copyright © 2017 Scripto, LLC. All rights reserved
18 changes: 18 additions & 0 deletions create-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = function createValidator (errDisplay, validator) {
const key = (new Date() % 9e6).toString(36)

return function checkValidity (evt) {
const $el = evt.currentTarget
if (typeof validator !== 'function') {
validator = $el.checkValidity.bind($el)
}

if (!validator($el)) {
$el.dataset.valid = false
return errDisplay.displayError(key, $el.validationMessage)
}

$el.dataset.valid = true
errDisplay.removeError(key)
}
}
4 changes: 3 additions & 1 deletion element-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ function applyClasses (classes, opts) {
return classes
}

// We don't want to set any of these keys directly on the DOM object that we create
const optsIgnore = ['classes', 'style', 'validator', 'errDisplay', 'validate']
function applyOpts (el, opts) {
Object.keys(opts)
.filter((key) => key !== 'classes' || key !== 'style')
.filter((key) => !optsIgnore.includes(key))
.forEach((key) => {
el[key] = opts[key]
})
Expand Down
6 changes: 4 additions & 2 deletions error-el.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ function error (opts) {
}

function removeError (key) {
messages.delete(key)
container.removeChild(getMsgDiv(key))
if (messages.has(key)) {
messages.delete(key)
container.removeChild(getMsgDiv(key))
}
}

function clear () {
Expand Down
15 changes: 14 additions & 1 deletion input-el.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const applyOpts = require('./element-helper')
const html = require('bel')

const applyOpts = require('./element-helper')
const mergeFuncs = require('./merge-function')
const createValidator = require('./create-validator')

const noop = () => {}

module.exports = input
Expand All @@ -19,5 +23,14 @@ function input (type, handler, value, inputOpts) {
oninput=${handler}
value="${value}">`

if (inputOpts.validate || typeof inputOpts.validator === 'function') {
const validator = createValidator(inputOpts.errorDisplay, inputOpts.validator)
const blur = inputOpts.onblur

inputOpts.onblur = typeof blur === 'function'
? mergeFuncs(validator, blur) : validator
}

return applyOpts.opts($inputEl, inputOpts)
}

7 changes: 7 additions & 0 deletions merge-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = function merge (...funcs) {
return function (...args) {
for (let i = 0, len = funcs.length; i < len; i++) {
funcs[i].call(funcs[i], ...args)
}
}
}
9 changes: 7 additions & 2 deletions standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ const scratch = {
}

function main (state, emit) {
const formErrors = els.error()

const inputOpts = {
classes: ['w-50'],
validate: true,
onblur: function (evt) {
console.log('you blurred me')
}
},
errorDisplay: formErrors
}

const $input = els.labeledInput('test input', 'text', oninput, scratch.test, inputOpts)
const $input = els.labeledInput('test input', 'email', oninput, scratch.test, inputOpts)

return html`<div class="dt w-third center vh-100">
<div class="v-mid dtc">
${formErrors.render()}<br>
<b>Button pushed:</b> ${scratch.pushed}<br>
<b>Input value:</b> ${scratch.test}<br>
${$input}<br>
Expand Down
23 changes: 23 additions & 0 deletions test/merge-function.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const test = require('tape')
const mergeFuncs = require('../merge-function')

test('funky merge', (t) => {
t.plan(8)

function foo (...args) {
t.equal(args.length, 2, '2 args')
t.equal(args[0], 'beep', 'beepin')
t.equal(args[1], 'boop', 'boopin')
t.pass('foo called')
}

function bar (...args) {
t.equal(args.length, 2, '2 args')
t.equal(args[0], 'beep', 'beepin')
t.equal(args[1], 'boop', 'boopin')
t.pass('bar called')
}

const merged = mergeFuncs(foo, bar)
merged('beep', 'boop')
})
101 changes: 101 additions & 0 deletions test/validation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const test = require('tape')

const error = require('../error-el')
const input = require('../input-el')

const emailErrors = {
chrome: "Please include an '@' in the email address. 't' is missing an '@'.",
firefox: 'Please enter an email address.',
safari: 'Please enter an email address.'
}

let browser = 'chrome'
let ua = navigator.userAgent.toLowerCase()
if (ua.includes('firefox')) {
browser = 'firefox'
} else if (ua.includes('chrome')) {
browser = 'chrome'
} else if (ua.includes('safari')) {
browser = 'safari'
}

test('no validation, no problems', (t) => {
const err = error()
const inputOpts = {
validate: false,
errorDisplay: err
}
const $err = err.render()
const $input = input('email', null, '', inputOpts)
$input.value = 't'
$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 0, 'no errors')
t.end()
})

test('no explicit validation, no problems', (t) => {
const err = error()
const inputOpts = {
errorDisplay: err
}
const $err = err.render()
const $input = input('email', null, '', inputOpts)
$input.value = 't'
$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 0, 'no errors')
t.end()
})

test('built-in validation', (t) => {
t.plan(9) // every time you call blur, the custom onblur runs...
const err = error()
const inputOpts = {
errorDisplay: err,
validate: true,
onblur: () => t.pass('ran custom on blur')
}
const $err = err.render()
const $input = input('email', null, '', inputOpts)

$input.value = 't'
$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 1, 'show built-in error')

$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 1, 'still only one error')
t.equal($err.children[0].innerText, emailErrors[browser], 'chrome error message')

$input.value = 't@t'
$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 0, 'no errors')

$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 0, 'still no errors')
})

test('custom validation', (t) => {
const err = error()
const inputOpts = {
errorDisplay: err,
validator: ($el) => {
if ($el.value !== 'boop') {
$el.setCustomValidity('You must boop!')
return false
}
$el.setCustomValidity('')
return true
}
}
const $err = err.render()
const $input = input('email', null, '', inputOpts)

$input.value = 't'
$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 1, 'only one error')
t.equal($err.children[0].innerText, 'You must boop!', 'custom error message')

$input.value = 'boop'
$input.dispatchEvent(new window.Event('blur'))
t.equal($err.children.length, 0, 'no errors')
t.end()
})

0 comments on commit f868a57

Please sign in to comment.