vident 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.
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ module RootComponent
5
+ module Base
6
+ def initialize(
7
+ controllers: nil,
8
+ actions: nil,
9
+ targets: nil,
10
+ named_classes: nil, # https://stimulus.hotwired.dev/reference/css-classes
11
+ data_maps: nil,
12
+ element_tag: nil,
13
+ id: nil,
14
+ html_options: nil
15
+ )
16
+ @element_tag = element_tag
17
+ @html_options = html_options
18
+ @id = id
19
+ @controllers = Array.wrap(controllers)
20
+ @actions = actions
21
+ @targets = targets
22
+ @named_classes = named_classes
23
+ @data_map_kvs = {}
24
+ @data_maps = data_maps
25
+ end
26
+
27
+ # The view component's helpers for setting stimulus data-* attributes on this component.
28
+
29
+ # TODO: rename
30
+ # Create a Stimulus action string, and returns it
31
+ # examples:
32
+ # action(:my_thing) => "current_controller#myThing"
33
+ # action(:click, :my_thing) => "click->current_controller#myThing"
34
+ # action("click->current_controller#myThing") => "click->current_controller#myThing"
35
+ # action("path/to/current", :my_thing) => "path--to--current_controller#myThing"
36
+ # action(:click, "path/to/current", :my_thing) => "click->path--to--current_controller#myThing"
37
+ def action(*args)
38
+ part1, part2, part3 = args
39
+ (args.size == 1) ? parse_action_arg(part1) : parse_multiple_action_args(part1, part2, part3)
40
+ end
41
+
42
+ def action_data_attribute(*actions)
43
+ {action: parse_actions(actions).join(" ")}
44
+ end
45
+
46
+ # TODO: rename & make stimulus Target class instance and returns it, which can convert to String
47
+ # Create a Stimulus Target and returns it
48
+ # examples:
49
+ # target(:my_target) => {controller: 'current_controller' name: 'myTarget'}
50
+ # target("path/to/current", :my_target) => {controller: 'path--to--current_controller', name: 'myTarget'}
51
+ def target(name, part2 = nil)
52
+ if part2.nil?
53
+ {controller: implied_controller_name, name: js_name(name)}
54
+ else
55
+ {controller: stimulize_path(name), name: js_name(part2)}
56
+ end
57
+ end
58
+
59
+ def target_data_attribute(name)
60
+ build_target_data_attributes([target(name)])
61
+ end
62
+
63
+ # Getter for a named classes list so can be used in view to set initial state on SSR
64
+ # Returns a String of classes that can be used in a `class` attribute.
65
+ def named_classes(*names)
66
+ names.map { |name| convert_classes_list_to_string(@named_classes[name]) }.join(" ")
67
+ end
68
+
69
+ # Helpers for generating the Stimulus data-* attributes directly
70
+
71
+ # Return the HTML `data-controller` attribute
72
+ def with_controllers
73
+ "data-controller='#{controller_list}'".html_safe
74
+ end
75
+
76
+ # Return the HTML `data-target` attribute
77
+ def as_targets(*targets)
78
+ build_target_data_attributes(parse_targets(targets))
79
+ .map { |dt, n| "data-#{dt}=\"#{n}\"" }
80
+ .join(" ")
81
+ .html_safe
82
+ end
83
+ alias_method :as_target, :as_targets
84
+
85
+ # Return the HTML `data-action` attribute to add these actions
86
+ def with_actions(*actions)
87
+ "data-action='#{parse_actions(actions).join(" ")}'".html_safe
88
+ end
89
+ alias_method :with_action, :with_actions
90
+
91
+ private
92
+
93
+ # An implicit Stimulus controller name is built from the implicit controller path
94
+ def implied_controller_name
95
+ stimulize_path(implied_controller_path)
96
+ end
97
+
98
+ # When using the DSL if you dont specify, the first controller is implied
99
+ def implied_controller_path
100
+ @controllers&.first || raise(StandardError, "No controllers have been specified")
101
+ end
102
+
103
+ # A complete list of Stimulus controllers for this component
104
+ def controller_list
105
+ @controllers&.map { |c| stimulize_path(c) }&.join(" ")
106
+ end
107
+
108
+ # Complete list of actions ready to be use in the data-action attribute
109
+ def action_list
110
+ return nil unless @actions&.size&.positive?
111
+ parse_actions(@actions).join(" ")
112
+ end
113
+
114
+ # Complete list of targets ready to be use in the data attributes
115
+ def target_list
116
+ return {} unless @targets&.size&.positive?
117
+ build_target_data_attributes(parse_targets(@targets))
118
+ end
119
+
120
+ def named_classes_list
121
+ return {} unless @named_classes&.size&.positive?
122
+ build_named_classes_data_attributes(@named_classes)
123
+ end
124
+
125
+ # stimulus "data-*" attributes map for this component
126
+ def tag_data_attributes
127
+ {controller: controller_list, action: action_list}
128
+ .merge!(target_list)
129
+ .merge!(named_classes_list)
130
+ .merge!(data_map_attributes)
131
+ .compact_blank!
132
+ end
133
+
134
+ # Actions can be specified as a symbol, in which case they imply an action on the primary
135
+ # controller, or as a string in which case it implies an action that is already fully qualified
136
+ # stimulus action.
137
+ # 1 Symbol: :my_action => "my_controller#myAction"
138
+ # 1 String: "my_controller#myAction"
139
+ # 2 Symbols: [:click, :my_action] => "click->my_controller#myAction"
140
+ # 1 String, 1 Symbol: ["path/to/controller", :my_action] => "path--to--controller#myAction"
141
+ # 1 Symbol, 1 String, 1 Symbol: [:hover, "path/to/controller", :my_action] => "hover->path--to--controller#myAction"
142
+
143
+ def parse_action_arg(part1)
144
+ if part1.is_a?(Symbol)
145
+ # 1 symbol arg, name of method on this controller
146
+ "#{implied_controller_name}##{js_name(part1)}"
147
+ elsif part1.is_a?(String)
148
+ # 1 string arg, fully qualified action
149
+ part1
150
+ end
151
+ end
152
+
153
+ def parse_multiple_action_args(part1, part2, part3)
154
+ if part3.nil? && part1.is_a?(Symbol)
155
+ # 2 symbol args = event + action
156
+ "#{part1}->#{implied_controller_name}##{js_name(part2)}"
157
+ elsif part3.nil?
158
+ # 1 string arg, 1 symbol = controller + action
159
+ "#{stimulize_path(part1)}##{js_name(part2)}"
160
+ else
161
+ # 1 symbol, 1 string, 1 symbol = as above but with event
162
+ "#{part1}->#{stimulize_path(part2)}##{js_name(part3)}"
163
+ end
164
+ end
165
+
166
+ # Parse actions, targets and attributes that are passed in as symbols or strings
167
+
168
+ def parse_targets(targets)
169
+ targets.map { |n| parse_target(n) }
170
+ end
171
+
172
+ def parse_target(raw_target)
173
+ return raw_target if raw_target.is_a?(String)
174
+ return raw_target if raw_target.is_a?(Hash)
175
+ target(raw_target)
176
+ end
177
+
178
+ def build_target_data_attributes(targets)
179
+ targets.map { |t| ["#{t[:controller]}-target".to_sym, t[:name]] }.to_h
180
+ end
181
+
182
+ def parse_actions(actions)
183
+ actions.map! { |a| a.is_a?(String) ? a : action(*a) }
184
+ end
185
+
186
+ def parse_attributes(attrs, controller = nil)
187
+ attrs.transform_keys { |k| "#{controller || implied_controller_name}-#{k}" }
188
+ end
189
+
190
+ def data_map_attributes
191
+ return {} unless @data_maps
192
+ @data_maps.each_with_object({}) do |m, obj|
193
+ if m.is_a?(Hash)
194
+ obj.merge!(parse_attributes(m))
195
+ elsif m.is_a?(Array)
196
+ controller_path = m.first
197
+ data = m.last
198
+ obj.merge!(parse_attributes(data, stimulize_path(controller_path)))
199
+ end
200
+ end
201
+ end
202
+
203
+ def parse_named_classes_hash(named_classes)
204
+ named_classes.map do |name, classes|
205
+ logical_name = name.to_s.dasherize
206
+ classes_str = convert_classes_list_to_string(classes)
207
+ if classes.is_a?(Hash)
208
+ {controller: stimulize_path(classes[:controller_path]), name: logical_name, classes: classes_str}
209
+ else
210
+ {controller: implied_controller_name, name: logical_name, classes: classes_str}
211
+ end
212
+ end
213
+ end
214
+
215
+ def build_named_classes_data_attributes(named_classes)
216
+ parse_named_classes_hash(named_classes)
217
+ .map { |c| ["#{c[:controller]}-#{c[:name]}-class", c[:classes]] }
218
+ .to_h
219
+ end
220
+
221
+ def convert_classes_list_to_string(classes)
222
+ return "" if classes.nil?
223
+ return classes if classes.is_a?(String)
224
+ return classes.join(" ") if classes.is_a?(Array)
225
+ classes[:classes].is_a?(Array) ? classes[:classes].join(" ") : classes[:classes]
226
+ end
227
+
228
+ # Convert a file path to a stimulus controller name
229
+ def stimulize_path(path)
230
+ path.split("/").map { |p| p.to_s.dasherize }.join("--")
231
+ end
232
+
233
+ # Convert a Ruby 'snake case' string to a JavaScript camel case strings
234
+ def js_name(name)
235
+ name.to_s.camelize(:lower)
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Gem.loaded_specs.has_key? "phlex"
4
+ require "phlex"
5
+
6
+ module Vident
7
+ module RootComponent
8
+ class UsingPhlexHTML < Phlex::HTML
9
+ include Base
10
+
11
+ VALID_TAGS = Set[*(Phlex::HTML::VOID_ELEMENTS.keys + Phlex::HTML::STANDARD_ELEMENTS.keys)].freeze
12
+
13
+ # Create a tag for a target with a block containing content
14
+ def target_tag(tag_name, targets, **options, &block)
15
+ parsed = parse_targets(Array.wrap(targets))
16
+ options[:data] ||= {}
17
+ options[:data].merge!(build_target_data_attributes(parsed))
18
+ generate_tag(tag_name, **options, &block)
19
+ end
20
+
21
+ # Build a tag with the attributes determined by this components properties and stimulus
22
+ # data attributes.
23
+ def template(&block)
24
+ # Generate tag options and render
25
+ tag_type = @element_tag.presence&.to_sym || :div
26
+ raise ArgumentError, "Unsupported HTML tag name #{tag_type}" unless VALID_TAGS.include?(tag_type)
27
+ options = @html_options&.dup || {}
28
+ data_attrs = tag_data_attributes
29
+ data_attrs = options[:data].present? ? data_attrs.merge(options[:data]) : data_attrs
30
+ options = options.merge(id: @id) if @id
31
+ options.except!(:data)
32
+ options.merge!(data_attrs.transform_keys { |k| "data-#{k}" })
33
+ generate_tag(tag_type, **options, &block)
34
+ end
35
+
36
+ private
37
+
38
+ def generate_tag(tag_type, **options, &block)
39
+ send((tag_type == :template) ? :template_tag : tag_type, **options, &block)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Gem.loaded_specs.has_key? "view_component"
4
+ require "view_component"
5
+
6
+ module Vident
7
+ module RootComponent
8
+ class UsingViewComponent < ::ViewComponent::Base
9
+ include Base
10
+
11
+ SELF_CLOSING_TAGS = Set[:area, :base, :br, :col, :embed, :hr, :img, :input, :link, :meta, :param, :source, :track, :wbr].freeze
12
+
13
+ def target_tag(tag_name, targets, **options, &block)
14
+ parsed = parse_targets(Array.wrap(targets))
15
+ options[:data] ||= {}
16
+ options[:data].merge!(build_target_data_attributes(parsed))
17
+ content = view_context.capture(&block) if block
18
+ view_context.content_tag(tag_name, content, options)
19
+ end
20
+
21
+ def call
22
+ # Capture inner block content
23
+ # It's important that we capture the block content before generating the tag options.
24
+ # This is because the content could contain calls to `s.data_map`.
25
+ # These calls add key-value pairs to the internal data_map, which can then be translated into
26
+ # the correct `data-*` attrs by the tag options.
27
+ generated = content
28
+
29
+ # Generate outer tag options and render
30
+ tag_type = @element_tag.presence || :div
31
+ options = @html_options&.dup || {}
32
+ data_attrs = tag_data_attributes
33
+ options[:data] = options[:data].present? ? data_attrs.merge(options[:data]) : data_attrs
34
+ options = options.merge(id: @id) if @id
35
+ if SELF_CLOSING_TAGS.include?(tag_type)
36
+ view_context.tag(tag_type, options)
37
+ else
38
+ view_context.content_tag(tag_type, generated, options)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ class StableId
5
+ class << self
6
+ def set_current_sequence_generator
7
+ ::Thread.current[:vident_number_sequence_generator] = id_sequence_generator
8
+ end
9
+
10
+ def next_id_in_sequence
11
+ generator = ::Thread.current[:vident_number_sequence_generator]
12
+ return "?" unless generator
13
+ generator.next.join("-")
14
+ end
15
+
16
+ private
17
+
18
+ def id_sequence_generator
19
+ number_generator = Random.new(296_865_628_524)
20
+ Enumerator.produce { number_generator.rand(10_000_000) }.with_index
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Gem.loaded_specs.has_key? "dry-struct"
4
+ require_relative "./attributes/typed"
5
+
6
+ module Vident
7
+ module TypedComponent
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include Vident::Base
12
+ include Vident::Attributes::Typed
13
+
14
+ attribute :id, String, delegates: false
15
+ attribute :html_options, Hash, delegates: false
16
+ attribute :element_tag, Symbol, delegates: false
17
+
18
+ # StimulusJS support
19
+ attribute :controllers, Array, default: [], delegates: false
20
+ attribute :actions, Array, default: [], delegates: false
21
+ attribute :targets, Array, default: [], delegates: false
22
+ attribute :data_maps, Array, default: [], delegates: false
23
+
24
+ # TODO normalise the syntax of defining actions, controllers, etc
25
+ attribute :named_classes, Hash, delegates: false
26
+ end
27
+
28
+ def initialize(attrs = {})
29
+ before_initialise(attrs)
30
+ prepare_attributes(attrs)
31
+ # The attributes need to also be set as ivars
32
+ attributes.each do |attr_name, attr_value|
33
+ instance_variable_set(self.class.attribute_ivar_names[attr_name], attr_value)
34
+ end
35
+ after_initialise
36
+ super()
37
+ end
38
+ end
39
+ end
40
+ else
41
+ module Vident
42
+ module TypedComponent
43
+ def self.included(base)
44
+ raise "Vident::TypedComponent requires dry-struct to be installed"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vident
4
+ VERSION = "0.1.0"
5
+ end
data/lib/vident.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vident/version"
4
+ require_relative "vident/railtie"
5
+
6
+ module Vident
7
+ class << self
8
+ def configuration
9
+ @configuration ||= Configuration.new
10
+ end
11
+
12
+ def configure
13
+ yield(configuration) if block_given?
14
+ configuration
15
+ end
16
+ end
17
+
18
+ class Configuration
19
+ attr_accessor :include_i18n_helpers
20
+
21
+ def initialize
22
+ @include_i18n_helpers = true
23
+ end
24
+ end
25
+ end
26
+
27
+ require_relative "vident/stable_id"
28
+ require_relative "vident/root_component/base"
29
+ require_relative "vident/root_component/using_phlex_html"
30
+ require_relative "vident/root_component/using_view_component"
31
+ require_relative "vident/base"
32
+ require_relative "vident/component"
33
+ require_relative "vident/typed_component"
data/sig/vident.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Vident
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vident
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Ierodiaconou
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ description: Vident makes using Stimulus with your `ViewComponent` or `Phlex` view
28
+ components as easy as writing Ruby. It also provides a base class for your components
29
+ to inherit from.
30
+ email:
31
+ - stevegeek@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".standard.yml"
37
+ - CODE_OF_CONDUCT.md
38
+ - Gemfile
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - examples/ex1.gif
43
+ - lib/tasks/vident.rake
44
+ - lib/vident.rb
45
+ - lib/vident/attributes/not_typed.rb
46
+ - lib/vident/attributes/typed.rb
47
+ - lib/vident/attributes/typed_niling_struct.rb
48
+ - lib/vident/attributes/types.rb
49
+ - lib/vident/base.rb
50
+ - lib/vident/component.rb
51
+ - lib/vident/railtie.rb
52
+ - lib/vident/root_component/base.rb
53
+ - lib/vident/root_component/using_phlex_html.rb
54
+ - lib/vident/root_component/using_view_component.rb
55
+ - lib/vident/stable_id.rb
56
+ - lib/vident/typed_component.rb
57
+ - lib/vident/version.rb
58
+ - sig/vident.rbs
59
+ homepage: https://github.com/stevegeek/vident
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://github.com/stevegeek/vident
64
+ source_code_uri: https://github.com/stevegeek/vident
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 3.0.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.3.7
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Vident is a view component base class for your design system implementation,
84
+ which provides helpers for working with Stimulus. For ViewComponent and Phlex.
85
+ test_files: []