opalla 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Opalla
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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
@@ -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
@@ -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