vident 2.0.0 → 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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +61 -14
- data/lib/generators/vident/component/component_generator.rb +65 -0
- data/lib/generators/vident/install/install_generator.rb +27 -4
- data/lib/generators/vident/install/templates/application_phlex_component.rb.tt +5 -0
- data/lib/generators/vident/install/templates/application_view_component.rb.tt +4 -0
- data/lib/tasks/website.rake +87 -0
- data/lib/vident/capabilities/stimulus_attribute_strings.rb +44 -0
- data/lib/vident/capabilities/stimulus_parsing.rb +3 -3
- data/lib/vident/component.rb +1 -0
- data/lib/vident/internals/dsl.rb +21 -2
- data/lib/vident/stimulus/action.rb +9 -4
- data/lib/vident/stimulus/outlet.rb +43 -38
- data/lib/vident/stimulus/selector.rb +13 -0
- data/lib/vident/stimulus/target.rb +5 -2
- data/lib/vident/types.rb +1 -0
- data/lib/vident/version.rb +1 -1
- data/lib/vident.rb +6 -2
- data/skills/vident/SKILL.md +30 -8
- data/skills/vident/api-reference.md +24 -10
- data/skills/vident/examples.md +6 -2
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad2b06ce20d99e816db5362f70a9ef4e068c9f6452d629253eb2283efc7e4b7e
|
|
4
|
+
data.tar.gz: 5d91a89eef5349d1ba7922875cef900c696684d2a627a5c958a7f5ed5d237ba9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19e30e85766415738689f4a746ade4c21fe4d649b83212f2f8cfeb829e93a4b7109d625ad46fb3391446afb9e7b6865a9542a78bac1be4f2cfca3644769b7902
|
|
7
|
+
data.tar.gz: f4f93452b54d1971ea135a39eb079f4a3e559b5057400154dbaf5998a2e22c072c70f6dd7cf786c14648de891ebeca2b959a5a9be670b7c8ea70a5249e91e207
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ 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
|
+
|
|
32
|
+
## [2.0.1] - 2026-04-24
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Base gem no longer fails to load when installed without the adapter gems — adapter requires moved to their own entry points (`lib/vident-phlex.rb`, `lib/vident-view_component.rb`) (#29).
|
|
37
|
+
|
|
38
|
+
|
|
9
39
|
## [2.0.0] - 2026-04-24
|
|
10
40
|
|
|
11
41
|
Vident 2.0 is a ground-up rearchitecture of the DSL, attribute resolution, and composition model. The public shape of `stimulus do ... end`, `root_element`, `child_element`, outlets, props, and the `stimulus_*:` prop/kwarg API is preserved for common cases, but several internals and a handful of edge-case behaviours changed. See `doc/reviews/v1-gotchas.md` for the full list of fixed gotchas; the highlights are below.
|
data/README.md
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
|
|
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
|
+
·
|
|
14
|
+
<a href="https://vident-gem.diaconou.com/introduction/getting-started/">Getting started</a>
|
|
15
|
+
·
|
|
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:
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
658
|
-
[
|
|
659
|
-
:
|
|
660
|
-
|
|
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 (
|
|
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="#<
|
|
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,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Vident
|
|
6
|
+
module Generators
|
|
7
|
+
class ComponentGenerator < ::Rails::Generators::NamedBase
|
|
8
|
+
desc "Scaffold a Vident component. Dispatches to vident:phlex:component or vident:view_component:component based on which engine gem is loaded. Pass --engine to disambiguate when both are present."
|
|
9
|
+
|
|
10
|
+
class_option :engine, type: :string, default: nil,
|
|
11
|
+
desc: "Which engine to scaffold for: phlex or view_component"
|
|
12
|
+
class_option :skip_stimulus, type: :boolean, default: false
|
|
13
|
+
class_option :skip_controller, type: :boolean, default: false
|
|
14
|
+
class_option :skip_test, type: :boolean, default: false
|
|
15
|
+
class_option :typescript, type: :boolean, default: false, aliases: "-t"
|
|
16
|
+
class_option :parent, type: :string, default: nil
|
|
17
|
+
|
|
18
|
+
def dispatch
|
|
19
|
+
target = resolve_target_generator
|
|
20
|
+
invoke target, [name], forwarded_options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def resolve_target_generator
|
|
26
|
+
engine = options[:engine]
|
|
27
|
+
if engine.nil?
|
|
28
|
+
available = available_engines
|
|
29
|
+
if available.empty?
|
|
30
|
+
raise ::Thor::Error,
|
|
31
|
+
"No Vident engine gem detected. Add `vident-phlex` or `vident-view_component` to your Gemfile."
|
|
32
|
+
elsif available.size == 1
|
|
33
|
+
generator_for(available.first)
|
|
34
|
+
else
|
|
35
|
+
raise ::Thor::Error,
|
|
36
|
+
"Both vident-phlex and vident-view_component are loaded. Pass --engine=phlex or --engine=view_component."
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
unless %w[phlex view_component].include?(engine)
|
|
40
|
+
raise ::Thor::Error, "Unknown engine '#{engine}'. Use --engine=phlex or --engine=view_component."
|
|
41
|
+
end
|
|
42
|
+
generator_for(engine.to_sym)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def available_engines
|
|
47
|
+
engines = []
|
|
48
|
+
engines << :phlex if defined?(::Vident::Phlex::HTML)
|
|
49
|
+
engines << :view_component if defined?(::Vident::ViewComponent::Base)
|
|
50
|
+
engines
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generator_for(engine)
|
|
54
|
+
case engine
|
|
55
|
+
when :phlex then "vident:phlex:component"
|
|
56
|
+
when :view_component then "vident:view_component:component"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def forwarded_options
|
|
61
|
+
options.to_h.except("engine").transform_keys(&:to_s).reject { |_, v| v.nil? }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -9,21 +9,29 @@ module Vident
|
|
|
9
9
|
|
|
10
10
|
desc "Install Vident: writes a StableId strategy initializer, wires a per-request seed into ApplicationController, and copies the Vident Claude Code skill to .claude/skills/vident/."
|
|
11
11
|
|
|
12
|
-
# Path to the gem's ./skills directory, resolved relative to this file.
|
|
13
12
|
SKILL_SOURCE = File.expand_path("../../../../skills/vident/SKILL.md", __dir__)
|
|
14
13
|
|
|
15
14
|
def create_initializer
|
|
16
15
|
template "vident.rb", "config/initializers/vident.rb"
|
|
17
16
|
end
|
|
18
17
|
|
|
18
|
+
def create_application_components
|
|
19
|
+
write_application_component("application_phlex_component.rb") if defined?(::Vident::Phlex::HTML)
|
|
20
|
+
write_application_component("application_view_component.rb") if defined?(::Vident::ViewComponent::Base)
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
def install_claude_skill
|
|
24
|
+
return unless File.exist?(SKILL_SOURCE)
|
|
20
25
|
destination = ".claude/skills/vident/SKILL.md"
|
|
21
26
|
absolute = File.expand_path(destination, destination_root)
|
|
22
|
-
|
|
27
|
+
# Preserve an existing skill file (user may have edited it);
|
|
28
|
+
# `--force` pulls the current SKILL from the installed gem over
|
|
29
|
+
# the top so upgrades can refresh it.
|
|
30
|
+
if File.exist?(absolute) && !options[:force]
|
|
23
31
|
say_status :exist, destination, :blue
|
|
24
|
-
|
|
32
|
+
else
|
|
25
33
|
empty_directory(File.dirname(destination))
|
|
26
|
-
copy_file(SKILL_SOURCE, destination)
|
|
34
|
+
copy_file(SKILL_SOURCE, destination, force: true)
|
|
27
35
|
end
|
|
28
36
|
end
|
|
29
37
|
|
|
@@ -48,6 +56,21 @@ module Vident
|
|
|
48
56
|
|
|
49
57
|
inject_into_class controller_path, "ApplicationController", "\n#{hook}"
|
|
50
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Mirror the skill file's preserve-on-existing semantics: re-running
|
|
63
|
+
# the install generator should not clobber a base class the user has
|
|
64
|
+
# extended. `--force` opts back into overwriting.
|
|
65
|
+
def write_application_component(filename)
|
|
66
|
+
destination = "app/components/#{filename}"
|
|
67
|
+
absolute = File.expand_path(destination, destination_root)
|
|
68
|
+
if File.exist?(absolute) && !options[:force]
|
|
69
|
+
say_status :exist, destination, :blue
|
|
70
|
+
else
|
|
71
|
+
template "#{filename}.tt", destination
|
|
72
|
+
end
|
|
73
|
+
end
|
|
51
74
|
end
|
|
52
75
|
end
|
|
53
76
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
WEBSITE_DIR = File.expand_path("../../website", __dir__)
|
|
4
|
+
|
|
5
|
+
namespace :website do
|
|
6
|
+
desc "Render demo components and write HTML/source fragments into the docs site"
|
|
7
|
+
task :demos do
|
|
8
|
+
require File.expand_path("../../test/dummy/config/environment", __dir__)
|
|
9
|
+
require "fileutils"
|
|
10
|
+
|
|
11
|
+
out = File.join(WEBSITE_DIR, "_includes", "demos")
|
|
12
|
+
FileUtils.mkdir_p(out)
|
|
13
|
+
|
|
14
|
+
# The homepage demo is the same Phlex component the dummy app renders at
|
|
15
|
+
# /components/tasks. We pre-render it here so the static site can ship
|
|
16
|
+
# byte-identical HTML, paired with the same Stimulus controller registered
|
|
17
|
+
# in `website/assets/js/demo.js`.
|
|
18
|
+
demos = [
|
|
19
|
+
{
|
|
20
|
+
slug: "task_card",
|
|
21
|
+
component: ::Tasks::TaskCardComponent,
|
|
22
|
+
source_path: "test/dummy/app/components/tasks/task_card_component.rb",
|
|
23
|
+
args: [
|
|
24
|
+
{task_id: 1, title: "Write the launch announcement", due: "Today", list: :today, status: :todo},
|
|
25
|
+
{task_id: 2, title: "Migrate the legacy importer", due: "Wed", list: :this_week, status: :done},
|
|
26
|
+
{task_id: 3, title: "Add Stripe webhooks", due: "—", list: :backlog, status: :wont_do}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
demos.each do |demo|
|
|
32
|
+
html = Vident::StableId.with_sequence_generator(seed: "vident-docs-#{demo[:slug]}") do
|
|
33
|
+
demo[:args].map { |a| demo[:component].new(**a).call }.join
|
|
34
|
+
end
|
|
35
|
+
html = clean(html)
|
|
36
|
+
|
|
37
|
+
File.write(File.join(out, "#{demo[:slug]}_rendered.html"), html + "\n")
|
|
38
|
+
File.write(File.join(out, "#{demo[:slug]}_html.html"), pretty_html(html))
|
|
39
|
+
File.write(
|
|
40
|
+
File.join(out, "#{demo[:slug]}_source.rb"),
|
|
41
|
+
File.read(File.expand_path("../../#{demo[:source_path]}", __dir__))
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
puts " rendered #{demo[:slug]} → #{html.bytesize} bytes"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts "Wrote demos to #{out}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "Build the documentation website"
|
|
51
|
+
task build: :demos do
|
|
52
|
+
Dir.chdir(WEBSITE_DIR) do
|
|
53
|
+
sh "bundle install"
|
|
54
|
+
sh "bundle exec jekyll build"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "Serve the documentation website locally with live reload"
|
|
59
|
+
task serve: :demos do
|
|
60
|
+
Dir.chdir(WEBSITE_DIR) do
|
|
61
|
+
sh "bundle install"
|
|
62
|
+
sh "bundle exec jekyll serve --livereload"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc "Clean the documentation website build"
|
|
67
|
+
task :clean do
|
|
68
|
+
Dir.chdir(WEBSITE_DIR) do
|
|
69
|
+
sh "bundle exec jekyll clean"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Strip the development-only "Before ..." HTML comment that the dummy
|
|
74
|
+
# ApplicationComponent injects so the embedded fragment stays clean.
|
|
75
|
+
def clean(html)
|
|
76
|
+
html.gsub(/<!--\s*Before [^>]*?-->/, "").strip
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Pretty-prints the rendered fragment for the "Rendered HTML" tab. Nokogiri
|
|
80
|
+
# escapes `>` inside attribute values to `>` (correct HTML5, but ugly to
|
|
81
|
+
# read), so we unescape that back — the result is still valid HTML and
|
|
82
|
+
# matches what a developer would expect to see in their source.
|
|
83
|
+
def pretty_html(html)
|
|
84
|
+
require "nokogiri"
|
|
85
|
+
Nokogiri::HTML5.fragment(html).to_xhtml(indent: 2).gsub(">", ">")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../internals/registry"
|
|
4
|
+
|
|
5
|
+
module Vident
|
|
6
|
+
module Capabilities
|
|
7
|
+
# `as_stimulus_*` returns a ready-to-splat HTML data-attribute string for
|
|
8
|
+
# the given stimulus declaration — usable from any rendering context
|
|
9
|
+
# (ERB partials, Phlex templates, helpers, controller renderers).
|
|
10
|
+
#
|
|
11
|
+
# Unlike `child_element`, these helpers don't write to a buffer, so they
|
|
12
|
+
# work even when called on a Phlex Vident component from outside its own
|
|
13
|
+
# `view_template` lifecycle.
|
|
14
|
+
module StimulusAttributeStrings
|
|
15
|
+
::Vident::Internals::Registry.each do |kind|
|
|
16
|
+
define_method(:"as_stimulus_#{kind.singular_name}") do |*args|
|
|
17
|
+
to_data_attribute_string(**send(:"stimulus_#{kind.singular_name}", *args).to_h)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
define_method(:"as_stimulus_#{kind.plural_name}") do |*args|
|
|
21
|
+
to_data_attribute_string(**send(:"stimulus_#{kind.plural_name}", *args).to_h)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alias_method :"as_#{kind.singular_name}", :"as_stimulus_#{kind.singular_name}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def to_data_attribute_string(**attributes)
|
|
30
|
+
attributes.map { |key, value| "data-#{escape_attribute_name_for_html(key)}=\"#{escape_attribute_value_for_html(value)}\"" }
|
|
31
|
+
.join(" ")
|
|
32
|
+
.html_safe
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def escape_attribute_name_for_html(name)
|
|
36
|
+
name.to_s.gsub(/[^a-zA-Z0-9\-_]/, "").tr("_", "-")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def escape_attribute_value_for_html(value)
|
|
40
|
+
value.to_s.gsub('"', """).gsub("'", "'")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -118,13 +118,13 @@ module Vident
|
|
|
118
118
|
|
|
119
119
|
def stimulus_outlet(*args)
|
|
120
120
|
case args
|
|
121
|
-
in [Symbol => _name,
|
|
121
|
+
in [Symbol => _name, ::Vident::Stimulus::Selector] | [String => _name, ::Vident::Stimulus::Selector]
|
|
122
122
|
::Vident::Stimulus::Outlet.parse(*args, implied: implied_controller_for_class)
|
|
123
123
|
else
|
|
124
124
|
raise ::Vident::ParseError,
|
|
125
|
-
"#{name}.stimulus_outlet requires (name,
|
|
125
|
+
"#{name}.stimulus_outlet requires (name, Vident::Selector(...)) — no component_id at class level. " \
|
|
126
126
|
"Use instance-level `component.stimulus_outlet(:name)` for auto-selector, " \
|
|
127
|
-
"or `#{name}.stimulus_outlet(:name, '.
|
|
127
|
+
"or `#{name}.stimulus_outlet(:name, Vident::Selector('.css'))` for a verbatim selector."
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
|
data/lib/vident/component.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Vident
|
|
|
14
14
|
include ::Vident::Capabilities::StimulusMutation
|
|
15
15
|
include ::Vident::Capabilities::StimulusDraft
|
|
16
16
|
include ::Vident::Capabilities::StimulusDataEmitting
|
|
17
|
+
include ::Vident::Capabilities::StimulusAttributeStrings
|
|
17
18
|
include ::Vident::Capabilities::ClassListBuilding
|
|
18
19
|
include ::Vident::Capabilities::RootElementRendering
|
|
19
20
|
include ::Vident::Capabilities::ChildElementRendering
|
data/lib/vident/internals/dsl.rb
CHANGED
|
@@ -80,11 +80,11 @@ module Vident
|
|
|
80
80
|
# Positional Hash arg supports keys like `"admin--users"` that can't be Ruby kwargs.
|
|
81
81
|
def outlets(positional = nil, **hash)
|
|
82
82
|
if positional.is_a?(Hash)
|
|
83
|
-
positional.each { |k, v|
|
|
83
|
+
positional.each { |k, v| record_outlet(k, v) }
|
|
84
84
|
elsif !positional.nil?
|
|
85
85
|
raise ArgumentError, "outlets: positional arg must be a Hash, got #{positional.class}"
|
|
86
86
|
end
|
|
87
|
-
hash.each { |k, v|
|
|
87
|
+
hash.each { |k, v| record_outlet(k, v) }
|
|
88
88
|
self
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -158,6 +158,25 @@ module Vident
|
|
|
158
158
|
replace_or_append(bucket, entry)
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
+
def record_outlet(key, value)
|
|
162
|
+
validate_outlet_value!(key, value)
|
|
163
|
+
entry = [key, value.nil? ? Declaration.of : Declaration.of(value)]
|
|
164
|
+
replace_or_append(@outlets, entry)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def validate_outlet_value!(key, value)
|
|
168
|
+
return if value.nil?
|
|
169
|
+
return if value.is_a?(Proc)
|
|
170
|
+
return if value.is_a?(::Vident::Stimulus::Selector)
|
|
171
|
+
return if value.is_a?(::Vident::Stimulus::Outlet)
|
|
172
|
+
return if value.respond_to?(:stimulus_identifier)
|
|
173
|
+
raise ::Vident::ParseError,
|
|
174
|
+
"outlets: value for #{key.inspect} must be nil (auto-selector), " \
|
|
175
|
+
"a `Vident::Selector(…)`, a Proc returning one, an Outlet, or a child component. " \
|
|
176
|
+
"Got #{value.class}: #{value.inspect}. " \
|
|
177
|
+
"If you meant a verbatim CSS selector, wrap it: `Vident::Selector(#{value.inspect})`."
|
|
178
|
+
end
|
|
179
|
+
|
|
161
180
|
def replace_or_append(bucket, entry)
|
|
162
181
|
key = entry.first
|
|
163
182
|
idx = bucket.index { |(k, _)| k == key }
|
|
@@ -34,8 +34,6 @@ module Vident
|
|
|
34
34
|
method_name: Naming.js_name(method_sym),
|
|
35
35
|
event: nil
|
|
36
36
|
)
|
|
37
|
-
in [String => s]
|
|
38
|
-
parse_qualified_string(s)
|
|
39
37
|
in [Symbol => event, Symbol => method_sym]
|
|
40
38
|
new(
|
|
41
39
|
controller: implied,
|
|
@@ -54,6 +52,12 @@ module Vident
|
|
|
54
52
|
method_name: Naming.js_name(method_sym),
|
|
55
53
|
event: event.to_s
|
|
56
54
|
)
|
|
55
|
+
in [String => s]
|
|
56
|
+
raise ::Vident::ParseError,
|
|
57
|
+
"Action.parse: a bare String is a controller path, not a fully-qualified action descriptor. " \
|
|
58
|
+
"For event/controller/method use structured args like `:click, \"path/ctrl\", :method` " \
|
|
59
|
+
"or the Hash descriptor form. To parse an existing wire-format string like " \
|
|
60
|
+
"\"click->ctrl#m\", call `Vident::Stimulus::Action.parse_descriptor(#{s.inspect})`."
|
|
57
61
|
else
|
|
58
62
|
raise ::Vident::ParseError, "Action.parse: invalid arguments #{args.inspect}"
|
|
59
63
|
end
|
|
@@ -103,8 +107,9 @@ module Vident
|
|
|
103
107
|
)
|
|
104
108
|
end
|
|
105
109
|
|
|
106
|
-
#
|
|
107
|
-
|
|
110
|
+
# Parses a wire-format Stimulus action descriptor (`"event->ctrl#method"` or
|
|
111
|
+
# `"ctrl#method"`). The controller segment is taken verbatim — not re-stimulized.
|
|
112
|
+
def self.parse_descriptor(s)
|
|
108
113
|
if s.include?("->")
|
|
109
114
|
event_part, ctrl_method = s.split("->", 2)
|
|
110
115
|
ctrl, method = ctrl_method.split("#", 2)
|
|
@@ -4,63 +4,61 @@ require "literal"
|
|
|
4
4
|
require_relative "naming"
|
|
5
5
|
require_relative "base"
|
|
6
6
|
require_relative "controller"
|
|
7
|
+
require_relative "selector"
|
|
7
8
|
|
|
8
9
|
module Vident
|
|
9
10
|
module Stimulus
|
|
10
|
-
# `data-<ctrl>-<
|
|
11
|
-
# the Stimulus runtime uses to resolve the outlet on the page.
|
|
11
|
+
# `data-<parent-ctrl>-<child-ctrl>-outlet` fragment.
|
|
12
12
|
class Outlet < Base
|
|
13
13
|
prop :controller, Controller
|
|
14
14
|
prop :name, String
|
|
15
15
|
prop :selector, String
|
|
16
16
|
|
|
17
|
+
# Characters that only make sense in a CSS selector, never in a
|
|
18
|
+
# Stimulus controller path/identifier.
|
|
19
|
+
SELECTOR_CHARS = /[.#\[>,*+:]/
|
|
20
|
+
|
|
17
21
|
def self.parse(*args, implied:, component_id: nil)
|
|
18
22
|
case args
|
|
23
|
+
in [Outlet => o]
|
|
24
|
+
o
|
|
19
25
|
in [Symbol => sym]
|
|
20
26
|
name = sym.to_s.dasherize
|
|
21
|
-
new(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
selector: auto_selector(name, component_id: component_id)
|
|
25
|
-
)
|
|
27
|
+
new(controller: implied, name: name, selector: auto_selector(name, component_id: component_id))
|
|
28
|
+
in [String => str] if SELECTOR_CHARS.match?(str)
|
|
29
|
+
raise ::Vident::ParseError, raw_string_selector_message(str)
|
|
26
30
|
in [String => str]
|
|
27
|
-
name = str
|
|
28
|
-
new(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
in [
|
|
34
|
-
|
|
35
|
-
controller: implied,
|
|
36
|
-
name: identifier.to_s.dasherize,
|
|
37
|
-
selector: selector
|
|
38
|
-
)
|
|
39
|
-
in [Symbol => sym, String => sel]
|
|
31
|
+
name = Naming.stimulize_path(str)
|
|
32
|
+
new(controller: implied, name: name, selector: auto_selector(name, component_id: component_id))
|
|
33
|
+
in [Symbol => sym, Selector => sel]
|
|
34
|
+
new(controller: implied, name: sym.to_s.dasherize, selector: sel.css)
|
|
35
|
+
in [String => str, Selector => sel]
|
|
36
|
+
new(controller: implied, name: Naming.stimulize_path(str), selector: sel.css)
|
|
37
|
+
in [String => parent_path, Symbol => child_sym]
|
|
38
|
+
child_name = child_sym.to_s.dasherize
|
|
40
39
|
new(
|
|
41
|
-
controller: implied,
|
|
42
|
-
name:
|
|
43
|
-
selector:
|
|
40
|
+
controller: Controller.parse(parent_path, implied: implied),
|
|
41
|
+
name: child_name,
|
|
42
|
+
selector: auto_selector(child_name, component_id: component_id)
|
|
44
43
|
)
|
|
45
|
-
in [String =>
|
|
44
|
+
in [String => parent_path, Symbol => child_sym, Selector => sel]
|
|
46
45
|
new(
|
|
47
|
-
controller: implied,
|
|
48
|
-
name:
|
|
49
|
-
selector: sel
|
|
50
|
-
)
|
|
51
|
-
in [String => ctrl_path, Symbol => sym, String => sel]
|
|
52
|
-
new(
|
|
53
|
-
controller: Controller.parse(ctrl_path, implied: implied),
|
|
54
|
-
name: sym.to_s.dasherize,
|
|
55
|
-
selector: sel
|
|
46
|
+
controller: Controller.parse(parent_path, implied: implied),
|
|
47
|
+
name: child_sym.to_s.dasherize,
|
|
48
|
+
selector: sel.css
|
|
56
49
|
)
|
|
57
50
|
in [obj] if obj.respond_to?(:stimulus_identifier)
|
|
58
51
|
ident = obj.stimulus_identifier
|
|
59
|
-
new(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
new(controller: implied, name: ident, selector: auto_selector(ident, component_id: component_id))
|
|
53
|
+
in [Selector => sel]
|
|
54
|
+
raise ::Vident::ParseError,
|
|
55
|
+
"Outlet.parse: a Selector must be paired with a child controller name. " \
|
|
56
|
+
"Use `(:name, Vident::Selector(#{sel.css.inspect}))` or " \
|
|
57
|
+
"`(\"some/parent\", :name, Vident::Selector(...))` for the cross-controller form."
|
|
58
|
+
in [_, String => sel]
|
|
59
|
+
raise ::Vident::ParseError, raw_string_selector_message(sel)
|
|
60
|
+
in [_, _, String => sel]
|
|
61
|
+
raise ::Vident::ParseError, raw_string_selector_message(sel)
|
|
64
62
|
else
|
|
65
63
|
raise ::Vident::ParseError, "Outlet.parse: invalid arguments #{args.inspect}"
|
|
66
64
|
end
|
|
@@ -80,6 +78,13 @@ module Vident
|
|
|
80
78
|
id.to_s.gsub(/[^A-Za-z0-9_-]/) { |c| "\\#{c.ord.to_s(16)} " }
|
|
81
79
|
end
|
|
82
80
|
|
|
81
|
+
def self.raw_string_selector_message(value)
|
|
82
|
+
"Outlet.parse: a bare String is a controller path, never a CSS selector. " \
|
|
83
|
+
"Wrap verbatim selectors in `Vident::Selector(...)` (got #{value.inspect}). " \
|
|
84
|
+
"For auto-selectors based on a child controller identifier, pass a Symbol or " \
|
|
85
|
+
"an unwrapped String — Vident builds `[data-controller~=…]` for you."
|
|
86
|
+
end
|
|
87
|
+
|
|
83
88
|
def to_s = selector
|
|
84
89
|
|
|
85
90
|
def data_attribute_key = :"#{controller.name}-#{name}-outlet"
|
|
@@ -16,13 +16,16 @@ module Vident
|
|
|
16
16
|
case args
|
|
17
17
|
in [Symbol => sym]
|
|
18
18
|
new(controller: implied, name: Naming.js_name(sym))
|
|
19
|
-
in [String => str]
|
|
20
|
-
new(controller: implied, name: str)
|
|
21
19
|
in [String => ctrl_path, Symbol => sym]
|
|
22
20
|
new(
|
|
23
21
|
controller: Controller.parse(ctrl_path, implied: implied),
|
|
24
22
|
name: Naming.js_name(sym)
|
|
25
23
|
)
|
|
24
|
+
in [String => s]
|
|
25
|
+
raise ::Vident::ParseError,
|
|
26
|
+
"Target.parse: a bare String is a controller path; target names must be Symbols " \
|
|
27
|
+
"(got #{s.inspect}). Use `target :name` for a local target, or " \
|
|
28
|
+
"`target \"path/to/ctrl\", :name` for cross-controller."
|
|
26
29
|
else
|
|
27
30
|
raise ::Vident::ParseError, "Target.parse: invalid arguments #{args.inspect}"
|
|
28
31
|
end
|
data/lib/vident/types.rb
CHANGED
data/lib/vident/version.rb
CHANGED
data/lib/vident.rb
CHANGED
|
@@ -16,6 +16,7 @@ require "vident/stimulus_null"
|
|
|
16
16
|
|
|
17
17
|
require "vident/stimulus/naming"
|
|
18
18
|
require "vident/stimulus/null"
|
|
19
|
+
require "vident/stimulus/selector"
|
|
19
20
|
require "vident/stimulus/controller"
|
|
20
21
|
require "vident/stimulus/action"
|
|
21
22
|
require "vident/stimulus/target"
|
|
@@ -44,6 +45,7 @@ require "vident/capabilities/stimulus_parsing"
|
|
|
44
45
|
require "vident/capabilities/stimulus_mutation"
|
|
45
46
|
require "vident/capabilities/stimulus_draft"
|
|
46
47
|
require "vident/capabilities/stimulus_data_emitting"
|
|
48
|
+
require "vident/capabilities/stimulus_attribute_strings"
|
|
47
49
|
require "vident/capabilities/class_list_building"
|
|
48
50
|
require "vident/capabilities/root_element_rendering"
|
|
49
51
|
require "vident/capabilities/child_element_rendering"
|
|
@@ -54,5 +56,7 @@ require "vident/caching"
|
|
|
54
56
|
|
|
55
57
|
require "vident/component"
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
# Adapter modules (`vident/phlex`, `vident/view_component`) ship in their own
|
|
60
|
+
# gems and are loaded by their own entry points (`lib/vident-phlex.rb`,
|
|
61
|
+
# `lib/vident-view_component.rb`). Do not require them unconditionally here —
|
|
62
|
+
# they only exist when the corresponding adapter gem is installed.
|
data/skills/vident/SKILL.md
CHANGED
|
@@ -273,7 +273,9 @@ These only tell the JS controller *which* classes to toggle; they do **not** app
|
|
|
273
273
|
|
|
274
274
|
### 1.7 Outlets
|
|
275
275
|
|
|
276
|
-
Stimulus: `data-<identifier>-<outlet-name>-outlet="<css-selector>"` attaches matching controller instances as `this.nameOutlet` / `this.nameOutlets` / `this.nameOutletElement(s)` / `this.hasNameOutlet`.
|
|
276
|
+
Stimulus: `data-<identifier>-<outlet-name>-outlet="<css-selector>"` attaches matching controller instances as `this.nameOutlet` / `this.nameOutlets` / `this.nameOutletElement(s)` / `this.hasNameOutlet`. The `<outlet-name>` is the kebab-case identifier of the *child* controller — Stimulus enforces this; it is not just a free label.
|
|
277
|
+
|
|
278
|
+
Argument vocabulary (same as Action/Target/Value/etc.): `Symbol` and bare `String` always denote a controller (path or identifier); a verbatim CSS selector must be wrapped with `Vident::Selector(...)`. A bare `".modal"` is rejected.
|
|
277
279
|
|
|
278
280
|
Vident has three forms.
|
|
279
281
|
|
|
@@ -281,19 +283,28 @@ Vident has three forms.
|
|
|
281
283
|
|
|
282
284
|
```ruby
|
|
283
285
|
stimulus do
|
|
284
|
-
outlets modal:
|
|
285
|
-
outlets
|
|
286
|
+
outlets modal: nil # auto-selector: [data-controller~=modal]
|
|
287
|
+
outlets user_status: Vident::Selector(".online-user")
|
|
288
|
+
outlets({"admin--users" => nil}) # positional-hash, namespaced child id
|
|
289
|
+
outlets({"admin--users" => Vident::Selector(".admin-user")})
|
|
286
290
|
end
|
|
287
291
|
```
|
|
288
292
|
|
|
293
|
+
The kwarg/Hash key is the **child controller identifier**. Use the singular `outlet` form for cross-controller cases:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
outlet "some/parent-ctrl", :child # auto-selector
|
|
297
|
+
outlet "some/parent-ctrl", :child, Vident::Selector(".x") # verbatim
|
|
298
|
+
```
|
|
299
|
+
|
|
289
300
|
**(b) `stimulus_outlets:` prop / `root_element_attributes` / `child_element(stimulus_outlet(s): …)`:**
|
|
290
301
|
|
|
291
302
|
```ruby
|
|
292
303
|
stimulus_outlets: [
|
|
293
|
-
|
|
294
|
-
[
|
|
295
|
-
:
|
|
296
|
-
other_component_instance,
|
|
304
|
+
:user_status, # auto-selector
|
|
305
|
+
[:modal, Vident::Selector(".modal")], # explicit selector
|
|
306
|
+
["admin/users", :row, Vident::Selector(".user-row")], # cross-controller, explicit
|
|
307
|
+
other_component_instance, # #<id> [data-controller~=<other identifier>]
|
|
297
308
|
]
|
|
298
309
|
```
|
|
299
310
|
|
|
@@ -362,7 +373,18 @@ Inline helper (ERB): `as_stimulus_param(:release_id, 42)` / `as_stimulus_params(
|
|
|
362
373
|
|
|
363
374
|
## 2. Component scaffolding
|
|
364
375
|
|
|
365
|
-
|
|
376
|
+
The fastest path is the bundled generator, which writes the component, its Stimulus controller sidecar, and a unit test in one go:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
bin/rails generate vident:component Dashboard::TaskCard
|
|
380
|
+
# or, when you want to be explicit:
|
|
381
|
+
bin/rails generate vident:phlex:component Dashboard::TaskCard
|
|
382
|
+
bin/rails generate vident:view_component:component Dashboard::TaskCard
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The umbrella `vident:component` dispatcher picks the engine when only one is in the Gemfile; pass `--engine=phlex` or `--engine=view_component` if both are. Useful flags: `--skip-stimulus`, `--skip-controller`, `--skip-test`, `--typescript` / `-t`, `--parent=ClassName`. A trailing `Component` in the input is stripped.
|
|
386
|
+
|
|
387
|
+
Generated components inherit from `ApplicationPhlexComponent` or `ApplicationViewComponent` (created by `vident:install`). If you're writing a component by hand, pick the right base class directly:
|
|
366
388
|
|
|
367
389
|
- **ViewComponent:** `class Foo::BarComponent < Vident::ViewComponent::Base`
|
|
368
390
|
- **Phlex:** `class Foo::BarComponent < Vident::Phlex::HTML`
|
|
@@ -22,13 +22,6 @@ Adds:
|
|
|
22
22
|
class precedence rules from SKILL.md §4). Self-closing tags (`:area`, `:br`, `:col`,
|
|
23
23
|
`:embed`, `:hr`, `:img`, `:input`, `:link`, `:meta`, `:param`, `:source`, `:track`,
|
|
24
24
|
`:wbr`) are emitted without children.
|
|
25
|
-
- 14 `as_stimulus_*` helpers — return an HTML-safe `String` of raw `data-*` attributes
|
|
26
|
-
suitable for embedding inside an HTML tag in ERB. Signatures match the corresponding
|
|
27
|
-
`stimulus_*` method (see section 4):
|
|
28
|
-
- Plural: `as_stimulus_controllers`, `as_stimulus_actions`, `as_stimulus_targets`,
|
|
29
|
-
`as_stimulus_outlets`, `as_stimulus_values`, `as_stimulus_params`, `as_stimulus_classes`.
|
|
30
|
-
- Singular: `as_stimulus_controller`, `as_stimulus_action`, `as_stimulus_target`,
|
|
31
|
-
`as_stimulus_outlet`, `as_stimulus_value`, `as_stimulus_param`, `as_stimulus_class`.
|
|
32
25
|
- Class-level cache support: `template_path`, `component_path`, `components_base_path`,
|
|
33
26
|
`cache_component_modified_time`, `cache_sidecar_view_modified_time`,
|
|
34
27
|
`cache_rb_component_modified_time` — used by `Vident::Caching` to chain
|
|
@@ -49,8 +42,13 @@ Adds:
|
|
|
49
42
|
`child_element` raises `ArgumentError`.
|
|
50
43
|
- Source-file tracking — the class-level `inherited` hook records each subclass's source
|
|
51
44
|
file in `component_source_file_path` so `Vident::Caching` can pick up an mtime.
|
|
52
|
-
-
|
|
53
|
-
|
|
45
|
+
- `child_element` lifecycle constraint — `child_element` writes to Phlex's render
|
|
46
|
+
buffer, so it is **only valid during the component's own `view_template`**. From
|
|
47
|
+
outside the render lifecycle (an external ERB partial holding a Phlex component
|
|
48
|
+
reference, a helper, `ApplicationController.renderer.render(...)`, etc.) it raises
|
|
49
|
+
`undefined method 'buffer' for nil`. Use the `as_stimulus_*` helpers (see
|
|
50
|
+
`Vident::Component`) or spread `data: { **component.stimulus_target(:name) }` inline
|
|
51
|
+
instead — those don't touch the buffer.
|
|
54
52
|
|
|
55
53
|
### `Vident::Component` (module)
|
|
56
54
|
|
|
@@ -77,6 +75,17 @@ Public instance methods:
|
|
|
77
75
|
- `id` — `String`, auto-generated from `StableId` if `@id` was nil. The generated form
|
|
78
76
|
is `"#{component_name}-#{StableId.next_id_in_sequence}"`.
|
|
79
77
|
- `prop_names` — instance-method alias for the class method.
|
|
78
|
+
- 14 `as_stimulus_*` helpers + 7 short aliases — return an HTML-safe `String` of raw
|
|
79
|
+
`data-*` attributes, suitable for embedding inside an HTML tag (ERB or Phlex `raw(...)`).
|
|
80
|
+
Signatures match the corresponding `stimulus_*` method (see section 4). Pure transforms
|
|
81
|
+
over `stimulus_*` outputs — they don't write to a render buffer, so they're safe to call
|
|
82
|
+
on a Phlex Vident component from outside its `view_template` (unlike `child_element`).
|
|
83
|
+
- Plural: `as_stimulus_controllers`, `as_stimulus_actions`, `as_stimulus_targets`,
|
|
84
|
+
`as_stimulus_outlets`, `as_stimulus_values`, `as_stimulus_params`, `as_stimulus_classes`.
|
|
85
|
+
- Singular: `as_stimulus_controller`, `as_stimulus_action`, `as_stimulus_target`,
|
|
86
|
+
`as_stimulus_outlet`, `as_stimulus_value`, `as_stimulus_param`, `as_stimulus_class`.
|
|
87
|
+
- Aliases (singular only): `as_controller`, `as_action`, `as_target`, `as_outlet`,
|
|
88
|
+
`as_value`, `as_param`, `as_class`.
|
|
80
89
|
|
|
81
90
|
Not public (override at your own risk, used internally):
|
|
82
91
|
|
|
@@ -468,7 +477,12 @@ def child_element(tag_name,
|
|
|
468
477
|
- `**options` passes through as HTML options.
|
|
469
478
|
- For ViewComponent's renderer, self-closing tags are emitted without the block.
|
|
470
479
|
- For Phlex's renderer, the tag name is validated against
|
|
471
|
-
`Vident::Phlex::HTML::VALID_TAGS`; unknown tags raise `ArgumentError`.
|
|
480
|
+
`Vident::Phlex::HTML::VALID_TAGS`; unknown tags raise `ArgumentError`. Phlex's
|
|
481
|
+
`child_element` writes to the component's render buffer, so it is **only valid
|
|
482
|
+
during the component's own `view_template`**. Calling it from outside the render
|
|
483
|
+
lifecycle (an external ERB partial, a helper, `ApplicationController.renderer.render`)
|
|
484
|
+
raises `undefined method 'buffer' for nil`. Use `as_stimulus_*` helpers there
|
|
485
|
+
instead — those have no buffer dependency.
|
|
472
486
|
|
|
473
487
|
---
|
|
474
488
|
|
data/skills/vident/examples.md
CHANGED
|
@@ -398,8 +398,12 @@ a hand-authored HTML tag. All three are equivalent; pick one per file for consis
|
|
|
398
398
|
<% end %>
|
|
399
399
|
```
|
|
400
400
|
|
|
401
|
-
Phlex users have
|
|
402
|
-
|
|
401
|
+
Phlex users have three choices — `child_element` (identical to ERB, but only valid
|
|
402
|
+
inside `view_template`), the native Phlex tag methods with
|
|
403
|
+
`data: { **component.stimulus_target(:name) }`, and the `as_stimulus_*` helpers
|
|
404
|
+
embedded via `raw(...)`. From an external ERB partial holding a Phlex component
|
|
405
|
+
reference, `child_element` won't work (no render buffer) — use `as_stimulus_*` or
|
|
406
|
+
the `data: { **stimulus_target(...) }` spread instead.
|
|
403
407
|
|
|
404
408
|
---
|
|
405
409
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vident
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Ierodiaconou
|
|
@@ -82,8 +82,12 @@ files:
|
|
|
82
82
|
- CHANGELOG.md
|
|
83
83
|
- LICENSE.txt
|
|
84
84
|
- README.md
|
|
85
|
+
- lib/generators/vident/component/component_generator.rb
|
|
85
86
|
- lib/generators/vident/install/install_generator.rb
|
|
87
|
+
- lib/generators/vident/install/templates/application_phlex_component.rb.tt
|
|
88
|
+
- lib/generators/vident/install/templates/application_view_component.rb.tt
|
|
86
89
|
- lib/generators/vident/install/templates/vident.rb
|
|
90
|
+
- lib/tasks/website.rake
|
|
87
91
|
- lib/vident.rb
|
|
88
92
|
- lib/vident/caching.rb
|
|
89
93
|
- lib/vident/capabilities/caching.rb
|
|
@@ -93,6 +97,7 @@ files:
|
|
|
93
97
|
- lib/vident/capabilities/identifiable.rb
|
|
94
98
|
- lib/vident/capabilities/inspectable.rb
|
|
95
99
|
- lib/vident/capabilities/root_element_rendering.rb
|
|
100
|
+
- lib/vident/capabilities/stimulus_attribute_strings.rb
|
|
96
101
|
- lib/vident/capabilities/stimulus_data_emitting.rb
|
|
97
102
|
- lib/vident/capabilities/stimulus_declaring.rb
|
|
98
103
|
- lib/vident/capabilities/stimulus_draft.rb
|
|
@@ -124,6 +129,7 @@ files:
|
|
|
124
129
|
- lib/vident/stimulus/null.rb
|
|
125
130
|
- lib/vident/stimulus/outlet.rb
|
|
126
131
|
- lib/vident/stimulus/param.rb
|
|
132
|
+
- lib/vident/stimulus/selector.rb
|
|
127
133
|
- lib/vident/stimulus/target.rb
|
|
128
134
|
- lib/vident/stimulus/value.rb
|
|
129
135
|
- lib/vident/stimulus_null.rb
|