{"id":13594634,"url":"https://github.com/ruby-hyperloop/todo-tutorial","last_synced_at":"2025-04-09T07:33:26.297Z","repository":{"id":145367500,"uuid":"58554992","full_name":"ruby-hyperloop/todo-tutorial","owner":"ruby-hyperloop","description":"The project has moved to Hyperstack!! - Build a todo app using Ruby Hyperloop","archived":true,"fork":false,"pushed_at":"2018-08-09T18:49:06.000Z","size":4399,"stargazers_count":19,"open_issues_count":6,"forks_count":12,"subscribers_count":12,"default_branch":"edge","last_synced_at":"2024-08-02T16:53:34.628Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://hyperstack.org/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ruby-hyperloop.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-05-11T15:03:34.000Z","updated_at":"2023-01-28T19:50:20.000Z","dependencies_parsed_at":"2023-05-11T13:36:56.996Z","dependency_job_id":null,"html_url":"https://github.com/ruby-hyperloop/todo-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruby-hyperloop%2Ftodo-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruby-hyperloop%2Ftodo-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruby-hyperloop%2Ftodo-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruby-hyperloop%2Ftodo-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruby-hyperloop","download_url":"https://codeload.github.com/ruby-hyperloop/todo-tutorial/tar.gz/refs/heads/edge","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223375398,"owners_count":17135364,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T16:01:36.832Z","updated_at":"2024-11-06T16:31:40.509Z","avatar_url":"https://github.com/ruby-hyperloop.png","language":"Ruby","readme":"\u003cdiv class=\"githubhyperloopheader\"\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"http://ruby-hyperloop.org/\" alt=\"Hyperloop\" title=\"Hyperloop\"\u003e\n\u003cimg width=\"350px\" src=\"http://ruby-hyperloop.org/images/hyperloop-github-logo.png\"\u003e\n\u003c/a\u003e\n\u003c/p\u003e\n\n\u003ch2 align=\"center\"\u003eThe Complete Isomorphic Ruby Framework\u003c/h2\u003e\n\n\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"http://ruby-hyperloop.org/\" alt=\"Hyperloop\" title=\"Hyperloop\"\u003e\n\u003cimg src=\"http://ruby-hyperloop.org/images/githubhyperloopbadge.png\"\u003e\n\u003c/a\u003e\n\n\u003ca href=\"http://ruby-hyperloop.org/tutorials/hyperlooprails/todo-tutorial/\" alt=\"Tutorial page\" title=\"Tutorial page\"\u003e\n\u003cimg src=\"http://ruby-hyperloop.org/images/githubtutorialbadge.png\"\u003e\n\u003c/a\u003e\n\n\u003ca href=\"https://gitter.im/ruby-hyperloop/chat\" alt=\"Gitter chat\" title=\"Gitter chat\"\u003e\n\u003cimg src=\"http://ruby-hyperloop.org/images/githubgitterbadge.png\"\u003e\n\u003c/a\u003e\n\u003c/p\u003e\n\n\u003c/div\u003e\n\n\u003ch2 align=\"center\"\u003eHyperloop TodoMVC Tutorial (Rails 5.2.0)\u003c/h2\u003e\n\n\u003cp align=\"center\"\u003e\n![](http://ruby-hyperloop.org/images/tutorials/Hyperloop-Railstodomvc.gif)\n\u003c/p\u003e\n\n### Prerequisites\n\n{ [Ruby On Rails](http://rubyonrails.org/) }, { [hyperloop GEM](http://ruby-hyperloop.org) }\n\n### The Goals of this Tutorial\n\nIn this tutorial you will build the classic [TodoMVC](http://todomvc.com) application using Hyperloop\n\nThe finished application will\n\n1. have the ability to add and edit todos;\n2. be able change the complete/incomplete state;\n3. filter the list of displayed todos to show all, complete, or incomplete (active) todos;\n4. have html5 history so that as the filter changes so does the URL;\n6. have server side persistence;\n7. and synchronization across multiple browser windows.\n\nYou will write less than 100 lines of code, and the tutorial should take about 1-2 hours to complete.\n\nYou can find the older application source code here:\u003cbr\u003e\n\n\u003ca href=\"https://github.com/ruby-hyperloop/todo-tutorial\" alt=\"Tutorial source code\" title=\"Tutorial source code\"\u003e\n\u003cimg src=\"http://ruby-hyperloop.org/images/githubsourcecodebadge.png\"\u003e\n\u003c/a\u003e\n\n### Skills required\n\nWorking knowledge of Rails and Hyperloop required\n\n\u003cbr\u003e\n\n## TUTORIAL\n\n### Chapter 1: Setting Things Up\n\nCreate a new rails application\n```ruby\n  rails _5.2.0_ new hyperloop_todo\n```\n_5.2.0_  will insure you are creating a rails 5.2 appear (tested with 5.0 and 5.1)\n\nAdd Hyperloop to your Gemfile\n\nUntil our official release, add the following to your Gemfile:\n```ruby\n  ...\n  # lap0 will use the latest release candidate\n  gem 'hyperloop', '~\u003e 1.0.0.lap0', git: 'https://github.com/ruby-hyperloop/hyperloop.git', branch: 'edge'\n  gem 'hyperloop-config', '~\u003e 1.0.0.lap0', git: 'https://github.com/ruby-hyperloop/hyperloop-config.git', branch: 'edge'\n  ...\n```\n\nthen\n```ruby\n  bundle install\n```\n\nOnce the Hyperloop Gem and all its dependencies have been installed, it's time to run the hyperloop install generator.\n```ruby\n  rails g hyperloop:install\n```\n\nThe generator creates the hyperloop structure inside the /app directory :\n```ruby\n  /app/hyperloop/\n  /app/hyperloop/components\n  /app/hyperloop/models\n  /app/hyperloop/operations\n  /app/hyperloop/stores\n```\n\nAnd updates your app/assets/javascripts/application.js file adding these lines:\n```ruby\n  //= require hyperloop-loader\n  Opal.OpalHotReloader.$listen() // optional (port, false, poll_seconds) i.e. (8081, false, 1)\n```\n\nTo be sure everything is setting up correctly, check your app/assets/javascripts/application.js:\n```ruby\n  ...\n  //= require rails-ujs\n  //= require activestorage\n  //= require turbolinks\n  //= require_tree .\n  //= require hyperloop-loader\n  Opal.OpalHotReloader.$listen() // optional (port, false, poll_seconds) i.e. (8081, false, 1)\n```\n\nRun foreman\n```ruby\n  $ foreman start\n```\n\nNavigate to the given location and you should see the word **App** displayed on the page.\n\n### Chapter 2:  Hyperloop Models are Rails Models\n\nWe are going to add our Todo Model, and discover that Hyperloop models are in fact Rails models.\n+ You can access the your rails models on the client using the same syntax you use on the server.\n+ Changes on the client are mirrored on the server.\n+ Changes to models on the server are synchronized with all participating browsers.\n+ Data access is is protected by a robust *policy* mechanism.\n\n\u003e*A Rails ActiveRecord Model is a Ruby class that is backed by a database table.  In this example we will have one model class called `Todo`.  When manipulating models, Rails automatically generates the necessary SQL code for you.  So when `Todo.all` is evaluated Rails generates the appropriate SQL\nand turns the result of the query into appropriate Ruby data structures.*\n\n\u003e*Hyperloop Models are extensions of ActiveRecord Models that synchronize the data between the client and server\nautomatically for you.  So now `Todo.all` can be evaluated on the server or the client.*\n\nOkay lets see it in action:\n\n1. **Add the Todo Model:**  \n  In a new terminal window run **on a single line**:   \n  ```ruby\n    bundle exec rails g model Todo title:string completed:boolean priority:integer\n  ```\n\n  This runs a Rails *generator* which will create the skeleton Todo model class, and create a *migration* which will\n  add the necessary tables and columns to the database.  \n\n  **VERY IMPORTANT!** Now look in the db/migrate/ directory, and edit the migration file you have just created. The file will be titled with a long string of numbers then \"create_todos\" at the end. Change the line creating the completed boolean field so that it looks like this:\n  ```ruby  \n    ...\n    t.boolean :completed, null: false, default: false\n    ...\n  ```  \n  For details on 'why' see [this blog post.](https://robots.thoughtbot.com/avoid-the-threestate-boolean-problem)\n  Basically this insures `completed` is treated as a true boolean, and will avoid having to check between `false` and `null` later on.   \n\n  Now run\n  ```ruby\n    bundle exec rails db:migrate\n  ```\n  which will create the table.\n\n2. **Make Some Models Public:**  \n  *Move* `models/todo.rb` and `models/application_record.rb` to `hyperloop/models`.  \n\n   This will make the model accessible on the clients *and the server*, subject to any data access policies.  \n\n   *Note: The hyperloop installer adds a policy that gives full permission to all clients but only in development and test modes.  Have a look at `app/policies/application_policy` if you are interested.*\n\n3. **Try It**\n  Change your `App` component's render method to:  \n  ```ruby\n    # app/hyperloop/components/app.rb\n    class App \u003c Hyperloop::Component\n     render(DIV) do\n       \"Number of Todos: #{Todo.count}\"\n     end\n    end\n  ```  \n\n   **Reload the Page**\n   You will now see *Number of Todos: 0* displayed.  *You must reload the page as you have changed the class of App from `Router` to `Component`*\n\n   Now start a rails console\n   ```ruby\n    bundle exec rails c\n   ```\n   and type:  \n   ```ruby\n     Todo.create(title: 'my first todo')\n   ```  \n   This is telling the server to create a new todo, which will update your hyperloop application, and you will see the count change to 1!   \n\n   Try it again:  \n   ```ruby\n     Todo.create(title: 'my second todo')\n   ```  \n   and you will see the count change to 2!   \n\nAre we having fun yet?  I hope so!  As you can see Hyperloop is synchronizing the Todo model between the client and server.  As the state of the database changes, HyperReact buzzes around updating whatever parts of the DOM were dependent on that data (in this case the count of Todos).\n\nNotice that we did not create any APIs to achieve this.  Data on the server is synchronized with data on the client for you.\n\n### Chapter 3: Creating the Top Level App Structure\n\nNow that we have all of our pieces in place, lets build our application.\n\nReplace the entire contents of `app.rb` with:\n\n```ruby\n# app/hyperloop/components/app.rb\nclass App \u003c Hyperloop::Component\n  render(SECTION) do\n    Header()\n    Index()\n    Footer()\n  end\nend\n```\n\nThe browser screen will go blank because we have not defined the three subcomponents.  Lets define them now:\n\nAdd three new ruby files to the `app/hyperloop/components` folder:\n\n```ruby\n# app/hyperloop/components/header.rb\nclass Header \u003c Hyperloop::Component\n  render(HEADER) do\n    'Header will go here'\n  end\nend\n```\n\n```ruby\n# app/hyperloop/components/index.rb\nclass Index \u003c Hyperloop::Component\n  render(SECTION) do\n    'List of Todos will go here'\n  end\nend\n```\n\n```ruby\n# app/hyperloop/components/footer.rb\nclass Footer \u003c Hyperloop::Component\n  render(DIV) do\n    'Footer will go here'\n  end\nend\n```\n\nOnce you add the Footer component you should see:\n\n  \u003cdiv style=\"border:solid; margin-left: 10px; padding: 10px\"\u003e\n    \u003cdiv\u003eHeader will go here\u003c/div\u003e\n    \u003cdiv\u003eList of Todos will go here\u003c/div\u003e  \n    \u003cdiv\u003eFooter will go here\u003c/div\u003e  \n  \u003c/div\u003e\n  \u003cbr\u003e\n\nIf you don't, restart the server, and reload the browser.\n\nNotice how the usual HTML tags such as DIV, SECTION, and HEADER are all available as well as all the other HTML and SVG tags.\n\n### Chapter 4: Listing the Todos, HyperReact Params, and Prerendering\n\nTo display each Todo we will create a TodoItem component that takes a parameter:\n\n```ruby\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  render(LI) do\n    params.todo.title\n  end\nend\n```\n\nWe can use this component in our Index component:\n\n```ruby\n# app/hyperloop/components/index.rb\nclass Index \u003c Hyperloop::Component\n  render(SECTION) do\n    UL do\n      Todo.each do |todo|\n        TodoItem(todo: todo)\n      end\n    end\n  end\nend\n```\n\nNow you will see something like\n\n  \u003cdiv style=\"border:solid; margin-left: 10px; padding: 10px\"\u003e\n    \u003cdiv\u003eHeader will go here\u003c/div\u003e\n    \u003cul\u003e\n      \u003cli\u003emy first todo\u003c/li\u003e\n      \u003cli\u003emy second todo\u003c/li\u003e\n    \u003c/ul\u003e\n    \u003cdiv\u003eFooter will go here\u003c/div\u003e\n  \u003c/div\u003e\n  \u003cbr\u003e\n\nAs you can see components can take parameters (or props in react.js terminology.)\n\n\u003e*Rails uses the terminology params (short for parameters) which have a similar purpose to React props, so to make the transition more natural for Rails programmers Hyperloop uses params, rather than props.*\n\nParams are declared using the `param` macro and are accessed via the `params` object.\nIn our case we *mount* a new TodoItem with each Todo record and pass the Todo as the parameter.   \n\nNow go back to Rails console and type\n```ruby\n  Todo.last.update(title: 'updated todo')\n```\nand you will see the last Todo in the list changing.\n\nTry adding another Todo using `create` like you did before. You will see the new Todo is added to the list.\n\n\n### Chapter 5: Adding Inputs to Components\n\nSo far we have seen how our components are synchronized to the data that they display.  Next let's add the ability for the component to *change* the underlying data.\n\nFirst add an `INPUT` html tag to your TodoItem component like this:\n\n```ruby\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  render(LI) do\n    INPUT(type: :checkbox, checked: params.todo.completed)\n    params.todo.title\n  end\nend\n```\n\nYou will notice that while it does display the checkboxes, you can not change them by clicking on them.\n\nFor now we can change them via the console like we did before.  Try executing\n```ruby\n  Todo.last.update(completed: true)\n```  \nand you should see the last Todo's `completed` checkbox changing state.\n\nTo make our checkbox input change its own state, we will add an `event handler` for the change event:\n\n```ruby\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  render(LI) do\n    INPUT(type: :checkbox, checked: params.todo.completed)\n      .on(:click) { params.todo.update(completed: !params.todo.completed) }\n    params.todo.title\n  end\nend\n```\nIt reads like a good novel doesn't it?  On a `click` event update the todo, setting the completed attribute to the opposite of its current value.\n\nMeanwhile HyperReact sees the value of `params.todo.checked` changing, and this causes the associated HTML INPUT tag to be re-rendered.\n\nWe will finish up by adding a delete link at the end of the Todo item:\n\n```ruby\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  render(LI) do\n    INPUT(type: :checkbox, checked: params.todo.completed)\n      .on(:click) { params.todo.update(completed: !params.todo.completed) }\n    SPAN { params.todo.title } # See note below...\n    A { ' -X-' }.on(:click) { params.todo.destroy }\n  end\nend\n```\n\n*Note: If a component or tag block returns a string it is automatically wrapped in a SPAN, to insert a string in the middle you have to wrap it a SPAN like we did above.*\n\nI hope you are starting to see a pattern here.  HyperReact components determine what to display based on the `state` of some\nobjects.  External events, such as mouse clicks, the arrival of new data from the server, and even timers update the `state`.  HyperReact recomputes whatever portion of the display depends on the `state` so that the display is always in sync with the `state`.  In our case the objects are the Todo model and its associated records, which have a number of associated internal `states`.  \n\nBy the way, you don't have to use Models to have states.  We will see later that states can be as simple as boolean instance variables.\n\n### Chapter 6: Routing\n\nNow that Todos can be *completed* or *active*, we would like our user to be able display either \"all\" Todos,\nonly \"completed\" Todos, or \"active\" (or incomplete) Todos.  We want our URL to reflect which filter is currently being displayed.\nSo `/all` will display all todos, `/completed` will display the completed Todos, and of course `/active` will display only active\n(or incomplete) Todos.  We would also like the root url `/` to be treated as `/all`\n\nTo achieve this we first need to be able to *scope* (or filter) the Todo Model. So let's edit the Todo model file so it looks like this:\n\n```ruby\n# app/hyperloop/models/todo.rb\nclass Todo \u003c ApplicationRecord\n  scope :completed, -\u003e () { where(completed: true)  }\n  scope :active,    -\u003e () { where(completed: false) }\nend\n```\n\nNow we can say `Todo.all`, `Todo.completed`, and `Todo.active`, and get the desired subset of Todos.\nYou might want to try it now in the rails console.  *Note: you will have to do a `reload!` to load the changes to the Model.*\n\nWe would like the URL of our App to reflect which of these *filters* is being displayed.  So if we load\n\n+ `/all` we want the Todo.all scope to be run;\n+ `/completed` we want the Todo.completed scope to be run;\n+ `/active` we want the Todo.active scope to be run;\n+ `/` (by itself) then we should redirect to `/all`.\n\nHaving the application display different data (or whole different components) based on the URL is called routing.  \n\nLets change `App` to look like this:\n\n```ruby\n# app/hyperloop/components/app.rb\nclass App \u003c Hyperloop::Router\n  history :browser\n  route do # note instead of render we use the route method\n    SECTION do\n      Header()\n      Route('/', exact: true) { Redirect('/all') }\n      Route('/:scope', mounts: Index)\n      Footer()\n    end\n  end\nend\n```\nand the `Index` component to look like this:\n\n```ruby\n# app/hyperloop/components/index.rb\nclass Index \u003c Hyperloop::Router::Component\n  render(SECTION) do\n    UL do\n      Todo.send(match.params[:scope]).each do |todo|\n        TodoItem(todo: todo)\n      end\n    end\n  end\nend\n```\n*Note that because we have changed the class of these components the hot reloader will break, and you will have to refresh the page and possibly your local server.*\n\nLets walk through the changes:\n+ `App` now inherits from `Hyperloop::Router` which is a subclass of `Hyperloop::Component` with *router* capabilities added.\n+ The `history` macro tells the router how to track the history (back/forward buttons).  \nThe `:browser` history tracks the history invisibly in the html5 browser history.\nThe other common option is the `:hash` history which tracks the history in the url hash.\n+ The `render` macro is replaced by `route`, and the `DIV` tag is moved inside the route block.\n+ We mount the `Header` components as before.\n+ We then check to see if the current route exactly matches `/` and if it does, redirect to `/all`.\n+ Then instead of directly mounting the `Index` component, we *route* to it based on the URL.  In this case if the url must look like `/xxx`.\n+ `Index` now inherits from `Hyperloop::Router::Component` which is a subclass of `Hyperloop::Component` with methods like `match` added.\n+ Instead of simply enumerating all the Todos, we decide which *scope* to filter using the URL fragment *matched* by `:scope`.  \n\nNotice the relationship between `Route('/:scope', mounts: Index)` and `match.params[:scope]`:\n\nDuring routing each `Route` is checked.  If it *matches* then the\nindicated component is mounted, and the match parameters are saved for that component to use.\n\nYou should now be able to change the url from `/all`, to `/completed`, to `/active`, and see a different set of Todos.  For example if you are displaying the `/active` Todos, you will only see the Todos that are not complete.  If you check one of these it will disappear from the list.\n\n\u003e*Rails also has the concept of routing, so how do the Rails and Hyperloop routers interact?  Have a look at the config/routes.rb file.  You will see a line like this:  \n`  get '/(*other)', to: 'hyperloop#app'`  \nThis is telling Rails to accept all requests and to process them using the `hyperloop` controller, which will attempt to mount a component named `App` in response to the request.  The mounted App component is then responsible for further processing the URL*  \n\n\u003e*For more complex scenarios Hyperloop provides Rails helper methods that can be used to mount components from your controllers, layouts, and views*\n\n### Chapter 7:  Helper Methods, Inline Styling, Active Support and Router Nav Links\n\nOf course we will want to add navigation to move between these routes.  We will put the navigation in the footer:\n\n```ruby\n# app/hyperloop/components/footer.rb\nclass Footer \u003c Hyperloop::Component\n  def link_item(path)\n    A(href: \"/#{path}\", style: { marginRight: 10 }) { path.camelize }\n  end\n  render(DIV) do\n    link_item(:all)\n    link_item(:active)\n    link_item(:completed)\n  end\nend\n```\nSave the file, and you will now have 3 links, that you will change the path between the three options.  \n\nHere is how the changes work:\n+ Hyperloop is just Ruby, so you are free to use all of Ruby's rich feature set to structure your code. For example the `link_item` method is just a *helper* method to save us some typing.\n+ The `link_item` method uses the `path` argument to construct an HTML *Anchor* tag.\n+ Hyperloop comes with a large portion of the Rails active-support library.  For the text of the anchor tag we use the active-support method `camelize`.\n+ Later we will add proper css classes, but for now we use an inline style.  Notice that the css `margin-right` is written `marginRight`, and that `10px` can be expressed as the integer 10.\n\nNotice that as you click each link the page reloads.  **However** what we really want is for the links to simply change the route, without reloading the page.\n\nTo make this happen we will *mixin* some router helpers by *including* `HyperRouter::ComponentMethods` inside of class.\n\nThen we can replace the anchor tag with the Router's `NavLink` component:\n\nChange\n\n```ruby\n  A(href: \"/#{path}\", style: { marginRight: 10 }) { path.camelize }\n```\nto\n\n```ruby\n  NavLink(\"/#{path}\", style: { marginRight: 10 }) { path.camelize }\n```\n\nOur component should now look like this:\n\n```ruby\n# app/hyperloop/components/footer.rb\nclass Footer \u003c Hyperloop::Component\n  include HyperRouter::ComponentMethods\n  def link_item(path)\n    NavLink(\"/#{path}\", style: { marginRight: 10 }) { path.camelize }\n  end\n  render(DIV) do\n    link_item(:all)\n    link_item(:active)\n    link_item(:completed)\n  end\nend\n```\nAfter this change you will notice that changing routes *does not* reload the page, and after clicking to different routes, you can use the browsers forward and back buttons.\n\nHow does it work?  The `NavLink` component reacts to a click just like an anchor tag, but instead of changing the window's URL directly, it updates the *HTML5 history object.*\nAssociated with this history is a (you guessed it, I hope) *state*.  So when the history changes it causes any components depending on the current URL to be re-rendered.\n\n### Chapter 8: Create a Basic EditItem Component\nSo far we can mark Todos as completed, delete them, and filter them.  Now we create an `EditItem` component so we can change the Todo title.\n\nAdd a new component like this:\n\n```ruby\n# app/hyperloop/components/edit_item.rb\nclass EditItem \u003c Hyperloop::Component\n  param :todo\n  render do\n    INPUT(defaultValue: params.todo.title)\n      .on(:key_down) do |evt|\n        next unless evt.key_code == 13\n        params.todo.update(title: evt.target.value)\n      end\n  end\nend\n```\nBefore we use this component let's understand how it works.\n+ It receives a `todo` param which will be edited by the user;\n+ The `title` of the todo is displayed as the initial value of the input;\n+ When the user types the enter key (key code 13) the todo is saved.\n\nNow update the `TodoItem` component replacing\n\n```ruby\n  SPAN { params.todo.title }\n```\nwith\n\n```ruby\n  EditItem(todo: params.todo)\n```\nTry it out by changing the text of some our your Todos followed by the enter key.  Then refresh the page to see that the Todos have changed.\n\n### Chapter 9: Adding State to a Component, Defining Custom Events, and a Lifecycle Callback.\nThis all works, but its hard to use.  There is no feed back indicating that a Todo has been saved, and there is no way to cancel after starting to edit.\nWe can make the user interface much nicer by adding *state* (there is that word again) to the `TodoItem`.\nWe will call our state `editing`.  If `editing` is true, then we will display the title in a `EditItem` component, otherwise we will display it in a `LABEL` tag.\nThe user will change the state to `editing` by double clicking on the label.  When the user saves the Todo, we will change the state of `editing` back to false.\nFinally we will let the user *cancel* the edit by moving the focus away (the `blur` event) from the `EditItem`.\nTo summarize:\n+ User double clicks on any Todo title: editing changes to `true`.\n+ User saves the Todo being edited: editing changes to `false`.\n+ User changes focus away (`blur`) from the Todo being edited: editing changes to `false`.\nIn order to accomplish this our `EditItem` component is going to communicate via two callbacks - `on_save` and `on_cancel` - with the parent component.  We can think of these callbacks as custom events, and indeed as we shall see they will work just like any other event.\nAdd the following 5 lines to the `EditItem` component like this:\n\n```ruby\n# app/hyperloop/components/edit_item.rb\nclass EditItem \u003c Hyperloop::Component\n  param :todo\n  param :on_save, type: Proc               # add\n  param :on_cancel, type: Proc             # add\n  after_mount { Element[dom_node].focus }  # add\n\n  render do\n    INPUT(defaultValue: params.todo.title)\n      .on(:key_down) do |evt|\n        next unless evt.key_code == 13\n        params.todo.update(title: evt.target.value)\n        params.on_save                       # add\n      end\n      .on(:blur) { params.on_cancel }        # add\n  end\nend\n```\nThe first two lines add our callbacks.  In HyperReact (and React.js) callbacks are just params.\nGiving them `type: Proc` and beginning their name with `on_` means that HyperReact will treat them syntactically like events (as we will see.)  \n\nThe next line uses one of several *Lifecycle Callbacks*.  In this case we need to move the focus to the `EditItem` component after is mounted.\nThe `Element` class is Hyperloop's jQuery wrapper, and `dom_node`\nis the method that returns the actual dom node where this instance of the component is mounted.\n\nThe `params.on_save` line will call the provided callback.  Notice that because we declared `on_save` as type `Proc`,\nwhen we refer to it in the component it invokes the callback rather than returning the value.\nFor example, if we had left off `type: Proc` we would have to say `params.on_save.call`.\n\nFinally we add the `blur` event handler and simply transform it into our custom `cancel` event.\n\nNow we can update our `TodoItem` component to be a little state machine, which will react to three events:  `double_click`, `save` and `cancel`.\n\n```ruby\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  state editing: false\n  render(LI) do\n    if state.editing\n      EditItem(todo: params.todo)\n        .on(:save, :cancel) { mutate.editing false }\n    else\n      INPUT(type: :checkbox, checked: params.todo.completed)\n        .on(:click) { params.todo.update(completed: !params.todo.completed) }\n      LABEL { params.todo.title }\n        .on(:double_click) { mutate.editing true }\n      A { ' -X-' }\n        .on(:click) { params.todo.destroy }\n    end\n  end\nend\n```\nFirst we declare a *state variable* called `editing` that is initialized to `false`.\n\nWe have already used a lot of states that are built into the HyperModel and HyperRouter. The state machines in these complex objects are built out of simple state variables like the `editing`.\n\nState variables are *just like instance variables* with the added power that when they change, any dependent components will be updated with the change.\n\nYou read a state variable using the `state` method (similar to the `params` method) and you change state variables using the `mutate` method.  Whenever you want to change a state variable whether its a simple assignment or changing the internal value of a complex structure like a hash or array you use the `mutate` method.\n\nLets read on:  Next, we see `if state.editing...`.  When the component executes this `if` statement, it reads the value of the `editing` state variable and will either render the `EditItem` or the input, label, and anchor tags.  In this way the `editing` state variable is acting no different than any other Ruby instance variable.  *But here is the key: The component now knows that if the value of the editing state changes, it must re-render this TodoItem.  When state variables are referenced by a component the component will keep track of this, and will re-rerender when the state changes.*\n\nBecause `editing` starts off false, when the TodoItem first mounts, it renders the input, label, and anchor tags.  Attached to the label tag is a `double_click` handler which does one thing:  *mutates* the editing state.  This then causes the component to re-render, and now instead of the three tags, we will render the `EditItem` component.\n\nAttached to the `EditItem` component is the `save` and `cancel` handler (which is shared between the two events) that *mutates* the editing state, setting it back to false.\n\nNotice that just as you read params using the `params` method, you read state variables using the `state` method.  Note that `state` is singular because we commonly think of the 'state' of an object as singular entity.\n\n### Chapter 10: Using EditItem to create new Todos\n\nOur EditItem component has a good robust interface.  It takes a Todo, and lets the user edit the title, and then either save or cancel, using two event style callbacks to communicate back outwards.\n\nBecause of this we can easily reuse EditItem to create new Todos.  Not only does this save us time, but it also insures that the user interface acts consistently.\n\nUpdate the `Header` component to use EditItem like this:\n\n```ruby\n# app/hyperloop/components/header.\nclass Header \u003c Hyperloop::Component\n  state(:new_todo) { Todo.new }\n  render(HEADER) do\n    EditItem(todo: state.new_todo)\n      .on(:save) { mutate.new_todo Todo.new }\n  end\nend\n```\nWhat we have done is create a state variable called `new_todo` and we have initialized it using a block that will return `Todo.new`.  The reason we use a block is to insure that we don't call `Todo.new` until after the system is loaded, at which point all state initialization blocks will be run.  A good rule of thumb is to use the block notation unless the initial value is a constant.\n\nThen we pass the value of the state variable to EditItem, and when it is saved, we generate another new Todo and save it the `new_todo` state variable.\n\nNotice `new_todo` is a state variable that is used in Header, so when it is mutated, it will cause a re-render of the Header, which will then pass the new value of `new_todo`, to EditItem, causing that component to re-render.  \n\nWe don't care if the user cancels the edit, so we simply don't provide a `:cancel` event handler.\n\nOnce the code is added a new input box will appear at the top of the window, and when you type enter a new Todo will be added to the list.\n\nHowever you will notice that the value of new Todo input box does not clear.  This is subtle problem that is easy to fix.\n\nReact treats the `INPUT` tags `defaultValue` specially.  It is only read when the `INPUT` is first mounted, so it *does not react* to changes like normal\nparameters.  Our `Header` component does pass in\nnew Todo records, but even though they are changing React *does not* update the INPUT.\n\nWe can easily fix this by adding a `key` param to the `INPUT` that is associated with each unique Todo.\nIn Ruby this is easy as every object has an `object_id` method that is guaranteed to return a unique value.\n\nChanging the value of the key, will inform React that we are referring to a new Todo, and thus a  new `INPUT` element will have to be mounted.\n\n```ruby\n  ...\n  INPUT(defaultValue: params.todo.title, key: params.todo.object_id)\n  ...\n```\n\n### Chapter 11: Adding Styling\n\nWe are just going to steal the style sheet from the benchmark Todo app, and add it to our assets.\n\n**Go grab the file in this repo here:** https://github.com/ruby-hyperloop/todo-tutorial/blob/master/app/assets/stylesheets/todo.css\nand copy it to a new file called `todo.css` in the `app/assets/stylesheets/` directory.\n\nYou will have to refresh the page after changing the style sheet.\n\nNow its a matter of updating the css classes which are passed to components via the `class` parameter.\n\nLet's start with the `App` component.  With styling it will look like this:\n\n```ruby\n# app/hyperloop/components/app.rb\nclass App \u003c Hyperloop::Router\n  history :browser\n  route do\n    SECTION(class: 'todo-app') do # add the class param\n      Header()\n      Route('/:scope', mounts: Index)\n      Footer()\n    end\n  end\nend\n```\nThe `Footer` components needs have a `UL` added to hold the links nicely,\nand we can also use the `NavLinks` `active_class` param to highlight the link that is currently active:\n\n```ruby\n# app/hyperloop/components/footer.rb\nclass Footer \u003c Hyperloop::Component\n  include HyperRouter::ComponentMethods\n  def link_item(path)\n    # wrap the NavLink in a LI and\n    # tell the NavLink to change the class to :selected when\n    # the current (active) path equals the NavLink's path.\n    LI { NavLink(\"/#{path}\", active_class: :selected) { path.camelize } }\n  end\n  render(DIV, class: :footer) do   # add class\n    UL(class: :filters) do         # wrap links in a UL\n      link_item(:all)\n      link_item(:active)\n      link_item(:completed)\n    end\n  end\nend\n```\nFor the Index component just add the `main` and `todo-list` classes.\n\n```ruby\n# app/hyperloop/components/index.rb\nclass Index \u003c Hyperloop::Router::Component\n  render(SECTION, class: :main) do         # add class main\n    UL(class: 'todo-list') do              # add class todo-list\n      Todo.send(match.params[:scope]).each do |todo|\n        TodoItem(todo: todo)\n      end\n    end\n  end\nend\n```\nFor the EditItem component we want the caller specify the class.  To keep things compatible with React.js we need to call the param `className`,\nbut we can still send it to EditItem with the usual hyperloop style `class` param.  \n\n```ruby\n# app/hyperloop/components/edit_item.rb\nclass EditItem \u003c Hyperloop::Component\n  param :todo\n  param :on_save, type: Proc\n  param :on_cancel, type: Proc\n  param :className # recieves class params\n  after_mount { Element[dom_node].focus }\n  render do\n    # pass the className param as the INPUT's class\n    INPUT(\n      class: params.className,\n      defaultValue: params.todo.title,\n      key: params.todo.object_id\n    ).on(:key_down) do |evt|\n      next unless evt.key_code == 13\n      params.todo.update(title: evt.target.value)\n      params.on_save\n    end\n    .on(:blur) { params.on_cancel }\n  end\nend\n```\nNow we can add classes to the TodoItem's list-item, input, anchor tags, and to the `EditItem` component:\n\n```ruby\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  state editing: false\n  render(LI, class: 'todo-item') do\n    if state.editing\n      EditItem(class: :edit, todo: params.todo)\n        .on(:save, :cancel) { mutate.editing false }\n    else\n      INPUT(type: :checkbox, class: :toggle, checked: params.todo.completed)\n        .on(:click) { params.todo.update(completed: !params.todo.completed) }\n      LABEL { params.todo.title }\n        .on(:double_click) { mutate.editing true }\n      A(class: :destroy) # also remove the { '-X-' } placeholder\n        .on(:click) { params.todo.destroy }\n    end\n  end\nend\n```\nIn the Header we can send a different class to the `EditItem` component.  While we are at it\nwe will add the `H1 { 'todos' }` hero unit.\n\n```ruby\n# app/hyperloop/components/header.rb\nclass Header \u003c Hyperloop::Component\n  state(:new_todo) { Todo.new }\n  render(HEADER, class: :header) do                   # add the 'header' class\n    H1 { 'todos' }                                    # Add the hero unit.\n    EditItem(class: 'new-todo', todo: state.new_todo) # add 'new-todo' class\n      .on(:save) { mutate.new_todo Todo.new }\n  end\nend\n```\nAt this point your Todo App should be properly styled.\n\n### Chapter 12: Other Features\n\n+ **Show How Many Items Left In Footer**  \nThis is just a span that we add before the link tags list in the Footer component:\n\n  ```ruby\n  ...\n  render(DIV, class: :footer) do\n    SPAN(class: 'todo-count') do\n      \"#{Todo.active.count} item#{'s' if Todo.active.count != 1} left\"\n    end\n    UL(class: :filters) do\n    ...\n  ```\n+ **Add 'placeholder' Text To Edit Item**  \nEditItem should display a meaningful placeholder hint if the title is blank:   \n\n  ```ruby\n    ...\n    INPUT(\n      class: params.className,\n      defaultValue: params.todo.title,\n      key: params.todo.object_id,\n      placeholder: 'What is left to do today?'\n    ).on(:key_down) do |evt| ...\n    ...\n  ```\n+ **Don't Show the Footer If There are No Todos**  \nIn the `App` component add a *guard* so that we won't show the Footer if there are no Todos:  \n\n  ```ruby\n  ...\n      Footer() unless Todo.count.zero?\n  ...\n  ```\n\n\nCongratulations! you have completed the tutorial.\n\n### Summary\n\nYou have built a small but feature rich full stack Todo application in less than 100 lines of code:\n\n```text\nSLOC  \n--------------  \nApp:        11  \nHeader:      8\nIndex:       9  \nTodoItem:   17  \nEditItem:   21  \nFooter:     16  \nTodo Model:  4  \nRails Route: 1  \n--------------  \nTotal:      87  \n```\n\nThe complete application is shown here:\n\n```ruby\n# app/hyperloop/components/app.rb\nclass App \u003c Hyperloop::Router\n  history :browser\n  route do # note instead of render we use the route method\n    SECTION(class: 'todo-app') do\n      Header()\n      Route('/', exact: true) { Redirect('/all') }\n      Route('/:scope', mounts: Index)\n      Footer() unless Todo.count.zero?\n    end\n  end\nend\n\n# app/hyperloop/components/header.rb\nclass Header \u003c Hyperloop::Component\n  state(:new_todo) { Todo.new }\n  render(HEADER, class: :header) do\n    H1 { 'todos' }\n    EditItem(class: 'new-todo', todo: state.new_todo)\n      .on(:save) { mutate.new_todo Todo.new }\n  end\nend\n\n# app/hyperloop/components/index.rb\nclass Index \u003c Hyperloop::Router::Component\n  render(SECTION, class: :main) do\n    UL(class: 'todo-list') do\n      Todo.send(match.params[:scope]).each do |todo|\n        TodoItem(todo: todo)\n      end\n    end\n  end\nend\n\n# app/hyperloop/components/footer.rb\nclass Footer \u003c Hyperloop::Component\n  include HyperRouter::ComponentMethods\n  def link_item(path)\n    LI { NavLink(\"/#{path}\", active_class: :selected) { path.camelize } }\n  end\n  render(DIV, class: :footer) do\n    SPAN(class: 'todo-count') do\n      \"#{Todo.active.count} item#{'s' if Todo.active.count != 1} left\"\n    end\n    UL(class: :filters) do\n      link_item(:all)\n      link_item(:active)\n      link_item(:completed)\n    end\n  end\nend\n\n# app/hyperloop/components/todo_item.rb\nclass TodoItem \u003c Hyperloop::Component\n  param :todo\n  state editing: false\n  render(LI, class: 'todo-item') do\n    if state.editing\n      EditItem(todo: params.todo, class: :edit)\n        .on(:save, :cancel) { mutate.editing false }\n    else\n      INPUT(type: :checkbox, class: :toggle, checked: params.todo.completed)\n        .on(:click) { params.todo.update(completed: !params.todo.completed) }\n      LABEL { params.todo.title }\n        .on(:double_click) { mutate.editing true }\n      A(class: :destroy).on(:click) { params.todo.destroy }\n    end\n  end\nend\n\n# app/hyperloop/components/edit_item.rb\nclass EditItem \u003c Hyperloop::Component\n  param :todo\n  param :on_save, type: Proc               \n  param :on_cancel, type: Proc             \n  param :className\n  after_mount { Element[dom_node].focus }  \n\n  render do\n    INPUT(\n      class: params.className,\n      defaultValue: params.todo.title,\n      key: params.todo.object_id,\n      placeholder: 'What is left to do today?'\n    ).on(:key_down) do |evt|\n      next unless evt.key_code == 13\n      params.todo.update(title: evt.target.value)\n      params.on_save                       \n    end\n    .on(:blur) { params.on_cancel }\n  end\nend\n\n# app/hyperloop/models/todo.rb\nclass Todo \u003c ApplicationRecord\n  scope :completed, -\u003e () { where(completed: true)  }\n  scope :active,    -\u003e () { where(completed: false) }\nend\n\n# config/routes.rb\nRails.application.routes.draw do\n  mount Hyperloop::Engine =\u003e '/hyperloop'\n  get '/(*other)', to: 'hyperloop#app'\n  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html\nend\n```\n\n### General troubleshooting\n\n1: Wait. On initial boot it can take several minutes to pre-compile all the system assets.  \n\n2: Make sure to save (or better yet do a git commit) after every instruction so that you can backtrack\n\n3: Its possible to get things so messed up the hot-reloader will not work.  Restart the server and reload the browser.\n\n\u003chr\u003e\n\nYou can find the final application source code here:\n\n\u003cbr\u003e\n\n\u003ca href=\"https://github.com/ruby-hyperloop/hyperloop-rails-webpackergem-helloworld\" alt=\"Tutorial source code\" title=\"Tutorial source code\"\u003e\n\u003cimg src=\"http://ruby-hyperloop.org/images/githubsourcecodebadge.png\"\u003e\n\u003c/a\u003e\n\n\u003cbr\u003e\u003cbr\u003e\n\nThe best way to get help and contribute is to join our Gitter Chat\n\n\u003ca href=\"https://gitter.im/ruby-hyperloop/chat\" alt=\"Gitter chat\" title=\"Gitter chat\"\u003e\n\u003cimg src=\"http://ruby-hyperloop.org/images/githubgitterbadge.png\"\u003e\n\u003c/a\u003e\n\n\u003c/div\u003e\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruby-hyperloop%2Ftodo-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruby-hyperloop%2Ftodo-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruby-hyperloop%2Ftodo-tutorial/lists"}