|
1 |
| -# Helper Methods |
| 1 | +# Helper Methods 3 |
2 | 2 |
|
3 |
| -The starting point of this project is where we left the ad2-getting-started project at the end of class on Day 1. |
| 3 | +The starting point of this project is a solution to Helper Methods after completing Parts 1 & 2. |
4 | 4 |
|
5 | 5 | ## Setup
|
6 | 6 |
|
7 | 7 | ```
|
8 | 8 | bin/setup
|
9 | 9 | ```
|
10 | 10 |
|
11 |
| -## Write some tests |
| 11 | +## Pull in Bootstrap, Font Awesome |
12 | 12 |
|
13 |
| -We're going to be learning a _lot_ of ways to improve our code, while holding functionality constant. |
| 13 | +Let's start to make this project look a little nicer. In the application layout: |
14 | 14 |
|
15 |
| -That means we're going to need to do a lot of testing to make sure we didn't break functionality as we evolve our code. Once you get tired of manually clicking through every link, form, etc, consider writing some automated tests to save yourself the trouble. |
| 15 | +- [Pull in Bootstrap CSS and Font Awesome with our quick-and-dirty CDN links.](https://chapters.firstdraft.com/chapters/788#quick-links-to-assets). |
| 16 | +- [Add a Bootstrap navbar.](https://getbootstrap.com/docs/5.1/components/navbar/) |
| 17 | +- For the `notice` and `alert`, switch to using [Bootstrap alerts](https://getbootstrap.com/docs/5.1/components/alerts/). |
| 18 | +- Add [a Bootstrap `div.container`](https://getbootstrap.com/docs/5.2/layout/containers/) around the `yield` so that all of our templates are rendered within one. |
16 | 19 |
|
17 |
| -Read Sections 1, 2, 4, 5, 6, and 7 of the [Rails Guide on Testing](https://guides.rubyonrails.org/testing.html). These are the kinds of tests that we write most frequently. |
| 20 | +## Partial view templates |
18 | 21 |
|
19 |
| -In this project, we have one fully functional web resource, `movies`. Create a test file, `test/system/movie_test.rb`, and take a stab at writing some System tests in it to lock down the current functionality of the application. For example, try adding a test that checks to make sure the URL `/movies` has an `<h1>` on it containing the copy "List of all movies". Run your tests with: |
| 22 | +Partial view templates (or just "partials", for short) are an extremely powerful tool to help us modularize and organize our view templates. Especially once we start adding in styling with Bootstrap, etc, our view files will grow to be hundreds or thousands of lines long, so it becomes increasingly helpful to break them up into partials. |
20 | 23 |
|
| 24 | +### Official docs |
| 25 | + |
| 26 | +[Here is the official article in the Rails API reference describing all the ways you can use partials.](https://edgeapi.rubyonrails.org/classes/ActionView/PartialRenderer.html) There are lots of powerful options available, but for now we're going to focus on the most frequently used ones. |
| 27 | + |
| 28 | +### Getting started: static HTML partials |
| 29 | + |
| 30 | +Create a partial view template in the same way that you create a regular view template, except that the first letter in the file name _must_ be an underscore. This is how we (and Rails) distinguish partial view templates from full view templates. |
| 31 | + |
| 32 | +For example, create a file called `app/views/zebra/_giraffe.html.erb`. Within it, write the following: |
| 33 | + |
| 34 | +```html |
| 35 | +<h1>Hello from the giraffe partial!</h1> |
| 36 | +``` |
| 37 | + |
| 38 | +Then, in any of your other view templates, e.g. `movies/index`, add: |
| 39 | + |
| 40 | +```html |
| 41 | +<%= render template: "zebra/giraffe" %> |
| 42 | +``` |
| 43 | + |
| 44 | +Notice that **we don't include the underscore when referencing the partial** in the `render` method, even though the underscore must be present in the actual filename. |
| 45 | + |
| 46 | +You can render the partial as many times as you want: |
| 47 | + |
| 48 | +```html |
| 49 | +<%= render template: "zebra/giraffe" %> |
| 50 | + |
| 51 | +<hr> |
| 52 | + |
| 53 | +<%= render template: "zebra/giraffe" %> |
| 54 | +``` |
| 55 | + |
| 56 | +A more realistic example of putting some static HTML into a partial is extracting a 200 line Bootstrap navbar into `app/views/shared/_navbar.html.erb` and then `render`ing it from within the application layout. Try doing that now. |
| 57 | + |
| 58 | +### Partials with inputs |
| 59 | + |
| 60 | +Breaking up large templates by putting bits of static HTML into partials is nice, but even better is the ability to dynamically render partials based on varying inputs. |
| 61 | + |
| 62 | +For example, create a file called `app/views/zebra/_elephant.html.erb`. Within it, write the following: |
| 63 | + |
| 64 | +```erb |
| 65 | +<h1>Hello, <%= person %>!</h1> |
| 66 | +``` |
| 67 | + |
| 68 | +Then, in `movies/index`, try: |
| 69 | + |
| 70 | +```erb |
| 71 | +<%= render template: "zebra/elephant" %> |
| 72 | +``` |
| 73 | + |
| 74 | +When you test it, it will break and complain about an undefined local variable `person`. To fix it, try: |
| 75 | + |
| 76 | +```erb |
| 77 | +<%= render template: "zebra/elephant", locals: { person: "Alice" } %> |
| 78 | +``` |
| 79 | + |
| 80 | +Now it becomes more clear why it can be useful to render the same partial multiple times: |
| 81 | + |
| 82 | +```erb |
| 83 | +<%= render template: "zebra/elephant", locals: { person: "Alice" } %> |
| 84 | +
|
| 85 | +<hr> |
| 86 | +
|
| 87 | +<%= render template: "zebra/elephant", locals: { person: "Bob" } %> |
| 88 | +``` |
| 89 | + |
| 90 | +If we think of rendering partials as _calling methods that return HTML_, then the `:locals` option is how we _pass in arguments_ to those methods. This allows us to create powerful, reusable HTML components. |
| 91 | + |
| 92 | +### Form partials |
| 93 | + |
| 94 | +In this application, can you find any ERB that's re-used in multiple templates? |
| 95 | + |
| 96 | +Well, since we evolved to using `form_with model: @movie`, the two forms in `movies/new` and `movies/edit` are exactly the same! |
| 97 | + |
| 98 | +1. Let's extract the common ERB into a template called `app/views/movies/_form.html.erb`. |
| 99 | +1. Then render it from both places with: |
| 100 | + |
| 101 | + ```erb |
| 102 | + render template: "movies/form" |
| 103 | + ``` |
| 104 | + |
| 105 | +If you test it out, you'll notice that it works. However, we're kinda getting lucky here that we named our instance variable the same thing in both actions —— `@movie`. Try making the following variable name changes in `MoviesController`: |
| 106 | +
|
| 107 | +```rb |
| 108 | +def new |
| 109 | + @new_movie = Movie.new # instead of @movie |
| 110 | +end |
| 111 | +
|
| 112 | +def edit |
| 113 | + @the_movie = Movie.find(params.fetch(:id)) # instead of @movie |
| 114 | +end |
21 | 115 | ```
|
22 |
| -rails test test/system/movie_test.rb |
| 116 | + |
| 117 | +Now if you test it out, you'll get errors complaining about undefined methods for `nil`, since the `movies/_form` partial expects an instance variable called `@movie` and we're no longer providing it. |
| 118 | + |
| 119 | +So, should we always just use the same exact variable name everywhere? That's not very flexible, and sometimes it's just not possible. Instead, we should use the `:locals` option: |
| 120 | + |
| 121 | +Update the `form` partial to use an arbitrary local variable name, e.g. `foo`, rather than `@movie`: |
| 122 | + |
| 123 | +```erb |
| 124 | +<%= form_with model: foo do |form| %> |
| 125 | +``` |
| 126 | + |
| 127 | +If you test it out now, you'll get the expected "undefined local variable `foo`" error. |
| 128 | + |
| 129 | +But then, update `movies/new`: |
| 130 | + |
| 131 | +```erb |
| 132 | +<%= render template: "movies/form", locals: { foo: @new_movie } %> |
| 133 | +``` |
| 134 | + |
| 135 | +And `movies/edit`: |
| 136 | + |
| 137 | +```erb |
| 138 | +<%= render template: "movies/form", locals: { foo: @the_movie } %> |
| 139 | +``` |
| 140 | + |
| 141 | +If you test it out, everything should be working again. And, it's much better, because the `movies/_form` partial is flexible enough to be called from any template, or multiple times within the same template (e.g. if we wanted to have multiple comment forms on a photos index page). |
| 142 | + |
| 143 | +So, a rule of thumb: **don't use instance variables within partials**. Instead, prefer to use the `:locals` option and pass in any data that the partial requires, even though it's more verbose to do it that way. |
| 144 | + |
| 145 | +### ActiveRecord object partials |
| 146 | + |
| 147 | +Rendering an HTML representation of a record from our database is the most common work we do in a CRUD web app. As you might expect, Rails provides several handy shortcuts for doing this efficiently with partials. Let's experiment! |
| 148 | + |
| 149 | +#### Bootstrap one movie |
| 150 | + |
| 151 | +First, let's improve `movies#show` to make use of [a Bootstrap card](https://getbootstrap.com/docs/5.1/components/card/) and [some Font Awesome icons](https://fontawesome.com/search?o=r&m=free): |
| 152 | + |
| 153 | +```html |
| 154 | +<div class="card"> |
| 155 | + <div class="card-header"> |
| 156 | + <%= link_to "Movie ##{@movie.id}", @movie %> |
| 157 | + </div> |
| 158 | + |
| 159 | + <div class="card-body"> |
| 160 | + <dl> |
| 161 | + <dt> |
| 162 | + Title |
| 163 | + </dt> |
| 164 | + <dd> |
| 165 | + <%= @movie.title %> |
| 166 | + </dd> |
| 167 | + |
| 168 | + <dt> |
| 169 | + Description |
| 170 | + </dt> |
| 171 | + <dd> |
| 172 | + <%= @movie.description %> |
| 173 | + </dd> |
| 174 | + </dl> |
| 175 | + |
| 176 | + <div class="row"> |
| 177 | + <div class="col"> |
| 178 | + <div class="d-grid"> |
| 179 | + <%= link_to edit_movie_path(@movie), class: "btn btn-outline-secondary" do %> |
| 180 | + <i class="fa-regular fa-pen-to-square"></i> |
| 181 | + <% end %> |
| 182 | + </div> |
| 183 | + </div> |
| 184 | + <div class="col"> |
| 185 | + <div class="d-grid"> |
| 186 | + <%= link_to @movie, method: :delete, class: "btn btn-outline-secondary" do %> |
| 187 | + <i class="fa-regular fa-trash-can"></i> |
| 188 | + <% end %> |
| 189 | + </div> |
| 190 | + </div> |
| 191 | + </div> |
| 192 | + </div> |
| 193 | + |
| 194 | + <div class="card-footer"> |
| 195 | + Last updated <%= time_ago_in_words(@movie.updated_at) %> ago |
| 196 | + </div> |
| 197 | +</div> |
| 198 | +``` |
| 199 | + |
| 200 | +#### Constrain the size with the grid |
| 201 | + |
| 202 | +Let's add [a Bootstrap grid row and cell](https://getbootstrap.com/docs/5.2/layout/grid/) to `movies#show` to constrain the card a bit: |
| 203 | + |
| 204 | +```html |
| 205 | +<div class="row"> |
| 206 | + <div class="col-md-6 offset-md-3"> |
| 207 | + |
| 208 | + <!-- code for movie card in here --> |
| 209 | + |
| 210 | + </div> |
| 211 | +</div> |
23 | 212 | ```
|
24 | 213 |
|
25 |
| -After you’ve spent 20-30 minutes on it, you’re allowed to look at the example specs in `test/system/example_specs.rb`. You can copy-paste some or all of them into your own test file and run then periodically as you’re doing the rest of the project to ensure you didn’t break anything as we refactor. |
| 214 | +#### Cards in index |
| 215 | + |
| 216 | +Now that we've styled one movie nicely, can we use the same styling on the index page? We _could_ copy-paste the ERB over from `movies#show`, but there's a better way: |
| 217 | + |
| 218 | +1. Make a partial called `movies/_movie_card.html.erb`. |
| 219 | +2. Copy the ERB that represents one movie from `movies#show` into this new partial. |
| 220 | +3. In the partial, wherever we were referencing the instance variable that was defined by `movies#show` (`@movie`), replace with a local variable (let's call it `baz`). |
| 221 | +4. Render the partial within `movies#show`: |
| 222 | + |
| 223 | + ```html |
| 224 | + <%= render partial: "movies/movie_card", locals: { baz: @movie } %> |
| 225 | + ``` |
| 226 | +5. Re-use the partial in `movies#index`: |
26 | 227 |
|
27 |
| -## Walkthrough Video |
| 228 | + ```html |
| 229 | + <% @movies.each do |movie| %> |
| 230 | + <%= render partial: "movies/movie_card", locals: { baz: movie } %> |
| 231 | + <% end %> |
| 232 | + ``` |
| 233 | +6. Constrain the size of each card with grid classes: |
28 | 234 |
|
29 |
| -There's a walkthrough video to go along with this project on Canvas. |
| 235 | + ```html |
| 236 | + <div class="row"> |
| 237 | + <% @movies.each do |movie| %> |
| 238 | + <div class="col-md-3"> |
| 239 | + <%= render partial: "movies/movie_card", locals: { baz: movie } %> |
| 240 | + </div> |
| 241 | + <% end %> |
| 242 | + </div> |
| 243 | + ``` |
30 | 244 |
|
31 |
| -### Clean-ups that we already know how to do |
| 245 | +Neat! Now if we change the appearance of the movie card, or add an attribute, we only have to change it in one place. |
32 | 246 |
|
33 |
| - - Anywhere you see the old `Hash` syntax (`:symbol` keys with hash rockets `=>`): switch to [the new `Hash` syntax](https://chapters.firstdraft.com/chapters/787#new-hash-syntax). |
34 |
| - - Anywhere you see [optional curly braces around `Hash` arguments](https://chapters.firstdraft.com/chapters/787#curly-braces-around-hash-arguments): remove them. |
35 |
| - - Where possible, drop `render` statements. |
36 |
| - - When providing keys to `params`, use `Symbol`s. (Unlike with most hashes, you can use `String`s and `Symbol`s interchangeably as keys to `params`.) |
| 247 | +### Partials shine along with Jump To File |
37 | 248 |
|
38 |
| -### Refactor routes |
| 249 | +In addition to all the other benefits, partials really help you get around your codebase efficiently. For example, if you need to make a change to the navbar, rather than going to the application layout file and digging around, you can use the Jump To File keyboard shortcut (Windows: Ctrl+P, Mac: Cmd+P). Start typing the name of the file — VSCode fuzzily matches what you type and usually finds the right file within a few characters. Hit <kbd>return</kbd> and boom you're transported to just where you want to be. |
39 | 250 |
|
40 |
| - - Re-write routes using more succinct forms of `get`, `post`, `patch`, `delete`, and, ultimately, `resources`. |
| 251 | +If you're still manually clicking files and folders in the sidebar, start trying to get used to navigating with Jump To File instead. |
41 | 252 |
|
42 |
| -### First new helper methods |
| 253 | +## before_action |
43 | 254 |
|
44 |
| - - Replace all references to URLs outside of `config/routes.rb` with route helper methods. |
45 |
| - - Replace all `<a>` elements in view templates with `link_to` methods. |
| 255 | +Read about [controller filters](https://guides.rubyonrails.org/action_controller_overview.html#filters). [Where have we seen this technique before?](https://chapters.firstdraft.com/chapters/888#current_user) |
| 256 | + |
| 257 | +In this application, try using `before_action` to DRY up the repetition we see with: |
| 258 | + |
| 259 | +``` |
| 260 | +@movie = Movie.find(params.fetch(:id)) |
| 261 | +``` |
| 262 | +
|
| 263 | +being repeated in the `show`, `edit`, `update`, and `destroy` actions. In order for this trick to work, we must use the same instance variable name in all four actions. This is a double-edged sword — relying on the same variable name isn't very flexible, but it does allow us to eliminate a lot of repeated code. |
| 264 | +
|
| 265 | +## Generate a scaffold |
| 266 | +
|
| 267 | +Try using the built-in `scaffold` generator to spin up another resource, e.g. directors: |
| 268 | +
|
| 269 | +``` |
| 270 | +rails g scaffold director name dob:date bio:text |
| 271 | +``` |
46 | 272 |
|
47 |
| -### Forms in general |
| 273 | +Carefully read through all of the code that was generated. Do you understand all of it now? Ask questions about anything that's fuzzy. |
48 | 274 |
|
49 |
| - - Replace all `<form>` elements in view templates with `form_with` methods. |
50 |
| - - Replace all `<input>` elements with `text_field_tag`, `number_field_tag`, `text_area_tag`, etc. |
51 |
| - |
52 |
| -### Forms specifically for model objects |
| 275 | +## Solutions |
53 | 276 |
|
54 |
| - - Change the names of inputs so that values in the `params` hash are nested within an inner hash. |
55 |
| - - Assign all of the values at once using ActiveRecord's mass assignment ability via `new` or `update`. |
56 |
| - - Whitelist which attributes you want to allow to be mass assigned from `params` with `params.require(:movie).permit(:title, :description)`. |
57 |
| - - Switch from `form_with(url:)` to `form_with(model:)` |
58 |
| - - Switch from `text_field_tag`, etc; to `form.text_field`, etc. |
59 |
| - - Move form to partial and re-use wherever possible. |
| 277 | +You can see my solutions for this project in [this pull request](https://github.com/appdev-projects/helper-methods-3/pull/1/files). |
0 commit comments