Overview
Symfony has Ajax helpers that make programming an elaborate interface a piece of cake. This tutorial will show you step-by-step how to create an Ajax-powered symfony application in minutes.
Introduction
Real lazy folks that can't stand reading long documentation are advised to watch the online screencast that demonstrates exactly what is written below.
Adding items to a shopping cart in common e-commerce applications isn't very close to the actual "add to cart" metaphor, since it requires clicking an "add to cart" button, watch a new page (the shopping cart), and then go back to the shop or checkout with buttons.
Ajax allows to get closer to the cart metaphor, by enabling drag-and-drop interactions and giving immediate visual feedback, without leaving the shop.
The target application of this tutorial will be a symfony ported version of the shopping cart demo published by script.aculo.us in Rails. It uses the prototype JavaScript framework (bundled with symfony) and some script.aculo.us JavaScript that is the core of the JavaScript helpers.
Application Setup
First, create a sfdemo
project, an app
application and a cart
module:
$ cd /home/steve $ mkdir sfdemo $ cd sfdemo $ symfony init-project sfdemo $ symfony init-app app $ symfony init-module app cart
Setup your web server to be able to access this new application (whether using a virtual host or an alias, as described in the web server setup chapter of the documentation). For this example, let's assume that this module is accessible via a localhost:
http://localhost/cart/
Congratulations, it says.
Your app must have access to the symfony JavaScript libraries. If your app doesn't work, check you can access these
libraries within your browser (test http://localhost/sf/prototype/js/prototype.js
for example). If not, you have 3
different ways to fix this problem:
configure Apache with the following
Alias
:Alias /sf /$data_dir/symfony/web/sf
create a
sf
symbolic link in yourweb
directory:$ cd /home/steve/sfdemo/web $ ln -sf /$data_dir/symfony/web/sf sf
copy the JavaScript files in
web/js
directory:$ cd /home/steve/sfdemo/web $ mkdir -p sf/js $ cp /$data_dir/symfony/web/sf/prototype/js/*.js sf/js/
The Main Page
First, you need to create a list of items to be purchased. To keep the project simple, the element list is accessed via a
simple getProducts()
method of the cart
actions class. The shopping cart is a simple parameter of the sfUser
object,
set with the Attribute parameter holder. Modify
the sfdemo/apps/app/modules/cart/actions/actions.class.php
to:
class cartActions extends sfActions { public function executeIndex() { $this->getUser()->setAttribute('cart', array()); $this->products = $this->getProducts(); } private function getProducts() { return array('iPod black', 'iMac', 'iMac RC', 'iPod'); } }
The main page of the cart
module will contain a list of items, and a zone to drag items to. This zone is the shopping
cart. So open the template sfdemo/apps/app/modules/cart/templates/indexSuccess.php
and write in:
<h1>symfony Apple store demo</h1> <div id="shopping_cart"> <h2>Products:</h2> <div id="product_list"> <?php foreach ($products as $id => $title): ?> <?php echo image_tag('product'.$id, array( 'id' => 'product_'.$id, 'class' => 'products' )) ?> <?php endforeach; ?> </div> <h2>Cart:</h2> <div id="cart" class="cart"> </div> </div>
You can see that products are shown as images. Use the images available in this
archive, and put them in the sfdemo/web/legacy/images/
directory. In addition, part of the styling was done for you, so it is recommended that you upload this
stylesheet to the sfdemo/web/css/
directory and add a
view.yml
in the sfdemo/apps/app/modules/cart/config/
directory with the following content:
all: stylesheets: [cart]
Now watch the chop and cart backdrop by requesting again:
http://localhost/cart/
Focus on the Cart
The cart content will change as you drag items to it. This means that the content of the cart in the template must be in
an independent file. Use the include_partial()
helper for that. The items in the shopping cart will be stored in divs
with float:left
style, so a clearing div is necessary after the container. So change the end of the indexSuccess.php
template to:
<h2>Cart:</h2> <div id="cart" class="cart"> <div id="items"> <?php include_partial('cart') ?> </div> <div style="clear: both"></div> </div> </div>
The include_partial()
helper will include a _cart.php
file, and look for this file in the
sfdemo/apps/app/modules/cart/templates/
directory. Create it with the following content:
<?php foreach ($sf_user->getAttribute('cart') as $product_id => $quantity): ?> <div> <?php for ($i = 1; $i <= $quantity; $i++): ?> <?php echo image_tag('product'.$product_id, array( 'class' => 'cart-items', 'id' => 'item_'.$product_id.'_'.$i, 'style' => 'position:relative' )) ?> <?php endfor; ?> (<?php echo $quantity ?> <?php echo $products[$product_id] ?>) </div> <?php endforeach; ?> <?php if (!$sf_user->getAttribute('cart')): ?> nothing yet in your shopping cart. <?php endif; ?>
If the cart contains items, they appear as images, as many times as they are added; the quantity is be displayed after each series.
Now watch again the shopping cart at:
http://localhost/cart/
Well, there is not much change, it is still very empty... It's time to make things AJAX.
Add JavaScript Behaviors
Edit the indexSuccess.php
template to require the JavaScript helper:
<?php use_helper('Javascript') ?>
Make the images draggable by adding the following call to the draggable_element
JavaScript helper:
<?php foreach ($products as $id => $title): ?> <?php echo image_tag('product'.$id, array( 'id' => 'product_'.$id, 'class' => 'products' )) ?> <?php echo draggable_element('product_'.$id, array('revert' => true)) ?> <?php endforeach; ?>
This adds a 'draggable' behavior to each of the images of the list of products. The revert
option will make images go
back to their origin position when released (unless received by a receiving element).
Now, define the cart as a receiving element. You just need to define which part of the template will have to be updated
when the event occurs, which action will be called for its content, and which type of draggable elements can be dragged
into it. Use the drop_receiving_elements
JavaScript helper for that:
<?php echo drop_receiving_element('cart', array( 'update' => 'items', 'url' => 'cart/add', 'accept' => 'products', )) ?>
Now try again, and move the products to the cart: it works. When a draggable item is dragged to the receiving element, an
XMLHTTPRequest is sent to the add
action, and the result is displayed in the items
div. The thing is, the add
action
of the cart
module is not defined yet...
Define the Updating Action
Edit the sfdemo/apps/app/modules/cart/actions/actions.class.php
to add an add
action:
public function executeAdd() { $tmp = split('_', $this->getRequestParameter('id', '')); $product_id = $tmp[1]; $cart = $this->getUser()->getAttribute('cart'); if (!isset($cart[$product_id])) { $cart[$product_id] = 1; } else { ++$cart[$product_id]; } $this->getUser()->setAttribute('cart', $cart); $this->products = $this->getProducts(); }
This action looks for the parameter sent by the JavaScript (the id of the dragged item) and adds it to the cart.
The result of the add
action will be the addSuccess.php
template. It is a simple inclusion of the _cart.php
partial,
but this time it is necessary to pass the products as a parameter :
<?php include_partial('cart', array('products' => $products)) ?>
This template must not use the global layout, so edit the view.yml
in the sfdemo/apps/app/modules/cart/config/
directory, and write in:
addSuccess: has_layout: off all: has_layout: on stylesheets: [cart]
Try it on: you can now add items to the cart by dragging them.
Focus on Usability
You could stop now, but this shopping cart has a big default: while the cart is updated, the interface doesn't change and
the user might be disoriented. This is a general issue of asynchronous requests: an indicator zone has to be added to show
that the request is being processed. In addition, nothing tells the user when the dragged item is considered accepted by
the cart, so the hover
style of the cart
div also has to be defined.
To do that, edit the indexSuccess.php
template and write in:
<div style="height:20px"> <p id="indicator" style="display:none"> <?php echo image_tag('indicator.gif') ?> updating cart... </p> </div>
Save the 'indicator.gif' image file to your
sfdemo/web/legacy/images/
directory.
Now, modify the drop_receiving_element()
JavaScript helper call in the same template to show this new indicator while
requests are processed and declare the hover style:
<?php echo drop_receiving_element('cart', array( 'update' => 'items', 'url' => 'cart/add', 'accept' => 'products', 'script' => 'true', 'hoverclass' => 'cart-active', 'loading' => "Element.show('indicator')", 'complete' => "Element.hide('indicator')" )) ?>
Conclusion
The complete source of the demo can be downloaded and is available online. You will notice a few minor differences with the code described in this tutorial (including a trash box), but the core behaviors are the same.
Until the full documentation of the JavaScript helpers is released, you can find more information about them in the script.aculo.us documentation.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.