live_component 0.1.0

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2 -0
  3. data/Gemfile +1 -0
  4. data/LICENSE +21 -0
  5. data/Rakefile +41 -0
  6. data/app/channels/live_component_channel.rb +23 -0
  7. data/app/components/live_component/render_component.rb +43 -0
  8. data/app/controllers/live_component/render_controller.rb +9 -0
  9. data/app/helpers/live_component/application_helper.rb +40 -0
  10. data/app/views/live_component/render/show.html.erb +1 -0
  11. data/config/routes.rb +5 -0
  12. data/ext/view_component_patch.rb +22 -0
  13. data/lib/live_component/action.rb +31 -0
  14. data/lib/live_component/base.rb +267 -0
  15. data/lib/live_component/big_decimal_serializer.rb +17 -0
  16. data/lib/live_component/component.html.erb +4 -0
  17. data/lib/live_component/controller_methods.rb +9 -0
  18. data/lib/live_component/date_serializer.rb +15 -0
  19. data/lib/live_component/date_time_serializer.rb +11 -0
  20. data/lib/live_component/duration_serializer.rb +18 -0
  21. data/lib/live_component/engine.rb +21 -0
  22. data/lib/live_component/inline_serializer.rb +34 -0
  23. data/lib/live_component/middleware.rb +25 -0
  24. data/lib/live_component/model_serializer.rb +65 -0
  25. data/lib/live_component/module_serializer.rb +15 -0
  26. data/lib/live_component/object_serializer.rb +29 -0
  27. data/lib/live_component/range_serializer.rb +19 -0
  28. data/lib/live_component/react.rb +24 -0
  29. data/lib/live_component/record_proxy.rb +85 -0
  30. data/lib/live_component/safe_dispatcher.rb +64 -0
  31. data/lib/live_component/serializer.rb +202 -0
  32. data/lib/live_component/state.rb +91 -0
  33. data/lib/live_component/tag_builder.rb +20 -0
  34. data/lib/live_component/target.rb +26 -0
  35. data/lib/live_component/time_object_serializer.rb +13 -0
  36. data/lib/live_component/time_serializer.rb +11 -0
  37. data/lib/live_component/time_with_zone_serializer.rb +20 -0
  38. data/lib/live_component/utils.rb +139 -0
  39. data/lib/live_component/version.rb +5 -0
  40. data/lib/live_component.rb +55 -0
  41. data/lib/tasks/test.rake +1 -0
  42. data/live_component.gemspec +21 -0
  43. metadata +136 -0
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveComponent
4
+ class TimeWithZoneSerializer < ObjectSerializer
5
+ NANO_PRECISION = 9
6
+
7
+ private
8
+
9
+ def object_to_hash(time_with_zone)
10
+ {
11
+ "value" => time_with_zone.iso8601(NANO_PRECISION),
12
+ "time_zone" => time_with_zone.time_zone.tzinfo.name
13
+ }
14
+ end
15
+
16
+ def hash_to_object(hash)
17
+ Time.iso8601(hash["value"]).in_time_zone(hash["time_zone"] || Time.zone)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveComponent
4
+ module Utils
5
+ WHITESPACE_REGEX = /[\f\n\r\t\v ]{2,}/
6
+ PLURAL_DATA_ATTRIBUTES = %i[action target].freeze
7
+
8
+ def normalize_html_whitespace(str)
9
+ str.gsub(WHITESPACE_REGEX, " ")
10
+ end
11
+
12
+ def lookup_component_class(const_str)
13
+ const = safe_lookup_component_class(const_str)
14
+ return const if const
15
+
16
+ raise UnexpectedConstantError,
17
+ "cannot find constant '#{const_str}' that does not inherit from ViewComponent::Base"
18
+ end
19
+
20
+ def safe_lookup_component_class(const_str)
21
+ const = const_str.safe_constantize
22
+
23
+ if const && const < ::ViewComponent::Base
24
+ const
25
+ end
26
+ end
27
+
28
+ def html_params_for_rerender(object)
29
+ case object
30
+ when :self, :parent
31
+ return { "data-rerender-target" => ":#{object}" }
32
+ when Symbol
33
+ const = safe_lookup_component_class(object.to_s)
34
+ return({ "data-rerender-target" => const.name }) if const
35
+
36
+ raise UnexpectedConstantError, "cannot find constant '#{object}' to use as a rerender target"
37
+ when String
38
+ return { "data-rerender-id" => object }
39
+ when Class
40
+ if object < LiveComponent::Base
41
+ return { "data-rerender-target" => object.name }
42
+ end
43
+ else
44
+ if object.respond_to?(:__lc_id)
45
+ return { "data-rerender-id" => object.__lc_id }
46
+ end
47
+ end
48
+
49
+ nil
50
+ end
51
+
52
+ def translate_all_attrs(kwargs)
53
+ # TODO: allow folks to add their own attr classes?
54
+ [Action, Target].inject(kwargs) do |memo, attr_klass|
55
+ if kwargs.include?(attr_klass.attr_name)
56
+ translate_attrs(attr_klass, memo)
57
+ else
58
+ memo
59
+ end
60
+ end
61
+ end
62
+
63
+ def translate_attrs(attr_klass, kwargs)
64
+ if kwargs[attr_klass.attr_name].is_a?(Array)
65
+ return kwargs unless kwargs[attr_klass.attr_name].all? { |action| action.is_a?(attr_klass) }
66
+ else
67
+ return kwargs unless kwargs[attr_klass.attr_name].is_a?(attr_klass)
68
+ end
69
+
70
+ attrs = Array(kwargs.delete(attr_klass.attr_name))
71
+
72
+ attrs.each do |attr|
73
+ kwargs[:data] = merge_data(kwargs, attr.to_attributes)
74
+ end
75
+
76
+ kwargs
77
+ end
78
+
79
+ # Borrowed from primer_view_components
80
+ # See: https://github.com/primer/view_components/blob/b0acdfffaa30e606a07db657d9b444b4de8ca860/app/lib/primer/attributes_helper.rb
81
+ #
82
+ # Merges hashes that contain "data-*" keys and nested data: hashes. Removes keys from
83
+ # each hash and returns them in the new hash.
84
+ #
85
+ # Eg. merge_data({ "data-foo": "true" }, { data: { bar: "true" } })
86
+ # => { foo: "true", bar: "true" }
87
+ #
88
+ # Certain data attributes can contain multiple values separated by spaces. merge_data
89
+ # will combine these plural attributes into a composite string.
90
+ #
91
+ # Eg. merge_data({ "data-target": "foo" }, { data: { target: "bar" } })
92
+ # => { target: "foo bar" }
93
+ def merge_data(*hashes)
94
+ merge_prefixed_attribute_hashes(
95
+ *hashes, prefix: :data, plural_keys: PLURAL_DATA_ATTRIBUTES
96
+ )
97
+ end
98
+
99
+ def merge_prefixed_attribute_hashes(*hashes, prefix:, plural_keys:)
100
+ {}.tap do |result|
101
+ hashes.each do |hash|
102
+ next unless hash
103
+
104
+ prefix_hash = hash.delete(prefix) || {}
105
+
106
+ prefix_hash.each_pair do |key, val|
107
+ result[key] =
108
+ if plural_keys.include?(key)
109
+ [*(result[key] || "").split, val].join(" ").strip
110
+ else
111
+ val
112
+ end
113
+ end
114
+
115
+ hash.delete_if do |key, val|
116
+ key_s = key.to_s
117
+
118
+ if key.start_with?("#{prefix}-")
119
+ bare_key = key_s.sub("#{prefix}-", "").to_sym
120
+
121
+ result[bare_key] =
122
+ if plural_keys.include?(bare_key)
123
+ [*(result[bare_key] || "").split, val].join(" ").strip
124
+ else
125
+ val
126
+ end
127
+
128
+ true
129
+ else
130
+ false
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ extend self
138
+ end
139
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveComponent
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveComponent
4
+ autoload :Action, "live_component/action"
5
+ autoload :Base, "live_component/base"
6
+ autoload :BigDecimalSerializer, "live_component/big_decimal_serializer"
7
+ autoload :ControllerMethods, "live_component/controller_methods"
8
+ autoload :DateSerializer, "live_component/date_serializer"
9
+ autoload :DateTimeSerializer, "live_component/date_time_serializer"
10
+ autoload :DurationSerializer, "live_component/duration_serializer"
11
+ autoload :InlineSerializer, "live_component/inline_serializer"
12
+ autoload :Middleware, "live_component/middleware"
13
+ autoload :ModelSerializer, "live_component/model_serializer"
14
+ autoload :ModuleSerializer, "live_component/module_serializer"
15
+ autoload :ObjectSerializer, "live_component/object_serializer"
16
+ autoload :RangeSerializer, "live_component/range_serializer"
17
+ autoload :RecordProxy, "live_component/record_proxy"
18
+ autoload :SafeDispatcher, "live_component/safe_dispatcher"
19
+ autoload :Serializer, "live_component/serializer"
20
+ autoload :TimeWithZoneSerializer, "live_component/time_with_zone_serializer"
21
+ autoload :TagBuilder, "live_component/tag_builder"
22
+ autoload :Target, "live_component/target"
23
+ autoload :TimeObjectSerializer, "live_component/time_object_serializer"
24
+ autoload :TimeSerializer, "live_component/time_serializer"
25
+ autoload :TimeWithZoneSerializer, "live_component/time_with_zone_serializer"
26
+ autoload :React, "live_component/react"
27
+ autoload :State, "live_component/state"
28
+ autoload :Utils, "live_component/utils"
29
+
30
+ class UnexpectedConstantError < StandardError; end
31
+ class SerializationError < ArgumentError; end
32
+ class SafeDispatchError < StandardError; end
33
+
34
+ class << self
35
+ def register_prop_serializer(name, klass)
36
+ registered_prop_serializers[name] = klass
37
+ end
38
+
39
+ def registered_prop_serializers
40
+ @registered_prop_serializers ||= {}
41
+ end
42
+
43
+ def serializer
44
+ @serializer ||= Serializer.make
45
+ end
46
+ end
47
+ end
48
+
49
+ LiveComponent.register_prop_serializer(:model_serializer, LiveComponent::ModelSerializer)
50
+
51
+ if defined?(Rails)
52
+ require "live_component/engine"
53
+ end
54
+
55
+ require File.join(File.dirname(__dir__), "ext", "view_component_patch")
@@ -0,0 +1 @@
1
+ # System tests are defined in the Rakefile to avoid Rails test infrastructure issues
@@ -0,0 +1,21 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), "lib")
2
+ require "live_component/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "live_component"
6
+ s.version = ::LiveComponent::VERSION
7
+ s.authors = ["Cameron Dutro"]
8
+ s.email = ["camertron@gmail.com"]
9
+ s.homepage = "http://github.com/camertron/live_component"
10
+ s.description = s.summary = "Client-side rendering and state management for ViewComponent."
11
+ s.platform = Gem::Platform::RUBY
12
+
13
+ s.add_dependency "railties", "~> 8.0"
14
+ s.add_dependency "view_component", "~> 4.0"
15
+ s.add_dependency "use_context", "~> 1.2"
16
+ s.add_dependency "weak_key_map", "~> 1.0"
17
+
18
+ s.require_path = "lib"
19
+
20
+ s.files = Dir["{app,config,ext,lib}/**/*", "Gemfile", "LICENSE", "CHANGELOG.md", "README.md", "Rakefile", "live_component.gemspec"]
21
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: live_component
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: view_component
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '4.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: use_context
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: weak_key_map
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ description: Client-side rendering and state management for ViewComponent.
69
+ email:
70
+ - camertron@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - CHANGELOG.md
76
+ - Gemfile
77
+ - LICENSE
78
+ - Rakefile
79
+ - app/channels/live_component_channel.rb
80
+ - app/components/live_component/render_component.rb
81
+ - app/controllers/live_component/render_controller.rb
82
+ - app/helpers/live_component/application_helper.rb
83
+ - app/views/live_component/render/show.html.erb
84
+ - config/routes.rb
85
+ - ext/view_component_patch.rb
86
+ - lib/live_component.rb
87
+ - lib/live_component/action.rb
88
+ - lib/live_component/base.rb
89
+ - lib/live_component/big_decimal_serializer.rb
90
+ - lib/live_component/component.html.erb
91
+ - lib/live_component/controller_methods.rb
92
+ - lib/live_component/date_serializer.rb
93
+ - lib/live_component/date_time_serializer.rb
94
+ - lib/live_component/duration_serializer.rb
95
+ - lib/live_component/engine.rb
96
+ - lib/live_component/inline_serializer.rb
97
+ - lib/live_component/middleware.rb
98
+ - lib/live_component/model_serializer.rb
99
+ - lib/live_component/module_serializer.rb
100
+ - lib/live_component/object_serializer.rb
101
+ - lib/live_component/range_serializer.rb
102
+ - lib/live_component/react.rb
103
+ - lib/live_component/record_proxy.rb
104
+ - lib/live_component/safe_dispatcher.rb
105
+ - lib/live_component/serializer.rb
106
+ - lib/live_component/state.rb
107
+ - lib/live_component/tag_builder.rb
108
+ - lib/live_component/target.rb
109
+ - lib/live_component/time_object_serializer.rb
110
+ - lib/live_component/time_serializer.rb
111
+ - lib/live_component/time_with_zone_serializer.rb
112
+ - lib/live_component/utils.rb
113
+ - lib/live_component/version.rb
114
+ - lib/tasks/test.rake
115
+ - live_component.gemspec
116
+ homepage: http://github.com/camertron/live_component
117
+ licenses: []
118
+ metadata: {}
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.6.7
134
+ specification_version: 4
135
+ summary: Client-side rendering and state management for ViewComponent.
136
+ test_files: []