37 |
38 | #title# Tests
39 |
40 | #system#
41 |
47 | #libs#
48 | #tests#
49 |
50 |
51 |
52 |
53 |
54 | """
55 |
56 | html: ->
57 | @render @template,
58 | system: @system()
59 | libs: @scripts project.libs()
60 | tests: @scripts project.tests()
61 | title: project.title()
62 |
63 | render: (template, params) ->
64 | html = template
65 | for name, value of params
66 | html = html.replace("##{name}#", value.replace(/\$/g, '$$$$'))
67 | html
68 |
69 | scripts: (files) ->
70 | files.map( (i) -> "" ).join("\n ")
71 |
72 | system: ->
73 | @scripts ['node_modules/jquery/dist/jquery.js',
74 | 'node_modules/should/should.js',
75 | 'node_modules/mocha/mocha.js']
76 |
77 | task 'server', 'Run test server', ->
78 | coffee = require('coffee-script')
79 |
80 | server = http.createServer (req, res) ->
81 | pathname = url.parse(req.url).pathname
82 |
83 | if pathname == '/'
84 | res.writeHead 200, 'Content-Type': 'text/html'
85 | res.write mocha.html()
86 |
87 | else if pathname == '/style.css'
88 | res.writeHead 200, 'Content-Type': 'text/css'
89 | res.write fs.readFileSync('node_modules/mocha/mocha.css')
90 |
91 | else if fs.existsSync('.' + pathname)
92 | file = fs.readFileSync('.' + pathname).toString()
93 | if pathname.match(/\.coffee$/)
94 | file = coffee.compile(file)
95 | if pathname.match(/\.(js|coffee)$/)
96 | res.writeHead 200, 'Content-Type': 'application/javascript'
97 | res.write file
98 |
99 | else
100 | res.writeHead 404, 'Content-Type': 'text/plain'
101 | res.write 'Not Found'
102 | res.end()
103 |
104 | server.listen 8000
105 | process.stdout.write("Open http://localhost:8000/\n")
106 |
107 | task 'clean', 'Remove all generated files', ->
108 | fs.removeSync('pkg/') if fs.existsSync('pkg/')
109 | for file in fs.readdirSync('./')
110 | fs.removeSync(file) if file.match(/\.gem$/)
111 |
112 | task 'min', 'Create minimized version of library', ->
113 | uglify = require('uglify-js')
114 |
115 | invoke('clean')
116 | fs.mkdirsSync('pkg/')
117 |
118 | for file in project.libs()
119 | name = file.replace(/^lib\//, '').replace(/\.js$/, '')
120 | fs.copySync(file, "pkg/#{name}-#{project.version()}.min.js")
121 |
122 | packages = fs.readdirSync('pkg/').filter( (i) -> i.match(/\.js$/) )
123 | for file in packages
124 | min = uglify.minify('pkg/' + file)
125 | fs.writeFileSync('pkg/' + file, min.code)
126 |
--------------------------------------------------------------------------------
/ChangeLog.md:
--------------------------------------------------------------------------------
1 | ## 0.7 (Ranger Baker, 28th January 1951)
2 | * Rename `evil-blocks.debug` to `evil-blocks-debug` for Sprockets 3 support.
3 |
4 | ## 0.6.4
5 | * Add Sprockets 3 support.
6 |
7 | ## 0.6.3
8 | * Add Slim 3.0 support (by Andrey Krivko).
9 |
10 | ## 0.6.2
11 | * Fix IE 8 support (by Dmitry Klimensky).
12 | * Fix event name in debugger (by Andrey Krivko).
13 |
14 | ## 0.6.1
15 | * Fix debugger scope (by Andrey Krivko)
16 |
17 | ## 0.6 (Ranger Able, 27th January 1951)
18 | * Add filters, which process block object before init was called.
19 | * Most build-in features was moved to filter to be disableable.
20 | * Listener `load on window` will call immediately, if page was already loaded.
21 |
22 | ## 0.5.1
23 | * Fix block vitalizing, when multiple blocks was binded to same DOM node.
24 |
25 | ## 0.5 (RDS-1, 29th August 1949)
26 | * Current event target was moved from first argument to `event.el`.
27 | * Inside finder was moved from `@(selector)` to `@$(selector)`.
28 | * Remove old function style API.
29 | * Add `@@block` alias.
30 | * Add debugger extension.
31 | * Vitalize blocks on next tick after page ready.
32 | * Don’t vitalize blocks twice.
33 | * Method `evil.block.vitalize()` calls on `document` by default.
34 | * Allow to use GitHub master in Bundler.
35 | * Add Bower support.
36 |
37 | ## 0.4.2 (Zebra, 14th May 1948)
38 | * Don’t listen bubbled events as block event.
39 | * Change license to MIT.
40 |
41 | ## 0.4.1 (Yoke, 30th April 1948)
42 | * Allow to listen body and window events from object style.
43 |
44 | ## 0.4 (X-Ray, 14th April 1948)
45 | * Add new object style.
46 |
47 | ## 0.3.2 (Helen of Bikini, 25th July 1946)
48 | * Fix searching elements inside several blocks (by Andrei Miroshnik).
49 |
50 | ## 0.3.1 (Gilda, 1st July 1946)
51 | * Fix in multiple block copies on one page (by Andrei Miroshnik).
52 |
53 | ## 0.3 (Fat Man, 9th August 1945)
54 | * Add shortcut to Slim to set data-role and class.
55 | * Run callback on every block copy in page.
56 |
57 | ## 0.2 (Little Boy, 6th August 1945)
58 | * Support non-Rails applications in gem.
59 |
60 | ## 0.1 (The Gadget, 16th July 1945)
61 | * Initial release.
62 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'slim'
4 | gem 'rspec'
5 |
6 | gemspec
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright 2013 Andrey Sitnik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Evil Blocks [](https://travis-ci.org/ai/evil-blocks)
2 |
3 |
4 |
5 | Evil Block is a tiny JS framework for web pages. It is based on 4 ideas:
6 |
7 | * **Split code to independent blocks.** “Divide and conquer” is always good idea.
8 | * **Blocks communicate by events.** Events is an easy and safe method
9 | to keep complicated dependencies between controls very clean.
10 | * **Separate JS and CSS.** You should only use classes for styles and bind JS
11 | by special attribute selectors. This way you can update your styles without
12 | fear to break any scripts.
13 | * **Try not to render on client.** 2-way data-binding looks very cool,
14 | but it has a [big price]. Most of web pages (unlike web applications)
15 | can render all HTML on server and use client rendering only in few places.
16 | Without rendering we can have incredibly clean code and architecture.
17 |
18 | See also [Evil Front], a pack of helpers for Ruby on Rails and Evil Blocks.
19 |
20 | Role aliases were taken from [Role.js]. Based on Pieces.js by [@chrome].
21 |
22 |
23 |
24 |
25 |
26 | [Role.js]: https://github.com/kossnocorp/role
27 | [big price]: http://staal.io/blog/2014/02/05/2-way-data-binding-under-the-microscope/
28 | [Evil Front]: https://github.com/ai/evil-front
29 | [@chrome]: https://github.com/chrome
30 |
31 | ## Quick Example
32 |
33 | Slim template:
34 |
35 | ```haml
36 | .todo-control@@todo
37 | ul@tasks
38 |
39 | - @tasks.each do |task|
40 | .task@task
41 | = task.name
42 | form@finishForm action="/tasks/#{ task.id }/finish"
43 | input type="submit" value="Finish"
44 |
45 | form@addForm action="/tasks/"
46 | input type="text" name="name"
47 | input type="submit" value="Add"
48 | ```
49 |
50 | Block’s CoffeeScript:
51 |
52 | ```coffee
53 | evil.block '@@todo',
54 |
55 | ajaxSubmit: (e) ->
56 | e.preventDefault()
57 |
58 | form = e.el
59 | form.addClass('is-loading')
60 |
61 | $.ajax
62 | url: form.attr('action')
63 | data: form.serialize()
64 | complete: -> form.removeClass('is-loading')
65 |
66 | 'submit on @finishForm': (e) ->
67 | @ajaxSubmit(e).done ->
68 | e.el.closest("@task").addClass("is-finished")
69 |
70 | 'submit on @addForm': (e) ->
71 | e.preventDefault()
72 | @ajaxSubmit(e).done (newTaskHTML) ->
73 | @tasks.append(newTaskHTML)
74 | ```
75 |
76 | ## Attributes
77 |
78 | If you use classes selectors in CSS and JS, your scripts will be depend
79 | on styles. If you change `.small-button` to `.big-button`, you must
80 | change all the button’s selectors in scripts.
81 |
82 | Separated scripts and styles are better, so Evil Blocks prefers to work with
83 | two HTML attributes to bind your JS: `data-block` (to define blocks)
84 | and `data-role` (to define elements inside block).
85 |
86 | ```html
87 |
88 |
89 |
90 |
91 | ```
92 |
93 | Evil Blocks extends Slim and jQuery, so you can use shortcuts for these
94 | attributes: `@@block` and `@role`. For Haml you can use [Role Block Haml] gem
95 | to use the same shortcuts.
96 |
97 | ```haml
98 | @@todo
99 | ul@tasks
100 | ```
101 |
102 | ```js
103 | $('@tasks')
104 | ```
105 |
106 | With these attributes you can easily change interface style
107 | and be sure in scripts:
108 |
109 | ```haml
110 | .big-button@addButton
111 | ```
112 |
113 | Of course, Evil Block doesn’t force you to write only these selectors.
114 | You can use any attributes, that you like.
115 |
116 | [Role Block Haml]: https://github.com/vladson/role_block_haml
117 |
118 | ## Blocks
119 |
120 | You should split your interface into independent controls and mark them
121 | with `data-block`:
122 |
123 | ```haml
124 | header@@header
125 | a.exit href="#"
126 |
127 | .todo-control@@todo
128 | ul.tasks
129 |
130 | .docs-page@@docs
131 | ```
132 |
133 | Also you can vitalize your blocks in scripts with `evil.block` function:
134 |
135 | ```coffee
136 | evil.block '@@header',
137 |
138 | init: ->
139 | console.log('Vitalize', @block)
140 | ```
141 |
142 | When a page was loaded Evil Blocks finds blocks by `@@header` selector
143 | (this is a shortcut for `[data-block=header]`) and calls `init` on every
144 | founded block. So, if your page contains two headers, `init` will be called
145 | twice with different `@block`’s.
146 |
147 | The `@block` property will contain a jQuery node of current block.
148 | You can search elements inside of current block with `@$(selector)` method:
149 |
150 | ```coffee
151 | evil.block '@@docs',
152 |
153 | init: ->
154 | @$('a').attr(target: '_blank') # Open all links inside docs in new tab
155 | # Same as @block.find('a')
156 | ```
157 |
158 | You can add any methods and properties to your block class:
159 |
160 | ```coffee
161 | evil.block '@@gallery',
162 | current: 0
163 |
164 | showPhoto: (num) ->
165 | @$('img').hide().
166 | filter("eql(#{ num })").show()
167 |
168 | init: ->
169 | @showPhoto(@current)
170 | ```
171 |
172 | Evil Blocks will automatically create properties with jQuery nodes
173 | for every element inside of a block with `data-role` attribute:
174 |
175 | ```haml
176 | .todo-control@@todo
177 | ul.tasks@tasks
178 | ```
179 |
180 | ```coffee
181 | evil.block '@@todo',
182 |
183 | addTask: (task) ->
184 | @tasks.append(task)
185 | ```
186 |
187 | If you add new HTML with AJAX, you can vitalize new blocks with
188 | `evil.block.vitalize()`. This function will vitalize only new blocks in
189 | a document.
190 |
191 | ```coffee
192 | @sections.append(html)
193 | evil.block.vitalize()
194 | ```
195 |
196 | ## Events
197 |
198 | You can bind listeners to events inside of a block with `events on selectors`
199 | method:
200 |
201 | ```coffee
202 | evil.block '@@todo',
203 |
204 | 'submit on @finishForm': ->
205 | # Event listener
206 | ```
207 |
208 | A more difficult example:
209 |
210 | ```coffee
211 | evil.block '@@form',
212 | ajaxSearch: -> …
213 |
214 | 'change, keyup on input, select': (event) ->
215 | field = event.el()
216 | @ajaxSearch('Changed', field.val())
217 | ```
218 |
219 | Listener will receive a jQuery Event object as the first argument.
220 | Current element (`this` in jQuery listeners) will be contained in `event.el`
221 | property. All listeners are delegated on current block, so `click on @button`
222 | is equal to `@block.on 'click', '@button', ->`.
223 |
224 | You should prevent default event behavior with `event.preventDefault()`,
225 | `return false` will not do anything in block’s listeners. I recommend
226 | [evil-front/links] to prevent default behavior in any links with `href="#"`
227 | to clean your code.
228 |
229 | You can also bind events on body and window:
230 |
231 | ```coffee
232 | evil.blocks '@@docs',
233 | recalcMenu: -> …
234 | openPage: -> …
235 |
236 | init: ->
237 | @recalcMenu()
238 |
239 | 'resize on window': ->
240 | @recalcMenu()
241 |
242 | 'hashchange on window': ->
243 | @openPage(location.hash)
244 | ```
245 |
246 | Listener `load on window` will execute immediately, if window is already loaded.
247 |
248 | [evil-front/links]: https://github.com/ai/evil-front/blob/master/evil-front/lib/assets/javascripts/evil-front/links.js
249 |
250 | ## Blocks Communications
251 |
252 | Blocks should communicate via custom jQuery events. You can bind an event
253 | listener to a block node with `on events` method:
254 |
255 | ```coffee
256 | evil.block '@@slideshow',
257 | nextSlide: -> …
258 |
259 | 'on play': ->
260 | @timer = setInterval(=> @nextSlide, 5000)
261 |
262 | 'on stop': ->
263 | clearInterval(@timer)
264 |
265 | evil.block '@@video',
266 |
267 | 'click on @fullscreenButton': ->
268 | $('@@slideshow').trigger('stop')
269 | ```
270 |
271 | If you want to use broadcast messages, you can use custom events on body:
272 |
273 | ```coffee
274 | evil.block '@@callUs',
275 |
276 | 'change-city on body': (e, city) ->
277 | @phoneNumber.text(city.phone)
278 |
279 | evil.block '@@cityChanger',
280 | getCurrentCity: -> …
281 |
282 | 'change on @citySelect': ->
283 | $('body').trigger('change-city', @getCurrentCity())
284 | ```
285 |
286 | ## Rendering
287 |
288 | If you render on the client and on the server-side, you must repeat helpers,
289 | i18n, templates. Client rendering requires a lot of libraries and architecture.
290 | 2-way data binding looks cool, but has a very [big price] in performance,
291 | templates, animation and overengeniring.
292 |
293 | If you develop a web page (not a web application with offline support, etc),
294 | server-side rendering will be more useful. Users will see your interface
295 | imminently, search engines will index your content and your code will be much
296 | simple and clear.
297 |
298 | In most of cases you can avoid client-side rendering. If you need to add a block
299 | with JS, you can render it hidden to page HTML and show it in right time:
300 |
301 | ```coffee
302 | evil.block '@@comment',
303 |
304 | 'click on @addCommentButton': ->
305 | @newCommentForm.slideDown()
306 | ```
307 |
308 | If a user changes some data and you need to update the view, you anyway need
309 | to send a request to save the new data on a server. Just ask the server
310 | to render a new view. For example, on a new comment server can return
311 | new comment HTML:
312 |
313 | ```coffee
314 | evil.block '@@comment',
315 |
316 | 'submit on @addCommentForm': ->
317 | $.post '/comments', @addCommentForm.serialize(), (newComment) ->
318 | @comments.append(newComment)
319 | ```
320 |
321 | But, of course, some cases require client-side rendering. Evil Blocks only
322 | recommends to do it on the server side, but not force you:
323 |
324 | ```coffee
325 | evil.block '@@comment',
326 |
327 | 'change, keyup on @commentField', ->
328 | html = JST['comment'](text: @commentField.text())
329 | @preview.html(html)
330 | ```
331 |
332 | [big price]: http://staal.io/blog/2014/02/05/2-way-data-binding-under-the-microscope/
333 |
334 | ## Debug
335 |
336 | Evil Blocks contains a debug extension, which logs all the events inside blocks.
337 | To enable it, just load `evil-blocks-debug.js`. For example, in Rails:
338 |
339 | ```haml
340 | - if Rails.env.development?
341 | = javascript_include_tag 'evil-blocks-debug'
342 | ```
343 |
344 | ## Extensions
345 |
346 | Evil Blocks has a tiny core. It only finds blocks via selectors,
347 | sets the `@block` property and calls the `init` method. Any other features
348 | (like event bindings or `@$()` method) are created by filters
349 | and can be disabled or replaced.
350 |
351 | Before calling `init`, Evil Blocks processes an object through the filters list
352 | in `evil.block.filters`. A filter accepts an object as its first argument and
353 | an unique class ID as the second. It can find some properties inside of
354 | the object, work with block DOM nodes and add/remove some object properties.
355 | If filter returns `false`, Evil Blocks will stop block vitalizing
356 | and will not call the `init` method.
357 |
358 | Default filters:
359 |
360 | 1. **Don’t vitalize same DOM node twice.** It returns `false` if a block
361 | was already initialized with a given ID.
362 | 2. **Add `@$()` method.** It adds a shortcut find method to an object.
363 | 3. **Add shortcuts to `@element`.** It adds properties for all children with
364 | `data-role` attribute.
365 | 4. **Bind block events.** Find, bind listeners and remove all the methods with
366 | a name like `on event`.
367 | 5. **Smarter window load listener.** Run `load on window` listener immediately,
368 | if window is already loaded.
369 | 6. **Bind window and body events.** Find, bind listeners and remove all
370 | the methods with a name like `event on window` or `event on body`.
371 | 7. **Bind elements events.** Find, bind listeners and remove all the methods
372 | with a name like `event on child`.
373 |
374 | You can add you own filter to `evil.block.filters`. Most filters should be added
375 | after first filter to not been called on already initialized blocks.
376 |
377 | Let’s write filter, which will initialize blocks only when they become
378 | to be visible.
379 |
380 | ```coffee
381 | filter = (obj) ->
382 | if not obj.block.is(':visible')
383 | # Check for visibility every 100 ms
384 | # and recall vitalizing if block become visible
385 | checking = ->
386 | evil.block.vitalize(obj.block) if obj.block.is(':visible')
387 | setTimeout(checking, 100);
388 |
389 | # Disable block initializing
390 | return false
391 |
392 | # Add filter to list
393 | evil.block.filters.splice(0, 0, filter)
394 | ```
395 |
396 | With the filters you can change Evil Blocks logic, add some new shortcuts
397 | or features like mixins.
398 |
399 | Also you can remove any default filters from `evil.block.filters`. For example,
400 | you can create properties for `data-role` children only from some white list.
401 |
402 | But Filters API is still unstable and you should be careful on major updates.
403 |
404 | ## Modules
405 |
406 | If your blocks have same behavior, you can create a module-block
407 | and set multiple blocks on the same tag:
408 |
409 | ```haml
410 | @popup@@closable
411 | a@closeLink href="#"
412 | ```
413 |
414 | ```coffee
415 | evil.block '@@closable',
416 |
417 | 'click on @closeLink': ->
418 | @block.trigger('close')
419 |
420 | evil.block '@@popup',
421 |
422 | 'on close': ->
423 | @block.removeClass('is-open')
424 | ```
425 |
426 | If you want to use same methods inside of multiple block, you can create
427 | an inject-function:
428 |
429 | ```coffee
430 | fancybox = (obj) ->
431 | for name, value of fancybox.module
432 | obj[name] = value
433 | # Initializer code
434 |
435 | fancybox.module =
436 | openInFancybox: (node) ->
437 |
438 | evil.block '@@docs',
439 |
440 | init: ->
441 | fancybox(@)
442 |
443 | 'click on @showExampleButton': ->
444 | @openInFancybox(@example)
445 | ```
446 |
447 | ## Install
448 |
449 | ### Ruby on Rails
450 |
451 | Add `evil-block-rails` gem to `Gemfile`:
452 |
453 | ```ruby
454 | gem "evil-blocks-rails"
455 | ```
456 |
457 | Load `evil-blocks.js` in your script:
458 |
459 | ```js
460 | //= require evil-blocks
461 | ```
462 |
463 | If you use Rails 3 on Heroku, you may need
464 | [some hack](https://github.com/ai/evil-blocks/issues/17).
465 |
466 | ### Ruby
467 |
468 | If you use Sinatra or other non-Rails framework you can add Evil Blocks path
469 | to Sprockets environment:
470 |
471 | ```ruby
472 | EvilBlocks.install(sprockets)
473 | ```
474 |
475 | And change Slim options to support `@@block` and `@rule` shortcuts:
476 |
477 | ```ruby
478 | EvilBlocks.install_to_slim!
479 | ```
480 |
481 | Then just load `evil-blocks.js` in your script:
482 |
483 | ```js
484 | //= require evil-blocks
485 | ```
486 |
487 | ### Others
488 |
489 | Add file `lib/evil-blocks.js` to your project.
490 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "evil-blocks",
3 | "version": "07.0",
4 | "homepage": "https://github.com/ai/evil-blocks",
5 | "authors": ["Andrey A.I. Sitnik "],
6 | "description": "Tiny JS framework for web pages to split your app to independent blocks",
7 | "keywords": ["framework", "blocks", "events"],
8 | "license": "MIT",
9 | "main": [
10 | "lib/evil-blocks.js"
11 | ],
12 | "dependencies": {
13 | "jquery": ">= 1.6.0"
14 | },
15 | "ignore": [
16 | "package.json",
17 | "*.gemspec",
18 | "Cakefile",
19 | "Gemfile*",
20 | "test",
21 | "*.rb",
22 | ".*"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/evil-blocks-rails.gemspec:
--------------------------------------------------------------------------------
1 | require 'pathname'
2 |
3 | package = Pathname(__FILE__).dirname.join('package.json').read
4 | version = package.match(/"version": "([\d\.]+)",/)[1]
5 |
6 | Gem::Specification.new do |s|
7 | s.platform = Gem::Platform::RUBY
8 | s.name = 'evil-blocks-rails'
9 | s.version = version
10 | s.summary = 'Tiny JS framework for web pages to split your app ' +
11 | 'to independent blocks'
12 |
13 | s.files = ['lib/evil-blocks-debug.js', 'lib/evil-blocks.js',
14 | 'lib/evil-blocks-rails.rb',
15 | 'LICENSE', 'README.md', 'ChangeLog.md']
16 | s.extra_rdoc_files = ['LICENSE', 'README.md', 'ChangeLog.md']
17 | s.require_path = 'lib'
18 |
19 | s.author = 'Andrey Sitnik'
20 | s.email = 'andrey@sitnik.ru'
21 | s.homepage = 'https://github.com/ai/evil-blocks'
22 | s.license = 'MIT'
23 |
24 | s.add_dependency 'sprockets', '>= 2'
25 | end
26 |
--------------------------------------------------------------------------------
/lib/evil-blocks-debug.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | "use strict";
3 |
4 | var log = function () {
5 | if ( !console || !console.log ) {
6 | return;
7 | }
8 | console.log.apply(console, arguments);
9 | };
10 |
11 | if ( !window.evil || !window.evil.block ) {
12 | log("You should include evil-blocks-debug.js after evil-blocks.js");
13 | return;
14 | }
15 |
16 | var logger = function (obj) {
17 | for ( var name in obj ) {
18 | if ( name.indexOf('on ') == -1 ) continue;
19 |
20 | var parts = name.split('on ');
21 | var event = parts[0] ? parts[0] : parts[1];
22 |
23 | var callback = obj[name];
24 |
25 | (function(event, callback){
26 | obj[name] = function (e) {
27 | var source = e.el ? e.el[0] : this.block[0];
28 | var messages = ['Event "' + event + '" on', source];
29 |
30 | var params = Array.prototype.slice.call(arguments, 1);
31 | if ( params.length > 0 ) {
32 | messages.push('with params');
33 | messages = messages.concat(params);
34 | }
35 |
36 | log.apply(this, messages);
37 | callback.apply(this, arguments);
38 | }
39 | })(event, callback);
40 | }
41 | };
42 |
43 | evil.block.filters.splice(2, 0, logger);
44 | })();
45 |
--------------------------------------------------------------------------------
/lib/evil-blocks-rails.rb:
--------------------------------------------------------------------------------
1 | module EvilBlocks
2 | # Change Slim options to support @data-role shortcut.
3 | def self.install_to_slim!
4 | # Add @data-role alias to Slim.
5 | #
6 | # Copy from role-rails by Sasha Koss.
7 | # https://github.com/kossnocorp/role-rails
8 | shortcut = Slim::Parser.options[:shortcut]
9 | shortcut['@'] = { attr: 'data-role' }
10 | shortcut['@@'] = { attr: 'data-block' }
11 | Slim::Engine.options[:merge_attrs]['data-role'] = ' '
12 | Slim::Engine.options[:merge_attrs]['data-block'] = ' '
13 | end
14 |
15 | # Add assets paths to standalone Sprockets environment.
16 | def self.install(sprockets)
17 | sprockets.paths << Pathname(__FILE__).dirname
18 | end
19 |
20 | if defined? ::Rails
21 | class Engine < ::Rails::Engine
22 | initializer 'evil-blocks' do |app|
23 | EvilBlocks.install(app.config.assets)
24 | EvilBlocks.install_to_slim! if defined?(Slim::Parser)
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/evil-blocks.js:
--------------------------------------------------------------------------------
1 | (function(factory) {
2 | "use strict";
3 |
4 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
5 | module.exports = factory(require('jquery'), window)
6 | } else {
7 | /*
8 | * Evil namespace. Also can be used in Evil Front.
9 | */
10 | if ( !window.evil ) window.evil = {};
11 | window.evil.block = factory(window.$, window)
12 | }
13 | }(function ($, window) {
14 | // Helpers
15 | var $window = $(window);
16 |
17 | // Clone object
18 | var clone = function (origin) {
19 | var cloned = { };
20 | for ( var name in origin ) {
21 | cloned[name] = origin[name];
22 | }
23 | return cloned;
24 | };
25 |
26 | // Is string ends with substring.
27 | var endsWith = function (string, substring) {
28 | return string.substr(-substring.length) === substring;
29 | };
30 |
31 | /*
32 | * Add `@data-role` alias to jQuery.
33 | *
34 | * Copy from jquery.role by Sasha Koss https://github.com/kossnocorp/role
35 | */
36 |
37 | var rewriteSelector = function (context, name, pos) {
38 | var original = context[name];
39 | if ( !original ) return;
40 |
41 | context[name] = function () {
42 | arguments[pos] = arguments[pos].replace(
43 | /@@([\w\u00c0-\uFFFF\-]+)/g, '[data-block~="$1"]');
44 | arguments[pos] = arguments[pos].replace(
45 | /@([\w\u00c0-\uFFFF\-]+)/g, '[data-role~="$1"]');
46 | return original.apply(context, arguments);
47 | };
48 |
49 | $.extend(context[name], original);
50 | };
51 |
52 | rewriteSelector($, 'find', 0);
53 | rewriteSelector($, 'multiFilter', 0);
54 | rewriteSelector($.find, 'matchesSelector', 1);
55 | rewriteSelector($.find, 'matches', 0);
56 |
57 | // Find selector inside base DOM node and cretae class for it.
58 | var find = function (base, id, selector, klass) {
59 | var blocks = $().add( base.filter(selector) ).
60 | add( base.find(selector) );
61 |
62 | if ( blocks.length == 0 ) return;
63 |
64 | var objects = [];
65 |
66 | blocks.each(function (_, node) {
67 | var block = $(node);
68 |
69 | var obj = clone(klass);
70 | obj.block = block;
71 |
72 | for ( var i = 0; i < evilBlock.filters.length; i++ ) {
73 | var stop = evilBlock.filters[i](obj, id);
74 | if ( stop === false ) return;
75 | }
76 |
77 | objects.push(obj)
78 | });
79 |
80 | return function () {
81 | for ( var i = 0; i < objects.length; i++ ) {
82 | if (objects[i].init) objects[i].init();
83 | }
84 | };
85 | };
86 |
87 | // If onready event was already happend.
88 | var ready = false;
89 |
90 | // If onload event was already happend.
91 | var loaded = false;
92 | $window.on('load', function (event) {
93 | loaded = event;
94 | });
95 |
96 | // Latest block ID
97 | var lastBlock = 0;
98 |
99 | /**
100 | * Create object for every `selector` finded in page and call their
101 | * `init` method.
102 | *
103 | * evilBlock '.user-page .buttons',
104 | * init: ->
105 | * @gallery.fotorama()
106 | * delete: ->
107 | * @deleteForm.submit ->
108 | * $('user-status').trigger('deleted')
109 | * 'click on @deleleLink': (e) ->
110 | * e.el.addClass('is-loading')
111 | * delete()
112 | * 'on update': ->
113 | * location.reload()
114 | *
115 | * Every `data-role="aName"` in HTML will create in object `aName` property
116 | * with jQuery node.
117 | *
118 | * To bind delegate listener just create `EVENT on SELECTOR` method.
119 | * In first argument it will receive jQuery node of `e.currentTarget`,
120 | * second will be event object and others will be parameters.
121 | *
122 | * To communicate between blocks, just trigget custom events. To receive
123 | * events from another blocks, create `on EVENT` method. Event object will
124 | * be on first argument here.
125 | *
126 | * Block node will be in `@block` property and you can search only inside
127 | * block by `@(selector)` method.
128 | *
129 | * If your block contrain only `init` method, you can use shortcut:
130 | *
131 | * evilBlock '.block', ->
132 | * # init method
133 | */
134 | var evilBlock = function (selector, klass) {
135 | lastBlock += 1;
136 | var id = lastBlock;
137 |
138 | if ( typeof(klass) == 'function' ) {
139 | klass = { init: klass };
140 | }
141 |
142 | evilBlock.defined.push([id, selector, klass]);
143 |
144 | if ( ready ) {
145 | var init = find($(document), id, selector, klass);
146 | if ( init ) init();
147 | }
148 | };
149 |
150 | /**
151 | * Vitalize all current blocks inside base. You must call it on every
152 | * new content from AJAX.
153 | *
154 | * 'on click on @load': ->
155 | * $.get '/comments', (comments) =>
156 | * evilBlock.vitalize $(comments).applyTo(@comments)
157 | */
158 | evilBlock.vitalize = function (base) {
159 | if ( base ) {
160 | base = $(base);
161 | } else {
162 | base = $(document);
163 | }
164 |
165 | var inits = [];
166 | for ( var i = 0; i < evilBlock.defined.length; i++ ) {
167 | var define = evilBlock.defined[i];
168 | inits.push( find(base, define[0], define[1], define[2]) );
169 | }
170 |
171 | for ( var i = 0; i < inits.length; i++ ) {
172 | if ( inits[i] ) inits[i]();
173 | }
174 | };
175 |
176 | /**
177 | * Evil blocks list.
178 | */
179 | evilBlock.defined = [];
180 |
181 | /**
182 | * Filters to process block object and add some extra functions
183 | * to Evil Blocks. For example, allow to write listeners.
184 | *
185 | * Filter will receive block object and unique class ID.
186 | * If filter return `false`, block will not be created.
187 | */
188 | evilBlock.filters = [];
189 |
190 | var filters = evilBlock.filters;
191 |
192 | /**
193 | * Don’t vitalize already vitalized block.
194 | *
195 | * For better perfomance, it should be last filter.
196 | */
197 | filters.push(function (obj, id) {
198 | var ids = obj.block.data('evil-blocks');
199 | if ( !ids ) {
200 | ids = [];
201 | } else if ( ids.indexOf(id) != -1 ) {
202 | return false;
203 | }
204 | ids.push(id);
205 | obj.block.data('evil-blocks', ids);
206 | });
207 |
208 | /**
209 | * Create `this.$()` as alias for `this.block.find()`
210 | */
211 | filters.push(function (obj) {
212 | obj.$ = function (subselector) {
213 | return obj.block.find(subselector);
214 | };
215 | });
216 |
217 | /**
218 | * Create properties for each element with `data-role`.
219 | */
220 | filters.push(function (obj) {
221 | obj.block.find('[data-role]').each(function (_, el) {
222 | var roles = el.attributes['data-role'].value.split(' ');
223 | for ( var i = 0; i < roles.length; i++ ) {
224 | var role = roles[i];
225 | if ( !obj[role] ) obj[role] = $();
226 | if ( obj[role].jquery ) obj[role].push(el);
227 | }
228 | });
229 | });
230 |
231 | /**
232 | * Syntax sugar to listen block events.
233 | */
234 | filters.push(function (obj) {
235 | for ( var name in obj ) {
236 | if ( name.substr(0, 3) != 'on ' ) continue;
237 |
238 | var events = name.substr(3);
239 | var callback = obj[name];
240 | delete obj[name];
241 |
242 | (function (events, callback) {
243 | obj.block.on(events, function (e) {
244 | if ( e.currentTarget == e.target ) {
245 | callback.apply(obj, arguments);
246 | }
247 | });
248 | })(events, callback);
249 | }
250 | });
251 |
252 | /**
253 | * Smart `load on window` listener, which fire immediately
254 | * if page was already loaded.
255 | */
256 | filters.push(function (obj) {
257 | var name = 'load on window';
258 | var callback = obj[name];
259 |
260 | if ( !callback ) return;
261 | delete obj[name];
262 |
263 | if ( loaded ) {
264 | setTimeout(function () {
265 | callback.call(obj, loaded);
266 | }, 1);
267 | } else {
268 | $window.on('load', function (event) {
269 | callback.call(obj, event);
270 | });
271 | }
272 | });
273 |
274 | /**
275 | * Syntax sugar to listen window and body events.
276 | */
277 | filters.push(function (obj) {
278 | for ( var name in obj ) {
279 | var elem = false;
280 | if ( endsWith(name, 'on body') ) {
281 | elem = $('body');
282 | } else if ( endsWith(name, 'on window') ) {
283 | elem = $window;
284 | }
285 |
286 | if ( !elem ) continue;
287 |
288 | var event = name.split(' on ')[0];
289 | var callback = obj[name];
290 | delete obj[name];
291 |
292 | (function (elem, event, callback) {
293 | elem.on(event, function () {
294 | callback.apply(obj, arguments);
295 | });
296 | })(elem, event, callback);
297 | }
298 | });
299 |
300 | /**
301 | * Syntax sugar to listen element events.
302 | */
303 | filters.push(function (obj) {
304 | for ( var name in obj ) {
305 | var parts = name.split(' on ');
306 | if ( !parts[1] ) continue;
307 |
308 | var callback = obj[name];
309 | delete obj[name];
310 |
311 | (function (parts, callback) {
312 | obj.block.on(parts[0], parts[1], function (e) {
313 | e.el = $(this);
314 | callback.apply(obj, arguments);
315 | });
316 | })(parts, callback);
317 | }
318 | });
319 |
320 | /*
321 | * Run all blocks on load.
322 | */
323 | $(document).ready(function () {
324 | ready = true;
325 | evilBlock.vitalize();
326 | });
327 |
328 | return evilBlock
329 | }))
330 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "evil-blocks",
3 | "version": "0.7.0",
4 | "dependencies": {
5 | "jquery": ">= 2.1.0"
6 | },
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/ai/evil-blocks.git"
11 | },
12 | "devDependencies": {
13 | "coffee-script": "1.10.0",
14 | "uglify-js": "2.6.1",
15 | "fs-extra": "0.26.4",
16 | "should": "8.1.1",
17 | "mocha": "2.3.4",
18 | "jsdom": "7.2.2"
19 | },
20 | "scripts": {
21 | "test": "mocha"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/blocks.coffee:
--------------------------------------------------------------------------------
1 | evilBlock = require('../lib/evil-blocks')
2 | body = (html) -> evilBlock.vitalize fixtures.html(html)
3 | fixtures = null
4 |
5 | describe 'evilBlock', ->
6 | before -> fixtures = $('#fixtures')
7 |
8 | afterEach ->
9 | fixtures.html('')
10 | evilBlock.defined = []
11 |
12 | it 'adds role alias', ->
13 | body '' +
14 | '' +
15 | ''
16 | $('@roleTest').length.should.eql(1)
17 | $('@multi').length.should.eql(2)
18 |
19 | it 'adds block alias', ->
20 | body '' +
21 | '' +
22 | ''
23 | $('@@roleTest').length.should.eql(1)
24 | $('@@multi').length.should.eql(2)
25 |
26 | describe '.vitalize()', ->
27 |
28 | it 'calls vitalize on document by default', ->
29 | called = false
30 | evilBlock '.page', ->
31 | called = true
32 |
33 | fixtures.html('')
34 | evilBlock.vitalize()
35 |
36 | called.should.be.true
37 |
38 | it 'calls vitalize on subnode', ->
39 | called = []
40 | evilBlock '.page', ->
41 | called.push @block[0].tagName
42 |
43 | fixtures.html('')
44 | evilBlock.vitalize($('span'))
45 |
46 | called.should.eql ['B']
47 |
48 | it 'accepts DOM nodes', ->
49 | called = []
50 | evilBlock '.page', ->
51 | called.push @block[0].tagName
52 |
53 | fixtures.html('')
54 | evilBlock.vitalize($('#fixtures span'))
55 |
56 | called.should.eql ['B']
57 |
58 | describe '()', ->
59 |
60 | it 'understands function as init', ->
61 | called = false
62 | evilBlock '.page', -> called = true
63 |
64 | body ''
65 | called.should.be.false
66 |
67 | body ''
68 | called.should.be.true
69 |
70 | it 'executes callback only if find selector', ->
71 | called = false
72 | evilBlock '.page',
73 | init: ->
74 | called = true
75 |
76 | body ''
77 | called.should.be.false
78 |
79 | body ''
80 | called.should.be.true
81 |
82 | it 'executes only once on same block', ->
83 | called = 0
84 | evilBlock '.page',
85 | init: ->
86 | called += 1
87 |
88 | body ''
89 | called.should.be.eql(1)
90 |
91 | evilBlock.vitalize fixtures
92 | called.should.be.eql(1)
93 |
94 | fixtures.append('')
95 | evilBlock.vitalize fixtures
96 | called.should.be.eql(2)
97 |
98 | it 'works with multiple blocks on same node', ->
99 | called = ''
100 | evilBlock '.page',
101 | init: ->
102 | called += '1'
103 | evilBlock '.page',
104 | init: ->
105 | called += '2'
106 |
107 | body ''
108 | called.should.be.eql('12')
109 |
110 | evilBlock.vitalize fixtures
111 | called.should.be.eql('12')
112 |
113 | it 'creates properties for each role', ->
114 | prop = false
115 | evilBlock '.page',
116 | init: ->
117 | prop = @roleName
118 |
119 | body '