[007.5] Making our app prettier

Improving layout and implementing other functionalities

Subscribe now

Making our app prettier [12.08.2017]

We will start this episode by using the tag after episode 007.4.

Adding PureCSS

We already have implemented some features, but there are still some missing. We will use PureCSS to improve our layout. We will just import the CDN in our application layout to add it.

doctype html
html
  head
    title
     = content_for?(:title) ? yield(:title) : "Produciton"
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
    link crossorigin="anonymous" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css" integrity="sha384-nn4HPE8lTHyVtfCBi5yW9d20FjT8BJwUXyWZT9InLYax14RDjBj46LmSztkmNP9w" rel="stylesheet"
  body
    - flash.each do |key, value|
      div{class= "alert alert-#{key}"}= value
    div.pure-g.pure-u-1
      = yield

In our pages we will need to use several classes to use pure correctly.

In our Checklist index, let’s add some classes.

- content_for(:title, "Checklists")

h2 Checklists
h3 Here are some available checklists to start with

ul.checklist-list.pure-menu-list
  - @checklists.each do |checklist|
    li.checklist-item.pure-u-1
      = link_to checklist.title, checklist_path(checklist)

Also, in our Checklist show.

- content_for(:title, "#{@checklist.title} | Produciton")

div.pure-g
  = form_tag(checklist_shares_path, class: "pure-form")
    = text_field_tag :email,nil, {placeholder: "Share this Checklist", id: "share-checklist-email"}
    = hidden_field_tag :checklist_id, @checklist.id
    = submit_tag "Share", id: "share-checklist-#{@checklist.id}", class: "pure-button"
    div.pure-u-1
      h3
        = @checklist.title
ul.checklist-list.pure-menu-list
  - @checklist.checklist_items.each do |item|
    li.checklist-item.pure-g{ class= checklist_item_class(item) }
      div.pure-u-1-2
        = link_to item.title, '#'
      div.pure-u-1-2
        = form_for(item) do |f|
          = f.hidden_field :completed, {value: !item.completed}
          = f.submit checklist_item_button(item), id: "complete-checklist-item-#{item.id}", class: "pure-button button-xsmall"
  - if @checklist.all_done?
    h3 Everything's finished!

This makes our App a bit prettier. However, we need to add some css to our application.

The alerts css properties will follow a common standard you can find on the Internet. Success with green, and error with a red tone.

.pure-g {
  display: flex;
  background: rgb(250, 250, 250);
  justify-content: center;
  text-align: center;
}

.checklist-item {
 padding: 1rem;
}

.checklist-item:hover {
  background: white;
}

.alert {
  padding: 1rem;
}

.button-xsmall {
  font-size: 70% !important;
}

.alert-error {
    background-color: #f2dede;
    border-color: #eed3d7;
    color: #b94a48;
    text-align: left;
 }

.alert-alert {
    background-color: #f2dede;
    border-color: #eed3d7;
    color: #b94a48;
    text-align: left;
 }

.alert-success {
    background-color: #dff0d8;
    border-color: #d6e9c6;
    color: #468847;
    text-align: left;
 }

.alert-notice {
    background-color: #dff0d8;
    border-color: #d6e9c6;
    color: #468847;
    text-align: left;
 }

It looks better now.

Managing Checklist Items

There are some feature tests we didn't implement yet. They are about managing Checklist Items. Adding, removing and editing items.

We will start by adding ChecklistItems. Our routes are not yet nested. It's good if they are nested, because we can easily use form_for. Then, pass the checklist and the checklist item, and generate the correct route.

  resources :checklists do
    resources :checklist_items
  end

Checklist Items will be nested with checklists. If we do a rake routes, we can see our routes:

➜  produciton git:(6248dfb) ✗ rake routes | grep items
     checklist_checklist_items GET      /checklists/:checklist_id/checklist_items(.:format)
                               POST     /checklists/:checklist_id/checklist_items(.:format)
  new_checklist_checklist_item GET      /checklists/:checklist_id/checklist_items/new(.:format)
 edit_checklist_checklist_item GET      /checklists/:checklist_id/checklist_items/:id/edit(.:format)
      checklist_checklist_item GET      /checklists/:checklist_id/checklist_items/:id(.:format)
                               PATCH    /checklists/:checklist_id/checklist_items/:id(.:format)
                               PUT      /checklists/:checklist_id/checklist_items/:id(.:format)
                               DELETE   /checklists/:checklist_id/checklist_items/:id(.:format)

We can see the items that need to have a checklist id.

In our Checklist show page, we can create a link to add a new item. We need to change the form for to pass the checklist and the item.

To create a new item, we just need to have the checklist. So, we are just passing the checklist.

    div.pure-u-1
      h3
        = @checklist.title
      = link_to "Add Item", new_checklist_checklist_item_path(@checklist)

In the form for, we need to pass the checklist and the item. This generates the correct URL.

      div.pure-u-1-2
        = form_for([@checklist, item]) do |f|
          = f.hidden_field :completed, {value: !item.completed}
          = f.submit checklist_item_button(item), id: "complete-checklist-item-#{item.id}", class: "pure-button button-xsmall"

Creating a new item

We need to have a page for a new ChecklistItem.

vim app/views/checklist_items/new.html.slim
h3= "New Item for #{@checklist.title}"

= form_for([@checklist, @checklist_item]) do |f|
  = f.text_field :title
  = f.submit "Save"

Our controller should have the create and new methods. For these two methods we are always finding the checklist. I'm using a before action, and calling the find checklist method. This is a private method.

  before_action :find_checklist, only: %i[new create]

  def new
    @checklist_item = ChecklistItem.new(checklist: @checklist)
  end

  def create
    @checklist_item = ChecklistItem.new(checklist_item_params)
    @checklist_item.checklist = @checklist
    @checklist_item.save!
    flash[:success] = 'Item created'
    redirect_to @checklist
  end

  private

  def find_checklist
    @checklist = Checklist.find(params[:checklist_id])
  end

ameters when creating the Checklist Item. . Then, I set the checklist. After creation, I redirect to the checklist show page.

Go to our acceptance test, and we will check the scenario. Then, make our test pass.

We need to change some details in the test. We need to add the login, and click in the Save button. We also need to change the uncompleted checklist item matcher, because now we have a new div inside of our ul element.

  scenario 'adding a checklist item' do
    visit checklist_path(ruby)
    log_in(user)
    click_on(ruby.title)
    click_on('Add Item')

    fill_in 'checklist_item_title', with: set_up_cdn.title
    click_button 'Save'
    expect(page).to have_uncompleted_checklist_item(set_up_cdn)
  end

Our test pass, and we are able to create Items in our Checklist.

We need to be able to remove and edit items to finish our acceptance tests.

Removing a new item

To remove a item, we will start by creating our controller method. This method will also call the find_checklist method.

  def destroy
    @checklist_item = @checklist.checklist_items.find(params[:id])
    @checklist_item.destroy
    redirect_to @checklist
  end

To destroy a checklist, we need to make sure the checklist item belongs to the checklist we are talking about. To make sure of this, we are calling the method checklist_items for the checklist, and looking for the checklist item by id.

In our view, we can use a link_to and have it as an HTTP delete method. We can also pass a confirmation question. This will open a confirmation dialog from the browser. Something simple, just to make sure the user really wants to delete it. On this delete link, we also have an id, so it can be tracked easily.

        div
          = link_to 'Delete', checklist_checklist_item_path(@checklist, item),
            method: :delete, id: "delete-#{item.id}", data: {confirm: 'Are you sure?'}

Going to our tests, we can see the remove checklist item test.

  scenario 'removing a checklist item' do
    visit checklist_path(ruby)
    log_in(user)
    click_on(ruby.title)

    expect(page).to have_uncompleted_checklist_item(set_up_cdn)
    within(checklist_item_selector(set_up_cdn)) do
      click_on('Delete')
    end
    expect(page).not_to have_uncompleted_checklist_item(set_up_cdn)
  end

To help us, we will create the method checklist_item_selector, and it receives an item.

The implementation is simple. We are looking for the item which has the same id as the item id.

def checklist_item_selector(item)
  "#item-#{item.id}"
end

Our view doesn't have this yet. Let's pass the id in our view.

ul.checklist-list.pure-menu-list
  - @checklist.checklist_items.each do |item|
    li.checklist-item{ class=checklist_item_class(item) id="item-#{item.id}" }

This should make the removing tests pass. Executing the test. It passes.

Editing a Item

The last missing test is about editing an item. It will have a form, and it will be the same form we use to create an item. A common way to reuse the form is creating a partial.

vim app/views/checklist_items/_form.html.slim

And our form will be rendering only the form and an h3 for Edit.

vim app/views/checklist_items/edit.html.slim
h3 Edit

= render 'checklist_items/form'

In our new view we will reuse the form as well.

h3= "New Item for #{@checklist.title}"

= render 'checklist_items/form'

And the last missing piece is the edit method in our controller. In the edit method, we just prepare the item to be edited. So, we get the checklist item from the checklist. We are also using the before action here.

  def edit
    @checklist_item = @checklist.checklist_items.find(params[:id])
  end

And we need to add the Edit link into our show page.

        div
          = link_to 'Edit', edit_checklist_checklist_item_path(@checklist, item)

Using this we can reuse the update method from our form.

Let's now go back to our test.

Our test will be:

  scenario 'editing a checklist item title' do
    visit checklist_path(ruby)
    log_in(user)
    click_on(ruby.title)

    expect(page).to have_uncompleted_checklist_item(set_up_cdn)
    edited_text = 'edited yay'
    within(checklist_item_selector(set_up_cdn)) do
      click_link 'Edit'
    end

    fill_in 'checklist_item_title', with: edited_text
    click_on('Save')

    expect(page).to have_uncompleted_checklist_item(set_up_cdn.reload)
    expect(page).to have_text edited_text
  end

In the text we will log in the user, and check if we have the uncompleted checklist item. We will edit the title of this checklist item, we save this, and we check if the new edited text is on the page.

We are now able to run it. Running and it passes. We can fix small details on the matchers, because we have changed the CSS. All of our feature tests now pass.

Summary

Today we have implemented simple features for our app. We did that based off of our own tests. We know our tests have feature tests covered. This helps us make sure the initial features are accomplished.

We will implement some other features to make sure the authorizations are working correctly. Stay tuned.

Resources