turbo_component 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d2d1eaf2e204ef8c7ac0f67baf7c127e5a9c2868157a0aea50ca7090c5a834a2
4
+ data.tar.gz: 6d975082d202636f196f56914f5b13902b615191ba744a512d27dd68fd6a8065
5
+ SHA512:
6
+ metadata.gz: 5bbf7ff1153b320f90c774b94b1057b4cacb3294e5911dd7b2c4d00949c01aba54a78e923412b532b180538f960fcdde6f6dfb25dc91ac6e14cc5e71bb054728
7
+ data.tar.gz: 89f5f9abfd287e9f962f36062b6cd252ce1dcf4183c79fdc3e8971eb9557e2e7c3f8e85afdc24b39fb8ec2ecbc95695c2a8939a2cbeca9b24aa89c4ee4b65f34
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Jose Galisteo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # TurboComponent
2
+
3
+ This project explores the idea of components not only in terms of view, like view_component or cells, but it also includes server actions required for the component.
4
+
5
+ I part from the codebase of "pagelet_rails" and take advantage of turbo_rails to explore this idea.
6
+
7
+ ## Usage
8
+
9
+ Do not use it, it is a work in progress.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+
8
+ load "rails/tasks/statistics.rake"
9
+
10
+ require "bundler/gem_tasks"
11
+
12
+ require "rake/testtask"
13
+
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << "test"
16
+ t.pattern = "test/**/*_test.rb"
17
+ t.verbose = false
18
+ t.warning = false
19
+ end
20
+
21
+ task default: :test
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponentsHelper
4
+ include Turbo::FramesHelper
5
+
6
+ def turbo_component(turbo_key, opts = {}, &block)
7
+ async = opts.delete(:async)
8
+
9
+ if async
10
+ turbo_component_async(turbo_key, opts, &block)
11
+ else
12
+ # TODO: raise exception, block not allowed
13
+ turbo_component_inline_frame(turbo_key, opts)
14
+ end
15
+ end
16
+
17
+ def turbo_component_async(turbo_key, opts = {}, &block)
18
+ turbo_id = opts.delete(:turbo_id) || turbo_key
19
+ locals = opts.delete(:locals) || {}
20
+ permanent = opts.delete(:permanent)
21
+
22
+ # TODO: cache
23
+ serialized = ActiveJob::Arguments.serialize(locals)
24
+ encoded = TurboComponent::Encryptor.encode(serialized, purpose: turbo_id)
25
+ attrs = { _encoded: encoded, _turbo_id: turbo_id }
26
+
27
+ turbo_url = "turbo_component_#{turbo_key}"
28
+ base_params = params.to_unsafe_h.except(:action, :controller)
29
+ url = polymorphic_url(turbo_url, base_params.merge(attrs))
30
+
31
+ opts[:src] = url
32
+ opts["data-turbo-permanent"] = true if permanent
33
+ turbo_frame_tag(turbo_id, **opts, &block)
34
+ end
35
+
36
+ def turbo_component_inline_frame(turbo_key, opts)
37
+ target = opts[:target]
38
+ locals = opts[:locals] || {}
39
+ turbo_id = opts[:turbo_id] || turbo_key
40
+ turbo_frame_tag turbo_id, target: target do
41
+ base_params = params.to_unsafe_h.except(:action, :controller)
42
+ base_params[:_turbo_id] = turbo_id
43
+ base_params = base_params.merge(locals)
44
+ turbo_component_inline(turbo_key, locals: base_params)
45
+ end
46
+ end
47
+
48
+ def turbo_component_inline(turbo_key, p_options = {})
49
+ Rails.logger.info "Rendering turbo component #{turbo_key}"
50
+
51
+ turbo_url = "turbo_component_#{turbo_key}"
52
+ p_params = p_options.delete(:locals) { {} }.with_indifferent_access
53
+ path = polymorphic_url(turbo_url, p_params)
54
+
55
+ path_opts = Rails.application.routes.recognize_path(path)
56
+ p_params.reverse_merge!(path_opts)
57
+
58
+ controller_class = "#{path_opts[:controller].camelize}Controller".constantize
59
+ action = path_opts[:action]
60
+
61
+ c = controller_class.new
62
+ c.turbo_component_options p_options
63
+ c.turbo_component_options original_options: p_options
64
+
65
+ env = controller.request.env.select do |key, _value|
66
+ case key.to_s
67
+ when /^action_dispatch\.request/i,
68
+ /^action_controller/i,
69
+ /^rack\.request/i,
70
+ /^request/i,
71
+ "HTTP_ACCEPT",
72
+ "CONTENT_TYPE",
73
+ "CONTENT_LENGTH",
74
+ "REQUEST_METHOD"
75
+ false
76
+ else
77
+ true
78
+ end
79
+ end
80
+
81
+ # env['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest"
82
+ env = Rack::MockRequest.env_for(path, env)
83
+ p_request = ActionDispatch::Request.new(env)
84
+ p_request.parameters.clear
85
+ p_request.parameters.merge! p_params
86
+
87
+ if c.method(:dispatch).arity == 3
88
+ p_response = controller_class.make_response! p_request
89
+ c.dispatch(action, p_request, p_response)
90
+ else
91
+ c.dispatch(action, p_request)
92
+ end
93
+
94
+ body = c.response.body
95
+ body.html_safe
96
+ end
97
+ end
@@ -0,0 +1,7 @@
1
+ <% if turbo_component_request? %>
2
+ <%= turbo_frame_tag turbo_key do %>
3
+ <%= content_for?(:content) ? yield(:content) : yield %>
4
+ <% end %>
5
+ <% else %>
6
+ <%= content_for?(:content) ? yield(:content) : yield %>
7
+ <% end %>
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Generates a turbo component scaffold
3
+
4
+ Example:
5
+ bin/rails generate turbo_component ComponentName
6
+
7
+ This will create:
8
+ app/turbo_components/component_name/component_controller.rb
9
+ app/turbo_components/component_name/views/show.html.erb
@@ -0,0 +1,7 @@
1
+ class <%= class_name %>::ComponentController < ApplicationController
2
+ include TurboComponent::Concerns::Controller
3
+
4
+ display :show
5
+ def show
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ <h1> Turbo component show </h1>
@@ -0,0 +1,2 @@
1
+ <h1><%= @component_class_name %>#show</h1>
2
+ <p>Find me in <%= @path %></p>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TurboComponentGenerator < Rails::Generators::NamedBase
4
+ desc "Create a scaffold structure for a new turbo_component"
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ def create_component_controller
8
+ template "component_controller.rb.tt", File.join(base_path, "component_controller.rb")
9
+ end
10
+
11
+ def create_component_show_view
12
+ @path = File.join(base_path, "views/show.html.erb")
13
+ @component_class_name = "#{name.classify}::Component"
14
+ template "show.html.erb.tt", @path
15
+ end
16
+
17
+ private
18
+ def base_path
19
+ File.join("app/turbo_components", name.underscore)
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :turbo_component do
4
+ # # Task goes here
5
+ # end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "turbo-rails"
4
+ require "turbo_component/version"
5
+ require "turbo_component/engine"
6
+
7
+ module TurboComponent
8
+ extend ActiveSupport::Autoload
9
+ eager_autoload do
10
+ autoload :Router
11
+ autoload :Encryptor
12
+ end
13
+
14
+ module Concerns
15
+ extend ActiveSupport::Autoload
16
+
17
+ eager_autoload do
18
+ autoload :Controller
19
+ autoload :Options
20
+ autoload :Routes
21
+ autoload :Tags
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponent::Concerns::Controller
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # order is important
8
+ include TurboComponent::Concerns::Routes
9
+ include TurboComponent::Concerns::Options
10
+
11
+ include TurboComponentsHelper
12
+
13
+ prepend_before_action :append_turbo_components_view_paths
14
+ prepend_before_action :parse_locals
15
+
16
+ append_view_path "app/turbo_components/"
17
+ append_view_path "test/dummy/app/turbo_components/" if Rails.env.test?
18
+
19
+ layout :layout_name
20
+
21
+ helper_method :turbo_component_request?
22
+ helper_method :turbo_key
23
+
24
+ turbo_component_options layout: "container"
25
+ end
26
+
27
+ def layout_name
28
+ layout = params[:layout] || turbo_component_options.layout
29
+
30
+ "turbo_components/#{layout}"
31
+ end
32
+
33
+ def turbo_component_request?
34
+ request.headers["Turbo-Frame"].present? && !turbo_stream_request?
35
+ end
36
+
37
+ def turbo_stream_request?
38
+ request.headers.fetch("HTTP_ACCEPT", "")&.include?("text/vnd.turbo-stream")
39
+ end
40
+
41
+ def turbo_key
42
+ params[:_turbo_id] || component_name
43
+ end
44
+
45
+ def component_name
46
+ controller_path.split("/component", 2)[0]
47
+ end
48
+
49
+ private
50
+ def parse_locals
51
+ return unless params[:_encoded].present?
52
+
53
+ decoded = TurboComponent::Encryptor.decode(params[:_encoded], purpose: params[:_turbo_id])
54
+ deserialized = ActiveJob::Arguments.deserialize(decoded)
55
+ deserialized.each do |key, value|
56
+ params[key] = value
57
+ end
58
+ end
59
+
60
+ def append_turbo_components_view_paths
61
+ # lookup_context.prefixes.clear
62
+ view = "#{component_name}/views"
63
+ lookup_context.prefixes.unshift view if lookup_context.prefixes.exclude?(view)
64
+
65
+ # https://github.com/rails/actionpack-action_caching/issues/32
66
+ lookup_context.formats.unshift :html if lookup_context.formats.exclude?(:html)
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponent::Concerns::Options
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Shared
8
+
9
+ helper_method :turbo_component_options
10
+ end
11
+
12
+ def turbo_component_options(*args)
13
+ set_turbo_component_options(*args)
14
+
15
+ opts = self.class.turbo_component_options
16
+ class_default_opts = opts.fetch("default", {})
17
+ class_action_opts = opts.fetch(action_name, {})
18
+
19
+ instance_default_opts = @turbo_component_options.fetch("default", {})
20
+ instance_action_opts = @turbo_component_options.fetch(action_name, {})
21
+
22
+ result = {}.with_indifferent_access
23
+ .deep_merge!(class_default_opts)
24
+ .deep_merge!(class_action_opts)
25
+ .deep_merge!(instance_default_opts)
26
+ .deep_merge!(instance_action_opts)
27
+
28
+ OpenStruct.new result
29
+ end
30
+
31
+ module Shared
32
+ def set_turbo_component_options(*args)
33
+ opts = args.extract_options!
34
+ actions = args
35
+ actions << "default" if actions.blank?
36
+
37
+ @turbo_component_options ||= {}.with_indifferent_access
38
+
39
+ if opts.any?
40
+ actions.each do |action|
41
+ @turbo_component_options.deep_merge! action => opts
42
+ end
43
+ end
44
+ @turbo_component_options
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ include Shared
50
+
51
+ def turbo_component_options(*args)
52
+ set_turbo_component_options(*args)
53
+
54
+ if superclass && superclass.instance_variable_defined?(:@turbo_component_options)
55
+ parent = superclass.instance_variable_get :@turbo_component_options
56
+ parent.merge(@turbo_component_options)
57
+ else
58
+ @turbo_component_options
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponent::Concerns::Routes
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ @turbo_component_routes = []
8
+ end
9
+
10
+ module ClassMethods
11
+ def display(action)
12
+ controller_name = self.controller_name
13
+ component_name = self.name.split("::ComponentController", 2)[0].underscore
14
+ turbo_component_routes do
15
+ get component_name, to: "#{controller_name}##{action}", as: component_name
16
+ end
17
+ end
18
+
19
+ def get(action, path = "")
20
+ turbo_route(:get, action, path)
21
+ end
22
+
23
+ def post(action, path = "")
24
+ turbo_route(:post, action, path)
25
+ end
26
+
27
+ def put(action, path = "")
28
+ turbo_route(:put, action, path)
29
+ end
30
+
31
+ def patch(action, path = "")
32
+ turbo_route(:patch, action, path)
33
+ end
34
+
35
+ def delete(action, path = "")
36
+ turbo_route(:delete, action, path)
37
+ end
38
+
39
+ def turbo_route(verb, action, path)
40
+ controller_name = self.controller_name
41
+ component_name = self.name.split("::ComponentController", 2)[0].underscore
42
+ turbo_component_routes do
43
+ send(verb, "#{component_name}/#{path}", to: "#{controller_name}##{action}", as: "#{component_name}_#{action}")
44
+ end
45
+ end
46
+
47
+ def turbo_component_routes(&block)
48
+ @turbo_component_routes << block
49
+ end
50
+
51
+ def load_turbo_component_routes!(context)
52
+ @turbo_component_routes.each do |proc|
53
+ context.instance_eval(&proc)
54
+ end
55
+ end
56
+
57
+ def inherited(subklass)
58
+ subklass.instance_variable_set(:@turbo_component_routes, [])
59
+ super
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TurboComponent::Encryptor
4
+ DEFAULT_SALT = '!@#Q156^tdSXggT0&*789++8&?_|T%\/++==RqE'
5
+
6
+ attr_reader :salt
7
+
8
+ def self.encode(data, opts = {})
9
+ purpose = opts.delete(:purpose)
10
+ new(opts).encode(data, purpose: purpose)
11
+ end
12
+
13
+ def self.decode(encrypted_data, opts = {})
14
+ purpose = opts.delete(:purpose)
15
+ new(opts).decode(encrypted_data, purpose: purpose)
16
+ end
17
+
18
+ def self.get_key(secret, salt)
19
+ @get_key_cache ||= {}
20
+ key = [secret, salt]
21
+
22
+ @get_key_cache[key] ||= ActiveSupport::KeyGenerator.new(secret).generate_key(salt)
23
+ end
24
+
25
+ def initialize(opts = {})
26
+ @salt = opts.fetch :salt, DEFAULT_SALT
27
+ @secret = opts[:secret]
28
+ end
29
+
30
+ def secret
31
+ @secret || Rails.application.secrets[:secret_key_base]
32
+ end
33
+
34
+ def encode(data, purpose: nil)
35
+ encryptor.encrypt_and_sign(data, purpose: purpose)
36
+ end
37
+
38
+ def decode(encrypted_data, purpose: nil)
39
+ encryptor.decrypt_and_verify(encrypted_data, purpose: purpose)
40
+ end
41
+
42
+ private
43
+ def encryptor
44
+ @encryptor ||= begin
45
+ key = self.class.get_key secret, salt
46
+ key = key[0..31]
47
+ ActiveSupport::MessageEncryptor.new(key)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponent
4
+ class Engine < ::Rails::Engine
5
+ # It loads the rails stuff like TurboComponentsHelper
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponent
4
+ class Router
5
+ def self.load_routes!(context)
6
+ controllers = Dir[Rails.root.join("app", "turbo_components", "**", "*controller.rb")]
7
+
8
+ controllers.each do |controller_file|
9
+ module_name = module_name(controller_file)
10
+ controller = "#{module_name.camelize}::ComponentController".constantize
11
+
12
+ next unless controller.respond_to? :load_turbo_component_routes!
13
+
14
+ context.instance_eval do
15
+ scope module: module_name, path: "/_turbo_components", as: "turbo_component" do
16
+ controller.load_turbo_component_routes! self
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.module_name(file)
23
+ File.dirname(file)
24
+ .split(Rails.root.join("app/turbo_components/").to_s, 2)[-1]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboComponent
4
+ VERSION = "0.1.0.pre"
5
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: turbo_component
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - Jose Galisteo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-07-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: turbo-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - ceritium@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/helpers/turbo_components_helper.rb
52
+ - app/views/layouts/turbo_components/container.html.erb
53
+ - lib/generators/turbo_component/USAGE
54
+ - lib/generators/turbo_component/templates/component_controller.rb.tt
55
+ - lib/generators/turbo_component/templates/show.html.erb
56
+ - lib/generators/turbo_component/templates/show.html.erb.tt
57
+ - lib/generators/turbo_component/turbo_component_generator.rb
58
+ - lib/tasks/turbo_component_tasks.rake
59
+ - lib/turbo_component.rb
60
+ - lib/turbo_component/concerns/controller.rb
61
+ - lib/turbo_component/concerns/options.rb
62
+ - lib/turbo_component/concerns/routes.rb
63
+ - lib/turbo_component/encryptor.rb
64
+ - lib/turbo_component/engine.rb
65
+ - lib/turbo_component/router.rb
66
+ - lib/turbo_component/version.rb
67
+ homepage: https://github.com/ceritium/turbo_component
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/ceritium/turbo_component
72
+ source_code_uri: https://github.com/ceritium/turbo_component
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">"
85
+ - !ruby/object:Gem::Version
86
+ version: 1.3.1
87
+ requirements: []
88
+ rubygems_version: 3.1.4
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Components with super powers
92
+ test_files: []