Notes for AWD with Rails 4

I checked out from LHU library and grab this book last two days.
And I am going to take some notes here so that I can review it easily in the future, and I also would like to record a story of how n00b wrote their thoughts down.

Creating application with specific Rails version

Here is the example for Rails 4, the command:

Command Line
1
$ rails _4.0.0_ new app_name

Creating your own Rails API documentation

Command Line
1
2
$ cd app_name
$ rake doc:rails

Alternative syntax

%{} is an alternative syntax for double-quoted string literals, convenient for use with long strings:

app/db/seed.rb
1
2
3
4
5
6
Product.create!(title: 'Baozi', description: %{
A
n00b
of
Rails
})

And here why I used exclamation mark (!) for create method? The reason why is that it will raise an exception if records cannot be inserted because of validation failed.

Load specific stylesheet in specific controller

Deponds on controller_name method (API)

app/views/layouts/application.html.erb
1
2
3
<body class="<%= controller.controller_name %>">
<%= yield %>
</body>

Alternate CSS classes for even and odd numbers

Deponds on cycle method (API)

app/views/layouts/application.html.erb
1
2
3
4
5
6
7
<h1>Listing products</h1>
<table>
<% @products.each do |product| %>
<tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
...
</table>

Helpful commands for Git

If you overwrite or delete files, directries that you didn’t mean to, you can always get back by using this command:

Command Line
1
$ git checkout .

On the contrary, if you create folders or files that you didn’t want, try:

Command Line
1
$ git clean -d -f

to get back.

Handling Errors

When we enter example.com/carts/baozi, Active Record will raise a RecordNotFound exception. We can handle it like this:

app/controllers/carts_controller.rb
1
2
3
4
class CartsController < ApplicationController
before_action :set_cart, only: [:show, :edit, :update, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
...

app/controllers/carts_controller.rb
1
2
3
4
5
6
private
def invalid_cart
logger.error "Attempt to access invalid cart #{params[:id]}"
flash[:error] = "Invalid cart."
redirect_to store_url
end

Calculate a sum from the element (API)

app/models/cart.rb
1
2
3
def total_price
line_items.to_a.sum { |item| item.total_price }
end

So the Line_item.rb need total_price method:

app/models/line_item.rb
1
2
3
def total_price
product.price * quantity
end

Nested form with Rails 4.1

Recently I got a guide of telling us how to be self-taught from on-line courses. It shows me how important taking note is. I highly recommended this guide to who also wants to be self-taught.

The average person forgets 40% of what they learn within 20 minutes.

So I decide to take notes here since now. It will spend most of my time to do, but if I do not do it today, I know I won’t do it anymore. That is my lazy personality. The first technical post I am going to present my reviews of #196 Nested Model Form (revised); meanwhile, some of the codes there are not working because of the version of Rails, so I will try to make it work like video shows with the latest Rails, please revise my code if you have better ways, thanks a bunch. OK so let us get started.

Like #196 Nested Model Form (revised) said, there are three separate models and already had associations with each other:

app/models/survey.rb
1
2
3
class Survey < ActiveRecord::Base
has_many :questions
end
app/models/question.rb
1
2
3
4
class Question < ActiveRecord::Base
belongs_to :survey
has_many :answers
end
app/models/answer.rb
1
2
3
class Answer < ActiveRecord::Base
belongs_to :question
end

Here is the schema:

db/schema.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ActiveRecord::Schema.define(version: 20140817155213) do
create_table "answers", force: true do |t|
t.text "content"
t.integer "question_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "questions", force: true do |t|
t.text "content"
t.integer "survey_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "surveys", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end

The Purpose

All we want is to manage them in a single form. In order to implement this, we have to call accepts_nested_attributes_for method in our model:

app/models/survey.rb
1
2
3
4
class Survey < ActiveRecord::Base
...
accepts_nested_attributes_for :questions
end

So that we can add fields_for to Question model in the form:

app/views/surveys/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
...
...
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<%= f.fields_for :questions do |builder| %>
<fieldset>
<%= builder.label :content, 'Question' %><br>
<%= builder.text_area :content %><br>
</fieldset>
<% end %>
...
...
...

Remember to add plural name of associated model (Followed by _attributes) to Strong Parameter from SurveysController:

app/controllers/surveys_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
...
...
...
private
...
...
...
# Never trust parameters from the scary internet, only allow the white list through.
def survey_params
params.require(:survey).permit(:name, questions_attributes: [:id, :content])
end

Removing Questions

Next we are going to implement removing questions feature. We will create a checkbox whose key is _destroy, so the question will be removed if checkbox is checked.

app/views/surveys/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
...
...
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<%= f.fields_for :questions do |question| %>
<fieldset>
<%= question.label :content, 'Question' %><br>
<%= question.text_area :content %><br>
<%= question.check_box :_destroy %>
<%= question.label :_destroy, 'Remove question' %>
</fieldset>
<% end %>

Add allow_destroy option to Survey model, set the value to be true:

app/models/survey.rb
1
2
3
4
class Survey < ActiveRecord::Base
...
accepts_nested_attributes_for :questions, allow_destroy: true
end

We also need to add _destroy to questions_attributes like before:

app/controllers/surveys_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
...
...
...
private
...
...
...
# Never trust parameters from the scary internet, only allow the white list through.
def survey_params
params.require(:survey).permit(:name, questions_attributes: [:id, :content, :_destroy])
end

Finally we can delete the questions through checking the checkboxes.

Editing Answers

Now, we also want to edit each question’s answers.

Just like before, we can call accepts_nested_attributes_for method to its associated model - Question:

app/models/question.rb
1
2
3
4
class Question < ActiveRecord::Base
...
accepts_nested_attributes_for :answers, allow_destroy: true
end

Remember again, insert the plural name of associated model, Question model in the strong parameter. Let’s see how to do it:

app/controllers/surveys_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
...
...
...
private
...
...
...
# Never trust parameters from the scary internet, only allow the white list through.
def survey_params
params ... questions_attributes: [:id, :content, :_destroy, answers_attributes: [:id, :content, :_destroy]])
end

Next, add fields for Answer:

app/views/surveys/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
...
<%= f.fields_for :questions do |question| %>
<fieldset>
<%= question.label :content, 'Question' %><br>
<%= question.text_area :content %><br>
<%= question.check_box :_destroy %>
<%= question.label :_destroy, 'Remove question' %>
<%= question.fields_for :answers do |answer| %>
<%= answer.label :content, 'Answer' %><br>
<%= answer.text_field :content %><br>
<% end %>
</fieldset>
<% end %>
...
...

Then Answer model can be managed by the form.

But it it better that we can make our form more neat, we will use render method to do it.

app/views/surveys/_form.html.erb
1
2
3
4
5
6
7
...
...
<%= f.fields_for :questions do |question| %>
<%= render 'question_fields', f: question %>
<% end %>
...
...

And create _question_fields.html.erb file into app/views/surveys folder:

app/views/surveys/_question_fields.html.erb
1
2
3
4
5
6
7
8
9
10
<fieldset>
<%= f.label :content, 'Question' %><br>
<%= f.text_area :content %><br>
<%= f.check_box :_destroy %>
<%= f.label :_destroy, 'Remove question' %>
<%= f.fields_for :answers do |answer| %>
<%= answer.label :content, 'Answer' %><br>
<%= answer.text_field :content %><br>
<% end %>
</fieldset>

render each question’s answers:

app/views/surveys/_question_fields.html.erb
1
2
3
4
5
6
7
8
9
<fieldset>
<%= f.label :content, 'Question' %><br>
<%= f.text_area :content %><br>
<%= f.check_box :_destroy %>
<%= f.label :_destroy, 'Remove question' %>
<%= f.fields_for :answers do |answer| %>
<%= render 'answer_fields', f: answer %>
<% end %>
</fieldset>

create _answer_fields.html.erb into app/views/surveys folder:

app/views/surveys/_answer_fields.html.erb
1
2
3
4
5
6
<fieldset>
<%= f.label :content, 'Answer' %>
<%= f.text_field :content %>
<%= f.check_box :_destroy %>
<%= f.label :_destroy, 'Remove' %>
</fieldset>

Editing Questions and Answers through JavaScript

This step we want to use links to remove questions and answers insted of checkboxes. Here are some steps for this action beforing coding:

  1. Click Remove link.
  2. Hide the Question or Answer fields.
  3. Datas will be updated after submitting.

Firstly, remove checkbox and label which has _destroy attribute. Add a input for _destroy and a link which has remove_fields class.

app/views/surveys/_question_fields.html.erb
1
2
3
4
5
6
7
8
9
<fieldset>
<%= f.label :content, 'Question' %><br>
<%= f.text_area :content %><br>
<%= f.hidden_field :_destroy %>
<%= link_to 'Remove', '#', class: 'remove_fields' %>
<%= f.fields_for :answers do |builder| %>
<%= render 'answer_fields', f: builder %>
<% end %>
</fieldset>

So the <%= f.hidden_field :_destroy %> will generate <input id="..." name="..." type="hidden" value="false"> for us.

And having coffee, when we click links with .remove_fields class, it is triggered these event:

app/assets/javascripts/custom.js.coffee
1
2
3
4
$(document).on 'click', '.remove_fields', (event) ->
$(this).prev('input[type="hidden"]').val(true)
$(this).closest('fieldset').hide()
event.preventDefault()

They means:

  1. Select previous elements of .remove_fields. The filter is type="hidden", and set its value to be true.
  2. Then hide the fieldset element which is closest to .remove_fields.

Do the same thing to Answer fields:

app/views/surveys/_answer_fields.html.erb
1
2
3
4
5
6
<fieldset>
<%= f.label :content, 'Answer' %>
<%= f.text_field :content %>
<%= f.hidden_field :_destroy %>
<%= link_to 'Remove', '#', class: 'remove_fields' %>
</fieldset>

Then we can remove fields by links.

Next, add helper link_to_add_fields, we will make it.

app/views/surveys/_question_fields.html.erb
1
2
3
4
5
6
7
8
9
10
<fieldset>
<%= f.label :content, 'Question' %><br>
<%= f.text_area :content %><br>
<%= f.hidden_field :_destroy %>
<%= link_to 'Remove', '#', class: 'remove_fields' %>
<%= f.fields_for :answers do |builder| %>
<%= render 'answer_fields', f: builder %>
<% end %>
<%= link_to_add_fields 'Add Answer', f, :answers %>
</fieldset>

Here are it look like:

app/helpers/application_helper.rb
1
2
3
4
5
6
7
8
9
10
11
module ApplicationHelper
def link_to_add_fields(name, form, association)
new_object = form.object.send(association).klass.new
id = new_object.object_id
fields = form.fields_for(association, new_object, child_index: id) do |builder|
# render partial
render(association.to_s.singularize + "_fields", f: builder)
end
link_to(name, '#', class: 'add_fields', data: { id: id, fields: fields.gsub("\n", "") })
end
end

We get Question instance from form.object, sending association to it and call klass, we get Answer class, then call new method. Next, we fetch its object id in order to make its field.

After create fields we make a link which has data-id and data-fields attributes. So that we can retrieve fields before .add_fields.

app/assets/javascripts/custom.js.coffee
1
2
3
4
5
$(document).on 'click', '.add_fields', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))
event.preventDefault()

References

Original And New Goal

Today is really special for me, because my Macbook Air 13" is already one year old. I remember that I felt unforgettable as soon as I touched her. I also remember that why did I think she is the one for me, do you want to know why?

Because of Ruby on Rails. I would like to learn that since I visited this blog. Those posts show me how wonderful Rails is, its porpose is to create websites, that’s what I want!

Few days later, I bought the the famous Rails developer, xdite, her book Rails 101 (It’s free now). I impressed by a part of paragraph:

Mac 是最好的 Ruby on Rails 開發環境,馬上買一台!

So I got Macbook immediatrly. I am easy to be incfuenced by someone, but I do not regret to do this. I think this pricy stuff can make money for me soon. But such a pitty, I did not try my best to learn Rails in the last year.

Now, I want to recreate my ambition. I am going to write down my thoughts of learning techniques here to train my writing skill. That’s why I spend afternoon to build this blog using Octopress, you can follow Octopress documentation to create it. If you have no idea why you must create a blog and insist on blogging, I believe Why You Should Write Daily may help you much.

Anyway, let’s blog!

I am Baozi Wu