beer.name
from the method to_s
of the rating object.
>
-> Delete the orphan ratings by hand from the console. Try to think first of a command/some commands, which can help you to make a list of the orphan ratings. If you can't think of it yourself, you can find a ready-made answer for the exercise above in this page.
+> Delete the orphan ratings by hand from the console. Try to think first of a command/some commands, which can help you to make a list of the orphan ratings. If you can't think of it yourself, you can find a ready-made answer for the exercise below in this page.
The ratings which belong to a beer can be deleted easily automatically. Alongside the beer model code has_many :ratings
, you should mark that ratings are dependent on beers, and that they should be destroyed if beers are destroyed:
@@ -1189,7 +1189,7 @@ The orphan issue is solved now.
>
>If you can't yet access individual breweries from the all breweries page, fix it now!
-## Inderect object connection
+## Indirect object connection
Your application is created in a way so that ratings belong to beers and that beers belong to breweries. This means that a set of ratings belong to each brewery, indirectly. Rails provides you with a simple way to go from the breweries to the ratings directly:
@@ -1227,7 +1227,7 @@ You will see, that beer and brewery both a method called average_ratingaverage_rating
that also work identically. It is not acceptable to leave our code this way.
> ## Exercise 15
>
-> Ruby provides you with a way to share methods between two classes with the help of modules, see https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/web/rubyn_perusteita.md#moduuli
+> Ruby provides you with a way to share methods between two classes with the help of modules, see https://ruby-doc.com/docs/ProgrammingRuby/html/tut_modules.html
>
> Modules have different uses – forming namespaces, for instance. However, now we are interested in the _mixin_ inheritance which can be implemented with modules.
>
@@ -1242,7 +1242,7 @@ We notice that beer and brewery both have an identically named method aver
> end
> ```
>
-> - Attention: if the name of your module is RatingAverage
, exactly like in the example, because of Ruby naming conventions it has to be placed in the file app/models/concerns/rating_average.rb
. In fact, even though classes names are CamelCase and start with capital letters, their files names follow the snake_case.rb style.
+> - Attention: if the name of your module is RatingAverage
, exactly like in the example, because of Ruby naming conventions it has to be placed in the file app/models/concerns/rating_average.rb
. In fact, even though classes names are PascalCase and start with capital letters, their files names follow the snake_case.rb style.
After you have done the exercise, the class Brewery should look more or less like below (assuming your module is called RatingAverage):
@@ -1390,7 +1390,7 @@ http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Digest.ht
>```
>and try what values variables _admin_accounts_, _username_ and _password_ contain ja form the right command.
>
-> HINT 2: The code block should be evaluated either as true or untrue depending on whether the password is correct. The value doesn't however necessarily have to be either _true_ or _false_ because Ruby interprets also other values as either true (truthy) or untrue (falsy). For example _nil_ is interpreted as untrue/falsy. See more eg. at https://learn.co/lessons/truthiness-in-ruby-readme.
+> HINT 2: The code block should be evaluated either as true or untrue depending on whether the password is correct. The value doesn't however necessarily have to be either _true_ or _false_ because Ruby interprets also other values as either true (truthy) or untrue (falsy). For example _nil_ is interpreted as untrue/falsy. See more at [Truth value - Wikipedia](https://en.wikipedia.org/wiki/Truth_value#Computing), [class TrueClass](https://docs.ruby-lang.org/en/master/TrueClass.html) and [class FalseClass](https://docs.ruby-lang.org/en/master/FalseClass.html) .
## Application to Internet
@@ -1511,7 +1511,7 @@ Because it is a program in production, resetting the database (rails db:dr
Commit all your changes and push the code to Github. Deploy to the newest version to Heroku or Fly.io, as well.
-Mark the exercises you have done at https://studies.cs.helsinki.fi/stats/courses/rails2023.
+Mark the exercises you have done at https://studies.cs.helsinki.fi/stats/courses/rails2023/submissions.
And let's continue coding: [week 3](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week3.md).
diff --git a/english/week3.md b/english/week3.md
index b1fee35..92905b0 100644
--- a/english/week3.md
+++ b/english/week3.md
@@ -332,7 +332,11 @@ The styling rules monitored by Rubocop are defined in _.rubocop.yml_ that is pla
The rules defined there are based on the [Relaxed Ruby](https://relaxed.ruby.style/) style, but they are a bit stricter on some points. The file contents also define that some files are to be left out of any style checks.
-A code style check is executed with the command _rubocop_ on the command line.
+A code style check is executed with the command
+```
+rubocop
+```
+on the command line.
There are quite a few problems in the code, for example:
@@ -480,7 +484,7 @@ Create a controller for sessions (in the file app/controllers/sessions_controlle
```ruby
class SessionsController < ApplicationController
def new
- # render the signing up page
+ # render the signing up page
end
def create
@@ -988,7 +992,7 @@ http://guides.rubyonrails.org/active_record_validations.html and https://apidock
>
> Add the following validations to your program
> * beer and brewery names are not empty
-> * the brewery founding year is an integer between 1040-2022
+> * the brewery founding year is an integer between 1040-2023
> * the length of the username attribute of the User class is 3 – 30 characters
If you try to create a beer with an empty name, you get an error message
diff --git a/english/week4.md b/english/week4.md
index f125b6f..986fa7b 100644
--- a/english/week4.md
+++ b/english/week4.md
@@ -227,7 +227,7 @@ You can initialize rspec in your application running the following from command
The initialization creates a folder /spec in the application and the various tests – or specs – will be placed in its subfolders.
-According to Rails standard but currently less common testing framework, the test are place in the folder /test. The folder will be useless after taking rspec, and you can delete it.
+According to Rails standard but currently less common testing framework, the tests are placed in the folder /test. The folder will be useless after taking up rspec as the only testing tool, and you can delete it.
The tests – the correct words would be specs or specifications when it comes to rspec, we will be using the word test in the future however – can be written at different levels: unit tests for models and controllers, view tests, and integration tests for controllers. In addition to these, the application can be tested using a simulated browser with the help of the capybara gem https://github.com/jnicklas/capybara.
@@ -663,7 +663,7 @@ FactoryBot.create(:user)
FactoryBot.create(:user, username: 'Vilma')
```
-More instructions for using FactoryBot at https://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md
+More instructions for using FactoryBot at https://thoughtbot.github.io/factory_bot/
## Users favourite beers, breweries, and styles
@@ -744,7 +744,7 @@ end
Your test will not succeed, because your method does not do anything so far, and its return value is always nil
.
-Use [in the spirit of TDD](https://stanislaw.github.io/2016/01/25/notes-on-test-driven-development-by-example-by-kent-beck.html) a "fake solution", without trying to make the final working version yet:
+Use [in the spirit of TDD](https://stanislaw.github.io/2016-01-25-notes-on-test-driven-development-by-example-by-kent-beck.html) a "fake solution", without trying to make the final working version yet:
```ruby
class User < ApplicationRecord
@@ -758,7 +758,15 @@ class User < ApplicationRecord
end
```
-Make another test which will force you to make a real implementation [(see triangulation)](https://stanislaw.github.io/2016/01/25/notes-on-test-driven-development-by-example-by-kent-beck.html):
+Make another test which will force you to make a real implementation [(see triangulation)](https://stanislaw.github.io/2016-01-25-notes-on-test-driven-development-by-example-by-kent-beck.html#triangulation):
+
+> How do you most conservatively drive abstraction with tests? Abstract only when you have two or more examples. (p.153)
+>
+> If two receiving stations at a known distance from teach other can both measure the direction of a radio signal, then there is enough information to calculate the range and bearing of the signal. This calculation is called Triangulation.
+>
+> By analogy when we triangulate, we only generalize code when we have two examples or more... When the second example demands a more general solution, then and only then do we generalize (p.16).
+>
+> I only use Triangulation when I'm really, really unsure about the correct abstraction for the calculation. Otherwise I rely on either Obvious Implementation or Fake It. (p.154)
```ruby
it "is the one with highest rating if several rated" do
@@ -817,6 +825,7 @@ If you look at the documentation (http://guides.rubyonrails.org/active_record_qu
```ruby
def favorite_beer
return nil if ratings.empty?
+
ratings.order(score: :desc).limit(1).first.beer
end
```
@@ -1126,7 +1135,7 @@ If/when you ran into problems:
>
>Add information about the user's favourite style to their page.
>
-> Do not do everything with one method (unless you solve the problem at database level with ActiveRecord or another elegantly compact solution), instead, define the suitable auxiliary methods! If you notice that you method is more than 6 lines long, you are doing either too much or something too complicated, so refactor your code. Ruby's collections have various auxiliary methods which might be useful for the exercise, see http://http://ruby-doc.org/core-2.5.1/Enumerable.html
+> Do not do everything with one method (unless you solve the problem at database level with ActiveRecord or another elegantly compact solution), instead, define the suitable auxiliary methods! If you notice that you method is more than 6 lines long, you are doing either too much or something too complicated, so refactor your code. Ruby's collections have various auxiliary methods which might be useful for the exercise, see https://docs.ruby-lang.org/en/3.2/Enumerable.html
> ## Exercise 4
>
@@ -1360,7 +1369,7 @@ The test expects that clicking the _Create user_ button will cause the number of
You will have to take into consideration a small detail, that is, the method expect
can be given parameters in two ways.
If the method has to test a value, the value is given between brackets, like expect(current_path).to eq(signin_path)
. Instead, if it tests the impact of an operation (like the one above, click_button('Create User')
) on the value of an application object (User.count
), the operation to execute is given to expect
in a code chunk.
-Read more about this in Rspec documentation https://relishapp.com/rspec/rspec-expectations/docs/built-in-matchers
+Read more about this in Rspec documentation https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
So the last test checked whether the operation executed at browser level created an object in the database. Should you make a separate test to see whether a username can sign in the system? Maybe. After all, the previous test did not questioned whether the user object was saved in the database correctly.
@@ -1403,7 +1412,7 @@ end
The test builds its brewery, two beers and a user with the method let!
instead of let
which we used earlier. In fact, the version without exclamation mark does not execute the operation immediately, but only once the code refers to the object explicitly. The object beer1
is mentioned only at the end of the code, so if you had created it with the method let
, you would have run into a problem creating the rating, because its beer would have not existed in the database yet, and the corresponding select element would not have been found.
-The code contained in the before
chunk of the test helps users to sign in the system. Most probably, the same code chunk will be useful in various different test files. You had better extract the test code needed in various different places and make a [module](https://relishapp.com/rspec/rspec-core/docs/helper-methods/define-helper-methods-in-a-module), which can be included in all test files which need it. Create a module Helpers
in a file named _helpers.rb_ in the _specs_ directory and put the sign-in code there:
+The code contained in the before
chunk of the test helps users to sign in the system. Most probably, the same code chunk will be useful in various different test files. You had better extract the test code needed in various different places and make a [module](https://rspec.info/features/3-12/rspec-core/helper-methods/modules/), which can be included in all test files which need it. Create a module Helpers
in a file named _helpers.rb_ in the _specs_ directory and put the sign-in code there:
```ruby
module Helpers
@@ -1795,4 +1804,4 @@ Commit all your changes and push the code to Github. Deploy to the newest versio
Mark the exercises you have done at https://studies.cs.helsinki.fi/stats/courses/rails2023.
-And towards next week: [week 5](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week5.md)
\ No newline at end of file
+And towards next week: [week 5](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week5.md)
diff --git a/english/week5.md b/english/week5.md
index 622e546..02b6f15 100644
--- a/english/week5.md
+++ b/english/week5.md
@@ -5,9 +5,7 @@ You will continue to develop your application from the point you arrived at the
A great part of modern Internet services makes use of open interfaces which provides useful data to enrich applications functionality.
-Interfaces for beers are also available, try to search for beer at http://www.programmableweb.com/
-
-The best interface among the ones available seems to be Beermapping API (see http://www.programmableweb.com/api/beer-mapping ja http://beermapping.com/api/), which makes it possible to search for beer restaurants.
+The best interface among the ones available seems to be Beermapping API , which makes it possible to search for beer restaurants.
Applications which make use of beermaping API need a singular API key. You can retrieve a key at https://beermapping.com/api/, after logging in to the page (after logging in edit the url in your browser's address bar back to https://beermapping.com/api/). This is a common procedure in use for the larger part of modern free interfaces.
@@ -501,7 +499,7 @@ response = HTTParty.get "#{url}#{ERB::Util.url_encode(params[:city])}"
## Refactoring your Places controller
-Rails controllers should not include application logic. It is a best practice to put external APIs in their own class. A good place for such class is in the _lib_ folder. Place the following code into the file _lib/beermapping_api.rb_:
+Rails controllers should not include application logic. It is a best practice to put external APIs in their own class. A good place for such class is in the _services_ folder under the _app_ folder. Even though this folder is not automatically created, starting from Rails 5 the _services_ folder is autoloaded in boot sequence. Place the following code into the file _app/services/beermapping_api.rb_:
```ruby
class BeermappingApi
@@ -527,26 +525,7 @@ end
So the class defines a static method which returns a table of the beer restaurants which have been found in the towns defined by the parameter. If no restaurant is found, the table will be empty. The API class is not in its best format yet, because you cannot know completely what other methods you need.
-To ensure that code in the _lib_ folder will work (not only on your computer but also in Heroku and Fly.io), you must add these two lines into the _config/application.rb_ file:
-
-```ruby
-config.autoload_paths << Rails.root.join("lib")
-config.eager_load_paths << Rails.root.join("lib")
-```
-
-The addition should be placed inside _Application_ class definition
-
-```ruby
-module Ratebeer
- class Application < Rails::Application
- # ...
-
- # add here
- end
-end
-```
-
-Restart the application for the changes to take effect.
+If needed, restart the application.
The controller will be looking neat, by now:
@@ -665,7 +644,7 @@ mluukkai@melkki$ curl http://beermapping.com/webservice/loccity/731955affc547174
Now you could copy-paste the information returned in XML form by the HTTP reqest to your test. If you want to be sure you place the XML right in the string, you should use a quite particular syntax
see http://blog.jayfields.com/2006/12/ruby-multiline-strings-here-doc-or.html where the string is placed between <<-END_OF_STRING
and END_OF_STRING
.
-You find below the test code which should be placed into spec/lib/beermapping_api_spec.rb (deciding to place the code in the lib subfolder because the test destination is an auxiliary class in the lib folder):
+You find below the test code which should be placed into spec/services/beermapping_api_spec.rb (deciding to place the code in the services subfolder because the test destination is an auxiliary class in the services folder):
```ruby
require 'rails_helper'
diff --git a/english/week7.md b/english/week7.md
index be4517e..889e277 100644
--- a/english/week7.md
+++ b/english/week7.md
@@ -1661,9 +1661,9 @@ Write a small description of your strategy to speed up the page in the ind
## An application made of various services
-You can scale your application performance only till a certain point if your application is a monolithic entity using a single database and running on a single server. The application can be optimised so that it is scaled horizontally – that is, increasing the physical resources of its server.
+You can scale your application performance only till a certain point if your application is a monolithic entity using a single database and running on a single server. The application can be optimised so that it is scaled vertically – that is, increasing the physical resources of its server.
-You will have better scaling results if you scale vertically, meaning that instead of improving the physical resources of one server, you start to use various servers, all executing your application actions at the same time. Vertical scaling is not necessarily trivial, you'll have to change the application architecture. If your application works with only one database, you may run into troubles despite vertical scaling, as that one database becomes a bottleneck. Especially so if that is a relational database, that is not easy to distribute and scale vertically.
+You will have better scaling results if you scale horizontally, meaning that instead of improving the physical resources of one server, you start to use various servers, all executing your application actions at the same time. Horizontal scaling is not necessarily trivial, you'll have to change the application architecture. If your application works with only one database, you may run into troubles despite horizontal scaling, as that one database becomes a bottleneck. Especially so if that is a relational database, that is not easy to distribute and scale horizontally.
Scaling an application (and sometimes also updating and extending it) is easier if the application is made of various different services that work independently and communicate with each other for instance with an HTTP protocol. In fact, your application is already making use of another service, that is BeermappingAPI. In the same way, your application functionality could be expanded if new services were integrated.
@@ -1752,6 +1752,4 @@ If Rails seems interesting you could dig more into it in the following ways
- https://www.ruby-toolbox.com/ help for finding gems
- [Eloquent Ruby](http://www.amazon.com/Eloquent-Ruby-Addison-Wesley-Professional-Series/dp/0321584104) an excellent book on Ruby
-You can also dive into [week 8 beta](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week8.md), upcoming addition for the course, if you want to already learn about Hotwire, Rails way of creating reactive applications with minimal Javascript.
-
-
+You can also dive into [week 8](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week8.md), upcoming addition for the course, if you want to already learn about Hotwire, Rails way of creating reactive applications with minimal Javascript.
diff --git a/english/week8.md b/english/week8.md
index af60a1f..4e554de 100644
--- a/english/week8.md
+++ b/english/week8.md
@@ -1,10 +1,16 @@
-**Note: This part of the course is still in beta testing. You can already try the material and exercises, but they cannot be submitted yet and the exercises can change before they are released.**
+You will continue to develop your application from the point you arrived at the end of week 7. The material that follows comes with the assumption that you have done all the exercises of the previous week. In case you have not done all of them, you can take the sample answer of the previous week from the submission system.
-You will continue to develop your application from the point you arrived at the end of week 6. The material that follows comes with the assumption that you have done all the exercises of the previous week. In case you have not done all of them, you can take the sample answer to the previous week from the submission system.
+This part is graded separately from the "base course". The part has 17 exercises. You need to complete 16 of those to get the ECTS credit registered.
+
+This part is provided by four awesome developers from Kisko Labs: [Eetu Mattila](https://github.com/zHarrowed), [Teemu Palokangas](https://github.com/palokangas), [Teemu Tammela](https://github.com/teemutammela), and [Kimmo Salonen](https://github.com/KimmoSalonen). [Kisko Labs](https://www.kiskolabs.com/en/) is a consultancy firm based in Helsinki, which has successfully used Ruby on Rails in various customer products for more than a decade. Check out [this video](https://www.youtube.com/watch?v=qyWdcRQfqI4&t=1s) for more!
+
+## Prerequisites
+
+Two of the three topics covered in this part are using just Ruby. The last topic (Stimulus) uses JavaScript and also some browser DOM APIs. If you have no experience using JavaScript on the browser side, the last topic might be quite challenging.
## Hotwire
-Ruby on Rails version 7.x introduces a new functionality called [Hotwire](https://hotwired.dev/), aimed at simplifying the creation of dynamic views with minimal reliance on Javascript. Hotwire empowers Rails developers to incorporate partial reloading of user interface elements in a similar fashion to popular Javascript libraries like [React](https://react.dev/), all while leveraging the familiar syntax of the Ruby language.
+Ruby on Rails version 7.x introduces a new functionality called [Hotwire](https://hotwired.dev/), aimed at simplifying the creation of dynamic views with minimal reliance on JavaScript. Hotwire empowers Rails developers to incorporate partial reloading of user interface elements in a similar fashion to popular JavaScript libraries like [React](https://react.dev/), all while leveraging the familiar syntax of the Ruby language.
### Why Hotwire?
@@ -12,7 +18,7 @@ Throughout its history, the Rails framework has been renowned for its ability to
To meet these expectations, developers have had to rely on additional software tools, such as the React library, to build the necessary functionality. Unfortunately, this approach adds complexity to the applications and often diminishes the role of the View component within the Rails [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) architecture, reducing it to merely serving as a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) or [GraphQL](https://en.wikipedia.org/wiki/GraphQL) API.
-With the introduction of Hotwire, Rails aims to tackle the challenges posed by the rapidly evolving Javascript landscape. This is in contrast to the more steadily-paced Ruby ecosystem, which tends to favor incremental and conservative evolution. Hotwire offers a more streamlined and cohesive approach to fulfilling the requirements of full-featured, full-stack applications. It achieves this by eliminating the reliance on disparate tools and aligning with the ethos of Rails as a comprehensive platform for web application development. Hotwire provides the tools to construct dynamic and interactive user experiences while maintaining consistency with the familiar Rails paradigms.
+With the introduction of Hotwire, Rails aims to tackle the challenges posed by the rapidly evolving JavaScript landscape. This is in contrast to the more steadily-paced Ruby ecosystem, which tends to favor incremental and conservative evolution. Hotwire offers a more streamlined and cohesive approach to fulfilling the requirements of full-featured, full-stack applications. It achieves this by eliminating the reliance on disparate tools and aligning with the ethos of Rails as a comprehensive platform for web application development. Hotwire provides the tools to construct dynamic and interactive user experiences while maintaining consistency with the familiar Rails paradigms.
## Introduction to Hotwire Components
@@ -28,17 +34,285 @@ Hotwire encompasses three core components, each serving a specific purpose: Turb
2. **Stimulus**
-Stimulus is a lightweight Javascript framework that enhances interactivity and user interactions in server-rendered HTML views. By attaching JavaScript behavior to HTML elements, it improves the user experience without complex frameworks or extensive coding.
+Stimulus is a lightweight JavaScript framework that enhances interactivity and user interactions in server-rendered HTML views. By attaching JavaScript behavior to HTML elements, it improves the user experience without complex frameworks or extensive coding.
3. **Strada**
Strada is an extension of Hotwire that allows developers to build iOS and Android applications using Rails and Turbo. Currently, Strada is being developed as separate repositories: [turbo-ios](https://github.com/hotwired/turbo-ios) for iOS and [turbo-android](https://github.com/hotwired/turbo-android) for Android, respectively.
-## Pagination
+## Turbo Frames, getting ready
+
+Before we start, let us simplify our app a bit. Start by removing the mini profiler by deleting the following line from Gemfile
+
+```
+gem 'rack-mini-profiler'
+```
+
+and by running _bundle install_.
+
+Let us also remove all the code that is implementing the [server-side caching](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week7.md#server-caching-functionality). So from the view templates, we get rid of all the the _cache_ elements that wrap the real page content. Eg. in _views/beers/index.html.erb_ we should get rid of this element that wraps the real content:
+
+```html
+Beers
+
+<% cache "beerlist-#{@order}", skip_digest: true do %>
+
+
+ ...
+
+
+<% end %>
+```
+
+and from the corresponding controllers, the guards that prevent full page render should also be removed. Eg. in the _controllers/beers.rb_ the change is the following:
+
+```ruby
+ def index
+ @order = params[:order] || 'name'
+
+ # remove this line:
+ return if request.format.html? && fragment_exist?("beerlist-#{@order}")
+
+ @beers = Beer.all
+
+ @beers = case @order
+ when "name" then @beers.sort_by(&:name)
+ when "brewery" then @beers.sort_by { |b| b.brewery.name }
+ when "style" then @beers.sort_by { |b| b.style.name }
+ when "rating" then @beers.sort_by(&:average_rating).reverse
+ end
+ end
+```
+
+Now we are ready to begin!
+
+### The first steps
+
+Turbo Frames provide a convenient way to update specific parts of a page upon request, allowing us to focus on updating only the necessary content while keeping the rest of the page intact.
+
+Let us add a Turbo Frame that contains a link element to the bottom of the styles page, that is, to the view _views/styles/index.html.erb_:
+
+```html
+Styles
+
+
+ <% @styles.each do |style| %>
+
+ <%= link_to style.name, style %>
+
+ <% end %>
+
+
+<%= link_to "New style", new_style_path %>
+
+
+
+<%= turbo_frame_tag "about_style" do %>
+ <%= link_to "about", styles_path %>
+<% end %>
+```
+
+The Turbo Frame is created with a helper function turbo_frame_tag that has an identifier as a parameter. We use now the string _"about_style_"_ as the identifier.
+
+The generated HTML looks like the following:
+
+![image](../images/8-1.png)
+
+So the frame has just a link element that points back to the page itself. We intend to show some information about beer styles within the Turbo Frame when the user clicks the link.
-Before jumping into the Hotwire components in detail, let's take a slight detour. After last week's [increased amount of beers](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week7.md#server-caching-functionality) you start to wonder that it would be kinda nice to have a pagination for our beers page. Let's add it first without utilizing Hotwire features.
+Let us now create a partial */views/styles/_about.html.erb* that also has the same Turbo Frame ID:
-First we start by adding links for previous and next pages to the end of our beers table in `/beers/index.html.erb`:
+![image](../images/8-2.png)
+
+Now when the user clicks the link, that creates a GET request to the same URL and the request is handled by the function _index_ of the _beers_ controller. We can use the helper function *turbo_frame_request?* to detect the Turbo request and handle it accordingly:
+
+```rb
+class StylesController < ApplicationController
+
+ def index
+ if turbo_frame_request?
+ # this was a request from the Turbo Frame
+ render partial: 'about'
+ else
+ # this was a normal requesst
+ @styles = Style.all
+ end
+ end
+
+ // ...
+end
+```
+
+So in case of a turbo request (that is link "about" is clicked), instead of a full page reload only the partial about is rendered.
+
+From the console, we can also see, that the GET request caused by the link clicking within the frame has a special header _Turbo frame_ that tells the Rails controller to treat the request as a turbo request and **not** cause a full page reload:
+
+![image](../images/8-3.png)
+
+We are now using the index controller function both for rendering the whole styles page and for the partial that renders the about information. Let us separate the partial rendering to on own controller function. The *routes.rb* extends as follows
+
+```rb
+Rails.application.routes.draw do
+ resources :styles do
+ get 'about', on: :collection
+ end
+ # ...
+end
+```
+
+The controller cleans up a bit:
+
+```rb
+class StylesController < ApplicationController
+ before_action :set_style, only: %i[show edit update destroy]
+
+ def index
+ # now this takes only care of the full page reloads
+ @styles = Style.all
+ end
+
+ # own controller function for the partial
+ def about
+ render partial: 'about'
+ end
+ # ...
+end
+```
+
+The link is changed accordingly:
+
+```html
+<%= turbo_frame_tag "about_style" do %>
+ <%= link_to "about", about_styles_path %>
+<% end %>
+```
+
+#### Rendering style details on demand
+
+Instead of having just an individual page for each style, let us show the style details on the styles page when the user clicks a style name on the list. We start by wrapping the style list in a Turbo Frame:
+
+```html
+Styles
+
+
+ <%= turbo_frame_tag "styles" do %>
+ <% @styles.each do |style| %>
+ <%= link_to style.name, style %>
+ <% end %>
+ <% end %>
+
+```
+
+We add the following to partial *_details.html.erb* that shows besides the style name, its description and the beers of that style:
+
+```html
+<%= turbo_frame_tag "styles" do %>
+ <%= style.name %>
+
+ Description:
+ <%= style.description %>
+
+
+ beers
+
+
+ <% @style.beers.each do |beer| %>
+ -
+ <%= link_to beer.name, beer %>
+
+ <% end %>
+
+<% end %>
+```
+
+Clicking a style name now causes a Turbo Frame request for a single style, and the controller is altered to render the above partial in this case:
+
+```rb
+class StylesController < ApplicationController
+ # ...
+
+ def show
+ if turbo_frame_request?
+ render partial: 'details', locals: { style: @style }
+ end
+ # the default is a full page reload
+ end
+
+ # ...
+end
+```
+
+Notice now that the partial is given the _style_ as a variable!
+
+Now when a style name is clicked, the list of styles is **replaced** with the details of a particular style.
+
+#### Targetting a different frame
+
+This is perhaps not quite what we want. Instead, let the style list remain visible all the time, and add a new Turbo Frame (with ID "style_details") where the details of the clicked style are shown:
+
+```html
+
+ <%= turbo_frame_tag "styles" do %>
+ <% @styles.each do |style| %>
+ <%= link_to style.name, style, data: { turbo_frame: "style_details" } %>
+ <% end %>
+ <% end %>
+
+ <%= turbo_frame_tag "style_details" do %>
+ <% end %>
+
+
+<%= link_to "New style", new_style_path %>
+
+```
+
+Since we now want to target a _different_ Turbo Frame instead of the one where links reside, we must define the targeted frame as an attribute. As seen from the above snippet it is done as follows:
+
+```html
+link_to style.name, style, data: { turbo_frame: "style_details" }
+```
+
+The Turbo Frame tag in the partial _details.html.erb needs to be changed accordingly:
+
+```html
+<%= turbo_frame_tag "style_details" do %>
+ <%= style.name %>
+
+ Description:
+ <%= style.description %>
+
+
+ # ...
+<% end %>
+```
+
+The result is finally as we expected it to be:
+
+![image](../images/8-4.png)
+
+### A very important thing to remember
+
+It is **EXTREMELY IMPORTANT** to follow all the possible error messages, in the Rails console and the network tab of the browser especially when working with the Hotwire. For unknown reasons, some beginners do not believe this and end up in deep trouble. Do not even think taking that dark path...
+
+
+
+## Exercise 1
+
+Extend the user page so that when clicking a rating, the basic info of the rated beer is shown.
+
+Note: it is **EXTREMELY IMPORTANT** to follow all the possible error messages, in the Rails console and the network tab of the browser!
+
+
+Your solution could look like this:
+
+![image](../images/8-5.png)
+
+### Pagination
+
+Before continuing with the Hotwire further, let's take a slight detour. After last week's [increased amount of beers](https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/english/week7.md#server-caching-functionality), you start to wonder that it would be kind of nice to have a pagination for our beers page. Let's add it first without utilizing Hotwire features.
+First, we start by adding links for the previous and next pages to the end of our beer table:
+
+**app/views/beers/index.html.erb**
```html
@@ -61,77 +335,51 @@ First we start by adding links for previous and next pages to the end of our bee
```
-Our links don't do much yet so let's add some logic to the controller as well. Last week we defined ordering of the beers in our controller like so:
-
-```ruby
-def index
- @beers = Beer.includes(:brewery, :style, :ratings).all
-
- order = params[:order] || 'name'
-
- @beers = case order
- when 'name' then @beers.sort_by(&:name)
- when 'brewery' then @beers.sort_by{ |b| b.brewery.name }
- when 'style' then @beers.sort_by{ |b| b.style.name }
- when "rating" then @beers.sort_by(&:average_rating).reverse
- end
-end
-```
+Our links don't do much yet so let's add some logic to the controller as well. Last week we defined the ordering of the beers in our controller as so:
-Which contains a bit of a problem. Method `sort_by` will load all the beers to central memory as an array and only then sort the order. But now we would want to fetch only limited amount of records from the database at a time, only what is needed for the current page. There's no sense fetching all the beers. That's why we'll opt out for using ActiveRecord SQL queries instead for ordering:
+**app/controllers/beers_controller.rb**
```ruby
def index
- @beers = Beer.includes(:brewery, :style, :ratings).all
+ @order = params[:order] || 'name'
- order = params[:order] || 'name'
+ @beers = Beer.all
@beers = case @order
- when "name" then @beers.order(:name)
- when "brewery" then @beers.joins(:brewery).order("breweries.name")
- when "style" then @beers.joins(:style).order("styles.name")
- when "rating" then @beers.left_joins(:ratings)
- .select("beers.*, avg(ratings.score)")
- .group("beers.id")
- .order("avg(ratings.score) DESC")
- end
+ when "name" then @beers.sort_by(&:name)
+ when "brewery" then @beers.sort_by { |b| b.brewery.name }
+ when "style" then @beers.sort_by { |b| b.style.name }
+ when "rating" then @beers.sort_by(&:average_rating).reverse
+ end
end
```
-This will allow us to use ActiveRecord methods `limit` and `offset` to fetch only the wanted amount of records from the database at a time. We can set the wanted amount per page to top our `beers_controller.rb` via constant:
+This approach contains a bit of a problem. The code loads all the beers to the main memory as an array and only then uses `sort_by` to get the appropriate order. But now we would want to fetch only a limited amount of records from the database at a time, only what is needed for the current page. There's no sense fetching all the beers. That's why we'll opt for using ActiveRecord SQL queries for the ordering.
+
+Let us at first forget about the different orderings and get the pagination to work for beers ordered by name. The controller changes as follows:
```ruby
class BeersController < ApplicationController
PAGE_SIZE = 20
- # ...
-end
-```
-After that we can define our `index` method like so:
-
-```ruby
-def index
+ def index
@order = params[:order] || 'name'
@page = params[:page]&.to_i || 1
- @last_page = (Beer.count / PAGE_SIZE).ceil
- return if request.format.html? && fragment_exist?("beerlist-#{@order}")
+ @last_page = (Beer.count / PAGE_SIZE.to_f).ceil
+ offset = (@page - 1) * PAGE_SIZE
- @beers = Beer.includes(:brewery, :style, :ratings).all
+ @beers = Beer.order(:name).limit(PAGE_SIZE).offset(offset)
+ end
- @beers = case @order
- when "name" then @beers.order(:name)
- when "brewery" then @beers.joins(:brewery).order("breweries.name")
- when "style" then @beers.joins(:style).order("styles.name")
- when "rating" then @beers.left_joins(:ratings)
- .select("beers.*, avg(ratings.score)")
- .group("beers.id")
- .order("avg(ratings.score) DESC")
- end
- @beers = @beers.limit(PAGE_SIZE).offset((@page - 1) * PAGE_SIZE)
+ # ...
end
```
-Pay attention to the `@last_page` instance variable as we are going to need it in our view. Now we are going to add the new `@page` instance variable to all of our links in the index and redefine our previous and next page links.
+We are using a combination of ActiveRecord [order](https://edgeguides.rubyonrails.org/active_record_querying.html#ordering), [limit and offset](https://edgeguides.rubyonrails.org/active_record_querying.html#limit-and-offset) to control what page of the ordered beers is queried from the database.
+
+Pay attention to the `@last_page` instance variable as we are going to need it in our view. Now we are going to add the new `@page` instance variable to all of our links in the index and redefine our previous and next page links:
+
+**app/views/beers/index.html.erb**
```html
@@ -166,42 +414,114 @@ Pay attention to the `@last_page` instance variable as we are going to need it i
```
-Remember to also update our cache key to include our new `@page` variable:
+Pagination works now nicely with the default ordering! We need a bit more advanced use of ActiveRecord to get the other orders to work.
-```html
-<% cache "beerlist-#{@page}-#{@order}", skip_digest: true do %>
+When ordering based on a brewery name or style name, we can not just use the data in the beer object, we must do an SQL [join](https://edgeguides.rubyonrails.org/active_record_querying.html#joining-tables) to get the associated rows from the database and to do the ordering based on the fields of those. The controller extends as follows:
+
+```ruby
+class BeersController < ApplicationController
+ PAGE_SIZE = 20
+
+ def index
+ @order = params[:order] || 'name'
+ @page = params[:page]&.to_i || 1
+ @last_page = (Beer.count / PAGE_SIZE.to_f).ceil
+ offset = (@page - 1) * PAGE_SIZE
+
+ @beers = case @order
+ when "name" then Beer.order(:name)
+ .limit(PAGE_SIZE).offset(offset)
+ when "brewery" then Beer.joins(:brewery)
+ .order("breweries.name").limit(PAGE_SIZE).offset(offset)
+ when "style" then Beer.joins(:style)
+ .order("styles.name").limit(PAGE_SIZE).offset(offset)
+ end
+
+ end
+
+ # ...
+end
```
-![image](../images/ratebeer-w8-1.png)
+So now depending on the order the user wants, a different kind of query is executed to get a page of beers.
-And voilà! We have working pagination for our beers. But one thing that is kinda annoying is that when we navigate between the pages, the whole pages gets reloaded with menus and all even though the contents of the table are the only thing changing. Here is where we come to where Turbo Frames can help us...
+The last one, ordering by ratings is the most tricky case. One way to achieve the functionality is shown below. The required SQL mastery is beyond the objectives of this course, so you may just copy-paste the code and believe that it works.
-## Turbo Frames
+```ruby
+class BeersController < ApplicationController
+ PAGE_SIZE = 20
-Turbo Frames provide a convenient way to update specific parts of a page upon request, allowing us to focus on updating only the necessary content while keeping the rest of the page intact.
+ def index
+ @order = params[:order] || 'name'
+ @page = params[:page]&.to_i || 1
+ @last_page = (Beer.count / PAGE_SIZE.to_f).ceil
+ offset = (@page - 1) * PAGE_SIZE
-To begin, let's create a new partial called `_beers_page.html.erb` to the folder `app/views/beers` and extract the table containing the beers from our `beers/index.html.erb` file. This way, our `index.html.erb` will appear as follows:
+ @beers = case @order
+ when "name" then Beer.order(:name)
+ .limit(PAGE_SIZE).offset(offset)
+ when "brewery" then Beer.joins(:brewery)
+ .order("breweries.name").limit(PAGE_SIZE).offset(offset)
+ when "style" then Beer.joins(:style)
+ .order("styles.name").limit(PAGE_SIZE).offset(offset)
+ when "rating" then Beer.left_joins(:ratings)
+ .select("beers.*, avg(ratings.score)")
+ .group("beers.id")
+ .order("avg(ratings.score) DESC").limit(PAGE_SIZE).offset(offset)
+ end
+
+ end
+
+ # ...
+end
+```
+
+And voilà! We have a working pagination for our beers. But one kinda annoying thing is that when we navigate between the pages, the whole page gets reloaded with menus and all even though the contents of the table are the only thing changing. Here is where we come to where Turbo Frames can help us...
+
+The controller code has now a slightly ugly feature, it contains repetition, eg. the following piece of code is repeated many times:
+
+```rb
+.limit(PAGE_SIZE).offset(offset)
+```
+
+It would be possible to clean up the repetition, but we will leave that as a volunteer exercise.
+
+
+
+## Exercise 2
+
+Change the ratings page to show *all* ratings in a paginated form. The default order is to show the most recent rating first. Add a button that allows reversing the order.
+
+
+
+Your solution could look like the following:
+
+![image](../images/8-6.png)
+
+### Turbo framing the beer list
+
+To begin, let's create a new partial called `_beer_list.html.erb` to the folder `app/views/beers` and extract the table containing the beers from our `beers/index.html.erb` file.
+
+This way, our `index.html.erb` will appear as follows:
**app/views/beers/index.html.erb**
```html
Beers
-<% cache "beerlist-#{@page}-#{@order}", skip_digest: true do %>
-
- <%= render "beers_page", beers: @beers, page: @page, order: @order, last_page: @last_page %>
-
-<% end %>
+
+ <%= render "beer_list", beers: @beers, page: @page, order: @order, last_page: @last_page %>
+
<%= link_to('New Beer', new_beer_path) if current_user %>
```
-Once the above is functioning correctly, we can enclose our table within the `_beers_page.html.erb` partial using a turbo frame:
+Once the above is functioning correctly, we can enclose our table within the `_beer_list.html.erb` partial using a Turbo Frame:
-**app/views/beers/\_beers_page.html.erb**
+**app/views/beers/\_beer_list.html.erb**
```html
-<%= turbo_frame_tag "beers_page" do %>
+<%= turbo_frame_tag "beer_list_frame" do %>
@@ -214,7 +534,7 @@ By using a Turbo Frame, all links and buttons within it will be controlled by Tu
def index
# ...
if turbo_frame_request?
- render partial: "beers_page",
+ render partial: "beer_list",
locals: { beers: @beers, page: @page, order: @order, last_page: @last_page }
else
render :index
@@ -222,19 +542,25 @@ def index
end
```
-The `turbo_frame_request?` condition ensures that when the request is made within a Turbo Frame, only the partial containing our beer table is returned. We can now observe the behavior within the network tab of our browser's developer tools.
+The `turbo_frame_request?` condition ensures that when the request is made within a Turbo Frame, only the partial containing our beer table is returned. We can now observe the behavior within the network tab of our browser's developer tools:
+
+![image](../images/8-7.png)
-![image](../images/ratebeer-w8-2.png)
+We can see that the headers include the ID of the Turbo Frame we are targeting, allowing Turbo to identify which part of the page should be replaced with the response data:
-We can see that the headers include the ID of the Turbo Frame we are targeting, allowing Turbo to identify which part of the page should be replaced.
+![image](../images/8-8.png)
-![image](../images/ratebeer-w8-3.png)
+Indeed, the response contains only the partial and excludes the application layout that accompanies the HTML document. The Turbo magic automatically handles this aspect.
-Indeed, the response contains only the partial and excludes the application layout that accompanies the HTML document. Turbo automatically handles this aspect.
+The only remaining issue is that the links to beers, breweries, and styles are no longer functional. If we eg. click a beer name, the response looks as follows:
-The only remaining issue is that the links to beers, breweries, and styles are no longer functional. Turbo attempts to load the links and replace our table with their content but fails to find a suitable turbo tag for replacement. We can easily resolve this by adding the target attribute to our links:
+![image](../images/8-9.png)
-**app/views/beers/\_beers_page.html.erb**
+Turbo attempts to replace our table with their content but fails to find a suitable turbo tag (*beer_list_frame*) for replacement so it simply renders nothing within the frame.
+
+We can easily resolve this by adding a suitable target attribute to our links:
+
+**app/views/beers/\_beer_list.html.erb**
```html
<% beers.each do |beer| %>
@@ -247,35 +573,123 @@ The only remaining issue is that the links to beers, breweries, and styles are n
<% end %>
```
-The `target="_top"` signifies that Turbo should break out of the frame and replace the entire page with the opened link. Alternatively, the `target` could be set to `_self`, targeting the current frame, or the ID of another Turbo Frame, in which case Turbo would attempt to replace that specific frame.
+The `turbo_frame="_top"` signifies that Turbo should break out of the frame and replace the entire page with the opened link. As seen from the earlier examples we can also use an ID of another Turbo Frame here, in which case Turbo would attempt to replace that specific frame.
We also notice that the URL remains unchanged when navigating between pages, and using the browser's back button may lead to unexpected results. We can easily address this by promoting our Turbo Actions into visits:
-**app/views/beers/\_beers_page.html.erb**
+**app/views/beers/\_beer_list.html.erb**
+
+```html
+<%= turbo_frame_tag "beer_list_frame", data: { turbo_action: "advance" } do %>
+```
+
+#### Asynchronous frame
+
+Let's say we want to suggest a beer to the user based on how they've rated other beers. Calculating the recommendation might take a long time, which is why we decided to load it asynchronously. So initially when the user goes to their own page, it just shows a "loading indicator", and when the recommendation is ready, that gets rendered to the page.
+
+This can be achieved with Turbo frames with a src attribute:
+
+```rb
+ <%= turbo_frame_tag "beer_recommendation_tag", src: recommendation_user_path do %>
+ calculating the recommendation...
+ <% end %>
+```
+
+Now initially only the text calculating the recommendation... is rendered. After the page is rendered Turbo makes an HTTP GET request to the specified path (users/id/recommendation) and fills in the HTML that it gets as a response. The partial for the recommendation looks the following:
+
+**views/users/_recommendation.html.erb**
```html
-<%= turbo_frame_tag "beers_page", data: { turbo_action: "advance" } do %>
+<%= turbo_frame_tag "beer_recommendation_tag" do %>
+
+ Recommendation based on your ratings
+
+ <%= link_to beer.name, beer %> by <%= link_to beer.brewery.name, beer.brewery %>
+
+<% end %>
```
-Under the hood, Turbo utilizes JavaScript to manipulate the [HTML DOM](https://www.w3schools.com/js/js_htmldom.asp) of the page, eliminating the need for us to write any JavaScript code ourselves!
+We will need a route and controller for the recommendation. The route (in *routes.rb*) is defined as follows:
+
+```rb
+resources :users do
+ post 'toggle_closed', on: :member
+ get 'recommendation', on: :member
+end
+```
+
+The controller finds out the recommendation (that is in our case just a randomly picked beer) and renders the partial. We have added a sleep of 2 seconds to simulate that calculating the recommendation takes a bit of time:
+
+```rb
+class UsersController < ApplicationController
+ # ...
+
+ def recommendation
+ # simulate a delay in calculating the recommendation
+ sleep(2)
+ ids = Beer.pluck(:id)
+ # our recommendation us just a randomly picked beer...
+ random_beer = Beer.find(ids.sample)
+ render partial: 'recommendation', locals: { beer: random_beer }
+ end
+
+ # ...
+end
+```
+
+Now when the user navigates to their own page, there is an indication that the recommendation is still to be calculated:
+
+![image](../images/8-10.png)
+
+After a while, the HTTP response is ready, and the returned partial containing the recommendation is rendered:
+
+![image](../images/8-11.png)
+
+#### Turbo under the hood
+
+As we have seen at the beginning of this week's material, Turbo Frame blocks are identified and separated with ```id``` tags and caught by the controller with the ```turbo_frame_request?```method. The controller then queries the model for the data needed and sends the updated part of HTML to the view. With the help of ID tags, only the specific part inside `````` is updated without having to refresh the entire page.
+
+Turbo Frames is built on the concept of [AJAX](https://www.w3schools.com/xml/ajax_intro.asp). In a traditional Rails application, a typical HTTP request (like ```GET```) would involve the controller processing the page load request and querying the model's database before delivering an entire HTML page back to the browser. With AJAX, and by extension Turbo Frames, instead of returning a full HTML page, only a section of the page is updated. This leads to faster loading as the application doesn't have to reload all data from the database. Turbo utilizes JavaScript to manipulate the [HTML DOM](https://www.w3schools.com/js/js_htmldom.asp) of the page, eliminating the need for us to write any JavaScript code ourselves!
-## Exercise 1
+## Exercise 3
+
+In this and the next exercise, we will refactor the breweries page to render the brewery lists asynchronously.
+
+Start by refactoring the breweries page so that there is a new partial `_brewery_list.html.erb` which is used separately to list breweries under active breweries and retired breweries.
-`turbo_frame_tag` has an attribute `src` available that will lazy load the contents of the source address into the turbo frame.
+Create the new endpoint GET `breweries/active` that returns the partial for the active breweries and uses that to render the active breweries asynchronously.
+
+The retired brewery list can still remain as it is.
+
+Note: it is **EXTREMELY IMPORTANT** to follow all the possible error messages, in the Rails console and the network tab of the browser when working with Action Frame!
+
+## Exercise 4
+
+Create also the new endpoint GET `breweries/retired` that returns the partial for the retired breweries and use that also in rendering the breweries page.
+
+The same partial should be used both for active and retired breweries. Note that you **can not** anymore use the **same** Turbo Frame tag for both the active and retired breweries.
+
+Notice that instead of defining a Turbo Frame tag as a hard-coded string, we can define it also as a variable that you set in the controller:
+
+```rb
+<%= turbo_frame_tag tag_as_a_variable do %>
+ # ...
+<% end %>
+```
+
+This makes it possible to use the same partial to render the contents of many different Turbo frames (that all have their own identifiers).
+
+Fix also the links to breweries so that they work inside the Turbo Frames.
-1. Refactor breweries page so that there is new partial `_breweries_list.html.erb` which is used separately to list breweries under active breweries and retired breweries.
-2. Create new endpoints behind `breweries/active` and `breweries/retired` routes that return the partials for the active and retired breweries respectively.
-3. Use `turbo_frame_tag` with `src` attribute to lazy load active and retired breweries into their respective turbo frames.
-4. Fix the links to breweries so that they work inside the turbo frames.
## Turbo Streams
-The purpose of [Turbo Streams](https://turbo.hotwired.dev/handbook/streams) is to enable page updates in fragments. For example, when a page displays a list of beers, instead of performing a complete page reload, a single beer can be appended or removed from the list in response to a change.
+The purpose of [Turbo Streams](https://turbo.hotwired.dev/handbook/streams) is to enable page updates in fragments. For example, when a page displays a list of breweries and a new beer is added or deleted, instead of performing a full page reload, a single brewery can be appended or removed from the list in response to a change.
-In modern web applications, achieving this behavior often involves having a separate server-side REST API or GraphQL API, commonly referred to as the back-end, to provide the necessary information in JSON format. The front-end queries this back-end, receives the JSON data, and renders the required HTML while updating the DOM accordingly.
+In modern web applications, achieving this kind of behavior often involves having a separate server-side REST API or GraphQL API, commonly referred to as the back-end, to provide the necessary information in JSON format. The front-end queries this back-end, receives the JSON data, and renders the required HTML while updating the DOM accordingly by using logic written in JavaScript.
Turbo simplifies this process by streaming pre-rendered HTML, compiled on the back-end, directly to the browser and handling the necessary actions internally.
@@ -285,7 +699,7 @@ Key concepts in Turbo Streams include **actions**, **targets**, and **templates*
In Turbo Streams, **actions** are a fundamental concept used to specify the changes or updates that should be performed on the client-side HTML DOM in response to a server-side event. An action represents a specific operation that can be applied to one or more target elements within a Turbo Stream response.
-**Actions** are defined using HTML-like syntax and consist of a combination of elements and attributes. Each action includes a target element, which represents the HTML element on the client-side that needs to be updated, and one or more operations that define how the target element should be modified.
+**Actions** are defined using HTML-like syntax and consist of a combination of elements and attributes. Each action includes a target element, which represents the HTML element on the client side that needs to be updated, and one or more operations that define how the target element should be modified.
The operations that can be applied to a target element include:
@@ -299,7 +713,7 @@ The operations that can be applied to a target element include:
### Turbo Streams Targets
-In order for **actions** to function properly, Turbo requires the identification of target elements within the DOM. This can be achieved by assigning unique HTML `id` parameters to individual elements or by utilizing `class` parameters to target multiple elements.
+For **actions** to function properly, Turbo requires the identification of target elements within the DOM. This can be achieved by assigning unique HTML `id` parameters to individual elements or by utilizing `class` parameters to target multiple elements.
For identifying a single element, one can explicitly create an ID value in the view or leverage the convenient Rails [dom_id](https://api.rubyonrails.org/classes/ActionView/RecordIdentifier.html) helper, which automatically generates the ID tag. For example:
@@ -329,15 +743,15 @@ you could use the remove action to remove all retired breweries from the list by
To leverage the capabilities of Turbo Streams, view templates should be designed as [partials](https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials) that can be rendered individually. This enables targeted streaming of changes to specific components. For example, when streaming updates for breweries and appending new breweries to a list, the brewery row should be implemented as a partial.
-To prepare the Breweries index page for streaming, extract the row rendering logic (created in Exercise 1) from `app/views/breweries/_breweries_list.html.erb`:
+To prepare the Breweries index page for streaming, extract the row rendering logic (created in Exercise 1) from `app/views/breweries/_brewery_list.html.erb`:
-**app/views/breweries/\_breweries_list.html.erb**
+**app/views/breweries/\_brewery_list.html.erb**
```html
<% breweries.each do |brewery| %>
- ">
- <%= link_to brewery.name, brewery, data: { turbo_frame: "_top"} %>
+
+ <%= link_to brewery.name, brewery, data: { turbo_frame: "_top" } %>
<%= brewery.year %>
<%= brewery.beers.count %>
<%= round(brewery.average_rating) %>
@@ -351,7 +765,7 @@ To new partial file `app/views/breweries/_brewery_row.html.erb`:
**app/views/breweries/\_brewery_row.html.erb**
```html
- ">
+
<%= link_to brewery.name, brewery, data: { turbo_frame: "_top"} %>
<%= brewery.year %>
<%= brewery.beers.count %>
@@ -359,7 +773,7 @@ To new partial file `app/views/breweries/_brewery_row.html.erb`:
```
-And change the original code to use this partial :
+And change the original code to use this partial:
```html
@@ -369,20 +783,16 @@ And change the original code to use this partial :
```
-Pay attention to new ID that we give to the tbody element. We need `active_brewery_rows` or `retired_brewery_rows` ID to **target** the **action** of appending new breweries as children of the correct table. If in Exercise 1 you did not define local `status` or something similar containing `active`/`retired` information for the different brewery listings, you should do that now as it will help us later.
+Pay attention to the new ID that we give to the tbody element. We need `active_brewery_rows` or `retired_brewery_rows` ID to **target** the **action** of appending new breweries as children of the correct table. If in Exercises 3 and 4 you did not define local `status` or something similar containing `active`/`retired` information for the different brewery listings, you should do that now as it will help us later.
Next, let's enable the addition of new breweries directly from the index page. Replace the following code in `app/views/breweries/index.html.erb`:
-**app/views/breweries/index.html.erb**
-
```html
<%= link_to "New brewery", new_brewery_path if current_user %>
```
With the following Turbo Frame tag:
-**app/views/breweries/index.html.erb**
-
```html
<%= turbo_frame_tag "new_brewery", src: new_brewery_path if current_user %>
```
@@ -391,8 +801,6 @@ This Turbo Frame will include a part of our existing code from the `new_brewery`
In `app/views/breweries/_new.html.erb` specify which part of the view you want to show in the Turbo Frame:
-**app/views/breweries/\_new.html.erb**
-
```html
New brewery
@@ -402,47 +810,51 @@ In `app/views/breweries/_new.html.erb` specify which part of the view you want t
# ...
```
+Now the breweries page looks like this:
+
![image](../images/ratebeer-w8-4.png)
-In order to append the created beer to the list without doing a full page update, we need to modify the response in the create action of the `app/controllers/breweries_controller.rb` file.
+To append the created brewery to the list without doing a full page update, we need to modify the response in the create action of the `app/controllers/breweries_controller.rb` file.
By adding the `format.turbo_stream` block, we specify that the response should be rendered as a Turbo Stream template with the action of appending the new brewery row to the target element with the ID `active_brewery_rows` or `retired_brewery_rows`. These steps enable the addition of breweries directly from the index page while only appending the created brewery to the list without refreshing the entire page.
-**app/controllers/breweries_controller.rb**
-
```ruby
-def create
- @brewery = Brewery.new(brewery_params)
+class BreweriesController < ApplicationController
- respond_to do |format|
- if @brewery.save
- format.turbo_stream {
- status = @brewery.active? ? "active" : "retired"
- render turbo_stream: turbo_stream.append("#{status}_brewery_rows", partial: "brewery_row", locals: { brewery: @brewery })
- }
- format.html { redirect_to brewery_url(@brewery), notice: "Brewery was successfully created." }
- format.json { render :show, status: :created, location: @brewery }
- else
- format.html { render :new, status: :unprocessable_entity }
- format.json { render json: @brewery.errors, status: :unprocessable_entity }
+ def create
+ @brewery = Brewery.new(brewery_params)
+
+ respond_to do |format|
+ if @brewery.save
+ format.turbo_stream {
+ status = @brewery.active? ? "active" : "retired"
+ render turbo_stream: turbo_stream.append("#{status}_brewery_rows", partial: "brewery_row", locals: { brewery: @brewery })
+ }
+ format.html { redirect_to brewery_url(@brewery), notice: "Brewery was successfully created." }
+ format.json { render :show, status: :created, location: @brewery }
+ else
+ format.html { render :new, status: :unprocessable_entity }
+ format.json { render json: @brewery.errors, status: :unprocessable_entity }
+ end
end
end
+
end
```
This change involves one line of code, but under the hood, several components enable this to work:
-1. When the browser initiates a new request, the Turbo framework includes the `text/vnd.turbo-stream.html` in the request's `Accept` headers. This informs the server that it expects a Turbo Stream template instead of a full page update.
+1. When the browser initiates a new request, the Turbo framework includes the `text/vnd.turbo-stream.html` in the request's `Accept` headers. This informs the server that it expects a Turbo Stream template instead of a full-page update.
![image](../images/ratebeer-w8-turbo-streams-header.png)
2. The controller, based on the `Accept` header, recognizes the request's Turbo Stream format and responds by rendering a Turbo Stream template instead of a complete page. This ensures that only the necessary HTML fragments are sent back to the browser.
-3. Using the `turbo_stream.append` method, a response HTML fragment is generated with the action set to `append`. This fragment targets the element with the identifier `brewery_rows` and utilizes the `_brewery_row.html.erb` partial to generate the content. Here is an example of the resulting fragment:
+3. Using the `turbo_stream.append` method, a response HTML fragment is generated with the action set to `append`. This fragment targets the element with the identifier `brewery_rows` and utilizes the `_brewery_row.html.erb` partial to generate the content. Here is an example of the resulting fragment (go and see yourself from the developer tools how the response looks):
```html
-
+
0
0.0
+ >
+
```
4. With the table body previously assigned an ID, such as ``, Turbo knows to append the generated template as the last child of the table body element. It intelligently places the new content in the appropriate location. You can test this behavior by removing or altering the ID and observing the resulting outcome.
@@ -478,12 +890,27 @@ To publish updates, we utilize the Brewery model `app/models/brewery.rb`. Whenev
**app/models/brewery.rb**
```ruby
+class Brewery < ApplicationRecord
+ include RatingAverage
+ extend TopRated
+
+ # ...
+
+ after_create_commit do
+ target_id = if active
+ "active_brewery_rows"
+ else
+ "retired_brewery_rows"
+ end
-after_create_commit -> { broadcast_append_to "breweries_index", partial: "breweries/brewery_row", target: "active_brewery_rows" }, if: :active?
-after_create_commit -> { broadcast_append_to "breweries_index", partial: "breweries/brewery_row", target: "retired_brewery_rows" }, if: :retired?
+ broadcast_append_to "breweries_index", partial: "breweries/brewery_row", target: target_id
+ end
+end
```
-This code broadcasts an `append` action to the `breweries_index` channel, targeting the element with the ID `active_brewery_rows` or `retired_brewery_rows`. It uses the `_brewery_row.html.erb` partial to create the template for the new beer. Essentially, it replicates the same functionality we implemented earlier by responding to client requests with fragments. However, the difference lies in the fact that the HTML fragment is now broadcasted to all browsers subscribed to the `breweries_index` channel, thanks to the power of WebSockets.
+Ruby on Rails calls the [callback](https://guides.rubyonrails.org/active_record_callbacks.html) function [after_create_commit](https://api.rubyonrails.org/v7.0.8/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_commit) always when a new object is created.
+
+The callback function broadcasts an `append` action to the `breweries_index` channel, targeting the element with the ID `active_brewery_rows` or `retired_brewery_rows`. It uses the `_brewery_row.html.erb` partial to create the template for the new brewery. Essentially, it replicates the same functionality we implemented earlier by responding to client requests with fragments. However, the difference lies in the fact that the HTML fragment is now broadcasted to **all browsers** subscribed to the `breweries_index` channel, thanks to the power of WebSockets.
You can test the functionality by opening two browser windows side by side and creating a new brewery. You'll observe that the updates are instantly reflected in both windows, demonstrating the real-time nature of ActionCable and WebSockets.
@@ -497,7 +924,7 @@ To address this issue, there are several possible solutions:
1. Comment out the stream template in the HTTP response from the controller. However, this approach has a downside: if there are any issues with WebSockets, the user won't see the effect of submitting a new brewer.
-2. Conditionally trigger the `after_create_commit` hook in the model based on the logged-in user. This approach ensures that the user only receives the WebSocket update once.
+2. Conditionally trigger the `after_create_commit` hook in the model based on the logged-in user. This approach ensures that the user only receives the WebSocket update once. This is pretty tricky to implement and would require the use of user-specific streams.
3. Opt for a simpler solution by giving each row a unique identifier. Let's proceed with this approach here.
@@ -520,31 +947,59 @@ It's worth noting that in our example, we used a simple string, `breweries_index
-## Exercise 2
+## Exercise 5
+
+With Turbo Streams and Action Cable, we are now equipped to create a beer chat for the users of our app!
-Enhance the breweries list functionality by adding a button or text "X" for removing a brewery from the database (see [Rails views documentation](https://guides.rubyonrails.org/layouts_and_rendering.html#rendering-by-default-convention-over-configuration-in-action)). The implementation should follow these steps:
+You need a model for the messages. Each message has the text content and ID of the creator. Note the order of the messages, the most recent is shown at the top!
-1. **Initial Removal (No Turbo, Full Page Reload)**
- Initially, make the removal work without using Turbo, requiring a full page reload after the delete action.
+Note: it is **EXTREMELY IMPORTANT** to follow all the possible error messages, in the Rails console and the network tab of the browser especially when working with Action Cable!
+
+Your solution could look like the following:
-2. **Dynamic Removal with Turbo Streams**
- Improve the functionality by dynamically removing the deleted brewery from the list using Turbo Streams. Ensure the removal is reflected in the UI without requiring a full page reload.
+![image](../images/8-12.png)
+
-3. **WebSocket Integration for Real-Time Updates**
- Leverage WebSockets to stream the removal action to all connected browsers in real time.
+## Exercise 6
-4. **Confirmation Pop-up**
- Enhance user experience by introducing a confirmation pop-up. When a user clicks the remove button, a confirmation dialog should appear with the text "Are you sure you want to remove brewery X and all beers associated with it?". The pop-up should provide options for "Cancel" and "Remove" actions.
+Enhance the breweries list functionality by adding a button or text "X" for removing a brewery from the database.
-
+The implementation should follow these steps:
-
+Initially, make the removal work without using Turbo, requiring a full page reload after the delete action.
-## Exercise 3
+Improve the functionality by dynamically removing the deleted brewery from the list using Turbo Streams. Ensure the removal is reflected in the UI without requiring a full page reload.
+
+Note: you might end up trying the following
+
+```html
+link_to("X", brewery, method: :delete) %>
+```
+
+this was a proper way to make a delete request in Rails up to version 6. In Rails 7 you need to specify the HTTP request verb a bit differently:
+
+```html
+link_to("X", brewery, data: {turbo_method: :delete }) %>
+```
+
+## Exercise 7
+
+Leverage WebSockets to stream the removal action to all connected browsers in real time.
+
+Enhance the user experience by introducing a confirmation pop-up. When a user clicks the remove button, a confirmation dialog should appear with the text "Are you sure you want to remove brewery X and all beers associated with it?". The pop-up should provide options for "Cancel" and "Remove" actions.
+
+You will [here](https://www.rubydoc.info/gems/turbo-rails/0.5.2/Turbo/Broadcastable) a suitable broadcast method. You might need to google a bit to get the parameters right.
+
+## Exercise 8
+
+Notice that _Number of Active Breweries_ and _Number of Retired Breweries_ require a full page reload to reflect the actual numbers. Make these numbers dynamic so that any addition or retirement of a brewery by the user triggers real-time updates. The changes should be streamed to reflect the updated counts instantly. In this exercise, the change made by other user/browsers does not need to affect the counts so Action Cable is not yet needed.
+
+Hint: you can render multiple Turbo Stream messages from a controller response by placing them in an array.
+
+## Exercise 9
-Notice that _Number of Active Breweries_ and _Number of Retired Breweries_ require a full page reload to reflect the actual numbers. Make these numbers dynamic so that any addition or retirement of a brewery by any user triggers real-time updates. The changes should be streamed to reflect the updated counts instantly.
+Extend the solution of the previous exercise to leverage Action Cable so that the brewery counts are updated also when somebody other creates or deletes a brewery.
-Hint: you can render multiple turbo stream messages from a controller response by placing them in an array.
## Stimulus
@@ -569,11 +1024,11 @@ Stimulus utilizes key concepts such as **controllers**, **actions**, **targets**
### Deleting ratings
-Let's try our hand at Stimulus with implementing a feature that allows users to delete multiple beer ratings at once without need for a full page reload.
+Let's try our hand at Stimulus by implementing a feature that allows users to delete multiple beer ratings at once without the need for a full page reload.
We can start by creating a new partial file named `_ratings.html.erb` within the `/app/views/users` folder.
-Then we extract the ratings code section (shown below) from the `/app/views/users/show.html.erb` file and place it into the ratings partial file.
+Then we extract the rating code section (shown below) from the `/app/views/users/show.html.erb` file and place it into the ratings partial file.
**/app/views/users/\_ratings.html.erb**
@@ -581,9 +1036,9 @@ Then we extract the ratings code section (shown below) from the `/app/views/user
<% @user.ratings.each do |rating| %>
-
- <%= "#{rating.score} #{rating.beer.name}" %>
+ <%= link_to "#{rating.score} #{rating.beer.name}", rating, data: { turbo_frame: "rating_details" } %>
<% if @user == current_user %>
- <%= button_to 'delete', rating, method: :delete, form: { style:'display:inline-block;', data: { 'turbo-confirm': 'Are you sure?' } } %>
+ <%= button_to 'delete', rating, method: :delete, form: { style: 'display:inline-block;', data: { 'turbo-confirm': 'Are you sure?' } } %>
<% end %>
<% end %>
@@ -599,7 +1054,7 @@ Then we delete the ratings code from the `/app/views/users/show.html.erb` file a
<%= render partial: 'ratings' %>
```
-We can then modify the partial by removing the list elements and delete button from the `/app/views/users/_ratings.html.erb` file, like so:
+We can then modify the partial by removing the list elements and the delete button from the `/app/views/users/_ratings.html.erb` file, like so:
**/app/views/users/\_ratings.html.erb**
@@ -610,7 +1065,9 @@ We can then modify the partial by removing the list elements and delete button f
<% if @user == current_user %>
<% end %>
- <%= "#{rating.score} #{rating.beer.name}" %>
+
+ <%= link_to "#{rating.score} #{rating.beer.name}", rating, data: { turbo_frame: "rating_details" } %>
+
<% end %>
<% if @user == current_user %>
@@ -619,16 +1076,16 @@ We can then modify the partial by removing the list elements and delete button f
```
-With the modified templates ready, let's update the `routes.rb` file (`/app/config/routes.rb`) to handle the ratings destroy action. Remove the `destroy` action from the ratings resources and add a separate delete method to handle the removal of rating IDs.
+With the modified templates ready, let's update the `routes.rb` file to handle the ratings destroy action. Remove the `destroy` action from the rating resources and add a separate delete method to handle the removal of rating IDs.
**/app/config/routes.rb**
```ruby
-resources :ratings, only: [:index, :new, :create]
+resources :ratings, only: [:index, :new, :create, :show]
delete 'ratings', to: 'ratings#destroy'
```
-Modify the `destroy` method within the `ratings_controller.rb` file (`app/controllers/ratings_controller.rb`) to handle the deletion of multiple rating IDs. We can do it like this:
+Modify the `destroy` method within the `ratings_controller.rb` to handle the deletion of multiple rating IDs. We can do it like this:
**app/controllers/ratings_controller.rb**
@@ -654,14 +1111,15 @@ Right now the delete button does not really do anything as it's not connected to
### Stimulus Controllers
-When working with Stimulus, it is essential to follow a specific naming convention for **controller** files. Each controller file should be named in the format `[identifier]_controller.js`, where the identifier corresponds to the data-controller attribute associated with the respective controller in your HTML markup.
-By adhering to this naming convention, Stimulus can seamlessly link the controllers in your HTML with their corresponding JavaScript files.
+The basic organizational unit of a Stimulus application is a [controller](https://stimulus.hotwired.dev/reference/controllers).
-Let's start by creating a `ratings_controller.js` and put it to file path `/app/javascript/controllers/ratings_controller.js`:
+When working with Stimulus, it is essential to follow a specific naming convention for **controller** files. Each controller file should be named in the format `[identifier]_controller.js`, where the identifier corresponds to the data-controller attribute associated with the respective controller in your HTML markup. By adhering to this naming convention, Stimulus can seamlessly link the controllers in your HTML with their corresponding JavaScript files.
-**/app/javascript/controllers/ratings_controller.js**
+Let's start by creating a `ratings_controller.js` and putting it to file path `/app/JavaScript/controllers/ratings_controller.js`:
-```javascript
+**/app/JavaScript/controllers/ratings_controller.js**
+
+```JavaScript
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
@@ -685,15 +1143,15 @@ We can then reload the page and see from the console log that we have indeed con
![image](../images/ratebeer-w8-9.png)
-`connect()` is a callback method which Stimulus supports out of the box which is executed when the controller is connected to the DOM.
+`connect()` is a callback method that Stimulus supports out of the box which is executed when the controller is connected to the DOM.
### Lifecycle Methods
-Lifecycle methods in Stimulus provide a capability for executing code at specific stages in the lifecycle of a controller. These methods, defined within the controller class, offer hooks for initialization, connection to the DOM, and disconnection from the DOM.
+[Lifecycle](https://stimulus.hotwired.dev/reference/lifecycle-callbacks) methods in Stimulus provide a capability for executing code at specific stages in the lifecycle of a controller. These methods, defined within the controller class, offer hooks for initialization, connection to the DOM, and disconnection from the DOM.
Here is an example showcasing the available lifecycle methods in a Stimulus controller:
-```javascript
+```JavaScript
import { Controller } from 'stimulus';
export default class extends Controller {
@@ -725,7 +1183,7 @@ By leveraging these lifecycle methods, developers can ensure proper initializati
### Stimulus Actions
-**Actions** in Stimulus are methods defined within a controller that respond to user events or changes in the application state. These actions are identified using the data-action attribute and can be triggered by various events, such as clicks, form submissions, or custom events.
+[Actions](https://stimulus.hotwired.dev/reference/actions) in Stimulus are methods defined within a controller that respond to user events or changes in the application state. These actions are identified using the data-action attribute and can be triggered by various events, such as clicks, form submissions, or custom events.
Let's add an action to our `