Skip to content

arkency/mmm-food

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Mmm Food

Rails 8 app with(out) Hotwire and TailwindCSS, used for the From Static to Reactive workshop.

SUPER IMPORTANT

Install all the dependencies before the workshop - it'll save you a lot of time! (./bin/setup)

Highly recommend adding hotwire-dev-tools to your browser.

Development

./bin/dev

Brief

During the workshop we'll transform a static food ordering Rails app into a reactive one using all parts of Hotwire. Make sure (again!) to check out this repo before the workshop starts and install all the dependencies.

Quests

00 Check the network tab

Open the network tab in your browser's dev tools and check the requests being made when you hover over various elements on the page.

01 Fix the broken link

Go the /products page.

The trash icon is not working correctly. Check it out yourself:

  • Add a dish to your order
  • Hover over the trash icon
  • Refresh the page

It shouldn't clear the entire cart when hovered over. Fix it so that it only clears it when clicked.

Take a look at the app/views/products/_sidebar.html.erb. Specifically at the link to orders_clear_path.

πŸ’‘ Solution

Add the data-turbo="false" to the link to keep the existing behaviour.

git checkout ex1_done

02 Let's use first frame

Once again, at the /products page, notice the heart icon next to the dish name. You can click it to make it a favourite. Currently it reloads the page. It's not good UX.

Let's improve the UX by introducing turbo frame to the like button. You'll find the like button at app/views/products/_product.html.erb.

Tip --> <%= turbo_frame_tag dom_id(product) do %>

πŸ’‘ Hint Wrap the content of `app/views/products/_product.html.erb` in a turbo frame. Click the like button. No reload!

But... Check the Order button now.

πŸ’‘ Hint Add `data-turbo-frame="_top"` to the `a` element redirecting to `add_to_order_product_path`.

03 Confirmation on deleting a dish from the order

Add functionality to confirm the deletion of a dish from the order.

Go to the app/views/products/_sidebar.html.erb file. Look for the button_to helper with remove_item_order_path.

Tip --> data-turbo-confirm

πŸ’‘ Solution Add `data: { turbo_confirm = "your_confirmation_prompt" }` to the `button_to` helper.

04 Don't reload the page when removing dish from the order

Now, use the turbo frame to stop the page from reloading when clearing the order. Go to the app/views/products/_sidebar.html.erb file.

πŸ’‘ Hint Wrap the content of sidebar in a turbo frame. But be careful! Sidebar is sticky and it should still be after the change :)

Now it works great. But... the scroll position is not preserved. Let's fix that by enabling morphing.

Go to the app/views/layouts/application.html.erb file. Uncomment the link with <%= turbo_refreshes_with method: :morph, scroll: :preserve %>

πŸ’‘ Solution `git checkout ex4_sidebar`

05 Don't reload the page when clearing the order

When you clear the whole order, the page still reloads. How to fix that?

πŸ’‘ Hint In the first exercise we added `data-turbo="false"` to the link that clears the order. We should change it to `data-turbo-prefetch="false"` instead.
πŸ’‘ Solution `git checkout ex5_clear_order_works`

06 Break out of turbo frame

Click the Go to checkout button. What happens?

πŸ’‘ Solution Add `data-turbo-frame="_top"` to the `a` element redirecting to `checkout_path`. `git checkout ex6_done`

07 Paginate Orders table without reload!

Currently when restaurant workers want to go through order history, they have to reload the page to see the next page of orders. Let's improve that.

Go to the app/views/restaurant/index.html.erb file. Use <%= turbo_frame_tag ... to wrap the orders table.

πŸ’‘ Hint You wrapped the table but it didn't work? Try extending the turbo frame to include the pagination links as well.

Alternatively you can add data-turbo-frame to the pagination links.

Solution at git checkout ex7_done

08 Improve user experience of dashboard for kitchen

Currently when moving orders through preparation process the page is reloaded. Our goal is to improve the user experience by using turbo frames.

Go to the app/views/restaurant/index.html.erb file. Use <%= turbo_frame_tag ... to wrap the kitchen kanban-like view.

πŸ’‘ Hint Solution at `git checkout ex8_done`

09 Inline edit order address at restaurant page

Before our last change it was possible to edit an order address in separate page. We broke that by introducing the turbo frame tag. No worries. We'll fix that and improve UX by introducing inline editing of order address.

Step 1 Go to the `app/views/restaurant/_order.html.erb` file. Wrap it in a turbo frame tag.

git checkout ex9_step_1

Step 2 Go to the `app/views/restaurant/edit_order.html.erb` file. Wrap it in a turbo frame tag.

git checkout ex9_step_2

Step 3 Go to the `app/controllers/restaurant_controller.rb` file. Instead of redirecting to `show_order_path`, redirect to `restaurant_index_path`.

git checkout ex9_step_3

Step 4 But what happened to moving orders between states in the kanban view? It stopped working!

Why?

Frame is the answer.

git checkout ex9_step_4

10 Refresh number of items in cart icon after removing dish from order

Currently when removing a dish from the order, the number of items in the cart icon is not updated. Let's fix that.

We have to do 3 steps:

  1. Add unique dom id to total quantity number that is displayed on the top of the cart icon (app/views/layouts/application.html.erb)
  2. In the app/controllers/orders_controller.rb change redirect_to to respond_to with format.turbo_stream
  3. Last but not least, we have to fix the bug with order items not being removed from the order itself. To do that, we have to: 3.1. Add unique dom id to the order item in the app/views/products/_sidebar.html.erb file. 3.2. Add the turbo_stream.remove(@order_item) instruction to the respond_to in controller
Step 1 ```erb ... ```

git checkout ex10_step_1

Step 2 ```ruby respond_to do |format| format.html { redirect_to products_path } format.turbo_stream do render turbo_stream: [ turbo_stream.update("total-quantity", @order.total_quantity), ] end end ```

git checkout ex10_step_2

Step 3 Into the `respond_to` block add: `turbo_stream.remove(@order_item)`

git checkout ex10_step_3

11 Convert columns with streams

Currently when an order is moved in the kitchen dashboard from one state to another, the full page is rendered.

Step 1 In the `app/views/restaurant/index.html.erb` file add ids to all three columns. Use: * new-orders * preparing-orders * done-orders

Those have to be the same as in the OrderHelper#status_to_kitchen_kanban_id

git checkout ex11_step_1

Step 2 Prepare `advance_order.turbo_stream.erb` file in the `app/views/restaurant` folder. Our goal is to remove the order from the old column and add it to the new one.
<%= turbo_stream.prepend(status_to_kitchen_kanban_id(@order.status)) do %>
<%= render 'restaurant/order', order: @order %>
<% end %>
<%= turbo_stream.remove(@order) %>
 

git checkout ex11_step_2

Step 3 Adjust the controller:
  def advance_order
    @order = Order.find(params[:id])
    @order.advance_order!

    respond_to do |format|
      format.html { redirect_to restaurant_index_path }
      format.turbo_stream
      end
  end

git checkout ex11_step_3

Step 4 It works well but the UI is kind of clunky after the change. There are multiple ways to fix that. We suggest moving the `
  • ` element into the turbo frame tag of order.

    git checkout ex11_step_4

  • Step 5 Fix the error that occurs in browsers dev console when moving the order from the last column.
    <% if status_to_kitchen_kanban_id(@order.status) %>
      <%= turbo_stream.prepend(status_to_kitchen_kanban_id(@order.status)) do %>
        <%= render 'restaurant/order', order: @order %>
      <% end %>
    <% end %>

    git checkout ex11_step_5

    12 Live update order status

    Our goal is to refresh the status page of an order (app/views/orders/show.html.erb) without reloading the page for hungry user waiting for their food.

    Step 1 In the `app/views/orders/_status.html.erb` file add a turbo frame tag (or div with an id) around the order progress partial.

    In order to receive streams at specific view we have to subscribe to the channel.

    <%= turbo_stream_from order-progress %>

    git checkout ex12_step1

    Step 2 Now our goal is to broadcast the streams when the order status is changed.

    We have to do that in two places.

    In both app/jobs/simulate_order_process_job.rb and app/controllers/orders_controller.rb files add following code:

        Turbo::StreamsChannel.broadcast_update_to(
          "order-progress",
          target: "order-progress-#{order_id}",
          partial: "orders/progress",
          locals: { status: status }
        )

    Adjust the code in the controller, it doesn't use status.

    git checkout ex12_step2

    13 Live updates for restaurant dashboard

    Our goal is to refresh the restaurant dashboard without reloading the page.

    At the app/views/restaurant/index.html we want to refresh the total daily income, chart and add new orders to the kanban board.

    Let's start with the chart. How to refresh the chart?

    We need to introduce one modification at the index page.

    Step 1 Extract the chart code to a partial.

    git checkout ex13_step_1

    After that we need to subscribe to changes.

    Step 2 `<%= turbo_stream_from "restaurant" %>`

    git checkout ex13_step_2

    Let's prepare the total income. How to refresh it?

    Step 3 `git checkout ex13_step_3`

    Now finally let's broadcast changes. Use SimulateOrderProcessJob to do that. To calculate daily_income use following code:

    if status == "paid"
          daily_income = Order
            .includes(:order_items)
            .includes(:products)
            .billed
            .where("orders.created_at >= ?", 6.days.ago.beginning_of_day)
            .group_by_day("orders.created_at")
            .sum("order_items.quantity * products.price")
    
          # perhaps something else will go here?
    end

    Now it is time to broadcast that value and update the chart as well!

    Step 4 ```ruby Turbo::StreamsChannel.broadcast_replace_to("restaurant", target: "chart", partial: "restaurant/chart", locals: { daily_income: }) Turbo::StreamsChannel.broadcast_update_to("restaurant", target: "total-income-today", content: daily_income.to_a.last.last) ```

    git checkout ex13_step_4

    The final step is to prepend the order to the kanban view "new-orders" using broadcasts.

    Step 5

    git checkout ex13_step_5

    14 Autosubmit search form

    We have a search form in the products index. It doesn't submit when we type, and it's not very reactive - let's fix that.

    Step 1 Wrap results in a turbo frame. `app/views/products/index.html.erb`

    Check how it works now.

    git checkout ex14_step1

    Step 2 Add `data: { turbo_frame: "results" }` to the search form `app/views/products/_search.html.erb`

    git checkout ex14_step2

    Step 3 Create new stimulus controller `rails g stimulus search`. Next add it to the search form `app/views/products/_search.html.erb` `data-controller="search"` Next add console.log to the controller `app/javascript/controllers/search_controller.js` `connect()` so we can see when it's connected

    git checkout ex14_step3

    Step 4 Let's add a new method to the controller `app/javascript/controllers/search_controller.js` `submit()`, and import debounce from `debounce` npm package.

    In the connect method add this.submit = debounce(this.submit, 300) which will debounce the submit method by 300ms.

    submit() should call this.element.requestSubmit()

    finally, add data: { action: "input->search#submit" } to the input field app/views/products/_search.html.erb

    git checkout ex14_step4

    Step 5

    Let's handle the url change. Notice the url is not updating when we type.

    Add data: { turbo_action: "advance" } to the form app/views/products/_search.html.erb

    git checkout ex14_step5

    15 Add a modal

    Time to fight the biggest enemy of all: the modal.

    Step 1 Add a `modal` turbo frame to the `app/layouts/application.html.erb` file.

    git checkout ex15_step1

    Step 2 Let's display the `add_to_order_product_path` in the new modal frame by adding a `data-turbo-frame="modal"` attribute to the link. Also let's wrap our add to order form in a turbo frame in `app/views/products/add_to_order.html.erb` file.

    git checkout ex15_step2

    Step 3 We've prepared a modal viewcomponent and stimulus controller, but it's not connected to the modal frame yet.

    Let's have a look inside the viewcomponent first and then use it inside app/views/products/add_to_order.html.erb file by replacing the existing modal frame with <%= render TurboModalComponent.new(title: "Add to order") do %>

    git checkout ex15_step3

    Step 4 Fix the close button by adding `this.element.close()` to the stimulus controller.

    git checkout ex15_step4

    Step 5 Nice! Now we have a working modal! But there's one more thing to do. The numbers of items & the order itself are not updating.

    Let's fix that by adding a turbo_stream to the orders controller.

        format.html { redirect_to products_path }
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.update("total-quantity", @order.total_quantity),
            turbo_stream.update("modal", ""),
            turbo_stream.update("sidebar-container", partial: "products/sidebar", locals: { order: @order })
          ]
        end
      end
    

    One more thing is to wrap the sidebar in another element with an id sidebar-container so we can reach it with the stream. This way we can update it without reloading the page. git checkout ex15_step5

    Final words

    We hope you enjoyed the workshop! There are many more things to explore in Hotwire within this app so feel free to experiment!

    If you have any questions, please feel free to ask.

    Cheers!

    Maciej and Łukasz from Arkency