vident-view_component 2.0.1 → 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa78ec0f7f2aad87ee0b10d3f4d354d294332a29dc0b7ad9814b44791739cb06
4
- data.tar.gz: da78bd43e7b73a5ce53e14126ad297950e6ab18bfb2ea3338ce37a4dbb4b68f4
3
+ metadata.gz: f8a28dd067143483f3c2e19e53665891d1d7044cb722bbb508cbc4a7b5efd987
4
+ data.tar.gz: b672d968e7b6916c1c8b57725917fa577b29574e1e0094b886dbdc21a281a7ae
5
5
  SHA512:
6
- metadata.gz: 1c5d0e27e9ced3bbc2a94e72c4593c2bc3aa69527f64bcf24e86612bd2f6d32a3bb929ee9a87cd1331088eace2e619cd3c4d0491063d3b8db0945479fe9ab2c1
7
- data.tar.gz: d3bb8e28b96a8b7e33bd538f1e4b0c01a377d7f806d6d625b9aee0e3908bfc60b65b73e6bcb2ec46bc25b1c3469aa82f381c87573a5054f3165beadad15b6721
6
+ metadata.gz: 8e102228d3387732845eabe708afb71dc4af84f7c4a810d22a34a5dbdc9855cb141c09658e09a3973653177dae44c10a1988f8ac30effe26feb51cc9df1ab48e
7
+ data.tar.gz: f0299d8efddff1c7bca06025c61c3f65e8064de8acb8d1c94ed79cc828444c5110b6ecf0f2fb02a58cbad46b8343779731035ac81d072f75483d954a6e302eab
data/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
 
9
+ ## [3.0.0] - 2026-05-04
10
+
11
+ A bare `String` now means the same thing across every Stimulus primitive — a controller path. The 2.x ambiguity where a `String` could variously mean controller path, outlet name, CSS selector, or fully-qualified action descriptor (depending on primitive and position) is gone, closing a class of silent-failure footguns. See `UPGRADING.md` "Upgrading to Vident 3.0" for symptom → fix on each breaking change.
12
+
13
+ ### Breaking
14
+
15
+ - **Outlet: bare String is no longer a CSS selector** (#32). Wrap verbatim selectors in `Vident::Selector(...)`, or pass `nil` for auto-selector. Class-level `stimulus_outlet(name, selector)` and the `[Symbol, String]` / `[String, String]` array forms follow the same rule.
16
+ - **Action: bare String is no longer a qualified descriptor.** Use structured args `:event, "ctrl", :method`, the Hash descriptor, or `Vident::Stimulus::Action.parse_descriptor(s)` for genuine wire strings.
17
+ - **Target: bare String is no longer a target name.** Names must be Symbols. The cross-controller `[String, Symbol]` form is unchanged.
18
+
19
+ ### Added
20
+
21
+ - `Vident::Stimulus::Selector` value class + `Vident::Selector(css)` (and `Vident::Stimulus.Selector(css)`) constructors for tagging verbatim CSS selectors at outlet sites (#32).
22
+ - `Vident::Stimulus::Action.parse_descriptor(string)` — public escape hatch for parsing wire-format Stimulus action descriptors verbatim (formerly the private `parse_qualified_string`).
23
+ - Component scaffold generators: `bin/rails g vident:phlex:component …` and `bin/rails g vident:view_component:component …` scaffold a component, sidecar Stimulus controller, and unit test, with `--skip-stimulus` / `--skip-controller` / `--skip-test` / `--typescript` / `--parent` flags.
24
+ - `vident:component` umbrella generator dispatches to the right engine; requires `--engine=phlex` or `--engine=view_component` when both adapters are loaded.
25
+ - `vident:install` now scaffolds `ApplicationPhlexComponent` / `ApplicationViewComponent` based on the installed adapter gem, mirroring `ApplicationRecord` / `ApplicationController`.
26
+
27
+ ### Changed
28
+
29
+ - `bin/rails generate vident:install --force` overwrites an existing `.claude/skills/vident/SKILL.md` so upgrades can refresh it; without `--force` the existing file is preserved.
30
+
31
+
9
32
  ## [2.0.1] - 2026-04-24
10
33
 
11
34
  ### Fixed
data/README.md CHANGED
@@ -1,10 +1,28 @@
1
- # Vident
1
+ <p align="center">
2
+ <a href="https://vident-gem.diaconou.com">
3
+ <img src="https://vident-gem.diaconou.com/assets/img/vident-logo.png" alt="Vident" width="180">
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">Vident</h1>
8
+
9
+ <p align="center">
10
+ <strong>Type-safe Rails components with first-class Stimulus, on Phlex or ViewComponent.</strong>
11
+ <br>
12
+ <a href="https://vident-gem.diaconou.com">Documentation</a>
13
+ &nbsp;·&nbsp;
14
+ <a href="https://vident-gem.diaconou.com/introduction/getting-started/">Getting started</a>
15
+ &nbsp;·&nbsp;
16
+ <a href="https://rubygems.org/gems/vident">RubyGems</a>
17
+ </p>
2
18
 
3
19
  A powerful Ruby gem for building interactive, type-safe components in Rails applications with seamless [Stimulus.js](https://stimulus.hotwired.dev/) integration.
4
20
 
5
21
  Vident supports both [ViewComponent](https://viewcomponent.org/) and [Phlex](https://www.phlex.fun/) rendering engines while providing a consistent API for creating
6
22
  reusable UI components powered by [Stimulus.js](https://stimulus.hotwired.dev/).
7
23
 
24
+ > **Full documentation lives at [vident-gem.diaconou.com](https://vident-gem.diaconou.com)** — including a live, clickable component demo on the home page.
25
+
8
26
  ## Table of Contents
9
27
 
10
28
  - [Introduction](#introduction)
@@ -62,7 +80,31 @@ bundle install
62
80
  bin/rails generate vident:install
63
81
  ```
64
82
 
65
- The `vident:install` generator writes `config/initializers/vident.rb`, wires per-request ID seeding into `ApplicationController`, and (if you use Claude Code) drops a Vident skill into `.claude/skills/vident/SKILL.md` so the model has first-party guidance on the gem's conventions. See [Element IDs and request-scoped seeding](#element-ids-and-request-scoped-seeding) for the initializer rationale, and [Claude Code skill](#claude-code-skill) for the skill.
83
+ The `vident:install` generator writes `config/initializers/vident.rb`, wires per-request ID seeding into `ApplicationController`, generates `app/components/application_phlex_component.rb` and/or `application_view_component.rb` (one per engine gem in your Gemfile, mirroring `ApplicationRecord`), and (if you use Claude Code) drops a Vident skill into `.claude/skills/vident/SKILL.md` so the model has first-party guidance on the gem's conventions. See [Element IDs and request-scoped seeding](#element-ids-and-request-scoped-seeding) for the initializer rationale, and [Claude Code skill](#claude-code-skill) for the skill.
84
+
85
+ ### Scaffolding components
86
+
87
+ Once `vident:install` has run, scaffold a component, its Stimulus controller sidecar, and a unit test in one go:
88
+
89
+ ```bash
90
+ bin/rails generate vident:phlex:component Dashboard::TaskCard
91
+ bin/rails generate vident:view_component:component Dashboard::TaskCard
92
+ ```
93
+
94
+ There's also an umbrella `vident:component` dispatcher that picks the right engine when only one is in the Gemfile (pass `--engine=phlex` or `--engine=view_component` if both are):
95
+
96
+ ```bash
97
+ bin/rails generate vident:component Dashboard::TaskCard
98
+ ```
99
+
100
+ Useful flags:
101
+ - `--skip-stimulus` — omit the `stimulus do` block and the JS controller sidecar.
102
+ - `--skip-controller` — omit the JS sidecar but keep the `stimulus do` block (e.g. when sharing a controller).
103
+ - `--skip-test` — skip the unit test.
104
+ - `--typescript` / `-t` — emit a `.ts` controller instead of `.js`.
105
+ - `--parent=ClassName` — override the default base class.
106
+
107
+ A trailing `Component` in the input is stripped, so `g vident:component TaskCardComponent` and `g vident:component TaskCard` produce the same files.
66
108
 
67
109
  ## Quick Start
68
110
 
@@ -640,12 +682,16 @@ Parallel helpers exist for every attribute kind: `as_stimulus_controller(s)`, `a
640
682
  ```ruby
641
683
  class DashboardComponent < Vident::ViewComponent::Base
642
684
  stimulus do
643
- # kwarg form: outlet name is the implied controller's identifier
644
- outlets modal: ".modal", user_status: ".online-user"
645
-
646
- # positional-hash form: required when the outlet identifier contains "--"
647
- # (e.g. cross-namespace controllers) because Ruby kwarg keys can't have dashes
648
- outlets({"admin--users" => ".admin-users"})
685
+ # kwarg form: key is the *child controller identifier*. nil = auto-selector
686
+ # (`#<host-id> [data-controller~=modal]`); wrap a verbatim CSS selector in
687
+ # Vident::Selector(...). Bare String values are rejected.
688
+ outlets modal: nil
689
+ outlets user_status: Vident::Selector(".online-user")
690
+
691
+ # positional-hash form: required when the child controller identifier contains "--"
692
+ # (e.g. cross-namespace) because Ruby kwarg keys can't carry dashes
693
+ outlets({"admin--users" => nil})
694
+ outlets({"admin--users" => Vident::Selector(".admin-users")})
649
695
  end
650
696
  end
651
697
  ```
@@ -654,14 +700,15 @@ Or via the `stimulus_outlets:` prop / `root_element_attributes`:
654
700
 
655
701
  ```ruby
656
702
  stimulus_outlets: [
657
- [:modal, ".modal"], # [name, selector] on implied controller
658
- ["admin/users", :row, ".user-row"], # [controller_path, name, selector] for cross-controller
659
- :user_status, # single symbol → auto-selector (see below)
660
- other_component # component instance reuses its stimulus_identifier + id
703
+ :user_status, # symbol → auto-selector
704
+ [:modal, Vident::Selector(".modal")], # [name, Selector] verbatim
705
+ ["admin/users", :row], # cross-controller, auto-selector
706
+ ["admin/users", :row, Vident::Selector(".x")], # cross-controller, verbatim
707
+ other_component # component instance → reuses its stimulus_identifier + id
661
708
  ]
662
709
  ```
663
710
 
664
- **Auto-generated selectors.** Pass just a name (symbol or string) and the selector becomes `[data-controller~=<name>]`. Pass a component instance and the selector additionally scopes to the component's id (`#<id> [data-controller~=...]`), which is what lets you target a specific instance rather than every matching controller on the page.
711
+ **Auto-generated selectors.** Pass just a name (Symbol or String) and the selector becomes `#<host-id> [data-controller~=<name>]` scoped to this component's element id so you target a specific instance rather than every matching controller on the page. Wrap a verbatim CSS selector in `Vident::Selector(...)` to opt out of auto-scoping. A bare String inside an outlet position is treated as a controller identifier, never as a CSS selector — that ambiguity used to silently produce broken outlets in v2 and is now rejected.
665
712
 
666
713
  **Self-registration via `stimulus_outlet_host`.** A built-in prop on every Vident component. When set to another component, the child registers itself as an outlet on that host at initialization — the host doesn't need to know about the child in its DSL:
667
714
 
@@ -671,7 +718,7 @@ render DashboardComponent.new do |dashboard|
671
718
  end
672
719
  ```
673
720
 
674
- The modal now appears on the dashboard's root element as `data-dashboard-component-modal-component-outlet="#<modal-id>"` without the dashboard declaring it upfront.
721
+ The modal now appears on the dashboard's root element as `data-dashboard-component-modal-component-outlet="#<dashboard-id> [data-controller~=modal-component]"` without the dashboard declaring it upfront.
675
722
 
676
723
  **On child elements** — `child_element` accepts `stimulus_outlet:` (singular) and `stimulus_outlets:` (plural / Enumerable) exactly like the target/action kwargs, so a nested `<div>` can carry its own outlet declarations.
677
724
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Vident
6
+ module ViewComponent
7
+ module Generators
8
+ class ComponentGenerator < ::Rails::Generators::NamedBase
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Scaffold a Vident ViewComponent (.rb + .html.erb), its Stimulus controller sidecar, and a unit test."
12
+
13
+ class_option :skip_stimulus, type: :boolean, default: false,
14
+ desc: "Omit the stimulus DSL block and the JS controller sidecar"
15
+ class_option :skip_controller, type: :boolean, default: false,
16
+ desc: "Omit the JS controller sidecar (keeps the stimulus DSL block)"
17
+ class_option :skip_test, type: :boolean, default: false,
18
+ desc: "Skip generating a unit test"
19
+ class_option :typescript, type: :boolean, default: false, aliases: "-t",
20
+ desc: "Emit a TypeScript controller (.ts) instead of JavaScript (.js)"
21
+ class_option :parent, type: :string, default: "ApplicationViewComponent",
22
+ desc: "Parent class for the component"
23
+
24
+ def create_component_file
25
+ template "component.rb.tt", File.join("app/components", class_path, "#{file_name}_component.rb")
26
+ end
27
+
28
+ def create_template_file
29
+ template "component.html.erb.tt", File.join("app/components", class_path, "#{file_name}_component.html.erb")
30
+ end
31
+
32
+ def create_controller_file
33
+ return if options[:skip_stimulus] || options[:skip_controller]
34
+ ext = options[:typescript] ? "ts" : "js"
35
+ template "controller.#{ext}.tt", File.join("app/components", class_path, "#{file_name}_component_controller.#{ext}")
36
+ end
37
+
38
+ def create_test_file
39
+ return if options[:skip_test]
40
+ template "component_test.rb.tt", File.join("test/components", class_path, "#{file_name}_component_test.rb")
41
+ end
42
+
43
+ private
44
+
45
+ # Allow `g vident:view_component:component TaskCardComponent` to produce
46
+ # the same files as `g ... TaskCard` rather than `TaskCardComponentComponent`.
47
+ def class_name
48
+ super.sub(/Component\z/, "")
49
+ end
50
+
51
+ def file_name
52
+ super.sub(/_component\z/, "")
53
+ end
54
+
55
+ def component_class_name
56
+ "#{class_name}Component"
57
+ end
58
+
59
+ def parent_class
60
+ options[:parent]
61
+ end
62
+
63
+ def stimulus_block?
64
+ !options[:skip_stimulus]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ <%%= root_element do %>
2
+ <h3 class="font-semibold"><%%= title %></h3>
3
+ <%% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= component_class_name %> < <%= parent_class %>
5
+ prop :title, String, reader: :public
6
+
7
+ <% if stimulus_block? -%>
8
+ stimulus do
9
+ values_from_props :title
10
+ action(:select).on(:click)
11
+ end
12
+
13
+ <% end -%>
14
+ def root_element_attributes
15
+ {html_options: {class: "rounded border p-4"}}
16
+ end
17
+ end
18
+ <% end -%>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ class <%= component_class_name %>Test < ViewComponent::TestCase
7
+ test "renders the title" do
8
+ render_inline(<%= component_class_name %>.new(title: "Hello"))
9
+ assert_text "Hello"
10
+ end
11
+ end
12
+ <% end -%>
@@ -0,0 +1,11 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ title: String,
6
+ }
7
+
8
+ select(event) {
9
+ this.dispatch("selected", { detail: { title: this.titleValue } })
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ title: String,
6
+ }
7
+
8
+ declare readonly titleValue: string
9
+
10
+ select(event: Event): void {
11
+ this.dispatch("selected", { detail: { title: this.titleValue } })
12
+ }
13
+ }
@@ -72,42 +72,6 @@ module Vident
72
72
  end
73
73
  end
74
74
 
75
- def as_stimulus_targets(...) = to_data_attribute_string(**stimulus_targets(...).to_h)
76
-
77
- def as_stimulus_target(...) = to_data_attribute_string(**stimulus_target(...).to_h)
78
-
79
- def as_stimulus_actions(...) = to_data_attribute_string(**stimulus_actions(...).to_h)
80
-
81
- def as_stimulus_action(...) = to_data_attribute_string(**stimulus_action(...).to_h)
82
-
83
- def as_stimulus_controllers(...) = to_data_attribute_string(**stimulus_controllers(...).to_h)
84
-
85
- def as_stimulus_controller(...) = to_data_attribute_string(**stimulus_controller(...).to_h)
86
-
87
- def as_stimulus_outlets(...) = to_data_attribute_string(**stimulus_outlets(...).to_h)
88
-
89
- def as_stimulus_outlet(...) = to_data_attribute_string(**stimulus_outlet(...).to_h)
90
-
91
- def as_stimulus_values(...) = to_data_attribute_string(**stimulus_values(...).to_h)
92
-
93
- def as_stimulus_value(...) = to_data_attribute_string(**stimulus_value(...).to_h)
94
-
95
- def as_stimulus_params(...) = to_data_attribute_string(**stimulus_params(...).to_h)
96
-
97
- def as_stimulus_param(...) = to_data_attribute_string(**stimulus_param(...).to_h)
98
-
99
- def as_stimulus_classes(...) = to_data_attribute_string(**stimulus_classes(...).to_h)
100
-
101
- def as_stimulus_class(...) = to_data_attribute_string(**stimulus_class(...).to_h)
102
-
103
- alias_method :as_target, :as_stimulus_target
104
- alias_method :as_action, :as_stimulus_action
105
- alias_method :as_controller, :as_stimulus_controller
106
- alias_method :as_outlet, :as_stimulus_outlet
107
- alias_method :as_value, :as_stimulus_value
108
- alias_method :as_param, :as_stimulus_param
109
- alias_method :as_class, :as_stimulus_class
110
-
111
75
  private
112
76
 
113
77
  def generate_child_element(tag_name, stimulus_data_attributes, options, &block)
@@ -121,20 +85,6 @@ module Vident
121
85
  view_context.content_tag(tag_name, nil, options)
122
86
  end
123
87
  end
124
-
125
- def escape_attribute_name_for_html(name)
126
- name.to_s.gsub(/[^a-zA-Z0-9\-_]/, "").tr("_", "-")
127
- end
128
-
129
- def escape_attribute_value_for_html(value)
130
- value.to_s.gsub('"', "&quot;").gsub("'", "&#39;")
131
- end
132
-
133
- def to_data_attribute_string(**attributes)
134
- attributes.map { |key, value| "data-#{escape_attribute_name_for_html(key)}=\"#{escape_attribute_value_for_html(value)}\"" }
135
- .join(" ")
136
- .html_safe
137
- end
138
88
  end
139
89
  end
140
90
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vident-view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
@@ -55,14 +55,14 @@ dependencies:
55
55
  requirements:
56
56
  - - "~>"
57
57
  - !ruby/object:Gem::Version
58
- version: 2.0.1
58
+ version: 3.0.0
59
59
  type: :runtime
60
60
  prerelease: false
61
61
  version_requirements: !ruby/object:Gem::Requirement
62
62
  requirements:
63
63
  - - "~>"
64
64
  - !ruby/object:Gem::Version
65
- version: 2.0.1
65
+ version: 3.0.0
66
66
  - !ruby/object:Gem::Dependency
67
67
  name: view_component
68
68
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +93,12 @@ files:
93
93
  - CHANGELOG.md
94
94
  - LICENSE.txt
95
95
  - README.md
96
+ - lib/generators/vident/view_component/component/component_generator.rb
97
+ - lib/generators/vident/view_component/component/templates/component.html.erb.tt
98
+ - lib/generators/vident/view_component/component/templates/component.rb.tt
99
+ - lib/generators/vident/view_component/component/templates/component_test.rb.tt
100
+ - lib/generators/vident/view_component/component/templates/controller.js.tt
101
+ - lib/generators/vident/view_component/component/templates/controller.ts.tt
96
102
  - lib/vident-view_component.rb
97
103
  - lib/vident/view_component.rb
98
104
  - lib/vident/view_component/base.rb