Straight to the point: Create a like button that provides a count with no page refresh...
What we'll be using:
- Template
- Feed
- jQuery/AJAX
- Stored Procedure
For this example we will create a product list and attach a like button to each one.
For our buttons I'm going to use Bootstrap and Font-Awesome to easily create the buttons with the appropriate icons so we can focus on the functionality and not methods of styling. You can however use whatever you'd like.
Step 1: Create our Tables
We need two tables for this example... One for our products, and another one for our likes. If you're unsure of how to create a table please refer to other tutorials or the documentation.
Table 1: Product
- ProductID INT
- ProductName VARCHAR(250)
Table 2: ProductLike
- LikeID INT
- ProductID INT
- UserID INT
Note: Depending on your situation, you may want to create a specific table to hold all likes, or branch things out into multiple tables. Once you grasp the concept of how this works the possibilities are endless. You'll be able to take this concept beyond a "like" button.
Now that our tables are complete, go ahead and insert 5 products or so. If you're unsure of how to use forms to insert data please refer to other tutorials or the documentation.
My Dummy Products:
- White shoes
- Red shoes
- Green shoes
- Black shoes
Now that we've created some products let's create a Template and add it our page. Keep in mind the focus of this tutorial is on the like button, so we're going to keep the Template super-simple.
Our Starter Template with a Few Things Removed:
<xmod:Template UsePaging="True" Ajax="False" AddRoles="" EditRoles="" DeleteRoles="" DetailRoles="">
<ListDataSource CommandText="SELECT [ProductID], [ProductName] FROM Product"/>
<HeaderTemplate>
<xmod:ScriptBlock ScriptId="Awesome" BlockType="HeadScript" RegisterOnce="True">
<style type="text/css">
ul.productlist {
list-style: none;
width: 500px;
}
ul.productlist li {
border: 1px solid black;
padding: 50px;
margin-bottom: 20px;
position: relative;
}
</style>
</xmod:ScriptBlock>
<ul class="productlist">
</HeaderTemplate>
<ItemTemplate>
<li>
<h1>[[ProductName]]</h1>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</xmod:Template>
<div>
<xmod:AddLink Text="New" />
</div>
After saving your Template it should look like this:
-
White Shoes
-
Red Shoes
-
Green Shoes
-
Black Shoes
Now lets add a Like Button.
We gave our li elements a position of relative so we can now use position: absolute for our buttons. Open your Template and add the like button. Remember we're using Bootstrap for our buttons and Font-Awesome for our icons, so when you see the icon classes and button classes you'll have to translate this to your own CSS situation.
* Add the following to your css:
ul.productlist .likebutton {
position: absolute;
top: 10px;
right: 10px;
}
* Add the following inside your li element of your ItemTemplate:
<li>
<a class="btn likebutton"><i class="icon-thumbs-up"></i> Like <span class="badge">5</span></a>
<h1>[[ProductName]]</h1>
</li>
Each product should now look like this (Or different depending on how you made your buttons and icons):
Adjusting our Template to produce a valid like count.
Things are looking good so far but we need a valid like count to replace our dummy number that we placed temporarily. Let's create a subquery in our select command that counts the number of likes based on the product.
*In your Template, change this:
<ListDataSource CommandText="SELECT [ProductID], [ProductName] FROM Product"/>
*To this (Changing layout for an easier read):
<ListDataSource CommandText="SELECT
ProductID]
,[ProductName]
,(SELECT COUNT(LikeID) FROM [ProductLike] WHERE dbo.ProductLike.ProductID = dbo.Product.ProductID) AS LikeCount
FROM Product"/>
*And replace our dummy number with the field alias we just created.
*Change:
<span class="badge">5</span>
*To this:
<span class="badge">[[LikeCount]]</span>
All of our products will now have a like count of 0.
Are you itching to make this button do something? Lets create a Feed:
Feeds in simplicity are used to retrieve data, but they can also be used to execute a stored procedure that does lots of awesome stuff and returns data based on that awesome stuff.
The process will be like this:
- Like button is clicked
- Feed is triggered via jQuery/AJAX
- Feed executes a stored procedure
- Stored procedure inserts a like into the database
- Stored procedure grabs the new like count
- Data is sent back to the feed
- Data is returned via on AJAX success
- We replace the clicked button with the new data
- And it all happens lighting fast!
1. Like button is clicked: Lets create the jQuery/AJAX
We added a class to our like button called "likebutton". We need a click even attached to that. We also need to tell the Feed which product is receiving the like. Open your template and make the following modifications:
In your ItemTemplate
, lets put a hidden <span>
tag before the closing of our link that houses the product ID. This will make sense in a moment.
*Change this:
<a class="btn likebutton"><i class="icon-thumbs-up"></i> Like <span class="badge">[[LikeCount]]</span></a>
*To this:
<a class="btn likebutton"><i class="icon-thumbs-up"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
*And at the bottom of your Template, lets use the <xmod:jQueryReady> tag to create our jQuery/AJAX call:
<xmod:jQueryReady>
// We'll use jQuery's .on because we'll need to be able to execute this click event after the DOM changes. We initially attach our click event to an element that is static, meaning it isn't going to be changed dynamically via AJAX. This will make sense later.
// Select the li ---- When the likebutton is clicked within the li
$('ul.productlist li').on('click', '.likebutton', function() {
// Lets create a variable for our productID. This is why we inserted that span tag with display set to none. We'll also parse it as an integer value so our stored procedure is happy.
var productID = parseInt($(this).find('.prod-id').text());
$.ajax({ url: '/DesktopModules/XModPro/Feed.aspx',
// <-- We're sending data to this URL.
type: 'POST',
// <-- We are using FORM POST method.
dataType: 'html',
// <-- The data type we're dealing with is HTML.
data: { "xfd" : "Like_Insert",
// <-- This is the name of our feed which we'll create in a moment.
"pid" : 0,
// <-- This is our portal ID.
"prodid" : productID
// <-- This is our product ID.
}, success: function(data) {
// Inside of our success function we'll be doing some awesome things shortly.
}
});
});
</xmod:jQueryReady>
2. Feed is triggered via jQuery/AJAX: Lets create our Feed!
Your Feed should look like this...
<xmod:Feed ContentType="text/html" ViewRoles="Registered Users">
<ListDataSource CommandText="Like_Insert" CommandType="StoredProcedure">
<Parameter Name="ProductID" Value='[[Form:prodid]]' DataType="Int32" />
<Parameter Name="UserID" Value='[[User:ID]]' DataType="Int32" />
</ListDataSource>
<HeaderTemplate></HeaderTemplate>
<ItemTemplate></ItemTemplate>
<FooterTemplate></FooterTemplate>
</xmod:Feed>
Lets break it down so we have a clear understanding of what's happening.
- Our AJAX sent two pieces of data that were very important. One was the ProductID and the other was the PortalID. You must supply the the portal ID in the syntax demonstrated in the AJAX call. Feed.aspx requires a Portal ID. The Product ID in the AJAX was called "prodid" and the Portal ID in the AJAX was called "pid".
- In our ListDataSource we are calling a stored procedure named "Like_Insert" which we haven't created yet. We're passing two parameters to the stored procedure. We're using (notice the single quotes instead of double) '[[Form:prodid]]' for the parameter ProductID because we sent a form post and called it prodid. Look at the AJAX above closely and you'll see. We're also using XMP Usert token '[[User:ID]]' for the UserID parameter to use the current authenticated user's ID.
- Also notice we secured our feed with ViewRoles="Registered Users" so that only registered users can use the like button. We're sending the UserID to the database, so it's a must.
3-5. Feed executes a stored procedure, inserts a new like, and returns the new like count: Lets create that stored procedure now!
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[Like_Insert]
@ProductID INT -- This is the ID of the product our feed sends to this stored procedure
,@UserID INT -- This is the ID of the user who clicked the like button
AS
BEGIN
SET NOCOUNT ON;
-- Lets insert a like into the database!
-- But first we want to be sure that there's no way the same user can create two likes for a single product.
IF NOT EXISTS (SELECT [UserID] FROM [ProductLike] WHERE [UserID] = @UserID AND [ProductID] = @ProductID)
BEGIN
INSERT INTO [ProductLike] (
[ProductID]
,[UserID]
) VALUES (
@ProductID
,@UserID
)
-- But wait... We're not done. We need to return the new count back to our feed.
-- Feeds by nature retrieve data, so we have to send it something or we'll cause errors. And we want to return the same product ID along with the new count.
SELECT
ProductID
,(SELECT COUNT(LikeID) FROM [ProductLike] WHERE [ProductID] = @ProductID) AS LikeCount
FROM ProductLike WHERE ProductID = @ProductID
END
END
GO
6. Data is sent back to the Feed: Lets give that data a home!
Open your feed back up... Things are getting exciting now!
Here comes the cool part. Our button needs to change appearance after all this stuff is over with, so we're going to create a DIFFERENT button that will be used to represent when a user liked a product, and we'll use our new count as well!
*Replace this:
<ItemTemplate></ItemTemplate>
*With this:
<ItemTemplate>
<a class="btn unlikebutton">
<i class="icon-check"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</ItemTemplate>
Couple things to notice... We changed the button's class name to "unlikebutton" and we changed the icon class to "icon-check". This "item" within the itemtemplate is what is sent back in our success function that we defined in our template... Lets go back to it now!
Open your template back up and lets account for proper positioning for the unlikebutton class.
*Change this:
ul.productlist .likebutton {
position: absolute;
top: 10px;
right: 10px;
}
*To this:
ul.productlist .likebutton, ul.productlist .unlikebutton {
position: absolute;
top: 10px;
right: 10px;
}
*And now in our <xmod:jQueryReady>, lets add a few things:
<xmod:jQueryReady>
$('ul.productlist li').on('click', '.likebutton', function() {
var btn = $(this); // <-- Lets create a variable for the entire button that was clicked
var productID = parseInt($(this).find('.prod-id').text());
$.ajax({
url: '/DesktopModules/XModPro/Feed.aspx',
type: 'POST',
dataType: 'html',
data: {
"xfd" : "Like_Insert",
"pid" : 0,
"prodid" : productID
},
success: function(data) {
// Now we're going to REPLACE the button that we clicked, with the new button returned by our feed!
btn.replaceWith(data);
// the variable data holds EVERYTHING returned by our feed as an object. In our case, it's simply a button.
// so the variable named btn that represented the button that we clicked, is replaced with our new one!
}
});
});
</xmod:jQueryReady>
Now load your page with your template and click the like button on one of your products!
But we have a problem... when we refresh the page it shows the like button again for that product and we don't want that!
Lets open our template back up and use <xmod:Select>
to solve all of our problems!
*Change this:
<ListDataSource CommandText="SELECT
[ProductID]
,[ProductName]
,(SELECT COUNT(LikeID) FROM [ProductLike] WHERE dbo.ProductLike.ProductID = dbo.Product.ProductID) AS LikeCount
FROM Product"/>
*To this:
<ListDataSource CommandText="SELECT
[ProductID]
,[ProductName]
,(SELECT COUNT(LikeID) FROM [ProductLike] WHERE dbo.ProductLike.ProductID = dbo.Product.ProductID) AS LikeCount
,(SELECT COUNT(LikeID) FROM [ProductLike] WHERE dbo.ProductLike.ProductID = dbo.Product.ProductID AND dbo.ProductLike.UserID = @UserID) AS UserLikeCount
FROM Product">
<Parameter Name="UserID" Value='[[User:ID]]' DataType="Int32" />
</ListDataSource>
Now we have two counts... One is for the overall count, and the other is for the authenticated user's count which should never be more than one.
*Now change this:
<li>
<a class="btn likebutton"><i class="icon-thumbs-up"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
<h1>[[ProductName]]</h1>
</li>
*To this:
<li>
<xmod:Select Mode="Standard">
<Case Comparetype="Numeric" Value='[[UserLikeCount]]' Operator="=" Expression="0">
<a class="btn likebutton"><i class="icon-thumbs-up"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Case>
<Else>
<a class="btn unlikebutton"><i class="icon-check"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Else>
</xmod:Select>
<h1>[[ProductName]]</h1>
</li>
So I just clicked all my like buttons and I bet you did too... now I have a new problem. How do I "unlike" something?
We could get super fancy and combine all of this stuff into a single feed and a single stored procedure but for demonstration purposes lets keep it all separated. What? Yes... I'm kidding. Why waste time with extra markup when we don't need it... Let's get super fancy!
Open your Template back up and navigate to your <xmod:jQueryReady>
spot.
*Change your script to the following:
<xmod:jQueryReady>
// Be sure to add .unlikebutton to your list in the .on
$('ul.productlist li').on('click', '.likebutton, .unlikebutton', function() {
var btn = $(this);
var productID = parseInt($(this).find('.prod-id').text());
// We want to know which button is being clicked. We can tell by asking which class it has.
if ( btn.hasClass('likebutton') ) {
var likeType = 1; // <-- To execute a like, we will call it 1.
} else { var likeType = 2; } // <-- To execute an unlike, we will call it 2.
$.ajax({
url: '/DesktopModules/XModPro/Feed.aspx',
type: 'POST',
dataType: 'html',
data: {
"xfd" : "Like_Insert", // <-- You can rename your feed if you want... but it will still serve the same purpose.
"pid" : 0,
"prodid" : productID,
"type" : likeType // <-- We need to send our type to the feed.
},
success: function(data) {
btn.replaceWith(data);
}
});
});
</xmod:jQueryReady>
Now add the following parameter to your Feed...
<Parameter Name="LikeType" Value='[[Form:type]]' DataType="Int32" />
*And change this:
<ItemTemplate>
<a class="btn unlikebutton"><i class="icon-check"></i> Like <span class="badge">[[LikeCount]]<span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</ItemTemplate>
*To this:
<ItemTemplate>
<xmod:Select Mode="Standard">
<Case Comparetype="Numeric" Value='[[LikeType]]' Operator="=" Expression="1">
<a class="btn unlikebutton"><i class="icon-check"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Case>
<Else>
<a class="btn likebutton"><i class="icon-thumbs-up"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Else>
</xmod:Select>
</ItemTemplate>
*Now lets alter our Stored Procedure...
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[Like_Insert]
@ProductID INT
,@UserID INT
,@LikeType INT
AS
BEGIN
SET NOCOUNT ON;
-- Lets execute an insert if the LikeType is 1
IF (@LikeType = 1)
BEGIN
IF NOT EXISTS (SELECT [UserID] FROM [ProductLike] WHERE [UserID] = @UserID and ProductID = @ProductID)
BEGIN
INSERT INTO [ProductLike] (
[ProductID]
,[UserID]
) VALUES (
@ProductID
,@UserID
)
END
END
-- If it is not type 1, it must be type 2
ELSE
BEGIN
DELETE FROM [ProductLike] WHERE [UserID] = @UserID AND [ProductID] = @ProductID
END
SELECT
ProductID
,(SELECT COUNT(LikeID) FROM [ProductLike] WHERE [ProductID] = @ProductID) AS LikeCount
,@LikeType AS LikeType -- And we need to return the type back to the feed so our xmod:Select works properly.
FROM Product WHERE ProductID = @ProductID
-- Notice that we changed our FROM above to Product table instead of ProductLike. This is because no data would be returned if it was a delete, because the ProductID in the ProductLike table would be nonexistent.
END
GO
Now reload your page and click like crazy!
Your buttons should now look like this:
There's still something unsettling about our work so far. We need to make it more obvious that the check inside of the box means it's already liked, and that clicking it will "unlike" the product. Facebook does a couple things. They make the button "appear" disabled and when you hover over it it changes into a X instead of the check. Think we can pull off the same effect? I do!
I'm using Bootstrap, and it's easy to make a button appear disabled without actually disabling it. You can do this however you see fit with your css, or if you're using Bootstrap while following along life will be even easier!
Let's open our Template up again!
*Change this:
<Else>
<a class="btn unlikebutton"><i class="icon-check"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Else>
*To this:
<Else>
<a class="btn unlikebutton" disabled=""><i class="icon-check"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Else>
This gives it the appearance of being disabled, but doesn't actually disable it.
*Now open your Feed and do the same thing...
<Case Comparetype="Numeric" Value='[[LikeType]]' Operator="=" Expression="1">
<a class="btn unlikebutton" disabled=""><i class="icon-check"></i> Like <span class="badge">[[LikeCount]]</span><span class="prod-id" style="display:none;">[[ProductID]]</span></a>
</Case>
Are we done yet? No... lets add one more finishing touch. Lets make the checkmark change to an X on hover and also force to show the pointer for cursor because the disabled trick took that away.
In your Template, add cursor: pointer; to your button classes... Then jump back down to your jQuery!
*Add this beneath your AJAX stuff:
$('ul.productlist li').on('mouseenter', '.unlikebutton', function() {
$(this).find('i').attr('class', 'icon-remove');
});
$('ul.productlist li').on('mouseleave', '.unlikebutton', function() {
$(this).find('i').attr('class', 'icon-check');
});