opalla 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +394 -14
- data/lib/opalla.rb +11 -4
- data/lib/opalla/component_helper.rb +28 -0
- data/lib/opalla/controller_add_on.rb +11 -0
- data/lib/opalla/engine.rb +27 -0
- data/lib/opalla/middleware.rb +20 -0
- data/lib/opalla/util.rb +63 -0
- data/lib/opalla/version.rb +1 -1
- data/lib/rails/generators/opalla/assets_generator.rb +23 -0
- data/lib/rails/generators/opalla/collection_generator.rb +19 -0
- data/lib/rails/generators/opalla/component_generator.rb +27 -0
- data/lib/rails/generators/opalla/install_generator.rb +62 -0
- data/lib/rails/generators/opalla/model_generator.rb +19 -0
- data/opal/collection.rb +71 -0
- data/opal/component.rb +136 -0
- data/opal/controller.rb +60 -0
- data/opal/diffDOM.js +1371 -0
- data/opal/diff_dom.rb +26 -0
- data/opal/element.rb +17 -0
- data/opal/hex_random.rb +12 -0
- data/opal/model.rb +50 -0
- data/opal/opalla.rb +21 -0
- data/opal/router.rb +66 -0
- data/opal/sha1.js +366 -0
- data/opal/view_helper.rb +168 -0
- data/opalla.gemspec +8 -0
- data/opalla.gif +0 -0
- metadata +109 -2
@@ -0,0 +1,28 @@
|
|
1
|
+
module Opalla
|
2
|
+
module ComponentHelper
|
3
|
+
def component(name, id: nil, model: nil, collection: nil)
|
4
|
+
comp_id = (id || "#{name}-#{cidn_and_increment}")
|
5
|
+
html = component_html(name, id: id, model: model, collection: collection)
|
6
|
+
output = Nokogiri::HTML.fragment(html).children.attr(id: comp_id).to_s
|
7
|
+
output.html_safe
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def component_html(name, id: nil, model: nil, collection: nil)
|
13
|
+
options = { partial: "components/#{name}", locals: {} }
|
14
|
+
model.nil? || options[:locals][:model] = model
|
15
|
+
collection.nil? || options[:locals][:collection] = collection
|
16
|
+
render(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def cidn
|
20
|
+
@cidn ||= 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def cidn_and_increment
|
24
|
+
@cidn = cidn + 1
|
25
|
+
@cidn - 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Opalla
|
2
|
+
module ControllerAddOn
|
3
|
+
def expose(variable_assignments)
|
4
|
+
Opalla::Util.add_vars(variable_assignments)
|
5
|
+
variable_assignments.each do |key, value|
|
6
|
+
define_singleton_method(key){ value }
|
7
|
+
self.class.helper_method key
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
config.before_configuration do |app|
|
6
|
+
config.app_generators.javascript_engine :opalla
|
7
|
+
app.middleware.use OpallaMiddleware
|
8
|
+
js_folder = app.root.join(*%w[app assets javascripts]).to_s
|
9
|
+
app.config.autoload_paths += ["#{js_folder}/lib"]
|
10
|
+
app.config.autoload_paths += ["#{js_folder}/models"]
|
11
|
+
app.config.autoload_paths += ["#{js_folder}/collections"]
|
12
|
+
Opal.append_path File.expand_path('../../../opal', __FILE__)
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load(:action_view) do
|
16
|
+
include Opalla::ComponentHelper
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveSupport.on_load(:action_controller) do
|
20
|
+
include Opalla::ControllerAddOn
|
21
|
+
end
|
22
|
+
|
23
|
+
config.after_initialize do |app|
|
24
|
+
ActionController::Base.prepend_view_path "app/assets/javascripts/views/"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class OpallaMiddleware
|
2
|
+
def initialize(app)
|
3
|
+
@app = app
|
4
|
+
end
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
s, h, r = @app.call(env)
|
8
|
+
return [s, h, r] unless h['Content-Type'] =~ %r{text/html}
|
9
|
+
html = r.body.gsub("<body>", "<body>#{js_routes}")
|
10
|
+
[s, h, [html]]
|
11
|
+
end
|
12
|
+
|
13
|
+
def js_routes
|
14
|
+
<<~JS
|
15
|
+
<script>
|
16
|
+
window.opalla_data = #{ Opalla::Util.data_dump.to_json }
|
17
|
+
</script>
|
18
|
+
JS
|
19
|
+
end
|
20
|
+
end
|
data/lib/opalla/util.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
module HexRandom
|
5
|
+
def self.[]
|
6
|
+
x = rand.to_s
|
7
|
+
y = Time.new.strftime("%S%L")
|
8
|
+
Digest::SHA1.hexdigest("#{y}#{x}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
module Util
|
12
|
+
class JsFormatter
|
13
|
+
def initialize
|
14
|
+
@buffer = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def result
|
18
|
+
@buffer
|
19
|
+
end
|
20
|
+
|
21
|
+
def section(routes)
|
22
|
+
@buffer = routes.each_with_object({}) do |r, memo|
|
23
|
+
path = r[:path].gsub(/\(.:format\)/, '')
|
24
|
+
memo[r[:name]] = {
|
25
|
+
verb: r[:verb],
|
26
|
+
path: path,
|
27
|
+
reqs: r[:reqs]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def header(routes); end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def add_vars(var_assign)
|
37
|
+
@vars ||= {}
|
38
|
+
@vars.merge!(var_assign)
|
39
|
+
end
|
40
|
+
|
41
|
+
def vars
|
42
|
+
@vars || {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def data_dump
|
46
|
+
Marshal.dump(data)
|
47
|
+
end
|
48
|
+
|
49
|
+
def data
|
50
|
+
{
|
51
|
+
routes: routes,
|
52
|
+
vars: vars
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def routes
|
57
|
+
all_routes = Rails.application.routes.routes
|
58
|
+
inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
|
59
|
+
inspector.format(JsFormatter.new)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/opalla/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
class AssetsGenerator < Rails::Generators::NamedBase
|
5
|
+
def create_controller
|
6
|
+
create_file js("controllers/#{file_name}_controller.rb"), <<~CONTROLLER
|
7
|
+
class #{class_name}Controller < ApplicationController
|
8
|
+
# Write your actions here!
|
9
|
+
end
|
10
|
+
CONTROLLER
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_view_folder
|
14
|
+
empty_directory js("views/#{file_name}")
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def js(path)
|
20
|
+
"app/assets/javascripts/#{path}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
class CollectionGenerator < Rails::Generators::NamedBase
|
5
|
+
def create_collection
|
6
|
+
create_file js("collections/#{file_name}.rb"), <<~CONTROLLER
|
7
|
+
class #{class_name} < Opalla::Collection
|
8
|
+
# attr_reader :attrs
|
9
|
+
end
|
10
|
+
CONTROLLER
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def js(path)
|
16
|
+
"app/assets/javascripts/#{path}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
class ComponentGenerator < Rails::Generators::NamedBase
|
5
|
+
def create_component
|
6
|
+
create_file js("components/#{file_name}_component.rb"), <<~CONTROLLER
|
7
|
+
class #{class_name}Component < ApplicationComponent
|
8
|
+
# Feel free to write your component actions, bindings, events
|
9
|
+
end
|
10
|
+
CONTROLLER
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_views
|
14
|
+
ext = defined?(Haml) ? 'haml' : 'erb'
|
15
|
+
create_file js("views/components/_#{file_name}.#{ext}"), <<~VIEW
|
16
|
+
.#{file_name.dasherize}
|
17
|
+
-# Component content
|
18
|
+
VIEW
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def js(path)
|
24
|
+
"app/assets/javascripts/#{path}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
5
|
+
desc 'Creates Opalla files'
|
6
|
+
|
7
|
+
def create_folders
|
8
|
+
%w[
|
9
|
+
components
|
10
|
+
controllers
|
11
|
+
lib
|
12
|
+
models
|
13
|
+
collections
|
14
|
+
views/components
|
15
|
+
].each {|dir| empty_directory js(dir) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_basic_files
|
19
|
+
create_file js('application.rb'), <<~APPLICATION
|
20
|
+
require 'opalla'
|
21
|
+
|
22
|
+
require_tree './lib'
|
23
|
+
require_tree './models'
|
24
|
+
require_tree './collections'
|
25
|
+
require_tree './components'
|
26
|
+
require_tree './controllers'
|
27
|
+
require_tree './views'
|
28
|
+
|
29
|
+
Document.ready? do
|
30
|
+
Opalla::Router.start
|
31
|
+
end
|
32
|
+
APPLICATION
|
33
|
+
|
34
|
+
delete_appjs = ask %q{
|
35
|
+
I've just created the main app file (application.rb)
|
36
|
+
Should I just delete your application.js, since you won't need it anymore?
|
37
|
+
(If you say no, please be sure to remove it later, ok?)
|
38
|
+
[Y/n]
|
39
|
+
}
|
40
|
+
|
41
|
+
remove_file(js('application.js')) if delete_appjs == 'Y'
|
42
|
+
|
43
|
+
create_file js('components/application_component.rb'), <<~COMPONENT
|
44
|
+
class ApplicationComponent < Opalla::Component
|
45
|
+
# Code shared between all components go here
|
46
|
+
end
|
47
|
+
COMPONENT
|
48
|
+
|
49
|
+
create_file js('controllers/application_controller.rb'), <<~CONTROLLER
|
50
|
+
class ApplicationController < Opalla::Controller
|
51
|
+
# Code shared between all controllers go here
|
52
|
+
end
|
53
|
+
CONTROLLER
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def js(path)
|
59
|
+
"app/assets/javascripts/#{path}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module Opalla
|
4
|
+
class ModelGenerator < Rails::Generators::NamedBase
|
5
|
+
def create_component
|
6
|
+
create_file js("models/#{file_name}.rb"), <<~CONTROLLER
|
7
|
+
class #{class_name} < Opalla::Model
|
8
|
+
# attr_reader :attrs
|
9
|
+
end
|
10
|
+
CONTROLLER
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def js(path)
|
16
|
+
"app/assets/javascripts/#{path}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/opal/collection.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Opalla
|
2
|
+
class Collection
|
3
|
+
attr_accessor :models
|
4
|
+
alias_method :all, :models
|
5
|
+
|
6
|
+
def initialize(models=nil)
|
7
|
+
@models = models || []
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(options)
|
11
|
+
model_class.new(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(*attributes)
|
15
|
+
new_model = model_class.new(*attributes)
|
16
|
+
bindings.each {|b| new_model.bind(*b[:attributes], b[:callback])}
|
17
|
+
models << new_model
|
18
|
+
trigger_callbacks
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove(arg)
|
22
|
+
case arg
|
23
|
+
when Element, Numeric, String then models.delete(find(arg))
|
24
|
+
else models.delete(arg)
|
25
|
+
end
|
26
|
+
trigger_callbacks
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(arg)
|
30
|
+
case arg
|
31
|
+
when Numeric, String
|
32
|
+
models.each do |model|
|
33
|
+
return model if model.model_id == arg
|
34
|
+
end
|
35
|
+
when Element
|
36
|
+
sel = '[data-model-id]'
|
37
|
+
$console.log arg
|
38
|
+
if arg.is(sel)
|
39
|
+
find(arg)
|
40
|
+
else
|
41
|
+
find(arg.closest(sel).first.data('model-id'))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def bind(*attributes, callback)
|
47
|
+
@bindings ||= []
|
48
|
+
@bindings << { attributes: attributes, callback: callback }
|
49
|
+
models.each {|m| m.bind(*attributes, callback) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def bindings
|
53
|
+
@bindings || []
|
54
|
+
end
|
55
|
+
|
56
|
+
def trigger_callbacks
|
57
|
+
callbacks.empty? && return
|
58
|
+
callbacks.each(&:call)
|
59
|
+
end
|
60
|
+
|
61
|
+
def callbacks
|
62
|
+
@bindings.map { |b| b[:callback] }
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def model_class
|
68
|
+
Object::const_get("#{self.class.to_s.singularize}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/opal/component.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module Opalla
|
2
|
+
class Component
|
3
|
+
include ViewHelper
|
4
|
+
attr_reader :template, :events, :model, :collection, :components, :id
|
5
|
+
|
6
|
+
def initialize(template: nil, model: nil, collection: nil, id: nil)
|
7
|
+
template ||= "components/_#{component_name}"
|
8
|
+
@template = Template["views/#{template}"]
|
9
|
+
@components = []
|
10
|
+
@id = id
|
11
|
+
@resource = @model = model unless model.nil?
|
12
|
+
@resource = @collection = collection unless collection.nil?
|
13
|
+
register_bindings
|
14
|
+
end
|
15
|
+
|
16
|
+
def render
|
17
|
+
if @rendered.nil?
|
18
|
+
@rendered = true
|
19
|
+
html = Element[template.render(self)]
|
20
|
+
@id = id || cid(cidn_and_increment)
|
21
|
+
html.attr(:id, id)
|
22
|
+
html
|
23
|
+
else
|
24
|
+
target = Element[template.render(self)]
|
25
|
+
el.morph(target).attr(:id, id)
|
26
|
+
bind_events
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def el
|
31
|
+
Element["##{id}"] if @rendered
|
32
|
+
end
|
33
|
+
|
34
|
+
def bind_events
|
35
|
+
remove_events
|
36
|
+
components.each &:bind_events
|
37
|
+
events_hash.nil? && return
|
38
|
+
events_hash.each do |caller, method|
|
39
|
+
event, selector = caller.split(' ', 2)
|
40
|
+
el.find(selector).on event do |e|
|
41
|
+
e.prevent
|
42
|
+
case method
|
43
|
+
when Symbol then send(method, e.element)
|
44
|
+
when Proc then instance_exec(e.element, &method)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_events
|
51
|
+
components.each &:remove_events
|
52
|
+
events_hash.nil? && return
|
53
|
+
events_hash.each do |caller, method|
|
54
|
+
event, selector = caller.split(' ', 2)
|
55
|
+
el.find(selector).off event
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def component(name, options={})
|
60
|
+
comp = Object::const_get("#{name.camelize}Component").new(options)
|
61
|
+
@components << comp
|
62
|
+
comp.render
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def cidn
|
68
|
+
n = self.class.instance_variable_get(:"@cidn")
|
69
|
+
return(n) unless n.nil?
|
70
|
+
self.class.instance_variable_set(:"@cidn", 0)
|
71
|
+
end
|
72
|
+
|
73
|
+
def cid
|
74
|
+
"#{component_name}-#{cidn}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def cidn_and_increment
|
78
|
+
self.class.instance_variable_set :"@cidn", cidn + 1
|
79
|
+
self.class.instance_variable_get(:"@cidn") - 1
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_bindings
|
83
|
+
self.class.instance_variable_get :@bindings
|
84
|
+
end
|
85
|
+
|
86
|
+
def register_bindings
|
87
|
+
get_bindings.nil? && return
|
88
|
+
if !@model.nil?
|
89
|
+
bind_model
|
90
|
+
elsif !@collection.nil?
|
91
|
+
bind_collection
|
92
|
+
end
|
93
|
+
include_input_bindings
|
94
|
+
end
|
95
|
+
|
96
|
+
def include_input_bindings
|
97
|
+
merge_events_hash({
|
98
|
+
'input [data-bind]' => -> target do
|
99
|
+
model = @resource.find(target)
|
100
|
+
attr = target.data('bind')
|
101
|
+
model.public_send(:"#{attr}=", target.value)
|
102
|
+
end
|
103
|
+
})
|
104
|
+
end
|
105
|
+
|
106
|
+
def component_name
|
107
|
+
self.class.to_s.underscore.gsub('_component', '')
|
108
|
+
end
|
109
|
+
|
110
|
+
def events_hash
|
111
|
+
self.class.instance_variable_get(:@events) || {}
|
112
|
+
end
|
113
|
+
|
114
|
+
def merge_events_hash(new_hash)
|
115
|
+
self.class.instance_variable_set(:@events, events_hash.merge(new_hash))
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.events(events_hash)
|
119
|
+
@events = events_hash
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.bind(*attributes)
|
123
|
+
@bindings = attributes
|
124
|
+
end
|
125
|
+
|
126
|
+
def bind_model
|
127
|
+
model.nil? && return
|
128
|
+
model.bind(*get_bindings, -> { render } )
|
129
|
+
end
|
130
|
+
|
131
|
+
def bind_collection
|
132
|
+
(collection.nil? || get_bindings.nil?) && return
|
133
|
+
collection.bind(*get_bindings, -> { render })
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|