Skip to content

Instantly share code, notes, and snippets.

@mariochavez
Created June 13, 2019 19:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mariochavez/c86e967893da0553ec9576db63aa805d to your computer and use it in GitHub Desktop.
Save mariochavez/c86e967893da0553ec9576db63aa805d to your computer and use it in GitHub Desktop.
Stimulus validations
export default class BaseValidator {
constructor(value, options, errorMessages) {
this.value = value
this.options = options
this.errorMessages = errorMessages
}
validate() {
return { valid: false, message: 'Implement me!'}
}
}
import { Validation } from "../validators"
import { FormValidationController } from "./form_validation_controller"
export default class BulmaFormValidationController extends FormValidationController {
render(target, validation) {
let validator = validation.find((validator) => !validator.result.valid)
if (validator) {
this.renderValidation(target, validator)
} else {
this.clearValidation(target, validator)
}
}
clearValidation(target, validator) {
target.classList.remove('is-danger')
let parent = target.parentNode
let help = parent.querySelector('p.help')
if (help) {
parent.removeChild(help)
}
}
renderValidation(target, validator) {
if (target.classList.contains('is-danger')) {
return
}
target.classList.add('is-danger')
let help = document.createElement('p')
help.setAttribute('class', 'help is-danger')
let message = document.createTextNode(validator.result.message)
help.appendChild(message)
let parent = target.parentNode
parent.appendChild(help)
}
}
import { Controller } from "stimulus"
import { Validation } from "../validators"
export class FormValidationController extends Controller {
connect() {
this.validation = new Validation()
this.validatableInputs = []
this.bindInputs()
this.bindForm()
this.errors = {}
}
disconnect() {
this.validatableInputs.forEach((input) => input.removeEventListener('blur', this.validate))
this.element.removeEventListener('submit', this.validateAll)
}
bindForm() {
this.element.addEventListener('submit', this.validateAll.bind(this))
}
bindInputs() {
let inputs = this.element.querySelectorAll('input')
inputs.forEach((input) => {
let input_name = input.getAttribute('id')
if (input_name) {
let validate = this.validation.validatable(input_name)
if (validate) {
input.addEventListener('blur', this.validate.bind(this))
this.validatableInputs.push(input)
}
}
})
}
inputValue(input) {
return input.value
}
validForm() {
let valid = Object.values(this.errors).some((value) => value)
return !valid
}
validateAll(event) {
this.validatableInputs.forEach((input) => this.validate({ target: input }))
if (!this.validForm()) {
event.preventDefault()
event.stopPropagation()
let submit = event.target.querySelector('[type="submit"]')
if (submit) {
submit.disabled = false
}
}
}
validate(event) {
let inputValue = this.inputValue(event.target)
let inputId = event.target.getAttribute('id')
let validationResult = this.validation.validate(inputValue, inputId)
let validator = validationResult.find((validator) => !validator.result.valid)
this.errors[inputId] = !!validator
this.render(event.target, validationResult)
}
render(target, validation) {
console.log('Implement me!')
}
}
import BaseValidator from "./base_validator"
export default class NumericalityValidator extends BaseValidator {
validate() {
let valid = true
let errorMessage = ''
if (!this.value || this.value === '') {
valid = false
errorMessage = this.errorMessages['not_a_number']
} else {
let number = Number(this.value)
Object.keys(this.options).some((key) => {
let result = this.verifyNumber(number, key, this.options[key])
if (!result) {
valid = result
errorMessage = this.getErrorMessage(key)
return !result
}
})
}
return { valid: valid, message: errorMessage }
}
verifyNumber(number, option, value) {
if (option === 'only_integer' && value) {
return Number.isInteger(number)
} else if (option === 'greater_than') {
return number > value
} else {
false
}
}
getErrorMessage(option) {
let errorKey = option
if (option === 'only_integer') {
errorKey = 'not_an_integer'
}
return this.errorMessages[errorKey]
}
}
import BaseValidator from "./base_validator"
export default class PresenceValidator extends BaseValidator {
validate() {
let valid = false
let errorMessage = ''
if (this.value) {
let cleanValue = this.value.trim()
valid = cleanValue !== ''
}
if (!valid) {
errorMessage = this.errorMessages['blank']
}
return { valid: valid, message: errorMessage }
}
}
class ValidationToJs
VALIDATORS = ["PresenceValidator", "NumericalityValidator"].freeze
CHECKS = {
presence: [:blank],
numericality: [:greater_than, :not_an_integer, :not_a_number]
}.freeze
def model_validations(model)
validations = {}
model_name = model.to_s.demodulize.downcase
model.validators.each do |validator|
validator_name = validator.class.to_s.demodulize
continue if !VALIDATORS.include?(validator_name)
validator.attributes.each do |attribute|
validator_clean_name = validator_name.downcase.sub("validator", "")
attribute_name = "#{model_name}_#{attribute}"
validations[attribute_name] ||= []
validations[attribute_name].push({
validator: validator_clean_name,
options: validator.options ,
error_messages: error_messages(model, attribute, CHECKS[validator_clean_name.to_sym], false, validator.options)
})
end
end
validations
end
private
def error_messages(model, attribute, types, full = false, options = {})
model_name = model.to_s.demodulize.downcase
errors = ActiveModel::Errors.new(model.new)
messages = {}
Array(types).each do |type|
value = options[type]
messages[type] = error_message(errors, attribute, type, full, value.present? ? { count: value } : {})
end
messages
end
def error_message(errors, attribute, type, full, options = {})
message = errors.generate_message(attribute, type, options)
errors.full_message(attribute, message) if full
message
end
end
<% models = [Expense] %>
<% validation = ValidationToJs.new %>
<% validations = {} %>
<% models.each do |model| %>
<% validations.merge!(validation.model_validations(model)) %>
<% end %>
let validations = <%= validations.to_json %>
export default validations;
import Validations from "./validations.js.erb"
import BaseValidator from "./base_validator"
import PresenceValidator from "./presence_validator"
import NumericalityValidator from "./numericality_validator"
let validators = {
presence: PresenceValidator,
numericality: NumericalityValidator
}
export class Validation {
validatable(input_id) {
return Validations[input_id] !== undefined
}
validate(value, id) {
let validations = Validations[id]
let results = validations.map((validation) => {
let validator = new validators[validation.validator](value, validation.options, validation.error_messages)
return { validator: validation.validator, result: validator.validate() }
});
return results
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment