modularity-rails 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,13 +1,15 @@
1
- *.gem
2
- *.rbc
3
1
  .bundle
4
2
  .config
3
+ .DS_Store
5
4
  .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
5
+ *.gem
6
+ *.rbc
7
+ *.swp
8
+ *.swo
9
9
  coverage
10
10
  doc/
11
+ Gemfile.lock
12
+ InstalledFiles
11
13
  lib/bundler/man
12
14
  pkg
13
15
  rdoc
@@ -15,3 +17,4 @@ spec/reports
15
17
  test/tmp
16
18
  test/version_tmp
17
19
  tmp
20
+ _yardoc
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'livereload' do
5
+ watch(%r{spec/javascripts/.+})
6
+ watch(%r{vendor/assets/javascripts/.+})
7
+ # Rails Assets Pipeline
8
+ # watch(%r{(app|vendor)/assets/\w+/(.+\.(css|js|html)).*}) { |m| "/assets/#{m[2]}" }
9
+ end
data/README.md CHANGED
@@ -3,8 +3,12 @@
3
3
  Makes the [Modularity CoffeeScript](http://github.com/kevgo/modularity-coffeescript) library available to
4
4
  Rails 3.1 applications.
5
5
 
6
+ Modularity is a pattern and framework for lightweight object-oriented JavaScript
7
+ that allows to compose functionally rich web pages in a clean and testable way
8
+ out of well structured and reusable components.
6
9
 
7
- ## Installation
10
+
11
+ # Installation
8
12
 
9
13
  Add this line to your application's Gemfile:
10
14
 
@@ -19,25 +23,39 @@ $ bundle
19
23
  ```
20
24
 
21
25
  Finally, you have to load the modularity file into your application's javascript.
22
- The easiest way is to add it to `application.js`:
26
+ The easiest way is to add it to `application.coffee`:
23
27
 
24
- ```javascript
25
- /**
26
- *= require jquery
27
- *= require modularity
28
- */
28
+ ```coffeescript
29
+ # require jquery
30
+ # require modularity
29
31
  ```
30
32
 
31
33
 
32
- ## Usage
34
+ # Usage
35
+
36
+ Modularity is a lightweight framework for building powerful AJAX applications.
37
+ Modularity avoids magic and heavyness. It focusses on providing a pragmatic and interoperable foundation
38
+ for clean hand-written code bases.
39
+ Modularity provides practices to create code bases of incredible complexity that are still
40
+ nicely manageable and perform very well.
41
+
42
+
43
+ # Modules
44
+
45
+ Modules are native CoffeeScript classes that are specialized for doing what most JavaScript running in browsers does:
46
+ managing a UI consisting of DOM elements, reacting to events that happen within that section,
47
+ representing application logic specific to that section, and providing high-level APIs for others to interact with the section.
48
+
49
+ Each module has a container. The container is the outermost DOM element of a section.
50
+ Everything the module does must happen inside this container.
51
+ The module is responsible for managing the inner DOM-structure of the container.
33
52
 
34
- See [http://github.com/kevgo/modularity-coffeescript].
35
53
 
36
54
  ## Mixins
37
55
 
38
56
  Similar to Ruby mixins, mixins in Modularity allow to include orthogonal functional aspects defined in separate objects into a class.
39
- ```coffeescript
40
57
 
58
+ ```coffeescript
41
59
  myMixin =
42
60
 
43
61
  # This will be called when an instance of a class that includes this mixin is created.
@@ -57,10 +75,28 @@ class MyModule extends Module
57
75
  super
58
76
 
59
77
  # ...
60
-
61
78
  ```
62
79
 
63
80
 
81
+ ## Reusable example modules and mixins.
82
+
83
+ Modularity comes bundled with a bunch of example modules and mixins that can be used in production code.
84
+ The example modules are located in `vendor/assets/javascripts/modules/` and _vendor/assets/javascripts/mixins_
85
+ and must be explicitly required in your Rails files using the `require` commands of the asset pipeline.
86
+
87
+
88
+ ### Modules
89
+
90
+ * __button.coffee__: A simple button. Fires the `clicked` event when anything inside the container is clicked. Uses the `clickable` mixin.
91
+ * __counter_button.coffee__: Similar to button, but includes the click count as data payload in the fired event.
92
+
93
+
94
+ ### Mixins
95
+
96
+ * __clickable.coffee__: Including this mixins adds a 'clickable' aspect to your module, i.e. turns it into a button. Clicking anywhere inside the container makes it fire the 'clicked' event.
97
+ * __closable.coffee__: Including this mixin makes a module closable. This means that when the user clicks on an embedded DOM element with the class 'CloseButton', a 'closing' event will be fired, and the whole module will be removed from the DOM.
98
+
99
+
64
100
  # Development
65
101
 
66
102
  ## Contributing
@@ -77,3 +113,16 @@ class MyModule extends Module
77
113
  ```bash
78
114
  $ evergreen run
79
115
  ```
116
+
117
+
118
+ ## Automatically refreshing the browser during development.
119
+
120
+ Modularity-Rails comes with support for [LifeReload](https://github.com/mockko/livereload) via [Guard](https://github.com/guard/guard).
121
+
122
+ * Install the LiveReload browser extension: [Chrome](https://chrome.google.com/webstore/detail/jnihajbhpnppcggbcgedagnkighmdlei)
123
+ * Run the evergreen server: ```$ evergreen run```
124
+ * Run the guard server: ``` $ bundle exec guard ```
125
+ * Start the LiveReload plugin in Chrome (button in address bar).
126
+ * Navigate to the test page that you want to observe.
127
+ * Change and save code and see the browser reload.
128
+
@@ -1,3 +1,3 @@
1
1
  module ModularityRails
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.1"
3
3
  end
@@ -13,6 +13,8 @@ Gem::Specification.new do |s|
13
13
  s.add_dependency "rails", ">= 3.1.0"
14
14
  s.add_development_dependency "capybara-webkit"
15
15
  s.add_development_dependency "evergreen"
16
+ s.add_development_dependency "rb-fsevent" if RUBY_PLATFORM =~ /darwin/i
17
+ s.add_development_dependency "guard-livereload"
16
18
 
17
19
  s.files = `git ls-files`.split("\n")
18
20
  s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
@@ -0,0 +1,288 @@
1
+ var readFixtures = function() {
2
+ return jasmine.getFixtures().proxyCallTo_('read', arguments);
3
+ };
4
+
5
+ var preloadFixtures = function() {
6
+ jasmine.getFixtures().proxyCallTo_('preload', arguments);
7
+ };
8
+
9
+ var loadFixtures = function() {
10
+ jasmine.getFixtures().proxyCallTo_('load', arguments);
11
+ };
12
+
13
+ var setFixtures = function(html) {
14
+ jasmine.getFixtures().set(html);
15
+ };
16
+
17
+ var sandbox = function(attributes) {
18
+ return jasmine.getFixtures().sandbox(attributes);
19
+ };
20
+
21
+ var spyOnEvent = function(selector, eventName) {
22
+ jasmine.JQuery.events.spyOn(selector, eventName);
23
+ }
24
+
25
+ jasmine.getFixtures = function() {
26
+ return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures();
27
+ };
28
+
29
+ jasmine.Fixtures = function() {
30
+ this.containerId = 'jasmine-fixtures';
31
+ this.fixturesCache_ = {};
32
+ this.fixturesPath = 'spec/javascripts/fixtures';
33
+ };
34
+
35
+ jasmine.Fixtures.prototype.set = function(html) {
36
+ this.cleanUp();
37
+ this.createContainer_(html);
38
+ };
39
+
40
+ jasmine.Fixtures.prototype.preload = function() {
41
+ this.read.apply(this, arguments);
42
+ };
43
+
44
+ jasmine.Fixtures.prototype.load = function() {
45
+ this.cleanUp();
46
+ this.createContainer_(this.read.apply(this, arguments));
47
+ };
48
+
49
+ jasmine.Fixtures.prototype.read = function() {
50
+ var htmlChunks = [];
51
+
52
+ var fixtureUrls = arguments;
53
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
54
+ htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]));
55
+ }
56
+
57
+ return htmlChunks.join('');
58
+ };
59
+
60
+ jasmine.Fixtures.prototype.clearCache = function() {
61
+ this.fixturesCache_ = {};
62
+ };
63
+
64
+ jasmine.Fixtures.prototype.cleanUp = function() {
65
+ jQuery('#' + this.containerId).remove();
66
+ };
67
+
68
+ jasmine.Fixtures.prototype.sandbox = function(attributes) {
69
+ var attributesToSet = attributes || {};
70
+ return jQuery('<div id="sandbox" />').attr(attributesToSet);
71
+ };
72
+
73
+ jasmine.Fixtures.prototype.createContainer_ = function(html) {
74
+ var container;
75
+ if(html instanceof jQuery) {
76
+ container = jQuery('<div id="' + this.containerId + '" />');
77
+ container.html(html);
78
+ } else {
79
+ container = '<div id="' + this.containerId + '">' + html + '</div>'
80
+ }
81
+ jQuery('body').append(container);
82
+ };
83
+
84
+ jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
85
+ if (typeof this.fixturesCache_[url] == 'undefined') {
86
+ this.loadFixtureIntoCache_(url);
87
+ }
88
+ return this.fixturesCache_[url];
89
+ };
90
+
91
+ jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
92
+ var self = this;
93
+ var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl;
94
+ jQuery.ajax({
95
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
96
+ cache: false,
97
+ dataType: 'html',
98
+ url: url,
99
+ success: function(data) {
100
+ self.fixturesCache_[relativeUrl] = data;
101
+ },
102
+ error: function(jqXHR, status, errorThrown) {
103
+ throw Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')');
104
+ }
105
+ });
106
+ };
107
+
108
+ jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
109
+ return this[methodName].apply(this, passedArguments);
110
+ };
111
+
112
+
113
+ jasmine.JQuery = function() {};
114
+
115
+ jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
116
+ return jQuery('<div/>').append(html).html();
117
+ };
118
+
119
+ jasmine.JQuery.elementToString = function(element) {
120
+ return jQuery('<div />').append(element.clone()).html();
121
+ };
122
+
123
+ jasmine.JQuery.matchersClass = {};
124
+
125
+ (function(namespace) {
126
+ var data = {
127
+ spiedEvents: {},
128
+ handlers: []
129
+ };
130
+
131
+ namespace.events = {
132
+ spyOn: function(selector, eventName) {
133
+ var handler = function(e) {
134
+ data.spiedEvents[[selector, eventName]] = e;
135
+ };
136
+ jQuery(selector).bind(eventName, handler);
137
+ data.handlers.push(handler);
138
+ },
139
+
140
+ wasTriggered: function(selector, eventName) {
141
+ return !!(data.spiedEvents[[selector, eventName]]);
142
+ },
143
+
144
+ cleanUp: function() {
145
+ data.spiedEvents = {};
146
+ data.handlers = [];
147
+ }
148
+ }
149
+ })(jasmine.JQuery);
150
+
151
+ (function(){
152
+ var jQueryMatchers = {
153
+ toHaveClass: function(className) {
154
+ return this.actual.hasClass(className);
155
+ },
156
+
157
+ toBeVisible: function() {
158
+ return this.actual.is(':visible');
159
+ },
160
+
161
+ toBeHidden: function() {
162
+ return this.actual.is(':hidden');
163
+ },
164
+
165
+ toBeSelected: function() {
166
+ return this.actual.is(':selected');
167
+ },
168
+
169
+ toBeChecked: function() {
170
+ return this.actual.is(':checked');
171
+ },
172
+
173
+ toBeEmpty: function() {
174
+ return this.actual.is(':empty');
175
+ },
176
+
177
+ toExist: function() {
178
+ return this.actual.size() > 0;
179
+ },
180
+
181
+ toHaveAttr: function(attributeName, expectedAttributeValue) {
182
+ return hasProperty(this.actual.attr(attributeName), expectedAttributeValue);
183
+ },
184
+
185
+ toHaveId: function(id) {
186
+ return this.actual.attr('id') == id;
187
+ },
188
+
189
+ toHaveHtml: function(html) {
190
+ return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html);
191
+ },
192
+
193
+ toHaveText: function(text) {
194
+ if (text && jQuery.isFunction(text.test)) {
195
+ return text.test(this.actual.text());
196
+ } else {
197
+ return this.actual.text() == text;
198
+ }
199
+ },
200
+
201
+ toHaveValue: function(value) {
202
+ return this.actual.val() == value;
203
+ },
204
+
205
+ toHaveData: function(key, expectedValue) {
206
+ return hasProperty(this.actual.data(key), expectedValue);
207
+ },
208
+
209
+ toBe: function(selector) {
210
+ return this.actual.is(selector);
211
+ },
212
+
213
+ toContain: function(selector) {
214
+ return this.actual.find(selector).size() > 0;
215
+ },
216
+
217
+ toBeDisabled: function(selector){
218
+ return this.actual.is(':disabled');
219
+ },
220
+
221
+ // tests the existence of a specific event binding
222
+ toHandle: function(eventName) {
223
+ var events = this.actual.data("events");
224
+ return events && events[eventName].length > 0;
225
+ },
226
+
227
+ // tests the existence of a specific event binding + handler
228
+ toHandleWith: function(eventName, eventHandler) {
229
+ var stack = this.actual.data("events")[eventName];
230
+ var i;
231
+ for (i = 0; i < stack.length; i++) {
232
+ if (stack[i].handler == eventHandler) {
233
+ return true;
234
+ }
235
+ }
236
+ return false;
237
+ }
238
+ };
239
+
240
+ var hasProperty = function(actualValue, expectedValue) {
241
+ if (expectedValue === undefined) {
242
+ return actualValue !== undefined;
243
+ }
244
+ return actualValue == expectedValue;
245
+ };
246
+
247
+ var bindMatcher = function(methodName) {
248
+ var builtInMatcher = jasmine.Matchers.prototype[methodName];
249
+
250
+ jasmine.JQuery.matchersClass[methodName] = function() {
251
+ if (this.actual instanceof jQuery) {
252
+ var result = jQueryMatchers[methodName].apply(this, arguments);
253
+ this.actual = jasmine.JQuery.elementToString(this.actual);
254
+ return result;
255
+ }
256
+
257
+ if (builtInMatcher) {
258
+ return builtInMatcher.apply(this, arguments);
259
+ }
260
+
261
+ return false;
262
+ };
263
+ };
264
+
265
+ for(var methodName in jQueryMatchers) {
266
+ bindMatcher(methodName);
267
+ }
268
+ })();
269
+
270
+ beforeEach(function() {
271
+ this.addMatchers(jasmine.JQuery.matchersClass);
272
+ this.addMatchers({
273
+ toHaveBeenTriggeredOn: function(selector) {
274
+ this.message = function() {
275
+ return [
276
+ "Expected event " + this.actual + " to have been triggered on" + selector,
277
+ "Expected event " + this.actual + " not to have been triggered on" + selector
278
+ ];
279
+ };
280
+ return jasmine.JQuery.events.wasTriggered(selector, this.actual);
281
+ }
282
+ })
283
+ });
284
+
285
+ afterEach(function() {
286
+ jasmine.getFixtures().cleanUp();
287
+ jasmine.JQuery.events.cleanUp();
288
+ });
@@ -0,0 +1,41 @@
1
+ #= require spec_helper
2
+
3
+ describe 'test environment setup', ->
4
+
5
+ it 'loading libraries', ->
6
+ load_modularity()
7
+ loadCS "/vendor/assets/javascripts/mixins/closable.coffee"
8
+
9
+ class window.ClosableModuleStandard extends Module
10
+ @mixin closable
11
+
12
+
13
+ describe 'Closable', ->
14
+ template 'closable.html'
15
+
16
+ beforeEach ->
17
+
18
+ describe 'embedded close button', ->
19
+ it 'finds the close button div automatically', ->
20
+ new ClosableModuleStandard('#test #closable1')
21
+ $('#test #closable1 .CloseButton').click()
22
+
23
+ expect($('#test #closable1').length).toEqual(0)
24
+
25
+
26
+ describe 'when clicking the close button', ->
27
+ it 'removes the container', ->
28
+ new ClosableModuleStandard('#test #closable1')
29
+ $('#test #closable1 .CloseButton').click()
30
+
31
+ expect($('#test #closable1').length).toEqual(0)
32
+
33
+
34
+ it "fires the 'closed' event", ->
35
+ closable_module = new ClosableModuleStandard('#test #closable1')
36
+ closable_module.bind_event('closed', (spy = jasmine.createSpy()))
37
+
38
+ $('#test #closable1 .CloseButton').click()
39
+
40
+ expect(spy).toHaveBeenCalled()
41
+ expect(spy.callCount).toEqual(1)
@@ -1,21 +1,12 @@
1
- require('/spec/javascripts/external/jquery.min.js')
2
- require('/spec/javascripts/external/coffee-script.js')
3
-
4
- # Helper method to circumvent that Evergreen doesn't load CoffeeScript files.
5
- loadCS = (url, callback) ->
6
- $.ajax
7
- url: url
8
- async: false
9
- success: (data) ->
10
- eval CoffeeScript.compile data
11
- callback() if callback
12
-
13
- # Helper method to load the modularity library before the tests.
14
- describe 'modularity loader', ->
15
- it 'loading Modularity library ...', ->
16
- loadCS '/vendor/assets/javascripts/modularity.js.coffee?'+(new Date()).getTime()
17
-
18
- # Test class.
1
+ #= require spec_helper
2
+
3
+ describe 'setting up test environment', ->
4
+
5
+ it 'loading libraries', ->
6
+ load_modularity()
7
+
8
+ it 'defining test classes', ->
9
+
19
10
  class window.TestModule extends Module
20
11
  constructor: (container) ->
21
12
  super
@@ -38,7 +29,7 @@ describe 'modularity', ->
38
29
  expect(alert).toHaveBeenCalled()
39
30
 
40
31
  it 'tries to load the DOM if the given container is a string', ->
41
- new TestModule('#module_container')
32
+ new TestModule('#test #module_container')
42
33
  expect(alert).not.toHaveBeenCalled()
43
34
 
44
35
  it 'shows an error if the container is not a jQuery object', ->
@@ -46,11 +37,11 @@ describe 'modularity', ->
46
37
  expect(alert).toHaveBeenCalled()
47
38
 
48
39
  it 'shows an error if the container is an empty jQuery object', ->
49
- new TestModule($('.zonk'))
40
+ new TestModule($('#test .zonk'))
50
41
  expect(alert).toHaveBeenCalled()
51
42
 
52
43
  it 'shows an error if the container has more than one elements', ->
53
- new TestModule($('.double'))
44
+ new TestModule($('#test .double'))
54
45
  expect(alert).toHaveBeenCalled()
55
46
 
56
47
  it "allows to provide 'testing' in tests", ->
@@ -100,11 +91,11 @@ describe 'modularity', ->
100
91
  spyOn window, 'alert'
101
92
 
102
93
  it 'works', ->
103
- $('#module_container').module(TestModule)
94
+ $('#test #module_container').module(TestModule)
104
95
  expect(alert).not.toHaveBeenCalled()
105
96
 
106
97
  it 'returns the created instance', ->
107
- result = $('#module_container').module(Module)
98
+ result = $('#test #module_container').module(Module)
108
99
  expect(result).toBeDefined()
109
100
  expect(typeof result).toEqual('object')
110
101
 
@@ -113,7 +104,7 @@ describe 'modularity', ->
113
104
  expect(alert).toHaveBeenCalled()
114
105
 
115
106
  it 'returns an error if the jQuery object is empty.', ->
116
- $('#zonk').module(Module)
107
+ $('#test #zonk').module(Module)
117
108
  expect(alert).toHaveBeenCalled()
118
109
 
119
110
 
@@ -124,7 +115,7 @@ describe 'modularity', ->
124
115
  mockContainer = null
125
116
 
126
117
  beforeEach ->
127
- mockContainer = $('#module_container')
118
+ mockContainer = $('#test #module_container')
128
119
  module = new TestModule(mockContainer)
129
120
  spyOn window, 'alert'
130
121
 
@@ -203,7 +194,7 @@ describe 'modularity', ->
203
194
  mockGlobalContainer = null
204
195
 
205
196
  beforeEach ->
206
- mockGlobalContainer = $('#module_container')
197
+ mockGlobalContainer = $('#test #module_container')
207
198
  spyOn(Module, 'global_event_container').andReturn(mockGlobalContainer)
208
199
 
209
200
  describe 'bind_global_event', ->
@@ -0,0 +1,45 @@
1
+ #= require spec_helper
2
+
3
+ describe 'test environment setup', ->
4
+
5
+ it 'loading libraries', ->
6
+ load_modularity()
7
+ loadCS "/vendor/assets/javascripts/mixins/clickable.coffee"
8
+ loadCS "/vendor/assets/javascripts/modules/button.coffee"
9
+
10
+
11
+ describe 'Button', ->
12
+ template 'button.html'
13
+
14
+ describe 'manual clicks', ->
15
+
16
+ it 'fires when clicking on the container directly', ->
17
+ button = new window.Button($('#test #button1'))
18
+ button.bind_event('clicked', (spy = jasmine.createSpy()))
19
+
20
+ button.container.click()
21
+
22
+ expect(spy).toHaveBeenCalled()
23
+ expect(spy.callCount).toEqual(1)
24
+
25
+
26
+ it 'fires when clicking embedded elements of the button', ->
27
+ button = new window.Button($('#test #button2'))
28
+ button.bind_event('clicked', (spy = jasmine.createSpy()))
29
+
30
+ button.container.find('.embedded').click()
31
+
32
+ expect(spy).toHaveBeenCalled()
33
+ expect(spy.callCount).toEqual(1)
34
+
35
+
36
+ describe 'programmatic clicks', ->
37
+
38
+ it 'programmatically clicks the button', ->
39
+ button = new window.Button($('#test #button2'))
40
+ spy = jasmine.createSpy()
41
+ button.bind_event('clicked', spy)
42
+
43
+ button.click()
44
+
45
+ expect(spy).toHaveBeenCalled()
@@ -0,0 +1,64 @@
1
+ #= require spec_helper
2
+
3
+ describe 'test environment setup', ->
4
+
5
+ it 'loading libraries', ->
6
+ load_modularity()
7
+ loadCS "/vendor/assets/javascripts/mixins/clickable.coffee"
8
+ loadCS "/vendor/assets/javascripts/modules/button.coffee"
9
+ loadCS "/vendor/assets/javascripts/modules/counter_button.coffee"
10
+
11
+
12
+ describe 'CounterButton', ->
13
+ template 'button.html'
14
+
15
+ describe 'standard button functionality', ->
16
+
17
+ describe 'manual clicks', ->
18
+
19
+ it 'fires when clicking on the container directly', ->
20
+ button = new window.CounterButton('#test #button1')
21
+ button.bind_event('clicked', (spy = jasmine.createSpy()))
22
+
23
+ button.container.click()
24
+
25
+ expect(spy).toHaveBeenCalled()
26
+ expect(spy.callCount).toEqual(1)
27
+
28
+
29
+ it 'fires when clicking embedded elements of the button', ->
30
+ button = new window.CounterButton('#test #button2')
31
+ button.bind_event('clicked', (spy = jasmine.createSpy()))
32
+
33
+ button.container.find('.embedded').click()
34
+
35
+ expect(spy).toHaveBeenCalled()
36
+ expect(spy.callCount).toEqual(1)
37
+
38
+ describe 'programmatic clicks', ->
39
+
40
+ it 'programmatically clicks the button', ->
41
+ button = new window.CounterButton('#test #button2')
42
+ spy = jasmine.createSpy()
43
+ button.bind_event('clicked', spy)
44
+
45
+ button.click()
46
+
47
+ expect(spy).toHaveBeenCalled()
48
+
49
+ describe 'CounterButton specific functionality', ->
50
+
51
+ it 'provides the click counter as the event payload', ->
52
+ button = new window.CounterButton('#test #button1')
53
+ button.bind_event('clicked', (spy = jasmine.createSpy()))
54
+
55
+ button.container.click()
56
+ button.container.click()
57
+ button.container.click()
58
+
59
+ expect(spy.callCount).toEqual(3)
60
+ expect(spy.argsForCall[0][1]).toEqual(1)
61
+ expect(spy.argsForCall[1][1]).toEqual(2)
62
+ expect(spy.argsForCall[2][1]).toEqual(3)
63
+
64
+
@@ -0,0 +1,22 @@
1
+ require('/spec/javascripts/external/jquery.min.js')
2
+ require('/spec/javascripts/external/jasmine-jquery.js')
3
+ require('/spec/javascripts/external/coffee-script.js')
4
+
5
+ # Helper method to circumvent that Evergreen doesn't load CoffeeScript files.
6
+ window.loadCS = (url, callback) ->
7
+ $.ajax
8
+ url: url
9
+ async: false
10
+ success: (data) ->
11
+ eval CoffeeScript.compile data
12
+ callback() if callback
13
+
14
+
15
+ window.load_modularity = ->
16
+ loadCS "/vendor/assets/javascripts/modularity.js.coffee?#{(new Date()).getTime()}"
17
+
18
+
19
+ beforeEach ->
20
+ @addMatchers
21
+ toHaveLength: (expected) ->
22
+ this.length == expected
@@ -0,0 +1,3 @@
1
+ <div id="button1">A normal button.</div>
2
+ <div id="button2">A button with <span class="embedded"></span> elements.</div>
3
+
@@ -0,0 +1,10 @@
1
+ <div id="closable1">
2
+ A closable section.
3
+ <div class="CloseButton"></div>
4
+ </div>
5
+
6
+ <div id="closable2">
7
+ A closable section with a custom close button.
8
+ <div class="CustomCloseButton"></div>
9
+ </div>
10
+
@@ -0,0 +1,21 @@
1
+ # This mixin adds a 'clickable' aspect to modules.
2
+ # This means clicking anywhere on the module fires the 'clicked' event.
3
+ window.clickable =
4
+
5
+ constructor: ->
6
+ @container.click @container_clicked
7
+
8
+
9
+ # Events that are fired by this mixin.
10
+ events:
11
+ clicked: 'clicked'
12
+
13
+
14
+ # Programmatically click this clickable element.
15
+ # For testing and scripting.
16
+ click: -> @container.click()
17
+
18
+
19
+ # Event handler for clicks on this clickable element.
20
+ container_clicked: -> @fire_event clickable.events.clicked
21
+
@@ -0,0 +1,18 @@
1
+ window.closable =
2
+
3
+ constructor: (container, close_button = undefined) ->
4
+ console.log close_button
5
+ close_button ||= @container.find('.CloseButton')
6
+ # TODO: add alert if no close button found.
7
+ close_button.click @close_button_clicked
8
+
9
+
10
+ # The events that can be fired by closable objects.
11
+ events:
12
+ closed: 'closed'
13
+
14
+
15
+ # Called when the button got clicked by the user or programmatically.
16
+ close_button_clicked: ->
17
+ @fire_event 'closed'
18
+ @container.remove()
@@ -19,14 +19,15 @@ class window.Module
19
19
 
20
20
 
21
21
  if @mixins?
22
- for mixin in @mixins
22
+ for mixin_data in @mixins
23
23
 
24
24
  # Attach all properties from mixin to the prototype.
25
- for methodName, method of mixin
26
- @[methodName] = method
25
+ for methodName, method of mixin_data.mixin
26
+ unless @[methodName]
27
+ @[methodName] = => method.call(@, mixin_data.params)
27
28
 
28
29
  # Call constructor function from mixin.
29
- mixin.constructor.apply @, arguments
30
+ mixin_data.mixin.constructor.apply @, arguments
30
31
 
31
32
 
32
33
  # Checks whether the given condition is true.
@@ -53,9 +54,10 @@ class window.Module
53
54
 
54
55
  # mixin = constructor of Draggable
55
56
  # self = Card
56
- @mixin: (mixin) ->
57
- @prototype.mixins = [] unless @prototype.mixins?
58
- @prototype.mixins.push mixin
57
+ @mixin: (mixin, p...) ->
58
+ @prototype.mixins or= []
59
+ console.log p
60
+ @prototype.mixins.push({mixin: mixin, params: p})
59
61
 
60
62
 
61
63
  # GLOBAL EVENTS.
@@ -0,0 +1,6 @@
1
+ # A button module.
2
+ # Responds to clicks on the container element.
3
+ # The container element is expected to already be populated.
4
+ class window.Button extends Module
5
+ @mixin clickable
6
+
@@ -0,0 +1,15 @@
1
+ # A button that counts how often it is clicked.
2
+ # This is implemented as a subclass of Button,
3
+ # to take advantage of the already existing functionality there.
4
+ class window.CounterButton extends Button
5
+
6
+ constructor: ->
7
+ super
8
+
9
+ # Counts how often this button has been clicked so far.
10
+ @click_count = 0
11
+
12
+
13
+ # We override the event handler for the 'clicked' event here.
14
+ container_clicked: =>
15
+ @fire_event 'clicked', ++@click_count
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modularity-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-24 00:00:00.000000000 Z
12
+ date: 2012-04-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &70150578537300 !ruby/object:Gem::Requirement
16
+ requirement: &70322603498400 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 3.1.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70150578537300
24
+ version_requirements: *70322603498400
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: capybara-webkit
27
- requirement: &70150578536860 !ruby/object:Gem::Requirement
27
+ requirement: &70322603497980 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70150578536860
35
+ version_requirements: *70322603497980
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: evergreen
38
- requirement: &70150578536380 !ruby/object:Gem::Requirement
38
+ requirement: &70322603497520 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,29 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70150578536380
46
+ version_requirements: *70322603497520
47
+ - !ruby/object:Gem::Dependency
48
+ name: rb-fsevent
49
+ requirement: &70322603497060 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70322603497060
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard-livereload
60
+ requirement: &70322603496640 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70322603496640
47
69
  description: Description of ModularityRails.
48
70
  email:
49
71
  - kevin.goslar@gmail.com
@@ -55,6 +77,7 @@ files:
55
77
  - .gitignore
56
78
  - .travis.yml
57
79
  - Gemfile
80
+ - Guardfile
58
81
  - LICENSE
59
82
  - README.md
60
83
  - Rakefile
@@ -63,10 +86,21 @@ files:
63
86
  - lib/modularity-rails/version.rb
64
87
  - modularity-rails.gemspec
65
88
  - spec/javascripts/external/coffee-script.js
89
+ - spec/javascripts/external/jasmine-jquery.js
66
90
  - spec/javascripts/external/jquery.min.js
91
+ - spec/javascripts/mixins/closable_spec.coffee
67
92
  - spec/javascripts/modularity_spec.coffee
93
+ - spec/javascripts/modules/button_spec.coffee
94
+ - spec/javascripts/modules/counter_button_spec.coffee
95
+ - spec/javascripts/spec_helper.coffee
96
+ - spec/javascripts/templates/button.html
97
+ - spec/javascripts/templates/closable.html
68
98
  - spec/javascripts/templates/test.html
99
+ - vendor/assets/javascripts/mixins/clickable.coffee
100
+ - vendor/assets/javascripts/mixins/closable.coffee
69
101
  - vendor/assets/javascripts/modularity.js.coffee
102
+ - vendor/assets/javascripts/modules/button.coffee
103
+ - vendor/assets/javascripts/modules/counter_button.coffee
70
104
  homepage: http://github.com/kevgo/modularity-rails
71
105
  licenses: []
72
106
  post_install_message: