Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Ask Ben: Creating A Simple Wish List / Shopping Cart

By Ben Nadel on

Someone wanted to create a simple Wish List on their site. This wish list was a single-session "shopping cart" that was not used for checkout. It was used only to keep track of items and to help print out a list that could be brought into a store and given to a store clerk.

For this solution, I have created a ColdFusion component, Cart.cfc, that keeps track of its Items using an internal query. This cart does not have ALL the needed features, but I think it is a pretty good base from which to start. Currently, it only allows you to add items and clear the cart. It does not provide any way to update the quantities directly or delete a given item from the list. However, these can be added with some fairly simple methods that use the Item's PKEY value.

As you can see from the code, there are a few core query columns for the Items query that cannot be altered:

PKEY: The unique ID used for each row which will be used to reference and update a particular row.

NAME: Name of the item in the cart.

SKU: Internal SKU value for inventory.

PRICE: Base cost of the item.

QUANTITY: The number of items that match a particular row of item criteria.

SUB_TOTAL: The cost of the row based on the item price and its quantity.

These are part of the Cart by default. In addition to these columns, you can add custom properties during cart creation (the Init() method call). These properties are added as VARCHAR columns to the internal Items query.

Here is an example of how the Cart.cfc can be used to keep track of items and then to print a list:

<!---
	Create an instance of the cart. When we initialize
	the cart, we are going to add three additional
	item properties.
--->
<cfset objCart = CreateObject( "component", "Cart" ).Init(
	"color, size, sex"
	) />


<!---
	Get an empty-value new item struct from the cart
	itself. We will populate this struct and add it back
	into the cart.
--->
<cfset objItem = objCart.GetNewItem() />

<!--- Now, let's set the value of this new item struct. --->
<cfset objItem.Name = "Girl's Baby Doll T-Shirt" />
<cfset objItem.Price = 15.95 />
<cfset objItem.Quantity = 1 />
<cfset objItem.Color = "Pink" />
<cfset objItem.Size = "M" />
<cfset objItem.Sex = "Female" />

<!--- Add the item back into the cart. --->
<cfset objCart.AddItem( objItem ) />


<!---
	Get an empty-value new item struct from the cart
	itself. We will populate this struct and add it back
	into the cart.
--->
<cfset objItem = objCart.GetNewItem() />

<!--- Now, let's set the value of this new item struct. --->
<cfset objItem.Name = "Men's Boxer Briefs" />
<cfset objItem.Price = 7.95 />
<cfset objItem.Quantity = 3 />
<cfset objItem.Color = "Black" />
<cfset objItem.Size = "M" />
<cfset objItem.Sex = "Male" />

<!--- Add the item back into the cart. --->
<cfset objCart.AddItem( objItem ) />


<!--- Get the items out of the cart. --->
<cfset qItem = objCart.GetItems() />


<!--- Output the items (wish list). --->
<cfloop query="qItem">

	<h4>
		#qItem.Name#
	</h4>

	<p>
		Price: #DollarFormat( qItem.price )#<br />
		Quantity: #qItem.quantity#<br />
		Sub Total: #DollarFormat( qItem.sub_total )#<br />
	</p>

</cfloop>

<h4>
	Cart Overview:
</h4>

<p>
	Size: #objCart.GetSize()#<br />
	Total: #DollarFormat( objCart.GetTotal() )#<br />
</p>

Running the above code, we get the following output:

Girl's Baby Doll T-Shirt
Price: $15.95
Quantity: 1
Sub Total: $15.95

Men's Boxer Briefs
Price: $7.95
Quantity: 3
Sub Total: $23.85

Cart Overview:
Size: 4
Total: $39.80

I am not an eCommerce person. I have not done that much with shopping carts so I am not sure if this is the best way to go. But, it seems like a fairly straight forward way of keeping a Wish List. Here is the code that makes the Cart.cfc possible:

<cfcomponent
	output="false">


	<!---
		Run pseudo constructor. Set up unique key for this
		instance of the Cart object.
	--->
	<cfset VARIABLES.InstanceID = CreateUUID() />


	<!---
		Set up the Instance structure to hold instance-
		specific data.
	--->
	<cfset VARIABLES.Instance = StructNew() />

	<!---
		This is the internal query that will be used to
		keep track of the items in our cart. Notice that
		our cart query has some default column values.

		- PKEY: The unique ID used for each row which will be
		used to reference and update a particular row.

		- NAME: Name of the item in the cart.

		- SKU: Internal SKU value for inventory.

		- PRICE: Base cost of the item.

		- QUANTITY: The number of items that match a particlar
		row of item criteria.

		- SUB_TOTAL: The cost of the row based on the tiem
		price and its quantity.
	--->
	<cfset VARIABLES.Instance.Items = QueryNew(
		"pkey, name, sku, price, quantity, sub_total",
		"CF_SQL_INTEGER, CF_SQL_VARCHAR, CF_SQL_VARCHAR, CF_SQL_DECIMAL, CF_SQL_INTEGER, CF_SQL_DECIMAL"
		) />

	<!---
		This will be a list of all the additional,
		custom properties (columns) that were added to
		the Items query.
	--->
	<cfset VARIABLES.Instance.CustomProperties = "" />

	<!---
		The number of items in the cart. This will be
		equivalent to the sum of the quantity column of
		the Items query.
	--->
	<cfset VARIABLES.Instance.Size = 0 />

	<!--- The total cost of the cart. --->
	<cfset VARIABLES.Instance.Total = 0 />

	<!---
		This is a structure that will be duplicated prior
		to adding a new item to the query.
	--->
	<cfset VARIABLES.Instance.NewItem = StructNew() />

	<!---
		Now, we can create a "empty-value" struct that will
		be used to populate new items in the cart. This will
		include every value EXCEPT the PKEY column and the
		SUB_TOTAL column as these are auto-generated after
		insert. First, let's add the values that we KNOW are
		going to exist. During the constructor (Init method)
		we will add the rest of the properties that are
		passed in.
	--->
	<cfset VARIABLES.Instance.NewItem.Name = "" />
	<cfset VARIABLES.Instance.NewItem.SKU = "" />
	<cfset VARIABLES.Instance.NewItem.Price = 0 />
	<cfset VARIABLES.Instance.NewItem.Quantity = 0 />


	<cffunction
		name="Init"
		access="public"
		returntype="any"
		output="false"
		hint="Returns an initialized Cart instance.">

		<!--- Define arguments. --->
		<cfargument
			name="Properties"
			type="string"
			required="false"
			default=""
			hint="A list of additional properties / columns to add to our internal Items query. Duplicate values will be ignored."
			/>


		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />


		<!--- Loop over the new properties. --->
		<cfloop
			index="LOCAL.Property"
			list="#ARGUMENTS.Properties#"
			delimiters=",">

			<!--- Make sure we have a trimmed value. --->
			<cfset LOCAL.Property = Trim( LOCAL.Property ) />

			<!---
				Check to see if this property is already a
				column in our internal Items query.
			--->
			<cfif NOT ListFindNoCase(
				VARIABLES.Instance.Items.ColumnList,
				LOCAL.Property
				)>

				<!---
					Add this column. All new columns are added
					as varchar values to keep it simple.
				--->
				<cfset QueryAddColumn(
					VARIABLES.Instance.Items,
					LOCAL.Property,
					"CF_SQL_VARCHAR",
					ArrayNew( 1 )
					) />

				<!---
					Add this property to our list of
					custom properties.
				--->
				<cfset VARIABLES.Instance.CustomProperties = ListAppend(
					VARIABLES.Instance.CustomProperties,
					LOCAL.Property
					) />


				<!---
					Add this property to the NewItem struct
					that has already been created.
				--->
				<cfset VARIABLES.Instance.NewItem[ LOCAL.Property ] = "" />

			</cfif>

		</cfloop>


		<!--- Return This reference. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="AddItem"
		access="public"
		returntype="struct"
		output="false"
		hint="Adds a new item to the Items query. If an item with the matching criteria already exists, the quantity is simply updated.">

		<!--- Define arguments. --->
		<cfargument
			name="Item"
			type="struct"
			required="true"
			hint="The struct generated by a call to GetNewItem()."
			/>


		<!--- Define the local scope. --->
		<cfset var LOCAL = StructNew() />


		<!---
			Since we are referencing "shared" memory space, we
			really should be locking all Add-Item relate logic.
			Use a named lock based on this CFC's Instance ID.
		--->
		<cflock
			name="#VARIABLES.InstanceID#"
			type="EXCLUSIVE"
			timeout="10">


			<!---
				First thing we want to do is check to see if
				this item already exists in the Items query.
				We will know this if key columns match up.
			--->
			<cfquery name="LOCAL.Exists" dbtype="query">
				SELECT
					pkey
				FROM
					VARIABLES.Instance.Items
				WHERE
					name = <cfqueryparam value="#ARGUMENTS.Item.Name#" cfsqltype="CF_SQL_VARCHAR" />
				AND
					sku = <cfqueryparam value="#ARGUMENTS.Item.SKU#" cfsqltype="CF_SQL_VARCHAR" />

				<!---
					We have to cast this to a decimal
					because otherwise, it doesn't seem to
					match up internally.
				--->
				AND
					CAST( price AS DECIMAL ) = <cfqueryparam value="#ARGUMENTS.Item.Price#" cfsqltype="CF_SQL_DECIMAL" />

				<!---
					Loop over the custom properties to
					check for equality. Remember that each
					of the custom properties has been stored
					as a string.
				--->
				<cfloop
					index="LOCAL.Property"
					list="#VARIABLES.Instance.CustomProperties#"
					delimiters=",">

					AND
						[#LOCAL.Property#] = <cfqueryparam value="#ARGUMENTS.Item[ LOCAL.Property ]#" cfsqltype="CF_SQL_VARCHAR" />

				</cfloop>
			</cfquery>


			<!---
				Check to see if an existing row of matching
				data was found.
			--->
			<cfif LOCAL.Exists.RecordCount>

				<!---
					Since we found a matching item, all we have
					to do is update the quantity. Based on the
					PKEY, get the matching row number.
				--->
				<cfset LOCAL.RowIndex = VARIABLES.Instance.Items[ "pkey" ].IndexOf(
					JavaCast( "int", LOCAL.Exists.pkey )
					) />

				<!---
					Add one to the value of the returned index.
					Remember, since IndexOf() uses the Java
					indexing, this value will be zero-based,
					not one-based.
				--->
				<cfset LOCAL.RowIndex = (LOCAL.RowIndex + 1) />

				<!--- Update the quantity. --->
				<cfset VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ] = (VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ] + ARGUMENTS.Item.Quantity) />

				<!--- Update the sub total. --->
				<cfset VARIABLES.Instance.Items[ "sub_total" ][ LOCAL.RowIndex ] = (VARIABLES.Instance.Items[ "price" ][ LOCAL.RowIndex ] * VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ]) />

				<!---
					Set the pkey back into the passed in item
					so that this row can be easily referenced.
				--->
				<cfset ARGUMENTS.Item.Pkey = LOCAL.Exists.pkey />

			<cfelse>

				<!---
					Since no matching record could be found, we
					are going to need to add a completely new
					record. To start with, we are going to need
					to get a new PKEY value.
				--->
				<cfif VARIABLES.Instance.Items.RecordCount>

					<!---
						Since we do have items in our cart
						already, we need to add ONE to the MAX
						of the pkey column (for ease of use).
					--->
					<cfset LOCAL.Pkey = (
						ArrayMax(
							VARIABLES.Instance.Items[ "pkey" ]
							) + 1
						) />

				<cfelse>

					<!---
						Since we have no items yet, we can just
						set our first PKEY to 1.
					--->
					<cfset LOCAL.Pkey = 1 />

				</cfif>


				<!--- Add a row to the items query. --->
				<cfset QueryAddRow(
					VARIABLES.Instance.Items
					) />

				<!---
					Get a short hand value for the new row
					index. We don't need this, but it makes
					the code slightly shorter.
				--->
				<cfset LOCAL.RowIndex = VARIABLES.Instance.Items.RecordCount />

				<!--- Set the PKEY value. --->
				<cfset VARIABLES.Instance.Items[ "pkey" ][ LOCAL.RowIndex ] = JavaCast( "int", LOCAL.Pkey ) />

				<!--- Set the built-in values. --->
				<cfset VARIABLES.Instance.Items[ "price" ][ LOCAL.RowIndex ] = JavaCast( "float", ARGUMENTS.Item.Price ) />
				<cfset VARIABLES.Instance.Items[ "quantity" ][ LOCAL.RowIndex ] = JavaCast( "int", ARGUMENTS.Item.Quantity ) />
				<cfset VARIABLES.Instance.Items[ "sub_total" ][ LOCAL.RowIndex ] = JavaCast( "float", (ARGUMENTS.Item.Price * ARGUMENTS.Item.Quantity) ) />

				<!---
					Set the other string and customer
					properties.
				--->
				<cfloop
					index="LOCAL.Property"
					list="name,sku,#VARIABLES.Instance.CustomProperties#"
					delimiters=",">

					<cfset VARIABLES.Instance.Items[ LOCAL.Property ][ LOCAL.RowIndex ] = JavaCast( "string", ARGUMENTS.Item[ LOCAL.Property ] ) />

				</cfloop>

				<!---
					Set the Pkey value back into the
					passed in Item.
				--->
				<cfset ARGUMENTS.Item.Pkey = LOCAL.Pkey />

			</cfif>


			<!---
				ASSERT: At this point, we have either added a
				new item or updated the quantity of an
				existing item. Either way, we can update the
				size and total of the cart.
			--->
			<cfset VARIABLES.Instance.Size = ArraySum(
				VARIABLES.Instance.Items[ "quantity" ]
				) />

			<cfset VARIABLES.Instance.Total = ArraySum(
				VARIABLES.Instance.Items[ "sub_total" ]
				) />

		</cflock>


		<!--- Return the updated struct. --->
		<cfreturn ARGUMENTS.Item />
	</cffunction>


	<cffunction
		name="Clear"
		access="public"
		returntype="void"
		output="false"
		hint="Clears the carts.">

		<!---
			Query the cart using a query of queries. This
			will keep the structure in tact.
		--->
		<cfquery name="VARIABLES.Instance.Items" dbtype="query">
			SELECT
				*
			FROM
				VARIABLES.Instance.Items
			WHERE
				1 = 0
		</cfquery>

		<!--- Reset the size and total. --->
		<cfset VARIABLES.Instance.Size = 0 />
		<cfset VARIABLES.Instance.Total = 0 />

		<!--- Return out. --->
		<cfreturn />
	</cffunction>


	<cffunction
		name="GetItems"
		access="public"
		returntype="query"
		output="false"
		hint="Returns a duplicate of the internal Items query.">

		<cfreturn Duplicate( VARIABLES.Instance.Items ) />
	</cffunction>


	<cffunction
		name="GetNewItem"
		access="public"
		returntype="struct"
		output="false"
		hint="This returns and empty-value struct that can be used to add a new item to the Items query.">

		<!---
			Return a dupcliate of the empty-value struct.
			Remember, since structs are passed by reference,
			it is important that each call to this method
			result in an entirely new struct instance.
		--->
		<cfreturn Duplicate(
			VARIABLES.Instance.NewItem
			) />
	</cffunction>


	<cffunction
		name="GetSize"
		access="public"
		returntype="numeric"
		output="false"
		hint="Returns the number of items in the cart.">

		<cfreturn VARIABLES.Instance.Size />
	</cffunction>


	<cffunction
		name="GetTotal"
		access="public"
		returntype="numeric"
		output="false"
		hint="Returns the total price of the cart.">

		<cfreturn VARIABLES.Instance.Total />
	</cffunction>

</cfcomponent>

Reader Comments