darwinjs-rails 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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