Adding live behavior to a new or existing component consists of two steps: including a Ruby module and defining a controller in JavaScript.
For this section of the documentation, we're going to create a component that represents a single item in a todo list. In the next section, we'll build out an entire todo list application; for now though, let's focus on something small.
The first step in the process is to define a regular 'ol view component to represent our todo item in app/components/todo_item_component.rb
class TodoItemComponent < ApplicationComponent def initialize(todo_item:) @todo_item = todo_item end end
class TodoItemComponent < ApplicationComponent def initialize(todo_item:) @todo_item = todo_item end end
This component accepts a single argument, todo_item, which is an instance of a TodoItem database model. We're not showing the model itself in this example, but it's a standard ActiveRecord model with a single text field.
Here's the template for our component, app/components/todo_item_component.html.erb, which displays the item's text and an 'Edit' button. Right now, the button doesn't do anything - we'll hook up click behavior in a subsequent step.
<div class="TodoItem"> <%= @todo_item.text %> <%= button_tag("Edit") %> </div>
<div class="TodoItem"> <%= @todo_item.text %> <%= button_tag("Edit") %> </div>
Let's turn our new component into a "live" component. To do so, include the LiveComponent::Base module:
class TodoItemComponent < ApplicationComponent include LiveComponent::Base # <-- add this line def initialize(todo_item:) @todo_item = todo_item end end
class TodoItemComponent < ApplicationComponent include LiveComponent::Base # <-- add this line def initialize(todo_item:) @todo_item = todo_item end end
Now that the server-side component is ready, we can define the client-side controller. LiveComponents are built on Stimulus, Rails' built-in JavaScript framework, and are defined in a similar fasion to normal Stimulus controllers.
LiveComponent requires that component JavaScript live in a "sidecar" file next to the component's Ruby and template files. Doing so ensures LiveComponent discovers your component's JavaScript and renders the appropriate web component wrapper. For this example, let's create app/components/todo_item_component.ts with the following contents. (NOTE: we're using TypeScript here because plain JavaScript does not support decorators, eg @live).
import { live, LiveController } from "@camertron/live-component"; @live("TodoItemComponent") // this is the Ruby class name export class TodoItemComponent extends LiveController { // nothing in here yet }
import { live, LiveController } from "@camertron/live-component"; @live("TodoItemComponent") // this is the Ruby class name export class TodoItemComponent extends LiveController { // nothing in here yet }
Although we don't have a way of saving the changes yet, let's make the "Edit" button show a text field so the item's text can be updated.
First, modify the component's Ruby code to accept an editing boolean argument:
class TodoItemComponent < ApplicationComponent include LiveComponent::Base def initialize(todo_item:, editing: false) @todo_item = todo_item @editing = editing end end
class TodoItemComponent < ApplicationComponent include LiveComponent::Base def initialize(todo_item:, editing: false) @todo_item = todo_item @editing = editing end end
Next, update the component's template to render the text field in edit mode:
<div class="TodoItem"> <% if @editing %> <%= form_with(model: @todo_item) do |f| %> <%= f.text_field :text %> <% end %> <% else %> <%= @todo_item.text %> <%= button_tag("Edit") %> <% end %> </div>
<div class="TodoItem"> <% if @editing %> <%= form_with(model: @todo_item) do |f| %> <%= f.text_field :text %> <% end %> <% else %> <%= @todo_item.text %> <%= button_tag("Edit") %> <% end %> </div>
Let's wire up the button's click event to our Stimulus controller using a Stimulus action attribute. If you're not familar with Stimulus actions, take a look at the documentation.
<div class="TodoItem"> <% if @editing %> <%= form_with(model: @todo_item) do |f| %> <%= f.text_field :text %> <%= f.submit %> <% end %> <% else %> <%= @todo_item.text %> <%= button_tag( "Edit", data: { target: "click->todoitemcomponent#edit" } ) %> <% end %> </div>
<div class="TodoItem"> <% if @editing %> <%= form_with(model: @todo_item) do |f| %> <%= f.text_field :text %> <%= f.submit %> <% end %> <% else %> <%= @todo_item.text %> <%= button_tag( "Edit", data: { target: "click->todoitemcomponent#edit" } ) %> <% end %> </div>
Now when the button gets clicked, Stimulus will call the edit() method on our controller.
Finally, let's update our controller to re-render the component in edit mode when the button gets clicked:
import { live, LiveController } from "@camertron/live-component"; @live("TodoItemComponent") // this is the Ruby class name export class TodoItemComponent extends LiveController { edit() { this.render((component) => { component.props.editing = true; }); } }
import { live, LiveController } from "@camertron/live-component"; @live("TodoItemComponent") // this is the Ruby class name export class TodoItemComponent extends LiveController { edit() { this.render((component) => { component.props.editing = true; }); } }
When the button gets clicked, LiveComponent makes an HTTP request to your Rails app, which renders the TodoItemComponent using the new edit mode. Rails in turn responds with a chunk of updated HTML. On the front-end, LiveComponent morphs the new HTML onto the page, and the update is complete.
A finished version of the todo item example can be seen below.
1 | <div class="TodoItem"> |
2 | <% if @editing %> |
3 | <%= form_with(model: @todo_item) do |f| %> |
4 | <%= f.text_field :text %> |
5 | <%= f.submit %> |
6 | <% end %> |
7 | <% else %> |
8 | <%= @todo_item.text %> |
9 | <%= button_tag( |
10 | "Edit", |
11 | data: { action: "click->todoitemcomponent#edit" } |
12 | ) %> |
13 | <% end %> |
14 | </div>
|
1 | <div class="TodoItem"> |
2 | <% if @editing %> |
3 | <%= form_with(model: @todo_item) do |f| %> |
4 | <%= f.text_field :text %> |
5 | <%= f.submit %> |
6 | <% end %> |
7 | <% else %> |
8 | <%= @todo_item.text %> |
9 | <%= button_tag( |
10 | "Edit", |
11 | data: { action: "click->todoitemcomponent#edit" } |
12 | ) %> |
13 | <% end %> |
14 | </div>
|
1 | class TodoItemComponent < ApplicationComponent |
2 | include LiveComponent::Base |
3 | |
4 | def initialize(todo_item:, editing: false) |
5 | @todo_item = todo_item |
6 | @editing = editing |
7 | end
|
8 | end
|
1 | class TodoItemComponent < ApplicationComponent |
2 | include LiveComponent::Base |
3 | |
4 | def initialize(todo_item:, editing: false) |
5 | @todo_item = todo_item |
6 | @editing = editing |
7 | end
|
8 | end
|
1 | import { live, LiveController } from "@camertron/live-component"; |
2 | |
3 | @live("TodoItemComponent") // this is the Ruby class name |
4 | export class TodoItemComponent extends LiveController { |
5 | edit() { |
6 | this.render((component) => { |
7 | component.props.editing = true; |
8 | });
|
9 | }
|
10 | }
|
1 | import { live, LiveController } from "@camertron/live-component"; |
2 | |
3 | @live("TodoItemComponent") // this is the Ruby class name |
4 | export class TodoItemComponent extends LiveController { |
5 | edit() { |
6 | this.render((component) => { |
7 | component.props.editing = true; |
8 | });
|
9 | }
|
10 | }
|