一键导入
javascript
Add interactivity with Import Maps, Stimulus controllers, and reactive components — FrontLoader API, StimulusRenderer, WebSocket components
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add interactivity with Import Maps, Stimulus controllers, and reactive components — FrontLoader API, StimulusRenderer, WebSocket components
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Run the asset_pipeline convention linter (`scripts/lint_conventions.cr`) — Crystal-side runner that enforces Phase 10 Family 1 naming rules across src/, samples/, and spec/. Use before commit, in CI, or when reviewing a PR.
Build cross-platform apps with UI::App + UI::Screen + UI::Controller + UI::ActionDispatcher + UI::FormState — route declarations, native action dispatch, Amber web integration, and the static-site web target.
The developer-facing guide to building Apple UI (iOS 26, iPadOS 26, macOS 26) with asset_pipeline's UI::View system. Backed by the Apple HIG corpus and validated by macOS + iOS screenshots. Answers "I want to show X — what component, what args, what HIG says."
Complete catalog of Android Jetpack Compose and Material Design 3 components. Covers M3 Expressive (2025), gesture system, graphics/drawing, navigation patterns, theming, and mapping guidance for Crystal cross-platform UI integration via JNI.
Full Apple Human Interface Guidelines corpus packaged offline as searchable markdown. Covers iOS, iPadOS, and macOS guidance for every HIG page, with cross-referenced links, downloaded illustrations, and a tag index. Use as the authoritative design reference when building native UI with asset_pipeline.
Comprehensive cross-platform UI component mapping matrix showing how every common UI pattern maps across Crystal UI::View, SwiftUI, UIKit, AppKit, Jetpack Compose, Android Views, and Web HTML/CSS. Implementation priority guide for expanding the asset_pipeline cross-platform component system.
基于 SOC 职业分类
| name | javascript |
| description | Add interactivity with Import Maps, Stimulus controllers, and reactive components — FrontLoader API, StimulusRenderer, WebSocket components |
| user-invocable | true |
You are a JavaScript integration specialist for the asset pipeline. The framework uses ESM import maps for dependency management, Stimulus for lightweight interactivity, and reactive components for real-time server-side updates via WebSocket. No heavy JS frameworks.
The FrontLoader class manages JavaScript imports and script rendering.
# Initialize with import maps
FRONT_LOADER = AssetPipeline::FrontLoader.new do |import_maps|
app = AssetPipeline::ImportMap.new("application", Path["/javascript"])
# CDN dependencies
app.add_import("@hotwired/stimulus", "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js")
# Local controllers (naming convention: *Controller)
app.add_import("HelloController", "controllers/hello_controller.js")
app.add_import("DropdownController", "controllers/dropdown_controller.js")
app.add_import("FormValidationController", "controllers/form_validation_controller.js")
import_maps << app
end
# In your layout (ECR)
<head>
<%= FRONT_LOADER.render_import_map("application") %>
</head>
<body>
<!-- content -->
<script type="module">
<%= FRONT_LOADER.render_stimulus_initialization_script %>
</script>
</body>
render_import_map generates:
<script type="importmap">
{
"imports": {
"@hotwired/stimulus": "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js",
"HelloController": "/javascript/controllers/hello_controller.js",
...
}
}
</script>
The StimulusRenderer automatically detects controllers from import maps and generates registration code.
Import names ending in Controller are auto-detected and registered:
| Import Name | Stimulus Identifier | Registration |
|---|---|---|
HelloController | hello | application.register("hello", HelloController) |
DropdownController | dropdown | application.register("dropdown", DropdownController) |
FormValidationController | form-validation | application.register("form-validation", FormValidationController) |
UserProfileController | user-profile | application.register("user-profile", UserProfileController) |
CamelCase is converted to kebab-case. The Controller suffix is stripped.
render_stimulus_initialization_script produces:
import { Application } from "@hotwired/stimulus"
import HelloController from "HelloController"
import DropdownController from "DropdownController"
const application = Application.start()
application.register("hello", HelloController)
application.register("dropdown", DropdownController)
Pass custom JS to be included after controller registration:
custom_js = <<-JS
// Custom initialization
document.addEventListener("turbo:load", () => {
console.log("Page loaded")
})
JS
FRONT_LOADER.render_stimulus_initialization_script(custom_js)
With Crystal element classes:
div = Components::Elements::Div.new
div.set_attribute("data-controller", "dropdown")
button = Components::Elements::Button.new("Menu", type: "button")
button.set_attribute("data-action", "click->dropdown#toggle")
button.set_attribute("data-dropdown-target", "trigger")
div << button
menu = Components::Elements::Ul.new(class: "dropdown-menu")
menu.set_attribute("data-dropdown-target", "menu")
menu.set_attribute("hidden", "true")
div << menu
Renders:
<div data-controller="dropdown">
<button type="button" data-action="click->dropdown#toggle" data-dropdown-target="trigger">Menu</button>
<ul class="dropdown-menu" data-dropdown-target="menu" hidden="true"></ul>
</div>
Framework-agnostic script rendering with dependency analysis.
import_map = AssetPipeline::ImportMap.new("app", Path["/js"])
import_map.add_import("chart", "https://cdn.example.com/chart.js")
renderer = AssetPipeline::ScriptRenderer.new(import_map, custom_javascript_block: <<-JS
import Chart from "chart"
new Chart(document.getElementById("canvas"), { type: "bar" })
JS
)
renderer.render_initialization_script
# => <script type="module">import Chart from "chart"\nnew Chart(...)</script>
The DependencyAnalyzer auto-detects imports in your custom JS and includes them.
Server-side components that push updates to connected clients via WebSocket.
class LiveCounterComponent < Components::Reactive::ReactiveComponent
protected def initialize_state
set_state("count", 0)
end
def render_content : String
count = get_state("count").try(&.as_i?) || 0
Components::Elements::Div.new(class: "counter").build do |div|
display = Components::Elements::Span.new(class: "count")
display << count.to_s
div << display
inc = Components::Elements::Button.new("+", type: "button")
inc.set_attribute("data-action", "click->reactive#action")
inc.set_attribute("data-action-name", "increment")
div << inc
end.render
end
on_action(:increment) { |data|
count = get_state("count").try(&.as_i?) || 0
set_state("count", count + 1)
# auto-broadcasts updated HTML to all connected clients
}
end
ReactiveComponent wraps output in a div with tracking attributes:
<div data-component-id="abc123" data-component-type="LiveCounterComponent">
<!-- render_content output -->
</div>
# Read state
value = get_state("key") # => JSON::Any?
count = get_state("count").try(&.as_i?) || 0
# Write state (triggers broadcast if auto_update is true)
set_state("count", 42) # Int32
set_state("name", "Alice") # String
set_state("active", true) # Bool
set_state("scores", [1, 2, 3]) # Array
set_state("meta", {"k" => "v"}) # Hash
# Batch updates (single broadcast at end)
update_state do |s|
set_state("count", 0)
set_state("status", "reset")
end
# Manual broadcast
push_update
# Disable auto-broadcasting
self.auto_update = false
class ChatComponent < Components::Reactive::ReactiveComponent
# Server-side events (called from Crystal code)
on_event(:new_message) { |data|
messages = get_state("messages")
# ... append message
set_state("messages", updated_messages)
}
# Client-side actions (triggered by browser via WebSocket)
on_action(:send) { |data|
draft = data["text"]?.try(&.as_s?) || ""
# ... process and broadcast
}
end
component = LiveCounterComponent.new
component.register # Register with ReactiveHandler
component.unregister # Remove from ReactiveHandler
# In your pipeline configuration
pipeline :web do
plug Components::Integration.reactive_handler(
websocket_path: "/ws/components",
action_path: "/components/action",
enable_fallback: true # HTTP POST fallback for non-WebSocket browsers
)
end
# In layout — include client-side JavaScript
<%= Components::Integration.reactive_script_tag(debug: false, auto_init: true) %>
# In controller
class DashboardController < Amber::Controller::Base
def index
counter = LiveCounterComponent.new
counter.register
render html: counter.render
end
end
# Or use the macro
render_component(LiveCounterComponent)
Include Components::AmberIntegration::ViewHelpers for template convenience:
# Render any component
<%= component(LiveCounterComponent) %>
# Include reactive client JS
<%= reactive_scripts(debug: false) %>
# Include generated CSS
<%= css_styles(config: my_config, mode: :production) %>
# Build CSS inline
<%= css { |c| c.base("flex").hover("shadow-lg").build } %>
# Layout with import maps, Stimulus, and reactive components
page = Components::Elements::Html.new(lang: "en").build do |html|
head = Components::Elements::Head.new
head << Components::Elements::Meta.charset
head << Components::Elements::Meta.viewport
head << Components::Elements::Title.new.build { |t| t << "Dashboard" }
head << Components::Elements::Link.stylesheet("/css/app.css")
head << Components::Elements::RawHTML.new(FRONT_LOADER.render_import_map("application"))
html << head
body = Components::Elements::Body.new
main = Components::Elements::Main.new(class: "container")
# Add reactive component
counter = LiveCounterComponent.new
counter.register
main << Components::Elements::RawHTML.new(counter.render)
body << main
# Stimulus init + reactive client JS
body << Components::Elements::RawHTML.new(
Components::Integration.reactive_script_tag(auto_init: true)
)
body << Components::Elements::Script.new(
FRONT_LOADER.render_stimulus_initialization_script
)
html << body
end