'
253 |
254 | $('.page').trigger('fire')
255 | value.should.eql(1)
256 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------