First Ajax Project
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.
form_for
secrets
Phase I: 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.
button_to
)
Phase II: Add friendships (remote 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
Event
object that is passed to your handler (check both$.on
and$.Event
contains acurrentTarget
attribute.- The target is also available as
this
inside the handler.
- The target is also available as
- You can use the
find
method 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
input
tags where thetype
is"submit"
. 0. Try both ways. Definitely lookup how to do a CSS search by attribute value.
Phase III: Remove friendships
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 offriend
andunfriend
. - If we are friends, set a second class on your div:
friended
. Otherwise, setunfriended
- Write a CSS rule so that
.friend_buttons.friended friend
isvisibility: hidden
. Do likewise for.friend_buttons.unfriended unfriend
. - Lastly, your
ajax:success
method needs only to swap a class (see$.toggleClass
).
Interlude: RESTful design and nested resources
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.
Phase IV: Remote secrets form
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 :-)
Phase V: Simple dynamic form (no nesting)
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.
JSON data script trick
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
Tag
s, I'd store the JSONified Tag
s 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());
Underscore template trick
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);
}