Skip to content

Latest commit

 

History

History
252 lines (203 loc) · 9.51 KB

README_CHAPTER_3.md

File metadata and controls

252 lines (203 loc) · 9.51 KB

Getting started with React.rb and Rails

Chapter 3 - A Component linked to a Model

So in chapter 2 we have seen how to display a basic component. Now we will make a component to update our Todo model. See the list of todos? Noticed how annoying it is to have to go to another page in order to edit them? Time to improve our UI. Let's do an in-place editing of those todos.

First we have to create our component file. Like last time we can do :

  • bundle exec rails g reactrb:component Todos::TodoItem

Which will create a file app/views/components/todos/todo_item.rb An interesting thing to consider here, is the name we gave the component. We did not call it "Todo". Why? Because components are classes. Which means that the class of our component would be called "Todo"... Which is the class name of the model Todo.

Now this component will be in charged to display a todo from the list. So we want to convert the code currently displaying the todo in the index to a react.rb code like we did in chapter 2. To be able to display the todo, we have to be able to pass it to the component, so let's define inside our new todo_item.rb file :

  param :todo, type: Todo

And now we want the component to be able to display the todo, just like the previous chapter we will transform the code from views/todos/index.html to some react.rb code inside the render method. So this code :

  <li class="<%= "completed" if todo.complete? %>">
    <div class="view">
      <%= link_to edit_todo_url(todo) do %>
        <input class="toggle" type="checkbox" <%= "checked" if todo.complete? %> onclick="return  true">
        <label><%= todo.title %></label>
      <% end %>
      <%= link_to '', todo, class: :destroy, method: :delete, data: { confirm: 'Are you sure?' } %>
    </div>
  </li>

Become :

li(class: "#{params.todo.complete ? "completed" : ""}") do
  div(class: "view")do
    input(type: :checkbox, (params.todo.complete ? :defaultChecked : :unchecked) => true, :class => "toggle")
    label do
      params.todo.title
    end
    a(class: :destroy)
  end
end

And call it in app/views/index.html like so :

<% @todos.each do |todo| %>
  <%= react_component "TodoItem", todo: todo %>
<% end %>

So this code will display a todo, but it's not doing much for the moment... We even removed the link_to edit_todo_url(todo) since we want to do some inline editing and the data for the destroy link. So it's time to wire this new UI to work as expected. We will do that by adding some handlers on our HTML elements and calling some reactive-record call.

Let's start with the checkbox. We want it to actually change the complete value of the todo ! With react.rb it's super simple. Let's add an event handler :

input(type: :checkbox, (params.todo.complete ? :defaultChecked : :unchecked) => true, :class => "toggle").on(:click) do
  params.todo.complete = !params.todo.complete
  params.todo.save
end

See how easy that is ?! At the end of the input, we call .on(:event) and write the code we want to execute for this event. Then we can easily change the value of our todo, and then call .save on it. This save method is executed by reactive-record. Do you remember in chapter_1 when we moved the Todo model to the public directory? That is the reactive-record magic. All the models in models/public are both available on the server and in our components. So now you don't have to worry about ajax calls or promises, reactive-record take care of all that for you.

OK, now let's do the same for the destroy link :

a(class: :destroy).on(:click) do
  params.todo.destroy
end

I'm sure you're getting the hang of it !

Time to get serious. We said we want to do some inline editing. When we double click the title label we want to have an input showing at its place so we can fiddle with the title. And then when we press enter, the new title should be saved.

OK first we have to be able to know when we are editing the title, and when we are not. To do that we will use a "state". In react a state is like an instance variable that notifies react when it changes. Anytime a state changes, react knows it needs to re-render the component. So we will add an editing state, which will be false by default :

  define_state editing: false

Now that we have our state, we will add an editing class to the li when editing is true and when it is true we will display an input, otherwise we will display our old display code :

li(class: "#{params.todo.complete ? "completed" : ""} #{state.editing ? "editing" : ""}") do
  if state.editing
    input(class: "edit", defaultValue: params.todo.title)
  else
    div(class: "view")do
      input(type: :checkbox, (params.todo.complete ? :cdefaultChecked : :unchecked) => true, :class => "toggle").on(:click) do
        params.todo.complete = !params.todo.complete
        params.todo.save
      end
      label do
        params.todo.title
      end
      a(class: :destroy).on(:click) do
        params.todo.destroy
      end
    end
  end
end

OK, the ground work is ready. Now to activate the editing state, what will we do ? Yeah you're right we are going to add an event listener to the label :

label do
  params.todo.title
end.on(:doubleClick) do
  state.editing! true
end

So now, when we double click the label, we enter the on(:doubleClick) block, which will change our state. Since we are changing the state, react will re-render the component, and this time when rendering the "editing" class will be added to our li, which will make our input shows !

But wait, what do you say? When we double click a label our element is not focused which is kind of annoying? Hmm ... right. We should really take care of that. So the solution would be to put the focus on our label. And we probably want to do that right after the rendering. OK there is actually a callback that we can call : after_update. So let's do inside our component :

after_update do
  if state.editing
    edit_element = Element[".edit"]
    edit_element.focus
  end
end

First we select our input. Actually Element[".edit"] is the syntax for the opal-jquery wrapper, so it's the opal equivalent to $(".edit") in javacript. And then we call focus on it.

So now our element is focused when it appears... but the cursor is at the beginning of the input, not the end ! OK, so now in JS we would do #{edit_element}[0].setSelectionRange(edit_element.val().length, edit_element.val().length) so in opal we would like to do something like edit_element[0].set_selection_range(edit_element.length(), edit_element.length()). However it seems that the opal-jquery wraper does not support the edit_element[0] syntax. So it is the perfect opportunity to show you how to access any javascript from our opal code. In opal any code inside \ `` is executed as javascript. So here we can do :

after_update do
  if state.editing
    edit_element = Element[".edit"]
    edit_element.focus
    `#{edit_element}[0].setSelectionRange(edit_element.val().length, edit_element.val().length)`
  end
end

OK, pretty good job so far. Now they are some behaviour we want to attach to our newly shown input. Let's first make it hide when you click elsewhere :

input(class: "edit", value: params.todo.title).on(:blur) do
  state.editing! false if state.editing
end

Easy peasy ! Now let's make it actually save the new title when you press enter. We will actually do that in 2 steps. First we will save the new value inside our params.todo like so :

input(class: "edit", value: params.todo.title).on(:blur) do
  state.editing! false if state.editing
end.on(:change) do |e|
  params.todo.title = e.target.value
end

And now the actual save :

input(class: "edit", value: params.todo.title).on(:blur) do
  state.editing! false if state.editing
end.on(:change) do |e|
  params.todo.title = e.target.value
end.on(:key_down) do |e|
  if e.key_code == 13  # the 13 key_code is the return key
    params.todo.save
    state.editing! false
  end
end

And that's it, it actually allow us to do some easy inline editing ! and our final component look like :

module Components
  module Todos
    class TodoItem < React::Component::Base

      param :todo, type: Todo
      define_state editing: false


      after_update do
        if state.editing
          edit_element = Element[".edit"]
          edit_element.focus
          `#{edit_element}[0].setSelectionRange(edit_element.val().length, edit_element.val().length)`
        end
      end

      def render
        li(class: "#{params.todo.complete ? "completed" : ""} #{state.editing ? "editing" : ""}") do
          if state.editing
            input(class: "edit", value: params.todo.title).on(:blur) do
              state.editing! false if state.editing
            end.on(:change) do |e|
              params.todo.title = e.target.value
            end.on(:key_down) do |e|
              if e.key_code == 13
                params.todo.save
                state.editing! false
              end
            end
          else
            div(class: "view")do
              input(type: :checkbox, (params.todo.complete ? :defaultChecked : :unchecked) => true, :class => "toggle").on(:click) do
                params.todo.complete = !params.todo.complete
                params.todo.save
              end
              label do
                params.todo.title
              end.on(:doubleClick) do
                state.editing! true
              end
              a(class: :destroy).on(:click) do
                params.todo.destroy
              end
            end
          end

        end
      end
    end
  end
end

How it works:

TODO