Browse files

adding some client side validation into Backbone.Model

  • Loading branch information...
1 parent b4d3212 commit bc10840d3dc27c346ce09795847692ebc76a0faa Chris Nelson committed Jun 14, 2012
View
15 README.md
@@ -6,10 +6,9 @@ Think formtastic meets backbone with some twitter bootstrap goodness.
At Gaslight we've built several rails apps with backbone and one of the more common complaints from new developers to this stuff is the lack of form helpery goodness that rails gives you. This is our attempt to start filling this gap. Most of the action right now happens in `Backtastic.Views.FormView`. This view is designed to be as a superclass within your application and give you several bits of goodness:
* helpers to generate a form fields with twitter bootstrap compatible markup. So far textField, selectField, dateField
-* save implementation which does the following
- * settings attributes for each form field on the model
- * saves the model and listens for errors
- * parses validation errors from rails and displays errors on individual fields
+* default save implementation that updates the model from the form and persists
+* handling validation errors from rails and displaying errors on individual fields
+* composite views
Usage
-----
@@ -33,6 +32,13 @@ Backtastic provides a render method that will invoke your template with the view
%form
= @textField(label: "Name", field: "Name")
+The field helper method (`@textField()` in this example) creates a subview of the appropriate type which renders a label and form input using twitter bootstrap friendly markup. They also listen to the model for validation errors and display appropriate styling and error messages.
+
+Validation
+----------
+
+Rails validation on the server side will be handled appropriately by the form element views. You can also add client side validation. So far we support presence and format, checkout the Person backbone model in the example app to see how it works. We'll be adding support for reflecting on the rails model adding client side validations to the backbone model "real soon now".
+
Example App
-----------
@@ -58,7 +64,6 @@ I'd like to have some metadata generated form rails and available to backbone so
@form fields["first_name", "last_name"]
-Also, it seems like making client side validation happen when appropriate would be nice. And I'd like to refactor the way FormView deals with rails validation errors, sooner rather than later probably.
Shameless self-promotion
------------------------
View
3 backtastic.gemspec
@@ -21,8 +21,11 @@ Gem::Specification.new do |s|
# specify any dependencies here; for example:
s.add_development_dependency "rspec"
+ s.add_development_dependency "pry"
+
s.add_runtime_dependency "haml_coffee_assets"
s.add_runtime_dependency "rails-backbone"
s.add_runtime_dependency "inflection-js-rails"
s.add_runtime_dependency "twitter-bootstrap-rails"
+ s.add_runtime_dependency "activesupport"
end
View
4 example/Gemfile
@@ -23,6 +23,10 @@ group :assets do
gem 'uglifier', '>= 1.0.3'
end
+group :development, :test do
+ gem "pry"
+end
+
gem 'jquery-rails'
gem "jasminerice"
gem "capybara-webkit"
View
4 example/app/assets/javascripts/backbone/models/people.coffee
@@ -1,6 +1,10 @@
class Example.Models.Person extends Backbone.Model
urlRoot: "/people"
+ @validatePresenceOf "first_name"
+
+ @validateFormatOf "last_name", pattern: /^J.*/
+
class Example.Collections.PeopleCollection extends Backbone.Collection
model: Example.Models.Person
View
2 example/app/assets/javascripts/backbone/views/edit_person_view.coffee
@@ -4,7 +4,7 @@ class Example.Views.EditPersonView extends Backtastic.Views.FormView
constructor: (options)->
super
@occupations = options.occupations
-
+
events:
"submit form": "save"
View
27 example/spec/javascripts/models/person_spec.coffee
@@ -0,0 +1,27 @@
+describe "Person", ->
+ beforeEach ->
+ @person = new Example.Models.Person()
+ describe "validation", ->
+ beforeEach ->
+ @validationErrors = {}
+ @person.on "error", (model, errors) =>
+ @validationErrors = errors
+ describe "failing validation", ->
+ beforeEach ->
+ @person.set first_name: "", last_name: "NotJones"
+ it "should not be valid", ->
+ expect(@person.isValid()).toBeFalsy()
+ it "should have triggered the error event for first name", ->
+ expect(@validationErrors.first_name.length).toEqual 1
+ it "should have triggered the error event for last name", ->
+ expect(@validationErrors.last_name.length).toEqual 1
+ describe "with valid attributes", ->
+ beforeEach ->
+ @person.set first_name: "Fred", last_name: "Jones"
+ it "should be valid", ->
+ expect(@person.isValid()).toTruthy
+ it "should have no errors", ->
+ expect(@validationErrors.first_name?).toBeFalsy()
+
+
+
View
16 example/spec/javascripts/util/module_spec.coffee
@@ -0,0 +1,16 @@
+FooModule =
+ bar: -> "baz"
+
+ classMethods:
+ wuzza: -> "howdy"
+
+class Includee
+
+Backtastic.include Includee, FooModule
+
+describe "Module", ->
+ it "should include module methods", ->
+ expect(new Includee().bar()).toEqual "baz"
+ it "should add class methods", ->
+ expect(Includee.wuzza()).toEqual "howdy"
+
View
2 example/spec/javascripts/views/edit_person_view_spec.coffee
@@ -19,7 +19,7 @@ describe "EditPersonView", ->
beforeEach ->
jasmine.Ajax.useMock()
@editPersonView.$("input[name='first_name']").val("Bob")
- @editPersonView.$("input[name='last_name']").val("Villa")
+ @editPersonView.$("input[name='last_name']").val("Jones")
@editPersonView.save(new jQuery.Event)
@request = mostRecentAjaxRequest()
it "disables the save button", ->
View
5 lib/assets/javascripts/backbone_wrap_errors.coffee
@@ -6,6 +6,5 @@ Backbone.wrapError = (onError, originalModel, options) ->
else
resp
if (onError)
- onError(originalModel, errors, resp, options);
- else
- originalModel.trigger('error', originalModel, errors, resp, options);
+ onError(originalModel, errors, resp, options)
+ originalModel.trigger('error', originalModel, errors, resp, options)
View
1 lib/assets/javascripts/backtastic.coffee
@@ -4,6 +4,7 @@
#= require bootstrap-datepicker
#= require backbone_wrap_errors
#= require_self
+#= require_directory ./util
#= require_tree .
window.Backtastic = {
View
12 lib/assets/javascripts/util/module.coffee
@@ -0,0 +1,12 @@
+moduleKeywords = ['included', 'classMethods']
+
+Backtastic.include = (klass, obj) ->
+ for key, value of obj when key not in moduleKeywords
+ # Assign properties to the prototype
+ klass::[key] = value
+
+ if obj.classMethods
+ klass[key] = value for key, value of obj.classMethods
+
+ obj.included?.apply(klass)
+ this
View
39 lib/assets/javascripts/util/validation.coffee
@@ -0,0 +1,39 @@
+Backtastic.Validators =
+
+ presence: (options) ->
+ (field, value) -> "is required" unless value?.length > 0
+
+ format: (options) ->
+ (field, value) -> "must match specified format." unless value?.match(options?.pattern)
+
+Backtastic.Validation =
+
+ classMethods:
+
+ addValidator: (validator, field, options) ->
+ @validations or = {}
+ @validations[field] or= []
+ @validations[field].push Backtastic.Validators[validator]?(options)
+
+ validatePresenceOf: (field) ->
+ @addValidator("presence", field)
+
+ validateFormatOf: (field, options) ->
+ @addValidator("format", field, options)
+
+ addError: (field, error) ->
+ @errors[field] or= []
+ @errors[field].push error
+
+ clearErrors: -> @errors = {}
+
+ validate: (attributes) ->
+ @clearErrors()
+ return unless @constructor.validations
+ for attr, validators of @constructor.validations
+ for validator in validators
+ error = validator(attr, attributes[attr])
+ @addError(attr, error) if error
+ @errors if _.keys(@errors).length > 0
+
+Backtastic.include Backbone.Model, Backtastic.Validation
View
18 lib/assets/javascripts/views/form_helpers.coffee
@@ -0,0 +1,18 @@
+Backtastic.Views.FormHelpers =
+ fieldView: (fieldViewClass, options) ->
+ fieldView = new fieldViewClass _.extend options,
+ parentView: @
+ model: @model
+ fieldView.toHtml()
+
+ dateField: (options) ->
+ @fieldView(Backtastic.Views.DateFieldView, options)
+
+ textField: (options) ->
+ @fieldView(Backtastic.Views.TextFieldView, options)
+
+ checkBoxField: (options) ->
+ @fieldView(Backtastic.Views.CheckBoxView, options)
+
+ selectField: (options) ->
+ @fieldView(Backtastic.Views.SelectFieldView, options)
View
33 lib/assets/javascripts/views/form_view.coffee
@@ -1,30 +1,11 @@
+#= require ./form_helpers
class Backtastic.Views.FormView extends Backtastic.View
-
- constructor: ->
- super
- @fieldViews = {}
- fieldView: (fieldViewClass, options) ->
- fieldView = new fieldViewClass _.extend options,
- parentView: @
- model: @model
- @fieldViews[options.field] = fieldView
- fieldView.toHtml()
-
- dateField: (options) ->
- @fieldView(Backtastic.Views.DateFieldView, options)
-
- textField: (options) ->
- @fieldView(Backtastic.Views.TextFieldView, options)
-
- checkBoxField: (options) ->
- @fieldView(Backtastic.Views.CheckBoxView, options)
-
- selectField: (options) ->
- @fieldView(Backtastic.Views.SelectFieldView, options)
-
save: (event)->
- @$("input[type='submit']").attr("disabled", "disabled")
event.preventDefault()
- @model.on "error", (model, errors) => @$("input[type='submit']").removeAttr("disabled")
- @model.save @$("form").serializeObject()
+ if @model.set @$("form").serializeObject()
+ @$("input[type='submit']").attr("disabled", "disabled")
+ @model.save {},
+ error: => @$("input[type='submit']").removeAttr("disabled")
+
+Backtastic.include Backtastic.Views.FormView, Backtastic.Views.FormHelpers

0 comments on commit bc10840

Please sign in to comment.