Tutorial: Using Ajax with Solspace’s Favorites Module for ExpressionEngine
ExpressionEngine addon developer Solspace has created an amazing module called “Favorites”, which allows a site visitor to mark individual articles as their favorites. It is set up to be utilized with Ajax scripting, and what follows is a tutorial on how to do exactly that.
By default, the module Favorites by Solspace works by sending a site visitor to a page that simply displays confirmation phrase, with no styles applied. The entire HTML page might simply say “Saved”, or “Deleted”, depending on the action. This was set up so that the developer could use Ajax to pull in this data right into the other pages on their site.
The End Result
To begin with, I’ll show how this will work in the end. My client was using the module to save recipes to a recipe box. There are several scenarios to think about here:
| Scenario | Message |
|---|---|
| 1. User isn’t logged in | |
| 2. User is logged in, but hasn’t saved the recipe yet. | |
| 3. User just clicked “Save to Recipe Box.” | |
| 4. User returns to an entry they previously saved. | |
| 5. User just clicked “Delete” |
|
As you can see, there are a number of scenarios that we have to account for. The methodology that I used was to write out all of those messages and links in the template, and use CSS to hide them depending on which ones were relevant. I needed to use Ajax to pull in some of the messages, such as “Saved” and “Deleted”, from the Favorites template that I set up according to Solspace’s instructions. Not only does pulling in that template’s code provide a status message, but it also is necessary to actually do the saving and deleting from the user’s recipe box.
The Template Code
{if logged_in}
{exp:favorites:saved}
{if not_saved}
<span class="favorited">
<span class="Favorites_Status"></span>
<a class="Favorites_Save_Full" href="{permalink="forms/favorite-add/"}">save to recipe box</a>
<a class="Favorites_Save Favorites_Trigger" style="display:none;" href="{permalink="forms/favorite-add/"}">save</a>
<a class="Favorites_Delete Favorites_Trigger" style="display:none;" href="{permalink="forms/favorite-add/delete/"}">delete</a>
</span>
{/if}
{if saved}
<span class="favorited">
<span class="Favorites_Status">Saved</span>
<a class="Favorites_Save Favorites_Trigger" style="display:none;" href="{permalink="forms/favorite-add/"}">save</a>
<a class="Favorites_Delete Favorites_Trigger" href="{permalink="forms/favorite-add/delete/"}">delete</a>
</span>
{/if}
{/exp:favorites:saved}
{if:else}
<a class="login_link" href="#">Login to Save</a>
{/if}
As mentioned previously, each of those scenarios is coded by hand into the template, so you can change the terminology within those links from Save to “add”, “favorite”, etc, depending on your needs. Also, you’ll need to make sure that you have a template set up somewhere with nothing but following code:
{exp:favorites:save}
For this example, the template was in “/forms/favorite-add”.
The JavaScript Code
The Ajax method that I used was based off of the jQuery framework:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('a.Favorites_Save') .click (function() {
var link = $(this).attr('href')
$('.Favorites_Status').load(link, function() {
$('.Favorites_Delete').show();
});
$(this).hide();
return false;
});
$('a.Favorites_Save_Full') .click (function() {
var link = $(this).attr('href')
$('.Favorites_Status').load(link, function() {
$('.Favorites_Delete').show();
});
$(this).hide();
return false;
});
$('a.Favorites_Delete') .click (function() {
var link = $(this).attr('href')
$('.Favorites_Status').load(link, function() {
$('.Favorites_Save').show();
});
$(this).hide();
return false;
});
});
</script>
There are three separate event handlers here that essentially do the following things:
- Wait for a particular link to be clicked
- Dynamically loads that link’s destination via Ajax into the span “Favorites_Status”. Fortunately for us that template only returns a confirmation message (customized in the Favorites Module Control Panel).
- Shows the save or delete link, depending upon which link you just clicked.
- Hides the initial button that you just clicked
- Returns false, so that the user never navigates away from the page, and everything happens dynamically.
The end result should be a smooth, Ajax-powered implementation of Solspace’s Favorites Module.
Optional: Load hidden lines with jQuery
As pointed out by Brian Warren in the comments, you can further improve upon this code by loading the hidden lines with jQuery instead of having them in the HTML by default. This will hide those links from search engines and screen readers.
In order to do this, you simply need to remove the lines from the template that have a style of “display:none”, and load them back in from within the document.ready function:
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
// Load in the hidden content
$('.favorited.not_saved').append('<a class="Favorites_Save Favorites_Trigger" style="display:none;" href="{permalink="forms/favorite-add/"}">save</a>');
$('.favorited.not_saved').append('<a class="Favorites_Delete Favorites_Trigger" style="display:none;" href="{permalink="forms/favorite-add/delete/"}">delete</a>');
$('.favorited.saved').append('<a class="Favorites_Save Favorites_Trigger" style="display:none;" href="{permalink="forms/favorite-add/"}">save</a>');
// Apply the event handlers
$('a.Favorites_Save') .click (function() {
var link = $(this).attr('href')
$('.Favorites_Status').load(link, function() {
$('.Favorites_Delete').show();
});
$(this).hide();
return false;
});
$('a.Favorites_Save_Full') .click (function() {
var link = $(this).attr('href')
$('.Favorites_Status').load(link, function() {
$('.Favorites_Delete').show();
});
$(this).hide();
return false;
});
$('a.Favorites_Delete') .click (function() {
var link = $(this).attr('href')
$('.Favorites_Status').load(link, function() {
$('.Favorites_Save').show();
});
$(this).hide();
return false;
});
});
</script>

Comments (35)
Lee
Dec. 27, 2010
Thank you very much for this.
I could never get the explanations over at SolSpace to work - had your code working in 10 minutes.
Can I be cheeky and ask how I would add a fading to white background color behind clicked links.
AJAX is nice, but it's so easy for people clicking links to think nothing has happened - a bit of subtle visual feedback would be great.
Ryan Battles
Dec. 27, 2010
@Lee, if you want to add a little animation to the interactions, you can change the ".hide()" and ".show()" commands in the javascript code to ".fadeIn()" and ".fadeOut()".
You can even throw a background color in there after they've clicked (like pale yellow), which will also fade out, by adding ".css({backgroundColor: '#FFF58F'}).fadeOut()"
Lee
Dec. 27, 2010
Thanks Ryan.
Andy Johnson
Dec. 27, 2010
This is excellent Ryan. Just what I need for a current site in the works. Now if Solspace would just update the add-on for EE 2.
Thanks!
Lee
Dec. 27, 2010
@Andy, Favorites already works on EE2. http://www.solspace.com/software/detail/favorites/
Andy Johnson
Dec. 27, 2010
@Lee, thanks, you're right. I was thinking of their Rating add-on. Oops.
Doug
Dec. 29, 2010
Thanks, works great on my single entry pages. Listing pages it wants to change the text on all the items. Using it for an on line magazine and we have a page that lists all articles in that issue. Would like to allow users to add favorites from this list page as well as the single entry page.
Ryan Battles
Dec. 29, 2010
Doug, that makes sense. The original code just looked for any div with class "Favorites_Status" and updated the status.
Perhaps in your situation, whenever there is a call to .Favorites_Status, .Favorites_Delete, and .Favorites_Save within the JavaScript, you should replace it with:
$(this).siblings('.Favorites_Status')
Just swap out Favorites_Status with the classname you are replacing.
I haven't tested this, but I think it will work.
Doug
Dec. 29, 2010
Thanks, that did get it working. The strange part is it now calls that template 2 times. If I click on the link it will save it as a favorite but I get the 'already saved as favorite' message. If I delete it says 'no such favorite exists'. It happens slow enough that I can see the message change from saved to already exists.
I was able to get around that by just using CSS to hide the messages (using image icons for the links). However I save, delete, then save the same favorite eventually I get both Favorites_Delete and Favorites_Save to be visible. Even weirder is if I click off the browser widow, then back in it does NOT do that.
Mostly there, just some strange bugs to work out.
Another thank you for this. I'm migrating from the design side of things to development and just getting into JQuery and JS. This has helped me learn (tweaked the code in my singe entry page to do some other animations).
Doug
Dec. 29, 2010
If it helps, the code thats acting oddly in the listing page:
$(document).ready(function() {
$('a.Favorites_Save') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.Favorites_Status').load(link, function() {
$(this).siblings('.Favorites_Delete').show();
});
$(this).hide();
return false;
});
$('a.Favorites_Save_Full') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.Favorites_Status').load(link, function() {
$(this).siblings('.Favorites_Delete').show();
});
$(this).hide();
return false;
});
$('a.Favorites_Delete') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.Favorites_Status').load(link, function() {
$(this).siblings('.Favorites_Save').show();
});
$(this).hide();
return false;
});
});
Doug
Dec. 30, 2010
tweaked the script a bit more and added it to the head (vs above the html it applied to) and its working now.
Ryan Battles
Dec. 30, 2010
Good to hear @Doug, glad this could get you started.
Brian Warren
Jan. 03, 2011
Slick tutorial Ryan. I just wrote up a little post on EE Insider linking to this!
I only have one thing I'd consider doing differently, and please don't consider this snarky or critical, because like I said in the EE Insider post, this would have been a godsend to me when I was trying to do this!
That being said, I'd at least think about having jquery insert the various messages and links that are hidden by default. That way screen readers, search engines and the like don't load them in. What do you think?
Ryan Battles
Jan. 03, 2011
@Brian, thanks for pointing out that tip. I just added an addendum to the tutorial for loading in those lines with jQuery.
Florian
Jan. 06, 2011
This is a great tutorial. Very helpful! thank you for sharing.
quick follow up question.
I have a sidebar in my template which displays the list of favorite items. it would be awesome if this list could also refresh when the favorites are added/deleted. Is there a jquery that I could add for that?
Ryan Battles
Jan. 07, 2011
@Florian, I would try looking at the jQuery documentation for the .hide() method http://api.jquery.com/hide/. There are a variety of ways that this can be done, but much of it would depend on how things are set up on your site.
Christian
Jan. 10, 2011
Hi Ryan
thanks for the excellent tut. I have implemented your suggestions with different add-ons. (delete and mefu) but have trouble with browser caching. I know its not really related to your example exactly, but what type of debug method are you using to overcome browser cache with ajax (reload and browser back button).
Ryan Battles
Jan. 10, 2011
I am not currently using anything special, other than the if/then logic in the EE templates to check what the current status is.
Christian
Jan. 10, 2011
Does your solution work fine in ff3 in regards to caching? I did have trouble with deleting content and by pressing the back button the result re-appeared from the browser cache, but only in FF3. At the moment I am using php header to bypass the issue.
Dmitry A Romanovsky
Apr. 06, 2011
very cool!
dsf
Apr. 08, 2011
zxczxczxczx
Andy
Jul. 14, 2011
Nice one dude. Excellent. Worked first time. I love you because you saved me shit loads of work.
Jeramiah Young
Sep. 03, 2011
This is just outstanding, honestly!! I'm running into an issue that I think has an easy fix. Basically, I have a weblog, and when I use your code, even though I'm specifying the entry id for the favorites, it displays the message "Saved to your Favorites" for all entries, even though it only really saves the one you want. How can I remove it from displaying across all weblog entries?
If you need access to see what I'm talking about I'd be more than happy to provide it.
Ryan Battles
Sep. 04, 2011
@Jeremiah - Earlier in the comments I helped out Doug with the same issue. Try the suggestion in the comments dated Dec. 29th.
It is true, my example was set up to target any div with the same class, and was assuming that you were on a single entry view page. With a slight tweak to your approach as mentioned above, you should be able to get it working on an entry listing page.
Marinka Stewart
Sep. 07, 2011
Thanks for sharing best topic. I am very much interested in the topic. I am really impressed with your blog post.
Jeramiah Young
Sep. 07, 2011
Ryan, thank you for the reply, you are right. I'm sorry, I did see this post but I'm didn't know javascript well enough to understand that that was the solution. In fact, it works like a charm. Thank you!
Kent Lew
Oct. 11, 2011
Ryan -- Thanks for the tutorial. It helped me get Favorites up and running. Somewhat.
I've encountered the same problem with a list of entries as others have.
So, I edited the javascript to include the (this).siblings modification. Basically the code posted by Doug Dec. 30, 2010.
But now, it's no longer dynamically replacing the status message. It's taking me right to the separate add-favorites page.
I don't know enough JS to know what's going on, so I'm stuck. Doug mentioned tweaking the code, but there was no update reported.
Any thoughts on why the favorites_status is no longer updating (which is was before the modification)?
Any help would be appreciated.
Ryan Battles
Oct. 11, 2011
Kent, you are most likely encountering a javascript syntax error. Perhaps a comma, bracket, or semicolon isn't correctly placed. Try to undo things until you have something working again, and perhaps you can pinpoint the line that is causing things to break. The browser's error console is also sometimes helpful in describing a javascript error.
Kent Lew
Oct. 11, 2011
Ryan -- I'll take another look. All I did was add (this).siblings.
All triggers lead to the separate add-favorite template with status message.
Here's the code (note, I did use all lc in my classes)
$(document).ready(function() {
$('a.favorites_save') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.favorites_status').load(link, function() {
$(this).siblings('.favorites_delete').show();
});
$(this).hide();
return false;
});
$('a.favorites_save_full') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.favorites_status').load(link, function() {
$(this)siblings('.favorites_delete').show();
});
$(this).hide();
return false;
});
$('a.favorites_delete') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.favorites_status').load(link, function() {
$(this).siblings('.favorites_save').show();
});
$(this).hide();
return false;
});
});
</pre>
Is the
(this).siblings('favorites_status')the wrong syntax for that part? I only want status replacement on the specific entry, but maybe this is messing up the replacement and that's why I get sent to the page instead?Ideas? Thanks for your attention.
Ryan Battles
Oct. 11, 2011
By no means am I a JavaScript guru, but I know enough to get by with a lot of "try and refresh" until I get it right. I'm probably not going to be able to help you personally with this without having both your html and js to test with. However, I do notice that you don't have a semicolon to close out your variable declaration lines, not sure if this is enough of a problem to render an error in the JavaScript, but I would start there.
Kent Lew
Oct. 11, 2011
Hmm. Not sure which lines you mean. I have basically copied yours and Doug's code. Punctuation and everything.
Do you mean the
var link = $(this).attr('href')lines?I guess I'll keep trying things and floundering around.
Kent Lew
Oct. 11, 2011
Well, I messed around with it and it seems to be working now.
Funny thing is I think I wound up with exactly the same script as I started with. Maybe it was a caching issue with the template pages?
Anyway, it's working now.
Thanks for your help.
Ryan Battles
Oct. 11, 2011
Is it the exact code you pasted above? If not, please post the code that is working for you so that others might be able to use it too. You can wrap the code in [ code ] tags (remove spaces) in order to format it
<p>Like This</p>Kent Lew
Oct. 11, 2011
Actually, after my last comment, I ran a compare and found a missing period from the second
(this).siblingsin the second block.I wouldn't have thought that would break the whole script, but perhaps it did.
Here's the final code with [ code ] tags (but doesn't that collapse the white space? -- I tried <pre> tags earlier, but I guess your comments don't accept those). Feel free to edit my markup if I don't get it right, since I can't preview.
<script type="text/javascript">
$(document).ready(function() {
$('a.favorites_save') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.favorites_status').load(link, function() {
$(this).siblings('.favorites_delete').show();
});
$(this).hide();
return false;
});
$('a.favorites_save_full') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.favorites_status').load(link, function() {
$(this).siblings('.favorites_delete').show();
});
$(this).hide();
return false;
});
$('a.favorites_delete') .click (function() {
var link = $(this).attr('href')
$(this).siblings('.favorites_status').load(link, function() {
$(this).siblings('.favorites_save').show();
});
$(this).hide();
return false;
});
});
</script>
Ryan Battles
Oct. 11, 2011
Thanks for posting! This seems like a common issue, so it's good to have the code sample here for others to use.
Commenting is no longer available for this entry.