We’ve created articles from the console, but that isn’t a viable long-term solution. The users of our app will expect to add content through a web interface. In this iteration we’ll create an HTML form to submit the article, then all the backend processing to get it into the database.
Previously we setup the map.resources :articles route in routes.rb, and that told Rails that we were going to follow the RESTful conventions for this model named Article. Following this convention, the URL for creating a new article would be http://localhost:3000/articles/new. Enter that into your browser and see what comes up.
Unknown action No action responded to new. Actions: index
This is an error message we’ve seen before. The router went looking for an action named new inside the articles_controller and didn’t find it. For our convenience the message lists the actions that are available — the only one being the index we created in I0.
So first let’s create that action. Open /app/controllers/articles_controller.rb and add this method structure, making sure it’s inside the ArticlesController class, but outside the existing index method:
def new
end
With that defined, refresh your browser and you should get this:
Template is missing Missing template articles/new.erb in view path app/views
Again, an error message we saw in I0. Create a new file /app/views/articles/new.html.erb with these contents:
<h1>Create a New Article</h1>
Refresh your browser and you should just see the heading “Create a New Article”.
It’s not very impressive so far — we need to add a form to the new.html.erb so the user can enter in the article title and body. Because we’re following the RESTful conventions, Rails can take care of many of the details. Inside that erb file, enter this code below your header:
<% form_for(@article) do |f| %>
<p>
<%= f.label :title %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit 'Create' %>
</p>
<% end %>
What is all that? Let’s look at it piece by piece:
form_for is a Rails helper method which takes one parameter, in this case @article and a block with the form fields. The first line basically says “Create a form for the object named @article, refer to the form by the name f and add the following elements to the form…”f.label helper creates an HTML label for a field, this is good usability practice and will have some other benefits for us laterf.text_field helper creates a single-line text box named titlef.text_area helper creates a multi-line text box named bodyf.submit helper creates a button labeled “Create”Refresh your browser and you’ll see this:
RuntimeError in Articles#new Showing app/views/articles/new.html.erb where line #3 raised: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id Extracted source (around line #3): 1: <h1>Create a New Article</h1> 2: 3: <% form_for(@article) do |f| %> 4: <%= f.error_messages %> 5:
What’s it trying to tell us. In our new.html.erb on line #3 there was an error about “Called id for nil”. As we learned in the Ruby in 100 minutes tutorial, nil is Ruby’s way of referring to nothingness. Somewhere in line #3 we’re working with an object that doesn’t exist.
And since there’s only one object in line #3, it makes it pretty obvious — the problem is that we started talking about a thing named @article without ever creating that thing. Rails uses some of the reflection techniques that we talked about earlier in order to setup the form. Remember in the console when we called Article.new to see what fields an Article has? Rails wants to do the same thing, but we need to create the blank object for it.
Go into your articles_controller.rb, and inside the new method, add this line:
@article = Article.new
Then refresh your browser and your form should come up. Enter in a title, some body text, and click CREATE.
You’re old friend pops up again…
Unknown action No action responded to create. Actions: index and new
When we loaded the form we accessed the new action, but when that form is submitted to the application, following the REST convention, it goes to a create action. We need to create that action. Inside your articles_controller.rb add this method (again, inside the ArticlesContoller class, but outside the other methods):
def create
@article = Article.new(params[:article])
@article.save!
redirect_to articles_path
end
This method says…
@article and send in the parameter params[:article]params. If were to look at params as a data structure, it’d be a hash with only one key — :article. The value of that pair is another hash with keys :title and :body. The values for those keys are the data we entered into the text boxes on the form. So when Article.new is called and the hash params[:article] is passed in, the new method looks for the value with key :title and puts that into the Article’s title attribute. Then it looks for the value for key :body and puts that into the article’s body attribute.@article.save! saves the object to the database, just like we did in the console. We’ve added the exclamation mark here because we haven’t implemented any error checking. The normal save method will “fail silently” if there’s a problem saving the article, but the save! method will raise errors and basically freak out.redirect_to tells Rails that we don’t want to render a view for this action. Once the previous steps are done, we want to bounce to the list of all articles. The router generates many friendly path-related variables for us just from the simple map.resources :articles declaration. One of them is the articles_path we use here — it’ll resolve to http://localhost:3000/articles/.Go back in your browser so you get to the form with the sample data you entered and click CREATE. You should then bounce to the full articles list with your new article added.
Right now our article list is very plain and we end up typing in a bunch of URLs by hand. Let’s add some links. Open your /app/views/articles/index.html.erb and…
<%= link_to "Create a New Article", new_article_path %>link_to helper, tells it we want a link with the text “Create a New Article” that points to the address new_article_path (which the router handles for us)article.title. Change it so it says link_to article.title, article_path(article). This creates a link with the text of the articles title which points to a page where we’ll show just that article.Refresh your browser and you should now see a list of just the article titles that are linked somewhere and a link at the bottom to “Create a New Article”. Test that this create link takes you to the new article form. Then go back to the article list and click one of the article titles.
Tired of this error message yet? Go to your articles_controller.rb and add a method like this:
def show end
Refresh the browser and you’ll get the “Template is Missing” error. Let’s pause here before creating the view template.
Look at the URL: http://localhost:3000/articles/1. When we added the link_to in the index and pointed it to the article_path for this article, the router created this URL. Following the RESTful convention, this URL goes to a SHOW method which would display the Article with ID number 1. Your URL might have a different number depending on which article title you clicked in the index.
So what do we want to do when the user clicks an article title? Find the article, then display a page with it’s title and body. We’ll use the number on the end of the URL to find the article in the database. The router will send us this number in the variable params[:id]. Inside the show method that we just created, add this line:
@article = Article.find(params[:id])
Now create the file /app/views/articles/show.html.erb and add this code:
<h2><%= @article.title %></h2> <p><%= @article.body %></p> <%= link_to "<< Back to Articles List", articles_path %>
Refresh your browser and your article should show up along with a link back to the index.
We can create articles and we can display them, but when we eventually deliver this to less perfect people than us, they’re going to make mistakes. Right now there’s no way to edit an article once it’s been created. There’s also no way to remove an article. Let’s add those functions.
Look at your index.html.erb and change the whole <li> segment so it looks like this:
<li>
<b><%= link_to article.title, article_path(article) %></b><br/>
<i>Actions:
<%= link_to "edit", edit_article_path(article) %>,
<%= link_to "remove", article, :method => :delete,
:confirm => "Remove the article '#{article.title}'?" %>
</i>
</li>
The first link we added, for edit, is pretty similar to what we’ve done before — creating a link with the text “edit” pointing to the address edit_article_path, which is defined by the router, and editing the thing named article.
The second one is a little more complex. Web browsers don’t yet properly implement all the REST conventions, so Rails creates a hack for destroying objects. The details aren’t too important. So this link will have the text “remove”, will point to the article, and will use the HTTP method “delete”. We’ve also added a :confirm parameter. If a link has a :confirm, then Rails will generate some Javascript which will popup a box when the link is clicked that contains the text in the :confirm. Here we’re setting the message to check that the user wants to remove the article and including the article’s title in the message.
Refresh your browser and you should see “edit” and “remove” links for each article. Click the EDIT link for your first article.
The router is expecting to find an action in articles_controller.rb named edit, so let’s add this:
def edit
@article = Article.find(params[:id])
end
All the edit action is really going to do is find the article to be edited, then display the editing form. If you refresh after adding that edit action you’ll see the template missing error. Create a file /app/views/articles/edit.html.erb but hold on before you type anything. Below is what the edit form should look like:
<h1>Edit an Article</h1>
<% form_for(@article) do|f| %>
<%= f.error_messages %>
<p>
<%= f.label :title %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit 'Update' %>
</p>
<% end %>
In the Ruby and Rails communities there is a mantra of “Don’t Repeat Yourself” — but that’s exactly what I’ve done here. This view is basically the same as the new.html.erb — the only changes are the H1 and the name of the button. We can abstract this form into a single file called a partial, then reference this partial from both new.html.erb and edit.html.erb.
Create a file /app/views/articles/_form.html.erb and, yes, it has to have the underscore at the beginning of the filename. Go into your /app/views/articles/new.html.erb and CUT all the text from and including the form_for line all the way to it’s end. The only thing left will be your H1 line. Then add the following code at the bottom of that view:
<%= render :partial => 'form' %>
Now go back to the _form.html.erb and paste the form code. Change the text on the submit button to say “Save” so it makes sense both when creating a new article and editing and existing one.
Then look at your edit.html.erb file, write an H1 header saying “Edit an Article”, then use the same code to render the partial named form.
Go back to your articles list and try creating a new article — it should work just fine. Try editing an article and you should see the form with the existing article’s data — it works OK until you click SAVE.
The router is looking for an action named update. Just like the new action sends it’s form data to the create action, the edit action sends it’s form data to the update action. In fact, within our articles_controller.rb, the update method will look very similar to create:
def update
@article = Article.find(params[:id])
@article.update_attributes(params[:article])
@article.save!
redirect_to article_path(@article)
end
The only new bit here is the update_attributes method. This method works very similar to when we called the Article.new method and passed in the hash of form data. When we call update_attributes on the @article object and pass in the data from the form, it changes the values in the object to match the values submitted with the form. Then we save the object to the database and redirect to the articles list.
Now try editing and saving some of your articles.
Next, click the REMOVE link for one article and hit OK. You can see that the router is expecting there to be a destroy action. Go into articles_controller.rb and add a destroy method like this:
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path
end
Here we’re doing a find based on params[:id] like we did in the show action. We call that object’s destroy method, then redirect back to the articles list.
Try it out in your browser.
It would be nice, though, if we gave the user some kind of status message about the operation that took place. When we create an article the message might say “Article ‘the-article-title’ was created”, or “Article ‘the-article-title’ was removed” for the remove action. We can accomplish this with a special object called the flash.
Rails creates the object named flash, so we don’t need to do anything to set it up. We can start by integrating it into our index.html.erb by adding this line at the very top:
<div class="flash"><p><%= flash[:message] %></p></div>
This just outputs the value stored in the flash object with the key :message. If you refresh your articles list you won’t see anything because we haven’t stored a message in there yet. Look at articles_controller.rb and add this line right after the save! line in your create method:
flash[:message] = "Article '#{@article.title}' was created."
Then go to your articles list, create another sample article, and when you click create you should see the flash message at the top of your view.
Here’s something cool about how Rails handles the flash — hit your browser’s REFRESH button while looking at the articles list. See how the flash disappears? Once you display the message in a flash Rails clears it out. That’s why it’s perfect for status messages like this.
Similarly, add a flash message into your destroy method and confirm that it shows up when an article is removed. Then add one to your update method that’ll display when an article is edited.
And, finally, you’re done with I1!