AJAX Actions

Hey guy's.

I've noticed that the actions listed on a page have a little arrow next to them pointing down. This has often made me think that some AJAX-y dialog should reveal itself when this arrow is clicked. Sadly though, this doesn't happen in PP 0.8.

Implementing this shouldn't be too difficult. In fact i have played about with it a bit in little RailsCollab experiment (though the code should be simple enough to be adapted to ProjectPier). Here is what i have so far:

Firstly, i altered the action list a bit. Compared to PP 0.8, the actual link for each action has "id" and "class" set, the importance of which i will get to in a minute.

  <div id="page_actions">
    <ul>
<% @page_actions.each do |action| -%>
   <li><a href="<%= action[:url] %>" class="action" id="action_<%= action[:title] %>"><%= action[:title].l %></a></li>
<% end %>
    </ul>
  </div>

For reference, the @page_actions list looks something like this:

  @page_actions = [{:title => :add_mushroom, :url => '/mushroom/add'}, {:title => :eat_pie, :url => '/pie/eat'}]

(Basically a list of Hashes if you can't read Ruby)

Now for the AJAX. For this i am using Prototype combined with Low Pro, which conveniently allows me to separate out my JavaScript from the HTML.

Remember "id" and "class" i set for the action links? Well with Low Pro, i can attach behaviours to objects based on class, and then construct my AJAX request using the associated id.

For reference, here is the JS code i have come up with:

  var ActionItem = Behavior.create({
  initialize: function()
  {
    ActionItem.last_action = null;
    ActionItem.actions_item = null;
    ActionItem.updater = null;
  },
 
  onclick: function(e)
  {
    var source = Event.element(e);
    Event.stop(e);
   
    if (ActionItem.actions_item)
    {
      // Clear previous selection
      Element.toggleClassName(ActionItem.last_action, 'action_selected');
      ActionItem.actions_item.remove();
      
      if (ActionItem.last_action == source.identify())
      {
        ActionItem.last_action = null;
        ActionItem.actions_item = null;
        return;
      }
    }

    if (source)
    {
      // Set new selection
      ActionItem.last_action = source.identify();
      Element.toggleClassName(ActionItem.last_action, 'action_selected');
      ActionItem.actions_item = new Element('div', {id: 'page_actions_partial'}).update('Loading...');
      $('page_actions').insert(ActionItem.actions_item);
    }
    else
    {
      ActionItem.last_action = null;
      ActionItem.actions_item = null;
      return;
    }

    // Clear in-progress query
     if (ActionItem.updater)
       ActionItem.updater.transport.abort();

     // Grab form partial
     ActionItem.updater = new Ajax.Updater(ActionItem.actions_item, '/actions/' + ActionItem.last_action,
     {
       onSuccess: function()
       {
         new Effect.Highlight(ActionItem.actions_item);
       },
      
       onError: function()
       {
         ActionItem.actions_item.update('Error loading!');
       },
                               
       onComplete: function(transport)
       {
         ActionItem.updater = null;
        
         // Assign form
        
         var form = $(ActionItem.last_action + '_form');
         if (!form)
           return;
        
         form.observe('submit', function(event){
           var element = event.element();
           event.stop();
          
           element.request({
             onComplete: function()
             {
               // Clean up form & selection
               Element.toggleClassName(ActionItem.last_action, 'action_selected');
               ActionItem.actions_item.remove();
              
               ActionItem.last_action = null;
               ActionItem.actions_item = null;
               return;
             }
           });
         });
       }
      
     });
   }
 
  });

  // Bind elements
  Event.addBehavior({
      '.action' : ActionItem()
  });

So now every time you select an action, it pops up a corresponding form contined in a div called'page_actions_partial' which is inserted into 'page_actions'. Or in plain English it appears with a flash below the action list. :)

You might have noticed the url i use to grab the form is "/action/[id]", so the actions i used for reference would grab their forms from "/action/action_add_mushroom" and "/action/action_eat_pie".

The "action" controller in my code is a separate controller (since i was trying to make it a bit simple), with each of its actions implementing a different form. e.g.:

  def action_add_mushroom
    @mushroom = Mushroom.new()
   
    @action_id = 'action_add_mushroom'
    @action_name = 'Add Mushroom'
    @action_url = {:controller => 'mushroom', :action => 'add'}

    render :partial => 'actions/form', :locals => {:action_partial => 'mushroom/form'}
  end

Finally i have the "form" view partial. This is simply:

  <%= form_tag (@action_url, {:id => "#{@action_id}_form", :class => 'action_form'}) %>
    <%= render :partial => action_partial %>
    <button class="submit" type="submit"><%= @action_name %></button>
  </form>

Which basically re-uses the form i would otherwise use for the non-AJAX version (/mushroom/add).

I hope this is of use to someone.

~ James

I think something like this would be a good addition to the 0.8.5 release

Im just switching for aC to PP and I miss some ajax (in completing tasks mostly) and also as a BaseCamp user i can say a little AJAX doesn't hurt, instead it helps a lot!

That would be fantastic! I'd really love to see a more interactive and convenient way for reorganizing the task list for starters :)!

Cool, thx! :)

ACK, AJAX-stuff is important for next release!

if you're already using prototype, why not just drop-in scriptaculous?

i particularly like projectpier because it's straight php and javascript. it is therefore easy to modify and customize.

i would not like projectpier to mutate into some sort of hybrid php/ruby project or to use ruby at all.

once again, if you're already using prototype, you could easily just drop-in the scriptaculous and have the ajax in no time.

citizenkeys, there are no plans to mix in some ruby code. The reason why jamesu talks about some ruby code is because he coded Railscollab, activeCollab's port to Ruby.

As for prototype/scriptaculous, there was some discussion on the mailing list very recently about what JS framework to use. Do not hesitate to chime in if you feel strongly about one framework over the rest.

Tim

i recommend prototype and scriptaculous because they are so well documented and supported. plus prototype and scriptaculous are like peanut butter and jelly. if you go with anything else for the framework, the user interface stuff isn't going to integrate as well.