21 Oct
Posted by ProCOM
on October 21, 2007 – 1:34 pm - 1,302 views
If you're new here, you may want to subscribe to my RSS feed. So that you can read the latest updates about Web2.0 tools, Making Money Online, Tips in SEO, Ajax and many more. Thanks for visiting ProgramimiCOM!
In this conclusion to a two-part article, you’ll learn how to drag and drop slides in our example slideshow to modify it, and how to filter by category. This article is excerpted from chapter six of the book Ruby on Rails: Up and Running, written by Bruce A. Tate and Curt Hibbs (O’Reilly, 2006; ISBN: 0596101325). Copyright © 2006 O’Reilly Media, Inc. All rights reserved. Used with permission from the publisher. Available from booksellers or direct from O’Reilly Media.
Drag and Drop Everything (Almost Everything)
We have already displayed a list of thumbnails of all photos that are in the slideshow and enabled the user to drag them around to rearrange their order in the slideshow. Now let’s add a second list of thumbnails, showing all photos that are not being used in the slideshow.
We’ll let the user add a photo to the slideshow by dragging it from the list of unused photos and dropping it onto the slideshow thumbnails. Similarly, we can enable the user to remove photos from the slideshow by dragging its thumbnail from the slideshow and dropping on the unused photos list. Finally, we’ll allow the user to filter the unused photos list by category.
As you might expect, we can accomplish all that in a very small amount of code. We will add a mere 58 lines of Ruby code to the models and controllers, 47 lines to the view templates, and 16 lines to our CSS stylesheet! Figure 6-4 gives you a preview of how this is going to look when we’re done.
Figure 6-4. Preview of drag-and-drop slideshow editing
Let’s start by updating the slideshow’s edit template. Edit photos/app/views/slideshows/edit.rhtml to look like this:
<h1>Editing slideshow</h1>
<div id=’slideshow-contents’>
<p style=’text-align: center;’><b>Slideshow Photos</b></p>
<div id=’slideshow-thumbs’>
<%= render :partial => ’show_slides_draggable’ %>
</div>
</div>
<div id=’slideshow-photo-picker’>
<p style=’text-align: center;’><b>Unused Photos</b></p>
<div id=’slideshow-photos’>
<%= render :partial => ‘photo_picker’ %>
</div>
</div>
<div id=’slideshow-attributes’>
<p><%= link_to ‘Play this Slideshow’, :action => ’show’, :id => @slideshow %></p>
<div style=’border: thin solid; padding-left: 1em;’>
<p style=’text-align: center;’><b>Attributes</b></p>
<%= start_form_tag :action => ‘update’, :id => @slideshow %>
<%= render :partial => ‘form’ %>
<%= submit_tag ‘Save Attributes’ %>
<%= end_form_tag %>
</div>
<p>
<b>Hint:</b> Drag and drop photos between the
two lists to add and remove photos from the
slideshow. Drag photos within the slideshow to
rearrange their order.
</p>
</div>
<%= drop_receiving_element(”slideshow-contents”,
:update => “slideshow-thumbs”,
:url => {:action => “add_photo” },
:accept => “photos”,
:droponempty => “true”,
:loading => visual_effect(:fade),
:complete => visual_effect(:highlight, ’sortable_thumbs’)
) %>
This file has been almost entirely rewritten, so there are no marked-as-changed lines. You can see that I have laid out this edit page into three sections:
<div id=’slideshow-contents’> . . . </div>
<div id=’slideshow-photo-picker’> . . . </div>
<div id=’slideshow-attributes’> . . . </div>
Only the slideshow-photo-picker is new. It shows the list of unused photos that can be added to the slideshow. We will set up the CSS stylesheet to display these sections side-by-side as you saw them in Figure 6-4.
slideshow-contents is rendered by the partial template show_slides_draggable,
slideshow-photo-picker is rendered by the partial template photo_picker, and slideshow-attributes is mostly rendered by the form partial template that was generated from the scaffolding. I say “mostly” because I added a few things inline around the rendering of form.
Finally, notice two Ajax related helpers: drop_receiving_element and observe_field. We’ll come back to these in a little bit after we have discussed some prerequisite details.
Now, make these changes to photos/app/controllers/slideshows_controller.rb, replacing the edit method and creating the unused_photos method:
def edit
@slideshow = Slideshow.find(params[:id])
session[:slideshow] = @slideshow
@photos = unused_photos(@slideshow)
end
def unused_photos(slideshow)
all_photos = Photo.find(:all)
candidates = []
for photo in all_photos
in_slideshow = false
for slide in slideshow.slides
if slide.photo.thumbnail === photo.thumbnail
in_slideshow = true
break
end
end
candidates << photo if not in_slideshow
end
return candidates
end
The purpose of this code is to retrieve all the data needed by the edit.rhtml view template:
@slideshow = Slideshow.find(params[:id])
The id of the slideshow that you want to edit is passed in the request parameters from the browser. Here you retrieve that id and read that slideshow from the database, which you store in the instance variable @slideshow to make it available to the view template.
session[:slideshow] = @slideshow
Ajax actions requests will be coming in as the user makes changes, and you need to know what slideshow to change. This line saves a reference to the slideshow in the session hash. I’m using a key value of :slideshow to save and retrieve this from the session, but that value is arbitrary and could have been any unique identifier.
@photos = unused_photos(@slideshow)
This line calls the new method unused_photos to retrieve a list of all photos that are not in the slideshow; it then saves that list in @photos.
def unused_photos(slideshow)
This method returns a list of photos that are not in the slideshow. The logic should be self-explanatory. First, create an empty array (candidates = []), and then iterate through the list of all photos, adding them to the array (candidates << photo) if they are not already in the slideshow. The technique used here is grossly inefficient, but it will suffice for our purposes.
We still need to create the photo_picker template that generates the HTML to display all the photos that can still be added to a slideshow, so go ahead and create the file photos/app/views/slideshows/_photo_picker.rhtml with this in it:
<% for photo in @photos %>
<%= image_tag(”photos/#{photo.thumbnail}”,
:style => “vertical-align: middle”,
:id => “photo_#{photo.id}”,
:class => “photos”) %>
<%= draggable_element “photo_#{photo.id}”, :revert => true %>
<% end %>
This template iterates through the list of photos in @photos. For each photo, it uses the image_tag helper to create an HTML image tag and the draggable_element helper to generate the JavaScript code that makes it draggable. You can see that the first parameter of draggable_element matches the value of the id attribute (:id => “photo_#{photo.id}”) on the image tag. The draggable_element helper expects the id of the HTML element that it should make draggable, followed by zero or more options. The single option used here (:revert => true) says to move the element back to its original position after it is dropped.
But where can these draggable images be dropped? Recall that at the end of the slideshow’s edit.rthtml template we had:
<%= drop_receiving_element(”slideshow-contents”,
:update => “slideshow-thumbs”,
:url => {:action => “add_photo” },
:accept => “photos”,
:droponempty => “true”,
:loading => visual_effect(:fade),
:complete => visual_effect(:highlight, ’sortable_thumbs’)
) %>
Just like the draggable_element helper, the drop_receiving_element helper expects the ID of the HTML element onto which you can drop something that was declared as draggable. The remaining parameters are options that given as name/value pairs (the order is not important). These options are doing a lot, so let’s go through them one at a time:
:update => “slideshow-thumbs”
This gives the ID of the HTML element that should be updated when a photo is dropped on our slideshow-contents div. The :position and :url options say how, and with what, that HTML element should be updated. When the :position option is omitted (as it is here), the HTML returned from the server replaces the target elements HTML. The :position option says that the returned HTML should be inserted into target element, instead of replacing it. The value :position can be specified as :before, :top, :bottom, and :after.
:url => {:action => “add_photo” }
This option constructs the URL that is sent to the server (via a background Ajax request) when a photo is dropped (you’ve seen this before). This executes the add_photo method in the current controller (the SlideshowsController). The add_ photo action adds the dropped photo to the slideshow and returns an HTML fragment that will replace the existing HTML in the target element, which, as you will see, is a rerendering of the slideshow’s contents, which now include the added photo.
:accept => “photos”
Without this option, you could drop any draggable element here. However, this line says that only HTML elements that have the class attribute “photos” can be dropped here. Remember that in our photo picker template we gave each photo class attribute of “photos”.
:droponempty => “true”
This option says that the user can drop photos here even if the target is completely empty.
:loading => visual_effect(:fade)
:complete => visual_effect(:highlight, ’sortable_thumbs’)
:loading and :complete (plus a few more events) specify client-side JavaScript event handlers that are executed at specific points in the progress of the Ajax request. In both cases, we are displaying a visual effect that gives the user positive feedback. The :loading event occurs when the browser begins loading the response, and the :complete event occurs when its all finished. The code specifies that the dropped photo will fade until it becomes invisible. It also highlights the target area on which the photo was dropped.
Adding a Dropped Photo
Now we need to create the add_photo method to actually add a dropped photo to the slideshow. Edit photos/app/controllers/slideshows_controller.rb, and add this:
def add_photo
slideshow_id = session[:slideshow].id
photo_id = params[:id].split(”_”)[1]
slide = Slide.new()
slide.photo_id = photo_id
slide.slideshow_id = slideshow_id
if !slide.save
flash[:notice] = ‘Error: unable to add photo.’
end
@slideshow = Slideshow.find(slideshow_id)
session[:slideshow] = @slideshow
render_partial ’show_slides_draggable’
end
Let’s walk through this code:
slideshow_id = session[:slideshow].id
This line retrieves the current slideshow from the session hash and gets the slideshow’s id.
photo_id = params[:id].split(”_”)[1]
The id attribute of the dropped photo get passed as the :id parameter. If you recall from the photo_picker template, we set those ids to values such as “photo_1″ and “photo_19″, so the remainder of this line of code splits the string on the underscore, grabs the second half, and assigns it to photo_id.
The next five lines create a new slide, assign to it the photo id and the slideshow id, and then save it to the database.
Finally, we render and return the show_slides_draggable partial, after setting @slideshow to the current slideshow (which is needed by the partial template).
All that code handles dragging new photos to add to the slideshow. Now we just need to add a little more code to implement dragging a photo from the slideshow to the unused photos list as an intuitive way to remove photos from the slideshow.
The displayed list of photos in the slideshow are already draggable because we made them into a sortable list. The only problem with the current implementation is that the photos can be dragged vertically only. They need to be dragged both vertically for reordering and horizontally to the unused photos column.
We can drag the photos only vertically because the default option for a sortable list is :constraint => ‘vertical’. Fortunately, you can change this by editing the file photos/app/views/slideshows/_show_slides_draggable.rhtml and changing the call to the sortable_element helper to add this :constraint option:
<%= sortable_element(’sortable_thumbs’,
:url => {:action => ‘update_slide_order’},
:constraint => ”) %>
Now you can drag those photos anywhere. But you still need to make the unused photos list into a drop receiver that uses Ajax to remove the dropped photo from the slideshow.
To do so, edit photos/app/views/slideshows/edit.rhtml, and add this at the end:
<%= drop_receiving_element(”slideshow-photo-picker”,
:update => “slideshow-photos”,
:url => {:action => “remove_slide” },
:accept => “slides”,
:droponempty => “true”,
:loading => visual_effect(:fade),
:complete => visual_effect(:highlight, ’slideshow-photos’)
) %>
This code is almost identical to the other drop_receiving_element we used. The difference is that the target is the slideshow-photo-picker, and the action taken on a drop is to call the remove_slide method. Also, notice that you can drop only “slides” here (that is, HTML elements with a class attribute of slides). If you go back and take a look at how we defined the partial template photos/app/views/ slideshows/_show_slides_draggable.rhtml, you will see that we did, indeed, make each item in the sortable list a slide.
Add the remove_slide method to photos/app/controllers/slideshows_controller.rb:
def remove_slide
slideshow_id = session[:slideshow].id
slide_id = params[:id].split(”_”)[1]
Slide.delete(slide_id)
@slideshow = Slideshow.find(slideshow_id)
session[:slideshow] = @slideshow
@photos = unused_photos(@slideshow)
render_partial ‘photo_picker’
end
In this code, you get the id of slide you want to remove, and then delete it from the slide database table. Remember, this action does not delete the photo from the database. The slide data says what photos are in a given slideshow, and deleting an entry from the slide table removes that slide from its slideshow. Finally, you render the HTML for the photo picker, which now includes the removed slide.
I’ll bet you’re anxious to see all this in action. All you need to do is to update the style sheet and then try it out. Edit photos/public/stylesheets/slideshows.css, and add the following:
#slideshow-photo-picker {
float: left;
width: 10em;
text-align: center;
border-right: thin solid #bbb;
padding: 0.50em;
padding-bottom: 10em;
}
img.thumbnail {
border: 2px solid black;
margin-bottom: 1em;
}
img.photos {
border: 2px solid black;
margin-bottom: 1em;
}
Whew! That’s it: try it now!
The first thing you’ll notice is that the Unused Photos section is empty (see Figure 6-5). That’s because all the photos are currently in the slideshow. Just drag a few of the slides out of the slideshow and drop them into the Unused Photos column; then you’ll have something more like Figure 6-6.
Filtering by Category
Displaying all unused photos might seem acceptable right now, but we have only nine photos. If there were 900, it would quickly become unusable. So, our final feature in this chapter will be to display only the unused photos in a particular category.
The first thing to do in our controller is get a list of all categories that can populate the drop-down selection box. Edit photos/app/controllers/slideshows_controller.rb, and add this line to the end of the edit method:
@all_categories = Category.find(:all, :order=>”name”)
This line retrieves a list of categories that can populate a drop-down selection box that the user will use to display only those unused photos that are in the selected category.
Figure 6-5. Drag and drop add and remov
Now, edit photos/app/views/slideshows/edit.rhtml, and add this right after the ‘Play this Slideshow’ line:
<p>
<label for=”category_id”>Filter “Unused Photos” on this Category</label><br/>
<%= collection_select(:category, :id, @all_categories, :id, :long_name) %>
<%= observe_field(:category_id,
:frequency => 2.0,
:update => ’slideshow-photos’,
:url => { :action => ‘change_filter’},
:with => ‘category_id’ ) %>
</p>
The collection_select helper is normally used inside an HTML form, but here we are using it because it conveniently knows how to display a collection in a drop-down box. It will never be submitted as part of a form.
As shown, the observe_field helper checks the category drop-down box for changes every two seconds. When a change is detected, an Ajax request is fired off to the change_filter method, which returns new HTML (that has been appropriately
filtered) to replace the slideshow-photos section.
Figure 6-6. Some unused photos
The Category model class automatically shows a collection of all photos that are in a particular category. However, we need to get a collection of photos that are in a given category and in all of its child categories.
Edit photos/app/models/category.rb, and add this method:
def photos_including_child_categories
result = photos.clone
children.each do |c|
c.photos_including_child_categories.each {|p|
result << p if not result.include? p}
end
result
end
This method recursively collects a list of all photos in its own category and all of its child categories. You can use this in to get the list of unused photos to display.
In the meantime, edit photos/app/controllers/slideshows_controller.rb to add the change_filter method:
def change_filter
slideshow_id = session[:slideshow].id
category_id = params[:category_id] || 1
session[:category_id] = category_id
@slideshow = Slideshow.find(slideshow_id)
session[:slideshow] = @slideshow
@photos = unused_photos(@slideshow)
render_partial ‘photo_picker’
end
This method stores the chosen category id in the session hash, retrieves a new list of unused photos, and then renders the photo_picker. Notice the bold code line in the previous code. This line tries to retrieve the category id from the request parameters. If there aren’t any parameters, params[:category_id] returns nil, and the || operator returns the rightmost argument (”1″ in this case).
Also, in this slideshow controller, we need to update the method that retrieves the unused photos to pay attention to the category setting. Do so by editing the unused_ photos method; then replace the line all_photos=Photo.find(:all) with the following:
category_id = session[:category_id] || 1
session[:category_id] = category_id
category = Category.find(category_id)
all_photos = category.photos_including_child_categories
We’re done; we’ve added category filtering! Fire up your browser, and try it (you may need to assign some categories to some unused photos). Now it looks like Figure 6-7.
Figure 6-7. Filtering on categories
We’ve come a long way in a very short time. With fewer than 200 lines of code, we’ve added drag-and-drop capability to add and reorder slides. We’ve also added the core capability to actually show a slideshow. Ajax made our application much easier to use and more attractive. Next, we’ll look into testing this application.
—
by O’Reilly Media
Print This Post
Email This Post
Comments RSS
TrackBack Identifier URI
You must be logged in to post a comment.