Your First ComponentLink to heading


Adding live behavior to a new or existing component consists of two steps: including a Ruby module and defining a controller in JavaScript.

Todo Item ExampleLink to heading

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>

Adding Live BehaviorLink to heading

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

The JavaScriptLink to heading

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
}

Adding Some InteractivityLink to heading

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.

Putting it all TogetherLink to heading

A finished version of the todo item example can be seen below.

Collapse file tree Expand file tree
  • app
    • components
      • todo_item_component.html.erb
      • todo_item_component.rb
      • todo_item_component.ts
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