Skip to content

Commit

Permalink
better widget client side infra
Browse files Browse the repository at this point in the history
  • Loading branch information
kristianmandrup committed Sep 6, 2012
1 parent 2b3f2ef commit ff6d7ca
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 73 deletions.
153 changes: 97 additions & 56 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ Extras (jQuery only):

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

Example usage:

Expand Down Expand Up @@ -237,38 +236,56 @@ The first argument `id` is always optional. It is meant to be used to select the

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.
A non-intrusive approach could involve the use of:

* widget_class_call(id, function, hash)
* widget_call(id, function, hash)

Example:

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

This will result in the call:
Will call a function on the widget class (or module):

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

This ensures that all our javascript for a widget is namespace contained nicely.
`widget_call 'Admin::TopBar', :toggle_active, {item: 'item:first'}`

Will call a function on a particular instance of that widget!

```javascript
Widgets.admin.topBar.toggleActive({'item': 'item:first'});
```

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

In your `application.js` manifest file

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

An alternative `apotomo/Namespace.js` is also available, which you might want to experiment with also.

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

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

Widget.TopBar = {}
Widget.TopBar = {
// add widget functions here
foo: function(bar) {},

bar: function(foor) {}
}
```

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

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

Widget.TopBar = {
update: function(item) {
Expand All @@ -286,83 +303,107 @@ Widget.TopBar = {
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
```javascript
$.extend(Widget.admin.TopBar, Widget.TopBar);
```

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

Also see the Coffeescript section below to see how to use namespaces.

== 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)
and [namespaces](http://spin.atomicobject.com/2011/04/01/namespace-a-coffeescript-nugget/).

Please also see:

* [namespaced classes](http://stackoverflow.com/questions/8730859/classes-within-coffeescript-namespace)
* [namespace.coffee](https://github.com/CodeCatalyst/namespace.coffee)
* [oop coffee](http://www.gridlinked.info/oop-with-coffeescript-javascript/)

Here an appetizer :) replace animals with widgets and you get the idea...

```coffeescript
namespace "samples.coffeescript.oop.abstract"
Animal :
class Animal
constructor : (@species, @isMammal=false) ->
return this

# NOTE: Animal must be defined BEFORE Dog
#
namespace "samples.coffeescript.oop.pets"
Dog :
class Dog extends samples.coffeescript.oop.abstract.Animal
constructor : (@name) ->
```

You should first require `namespaces`:

```
//= require apotomo/namespaces.min
```

When you generate a widget, the coffescript file for it should look like this:

`rails g apotomo:widget TopBar show -e haml`

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

toggleActive: (options) ->
```

```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'
namespace "Widget"
TopBar :
class TopBar
constructor : (@widget_id = widget_id) ->

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

`rails g apotomo:widget TopBar::SuperThang show -e haml`

```coffeescript
firstWidget = new Widget.TopBar.MyFirstWidget
firstWidget.myFunc
namespace "Widget.topBar"
SuperThang :
class SuperThang extends Widget.TopBar
constructor : (@widget_id = widget_id) ->

createBottomBar : (widget_id) -> new BottomBar(widget_id)
createFooter : (widget_id) -> new Footer(widget_id)

# private variable declarations (aliases)
#
BottomBar = Widget.BottomBar
Footer = Widget.Footer
```

=== javascript Widget instance
=== Browser Widget instances

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"
Widgets.thang = new Widget.topBar.SuperThang
``

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'
call_widget :thang, :toggleActive, item: ".item:first"
```

Which will result in the statement:

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

An even better way...

See [namespace.coffee](https://github.com/CodeCatalyst/namespace.coffee) for a good namespacing/package solution for coffee classes, as described in [oop coffee](http://www.gridlinked.info/oop-with-coffeescript-javascript/)

```coffeescript
namespace "samples.coffeescript.oop"
PetMaker :
class PetMaker
constructor : ->

createDog : (name) -> new Dog(name)
createCat : (name) -> new Cat(name)
createBird: (name) -> new Bird(name)

# private variable declarations (aliases)
#
Dog = samples.coffeescript.oop.pets.Dog
Cat = samples.coffeescript.oop.pets.Cat
Bird= samples.coffeescript.oop.pets.Bird
Widgets.thang.toggleActive('action': ".item:first");
```

== Testing
Expand Down
19 changes: 10 additions & 9 deletions lib/apotomo/javascript_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,24 @@ def selector_for var, id, selector
end

# call existing widget
# - call_fun :update, :top_bar, item: 1
# - widget_class_call :top_bar, :update, 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});"
def widget_class_call id, function, hash
function_name = jq_helper.js_camelize function
widget_name = jq_helper.ns_name id.to_s.camelize
namespace = "Widget.#{widget_name}"
"#{namespace}.#{function_name}(#{hash.to_json});"
end

# call existing widget
# - call_widget :top_bar, :flash_light, action: 'search'
# - widget_call :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});"
def widget_call id, function, hash
function_name = jq_helper.js_camelize function
"Widgets.#{id}.#{function_name}(#{hash.to_json});"
end

[:replace_all, :prepend_to, :append_to].each do |name|
Expand Down
7 changes: 7 additions & 0 deletions lib/apotomo/javascript_generator/jquery_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def js_action action
".#{action};"
end

def ns_name class_name
names = class_name.split('::')
ns = names[0..-2].map {|name| js_camelize name }.join('.')
return names.last if ns.blank?
ns << ".#{names.last}"
end

def js_camelize str
str = str.to_s
str.camelize.sub(/^\w/, str[0].downcase)
Expand Down
14 changes: 13 additions & 1 deletion lib/generators/apotomo/widget_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,24 @@ def create_stylesheet_file
end

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

protected

def ns_name
names = class_name.split('::')
ns = names[0..-2].map {|name| js_camelize name }.join('.')
return names.last if ns.blank?
ns << ".#{names.last}"
end

def js_camelize str
str = str.to_s
str.camelize.sub(/^\w/, str[0].downcase)
end

def javascript?
options[:js]
end
Expand Down
4 changes: 0 additions & 4 deletions lib/generators/templates/widget.coffee

This file was deleted.

4 changes: 2 additions & 2 deletions lib/generators/templates/widget.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var Widget.<%= class_name %> = Namespace('Apotomo.Widget.<%= class_name %>');
var Widget.<%= ns_name %> = namespace('Widget.<%= ns_name %>');

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

Expand Down
8 changes: 8 additions & 0 deletions lib/generators/templates/widget.js.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Define your coffeescript code for the <%= class_name %> widget
namespace "Widget.<%= ns_name %>"
<%= class_name %>:
class <%= class_name %>
constructor : (@widget_id = widget_id) ->

# add custom widget function here
toggleActive: (options) ->
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Script: Namespace.js
Copyright:
Copyright (c) 2009 Maxime Bouroumeau-Fuseau
https://github.com/maximebf/Namespace.js
License:
MIT-style license.
Expand Down Expand Up @@ -477,7 +479,7 @@ Namespace.baseUri = './';
Namespace.autoInclude = true;

// for widget namespaces
var Widget = Namespace('Apotomo.Widget');
var widget = Namespace('widget');

// for widget instances
var Widgets = {};
Expand Down
23 changes: 23 additions & 0 deletions vendor/assets/javascripts/apotomo/namespaces.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# [namespace.coffee](http://github.com/CodeCatalyst/namespace.coffee) v1.0.1
# Copyright (c) 2011-2012 [CodeCatalyst, LLC](http://www.codecatalyst.com/).
# Open source under the [MIT License](http://en.wikipedia.org/wiki/MIT_License).

# A lean namespace implementation for JavaScript written in [CoffeeScript](http://coffeescript.com/).

# *Export the specified value(s) to the specified package name.*
namespace = ( name, values ) ->
# Export to `exports` for node.js or `window` for the browser.
target = exports ? window
# Nested packages may be specified using dot-notation, and are automatically created as needed.
if name.length > 0
target = target[ subpackage ] ||= {} for subpackage in name.split( '.' )
# Export each value in the specified values Object to the specified package name by the value's key.
target[ key ] = value for key, value of values
# Return a reference to the specified namespace.
return target

# *Export the namespace function to global scope, using itself.*
namespace( '', namespace: namespace )

namespace "Widget"
namespace "Widgets"
Loading

0 comments on commit ff6d7ca

Please sign in to comment.