opalla 0.1.0 → 0.1.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.
- 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
|