This project is a simple secret sharing project. I've written two
models: User and Secret. I've also built UsersController and
SessionsController to do login for you.
Write a /users/123/secrets/new form. Use form_for. You'll need to
create a nested route.
Clarification: When you post a secret, you are sharing it to someone's
'wall'. The recipient is going to be the user who's id is in the
url. For example, /users/123/secrets/new is going to create a secret
where User 123 is the recipient. You should not need any hidden
fields or other form fields to accomplish this.
Write a Friendship model to join User to User. Friendship is
one-way in this application. Write a simple Friendships controller
(the only action needed is create, I think). Nest a friendship
resource inside the users resource. Friending someone should be as
simple as POSTing to /users/123/friendship.
On the /users page, list all users, and add a friend button for
each. Make this button remote. When clicked, change the button text to
"Friending..." (use ajax:before), and disable. On ajax:success,
change text to "Friended".
When the template is first rendered, appropriately grey-out the button if the user has already been friended.
NB: To change the text on a clicked button, you will have to find the button. Here are some hints:
- the
Eventobject that is passed to your handler (check both$.onand$.Eventcontains acurrentTargetattribute.- The target is also available as
thisinside the handler.
- The target is also available as
- You can use the
findmethod to do a CSS selector search for elements nested inside a jQuery element. - There are two ways to find a submit button inside a form:
0. Give the button an id or class and look for that.
0. Search for
inputtags where thetypeis"submit". 0. Try both ways. Definitely lookup how to do a CSS search by attribute value.
All things must end; you grow apart. You're still proud of your friend, but you don't stay in touch anymore.
Add a second button, to unfriend a user. You'll need a destroy
action on FriendshipsController.
You now want to the unfriend button to appear when you are friends, and the friend button to appear when you are not. The cleanest way to do this is to:
- Writeboth buttons, display them both.
- Place the two buttons in a div or span, give this a CSS
class of
friend_buttons. Likewise, give your buttons classes offriendandunfriend. - If we are friends, set a second class on your div:
friended. Otherwise, setunfriended - Write a CSS rule so that
.friend_buttons.friended friendisvisibility: hidden. Do likewise for.friend_buttons.unfriended unfriend. - Lastly, your
ajax:successmethod needs only to swap a class (see$.toggleClass).
Notice how the friend/unfriend action has been written in terms of a
nested friendship resource. This is a common pattern: take a verb
action, think of the noun that might be created by that action, and
nest that as a resource. This is one of the secrets to nice, RESTful
designs.
We have a /users/123/secrets/new page that displays a form. I'd like
to be able to post a new secret directly from the /users/123 page.
Move the new.html.erb template into a partial (perhaps
_form.html.erb). Make the form remote. Render the partial in
users/show.html.erb page.
On successful submission, add the new secret to the ul listing all
the secrets. Clear the form so the user can submit again :-)
Let's allow users to tag secrets when they create them. Add Tag and
SecretTagging models. Set up appropriate associations.
Because Secret has_many :tags, :through => :secret_taggings, we
can use Secret#tag_ids=. We saw how to tag a secret with many tags
through a set of checkboxes. But what if there are lots of tags to
choose from? Do we really want to present 100 checkboxes?
Instead, let's present a single select tag. Let's also present a
link "Add another tag". Clicking this link should invoke a JS function
that will add another select tag.
Creating new select tags means you'll have to create option tags:
one for each Tag. To give your JavaScript code access to the list of
Tags, I'd store the JSONified Tags in an HTML script tag:
<script type="application/json" id="tags_json" >
<%= Tag.all.to_json %>
</script>You can then use these client-side via:
var tag_objects = JSON.parse($("#tags_json").html());When the user clicks the "Add another tag" link, we need to insert another select box into the form. Since this involves building HTML to inject into the form, we can use an underscore template.
I would make a partial called secrets/_tag_select.html. Note that I
don't say .erb, since this is going to contain an underscore
template. The template should not be processed server-side; we want to
send the template, unprocessed, to the client, so that it can be
rendered client-side by underscore.
<script type="text/template" id="tag_select">
<% unique_num = (new Date).getTime() %>
<% input_id = "secret_tag_ids_" + unique_num %>
<% name = "secret[tag_ids][]" %>
// code for select and options...
</script>Now, inside your secrets/_form.html.erb partial, I would add:
<!-- we'll learn a better way to include JS templates soon. Promise! -->
<%= File.read("./app/views/secrets/_tag_select.html").html_safe %>
<%= form_for(@secret) do |f| %>
<!-- ... -->
<% end %>Finally, add a link tag, and install a click handler. Your handler could run something like:
function addAnotherTagHandler() {
var template_code = $("#tag_select").html();
var template_fn = _.template(template_code);
var rendered_content = template_fn();
$("ul.tag_selects").append(rendered_content);
}