darwinjs-rails 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in darwinjs-rails.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Olivier El Mekki
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Darwinjs
2
+
3
+ Darwin is a javascript framework for people that take error
4
+ handling seriously and want to achieve it through progressive
5
+ enhancement and graceful degradation.
6
+
7
+ Darwin will also let developer write clean and encapsulated
8
+ code that encourages self documentation.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'darwinjs-rails'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install darwinjs-rails
23
+
24
+ ## Getting Started
25
+
26
+ First, as one time configuration, add autoloader in your
27
+ application.coffee file :
28
+
29
+ ```
30
+ $(->
31
+ Darwin.Loader.run()
32
+ )
33
+ ```
34
+
35
+ You can now generate a javascript module using the provided
36
+ generator :
37
+
38
+ ```
39
+ $ rails generate darwin:assets users/index
40
+
41
+ create app/assets/javascripts/controllers/users.coffee
42
+ create app/assets/javascripts/views/users.coffee
43
+ create app/assets/javascripts/controllers/users/index.coffee
44
+ create app/assets/javascripts/views/users/index.coffee
45
+ ```
46
+
47
+ This will create your controller and your view in the `users` namespace.
48
+
49
+ Now add a `data-module` attribute in your users index view to
50
+ autoload your module :
51
+
52
+ ```erb
53
+ <div id="users" data-module="User.Index">
54
+ <ul>
55
+ <% @users.each do |user| %>
56
+ <%= render 'user', user: user %>
57
+ <% end %>
58
+ </ul>
59
+ </div>
60
+ ```
61
+
62
+ This will automatically initialize your module.
63
+
64
+ A module is composed of two files :
65
+
66
+ * a controller that handles events
67
+ * a view that handles DOM manipulation
68
+
69
+ Here is a typical view :
70
+
71
+ ```coffee
72
+ class App.Views.Users.Index extends Darwin.Controller
73
+ @options {
74
+ selectors:
75
+ show_users: 'a#show_users'
76
+ user_block: '#users'
77
+ user:
78
+ 'sel': '.user'
79
+ more: '.more a'
80
+ delete: 'a[data-method="delete"]'
81
+ }
82
+
83
+ show_info_for( $link ) ->
84
+ $link.next( '.info' ).show()
85
+
86
+ remove_user_for( $link ) ->
87
+ $link.parent().remove()
88
+ ```
89
+
90
+ And the corresponding controller :
91
+
92
+ ```coffee
93
+ class App.Controllers.Users.Index extends Darwin.Controller
94
+ @options {
95
+ View: App.Views.Users.Index
96
+
97
+ events:
98
+ 'Toggle user block': { el: 'show_users', type: 'click' }
99
+ 'Show user info': { el: 'user_more', type: 'click' }
100
+ 'Delete user on server': { el: 'user_delete', type: 'click' }
101
+ }
102
+
103
+
104
+ show_users_clicked: ->
105
+ @view.get( 'user_block' ).fadeIn()
106
+
107
+
108
+ user_more_clicked: ( $link ) ->
109
+ @view.show_info_for( $link )
110
+
111
+
112
+ user_delete_clicked: ( $link ) ->
113
+ if confirm( 'Really delete user ?' )
114
+ $.get( $link.attr( 'href' ), =>
115
+ @view.remove_user_for( $link )
116
+ )
117
+ ```
118
+
119
+ As you see, a view acts as single point of configuration for selectors.
120
+ Any change needed then reflect to the whole javascript codebase.
121
+
122
+ In the same way, controller acts as a single point of configuration
123
+ for events. You can tell what a module does looking at the first
124
+ lines of the controller file.
125
+
126
+ But there is more happening under the hood, here. First, all you DOM
127
+ elements retrieved by view selectors are cached. Upon further call
128
+ they are retrieved without hitting the DOM again, which is very
129
+ costly in term of performances.
130
+
131
+ Furthermore, all event callbacks are wrapped so that they do not
132
+ execute if an error occured. In case of error, events are simply
133
+ deactivated and any link is followed, reloading the page and letting
134
+ server side handle what has to be done, so your user doesn't even
135
+ notice something got wrong.
136
+
137
+ Ready for more ? See [introduction](doc/introduction.md).
138
+
139
+ ## Contributing
140
+
141
+ 1. Fork it
142
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
143
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
144
+ 4. Push to the branch (`git push origin my-new-feature`)
145
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,75 @@
1
+ # Documentation is in /doc/base.md
2
+ #
3
+ reserved_keywords = [ 'extended', 'included' ]
4
+
5
+ class Darwin.Base
6
+ @options: ( options ) ->
7
+ parent_options = @__super__.constructor._options
8
+
9
+ if parent_options?
10
+ @_options = $.extend true, {}, parent_options, options
11
+ else
12
+ @_options = options
13
+
14
+
15
+ @extend: ( obj ) ->
16
+ for key, value of obj when key not in reserved_keywords
17
+ @[ key ] = value
18
+
19
+ obj.extended?.apply(@)
20
+ @
21
+
22
+
23
+ @include: ( obj ) ->
24
+ for key, value of obj when key not in reserved_keywords
25
+ @::[ key ] = value
26
+
27
+ obj.included?.apply(@)
28
+ @
29
+
30
+
31
+ constructor: ( options ) ->
32
+ @_bound = {}
33
+
34
+ @options = $.extend( {}, @constructor._options, options )
35
+
36
+ if @options.dependencies?
37
+ for own name, dependency of @options.dependencies
38
+ @[ name ] = dependency
39
+
40
+
41
+ bind: ( event_name, callback ) ->
42
+ @_bound[ event_name ] ?= []
43
+ @_bound[ event_name ].push( callback )
44
+
45
+
46
+ on: ( event_name, callback ) ->
47
+ @bind( event_name, callback )
48
+
49
+
50
+ one: ( event_name, callback ) ->
51
+ callback = =>
52
+ @unbind event_name, callback
53
+
54
+ @bind event_name, callback
55
+
56
+
57
+ unbind: ( event_name, callback ) ->
58
+ if @_bound[ event_name ]
59
+ if callback
60
+ $.each @_bound[ event_name ], ( i, bound ) ->
61
+ if bound == callback
62
+ delete @_bound[ event_name ][ i ]
63
+
64
+ else
65
+ delete @_bound[ event_name ]
66
+
67
+
68
+ unbind_all: ->
69
+ @_bound = {}
70
+
71
+
72
+ trigger: ( event_name, params... ) ->
73
+ if @_bound[ event_name ]
74
+ for own callback in @_bound[ event_name ]
75
+ callback( params... )
@@ -0,0 +1,125 @@
1
+ # Documentation is in /doc/controller.md
2
+ #
3
+ class Darwin.Controller extends Darwin.Base
4
+ @options {
5
+ events: {}
6
+ View: Darwin.View
7
+ failsafe: true
8
+ }
9
+
10
+
11
+ constructor: ( root, options ) ->
12
+ super options
13
+
14
+ @root = root.get(0)
15
+
16
+ if @root
17
+ @_dom_bound = []
18
+ @$root = $( @root )
19
+ @view = new @options.View @$root
20
+
21
+ @bind_dom_events() unless exports?
22
+ @view.run()
23
+ @run() unless exports?
24
+ else
25
+ throw new Error( 'Controller initialized without any element' )
26
+
27
+
28
+ run: ->
29
+
30
+
31
+ bind_dom_events: ->
32
+ @bind_dom_event( event, name ) for own name, event of @options.events
33
+
34
+
35
+ bind_dom_event: ( definition, name ) ->
36
+ unless definition.el?
37
+ throw new Error( "No el key for event : #{name or 'manually bound event'}" )
38
+
39
+ unless definition.type?
40
+ throw new Error( "No type key for event : #{name or 'manually bound event'}" )
41
+
42
+
43
+ wrap = ( callback, stop ) =>
44
+ ( event ) =>
45
+ unless ( window.crashed and @options.failsafe is true )
46
+ event.preventDefault() if stop
47
+ $target = $( event.target )
48
+
49
+ if definition.ensure_element isnt false
50
+ el = if definition.delegate? then definition.delegate else definition.el
51
+
52
+ if el == 'root'
53
+ sel = 'root'
54
+ else
55
+ sel = ( @view.selectors[ el ] or @view._find_alternate_name( el ) ).sel
56
+
57
+ $target = $target.parents( sel ).first() unless $target.is( sel )
58
+
59
+ callback( $target, event )
60
+
61
+ definition.stop = true if definition.type == 'click' and ! definition.stop?
62
+
63
+ switch true
64
+ when !! definition.controller_method
65
+ method_name = definition.controller_method
66
+ if @[ method_name ]
67
+ method = $.proxy( @[ method_name ], this )
68
+ else
69
+ throw new Error( "Undefined method for controller : #{method_name}" )
70
+
71
+ when !! definition.view_method
72
+ method_name = definition.view_method
73
+ if @view[ method_name ]
74
+ method = $.proxy( @view[ method_name ], @view )
75
+ else
76
+ throw new Error( "Undefined method for view : #{method_name}" )
77
+
78
+ else
79
+ method_name = "#{definition.delegate or definition.el}_#{definition.type}#{if definition.type.match( /e$/ ) then 'd' else 'ed' }"
80
+ if @[ method_name ]
81
+ method = $.proxy( @[ method_name ], this )
82
+ else
83
+ throw new Error( "Undefined method for controller : #{method_name}" )
84
+
85
+ $element = @view.get( definition.el )
86
+
87
+
88
+ if definition.delegate
89
+ delegate_to = @view.selectors[ definition.delegate ] or @view._find_alternate_name( definition.delegate )
90
+
91
+ if definition.cancel_delay and definition.cancel_delay > 0
92
+ callback = ( event ) =>
93
+ window.clearTimeout @[ '_' + method_name + '_timeout' ]
94
+ wrapped = wrap( method, definition.stop )
95
+ @[ '_' + method_name + '_timeout' ] = window.setTimeout( ( -> ( wrapped event ) ), definition.cancel_delay )
96
+ else
97
+ callback = wrap method, definition.stop
98
+
99
+ throw new Error "Selector not found : #{definition.delegate}" unless delegate_to
100
+ $element.delegate delegate_to.sel, definition.type, callback
101
+ @_dom_bound.push { el: $element, delegate: delegate_to.sel, type: definition.type, callback: callback }
102
+ else
103
+ if definition.cancel_delay and definition.cancel_delay > 0
104
+ callback = ( event ) =>
105
+ window.clearTimeout @[ '_' + method_name + '_timeout' ]
106
+ wrapped = wrap( method, definition.stop )
107
+ @[ '_' + method_name + '_timeout' ] = window.setTimeout( ( -> ( wrapped event ) ), definition.cancel_delay )
108
+ else
109
+ callback = wrap method, definition.stop
110
+
111
+ $element.bind definition.type, callback
112
+ @_dom_bound.push { el: $element, type: definition.type, callback: callback }
113
+
114
+
115
+ destructor: ->
116
+ @view._destructor()
117
+ for bound in @_dom_bound
118
+ if bound.delegate
119
+ bound.el.undelegate bound.delegate, bound.type, bound.callback
120
+ else
121
+ bound.el.unbind bound.type, bound.callback
122
+
123
+ delete Darwin.Loader.controllers()[ @id ]
124
+
125
+
@@ -0,0 +1,54 @@
1
+ # Documentation is in /doc/loader.md
2
+ #
3
+ controllers = {}
4
+ errors_got = 0
5
+
6
+ loader = Darwin.Loader =
7
+ run: ->
8
+ loader.module_roots().each( ( i, $module ) =>
9
+ $module = $( $module )
10
+ module_name = loader.compute_name( $module.attr( 'data-module' ) )
11
+ path = $module.attr( 'data-module' ).split( '.' )
12
+ module = App.Controllers
13
+ module = module[ path.shift() ] while path.length
14
+
15
+ if module
16
+ controllers[ module_name ] = new module( $module )
17
+ controllers[ module_name ].id = module_name
18
+ else
19
+ throw new Error( "Can't find module #{$module.attr( 'data-module' )}" )
20
+ )
21
+
22
+
23
+ module_roots: ->
24
+ $( '*[data-module]' )
25
+
26
+
27
+ compute_name: ( module_path ) ->
28
+ name = module_path.replace( /\./g, '_' ).toLowerCase()
29
+
30
+ if controllers[ name ]
31
+ i = 1
32
+
33
+ for own controller_name, controller of controllers
34
+ i++ if controller_name.indexOf( name ) isnt -1
35
+
36
+ name = "#{name}_#{i}"
37
+
38
+ name
39
+
40
+ controllers: ->
41
+ controllers
42
+
43
+ window.onerror = ( error, url, lineno ) =>
44
+ if url && url.match( /https?:\/\/.*?assets/ )
45
+ @crashed = true
46
+ console?.log( "Error on #{url}, line #{lineno}" )
47
+ errors_got += 1
48
+
49
+ if window.js_exception_url and errors_got <= 5
50
+ $.post( window.js_exception_url, js_error: { error: error, url: url, lineno: lineno, page_url: window.location.href } )
51
+
52
+ for own controller_name, controller of controllers
53
+ controller.destructor() if controller.options.failsafe is true
54
+ error
@@ -0,0 +1,43 @@
1
+ #= require './base'
2
+
3
+ class Darwin.Template extends Darwin.Base
4
+ @_cached: {}
5
+
6
+ @options {
7
+ dependencies:
8
+ Mustache: window.Mustache
9
+ }
10
+
11
+ constructor: ( @template ) ->
12
+ super
13
+
14
+ @retrieve_template_from_dom() unless @retrieve_template_from_memory()
15
+
16
+
17
+ retrieve_template_from_memory: ->
18
+ if Templates?[ @template ]
19
+ @template = Templates[ @template ]
20
+ true
21
+ else
22
+ false
23
+
24
+ retrieve_template_from_dom: ->
25
+ name = @template.replace( '#', '' )
26
+
27
+ if Darwin.Template._cached[ name ]
28
+ @template = Darwin.Template._cached[ name ]
29
+ else
30
+ $template = $( "script##{name}_template[type=\"text/mustache\"]" )
31
+
32
+ unless $template.length
33
+ throw new Error( "can't find template #{name}" )
34
+
35
+ @template = $template.html()
36
+ Darwin.Template._cached[ name ] = @template
37
+
38
+
39
+ render: ( data ) ->
40
+ $( @render_to_string(data ) )
41
+
42
+ render_to_string: (data) ->
43
+ Mustache.render( @template, data )
@@ -0,0 +1,108 @@
1
+ # Documentation is in /doc/view.md
2
+ #
3
+ class Darwin.View extends Darwin.Base
4
+ _cached: {}
5
+
6
+ constructor: ( @$root, options ) ->
7
+ super options
8
+ @_flatten_selectors()
9
+ @$root.find( '.fallback-submits' ).hide()
10
+
11
+
12
+ run: ->
13
+
14
+
15
+ get: ( selector_name ) ->
16
+ if @[ "get_#{selector_name}" ]
17
+ @[ "get_#{selector_name}" ]()
18
+ else
19
+ @[ "$#{selector_name}" ] or @_find_element( selector_name )
20
+
21
+
22
+ _destructor: ->
23
+ @$root.find( '.fallback-submits' ).show()
24
+ @destructor()
25
+
26
+
27
+ destructor: ->
28
+
29
+
30
+ clear_cache: ( selector_name ) ->
31
+ if selector_name
32
+ delete @[ "$#{selector_name}" ]
33
+ else
34
+ @clear_cache( selector_name ) for own selector_name, _ of @_cached
35
+ @_cached = {}
36
+
37
+
38
+ _find_element: ( selector_name ) ->
39
+ definition = @selectors[ selector_name ] or @_find_alternate_name( selector_name )
40
+ if definition
41
+ $base = if definition.within? then @get( definition.within ) else @$root
42
+ $found = $base.find definition.sel
43
+
44
+ unless definition.cache is false
45
+ @[ "$#{selector_name}" ] = $found
46
+ @_cached[ selector_name ] = true
47
+
48
+ $found
49
+ else
50
+ $element = @$root.find( "##{selector_name}" )
51
+ if $element.get(0)
52
+ @[ "$#{selector_name}" ] = $element
53
+ @_cached[ selector_name ] = true
54
+ $element
55
+ else
56
+ throw new Error "Selector not found : #{selector_name}"
57
+
58
+
59
+ _flatten_selectors: ->
60
+ selectors = {}
61
+ walk = ( values, name, parent_names ) ->
62
+ selector_key = ''
63
+
64
+ if name
65
+ if parent_names
66
+ selector_key += "#{parent_name.short}_" for parent_name in parent_names
67
+
68
+ selector_key += name
69
+ selectors[ selector_key ] = {}
70
+
71
+ if typeof values == 'string'
72
+ selectors[ selector_key ] = { sel: values }
73
+ else
74
+
75
+ for own attr, value of values
76
+ if $.inArray( attr, [ 'cache', 'sel', 'within' ] ) isnt -1
77
+ selectors[ selector_key ][ attr ] = value
78
+ else
79
+ parents = $.merge [], ( parent_names or [] )
80
+ parents.push({ short: name, long: selector_key }) if name
81
+ walk value, attr, parents
82
+
83
+ if name and not values.sel
84
+ selectors[ selector_key ].sel = "##{name}"
85
+
86
+ if parent_names and parent_names.length
87
+ selectors[ selector_key ].within = parent_names[ parent_names.length - 1 ].long
88
+
89
+ selectors[ selector_key ].alternate_name = name if name
90
+
91
+ walk @options.selectors
92
+ @selectors = selectors
93
+
94
+
95
+ _find_alternate_name: ( name ) ->
96
+ definitions = []
97
+
98
+ for selector, definition of @selectors
99
+ definitions.push( definition ) if definition.alternate_name == name
100
+
101
+ if definitions.length
102
+ if definitions.length > 1
103
+ throw new Error "Multiple definitions for #{name}"
104
+ else
105
+ definitions[0]
106
+ else
107
+ null
108
+
@@ -0,0 +1,16 @@
1
+ #= require_self
2
+ #= require './darwin/base'
3
+ #= require './darwin/loader'
4
+ #= require './darwin/template'
5
+ #= require './darwin/view'
6
+ #= require './darwin/controller'
7
+ #
8
+ # Documentation is in /doc/introduction.md
9
+ #
10
+
11
+ window.Darwin = {}
12
+
13
+ window.App =
14
+ Views: {}
15
+ Controllers: {}
16
+ Helpers: {}
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'darwinjs/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "darwinjs-rails"
8
+ spec.version = Darwinjs::Rails::VERSION
9
+ spec.authors = ["Olivier El Mekki"]
10
+ spec.email = ["olivier@el-mekki.com"]
11
+ spec.description = %q{Javascript framework with progressive enhancement in mind.}
12
+ spec.summary = %q{Darwin lets create complex javascript interfaces that do not expect they own the application and that degrade gracefully when an error occurs}
13
+ spec.homepage = "https://github.com/oelmekki/darwinjs-rails"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'railties', '~> 3.2'
22
+ spec.add_dependency 'coffee-rails', '~> 3.2'
23
+ spec.add_dependency 'jquery-rails', '~> 2.2'
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ end