Skip to content

Instantly share code, notes, and snippets.

@h3h
Created October 20, 2014 14:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save h3h/cbd225d944fa392b31d6 to your computer and use it in GitHub Desktop.
Save h3h/cbd225d944fa392b31d6 to your computer and use it in GitHub Desktop.
Automatic Per-field Background Validation for Forms
# Automatic Per-field Background Validation for Forms
#
# Usage:
#
# 1. Add a 'bg-validatible' class to your <form>
# 2. Add data-bg-validation-url-template="/validation/{field_id}" to your <form>
# 3. Add data-bg-validation-success-message="Looks good!" to your <form> or each validatible element
# 4. Add data-bg-validation-output="relative_selector_to_field" to the <form> or each validatable element
# 5. Add data-bg-validation-field="some_field_id" to each validatable field
# 6. (Optionally) Add a data-bg-validation-include="other_id,other2_id" attribute to the validatable field to send
# other form values along with the validation.
#
# The validator will make requests to the URL constructed with the bg-validation-url-template value and decide on
# success or failure based on the HTTP status code returned: 204 for success, 400 for failure.
#
# Copyright (c) 2014 Brad Fults
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
class BackgroundValidator
constructor: (@form, attrs) ->
@attr_prefix = 'data-bg-validation'
@attrs_for_element = {}
@dataset_prefix_regex = /^bgValidation/
# Ensure that all required attributes are on the <form> & default optional ones
attrs = @cleanAttrs(attrs)
@output_selector = attrs['output']
@success_message = attrs['success-message'] or "Success"
@url_template = attrs['url-template'] or throw "Missing attribute: #{@attr_prefix}-url-template!"
# Ensure that each field has all required attributes & attach events
@fields = @form.find("[#{@attr_prefix}-field]:input")
@fields.each (ix, el) =>
@ensureFieldValidatible(el)
@attachEvents(el)
@elLastFocused = null
# == Validation Lifecycle Methods
# Clear the validation results for a set of fields in a jQuery object.
#
clearFieldResults: ($query) ->
$query.each (ix, el) =>
@writeData(el, 'validation-value', false)
@setValidationResult(el, false)
# Run validation for a set of fields in a jQuery object unless validation has
# already been run for the current value of the field.
#
ensureValidationOrValidate: ($query) ->
$query.each (ix, el) =>
$el = $(el)
# if this element hasn't been validated (with its current value)
unless @readData(el, 'validation-value') is $el.val()
# save its current value as the value we're validating
@writeData(el, 'validation-value', $el.val())
# add any included fields for co-validation
elements = $el.add($("##{id}") for id in @readData(el, 'includes'))
data = ($(element).fieldSerialize() for element in elements).join('&')
# make a validation call
$.ajax @url_template.replace(/\{field_id\}/, @readData(el, 'field')),
data: data
dataType: 'json'
type: 'POST'
statusCode:
204: @validationSuccessHandlerFor(el)
400: @validationFailureHandlerFor(el)
# Return a success handler for the given element.
#
validationSuccessHandlerFor: (el) ->
(data, status_string, xhr) =>
@setValidationResult(el, 'success')
# Return a failure handler for the given element.
#
validationFailureHandlerFor: (el) ->
(xhr, status_string, error_string) =>
response = $.parseJSON(xhr.responseText)
@setValidationResult(el, response['errors'][0])
# == Utility Methods
# Attaches all event handlers needed for validation of a given field.
#
attachEvents: (el) ->
$(el).on 'focus change', (evt) =>
# track the most recent element to receive the 'focus' event to get around
# a browser bug where 'focus' is fired again on blur
if @elLastFocused isnt evt.target
@elLastFocused = evt.target
@clearFieldResults($(el))
@ensureValidationOrValidate(@fieldsBefore(el))
$(el).on 'change', (evt) =>
@ensureValidationOrValidate($(el))
# Removes common prefix from all attribute keys.
#
cleanAttrs: (attrs) ->
newAttrs = {}
for own key, value of attrs
if @dataset_prefix_regex.test(key)
# remove prefix, convert camelCase to lowercase-dashes
newKey = key.replace(@dataset_prefix_regex, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
newAttrs[newKey] = value
newAttrs
# Ensure that a given field can be validated with the library.
#
ensureFieldValidatible: (el) ->
attrs = @cleanAttrs($(el).data())
includes = attrs['include'] and attrs['include'].split(',') or []
attrs['field'] or throw "Missing attribute on element #{el.id}: #{@attr_prefix}-field."
attrs['output'] or @output_selector or throw "Attribute #{@attr_prefix}-output must be defined."
$("##{include_id}").exists() or throw "Missing field #{include_id} referenced from #{el.id}." for include_id in includes
@writeData el,
'field': attrs['field']
'output': attrs['output'] or @output_selector
'includes': includes
'success-message': attrs['success-message'] or @success_message
$output = @form.find(@readData(el, 'output'))
@writeData(el, 'original-tip', $output.html())
# Return a jQuery set of validatible elements occurring before the given element.
#
fieldsBefore: (el) ->
@fields.filter(":lt(#{@fields.index(el)})")
# Store a validation result on a particular element.
#
setValidationResult: (el, value) ->
@writeData(el, 'validation-result', value)
@updateValidationUI(el)
# Reads custom data for a given element.
#
readData: (el, key) ->
data = @attrs_for_element[el.dataset.bgValidationField]
if key? then data[key] else data
# Writes custom data for a given element.
#
writeData: (el, key_or_values, value) ->
base_key = el.dataset.bgValidationField
if value?
@attrs_for_element[base_key][key_or_values] = value
else
@attrs_for_element[base_key] = key_or_values
# Update the output UI based on the current validation result, including text
# in the specified output element and CSS classes on the input element.
#
updateValidationUI: (el) ->
$output = @form.find(@readData(el, 'output'))
result = @readData(el, 'validation-result')
$(el).removeClass('input-success input-warning')
message = if result is false
@readData(el, 'original-tip')
else if result is 'success'
$(el).addClass('input-success')
@readData(el, 'success-message')
else
$(el).addClass('input-warning')
result
$output.html(message)
# Initialize all bg-validatible forms on the current page.
#
$ ->
$('form.bg-validatible').each (ix, form) ->
$form = $(form)
new BackgroundValidator($form, $form.data())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment