Compare commits

..

24 Commits

Author SHA1 Message Date
John Bintz 7ff9dc5ff9 remove a thing 2011-05-19 22:31:27 -04:00
John Bintz 0ba8019c64 fix the helper 2011-05-19 22:23:48 -04:00
John Bintz 134f93be86 oops 2011-05-19 20:58:34 -04:00
John Bintz 81bd5c176c better routes to trigger history 2011-05-19 20:56:04 -04:00
John Bintz 1ab4d353fa tyop and history fix 2011-05-19 20:53:39 -04:00
John Bintz b7d0e181d8 match rails 3.1 asset structure 2011-05-19 18:38:30 -04:00
John Bintz b0a8425617 more fixes 2011-05-13 10:22:13 -04:00
John Bintz 6b31491da7 another fix 2011-05-13 10:11:03 -04:00
John Bintz 8aea97569e view cleanup 2011-05-13 10:06:44 -04:00
John Bintz 755ffdb6c0 fix up collection view 2011-05-13 10:04:42 -04:00
John Bintz d7a77e672b update some templates 2011-05-09 21:47:00 -04:00
John Bintz 429549027f clean up mistakes 2011-05-09 07:35:37 -04:00
John Bintz a37de3c20c add coffeescript 2011-05-08 22:44:21 -04:00
John Bintz 3b1f15f9ab clean up test 2011-05-07 12:50:06 -04:00
John Bintz 5c2e078c02 fix bugs in the view templates 2011-05-03 16:10:26 -04:00
John Bintz ed9a824c37 tyop 2011-05-03 15:57:57 -04:00
John Bintz 97efc633de more template work, more fun defaults 2011-05-03 15:53:36 -04:00
John Bintz 14a2f3f714 more javascript updates 2011-05-03 15:43:19 -04:00
John Bintz 2919235390 make views more useful when created 2011-04-29 15:05:54 -04:00
John Bintz 8fbe5628ff merge 2011-04-23 19:09:03 -04:00
John Bintz b9c2f64d95 app scaffold 2011-04-23 19:02:33 -04:00
John Bintz adf18ae119 add app helper 2011-04-23 19:02:33 -04:00
John Bintz a22177ba54 Merge branch 'app-helper' into develop 2011-04-23 11:15:10 -04:00
John Bintz 1e9213f653 add app helper 2011-04-23 11:13:54 -04:00
27 changed files with 625 additions and 105 deletions

View File

@ -2,4 +2,12 @@ Autotest.add_hook(:initialize) do |at|
at.add_mapping(%r{bin/(.*)}, true) do |filename, matches|
at.files_matching(%r{spec/bin/#{matches[1]}_spec\.rb})
end
at.add_mapping(%r{spec/(.*)}, true) do |filename, matches|
at.files_matching(%r{#{filename}})
end
at.add_mapping(%r{templates/.*}, true) do |filename, matches|
at.files_matching(%r{spec/bin/.*_spec\.rb})
end
end

View File

@ -7,15 +7,36 @@ require 'thor/group'
class BackboneGenerator < Thor
include Thor::Actions
class_option :coffee, :type => :boolean, :desc => 'Generate CoffeeScript instead of JavaScript'
def self.source_root
File.expand_path('../../templates', __FILE__)
end
no_tasks do
def extension
options[:coffee] ? 'coffee' : 'js'
end
def script_target
options[:coffee] ? 'app/assets/javascripts' : 'public/javascripts'
end
def view_target
'app/views'
end
def test_target
'spec/javascripts'
end
def underscore_name
singularize(Thor::Util.snake_case(@name.gsub("::", "/")))
end
def css_class_name
underscore_name.gsub(%r{[^a-z0-9_]}, '-')
end
def plural_underscore_name
pluralize(underscore_name)
end
@ -29,25 +50,25 @@ class BackboneGenerator < Thor
end
def generate_model
template('model.js.erb', "public/javascripts/models/#{underscore_name}.js")
template('model_spec.js.erb', "spec/javascripts/models/#{underscore_name}_spec.js")
template("model.#{extension}.erb", File.join(script_target, "models", "#{underscore_name}.#{extension}"))
template("model_spec.#{extension}.erb", File.join(test_target, "models", "#{underscore_name}_spec.#{extension}"))
end
def generate_view
template('view.js.erb', "public/javascripts/views/#{underscore_name}_view.js")
template('view.jst.erb', "app/views/#{underscore_name}s/view.jst")
template('view_spec.js.erb', "spec/javascripts/views/#{underscore_name}_view_spec.js")
template("view.#{extension}.erb", File.join(script_target, "views", "#{underscore_name}_view.#{extension}"))
template("view.jst.erb", File.join(view_target, "#{underscore_name}s/view.jst"))
template("view_spec.#{extension}.erb", File.join(test_target, "views", "#{underscore_name}_view_spec.#{extension}"))
end
def generate_collection
template('collection.js.erb', "public/javascripts/collections/#{plural_underscore_name}.js")
template('collection_spec.js.erb', "spec/javascripts/collections/#{plural_underscore_name}_spec.js")
template("collection.#{extension}.erb", File.join(script_target, "collections", "#{plural_underscore_name}.#{extension}"))
template("collection_spec.#{extension}.erb", File.join(test_target, "collections", "#{plural_underscore_name}_spec.#{extension}"))
end
def generate_collection_view
template('collection_view.js.erb', "public/javascripts/views/#{plural_underscore_name}_view.js")
template('collection_view.jst.erb', "app/views/#{plural_underscore_name}/list.jst")
template('collection_view_spec.js.erb', "spec/javascripts/views/#{plural_underscore_name}_view_spec.js")
template("collection_view.#{extension}.erb", File.join(script_target, 'views', "#{plural_underscore_name}_view.#{extension}"))
template("collection_view.jst.erb", File.join(view_target, "#{plural_underscore_name}/list.jst"))
template("collection_view_spec.#{extension}.erb", File.join(test_target, "views", "#{plural_underscore_name}_view_spec.#{extension}"))
end
end
@ -83,7 +104,20 @@ class BackboneGenerator < Thor
desc 'spec-helper', "Generate a spec helper for Backbone things"
def spec_helper
template('spec_helper.js.erb', 'spec/javascripts/helpers/backbone_spec_helper.js')
template("spec_helper.#{extension}.erb", File.join(test_target, "helpers", "backbone_spec_helper.#{extension}"))
end
desc "app-helper", "Generate an application helper for useful Backbone things"
def app_helper
template("app_helper.#{extension}.erb", File.join(script_target, "application", "backbone_helper.#{extension}"))
end
desc "app-scaffold", "Generate an application scaffold"
def app_scaffold
template("app_view.#{extension}.erb", File.join(script_target, "application", "app_view.#{extension}"))
template("app_view.jst.erb", File.join(view_target, "application", "app_view.jst"))
template("controller.#{extension}.erb", File.join(script_target, "application", "controller.#{extension}"))
template("app_view_spec.#{extension}.erb", File.join(test_target, "application", "app_view_spec.#{extension}"))
end
private

View File

@ -8,9 +8,128 @@ describe 'backbone-generator' do
FileUtils.rm_rf 'app'
end
def run(*opts)
system %{bin/backbone-generator #{opts.join(' ')}}
end
before { clean! }
after { clean! }
describe 'coffeescript' do
def should_generate_model
File.file?(model = 'app/assets/javascripts/models/section/model.coffee').should be_true
File.file?(spec = 'spec/javascripts/models/section/model_spec.coffee').should be_true
File.read(model).should match(/SectionModel/)
File.read(spec).should match(/SectionModel/)
end
def should_generate_view
File.file?(view = 'app/assets/javascripts/views/section/model_view.coffee').should be_true
File.file?(spec = 'spec/javascripts/views/section/model_view_spec.coffee').should be_true
File.file?(template = 'app/views/section/models/view.jst').should be_true
File.read(view).should match(/SectionModel/)
File.read(view).should match(%r{template: JST\['section/models/view'\]})
File.read(spec).should match(/SectionModel/)
end
def should_generate_collection
File.file?(collection = 'app/assets/javascripts/collections/section/models.coffee').should be_true
File.file?(spec = 'spec/javascripts/collections/section/models_spec.coffee').should be_true
File.read(collection).should match(/SectionModels/)
File.read(collection).should_not match(/SectionModelss/)
File.read(collection).should match(%r{section/model})
File.read(spec).should match(/SectionModels/)
File.read(spec).should_not match(/SectionModelss/)
end
def should_generate_collection_view
File.file?(view = 'app/assets/javascripts/views/section/models_view.coffee').should be_true
File.file?(spec = 'spec/javascripts/views/section/models_view_spec.coffee').should be_true
File.file?(template = 'app/views/section/models/list.jst').should be_true
File.read(view).should match(/SectionModelsView/)
File.read(view).should_not match(/SectionModelssView/)
File.read(view).should match(/SectionModelView/)
File.read(view).should match(%r{template: JST\['section/models/list'\]})
File.read(spec).should match(/SectionModelsView/)
File.read(spec).should_not match(/SectionModelssView/)
end
describe 'model' do
it "should generate the model files" do
run "model", "Section::Model", '--coffee'
should_generate_model
end
end
describe 'view' do
it "should generate the model files" do
run "view", "Section::Model", '--coffee'
should_generate_view
end
end
describe 'collection view' do
it "should generate the collection view files" do
run "collection-view", "Section::Model", '--coffee'
should_generate_collection_view
end
end
describe 'collection' do
it "should generate the collection files" do
run "collection", "Section::Model", '--coffee'
should_generate_collection
end
end
describe 'scaffold' do
it "should generate everything!" do
run "scaffold", "Section::Model", '--coffee'
should_generate_model
should_generate_view
should_generate_collection
should_generate_collection_view
end
end
describe 'spec helper' do
it "should generate a spec helper" do
run "spec-helper", '--coffee'
File.file?(collection = 'spec/javascripts/helpers/backbone_spec_helper.coffee').should be_true
end
end
describe 'app helper' do
it "should generate an app helper" do
run "app-helper", '--coffee'
File.file?(collection = 'app/assets/javascripts/application/backbone_helper.coffee').should be_true
end
end
describe 'application scaffold' do
it "should generate an application scaffold" do
run "app-scaffold", '--coffee'
File.file?(app = 'app/assets/javascripts/application/app_view.coffee').should be_true
File.file?(app_view = 'app/views/application/app_view.jst').should be_true
File.file?(controller = 'app/assets/javascripts/application/controller.coffee').should be_true
File.file?(spec = 'spec/javascripts/application/app_view_spec.coffee').should be_true
end
end
end
describe 'javascript' do
def should_generate_model
File.file?(model = 'public/javascripts/models/section/model.js').should be_true
File.file?(spec = 'spec/javascripts/models/section/model_spec.js').should be_true
@ -57,7 +176,7 @@ describe 'backbone-generator' do
describe 'model' do
it "should generate the model files" do
system %{bin/backbone-generator model Section::Model}
run "model", "Section::Model"
should_generate_model
end
@ -65,7 +184,7 @@ describe 'backbone-generator' do
describe 'view' do
it "should generate the view files" do
system %{bin/backbone-generator view Section::Model}
run "view", "Section::Model"
should_generate_view
end
@ -74,7 +193,7 @@ describe 'backbone-generator' do
describe 'collection view' do
context 'without trailing s' do
it "should generate the collection view files" do
system %{bin/backbone-generator collection-view Section::Model}
run "collection-view", "Section::Model"
should_generate_collection_view
end
@ -82,7 +201,7 @@ describe 'backbone-generator' do
context 'with trailing s' do
it "should generate the collection view files" do
system %{bin/backbone-generator collection-view Section::Models}
run "collection-view", "Section::Models"
should_generate_collection_view
end
@ -92,7 +211,7 @@ describe 'backbone-generator' do
describe 'collection' do
context 'without trailing s' do
it "should generate the collection files" do
system %{bin/backbone-generator collection Section::Model}
run "collection", "Section::Model"
should_generate_collection
end
@ -100,7 +219,7 @@ describe 'backbone-generator' do
context 'with trailing s' do
it "should generate the collection files" do
system %{bin/backbone-generator collection Section::Models}
run "collection", "Section::Models"
should_generate_collection
end
@ -109,7 +228,7 @@ describe 'backbone-generator' do
describe 'scaffold' do
it "should generate everything!" do
system %{bin/backbone-generator scaffold Section::Model}
run "scaffold", "Section::Model"
should_generate_model
should_generate_view
@ -120,9 +239,29 @@ describe 'backbone-generator' do
describe 'spec helper' do
it "should generate a spec helper" do
system %{bin/backbone-generator spec-helper}
run "spec-helper"
File.file?(collection = 'spec/javascripts/helpers/backbone_spec_helper.js').should be_true
end
end
describe 'app helper' do
it "should generate an app helper" do
run "app-helper"
File.file?(collection = 'public/javascripts/application/backbone_helper.js').should be_true
end
end
describe 'application scaffold' do
it "should generate an application scaffold" do
run "app-scaffold"
File.file?(app = 'public/javascripts/application/app_view.js').should be_true
File.file?(app_view = 'app/views/application/app_view.jst').should be_true
File.file?(controller = 'public/javascripts/application/controller.js').should be_true
File.file?(spec = 'spec/javascripts/application/app_view_spec.js').should be_true
end
end
end
end

View File

@ -0,0 +1,19 @@
Backbone.Collection.prototype.ensureFetched = (callback) ->
if !@_alreadyEnsureFetched
_refresher = null
_refresher =>
this.unbind('refresh', _refresher)
callback.apply(this)
@_alreadyEnsureFetched = true
this.bind('refresh', _refresher)
this.fetch()
else
callback.apply(this)
Backbone.View.prototype.attributes = ->
attrs = {}
for field in @attributeFields
do (field) =>
attrs[field] = this.$("input[name='#{field}']").val()
attrs

View File

@ -0,0 +1,29 @@
/* Ensure a Collection is fetched and has entries before running a block of code. */
_.extend(Backbone.Collection.prototype, {
ensureFetched: function(callback) {
if (this._alreadyEnsureFetched) {
var _this = this;
var _refresher = function() {
_this.unbind('refresh', _refresher);
callback.apply(_this);
_this._alreadyEnsureFetched = true;
};
this.bind('refresh', _refresher);
this.fetch();
} else {
callback.apply(this);
}
}
});
_.extend(Backbone.View.prototype, {
attributes: function() {
var attrs = {};
var _this = this;
_.each(this.attributeFields, function(field) {
attrs[field] = _this.$('input[name="' + field + '"]').val();
});
return attrs;
}
});

View File

@ -0,0 +1,10 @@
class window.AppView extends Backbone.View
el: '#application'
template: JST['application/app_view']
initialize: ->
controller = new Controller({app: this})
Backbone.history.start()
render: =>
$(@el).html(@template())
this

16
templates/app_view.js.erb Normal file
View File

@ -0,0 +1,16 @@
var AppView = Backbone.View.extend({
el: '#application',
template: JST['application/app_view'],
initialize: function() {
_.bindAll(this, 'render');
var controller = new Controller({app: this});
Backbone.history.start();
},
render: function() {
$(this.el).html(this.template());
return this;
},
});

View File

@ -0,0 +1 @@
<!-- application view goes here -->

View File

@ -0,0 +1,9 @@
describe 'AppView', ->
appView = null
beforeEach, ->
appView = new AppView()
it 'should render', ->
expect($(appView.render().el)).toContain('.something')

View File

@ -0,0 +1,12 @@
describe('AppView', function() {
var app_view;
beforeEach(function() {
app_view = new AppView();
});
it('should render', function() {
expect($(app_view.render().el)).toContain('.something');
});
});

View File

@ -0,0 +1,4 @@
class window.<%= plural_object_name %> extends Backbone.Collection
url: '/<%= plural_underscore_name %>'
model: <%= object_name %>

View File

@ -0,0 +1,14 @@
describe '<%= plural_object_name %>', ->
collection = null
withServer()
it 'should fetch records from the API', ->
collection = new <%= plural_object_name %>()
@server.respondWith('GET', '/<%= plural_underscore_name %>', @validJSONResponse([{id: 1}]))
collection.fetch()
@server.respond()
expect(collection.length).toEqual(1)

View File

@ -0,0 +1,21 @@
class window.<%= plural_object_name %>View extends Backbone.View
events: {
'click button.new': 'addNew'
}
template: JST['<%= plural_underscore_name %>/list']
initialize: ->
@collection.bind('refresh', this.addAll)
render: =>
$(this.el).html(this.template())
this.addAll()
this
addOne: (model) =>
view = new <%= object_name %>View({model: model})
this.$('.list').append(view.render().el)
addAll: =>
@collection.each(this.addOne)
addNew: =>
object = new <%= object_name %>()
@collection.add(object)
this.addOne(object)

View File

@ -1,9 +1,11 @@
var <%= plural_object_name %>View = Backbone.View.extend({
events: {
'click button.new': 'addNew'
},
template: JST['<%= plural_underscore_name %>/list'],
initialize: function(collection) {
_.bindAll(this, 'render', 'addOne', 'addAll');
initialize: function() {
_.bindAll(this, 'render', 'addOne', 'addAll', 'addNew');
this.collection = collection;
this.collection.bind('refresh', this.addAll);
this.render();
@ -19,5 +21,11 @@ var <%= plural_object_name %>View = Backbone.View.extend({
},
addAll: function() {
this.collection.each(this.addOne);
},
addNew: function() {
var object = new <%= object_name %>();
this.collection.add(object);
this.addOne(object);
}
});

View File

@ -1 +1,3 @@
<div class="list"></div>
<button class="new">Create</button>

View File

@ -0,0 +1,26 @@
describe '<%= plural_object_name %>View', ->
view = collection = null
beforeEach ->
collection = new <%= plural_object_name %>()
view = new <%= plural_object_name %>View({collection: collection})
view.render()
it 'should render', ->
expect($(view.el)).toContain('.list')
expect($(view.el)).toContain('button.new')
it 'should add a new model when new is clicked', ->
view.$('button.new').trigger('click')
expect(view.$('.list')).toContain('.<%= css_class_name %>')
expect(collection.length).toEqual(1)
it 'should render the models when re-rendered', ->
expect(view.$('.list')).not.toContain('.<%= css_class_name %>')
model = new <%= object_name %>({id: 1})
collection.add(model)
view.render()
expect(view.$('.list')).toContain('.<%= css_class_name %>')

View File

@ -6,9 +6,17 @@ describe('<%= plural_object_name %>View', function() {
});
it('should render', function() {
view = new <%= plural_object_name %>View(collection);
view = new <%= plural_object_name %>View({collection: collection});
view.render();
expect($(view.el)).toContain('.list');
expect($(view.el)).toContain('button.new');
});
it('should add a new model when new is clicked', function() {
view.$('button.new').trigger('click');
expect(view.$('.list')).toContain('.<%= underscore_name %>');
expect(collection.length).toEqual(1);
});
});

View File

@ -0,0 +1,4 @@
class window.Controller extends Backbone.Controller
routes: {
'index': 'index'
}

View File

@ -0,0 +1,6 @@
var Controller = Backbone.Controller.extend({
routes: {
'index': 'index'
}
});

View File

@ -0,0 +1 @@
class window.<%= object_name %> extends Backbone.Model

View File

@ -0,0 +1,8 @@
describe '<%= object_name %>', ->
model = null
it 'should have some tests', ->
model = new <%= object_name %>()
expect(true).toEqual(false)

View File

@ -0,0 +1,14 @@
window.withServer ->
jasmine.getEnv().withServer()
jasmine.Env.prototype.withServer ->
@currentSuite.beforeEach ->
@server = sinon.fakeServer.create()
@currentSuite.afterEach ->
@server.restore()
beforeEach ->
@validJSONResponse = (data) ->
[ 200, { 'Content-type': 'application/json' }, JSON.stringify(data) ]

25
templates/view.coffee.erb Normal file
View File

@ -0,0 +1,25 @@
class window.<%= object_name %>View extends Backbone.View
events: {
'click button.save': 'save',
'click button.delete': 'destroy'
}
attributeFields: []
template: JST['<%= underscore_name %>s/view']
className: '<%= css_class_name %>'
initialize: ->
_.bindAll(this, 'remove')
@model.bind('change', this.render)
@model.bind('remove', this.remove)
@model.view = this
render: =>
$(@el).html(this.template(@model.toJSON()))
this.$('button.save').text(if @model.isNew() then 'Create' else 'Update')
this.$('button.delete')[if @model.isNew() then 'hide' else 'show']();
this
save: =>
@model.save(this.attributes())
destroy: =>
if confirm("Are you sure?")
@model.destroy()

View File

@ -1,11 +1,31 @@
var <%= object_name %>View = Backbone.View.extend({
events: {
'click button.save': 'save',
'click button.delete': 'destroy'
},
attributeFields: [],
template: JST['<%= underscore_name %>s/view'],
className: '<%= underscore_name %>',
initialize: function() {
_.bindAll(this, 'render');
_.bindAll(this, 'render', 'save', 'destroy', 'remove');
this.model.bind('change', this.render);
this.model.bind('remove', this.remove);
this.model.view = this;
},
render: function() {
$(this.el).html(this.template());
$(this.el).html(this.template(this.model.toJSON()));
this.$('button.save').text(this.model.isNew() ? 'Create' : 'Update');
this.$('button.delete')[this.model.isNew() ? 'hide' : 'show']();
return this;
},
save: function() {
this.model.save(this.attributes(), { success: function(model) { model.view.render(); } });
},
destroy: function() {
if (confirm("Are you sure?")) {
this.model.destroy();
}
}
});

View File

@ -1 +1,2 @@
<!-- your <%= object_name %>View template goes here -->
<!-- your <%= object_name %>View template fields go here -->
<button class="save" /><button class="delete">Delete</button>

View File

@ -0,0 +1,41 @@
describe '<%= object_name %>View', ->
view = model = null
describe 'new record', ->
beforeEach ->
model = new <%= object_name %>
it 'should render with a create button', ->
view = new <%= object_name %>View({model: model})
view.render()
expect(view.$('button.save')).toHaveText('Create')
describe 'existing record', ->
beforeEach ->
model = new <%= object_name %>({id: 1})
setFixtures('<div id="container" />')
view = new <%= object_name %>View({model: model})
view.render()
it 'should render with an update button', ->
expect(view.$('button.save')).toHaveText('Update')
it 'should destroy the model', ->
spyOn(window, 'confirm').andReturn(true)
spyOn(model, 'destroy')
$('#container').append(view.el)
view.$('button.delete').trigger('click')
expect(model.destroy).toHaveBeenCalled()
expect(window.confirm).toHaveBeenCalled()
it 'should remove the view when the model is destroyed', ->
$('#container').append(view.render().el)
expect($('button.save')).toExist()
model.trigger('remove')
expect($('button.save')).not.toExist()

View File

@ -1,10 +1,50 @@
describe('<%= object_name %>View', function() {
var view;
var view, model;
it('should render', function() {
view = new <%= object_name %>View();
describe('new record', function() {
beforeEach(function() {
model = new <%= object_name %>();
});
it('should render with a create button', function() {
view = new <%= object_name %>View({model: model});
view.render();
expect($(view.el)).toContain('.something');
expect(view.$('button.save')).toHaveText('Create');
});
});
describe('existing record', function() {
beforeEach(function() {
model = new <%= object_name %>({id: 1});
setFixtures('<div id="container" />');
view = new <%= object_name %>View({model: model});
});
it('should render with an update button', function() {
view.render();
expect(view.$('button.save')).toHaveText('Update');
});
it('should destroy the model', function() {
spyOn(window, 'confirm').andReturn(true);
spyOn(model, 'destroy');
$('#container').append(view.el);
view.$('button.delete').trigger('click');
expect(model.destroy).toHaveBeenCalled();
expect(window.confirm).toHaveBeenCalled();
});
it('should remove the view when the model is destroyed', function() {
$('#container').append(view.render().el);
expect($('button.save')).toExist();
model.trigger('remove');
expect($('button.save')).not.toExist();
});
});
});