Ajax, ZF and Smarty feed reader: part 3

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">&nbsp;</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">&nbsp;</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.

Related Posts

Tags: , , ,