MarkUs Blog

MarkUs Developers Blog About Their Project

New Notables

without comments

The other evening I worked on the New Notes page.

The current one (left) deals only with Groupings. First you select the assignment, and then you select the grouping that you want to write the note about. It dynamically updates the Groupings drop down with the associated group names when you select an Assignment.

This is very good behaviour!

The task I set for myself was to extend this type of thing to our new noteables, Assignments and Students.

One way about it would be to have a separate form link for each type, before getting to the proper form. That has the downside of requiring additional page loads and then you need to start back at the hub page if you change your mind about what you want to write notes about.

The approach that I’m taking is to have an additional drop box where you first select the Notable type, and the rest of the form changes as required to select the exact object you want to create the note on. This way, less page loads, and hopefully, less work for me, as I don’t wish to refactor the entire notes system for a minor bit of convenience.

What convenience and I talking about? Well, it’s entirely possible to write a completely general system that automatically creates the form I want whenever we add the notable property to a model. However, that would take me longer than I have left in the term to do properly, and further isn’t really necessary. Such a solution would be the epitome of the Rails DRY philosophy.

It’s not a good idea though. When adhering to a philosophy involves more work than the less elegant but simpler solution, it’s not the solution you want to implement.

So my approach will be thus: have a static list of notable types and partials forms we want to render. We have one partial form for each, and whenever we observe the Notables box changing, we use a little bit of AJAX magic to render the new part of the form.

With that said, let’s see what we have now:


<div id="title_bar"><h1><%=I18n.t('notes.new.title')%></h1></div>
<div>
<% form_for :note, :url => {:action => 'create'} do |f| %>
<%= f.error_messages %>
<fieldset>
<p>
<%= f.label :assignment_id %>
<%= select_tag "assignment_id", options_from_collection_for_select(@assignments, :id, :short_identifier),
\:onchange => remote_function(:url => { :action => 'new_update_groupings' },
:with => 'Form.Element.serialize(this)',
:before => "Element.show('loading_groupings')",
:success => "Element.hide('loading_groupings')") %>
</p>
<p>
<%= f.label :noteable_id, "Grouping" %>
<%= f.select :noteable_id, @groupings.collect {|p| [p.group_name_with_student_user_names,p.id]} %>
<span id="loading_groupings" style="display:none">
<%=image_tag('spinner.gif')%> <%=I18n.t('notes.new.loading_groupings')%>
</span>
</p>
<p>
<%= f.label :notes_message %>
<%= f.text_area :notes_message, :rows => 10 %>
</p>
</fieldset>
<%= f.submit I18n.t('save') %>
<% end %>
</div>

The basic structure is what you’d expect. It creates a form object, and then builds the form in the way you want it to later display. The user can select things, type up a note, and get the end result no problem. Heck, the user can even start writing a note, change the assignment and grouping it’s onand not have to start writing their note over again! That’s slick!

What I want to do is extract out the mechanism to select the noteable object from the form. This isn’t too hard. The selection boxes are what’s the same so I just need to put  those first two things into a file I called _grouping.html.erb . Here’s what it looks like:


<p>
<%= f.label :assignment_id %>
<%= select_tag "assignment_id", options_from_collection_for_select(@assignments, :id, :short_identifier),
\:onchange => remote_function(:url => { :action => 'new_update_groupings' },
:with => 'Form.Element.serialize(this)',
:before => "Element.show('loading_groupings')",
:success => "Element.hide('loading_groupings')") %>
</p>
<p>
<%= f.label :noteable_id, "Grouping" %>
<%= f.select :noteable_id, @groupings.collect {|p| [p.group_name_with_student_user_names,p.id]} %>
<span id="loading_groupings" style="display:none">
<%=image_tag('spinner.gif')%> <%=I18n.t('notes.new.loading_groupings')%>
</span>
</p>

Note that nothing is different from when those lines were in the original file. Then I add one bit of rails magic to replace the lines I removed:


<%= render :partial => 'grouping', :locals => {:f => f} -%>

This line of code does two things. First: it retrieves the partial file I just created, in this case for the Groupings. Second: it passes in the form object we created. That part is important since it allows us to modify the form in the partial, as we need to.

Visually, this produces nothing different. It’s identical to what we saw before. Now for the tricky bit.

A few hours of writing, experimenting and testing later, having gone so far as having it generate Javascript that would be treated as values in the dropdown and then a generic function that dealt with each case, then to the final result here…

You can now select the type of notable!The tricky bit is getting it so we can select the kind noteable we want to write a Note for, and be able to retrieve the right partial, while getting the parts of the form in the partial to work properly. Getting behaviour like this isn’t too hard, but getting it to be reasonably generic and easy to extend is.

The noteables we have right now are fairly straight forward and the two we are adding require only one drop down for selection. We could just replace the list in the dropdown with some RJS magic, and hide the assignment selector when we aren’t selecting a Group.

The problem with that approach is we have separate sets of cases to deal with, and we have to ensure they all work when testing and adding new ones. Also, we have more things to look out for if we want to change how we are selecting the object (ie. filtering Students by Section, rather than a single list of Students). By dealing with all cases uniformly, we reduce the opportunity for error.

The end result for the business bits of the new note form is as follows:


<p>
<%= f.label :noteable_type, I18n.t('notes.noteable')%>
<%= select_tag "noteable_type", options_for_select(Note::NOTEABLES) %>
<%= observe_field :noteable_type, \:on => 'onchange',:url => {:action => :noteable_object_selector},
:with => "noteable_type",
:before => "Element.show('loading_selector')",
:success => "Element.hide('loading_selector')" %>
<span id="loading_selector" style="display:none">
<%=image_tag('spinner.gif')%> <%=I18n.t('notes.new.loading_selector')%>
</span>
</p>
<div id="noteable_selector">
<%= render :partial => 'grouping' %>
</div>

A few things have changed.  We now have a selector that choses between a list of Noteables that I put in the Notes model. Whenever the user changes the selected value, I call this ‘noteable_object_selector’ function in the controller, and flash this little loading box so the user knows that something is reacting to their selection. There is also that “noteable_selector” div towards the bottom that renders the partial for Groupings as default.

The ‘noteable_object_selector’ is actually an RJS call. RJS is a handy mechanism Rails provides to return snippets of Javascript that get executed by the browser immediately when received, usually to do a bit of DOM manipulation, but generally anything that you can get Javascript to do. Here’s what it looks like.



page.replace_html 'noteable_selector', :partial => params[:noteable_type].downcase


It’s sheer elegance in its simplicity! All it does is generate Javascript that replaces the contents of our “noteable_selector” div with the contents of the desired partial. There’s a little bit before that gets the required entries from the DB in the controller, but that’s so we have something to display in the dropdowns in the partials, rather than being magic to explain.

The astute reader will have noticed that I don’t have that “:locals => {:f => f}” bit at the end of the line like I had in the first step. How did I get around not having the form object anymore? The answer is simple. Rails provides exactly a mechanism to get around that. Here’s the partial for selecting Students to demonstrate. The partial for Assignments and Groupings are similar.


<% fields_for :note, :url => {:action => 'create'} do |f| %>
<p>
<%= f.label :noteable_id, "Student" %>
<%= f.select :noteable_id, @students.collect {|p| [p.user_name,p.id]} %>
</p>
<% end %>

The fields_for function does exactly the same thing as form_for except that it doesn’t generate the <form> tags, permitting exactly the behaviour I was aiming for with the locals parameter. We just wrap whatever partial we might want to use as parts of the form in a fields_for and it will generate form elements compatible with the form that we set it up for. Very handy!

Now we have a solution that is relatively simple to extend to new noteables as well as change how noteable objects are selected if we like. Everything is dealt with equally, which is important for testing since we don’t have special cases to deal with.

Written by Robert B

March 16th, 2010 at 11:35 pm

Posted in Uncategorized

Leave a Reply