{"id":14555,"date":"2021-12-13T12:12:00","date_gmt":"2021-12-13T09:12:00","guid":{"rendered":"https:\/\/railsware.com\/blog\/?p=14555"},"modified":"2022-01-26T16:41:31","modified_gmt":"2022-01-26T13:41:31","slug":"almost-jsless-frontend-on-rails","status":"publish","type":"post","link":"https:\/\/railsware.com\/blog\/almost-jsless-frontend-on-rails\/","title":{"rendered":"Almost JSless Frontend on Rails"},"content":{"rendered":"\n<div class=\"intro-text\">Despite some thoughts you can hear in the developer communities, Rails is not becoming a legacy technology, it\u2019s not going to die, and it\u2019s still a great tool to build your new project. One of the reasons is that Rails has enough tooling to cover the essential parts of a typical web application. You don\u2019t have to think about how to handle HTTP requests, what to use to input and retrieve data from the database, how to generate HTML that users will see in their browsers, and even how to give life to the UI.<\/div>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"519\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-1024x519.jpg\" alt=\"JSless Frontend on Rails\" class=\"wp-image-14593\" srcset=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-1024x519.jpg 1024w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-360x182.jpg 360w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-768x389.jpg 768w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-1536x778.jpg 1536w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-2048x1038.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Rails tools out of the box: rails-ujs, turbolinks<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Rails UJS<\/h3>\n\n\n\n<p>For a long time, since back when I was trying to build my first web page using only HTML, Rails had a cool tool called <em>jquery-ujs <\/em>(unobtrusive javascript), which is now called <em>rails-ujs<\/em>. It works great with a rails backend when you want to add a pinch of AJAX requests for a small price.<\/p>\n\n\n\n<p>You can do something like this.<\/p>\n\n\n\n<p><strong>app\/controllers\/money_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class MoneyController &lt; ApplicationController\n  def show\n    @money = GetAllMoney.call\n  end\n\n  def destroy\n    SpendAllMoney.call\n  end\nend<\/code><\/pre>\n\n\n\n<p><strong>views\/money\/show.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div class=\"money\"&gt;\n  &lt;h3&gt;Your money&lt;\/h3&gt;\n  &lt;span id=\"money-amount\"&gt;&lt;%= @money %&gt;&lt;\/span&gt;\n  &lt;span&gt;$&lt;\/span&gt;\n\n  &lt;%= link_to 'Spend all money',\n              money_path,\n              method: 'delete',\n              remote: true,\n              data: { confirm: 'Do you want to spend all money?' },\n              class: 'spend-money-button' %&gt;\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p><strong>views\/money\/destroy.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>document.querySelector('#money-amount').innerHTML = 0<\/code><\/pre>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"817\" height=\"638\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/90f4fb7dc211443885191c750c05f4e9.gif\" alt=\"\" class=\"wp-image-14561\"\/><\/figure><\/div>\n\n\n\n<p>So you\u2019ve made an AJAX request with a few HTML attributes and one JS file with one line of code. Pretty cool!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Turbolinks<\/h3>\n\n\n\n<p>Another old-timer in the Rails world is Turbolinks. It is not under active development, but we will talk about its successor later. To make a long story short, Turbolinks brings you a SPA experience with almost no client-side code. To be more detailed it:&nbsp;<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>loads new page content with js and replaces it on the page without reloading the browser;<\/li><li>caches pages, so return visits will seem instant;<\/li><li>gives the ability to persist elements on the page during the navigation.<\/li><\/ul>\n\n\n\n<p>The first two features work out of the box, but the last one must be explicitly defined in your code. I will show a little and far-fetched example of what we can achieve with it.&nbsp;&nbsp;<\/p>\n\n\n\n<p>Let\u2019s assume we have a notifications counter somewhere on the page.<\/p>\n\n\n\n<p><strong>app\/helpers\/application_helper.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>module ApplicationHelper\n  def notifications_count\n    sleep 3 # emulate some calculations\n\n    10\n  end\n\n  def articles\n    Article.last(5)\n  end\nend<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/layouts\/application.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;\n  &lt;title&gt;Turbolinks&lt;\/title&gt;\n  &lt;meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"&gt;\n  &lt;%= csrf_meta_tags %&gt;\n  &lt;%= csp_meta_tag %&gt;\n\n  &lt;%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %&gt;\n  &lt;%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %&gt;\n&lt;\/head&gt;\n\n&lt;body&gt;\n&lt;div class=\"container\"&gt;\n  &lt;nav class=\"navigation\"&gt;\n    &lt;ul&gt;\n      &lt;%- articles.each do |article| %&gt;\n        &lt;li&gt;\n          &lt;%= link_to article.title, article_path(article.id) %&gt;\n        &lt;\/li&gt;\n      &lt;% end %&gt;\n    &lt;\/ul&gt;\n    &lt;div class=\"notifications\"&gt;\n      &lt;div class=\"notifications-badge\"&gt;\n        &lt;%= notifications_count %&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  &lt;\/nav&gt;\n  &lt;section class=\"content\"&gt;\n    &lt;%= yield %&gt;\n  &lt;\/section&gt;\n&lt;\/div&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n\n\n\n<p>Calculating the number of notifications may take a while, but this is a price you pay to keep it up-to-date.<\/p>\n\n\n\n<p>As a next step, you might also want to keep the number of notifications updated by subscribing to notifications updates in real-time. Rails has an Action Cable built-in for this.<\/p>\n\n\n\n<p>Since this work is done on a frontend you don\u2019t really need to calculate the total number between page transitions handled by Turbolinks. Of course, the whole issue could be solved by a simple caching but you know&#8230;there are only two hard things in CS&#8230;cache invalidation&#8230;and we are talking about Turbolinks anyway.<\/p>\n\n\n\n<p>So we can just exclude code from execution if the page was requested by Turbolinks and exclude the part of the page from being updated by Turbolinks.<\/p>\n\n\n\n<p><strong><strong>app\/helpers\/application_helper.rb<\/strong><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>module ApplicationHelper\n  def notifications_count\n+   return nil if request.headers&#91;'Turbolinks-Referrer'].present?\n+\n    sleep 3 # emulate some calculations\n\n    10\n  end\n\n  def articles\n    Article.last(5)\n  end\nend<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/layouts\/application.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div class=\"notifications\"&gt;\n-   &lt;div class=\"notifications-badge\" id=\"notifications-badge\"&gt;\n+   &lt;div class=\"notifications-badge\" id=\"notifications-badge\" data-turbolinks-permanent&gt;\n    &lt;%= notifications_count %&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">What is not enough for us<\/h3>\n\n\n\n<p>Even though these Rails long-livers do their job well and many apps successfully built complex UIs on top of them without introducing any complex JS framework, we still miss some features that will make our applications more maintainable and building interfaces easier.<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">New tools from the Rails team<\/h2>\n\n\n\n<p>At the beginning of 2021, DHH made some noise announcing the <a href=\"https:\/\/hotwire.dev\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Hotwire<\/a>, which presented a new Rails way for building user interfaces. Despite Hotwire being a collective name for a family of libraries, this family is relatively small. It only has two libraries as of October 2021:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/turbo.hotwire.dev\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Turbo<\/a><\/li><li><a href=\"https:\/\/stimulus.hotwire.dev\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Stimulus<\/a><\/li><\/ul>\n\n\n\n<p>The Rails team developed both of them, and both can be seamlessly integrated into your majestic monolith. I will share more information about Turbo since it\u2019s relatively new and will replace the already existing Turbolinks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Turbo<\/h3>\n\n\n\n<p>If you thought that Turbolinks had lost its \u201clinks\u201d part because it\u2019s not only about the navigation anymore, you are 100% right. Turbo is divided into a few parts, with each of them serving one purpose \u2013 to bring the server-rendered HTML to your app, with some differences in when and how it\u2019s done:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Turbo Drive: That good old Turbolinks we are familiar with.<\/li><li>Turbo Frames: Decomposable frames that can be loaded asynchronously and updated when the server returns a frame with the same id.<\/li><li>Turbo Streams: Another kind of frame in which an update is triggered due to an HTTP request or by the server via Websocket.<\/li><li>Turbo Native: A wrapper over your turbo-enabled web application that integrates it into your mobile application.<\/li><\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Turbo Drive<\/h4>\n\n\n\n<p>As was mentioned before, Turbo Drive replaces Turbolinks and handles page navigation. Since almost nothing changed, the migration is pretty simple. And replace <code>data-turbolinks<\/code>&#8230; attributes with <code>data-turbo<\/code>&#8230;<\/p>\n\n\n\n<p>You just have to add npm package<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>yarn add @hotwired\/turbo<\/code><\/pre>\n\n\n\n<p>Replace Turbolinks with Turbo in your javascript code<\/p>\n\n\n\n<p><strong>app\/javascript\/packs\/application.js<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code> import Rails from \"@rails\/ujs\"\n- import Turbolinks from \"turbolinks\"\n+ import * as Turbo from \"@hotwired\/turbo\"\n \n  Rails.start()\n- Turbolinks.start()<\/code><\/pre>\n\n\n\n<p>And replace <code>data-turbolinks<\/code>&#8230; attributes with <code>data-turbo<\/code>&#8230;<\/p>\n\n\n\n<p><strong>app\/views\/layouts\/application.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\n-    &lt;%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %&gt;\n-    &lt;%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %&gt;\n+    &lt;%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %&gt;\n+    &lt;%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %&gt;<\/code><\/pre>\n\n\n\n<p>One thing that needs a bit more attention is a form submission. Turbo drive handles this too. First of all, it expects that your redirects after the form submission will have a 303 status code to allow Fetch API to follow this redirect automatically. This is the correct HTTP status for non-idempotent (a clever word to describe non-GET or HEAD methods \ud83d\ude01) requests if you want redirects to be made via GET method. Otherwise, only POST requests will be redirected correctly since they allow 301 and 302 statuses as well. So you might want to explicitly add a status code to your redirects.<\/p>\n\n\n\n<p><strong>app\/controllers\/any_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>redirect_to money_path\n+    redirect_to money_path, status: :see_other<\/code><\/pre>\n\n\n\n<p>However, Rails\u2019 forms use the POST method anyway and add&nbsp; <code>&lt;input type=\"hidden\" name=\"_method\" value=\"patch\"&gt;<\/code> to define what controller action to use. This means that your forms will still work and there have already been <a href=\"https:\/\/github.com\/hotwired\/turbo\/issues\/84\">heated discussions<\/a> about the necessity for the correct status code.&nbsp;<\/p>\n\n\n\n<p>The second thing to note is that Turbo doesn\u2019t respect the <code>local: true<\/code> parameter that you might have used to disable JS control over the form. If this is your case, another slight change should be made.<\/p>\n\n\n\n<p><strong>app\/views\/_any_form.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>- &lt;%= form_with(url: money_path, local: true) do |f| %&gt;\n+ &lt;%= form_with(url: money_path, data: { turbo: false }) do |f| %&gt;<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Turbo Frames<\/h4>\n\n\n\n<p>We had finally reached something new in the Rails stack. Turbo Frame is a simple tool to create a container with content that can be loaded and updated separately. Just like the <a href=\"https:\/\/github.com\/renderedtext\/render_async\">render_async<\/a> gem or Rails\u2019 <code>.ejs<\/code> responses, but with less code to be written&nbsp;&nbsp;&nbsp;<\/p>\n\n\n\n<p>Let\u2019s see an example of how we can decompose our page into a few asynchronous loaded parts that are loaded only when a user sees them. Imagine a product page with general information, product properties, and customer reviews. There is no guarantee that a user will visit each of these sections so we can load them only if a user really reached that part of the page.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1460\" height=\"939\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/f246917b1a5b8252d925d04e83749a42.gif\" alt=\"\" class=\"wp-image-14565\"\/><\/figure><\/div>\n\n\n\n<p>I\u2019ll skip the part of the example related to the setting up models, routes, Bootstrap installation, and adding some CSS. I assume you are not interested in such basic things.<\/p>\n\n\n\n<p>This is how our <code>products\/show.html.erb<\/code> looks like.&nbsp;<\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div class=\"product\"&gt;\n  &lt;ul class=\"nav nav-tabs\" id=\"product-tab\" role=\"tablist\"&gt;\n    &lt;li class=\"nav-item\" role=\"presentation\"&gt;\n      &lt;button class=\"nav-link active\" id=\"home-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#general\" type=\"button\" role=\"tab\" aria-controls=\"general\" aria-selected=\"true\"&gt;General&lt;\/button&gt;\n    &lt;\/li&gt;\n    &lt;li class=\"nav-item\" role=\"presentation\"&gt;\n      &lt;button class=\"nav-link\" id=\"properties-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#properties\" type=\"button\" role=\"tab\" aria-controls=\"properties\" aria-selected=\"false\"&gt;Properties&lt;\/button&gt;\n    &lt;\/li&gt;\n    &lt;li class=\"nav-item\" role=\"presentation\"&gt;\n      &lt;button class=\"nav-link\" id=\"reviews-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#reviews\" type=\"button\" role=\"tab\" aria-controls=\"reviews\" aria-selected=\"false\"&gt;Reviews&lt;\/button&gt;\n    &lt;\/li&gt;\n  &lt;\/ul&gt;\n\n  &lt;div class=\"tab-content\"&gt;\n    &lt;div class=\"tab-pane active p-3\" id=\"general\" role=\"tabpanel\" aria-labelledby=\"general-tab\"&gt;\n      &lt;turbo-frame id=\"&lt;%= dom_id(@product, 'general') %&gt;\" loading=\"lazy\" src=\"&lt;%= general_product_path %&gt;\"&gt;\n        &lt;%= render('common\/spinner') %&gt;\n      &lt;\/turbo-frame&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"tab-pane p-3\" id=\"properties\" role=\"tabpanel\" aria-labelledby=\"properties-tab\"&gt;\n      &lt;turbo-frame id=\"&lt;%= dom_id(@product, 'properties') %&gt;\" loading=\"lazy\" src=\"&lt;%= properties_product_path %&gt;\"&gt;\n        &lt;%= render('common\/spinner') %&gt;\n      &lt;\/turbo-frame&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"tab-pane p-3\" id=\"reviews\" role=\"tabpanel\" aria-labelledby=\"reviews-tab\"&gt;\n      &lt;turbo-frame id=\"&lt;%= dom_id(@product, 'reviews') %&gt;\" loading=\"lazy\" src=\"&lt;%= reviews_product_path %&gt;\"&gt;\n        &lt;%= render('common\/spinner') %&gt;\n      &lt;\/turbo-frame&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p>It\u2019s the basic Bootstrap\u2019s tab bar. But the interesting things happen in the <code>.tab-page<\/code> elements. We have added the <code>turbo-frame<\/code> tag which is our container for being loaded and updated. Each frame must have its own unique <code>id<\/code> attribute and <code>dom_id<\/code> helper is a good tool to free us from the need to think about naming.<\/p>\n\n\n\n<p>To load the frame asynchronously we have to add an <code>src<\/code> attribute and the response from this path should return the frame with the same id.<\/p>\n\n\n\n<p>And since we want to load only the visible part, we add <code>loading=\"lazy\"<\/code> and the frame will be loaded only when this tag of the page will become visible. Pay attention that it doesn\u2019t matter how this element became visible. A user can just scroll to this tag and it will load its content. Its parent\u2019s style can change from <code>display: none<\/code> to <code>display: block<\/code>. The app can insert it into the page via Javascript or you can even recursively render one frame from another (but don\u2019t forget to exit from the recursion somehow).<\/p>\n\n\n\n<p>The spinner in the example is just a div with CSS animation. You don\u2019t need to manage it because it will just spin until the frame\u2019s content is loaded and inserted into the page.<\/p>\n\n\n\n<p><strong>app\/views\/common\/_spinner.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div class=\"text-center mt-5\"&gt;\n  &lt;div class=\"spinner-grow text-secondary\" role=\"status\"&gt;\n    &lt;span class=\"visually-hidden\"&gt;Loading...&lt;\/span&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p>Alternatively, you could utilize the <code>busy<\/code> attribute that is added to the frame when it loads and add some custom CSS to show the loading state.<\/p>\n\n\n\n<p>Our controller is pretty straightforward.<\/p>\n\n\n\n<p><strong>app\/controllers\/products_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ProductsController &lt; ApplicationController\n  def show\n    @product = Product.find(params&#91;:id])\n  end\n\n  def general\n    @product = Product.find(params&#91;:id])\n\n    render partial: 'products\/general'\n  end\n\n  def properties\n    @product = Product.find(params&#91;:id])\n\n    render partial: 'products\/properties'\n  end\n\n  def reviews\n    @product = Product.find(params&#91;:id])\n\n    render partial: 'products\/reviews'\n  end\nend<\/code><\/pre>\n\n\n\n<p><strong><strong>app\/views\/products\/_general.html.erb<\/strong><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;turbo-frame id=\"&lt;%= dom_id(@product, 'general') %&gt;\" loading=\"lazy\" src=\"&lt;%= general_product_path(product_id: @product) %&gt;\"&gt;\n  &lt;div class=\"product--general\"&gt;\n    &lt;h1&gt;\n      &lt;%= @product.title %&gt;\n    &lt;\/h1&gt;\n    &lt;div class=\"row mt-4\"&gt;\n      &lt;div class=\"col\"&gt;\n        &lt;div class=\"product--image\"&gt;\n          &lt;%= image_tag @product.image %&gt;\n        &lt;\/div&gt;\n\n      &lt;\/div&gt;\n      &lt;div class=\"col\"&gt;\n        &lt;h3&gt;\n          &lt;%= @product.price %&gt;\n        &lt;\/h3&gt;\n\n        &lt;%= @product.content %&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/turbo-frame&gt;<\/code><\/pre>\n\n\n\n<p><strong><strong>app\/views\/products\/_properties.html.erb<\/strong><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;turbo-frame id=\"&lt;%= dom_id(@product, 'properties') %&gt;\"&gt;\n  &lt;h1&gt;\n    &lt;%= @product.title %&gt; properties\n  &lt;\/h1&gt;\n  &lt;dl class=\"row mt-4\"&gt;\n    &lt;%- @product.properties.each do |name, value| %&gt;\n      &lt;dt class=\"col-sm-3\"&gt;&lt;%= name.to_s.titleize %&gt;&lt;\/dt&gt;\n      &lt;dd class=\"col-sm-9\"&gt;&lt;%= value %&gt;&lt;\/dd&gt;\n    &lt;% end %&gt;\n  &lt;\/dl&gt;\n&lt;\/turbo-frame&gt;<\/code><\/pre>\n\n\n\n<p><strong><strong>app\/views\/products\/_review.html.erb<\/strong><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;turbo-frame id=\"&lt;%= dom_id(@product, 'reviews') %&gt;\"&gt;\n  &lt;%- @product.reviews.each do |review| %&gt;\n    &lt;div class=\"card mb-3\"&gt;\n      &lt;div class=\"card-body\"&gt;\n        &lt;div class=\"card-title\"&gt;\n          &lt;%= review.author %&gt;\n        &lt;\/div&gt;\n        &lt;div class=\"card-text\"&gt;\n          &lt;%= review.content %&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  &lt;% end %&gt;\n&lt;\/turbo-frame&gt;<\/code><\/pre>\n\n\n\n<p>We render partials here but it can be the whole page with the layout as well. The main thing is to render a <code>turbo-frame<\/code> tag with the same id as the tag-requestor has.&nbsp;&nbsp;<\/p>\n\n\n\n<p>Actually, that\u2019s all you need to have a lazy-loaded page.&nbsp;<\/p>\n\n\n\n<p>Unfortunately, I wasn\u2019t able to find a convenient way to handle errors with turbo frames but this solution can be handy:<\/p>\n\n\n\n<p><strong>app\/controllers\/any_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def general\n  @product = Product.find(params&#91;:id])\n\n  raise StandardError, 'Some error'\n\n  render partial: 'products\/general', locals: { in_cart: product_in_cart?(@product) }\nrescue StandardError\n  render partial: 'common\/turbo_error',\n         locals: { id: dom_id(@product, 'general'), error_message: 'Oops. Something went wrong' }\nend<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/common\/_turbo_error.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;turbo-frame id=\"&lt;%= id %&gt;\"&gt;\n  &lt;%= error_message %&gt;\n&lt;\/turbo-frame&gt;<\/code><\/pre>\n\n\n\n<p>Another cool thing we can do with turbo-frame is replace parts of the page as a response to the form submission. The idea is pretty much the same. Controller action should return the <code>turbo-frame<\/code> tag and turbo will replace it on the page. Let\u2019s extend the previous example to have the ability to add an item to the cart and remove it from it.<\/p>\n\n\n\n<p><strong>app\/controllers\/products_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ProductsController &lt; ApplicationController\n  include ActionView::RecordIdentifier\n\n  def show\n    @product = Product.find(params&#91;:id])\n  end\n\n  def general\n    @product = Product.find(params&#91;:id])\n\n-   render partial: 'products\/general'\n+   render partial: 'products\/general', locals: { in_cart: product_in_cart?(@product) }\n  end\n\n  def properties\n    @product = Product.find(params&#91;:id])\n\n    render partial: 'products\/properties'\n  end\n\n  def reviews\n    @product = Product.find(params&#91;:id])\n\n    render partial: 'products\/reviews'\n  end\n\n+  def add_to_cart\n+    @product = Product.find(params&#91;:id])\n+\n+    session&#91;:cart] = (session&#91;:cart] || &#91;]) &lt;&lt; @product.id\n+\n+    render partial: 'products\/general', locals: { in_cart: product_in_cart?(@product) }\n+  end\n+\n+  def remove_from_cart\n+    @product = Product.find(params&#91;:id])\n+\n+    session&#91;:cart] = (session&#91;:cart] || &#91;]).reject { |id| @product.id == id }\n+\n+    render partial: 'products\/general', locals: { in_cart: product_in_cart?(@product) }\n+  end\n+\n+  private\n+\n+  def product_in_cart?(product)\n+    return false unless product &amp;&amp; session&#91;:cart]\n+\n+    session&#91;:cart].include?(product.id)\n+  end\nend<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/products\/_general.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;turbo-frame id=\"&lt;%= dom_id(@product, 'general') %&gt;\" loading=\"lazy\" src=\"&lt;%= general_product_path(product_id: @product) %&gt;\"&gt;\n  &lt;div class=\"product--general\"&gt;\n    &lt;h1&gt;\n      &lt;%= @product.title %&gt;\n    &lt;\/h1&gt;\n    &lt;div class=\"row mt-4\"&gt;\n      &lt;div class=\"col\"&gt;\n        &lt;div class=\"product--image\"&gt;\n          &lt;%= image_tag @product.image %&gt;\n        &lt;\/div&gt;\n\n      &lt;\/div&gt;\n      &lt;div class=\"col\"&gt;\n        &lt;h3&gt;\n          &lt;%= @product.price %&gt;\n        &lt;\/h3&gt;\n\n+        &lt;%- if in_cart %&gt;\n+          &lt;%= form_with(url: remove_from_cart_product_path) do |f| %&gt;\n+            &lt;%= f.submit 'Remove from cart', class: 'my-3 btn btn-danger' %&gt;\n+          &lt;%- end %&gt;\n+        &lt;%- else %&gt;\n+          &lt;%= form_with(url: add_to_cart_product_path) do |f| %&gt;\n+            &lt;%= f.submit 'Add to cart', class: 'my-3 btn btn-success' %&gt;\n+          &lt;%- end %&gt;\n+        &lt;% end %&gt;\n        &lt;%= @product.content %&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/turbo-frame&gt;<\/code><\/pre>\n\n\n\n<p>You can see that our controller now has actions to add items to the cart and remove them from it. Both of these methods just render the <code>general<\/code> partial and it\u2019s magically replaced on the page. This is pretty similar to what was usually done in .<code>js.erb<\/code> templates but I would prefer turbo way more to not have extra js code that is outside of all your javascripts.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Turbo Streams<\/h4>\n\n\n\n<p>Turbo has brought us one more interesting tool to modify the html on the page &#8211; Turbo Streams. It brings more opportunities to update DOM and you are not limited to replacing only one frame as you are with Turbo frames. This DOM manipulation is called <code>action<\/code> and should be performed around the <code>targets<\/code> &#8211; elements received by some selector. Turbo streams gives you 7 actions to be performed<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>append &#8211; add html to the beginning of the target<\/li><li>prepend &#8211; add html to the end of the target<\/li><li>replace &#8211; replace the whole target with html provided<\/li><li>update &#8211; update html inside the target<\/li><li>remove &#8211; remove the whole target&nbsp;<\/li><li>before &#8211; add html after the target<\/li><li>after &#8211; add html before the target&nbsp;<\/li><\/ul>\n\n\n\n<p>You can usually hear\/read about the Turbo Streams when it comes to the real-time updates and building another chat application. But we can start with a simpler example and see how Turbo Streams helps to reflect the form submission in UI.<\/p>\n\n\n\n<p>Let\u2019s proceed with the previous example and add the ability to post a new review and show the total number of reviews.<\/p>\n\n\n\n<p>At the beginning, I\u2019ll just add the number of reviews and a form for adding a new review.<\/p>\n\n\n\n<p><strong><strong>app\/views\/products\/show.html.erb<\/strong><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;ul class=\"nav nav-tabs\" id=\"product-tab\" role=\"tablist\"&gt;\n    &lt;li class=\"nav-item\" role=\"presentation\"&gt;\n      &lt;button class=\"nav-link active\" id=\"home-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#general\" type=\"button\" role=\"tab\" aria-controls=\"general\" aria-selected=\"true\"&gt;General&lt;\/button&gt;\n    &lt;\/li&gt;\n    &lt;li class=\"nav-item\" role=\"presentation\"&gt;\n      &lt;button class=\"nav-link\" id=\"properties-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#properties\" type=\"button\" role=\"tab\" aria-controls=\"properties\" aria-selected=\"false\"&gt;Properties&lt;\/button&gt;\n    &lt;\/li&gt;\n    &lt;li class=\"nav-item\" role=\"presentation\"&gt;\n-      &lt;button class=\"nav-link\" id=\"reviews-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#reviews\" type=\"button\" role=\"tab\" aria-controls=\"reviews\" aria-selected=\"false\"&gt;Reviews&lt;\/button&gt;\n+      &lt;button\n+        class=\"nav-link\"\n+        id=\"reviews-tab\"\n+        data-bs-toggle=\"tab\"\n+        data-bs-target=\"#reviews\"\n+        type=\"button\"\n+        role=\"tab\"\n+        aria-controls=\"reviews\"\n+        aria-selected=\"false\"\n+      &gt;\n+        Reviews\n+        &lt;span id=&lt;%= dom_id(@product, 'reviews_count') %&gt;&gt;\n+          &lt;%= render(partial: 'products\/reviews\/count_badge', locals: { count: @product.reviews.count }) %&gt;\n+        &lt;\/span&gt;\n+      &lt;\/button&gt;\n    &lt;\/li&gt;\n  &lt;\/ul&gt;<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/products\/_reviews.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;turbo-frame id=\"&lt;%= dom_id(@product, 'reviews') %&gt;\"&gt;\n  &lt;%= render(partial: 'products\/reviews\/form', locals: { product: @product }) %&gt;\n\n  &lt;div id=\"&lt;%= dom_id(@product, 'reviews_list') %&gt;\"&gt;\n    &lt;%- @product.reviews.each do |review| %&gt;\n      &lt;%= render(partial: 'products\/reviews\/card', locals: { review: review }) %&gt;\n    &lt;% end %&gt;\n  &lt;\/div&gt;\n&lt;\/turbo-frame&gt;<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/products\/reviews\/_count_badge.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;span class=\"badge bg-primary\"&gt;\n  &lt;%= count %&gt;\n&lt;\/span&gt;<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/products\/reviews\/_form.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%= form_with(url: add_review_product_path(id: product.id), class: 'mb-4', id: dom_id(product, 'reviews_form')) do |f| %&gt;\n  &lt;%= f.text_area :review, class: \"form-control mb-1\" %&gt;\n  &lt;%= f.submit 'Add a review', class: 'btn btn-primary' %&gt;\n&lt;% end %&gt;<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/products\/reviews\/_card.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%= form_with(url: add_review_product_path(id: product.id), class: 'mb-4', id: dom_id(product, 'reviews_form')) do |f| %&gt;\n  &lt;%= f.text_area :review, class: \"form-control mb-1\" %&gt;\n  &lt;%= f.submit 'Add a review', class: 'btn btn-primary' %&gt;\n&lt;% end %&gt;<\/code><\/pre>\n\n\n\n<p>This is how it\u2019s going to look.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1560\" height=\"720\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/45479aaad413d52106705dab6e8db7d2.jpg.png\" alt=\"\" class=\"wp-image-14569\" srcset=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/45479aaad413d52106705dab6e8db7d2.jpg.png 1560w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/45479aaad413d52106705dab6e8db7d2.jpg-360x166.png 360w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/45479aaad413d52106705dab6e8db7d2.jpg-1024x473.png 1024w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/45479aaad413d52106705dab6e8db7d2.jpg-768x354.png 768w, https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/45479aaad413d52106705dab6e8db7d2.jpg-1536x709.png 1536w\" sizes=\"auto, (max-width: 1560px) 100vw, 1560px\" \/><\/figure><\/div>\n\n\n\n<p>Now we can add some interactivity using Turbo Streams.<\/p>\n\n\n\n<p><strong>app\/controllers\/products_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>+ def add_review\n+    @product = Product.find(params&#91;:id])\n+    @review = @product.add_review(author: 'You', content: params&#91;:review])\n+ end<\/code><\/pre>\n\n\n\n<p><strong>app\/views\/products\/add_review.turbo_stream.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;%= turbo_stream.update(dom_id(@product, 'reviews_count')) do %&gt;\n  &lt;%= render(partial: 'products\/reviews\/count_badge', locals: { count: @product.reviews.count }) %&gt;\n&lt;% end %&gt;\n\n&lt;%= turbo_stream.replace(dom_id(@product, 'reviews_form')) do %&gt;\n  &lt;%= render(partial: 'products\/reviews\/form', locals: { product: @product }) %&gt;\n&lt;% end %&gt;\n\n&lt;%= turbo_stream.prepend(dom_id(@product, 'reviews_list')) do %&gt;\n  &lt;%= render(partial: 'products\/reviews\/card', locals: { review: @review }) %&gt;\n&lt;% end %&gt;<\/code><\/pre>\n\n\n\n<p>The most interesting file here is <code>_add_review.turbo_stream.erb.<\/code> The format turbo_stream must be new for you if you\u2019ve not encountered Turbo Streams before. Turbo requires HTTP response to have content-type <code>text\/vnd.turbo-stream.html<\/code> so you should either pass <code>content_type<\/code>: &#8220;<code>text\/vnd.turbo-stream.html<\/code>&#8221; to the <code>render<\/code> method in the controller action or add .<code>turbo_stream.erb<\/code> extension for your view. I found the second option handier.<\/p>\n\n\n\n<p>The main actor in <code>_add_review.turbo_stream.erb<\/code> is <code>turbo_stream<\/code> helper. We use it to call the previously mentioned actions. To be more accurate &#8211; it generates xml tags that describe what DOM manipulations should be made.&nbsp;<\/p>\n\n\n\n<p>This file does three things:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Updates the total number of reviews &#8211; updates the content of the tag with id <code>dom_id(@product, 'reviews_count')<\/code><\/li><li>Resets review form &#8211; replace the whole tag with id <code>dom_id(@product, 'reviews_form')<\/code><\/li><li>Display a new review on the page &#8211; add content in the beginning of the tag with id <code>dom_id(@product, 'reviews_list')<\/code><\/li><\/ul>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1214\" height=\"801\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/6aaba1054de904c6e2ad5accbb1c7f12.gif\" alt=\"\" class=\"wp-image-14570\"\/><\/figure><\/div>\n\n\n\n<p>So this is all you need to build a truly interactive web application. Without a line of JS code. And it will be enough for the majority of the applications.&nbsp;<\/p>\n\n\n\n<p>Turbo Streams also gives us a chance to deliver page changes over WebSocket. It won\u2019t require many actions from our side so let\u2019s update our reviews in all opened browsers when a new review is added.<\/p>\n\n\n\n<p>Before we start you should add gem \u201cturbo-rails\u201d to your Gemfile and run this command:<\/p>\n\n\n\n<p><code>bundle exec rails turbo:install<\/code><\/p>\n\n\n\n<p>It will install <code>@hotwired\/turbo-rails<\/code> and replace the Action Cable adapter from async (the default one) to redis.<\/p>\n\n\n\n<p>Now we are ready for some real-time.<\/p>\n\n\n\n<p>The first thing we need to do is to subscribe to the product\u2019s updates. That is super easy thanks to the <code>turbo_stream_from<\/code> helper&nbsp;<\/p>\n\n\n\n<p><strong>app\/views\/products\/show.html.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div class=\"product\"&gt;\n+  &lt;%= turbo_stream_from @product %&gt;\n\n  &lt;ul class=\"nav nav-tabs\" id=\"product-tab\" role=\"tablist\"&gt;<\/code><\/pre>\n\n\n\n<p>Now instead of returning <code>turbo-frame<\/code> tags, which tell what action should be performed on the user&#8217;s UI, we will broadcast those actions to all subscribers (all open product\u2019s pages.)<\/p>\n\n\n\n<p><strong>app\/controllers\/products_controller.rb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def add_review\n  @product = Product.find(params&#91;:id])\n  @review = @product.add_review(author: 'You', content: params&#91;:review])\n\n+  Turbo::StreamsChannel.broadcast_update_to(\n+    @product,\n+    target: ActionView::RecordIdentifier.dom_id(@product, 'reviews_count'),\n+    partial: 'products\/reviews\/count_badge',\n+    locals: { count: @product.reviews.count }\n+  )\n+\n+  Turbo::StreamsChannel.broadcast_prepend_to(\n+    @product,\n+    target: ActionView::RecordIdentifier.dom_id(@product, 'reviews_list'),\n+    partial: 'products\/reviews\/card',\n+    locals: { review: @review }\n+  )\nend<\/code><\/pre>\n\n\n\n<p>And to avoid some actions being performed twice we have to remove them from the HTTP response.<\/p>\n\n\n\n<p><strong>app\/views\/products\/add_review.turbo_stream.erb<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>- &lt;%= turbo_stream.update(dom_id(@product, 'reviews_count')) do %&gt;\n-   &lt;%= render(partial: 'products\/reviews\/count_badge', locals: { count: @product.reviews.count }) %&gt;\n- &lt;% end %&gt;\n\n&lt;%= turbo_stream.replace(dom_id(@product, 'reviews_form')) do %&gt;\n  &lt;%= render(partial: 'products\/reviews\/form', locals: { product: @product }) %&gt;\n&lt;% end %&gt;\n\n- &lt;%= turbo_stream.prepend(dom_id(@product, 'reviews_list')) do %&gt;\n-   &lt;%= render(partial: 'products\/reviews\/card', locals: { review: @review }) %&gt;\n- &lt;% end %&gt;<\/code><\/pre>\n\n\n\n<p>Form update will still be there since the form should be cleared after the submission and we don\u2019t want to clear the form for all users.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1792\" height=\"1012\" src=\"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/ecab4919bbe7bf7a20cceebd799ac217.gif\" alt=\"\" class=\"wp-image-14571\"\/><\/figure><\/div>\n\n\n\n<p>That is all you need to do when you want to add some real-time communication in your Rails app. Rails\u2019 magic in all its beauty.<\/p>\n\n\n\n<p>The Rails team did a great job to minimize interaction with all that big and scary JS world while leaving the framework a great tool for building modern web applications. Of course, the real world may (and most probably will) require more than Turbo can give to you. And the Rails team has brought <a href=\"https:\/\/stimulus.hotwired.dev\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Stimulus<\/a> and <a href=\"https:\/\/github.com\/rails\/request.js\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">request.js<\/a> to make your life easier when you have to write JS code within this Rails application. But this is a topic for a separate article.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Additional Ruby on Rails resources<\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/railsware.com\/blog\/ruby-on-rails-guide\/\" target=\"_blank\" rel=\"noreferrer noopener\" title=\"Everything You Need to Know about Ruby on Rails Web Application Framework\">Ruby on Rails Guide<\/a><\/li><li><a href=\"https:\/\/guides.rubyonrails.org\/getting_started.html\" target=\"_blank\" rel=\"noopener noreferrer\">Ruby on Rails official documentation<\/a><\/li><li><a href=\"https:\/\/www.freecodecamp.org\/news\/tag\/ruby-on-rails\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">Collection of posts explaining RoR concepts from freecodecamp<\/a><\/li><li><a href=\"https:\/\/railsware.com\/blog\/top-online-courses-and-tutorials-to-learn-ruby-on-rails-web-development\/\" target=\"_blank\" rel=\"noopener noreferrer\">Best Ruby on Rails courses and tutorials<\/a><\/li><li><a href=\"https:\/\/www.udemy.com\/topic\/ruby-on-rails\/\" target=\"_blank\" rel=\"noreferrer noopener nofollow\">RoR courses on Udemy<\/a><\/li><li><a href=\"https:\/\/railsware.com\/blog\/best-books-to-learn-ruby-on-rails\/\" target=\"_blank\" rel=\"noopener noreferrer\">A selection of best books to learn Ruby on Rails<\/a><\/li><li><a href=\"https:\/\/railsware.com\/blog\/how-to-hire-good-ruby-on-rails-developer\/\" target=\"_blank\" rel=\"noopener noreferrer\">How to find Ruby on Rails developers<\/a><\/li><li><a href=\"https:\/\/railsware.com\/services\/ruby-on-rails-web-development-services\/\" target=\"_blank\" rel=\"noopener noreferrer\">Ruby on Rails development services<\/a><\/li><\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A simple tutorial on how to build feature-rich user interfaces using mostly Ruby on Rails. <\/p>\n","protected":false},"author":90,"featured_media":14594,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[3],"tags":[],"coauthors":["Dmitriy Shcherbakan"],"class_list":["post-14555","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"acf":[],"aioseo_notices":[],"categories_data":[{"name":"Engineering","link":"https:\/\/railsware.com\/blog?category=development"}],"post_thumbnails":"https:\/\/railsware.com\/blog\/wp-content\/uploads\/2021\/12\/almost-jsless-frontend-on-rails-1024x519.jpg","amp_enabled":true,"_links":{"self":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/14555","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/users\/90"}],"replies":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/comments?post=14555"}],"version-history":[{"count":22,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/14555\/revisions"}],"predecessor-version":[{"id":14734,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/posts\/14555\/revisions\/14734"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media\/14594"}],"wp:attachment":[{"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/media?parent=14555"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/categories?post=14555"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/tags?post=14555"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/railsware.com\/blog\/wp-json\/wp\/v2\/coauthors?post=14555"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}