turbo_component 0.1.0.pre

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 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: []