Skip to content

Commit

Permalink
all specs pass - much better coffe/js integration using js/coffee nam…
Browse files Browse the repository at this point in the history
…espaces and classes for widgets :)
  • Loading branch information
kristianmandrup committed Sep 6, 2012
1 parent b17ec08 commit 5432319
Show file tree
Hide file tree
Showing 14 changed files with 690 additions and 24 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -4,3 +4,4 @@ pkg/*
test/dummy/log/* test/dummy/log/*
test/dummy/tmp/* test/dummy/tmp/*
Gemfile.lock Gemfile.lock
.DS_Store
172 changes: 152 additions & 20 deletions README.rdoc
Expand Up @@ -42,17 +42,36 @@ Let's wrap that comments block in a widget.


== Generate == Generate


Go and generate a widget stub. $ rails g apotomo:widget comments show -e haml

invoke haml
$ rails generate apotomo:widget Comments display write -e haml create app/widgets/comments/views/show.html.haml
create app/cells/ invoke rspec

This comment has been minimized.

Copy link
@kuraga

kuraga Feb 24, 2013

I don't think that it's right to show rspec situation... Because rspec usage is a custom situation and dependences on rspec-apotomo gem? Yes?
P.S. Maybe haml too... But it's easy to use haml syntax in examples and Nick always did that... :)

create app/cells/comments create spec/widgets/comments/comments_widget_spec.rb
create app/cells/comments/comments_widget.rb create app/widgets/comments/comments_widget.rb
create app/cells/comments/views/display.html.haml create app/assets/stylesheets/widgets/comments_widget.css
create app/cells/comments/views/write.html.haml create app/assets/javascripts/widgets/comments_widget.coffee
create test/widgets/comments/comments_widget_test.rb

Go and generate a widget TopBar stub.
Nothing special.
$ rails g apotomo:widget TopBar show -e haml
invoke haml
create app/widgets/top_bar/views/show.html.haml
invoke rspec
create spec/widgets/top_bar/top_bar_widget_spec.rb
create app/widgets/top_bar/top_bar_widget.rb
create app/assets/stylesheets/widgets/top_bar_widget.css
create app/assets/javascripts/widgets/top_bar_widget.coffee

And a Form widget within the TopBar namespace

$ rails g apotomo:widget TopBar::Form show -e haml
invoke haml
create app/widgets/top_bar/form/views/show.html.haml
invoke rspec
create spec/widgets/top_bar/form/form_widget_spec.rb
create app/widgets/top_bar/form/form_widget.rb
create app/assets/javascripts/widgets/top_bar/form_widget.coffee
create app/assets/stylesheets/widgets/top_bar/form_widget.css


== Plug it in == Plug it in


Expand Down Expand Up @@ -86,28 +105,28 @@ A widget is like a cell which is like a mini-controller.
class CommentsWidget < Apotomo::Widget class CommentsWidget < Apotomo::Widget
responds_to_event :post responds_to_event :post


def display(args) def show(args)
@comments = args[:post].comments # the parameter from outside. @comments = args[:post].comments # the parameter from outside.
render render
end end


Having +display+ as the default state when rendering, this method collects comments to show and renders its view. Having +show+ as the default state when rendering, this method collects comments to show and renders its view.


And look at line 2 - if encountering a <tt>:post</tt> event we invoke +#post+, which is simply another state. How cool is that? And look at line 2 - if encountering a <tt>:post</tt> event we invoke +#post+, which is simply another state. How cool is that?


def post(evt) def post(evt)
@comment = Comment.new(:post_id => evt[:post_id]) @comment = Comment.new(:post_id => evt[:post_id])
@comment.update_attributes evt[:comment] # a bit like params[]. @comment.update_attributes evt[:comment] # a bit like params[].


update :state => :display update :state => :show
end end
end end




The event is processed with three steps in our widget: The event is processed with three steps in our widget:


* create the new comment * create the new comment
* re-render the +display+ state * re-render the +show+ state
* update itself on the page * update itself on the page


Apotomo helps you focusing on your app and takes away the pain of <b>action dispatching</b> and <b>page updating</b>. Apotomo helps you focusing on your app and takes away the pain of <b>action dispatching</b> and <b>page updating</b>.
Expand All @@ -116,7 +135,7 @@ Apotomo helps you focusing on your app and takes away the pain of <b>action disp


So how and where is the <tt>:post</tt> event triggered? So how and where is the <tt>:post</tt> event triggered?


Take a look at the widget's view <tt>display.html.haml</tt>. Take a look at the widget's view <tt>show.html.haml</tt>.
= widget_div do = widget_div do
%ul %ul
- for c in @comments - for c in @comments
Expand Down Expand Up @@ -172,6 +191,7 @@ Extras (jQuery only):


* find_element(id, selector) * find_element(id, selector)
* selector_for(var, id, selector) * selector_for(var, id, selector)
* call_fun(name, id, hash)


Example usage: Example usage:


Expand All @@ -182,13 +202,13 @@ render js: top_item + append_to(:_top_item, markup)
Will select `.item:first` under the widget container element as a variable `_apo_top_item` and then append the markup to the DOM element(s) pointed to by that variable. Will select `.item:first` under the widget container element as a variable `_apo_top_item` and then append the markup to the DOM element(s) pointed to by that variable.
``` ```


Inverse jQuery actions Inverse jQuery manipulation API


* append_to(selector, markup) * append_to(selector, markup)
* prepend_to(selector, markup) * prepend_to(selector, markup)
* replace_all(selector, markup) * replace_all(selector, markup)


Normal jQuery action Normal jQuery manipulation API


* update_text(id, selector, markup) * update_text(id, selector, markup)
* append(id, selector, markup) * append(id, selector, markup)
Expand All @@ -204,13 +224,125 @@ Normal jQuery action
* add_class(id, selector, *classes) * add_class(id, selector, *classes)
* toggle_class(id, selector, *classes) * toggle_class(id, selector, *classes)
* toggle_class_fun(id, selector, fun) * toggle_class_fun(id, selector, fun)
* empty(id, selector)

jQuery "get" functions

* get_attr(id, selector, name) * get_attr(id, selector, name)
* get_prop(id, selector, name) * get_prop(id, selector, name)
* get_val(id, selector) * get_val(id, selector)
* get_html(id, selector) * get_html(id, selector)
* empty(id, selector)


Note: The first argument id is always optional The first argument `id` is always optional. It is meant to be used to select the div for the widget. The `selector` then finds one or more elements within the widget to perform the action on.

These functions should be used sparingly, for _Proof of Concept_ only if possible. It is far better to have javascript asset files if possible (== non-intrusive javascript).

A non-intrusive approach could involve the use of the `call_fun` method which takes the `name` of the javascript method to call, the widget `id` and a hash of data.

Example:

`call_fun :toggle_active, 'TopBar', {item: 'item:first'}`

This will result in the call:

```javascript
Widget.TopBar.toggleActive({'item': 'item:first'});
```

This ensures that all our javascript for a widget is namespace contained nicely.

In your `application.js` manifest file

```
//= require apotomo/Namespace
```

In the `top_bar.coffee` file the following is generated for our convenience

```javascript
Widget.TopBar = Namespace('Apotomo.Widget.TopBar');

Widget.TopBar = {}
```

We can then implement clean non-intrusive, namespaced javascript functionality as follows:

```javascript
Widget.TopBar = Namespace('Apotomo.Widget.TopBar');

Widget.TopBar = {
update: function(item) {
'updated:' + item }
},

toggleActive: function(widget_id, options) {
item = options['item'];
// do some toggle magic!!!
$(widget_id).find(item).toggleClass('active');
}
}
```

Note that you can use fx jQuery [extend](http://api.jquery.com/jQuery.extend/) to extend javascript widget functionality similar to modules (prototypical inheritance).
There are many other powerful javascript libraries out there (fx Base2, Prototype.js, JS.Class) that can be used to great effect for OOP javascript with inheritance etc.

```coffeescript
$.extend Widget.Admin.TopBar, Widget.TopBar
```

Here we extended the `Admin.TopBar` with base functionality from `TopBar` :)

== Using Coffeescript

Coffeescript has built in [class structure](http://coffeescript.org/#classes).
and [namespaces](http://spin.atomicobject.com/2011/04/01/namespace-a-coffeescript-nugget/). Also see [namespaced classes](http://stackoverflow.com/questions/8730859/classes-within-coffeescript-namespace)

```coffeescript
namespace "Widget.TopBar", (exports) ->
# add functions or classes here
bar: (foo) ->
```

```coffeescript
class MyFirstWidget
constructor: (options = {}) ->
@options = options
@name = options.name
myFunc: () ->
console.log 'works'

namespace "Widget.TopBar", (exports) ->
exports.MyFirstWidget = MyFirstWidget

toggleActive: (widget_id, options) ->
item = options.item
$(widget_id).find(item).toggleClass 'active'
```

```coffeescript
firstWidget = new Widget.TopBar.MyFirstWidget
firstWidget.myFunc
```

=== javascript Widget instance

For an Ajax enabled page, you can create javascript widget instances in the `Widgets` namespace.

```coffeescript
Widgets.firstWidget = new Widget.TopBar.MyFirstWidget name: "cool widget"
``

Then you can update an existing widget instance from your Widget (controller) using `call_widget` as follows:

```ruby
call_widget :firstWidget, :flash_light, action: 'search'
```

Which will result in the statement:

```javascript
Widgets.firstWidget.flashLight('action': 'search');
```


== Testing == Testing


Expand Down
7 changes: 6 additions & 1 deletion lib/apotomo/cell/rendering.rb
@@ -1,7 +1,12 @@
module Cell module Cell
module Rendering module Rendering
def render(*args) def render(*args)
view_name = File.join('views', self.action_name) if args.first.kind_of?(Hash) && args.first[:view]
hash = args.first
hash[:view] = File.join('views', hash[:view].to_s)
args = [hash, args[1..-1]]
end
view_name = File.join('views', self.action_name || '')
render_view_for(view_name, *args) render_view_for(view_name, *args)
end end
end end
Expand Down
21 changes: 21 additions & 0 deletions lib/apotomo/javascript_generator.rb
@@ -1,4 +1,5 @@
require 'action_view/helpers/javascript_helper' require 'action_view/helpers/javascript_helper'
require "active_support/core_ext" # for Hash.to_json etc.


module Apotomo module Apotomo
class JavascriptGenerator class JavascriptGenerator
Expand Down Expand Up @@ -67,6 +68,26 @@ def selector_for var, id, selector
"var _apo_#{var} = " + element("##{id}") + ".find(\"#{selector}\");" "var _apo_#{var} = " + element("##{id}") + ".find(\"#{selector}\");"
end end


# call existing widget
# - call_fun :update, :top_bar, item: 1

# --> Widget.TopBar.update('action': 1)
def call_fun name, id, hash
function_name = jq_helper.js_camelize name
namespace = "Widget.#{id.to_s.camelize}"
"#{namespace}.#{function_name}(\"##{id}\", #{hash.to_json});"
end

# call existing widget
# - call_widget :top_bar, :flash_light, action: 'search'

# --> Widgets.topBar.flashLight('action': 'search')

def call_widget name, fun, hash
function_name = jq_helper.js_camelize name
"#{name}.#{function_name}(#{hash.to_json});"
end

[:replace_all, :prepend_to, :append_to].each do |name| [:replace_all, :prepend_to, :append_to].each do |name|
define_method name do |selector, markup| define_method name do |selector, markup|
jq_helper.inv_markup_action selector, markup, name.to_sym jq_helper.inv_markup_action selector, markup, name.to_sym
Expand Down
17 changes: 14 additions & 3 deletions lib/generators/apotomo/widget_generator.rb
Expand Up @@ -9,7 +9,6 @@ def base_path
File.join('app/widgets', class_path, file_name) File.join('app/widgets', class_path, file_name)
end end



def js_path def js_path
File.join('app/assets/javascripts/widgets', class_path, file_name) File.join('app/assets/javascripts/widgets', class_path, file_name)
end end
Expand Down Expand Up @@ -39,14 +38,26 @@ class WidgetGenerator < ::Cells::Generators::Base


check_class_collision :suffix => "Widget" check_class_collision :suffix => "Widget"


class_option :js, :type => :boolean, :default => false, :desc => 'Generate javascript asset file'

def create_cell_file def create_cell_file
template 'widget.rb', File.join(base_path, "#{file_name}_widget.rb") template 'widget.rb', File.join(base_path, "#{file_name}_widget.rb")
end end


def create_assets_files def create_stylesheet_file
template 'widget.coffee', "#{js_path}_widget.coffee"
template 'widget.css', "#{css_path}_widget.css" template 'widget.css', "#{css_path}_widget.css"
end end

def creates_script_file
return template 'widget.coffee', "#{js_path}_widget.coffee" if !javascript?
template 'widget.js', "#{js_path}_widget.js"
end

protected

def javascript?
options[:js]
end
end end
end end
end end
3 changes: 3 additions & 0 deletions lib/generators/templates/widget.coffee
@@ -1 +1,4 @@
# Define your coffeescript code for the <%= class_name %> widget # Define your coffeescript code for the <%= class_name %> widget
namespace "Widget.TopBar", (exports) ->
# add functions or classes here
bar: (foo) ->
9 changes: 9 additions & 0 deletions lib/generators/templates/widget.js
@@ -0,0 +1,9 @@
var Widget.<%= class_name %> = Namespace('Apotomo.Widget.<%= class_name %>');

Widget.<%= class_name %> = {
// add widget functions here
foo: function(bar) {},

bar: function(foor) {}
}

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

1 comment on commit 5432319

@kuraga
Copy link

@kuraga kuraga commented on 5432319 Feb 24, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attention!!! It seems that incompatibilities have been introduced here!

Please sign in to comment.