From 22167faf2832c5d883817b5c851fa68c0fe15ee0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 15:50:39 -0600 Subject: [PATCH 01/15] Add template/Builder. It generates basic tags. --- spec/stdlib/template/builder-spec.coffee | 16 +++++++++++++++ src/stdlib/template/builder.coffee | 25 ++++++++++++++++++++++++ src/stdlib/template/close-tag.coffee | 7 +++++++ src/stdlib/template/open-tag.coffee | 7 +++++++ 4 files changed, 55 insertions(+) create mode 100644 spec/stdlib/template/builder-spec.coffee create mode 100644 src/stdlib/template/builder.coffee create mode 100644 src/stdlib/template/close-tag.coffee create mode 100644 src/stdlib/template/open-tag.coffee diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee new file mode 100644 index 000000000..4d552cd2c --- /dev/null +++ b/spec/stdlib/template/builder-spec.coffee @@ -0,0 +1,16 @@ +Builder = require 'template/builder' + +fdescribe "Builder", -> + builder = null + + beforeEach -> builder = new Builder + + describe ".tag(name, args...)", -> + it "can generate simple tags", -> + builder.tag 'div' + expect(builder.toHtml()).toBe("
") + + builder.reset() + builder.tag 'ol' + expect(builder.toHtml()).toBe("
    ") + diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee new file mode 100644 index 000000000..ce3b7964c --- /dev/null +++ b/src/stdlib/template/builder.coffee @@ -0,0 +1,25 @@ +_ = require 'underscore' +OpenTag = require 'template/open-tag' +CloseTag = require 'template/close-tag' + +module.exports = +class Builder + constructor: -> + @reset() + + toHtml: -> + _.map(@document, (x) -> x.toHtml()).join('') + + tag: (name) -> + @openTag(name) + @closeTag(name) + + openTag: (name) -> + @document.push(new OpenTag(name)) + + closeTag: (name) -> + @document.push(new CloseTag(name)) + + reset: -> + @document = [] + diff --git a/src/stdlib/template/close-tag.coffee b/src/stdlib/template/close-tag.coffee new file mode 100644 index 000000000..3e4874dfc --- /dev/null +++ b/src/stdlib/template/close-tag.coffee @@ -0,0 +1,7 @@ +module.exports = +class CloseTag + constructor: (@name) -> + + toHtml: -> + "" + diff --git a/src/stdlib/template/open-tag.coffee b/src/stdlib/template/open-tag.coffee new file mode 100644 index 000000000..c913728b9 --- /dev/null +++ b/src/stdlib/template/open-tag.coffee @@ -0,0 +1,7 @@ +module.exports = +class OpenTag + constructor: (@name) -> + + toHtml: -> + "<#{@name}>" + From e570c5d4541cef2d279fc7a7d27253e0bf2df52e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 15:57:29 -0600 Subject: [PATCH 02/15] Tags can take a function for their content. --- spec/stdlib/template/builder-spec.coffee | 6 ++++++ src/stdlib/template/builder.coffee | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index 4d552cd2c..28f1da96c 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -14,3 +14,9 @@ fdescribe "Builder", -> builder.tag 'ol' expect(builder.toHtml()).toBe("
      ") + it "can generate tags with content", -> + builder.tag 'ol', -> + builder.tag 'li' + builder.tag 'li' + + expect(builder.toHtml()).toBe("
      ") diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee index ce3b7964c..41b6807cf 100644 --- a/src/stdlib/template/builder.coffee +++ b/src/stdlib/template/builder.coffee @@ -10,10 +10,18 @@ class Builder toHtml: -> _.map(@document, (x) -> x.toHtml()).join('') - tag: (name) -> + tag: (name, args...) -> + options = @extractOptions(args) @openTag(name) + options.content?() @closeTag(name) + extractOptions: (args) -> + options = {} + for arg in args + options.content = arg if _.isFunction(arg) + options + openTag: (name) -> @document.push(new OpenTag(name)) From b5a06c288efa35b5a6862ce6fa7484eecb30f90c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 16:05:01 -0600 Subject: [PATCH 03/15] Tags can take textual content. --- spec/stdlib/template/builder-spec.coffee | 9 +++++++++ src/stdlib/template/builder.coffee | 7 +++++++ src/stdlib/template/text.coffee | 6 ++++++ 3 files changed, 22 insertions(+) create mode 100644 src/stdlib/template/text.coffee diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index 28f1da96c..173a3bb60 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -20,3 +20,12 @@ fdescribe "Builder", -> builder.tag 'li' expect(builder.toHtml()).toBe("
      ") + + it "can generate tags with text", -> + builder.tag 'div', "hello" + expect(builder.toHtml()).toBe("
      hello
      ") + + builder.reset() + builder.tag 'div', 22 + expect(builder.toHtml()).toBe("
      22
      ") + diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee index 41b6807cf..1677f4386 100644 --- a/src/stdlib/template/builder.coffee +++ b/src/stdlib/template/builder.coffee @@ -1,6 +1,7 @@ _ = require 'underscore' OpenTag = require 'template/open-tag' CloseTag = require 'template/close-tag' +Text = require 'template/text' module.exports = class Builder @@ -14,12 +15,15 @@ class Builder options = @extractOptions(args) @openTag(name) options.content?() + @text(options.text) if options.text @closeTag(name) extractOptions: (args) -> options = {} for arg in args options.content = arg if _.isFunction(arg) + options.text = arg if _.isString(arg) + options.text = arg.toString() if _.isNumber(arg) options openTag: (name) -> @@ -28,6 +32,9 @@ class Builder closeTag: (name) -> @document.push(new CloseTag(name)) + text: (string) -> + @document.push(new Text(string)) + reset: -> @document = [] diff --git a/src/stdlib/template/text.coffee b/src/stdlib/template/text.coffee new file mode 100644 index 000000000..5f20e1d2e --- /dev/null +++ b/src/stdlib/template/text.coffee @@ -0,0 +1,6 @@ +module.exports = +class Text + constructor: (@string) -> + + toHtml: -> @string + From 21fb88141ee52c3c0acac841b75e7f6036b555f8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 16:16:35 -0600 Subject: [PATCH 04/15] Builder can generate tags with attributes. --- spec/stdlib/template/builder-spec.coffee | 6 ++++++ src/stdlib/template/builder.coffee | 11 ++++++++--- src/stdlib/template/open-tag.coffee | 9 +++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index 173a3bb60..fddbbb9a8 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -29,3 +29,9 @@ fdescribe "Builder", -> builder.tag 'div', 22 expect(builder.toHtml()).toBe("
      22
      ") + it "can generate tags with attributes", -> + builder.tag 'div', id: 'foo', class: 'bar' + fragment = builder.toFragment() + expect(fragment.attr('id')).toBe('foo') + expect(fragment.attr('class')).toBe('bar') + diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee index 1677f4386..f86e6a102 100644 --- a/src/stdlib/template/builder.coffee +++ b/src/stdlib/template/builder.coffee @@ -1,4 +1,5 @@ _ = require 'underscore' +$ = require 'jquery' OpenTag = require 'template/open-tag' CloseTag = require 'template/close-tag' Text = require 'template/text' @@ -11,9 +12,12 @@ class Builder toHtml: -> _.map(@document, (x) -> x.toHtml()).join('') + toFragment: -> + $(@toHtml()) + tag: (name, args...) -> options = @extractOptions(args) - @openTag(name) + @openTag(name, options.attributes) options.content?() @text(options.text) if options.text @closeTag(name) @@ -24,10 +28,11 @@ class Builder options.content = arg if _.isFunction(arg) options.text = arg if _.isString(arg) options.text = arg.toString() if _.isNumber(arg) + options.attributes = arg if _.isObject(arg) options - openTag: (name) -> - @document.push(new OpenTag(name)) + openTag: (name, attributes) -> + @document.push(new OpenTag(name, attributes)) closeTag: (name) -> @document.push(new CloseTag(name)) diff --git a/src/stdlib/template/open-tag.coffee b/src/stdlib/template/open-tag.coffee index c913728b9..ec37868aa 100644 --- a/src/stdlib/template/open-tag.coffee +++ b/src/stdlib/template/open-tag.coffee @@ -1,7 +1,12 @@ +_ = require 'underscore' + module.exports = class OpenTag - constructor: (@name) -> + constructor: (@name, @attributes) -> toHtml: -> - "<#{@name}>" + "<#{@name}#{@attributesHtml()}>" + attributesHtml: -> + s = _.map(@attributes, (value, key) -> "#{key}=\"#{value}\"").join(' ') + if s == "" then "" else " " + s From bd79d9cd5d6aa741daae23cc9d1bd326c9d5fa09 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 16:43:28 -0600 Subject: [PATCH 05/15] Builder correctly generates void (self-closing) tags. --- spec/stdlib/template/builder-spec.coffee | 4 +++ src/stdlib/template/builder.coffee | 35 +++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index fddbbb9a8..deb3af743 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -35,3 +35,7 @@ fdescribe "Builder", -> expect(fragment.attr('id')).toBe('foo') expect(fragment.attr('class')).toBe('bar') + it "can generate self-closing tags", -> + builder.tag 'br', id: 'foo' + expect(builder.toHtml()).toBe('
      ') + diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee index f86e6a102..819842bc7 100644 --- a/src/stdlib/template/builder.coffee +++ b/src/stdlib/template/builder.coffee @@ -6,6 +6,18 @@ Text = require 'template/text' module.exports = class Builder + @elements: + normal: 'a abbr address article aside audio b bdi bdo blockquote body button + canvas caption cite code colgroup datalist dd del details dfn div dl dt em + fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup + html i iframe ins kbd label legend li map mark menu meter nav noscript object + ol optgroup option output p pre progress q rp rt ruby s samp script section + select small span strong style sub summary sup table tbody td textarea tfoot + th thead time title tr u ul video'.split /\s+/ + + void: 'area base br col command embed hr img input keygen link meta param + source track wbr'.split /\s+/ + constructor: -> @reset() @@ -17,10 +29,19 @@ class Builder tag: (name, args...) -> options = @extractOptions(args) - @openTag(name, options.attributes) - options.content?() - @text(options.text) if options.text - @closeTag(name) + + @document.push(new OpenTag(name, options.attributes)) + if @elementIsVoid(name) + if (options.text? or options.content?) + throw new Error("Self-closing tag #{tag} cannot have text or content") + else + options.content?() + @text(options.text) if options.text + @document.push(new CloseTag(name)) + + + elementIsVoid: (name) -> + _.contains(this.constructor.elements.void, name) extractOptions: (args) -> options = {} @@ -31,12 +52,6 @@ class Builder options.attributes = arg if _.isObject(arg) options - openTag: (name, attributes) -> - @document.push(new OpenTag(name, attributes)) - - closeTag: (name) -> - @document.push(new CloseTag(name)) - text: (string) -> @document.push(new Text(string)) From a7aa1d2b759fccb1768cf81a6a7e6e8256739d8d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 17:42:29 -0600 Subject: [PATCH 06/15] Add jasmine jquery matchers. --- spec/spec-helper.coffee | 1 + vendor/jasmine-jquery.js | 177 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 vendor/jasmine-jquery.js diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index f9aceeab6..94204773f 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -1,3 +1,4 @@ +nakedLoad 'jasmine-jquery' $ = require 'jquery' _ = require 'underscore' Native = require 'native' diff --git a/vendor/jasmine-jquery.js b/vendor/jasmine-jquery.js new file mode 100644 index 000000000..e1cafb895 --- /dev/null +++ b/vendor/jasmine-jquery.js @@ -0,0 +1,177 @@ +jQuery = require('jquery'); +jasmine.JQuery = function() {}; + +jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { + return jQuery('
      ').append(html).html(); +}; + +jasmine.JQuery.elementToString = function(element) { + return jQuery('
      ').append(element.clone()).html(); +}; + +jasmine.JQuery.matchersClass = {}; + +(function(namespace) { + var data = { + spiedEvents: {}, + handlers: [] + }; + + namespace.events = { + spyOn: function(selector, eventName) { + var handler = function(e) { + data.spiedEvents[[selector, eventName]] = e; + }; + jQuery(selector).bind(eventName, handler); + data.handlers.push(handler); + }, + + wasTriggered: function(selector, eventName) { + return !!(data.spiedEvents[[selector, eventName]]); + }, + + cleanUp: function() { + data.spiedEvents = {}; + data.handlers = []; + } + } +})(jasmine.JQuery); + +(function(){ + var jQueryMatchers = { + toHaveClass: function(className) { + return this.actual.hasClass(className); + }, + + toBeVisible: function() { + return this.actual.is(':visible'); + }, + + toBeHidden: function() { + return this.actual.is(':hidden'); + }, + + toBeSelected: function() { + return this.actual.is(':selected'); + }, + + toBeChecked: function() { + return this.actual.is(':checked'); + }, + + toBeEmpty: function() { + return this.actual.is(':empty'); + }, + + toExist: function() { + return this.actual.size() > 0; + }, + + toHaveAttr: function(attributeName, expectedAttributeValue) { + return hasProperty(this.actual.attr(attributeName), expectedAttributeValue); + }, + + toHaveId: function(id) { + return this.actual.attr('id') == id; + }, + + toHaveHtml: function(html) { + return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html); + }, + + toHaveText: function(text) { + if (text && jQuery.isFunction(text.test)) { + return text.test(this.actual.text()); + } else { + return this.actual.text() == text; + } + }, + + toHaveValue: function(value) { + return this.actual.val() == value; + }, + + toHaveData: function(key, expectedValue) { + return hasProperty(this.actual.data(key), expectedValue); + }, + + toMatchSelector: function(selector) { + return this.actual.is(selector); + }, + + toContain: function(selector) { + return this.actual.find(selector).size() > 0; + }, + + toBeDisabled: function(selector){ + return this.actual.is(':disabled'); + }, + + // tests the existence of a specific event binding + toHandle: function(eventName) { + var events = this.actual.data("events"); + return events && events[eventName].length > 0; + }, + + // tests the existence of a specific event binding + handler + toHandleWith: function(eventName, eventHandler) { + var stack = this.actual.data("events")[eventName]; + var i; + for (i = 0; i < stack.length; i++) { + if (stack[i].handler == eventHandler) { + return true; + } + } + return false; + } + }; + + var hasProperty = function(actualValue, expectedValue) { + if (expectedValue === undefined) { + return actualValue !== undefined; + } + return actualValue == expectedValue; + }; + + var bindMatcher = function(methodName) { + var builtInMatcher = jasmine.Matchers.prototype[methodName]; + + jasmine.JQuery.matchersClass[methodName] = function() { + if (this.actual instanceof jQuery) { + var result = jQueryMatchers[methodName].apply(this, arguments); + this.actual = jasmine.JQuery.elementToString(this.actual); + return result; + } + + if (builtInMatcher) { + return builtInMatcher.apply(this, arguments); + } + + return false; + }; + }; + + for(var methodName in jQueryMatchers) { + bindMatcher(methodName); + } +})(); + +beforeEach(function() { + this.addMatchers(jasmine.JQuery.matchersClass); + this.addMatchers({ + toHaveBeenTriggeredOn: function(selector) { + this.message = function() { + return [ + "Expected event " + this.actual + " to have been triggered on" + selector, + "Expected event " + this.actual + " not to have been triggered on" + selector + ]; + }; + return jasmine.JQuery.events.wasTriggered(selector, this.actual); + } + }) +}); + +afterEach(function() { + jasmine.JQuery.events.cleanUp(); +}); + From 43ddb6b16a6e5f552e362a72907ab0aeee7038f2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 17:43:05 -0600 Subject: [PATCH 07/15] Builder.toFragment creates outlet references on the fragment. --- spec/stdlib/template/builder-spec.coffee | 10 ++++++++++ src/stdlib/template/builder.coffee | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index deb3af743..07ed5e2e7 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -5,6 +5,16 @@ fdescribe "Builder", -> beforeEach -> builder = new Builder + describe ".toFragment()", -> + it "creates outlet references on the fragment for elements with an outlet", -> + builder.tag 'div', -> + builder.tag 'div', id: 'foo', outlet: 'a' + builder.tag 'div', id: 'bar', outlet: 'b' + + fragment = builder.toFragment() + expect(fragment.a).toMatchSelector '#foo' + expect(fragment.b).toMatchSelector '#bar' + describe ".tag(name, args...)", -> it "can generate simple tags", -> builder.tag 'div' diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee index 819842bc7..304009db1 100644 --- a/src/stdlib/template/builder.coffee +++ b/src/stdlib/template/builder.coffee @@ -25,7 +25,12 @@ class Builder _.map(@document, (x) -> x.toHtml()).join('') toFragment: -> - $(@toHtml()) + fragment = $(@toHtml()) + fragment.find('[outlet]').each -> + elt = $(this) + outletName = elt.attr('outlet') + fragment[outletName] = elt + fragment tag: (name, args...) -> options = @extractOptions(args) From 08c1d33836bc90127e655ab41e4ddbc2f1b59529 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 18:21:00 -0600 Subject: [PATCH 08/15] Switch Template to use builder. Wire outlets in Template. --- spec/stdlib/template-spec.coffee | 29 ++++++++++++++++++------ spec/stdlib/template/builder-spec.coffee | 10 -------- src/stdlib/template.coffee | 27 ++++++++++++++++++---- src/stdlib/template/builder.coffee | 7 +----- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index 1a139ffd0..299a0aa4f 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -1,20 +1,35 @@ Template = require 'template' -describe "Template", -> +fdescribe "Template", -> describe "toView", -> Foo = null + view = null beforeEach -> class Foo extends Template - content: -> - div -> - h1 @title + content: (attrs) -> + @div => + @h1 attrs.title + @list() + + list: -> + @ol => + @li outlet: 'li1', class: 'foo', "one" + @li outlet: 'li2', class: 'bar', "two" + + view = Foo.build(title: "Zebra") afterEach -> delete window.Foo - it "builds a jquery object based on the content method and extends it with the viewProperties", -> - view = Foo.buildView(title: "Hello World") - expect(view.find('h1').text()).toEqual "Hello World" + describe ".build(attributes)", -> + it "generates markup based on the content method", -> + expect(view).toMatchSelector "div" + expect(view.find("h1:contains(Zebra)")).toExist() + expect(view.find("ol > li.foo:contains(one)")).toExist() + expect(view.find("ol > li.bar:contains(two)")).toExist() + it "wires references for elements with 'outlet' attributes", -> + expect(view.li1).toMatchSelector("li.foo:contains(one)") + expect(view.li2).toMatchSelector("li.bar:contains(two)") diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index 07ed5e2e7..deb3af743 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -5,16 +5,6 @@ fdescribe "Builder", -> beforeEach -> builder = new Builder - describe ".toFragment()", -> - it "creates outlet references on the fragment for elements with an outlet", -> - builder.tag 'div', -> - builder.tag 'div', id: 'foo', outlet: 'a' - builder.tag 'div', id: 'bar', outlet: 'b' - - fragment = builder.toFragment() - expect(fragment.a).toMatchSelector '#foo' - expect(fragment.b).toMatchSelector '#bar' - describe ".tag(name, args...)", -> it "can generate simple tags", -> builder.tag 'div' diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee index 35f358e53..49c3d3197 100644 --- a/src/stdlib/template.coffee +++ b/src/stdlib/template.coffee @@ -1,10 +1,27 @@ $ = require 'jquery' -coffeekup = require 'coffeekup' +_ = require 'underscore' +Builder = require 'template/builder' module.exports = class Template - @buildView: (attributes) -> - (new this).buildView(attributes) + @buildTagMethod: (name) -> + this.prototype[name] = (args...) -> @builder.tag(name, args...) - buildView: (attributes) -> - $(coffeekup.render(@content, attributes)) + _.each(Builder.elements.normal, (name) => @buildTagMethod(name)) + _.each(Builder.elements.void, (name) => @buildTagMethod(name)) + + @build: (attributes) -> + (new this).build(attributes) + + build: (attributes) -> + @builder = new Builder + @content(attributes) + view = @builder.toFragment() + @wireOutlets(view) + view + + wireOutlets: (view) -> + view.find('[outlet]').each -> + elt = $(this) + outletName = elt.attr('outlet') + view[outletName] = elt diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee index 304009db1..819842bc7 100644 --- a/src/stdlib/template/builder.coffee +++ b/src/stdlib/template/builder.coffee @@ -25,12 +25,7 @@ class Builder _.map(@document, (x) -> x.toHtml()).join('') toFragment: -> - fragment = $(@toHtml()) - fragment.find('[outlet]').each -> - elt = $(this) - outletName = elt.attr('outlet') - fragment[outletName] = elt - fragment + $(@toHtml()) tag: (name, args...) -> options = @extractOptions(args) From 7948543a5b74f480d7a29da0948232d413da13ff Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 18:36:54 -0600 Subject: [PATCH 09/15] Template extends view with @viewProperties and calls initialize with attributes passed to build. --- spec/stdlib/template-spec.coffee | 10 ++++++++++ src/stdlib/template.coffee | 3 +++ 2 files changed, 13 insertions(+) diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index 299a0aa4f..184fd0cae 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -17,6 +17,11 @@ fdescribe "Template", -> @li outlet: 'li1', class: 'foo', "one" @li outlet: 'li2', class: 'bar', "two" + viewProperties: + initialize: (attrs) -> + @initializeCalledWith = attrs + foo: "bar" + view = Foo.build(title: "Zebra") afterEach -> @@ -29,6 +34,11 @@ fdescribe "Template", -> expect(view.find("ol > li.foo:contains(one)")).toExist() expect(view.find("ol > li.bar:contains(two)")).toExist() + it "extends the view with viewProperties, calling the 'constructor' property if present", -> + expect(view.constructor).toBeDefined() + expect(view.foo).toBe("bar") + expect(view.initializeCalledWith).toEqual(title: "Zebra") + it "wires references for elements with 'outlet' attributes", -> expect(view.li1).toMatchSelector("li.foo:contains(one)") expect(view.li2).toMatchSelector("li.bar:contains(two)") diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee index 49c3d3197..cb4f7a31f 100644 --- a/src/stdlib/template.coffee +++ b/src/stdlib/template.coffee @@ -18,6 +18,9 @@ class Template @content(attributes) view = @builder.toFragment() @wireOutlets(view) + if @viewProperties + $.extend(view, @viewProperties) + view.initialize?(attributes) view wireOutlets: (view) -> From ba18614c2f7bc7ad98f2025b586d0637a997aedc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 19:00:00 -0600 Subject: [PATCH 10/15] Bind DOM events to view methods based on element attributes. For example, if you give an element the attribute click: 'elementClicked', the template will bind a click event to that element which calls the elementClicked method on the view. --- spec/stdlib/template-spec.coffee | 21 ++++++++++++++++++--- src/stdlib/template.coffee | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index 184fd0cae..e8d409ccb 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -14,13 +14,15 @@ fdescribe "Template", -> list: -> @ol => - @li outlet: 'li1', class: 'foo', "one" - @li outlet: 'li2', class: 'bar', "two" + @li outlet: 'li1', click: 'li1Clicked', class: 'foo', "one" + @li outlet: 'li2', keypress:'li2Keypressed', class: 'bar', "two" viewProperties: initialize: (attrs) -> @initializeCalledWith = attrs - foo: "bar" + foo: "bar", + li1Clicked: ->, + li2Keypressed: -> view = Foo.build(title: "Zebra") @@ -43,3 +45,16 @@ fdescribe "Template", -> expect(view.li1).toMatchSelector("li.foo:contains(one)") expect(view.li2).toMatchSelector("li.bar:contains(two)") + it "binds events for elements with event name attributes", -> + spyOn(view, 'li1Clicked') + spyOn(view, 'li2Keypressed') + view.li1.click() + expect(view.li1Clicked).toHaveBeenCalled() + expect(view.li2Keypressed).not.toHaveBeenCalled() + + view.li1Clicked.reset() + + view.li2.keypress() + expect(view.li2Keypressed).toHaveBeenCalled() + expect(view.li1Clicked).not.toHaveBeenCalled() + diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee index cb4f7a31f..43c723cf4 100644 --- a/src/stdlib/template.coffee +++ b/src/stdlib/template.coffee @@ -4,11 +4,15 @@ Builder = require 'template/builder' module.exports = class Template + @events: 'blur change click dblclick error focus keydown + keypress keyup load mousedown mousemove mouseout mouseover + mouseup resize scroll select submit unload'.split /\s+/ + @buildTagMethod: (name) -> this.prototype[name] = (args...) -> @builder.tag(name, args...) - _.each(Builder.elements.normal, (name) => @buildTagMethod(name)) - _.each(Builder.elements.void, (name) => @buildTagMethod(name)) + @buildTagMethod(name) for name in Builder.elements.normal + @buildTagMethod(name) for name in Builder.elements.void @build: (attributes) -> (new this).build(attributes) @@ -18,6 +22,7 @@ class Template @content(attributes) view = @builder.toFragment() @wireOutlets(view) + @bindEvents(view) if @viewProperties $.extend(view, @viewProperties) view.initialize?(attributes) @@ -28,3 +33,13 @@ class Template elt = $(this) outletName = elt.attr('outlet') view[outletName] = elt + + bindEvents: (view) -> + for event in this.constructor.events + view.find("[#{event}]").each -> + elt = $(this) + methodName = elt.attr(event) + elt[event](-> view[methodName]()) + + + From eeb20673b3c68003df5d3b0d409a288d72edd14b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 19:13:54 -0600 Subject: [PATCH 11/15] Pass the event and element to event-handling methods on the view. --- spec/stdlib/template-spec.coffee | 8 ++++++++ src/stdlib/template.coffee | 10 ++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index e8d409ccb..b33a59cac 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -48,6 +48,14 @@ fdescribe "Template", -> it "binds events for elements with event name attributes", -> spyOn(view, 'li1Clicked') spyOn(view, 'li2Keypressed') + spyOn(view, 'li1Clicked').andCallFake (event, elt) -> + expect(event.type).toBe 'click' + expect(elt).toMatchSelector 'li.foo:contains(one)' + + spyOn(view, 'li2Keypressed').andCallFake (event, elt) -> + expect(event.type).toBe 'keypress' + expect(elt).toMatchSelector "li.bar:contains(two)" + view.li1.click() expect(view.li1Clicked).toHaveBeenCalled() expect(view.li2Keypressed).not.toHaveBeenCalled() diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee index 43c723cf4..d4585a360 100644 --- a/src/stdlib/template.coffee +++ b/src/stdlib/template.coffee @@ -35,11 +35,9 @@ class Template view[outletName] = elt bindEvents: (view) -> - for event in this.constructor.events - view.find("[#{event}]").each -> + for eventName in this.constructor.events + view.find("[#{eventName}]").each -> elt = $(this) - methodName = elt.attr(event) - elt[event](-> view[methodName]()) - - + methodName = elt.attr(eventName) + elt[eventName]((event) -> view[methodName](event, elt)) From a622da2904b013586525f123bbbfd1e7f3c1a370 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Dec 2011 19:14:29 -0600 Subject: [PATCH 12/15] :lipstick: --- spec/stdlib/template-spec.coffee | 8 +++----- spec/stdlib/template/builder-spec.coffee | 16 ++++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index b33a59cac..3157ae0e0 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -39,15 +39,13 @@ fdescribe "Template", -> it "extends the view with viewProperties, calling the 'constructor' property if present", -> expect(view.constructor).toBeDefined() expect(view.foo).toBe("bar") - expect(view.initializeCalledWith).toEqual(title: "Zebra") + expect(view.initializeCalledWith).toEqual title: "Zebra" it "wires references for elements with 'outlet' attributes", -> - expect(view.li1).toMatchSelector("li.foo:contains(one)") - expect(view.li2).toMatchSelector("li.bar:contains(two)") + expect(view.li1).toMatchSelector "li.foo:contains(one)" + expect(view.li2).toMatchSelector "li.bar:contains(two)" it "binds events for elements with event name attributes", -> - spyOn(view, 'li1Clicked') - spyOn(view, 'li2Keypressed') spyOn(view, 'li1Clicked').andCallFake (event, elt) -> expect(event.type).toBe 'click' expect(elt).toMatchSelector 'li.foo:contains(one)' diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index deb3af743..24ce2779a 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -8,34 +8,34 @@ fdescribe "Builder", -> describe ".tag(name, args...)", -> it "can generate simple tags", -> builder.tag 'div' - expect(builder.toHtml()).toBe("
      ") + expect(builder.toHtml()).toBe "
      " builder.reset() builder.tag 'ol' - expect(builder.toHtml()).toBe("
        ") + expect(builder.toHtml()).toBe "
          " it "can generate tags with content", -> builder.tag 'ol', -> builder.tag 'li' builder.tag 'li' - expect(builder.toHtml()).toBe("
          ") + expect(builder.toHtml()).toBe "
          " it "can generate tags with text", -> builder.tag 'div', "hello" - expect(builder.toHtml()).toBe("
          hello
          ") + expect(builder.toHtml()).toBe "
          hello
          " builder.reset() builder.tag 'div', 22 - expect(builder.toHtml()).toBe("
          22
          ") + expect(builder.toHtml()).toBe "
          22
          " it "can generate tags with attributes", -> builder.tag 'div', id: 'foo', class: 'bar' fragment = builder.toFragment() - expect(fragment.attr('id')).toBe('foo') - expect(fragment.attr('class')).toBe('bar') + expect(fragment.attr('id')).toBe 'foo' + expect(fragment.attr('class')).toBe 'bar' it "can generate self-closing tags", -> builder.tag 'br', id: 'foo' - expect(builder.toHtml()).toBe('
          ') + expect(builder.toHtml()).toBe '
          ' From 28ef202a6a5be9f097b4071918d8e6d52a8217c8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Dec 2011 12:47:04 -0600 Subject: [PATCH 13/15] Un-focus and fix Layout template. --- spec/stdlib/template-spec.coffee | 2 +- spec/stdlib/template/builder-spec.coffee | 2 +- src/atom/layout.coffee | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index 3157ae0e0..c4d9e4824 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -1,6 +1,6 @@ Template = require 'template' -fdescribe "Template", -> +describe "Template", -> describe "toView", -> Foo = null view = null diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee index 24ce2779a..ea055a5f7 100644 --- a/spec/stdlib/template/builder-spec.coffee +++ b/spec/stdlib/template/builder-spec.coffee @@ -1,6 +1,6 @@ Builder = require 'template/builder' -fdescribe "Builder", -> +describe "Builder", -> builder = null beforeEach -> builder = new Builder diff --git a/src/atom/layout.coffee b/src/atom/layout.coffee index b59fa7dd5..2496aa567 100644 --- a/src/atom/layout.coffee +++ b/src/atom/layout.coffee @@ -4,13 +4,13 @@ Template = require 'template' module.exports = class Layout extends Template @attach: -> - view = @buildView() + view = @build() $('body').append(view) view content: -> - link rel: 'stylesheet', href: 'static/atom.css' - div id: 'app-horizontal', -> - div id: 'app-vertical', -> - div id: 'main' + @link rel: 'stylesheet', href: 'static/atom.css' + @div id: 'app-horizontal', => + @div id: 'app-vertical', => + @div id: 'main' From 51c89acc5b0dfb5c17297c129f4ef9e5bc7a8cb5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Dec 2011 13:53:39 -0600 Subject: [PATCH 14/15] Add FileFinder with a findMatches method that returns ranked urls. Change stringscore.js to be a function that takes a string, rather than extending the prototype. --- spec/atom/file-finder-spec.coffee | 14 ++++++++++++++ src/atom/file-finder.coffee | 17 +++++++++++++++++ vendor/stringscore.js | 6 ++++-- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 spec/atom/file-finder-spec.coffee create mode 100644 src/atom/file-finder.coffee diff --git a/spec/atom/file-finder-spec.coffee b/spec/atom/file-finder-spec.coffee new file mode 100644 index 000000000..4a783a076 --- /dev/null +++ b/spec/atom/file-finder-spec.coffee @@ -0,0 +1,14 @@ +FileFinder = require 'file-finder' + +fdescribe 'FileFinder', -> + finder = null + + beforeEach -> + urls = ['app.coffee', 'buffer.coffee', 'atom/app.coffee', 'atom/buffer.coffee'] + finder = FileFinder.build {urls} + + describe 'findMatches(queryString)', -> + it "returns urls sorted by score of match against the given query", -> + expect(finder.findMatches('ap')).toEqual ["app.coffee", "atom/app.coffee"] + expect(finder.findMatches('a/ap')).toEqual ["atom/app.coffee"] + diff --git a/src/atom/file-finder.coffee b/src/atom/file-finder.coffee new file mode 100644 index 000000000..daa348db4 --- /dev/null +++ b/src/atom/file-finder.coffee @@ -0,0 +1,17 @@ +Template = require 'template' +stringScore = require 'stringscore' + +module.exports = +class FileFinder extends Template + content: -> @div + + viewProperties: + urls: null + + initialize: ({@urls}) -> + + findMatches: (query) -> + scoredUrls = ({url, score: stringScore(url, query)} for url in @urls) + sortedUrls = scoredUrls.sort (a, b) -> a.score > b.score + urlAndScore.url for urlAndScore in sortedUrls when urlAndScore.score > 0 + diff --git a/vendor/stringscore.js b/vendor/stringscore.js index a734adc9f..7e758b002 100644 --- a/vendor/stringscore.js +++ b/vendor/stringscore.js @@ -1,3 +1,5 @@ +// MODIFIED BY NS/CJ - Don't extend the prototype of String + /*! * string_score.js: String Scoring Algorithm 0.1.9 * @@ -16,10 +18,10 @@ * 'Hello World'.score('he'); //=> 0.5931818181818181 * 'Hello World'.score('Hello'); //=> 0.7318181818181818 */ -String.prototype.score = function(abbreviation, fuzziness) { + +module.exports = function(string, abbreviation, fuzziness) { var total_character_score = 0, abbreviation_length = abbreviation.length, - string = this, string_length = string.length, start_of_string_bonus, abbreviation_score, From 886c435b43994c5871ef43e2cf1a11444b0be5dc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Dec 2011 14:21:43 -0600 Subject: [PATCH 15/15] Populate url list of file finder as user types. --- spec/atom/file-finder-spec.coffee | 22 ++++++++++++++++++++-- src/atom/file-finder.coffee | 11 ++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spec/atom/file-finder-spec.coffee b/spec/atom/file-finder-spec.coffee index 4a783a076..d83242991 100644 --- a/spec/atom/file-finder-spec.coffee +++ b/spec/atom/file-finder-spec.coffee @@ -1,13 +1,31 @@ FileFinder = require 'file-finder' -fdescribe 'FileFinder', -> +describe 'FileFinder', -> finder = null beforeEach -> urls = ['app.coffee', 'buffer.coffee', 'atom/app.coffee', 'atom/buffer.coffee'] finder = FileFinder.build {urls} - describe 'findMatches(queryString)', -> + describe "when characters are typed into the input element", -> + it "displays matching urls in the ol element", -> + expect(finder.urlList.find('li')).not.toExist() + + finder.input.text('ap') + finder.input.keypress() + + expect(finder.urlList.children().length).toBe 2 + expect(finder.urlList.find('li:contains(app.coffee)').length).toBe 2 + expect(finder.urlList.find('li:contains(atom/app.coffee)').length).toBe 1 + + # we should clear the list before re-populating it + finder.input.text('a/ap') + finder.input.keypress() + + expect(finder.urlList.children().length).toBe 1 + expect(finder.urlList.find('li:contains(atom/app.coffee)').length).toBe 1 + + describe "findMatches(queryString)", -> it "returns urls sorted by score of match against the given query", -> expect(finder.findMatches('ap')).toEqual ["app.coffee", "atom/app.coffee"] expect(finder.findMatches('a/ap')).toEqual ["atom/app.coffee"] diff --git a/src/atom/file-finder.coffee b/src/atom/file-finder.coffee index daa348db4..e615017e0 100644 --- a/src/atom/file-finder.coffee +++ b/src/atom/file-finder.coffee @@ -1,15 +1,24 @@ +$ = require 'jquery' Template = require 'template' stringScore = require 'stringscore' module.exports = class FileFinder extends Template - content: -> @div + content: -> + @div class: 'file-finder', => + @ol outlet: 'urlList' + @input outlet: 'input', keypress: 'populateUrlList' viewProperties: urls: null initialize: ({@urls}) -> + populateUrlList: -> + @urlList.empty() + for url in @findMatches(@input.text()) + @urlList.append $("
        1. #{url}
        2. ") + findMatches: (query) -> scoredUrls = ({url, score: stringScore(url, query)} for url in @urls) sortedUrls = scoredUrls.sort (a, b) -> a.score > b.score