What if you want to use some HTML Ajax without having to move code from the controller? The answer is that you have to be able to make instances of each controller.
Start with the class you extend Zend_Controller_Action with. My constructor in that class looks like this:
function __construct($request = false, $response = false, $invokeArgs = false){
if(!$request)
$request = new Zend_Controller_Request_Http();
if(!$response)
$response = new Zend_Controller_Response_Http();
if(!$invokeArgs)
$invokeArgs = array();
parent::__construct($request, $response, $invokeArgs);
}The extra lines are necessary because you won’t have valid request and response objects when you instatiate a controller so they need to be created if they don’t exist.
Next you could create a factory function in the class that handles the ajax requests, it can look something like this:
function loadController($controller){
$class_name = ucfirst($controller)."Controller";
$file_name = "controllers/".$class_name.".php";
include_once($file_name);
$ctrl = new $class_name;
return $ctrl;
}Finally you are ready to use this to actually do something useful:
function searchResult($post, $id, $controller){
$post = unserialize($post);
$ctrl = $this->loadController($controller);
$inner_html = $ctrl->getSearchResult($post, false, true);
$attr_arr = array(
'innerHTML' => $inner_html,
'class' => 'subform_container'
);
$this->response->assignAttr($id, $attr_arr);
return $this->response;
}In the above example some kind of search form is serialized and passed to php from a javascript. It is unserialized and passed to an instance of a controller. We recieve the output which is generated by a Smarty object with the fetch() function called in the getSearchResult() function of the controller.
The above way of doing things enables us to keep as much of the logic as possible where it is supposed to be, in the controllers. I hope this post was useful.
this series you could download the whole source which this tutorial is based on. This part and subsequent parts will make use of that source.
Let’s begin with index.php. The file simply loads initiate.php and then renders index.tpl. This will be the only page render we do, from now on it’s ajax only.
I will not explain initiate.php, the IBM ZF tutorial already does a good job of doing that. This tutorial is actually based on that one but here we take the ajax to insane levels.
We move on to head.tpl and index.tpl. In head.tpl all the javascript gets loaded:
<script src="js/prototype.js" type="text/javascript"></script>
<script src="js/scriptaculous.js?load=effects,dragdrop" type="text/javascript"></script>
<script src="js/jspanserializer.js" type="text/javascript"></script>The above loads prototype which is needed for scriptaculous which is needed in order to be able to drag windows around. However, as you might recall from my description of HTML Ajax we also need jspanserializer to be able to serialize to a format that php understands. JSON just doesn’t cut it for us, at least not the version I tried before I found jspan. If you try the newest version of HTML Ajax don’t hesitate to comment and tell me if the built in php serialize function works with prototype nowadays. If it does then jspan is not needed anymore.
<script type='text/javascript' src="ajax_server.php?client=all"></script>
<script type='text/javascript' src="ajax_server.php?stub=AjaxRss"></script>
<script type='text/javascript' src="js/common.js"></script>Here we load our HTML Ajax and our own script which is called common.js because it’s filled with commonly needed functionality. Exactly how the loading works you can check in the documentation that comes with HTML Ajax. I’m sure there must be some tutorials and info on The Creator’s blog too.
Let’s move on to index.tpl, the main feature is the window loading:
<input type="button" value="register" onclick="createInsertWindow('parent-template', 'list-template','default_window','dbuser', 'dbuser-insert', 'dbuser-insert-dragger', '', '', false)"/>Obviously the first two parameters refer to the two empty divs you see at the end of the file, these two divs will be used to create windows. To find out exactly what the others are about requires us to follow the trail, so let’s take a look at common.js and createInsertWindow(). Aha, so we call createWindow() straight away, let’s map the parameters:
parent_id = parent-template (a div element)
origin_id = list-template (a div element)
new_class = default_window (a css style)
target_id = dbuser-insert (the id of the window to create)
focus_id = dbuser-insert-dragger (the id of the area used to drag our future window)
function createWindow(parent_id, origin_id, new_class, target_id, focus_id){
var target_el = document.getElementById(target_id);
if(target_el == null){
var newNode = document.getElementById(origin_id).cloneNode(true);
newNode.id = target_id;
if(new_class != ''){
newNode.className = new_class;
}else{
newNode.style.display = 'block';
}
document.getElementById(parent_id).appendChild(newNode);
}
}Armed with our parameter mapping above we are now ready to make sense of the createWindow function. First we try to actually get the window we are trying to create! The reason is that this window is a singleton, only one of a kind is allowed. If we already have one we can’t create a new one. So if the window doesn’t exist we create it by cloning the list-template div and set it’s id to dbuser-insert in this case. We assign the default_window style to the window, lastly we append this new window to the parent-template div. I’m not an expert with DOM manipulation in javascript but I think I can recall that I weren’t able to position or control the window before I appended it to something. Since we append the window to the parent-template div we will also know how to remove it by calling parent.removeChild().
default_window looks like this:
.default_window{
opacity:1;
position:absolute;
background-color:#335588;
border-color:#dddddd;
border-style:solid;
border-width:1px;
padding:0;
margin:0;
left:100px;
top:100px;
}Note that the window will appear at x:100 and y:100.
createInsertWindow() also executes ajax_rss.getInsertForm(). Let’s do some parameter mapping again:
table = dbuser (the name of the table in the MySQL database that we want to work with)
target_id = dbuser-insert (the id of the window to create)
focus_id = dbuser-insert-dragger (the id of the area used to drag our future window)
template = null
topstyle = null
update = false
We open AjaXRss.class.php and check, aha it’s an extension of AjaxForm, so we open AjaxForm.class.php instead:
function getInsertForm($table, $target_id, $focus_id = '', $template = '', $topstyle = '', $type = false){
$obj = AjaxCommon::loadModelWithLabels($table);
switch ($type) {
case 'update':
$this->smarty->assign('prepop', $obj->prePop());
$this->smarty->assign('update_id', $obj->session->id);
$this->insertFormCommon($table, $target_id, $template, $obj);
break;
default:
$this->insertFormCommon($table, $target_id, $template, $obj, $type);
break;
}
$this->response->assignAttr($target_id, 'onclick', "ajax_rss.setFocus('$target_id', '$focus_id')");
$this->response->combineActions($this->setFocus($target_id, $focus_id, $topstyle));
return $this->response;
}So we load a dbuser model object with the AjaxCommon::loadModelWithLabels() factory method. Since we pass false as type the default switch executes which is a simple call to insertFormCommon():
function insertFormCommon($table, $target_id, $template, &$obj, $type = false){
$tpl = $template == '' ? 'insert_form.tpl' : $template;
$this->smarty->assign('table', $table);
$this->smarty->assign('form_type', $type);
$this->smarty->assign('parent_id', $target_id);
$this->smarty->assign('fields', $obj->fetchForm($type));
$inner_html = $this->smarty->fetch($tpl);
$this->response->assignAttr($target_id,'innerHTML',$inner_html);
}Since we didn’t pass any template information the default insert_form.tpl will be used. We assign some variables and call fetch(), the result is made to fill the new window div. Finally we try to set the focus to the new window, I say try because I think this function is not really working properly at the moment. Finding out why is definitely your homework!
Let’s open insert_form.tpl. So we include top_win_bar.tpl at the top, let’s go there then. Apparently the scriptaculous object called Draggable takes two parameters. The div to drag and the div to drag with. In our case the div to drag would be our parent window and the dragger the div that basically top_win_bar is all about. The info of who is who has been passed along all along. But isn’t this amazing, we only create the Draggable object with these two parameters and voila we drag, so easy!
Back to insert_form.tpl. Nothing really strange here, this is basically the same thing we would draw even if we weren’t using ajax. Note that we have to keep track of everything though, all important stuff need their own unique ids. How else could we keep track of them from withing the php code? The conditionals at the bottom keeps track of what kind of form we are dealing with, update, insert or login? In our case it’s insert and we call this:
ajax_rss.submitInsertForm(serializeForm(’{$table}-form’),’{$table}’)
So the form gets serialized, back to common.js:
function serializeForm(form_id){
var form_element = document.getElementById(form_id);
var rarr = new Array();
for(var i=0; i < form_element.elements.length; i++){
var fieldInfo = new Array();
var el_name = form_element.elements[i].name;
fieldInfo['id'] = form_element.elements[i].id;
fieldInfo['value'] = form_element.elements[i].value;
rarr[el_name] = fieldInfo;
}
return serialize(rarr);
}So we loop through all the elements in the form and create a 2D array with the info and the table field names as key for each sub array. The serialize function at the end is actually jspanserializer doing it’s magic. Let’s move on to submitInsertForm in the AjaxForm class. It’s quite big so I will not post it here as I’m afraid of the wordpress 404 bug, I will reference it instead.
First we unserialize with the standard php unserialize function which means that jspanserializer did a good job of hacking the form into something that php can easily understand. Then we validate the form, I leave that part for you to explore on your own. Next we filter out bullshit that we can’t insert into the database. I just love array_intersect_key() for this, it’s an awesome little piece of heaven. Finally we insert the posted data and have HTML Ajax return some kind of appropriate display.
That was all for this time, In the next part we will look at our other windows and what can be done in them
This time we will take a look at the feed list window and the manage window. This will also be the concluding part of the series.
Revisiting index.tpl shows us this (Three vertical dots mark areas that I’ve left out.):
.
.
.
<script type="text/javascript">
var params = new Array();
params['parent_id'] = "parent-template";
params['origin_id'] = "list-template";
params['new_class'] = "default_window";
params['target_id'] = "dbuser-drop";
params['focus_id'] = "dbuser-drop-dragger";
params['table'] = "dbuser";
params['topstyle'] = "";
params['template'] = "rss_drop.tpl";
</script>
<input type="button" value="manage feeds" onclick="createWindowParam(params, ajax_rss, 'manageFeeds')"/>
.
.
.This is a different way of doing basically the same thing as is being done above with function calls using massive amounts of parameters. It is however superior because it’s more dynamic, the number of parameters can change without breaking anything, least of all your programmer back.
Let’s revisit common.js:
function createWindowParam(params, ajax_obj, func){
createWindow(params['parent_id'], params['origin_id'], params['new_class'], params['target_id'], params['focus_id']);
params = serialize(params);
ajax_obj.displayWin(params, func);
}And AjaxForm.class.php:
function displayWin($params, $func){
$params = unserialize($params);
$this->$func($params);
return $this->response;
}$func would in this case be manageFeeds():
function manageFeeds($params){
$table = $params['table'];
$target_id = $params['target_id'];
$obj = AjaxCommon::loadModel($table);
$obj->loadSession();
$sess_key = $table."_dropchildren";
$_SESSION[$sess_key]['target_id'] = $table."-dropchildren";
$_SESSION[$sess_key]['id_value'] = $obj->session->id;
$_SESSION[$sess_key]['template'] = "rss_drop_children.tpl";
$this->smarty->assign('parent_table', $table);
$this->smarty->assign('parent_table_id', $obj->session->id);
$this->smarty->assign('drop_children', $obj->getChildren($obj->session->id));
$this->response->assignAttr($target_id, 'onclick', "ajax_rss.setFocus('$target_id', '{$params['focus_id']}')");
$this->displayDropForm($target_id, $params['template'], $table);
$this->response->combineActions($this->setFocus($target_id, $params['focus_id'], $params['topstyle']));
return $this->response;
}function displayDropForm($target_id, $tpl, $table){
$this->smarty->assign('parent_id', $target_id);
$this->smarty->assign('table', $table);
$inner_html = utf8_encode($this->smarty->fetch($tpl));
$this->response->assignAttr($target_id,'innerHTML',$inner_html);
}Notice how we use the $_SESSION in a straight way. The ZF session handling with namespaces would have been better, an example of just that can be found in the Writing a CMS/Community with Smarty and the Zend Framework series. I also opened the manage window when I had already opened the list window and then setFocus() worked just fine. Apparently the issue, with not setting focus that we touched upon in part 2, only applies when we have a single window. An important detail if we ever wanted to resolve that bug. Something else worth remarking on is the use of combineActions() to execute actions of different HTML_AJAX_Action objects. Apparently we use a template called rss_drop_children. This markup is drawn inside a div in the window:
.
.
.
<script type="text/javascript">
var {$param_name} = new Array();
{$param_name}['parent_id'] = "parent-template";
{$param_name}['origin_id'] = "list-template";
{$param_name}['new_class'] = "default_window";
{$param_name}['target_id'] = "{$param_name}";
{$param_name}['focus_id'] = "{$param_name}-dragger";
{$param_name}['table'] = "feeds";
{$param_name}['id_value'] = "{$row.i_table.feeds}";
{$param_name}['topstyle'] = "";
{$param_name}['template'] = "rss_headlines.tpl";
</script>
<a href="#" class="default_link" onclick="createWindowParam({$param_name}, ajax_rss, 'displayHeadlines')">
{$row[$child_table.row_headline]}
</a>
.
.
.
<script type="text/javascript">
var drop_id = '{$target_id}';
{literal}
Droppables.add(drop_id, {hoverclass:'hoverRow', onDrop:function(element, dropon){dropInsert(element, dropon)}});
{/literal}
</script>
.
.
.The first block above will open the feed reader itself where we display all articles for reading. The second block handles subscriptions, a feed can bee drawn from the list feeds window and dropped here which will add it as a subscription. Basically what happens in the second block is that we assign two callback functions and the id of the drop area. Back to common.js:
function dropHandler(dragged_el, target_el){
var dragger_info = dragged_el.id.split("-");
var target_info = target_el.id.split("-");
if(dragger_info[0] != target_info[0]){
ajax_rss.dropHandler(dragged_el.id, target_el.id);
}
}
function dropInsert(dragged_el, target_el){
var dragger_info = dragged_el.id.split("-");
var target_info = target_el.id.split("-");
if(dragger_info[0] != target_info[0]){
ajax_rss.dropInsertReference(dragged_el.id, target_el.id);
}
}And in php:
function dropHandler($dragger_id, $target_id){
list($d_table, $d_id, $d_type) = explode("-", $dragger_id);
list($t_table, $t_id, $t_type) = explode("-", $target_id);
$t_obj = AjaxCommon::loadModel($t_table);
$new_values = $t_obj->updateRowWithDeps($d_table, $d_id, $t_id);
if(is_array($new_values)){
foreach($new_values as $field => $value){
$this->response->assignAttr("{$t_table}-{$t_id}-{$field}", 'innerHTML', $value);
}
}else{
$this->response->insertScript("alert('no connection')");
}
return $this->response;
}
function dropInsertReference($dragger_id, $target_id){
list($d_table, $d_id, $d_type) = explode("-", $dragger_id);
list($t_table, $t_id, $t_type) = explode("-", $target_id);
$obj = AjaxCommon::loadModel($t_table);
$obj->insertReference($d_id, $d_table, $t_id, $t_table);
$sess_key = $t_table."_dropchildren";
$sess_val =& $_SESSION[$sess_key];
$this->displayDropChildren($sess_val['id_value'], $sess_val['target_id'], $sess_val['template'], $t_table);
return $this->response;
}Apparently the two methods responsible are updateRowWithDeps() and insertReference() in AjaxModel. Feel free to explore the inner workings of those two methods at your leisure. In the above code we se $dragger_id, let’s find out where this div comes from. In index.tpl we see the function createListWindow which in turn calls listData():
function listData($table, $target_id, $focus_id = '', $template = '', $topstyle = '', $text_size = 10){
$_SESSION[$target_id]['desc'] = true;
$tpl = $template == '' ? 'list.tpl' : $template;
$_SESSION[$target_id]['tpl'] = $tpl;
$obj = AjaxCommon::loadModelWithLabels($table);
$_SESSION[$target_id]['headlines'] = $obj->fields;
$rows = $obj->fetchToArrDeps();
$_SESSION[$target_id]['rows'] = $rows;
$widths = AjaxCommon::getWidths($rows, $text_size, $obj);
$_SESSION[$target_id]['widths'] = $widths;
$this->response->assignAttr($target_id, 'onclick', "ajax_rss.setFocus('$target_id', '$focus_id')");
$total_width = array_sum($widths) + 65;
$this->response->assignAttr($target_id, 'style', "width:{$total_width}px;");
$this->displayList($table, $target_id, $rows, $widths, $tpl, $obj->fields);
$this->response->combineActions($this->setFocus($target_id, $focus_id, $topstyle));
return $this->response;
}function displayList($table, $target_id, &$rows, &$widths, $tpl, $headlines){
$this->smarty->assign('headlines', $headlines);
$this->smarty->assign('parent_id', $target_id);
$this->smarty->assign('rows', $rows);
$this->smarty->assign('widths', $widths);
$this->smarty->assign('table', $table);
$inner_html = utf8_encode($this->smarty->fetch($tpl));
$this->response->assignAttr($target_id,'innerHTML',$inner_html);
}Notice how I’ve made some half crappy attempt at calculating widths with AjaxCommon::getWidths(), reason being that the headlines of the list are separate from the list itself which is in it’s own div. And the reason for that is that a long list should be scrollable while at the same time the user should be able to see the headlines. So some kind of logic to control the widths are in order to sync with the list, however as you easily can see if you use the application this does not work 100%. Anyway, apparently we use list.tpl here:
.
.
.
{foreach from=$headlines key=field item=info}
<td valign="top" width="{$widths[$field]}" height="25px">
<a href="#" class="default_link" onclick="ajax_rss.sortData('{$table}', '{$parent_id}', '{$field}')">
{$info.label}
</a>
</td>
{/foreach}
.
.
.
{foreach from=$row key=field item=value}
{if $field neq "id"}
{capture assign="el_id"}{$table}-{$id}-{$field}{/capture}
<td width="{$widths[$field]}" align="left" height="25px">
<div id="{$el_id}" onclick="ajax_rss.edit('{$el_id}', '{$value}')">
{$value}
</div>
</td>
{/if}
{/foreach}
.
.
.
<td width="25">
{capture assign="dragger_id"}{$table}-{$id}-dragger{/capture}
<img id="{$dragger_id}" src="images/drag.png"> </div>
<script type="text/javascript">
var dragger_id = '{$dragger_id}';
{literal}
new Draggable(dragger_id, {revert:true})
{/literal}
</script>
</td>
.
.
.
<td width="25">
{capture assign="target_id"}{$table}-{$id}-target{/capture}
<img id="{$target_id}" src="images/drop.png"> </div>
<script type="text/javascript">
var drop_id = '{$target_id}';
{literal}
Droppables.add(drop_id, {hoverclass:'hoverRow', onDrop:function(element, dropon){dropHandler(element, dropon)}});
{/literal}
</script>
</td>
.
.
.Interesting parts here are calls to sortData() to sort the feeds, edit() to do the basic text to dropdown and back to text routine. And finally at the bottom we see the dragger we use to drop in order to add new feeds to our subscription. The last block is unused, it was used to be the target for feed categories but this logic was abandoned in preference of the text->dropdown->text routine. It should have been removed but I forgot to remove it from the source before I uploaded it.
I realize that I’ve only brushed the surface of the code in this application. However if you are genuinely interested and have questions regarding something in the code, which by the way has room for a lot of improvements, then just comment on this post and I will do my best to explain. I hope this series was helpful for someone starting out in the world of desktop apps on the web.
discuss this topic to forum
