README.md in evil-blocks-rails-0.4.2 vs README.md in evil-blocks-rails-0.5.0

- old
+ new

@@ -1,149 +1,413 @@ # Evil Blocks -Evil Block is a tiny framework for web pages. It splits your application -to separated blocks with isolated styles and scripts. +Evil Block is a tiny JS framework for web pages. It is based on 4 ideas: -Sponsored by [Evil Martians](http://evilmartians.com/). -Role aliases taken from [role](https://github.com/kossnocorp/role) -and [role-rails](https://github.com/kossnocorp/role-rails) by Sasha Koss. +* **Split code to independent blocks.** “Divide and rule” is always good idea. +* **Blocks communicate by events.** Events is easy and safe method to clean + very complicated dependencies between controls. +* **Separate JS and CSS.** You use classes only for styles and bind JS + by selectors with special attributes. So you can update your styles without + fear to break your scripts. +* **Try not to render on client.** 2 way data-binding looks very cool, + but it has [big price]. Most of web pages (instead of web applications) + can render all HTML on server and use client rendering only in few small + places. Without rendering we can incredibly clean code and architecture. +See also [Evil Front], a pack of helpers for Ruby on Rails and Evil Blocks. + +Sponsored by [Evil Martians]. Role aliases was taken from [Role.js]. + +[Role.js]: https://github.com/kossnocorp/role +[big price]: http://staal.io/blog/2014/02/05/2-way-data-binding-under-the-microscope/ +[Evil Front]: https://github.com/ai/evil-front +[Evil Martians]: http://evilmartians.com/ + ## Quick Example -### Slim Template +Slim template: ```haml -.user-page - .title User Name - .gallery-control.is-big - // Evil Blocks add @data-role alias to Slim - // .class to bind styles, @data-role to bind JavaScript - a.gallery-left@nextPhoto href="#" - a.gallery-right@prevPhoto href="#" - img src="photos/1.jpg" - img src="photos/2.jpg" - img src="photos/3.jpg" +.todo-control@@todo + ul@tasks + + - @tasks.each do |task| + .task@task + = task.name + form@finishForm action="/tasks/#{ task.id }/finish" + input type="submit" value="Finish" + + form@addForm action="/tasks/" + input type="text" name="name" + input type="submit" value="Add" ``` -### Sass Styles +Block’s CoffeeScript: -```sass -// Block -.user-page - // All elements must be inside blocks (like in namespaces) - .title - font-size: 20px +```coffee +evil.block '@@todo', -// Block -.gallery-control - position: relative - img, .gallery-left, .gallery-right - position: absolute - // Modificator - &.is-big - width: 600px + ajaxSubmit: (e) -> + e.preventDefault() + + form = e.el + form.addClass('is-loading') + + $.ajax + url: form.attr('action') + data: form.serialize() + complete: -> form.addClass('is-loading') + + 'submit on @finishForm': (e) -> + @ajaxSubmit(e).done -> + e.el.closest("@task").addClass("is-finished") + + 'submit on @addForm': (e) -> + e.preventDefault() + @ajaxSubmit(e).done (newTaskHTML) -> + @tasks.append(newTaskHTML) ``` -### CoffeeScript +## Attributes +If you use classes selectors in CSS and JS, your scripts will be depend +on styles. If you will change `.small-button` to `.big-button`, you must +change all button’s selectors in scripts. + +Separated scripts and styles are better, so Evil Blocks prefer to work with +two HTML attributes to bind your JS: `data-block` (to define blocks) +and `data-role` (to define elements inside block). + +```html +<div data-block="todo"> + <ul data-role="tasks"> + </ul> +</div> +``` + +Evil Blocks extends Slim and jQuery, so you can use shortcuts for this +attributes: `@@block` and `@role`: + +```haml +@@todo + ul@tasks +``` + +```js +$('@tasks') +``` + +With this attributes you can easily change interface style +and be sure in scripts: + +```haml +.big-button@addButton +``` + +Of course, Evil Block doesn’t force you to use only this selectors. +You can any attributes, that you like. + +## Blocks + +You should split your interface to independent controls and mark them +with `data-block`: + +```haml +header@@header + a.exit href="#" + +.todo-control@@todo + ul.tasks + +.docs-page@@docs +``` + +Then you can vitalize your blocks in scripts by `evil.block` function: + ```coffee -# Will execute init only if .gallery-control is in current page -evil.block '.gallery-control', +evil.block '@@header', + + init: -> + console.log('Vitalize', @block) +``` + +When page will be loaded Evil Blocks finds blocks by `@@header` selector +(this is shortcut for `[data-block=header]`) and call `init` on every +founded block. So, if your page contains two headers, `init` will be called +twice with different `@block`. + +Property `@block` will contain jQuery-node of current block. You can search +inside current block by `@$(selector)` method: + +```coffee +evil.block '@@docs', + + init: -> + @$('a').attr(target: '_blank') # Open all links inside docs in new tab + # Same as @block.find('a') +``` + +You can add any methods and properties to your block class: + +```coffee +evil.block '@@gallery', current: 0 showPhoto: (num) -> - @('img').hide(). + @$('img').hide(). filter("eql(#{ num })").show() init: -> @showPhoto(@current) +``` - 'click on @nextPhoto', (link, event) -> - @showPhoto(current += 1) +Evil Blocks will automatically create properties with jQuery-nodes +for every element inside block with `data-role` attribute: - 'on start-slideshow', -> - # You can communicate between blocks by simple events - setTimeout( => @nextPhoto.click() , 5000) +```haml +.todo-control@@todo + ul.tasks@tasks +``` -# Will execute init only on user page, where .user-page exists -evil.block '.user-page', +```coffee +evil.block '@@todo', + addTask: (task) -> + @tasks.append(task) +``` + +If you add new HTML by AJAX, you can vitalize new blocks by +`evil.block.vitalize()`. This function will vitalize only new blocks in +document. + +```coffee +@sections.append(html) +evil.block.vitalize() +``` + +## Events + +You can bind listeners to events inside block by `"events on selectors"` method: + +```coffee +evil.block '@@todo', + + 'submit on @finishForm': -> + # Event listener +``` + +More difficult example: + +```coffee +evil.block '@@form', + ajaxSearch: -> … + + 'change, keyup on input, select': (event) -> + field = event.el() + @ajaxSearch('Changed', field.val()) +``` + +Listener will receive jQuery Event object as first argument. +Current element (`this` in jQuery listeners) will be in `event.el` property. +All listeners are delegated on current block, so `click on @button` will be +equal to `@block.on 'click', '@button', ->`. + +You should prevent default event behavior by `event.preventDefault()`, +`return false` will not do anything in block’s listeners. I recommend +[evil-front/links] to prevent default behavior in any links with `href="#"` +to clean your code. + +You can also bind events on body and window: + +```coffee +evil.blocks '@@docs', + recalcMenu: -> … + openPage: -> … + init: -> - @('.gallery-control').trigger('start-slideshow') + @recalcMenu() + + 'resize on window': -> + @recalcMenu() + + 'hashchange on window': -> + @openPage(location.hash) ``` -## Styles +[evil-front/links]: https://github.com/ai/evil-front/blob/master/evil-front/lib/assets/javascripts/evil-front/links.js -* You split all you tags to “blocks” and “elements”. Elements must belong - to block. Blocks can be nested. -* Blocks classes must have special suffix, that can’t be accidentally used - in elements. For example. `-page`, `-layout`, `-control` suffixes - (for example, `.user-page`, `.header-layout`, `.gallery-control`). -* All styles must have block class in condition. For example, - `.user-page .title`, `.user-page .edit`. So all you styles is protected - from accidentally same classes (it’s important, if you join all your CSS files - in one file). -* If block can be nested in another blocks (like common controls), adds prefix - to all its elements (like `.gallery-left`). If block is a root block - (like `.user-page` or `.header-layout`) be free to use short classes. -* Classes to modificate elements or blocks, must be like sentence without a noun - (for example, `.is-big`, `.with-header`). +## Blocks Communications -## JavaScript +Blocks should communicates by custom jQuery events. You can bind event listener +to block node by `"on events"` method: -* Unobtrusive JavaScript. -* Write animation and states in CSS. JavaScript just changes CSS classes. -* Avoid rendering. Send from server HTML, not JSON. -* Split JS by widgets. Describe widget class in `evil.block(selector, klass)`. - It will create `klass` instance and call `init` method for each selectors, - which exist in current page. So you can be free to join all JS files in one. -* Describe events by `EVENTS on SELECTORS` methods - (like `keyup submit on @name, @family`). This methods save this and - receive jQuery node in first argument and event in second. -* Bind JavaScript to `data-role` attribute to be free to change styles - and classes without dangeros of breaking scripts. -* Every tag with `data-role` will by as property in object with jQuery node. -* If you need to find elements inside block, use `@(selector)` function. -* If you need to communicate between blocks, use custom events and create - block events listeners by `on EVENTS` method. It will receive events object - as first argument and event parameters as next arguments. +```coffee +evil.block '@@slideshow', -> + nextSlide: -> … + 'on play': -> + @timer = setInterval(=> @nextSlide, 5000) + + 'on stop': -> + clearInterval(@timer) + +evil.block '@@video', -> + + 'click on @fullscreenButton': -> + $('@@slideshow').trigger('stop') +``` + +If you want to use broadcast messages, you can use custom events on body: + +```coffee +evil.block '@@callUs', -> + + 'change-city on body': (e, city) -> + @phoneNumber.text(city.phone) + +evil.block '@@cityChanger', -> + getCurrentCity: -> … + + 'change on @citySelect': -> + $('body').trigger('change-city', @getCurrentCity()) +``` + +## Rendering + +If you will render on client and on server-side, you must repeat helpers, i18n, +templates. Client rendering requires a lot of libraries and architecture. +2-way data binding looks cool, but has very [big price] in performance, +templates, animation and overengeniring. + +If you develop web page (not web application with offline support, etc), +server-side rendering will be more useful. Users will see your interface +imminently, search engines will index your content and your code will be much +simple and clear. + +In most of cases you can avoid client rendering. If you need to add some block +by JS, you can render it hidden to page HTML and show in right time: + +```coffee +evil.block '@@comment', + + 'click on @addCommentButton': -> + @newCommentForm.slideDown() +``` + +If user change some data and you need to update view, you anyway need to send +request to save new data on server. Just ask server to render new view. +For example, on new comment server can return new comment HTML: + +```coffee +evil.block '@@comment', + + 'submit on @addCommentForm': -> + $.post '/comments', @addCommentForm.serialize(), (newComment) -> + @comments.append(newComment) +``` + +But, of course, some cases require client rendering. Evil Blocks only recommend +to do it server-side, but not force you: + +```coffee +evil.block '@@comment', + + 'change, keyup on @commentField', -> + html = JST['comment'](text: @commentField.text()) + @preview.html(html) +``` + +[big price]: http://staal.io/blog/2014/02/05/2-way-data-binding-under-the-microscope/ + +## Modules + +If your blocks has same behavior, you can create module-block and set +multiple blocks on same tag: + +```haml +@popup@@closable + a@closeLink href="#" +``` + +```coffee +evil.block '@@closable', -> + + 'click on @closeLink': -> + @block.trigger('close') + +evil.block '@@popup', -> + + 'on close': -> + @clock.removeClass('is-open') +``` + +If you want to use same methods inside multiple block, you can create +inject-function: + +```coffee +fancybox = (obj) -> + for name, value of fancybox.module + obj[name] = value + # Initializer code + +fancybox.module = + openInFancybox: (node) -> + +evil.block '@@docs', + + init: -> + fancybox(@) + + 'click on @showExampleButton': -> + @openInFancybox(@example) +``` + +## Debug + +Evil Blocks contains debug extension, which log every events inside blocks. +To enable it, just load `evil-block.debug.js`. For example, in Rails: + +```haml +- if Rails.env.development? + = javascript_include_tag 'evil-block.debug' +``` + ## Install ### Ruby on Rails Add `evil-block-rails` gem to `Gemfile`: + ```ruby gem "evil-blocks-rails" ``` Load `evil-blocks.js` in your script: + ```js //= require evil-blocks ``` ### Ruby If you use Sinatra or other non-Rails framework you can add Evil Blocks path to Sprockets environment: + ```ruby EvilBlocks.install(sprockets) ``` -And change Slim options to support `@data-rule` shortcut: +And change Slim options to support `@@block` and `@rule` shortcuts: + ```ruby EvilBlocks.install_to_slim! ``` Then just load `evil-blocks.js` in your script: + ```js //= require evil-blocks ``` ### Others Add file `lib/evil-blocks.js` to your project. - -## See Also - -* [Evil Front](https://github.com/ai/evil-front) – pack of helpers and libraries - for Ruby on Rails and Evil Blocks.