Yii中CGridView单元格组件delete之Ajax特性

Yii中用Gii生成的admin视图是用CGridView这个Zii组件实现的,它的添加、更新操作仍然是转换到create、update视图来完成的,而删除操作是Ajax方式,从而可以实现连续删除(因为admin视图“不动”)。

当然,实际控制器代码中为不支持Ajax的情况(虽然现在禁用javascript的情况较少出现了)留了后路,即非ajax方式,删除后跳转到预设的返回URL或admin:

if (!isset($_POST['ajax']))
        $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));

还是来看看admin视图中“调用”CGridView的情况:

$this->widget('zii.widgets.grid.CGridView', array(
        'id' => 'post-grid',
        'dataProvider' => $model->search(),
        'filter' => $model,
        'columns' => array(
……………………
            array('class' => 'CButtonColumn', ),
            ),
        ));

单元格组件的最后一列(按钮列)是通过类CButtonColumn来实现的——Yii中定义数据值的方式常常有3种,即直接定义值、方法定义值、类定义值

CButtonColumn类的使用方法,http://blog.sina.com.cn/s/blog_795914e701014sav.html 有较详细的论述,这里对其进行进一步理解

默认CButtonColumn类呈现3个按钮,从直观上理解,每个按钮都有对应的图片、鼠标移上去显示的提示文字、状态栏可见的对应URL,所以,每个按钮包含了XxxButtonImageUrl 、XxxButtonLabel、XxxButtonUrl 属性,还有XxxButtonOptions属性 (按钮的HTML选项,像其他插件的htmlOptions 属性一样)。删除按钮略特殊,可以使用deleteConfirmation属性(删除前的提示信息)和类事件属性afterDelete。XxxButtonUrl 属性中可以使用$row、$data、$this等变量来定制目标url。

CButtonColumn的灵活性在于可以通过模板实现按钮定制,定制的基本思路是:

 array  
(  
    'class'=>'CButtonColumn',  
    'template'=>'{id1}{id2}{delete}',  
    'buttons'=>array  
    (  
        'id1' => array  
        (  
            'label'=>'功能1',  
            'imageUrl'=>Yii::app()->request->baseUrl.'/images/id1.png',  
            'url'=>'Yii::app()->createUrl("users/id1", array("id"=>$data->id))',  
        ),  
        'id2' => array  
        (  
            'label'=>'功能2',  
            'url'=>'"#"',  
            'visible'=>'$data->score > 0',  
            'click'=>'function(){alert("Going down!");}',  
        ),  
    ),  
),   

即:指明类CButtonColumn,然后用template参数指明所有按钮id(update、view、delete等我默认类可识别的id),用buttons数组指明每个按钮对应的有关属性(默认的按钮可以不进行属性设定,使用它本身默认的功能,如上面的delete)。

一些细节用法:

*指定按钮列头部的显示文字:用'header' => '操作', 进行属性设定即可

*隐藏某个按钮:类似'updateButtonOptions'=>array('style'=>'display:none'), 
*每个按钮可设定的有关属性

 'buttonID' => array  
(  
    'label'=>'...',     //按钮的文本标签.  
    'url'=>'...',       //使用 PHP 表达式得出按钮的 URL.  
    'imageUrl'=>'...',  //按钮的图片路径.  
    'options'=>array(), //按钮的 HTML 选项.  
    'click'=>'...',     //当点击按钮时调用的 javascript 函数  
    'visible'=>'...',   //确定按钮是否显示的 PHP 表达式  

*删除的确定信息中包含和该行实例相关的信息:参考以下用法

 array  
(  
        'class'=>'CButtonColumn',  
        'deleteConfirmation'=>"js:'Record with ID '+$(this).parent().parent().children(':first-child').text()+' will be deleted! Continue?'",  
),  

 array  
(  
        'class'=>'CButtonColumn',  
        'deleteConfirmation'=>"js:'Do you really want to delete record with ID '+$(this).parent().parent().children(':nth-child(2)').text()+'?'",  
), 


由于Gii生成的是默认按钮,仅仅指定了类名,要理解默认的删除的动作,还是需要了解该类源码。

用一定的IDE,可以从源码清晰看到,CButtonColumn类从CGridColumn继承,有很多(19个)自己的变量,但只有少量(5个)自己的函数:init()、initDefaultButtons()、registerClientScript()、renderButton、renderDataCellContent

init()用于初始化列(为按钮列注册必要的客户端Javascript脚本):先调用initDefaultButtons初始化默认按钮,再设定每个按钮对应的click事件处理代码,最后用registerClientScript注册Javascript脚本。

initDefaultButtons()用于初始化默认的三个按钮:设定默认按钮的Label、ImageUrl等,构建buttons数组。其中对delete按钮的设定明显较复杂,其中对delete的click事件处理代码进行了设置,如下:

    if(!isset($this->buttons['delete']['click']))
        {
            if(is_string($this->deleteConfirmation))
                $confirmation="if(!confirm(".CJavaScript::encode($this->deleteConfirmation).")) return false;";

…………………………………………

if($this->afterDelete===null)
                $this->afterDelete='function(){}';


			$this->buttons['delete']['click']=<<<EOD
function() {
	$confirmation
	var th = this,
		afterDelete = $this->afterDelete;
	jQuery('#{$this->grid->id}').yiiGridView('update', {
		type: 'POST',
		url: jQuery(this).attr('href'),$csrf
		success: function(data) {
			jQuery('#{$this->grid->id}').yiiGridView('update');
			afterDelete(th, true, data);
		},
		error: function(XHR) {
			return afterDelete(th, false, XHR);
		}
	});
	return false;
}
EOD;

上面的$confirmation其实是一小段Javascript,当confirm对话返回false(选择取消)时,return false得到执行,事件处理就结束了,什么也不会被删除。反之,当confirm返回true时,事件处理过程就不会被“拦截”,继续向下执行。

afterDelete变量获取用户设定的处理过程(如果用户未设定,就是个空函数)。用jQuery选择符选择该单元格并调用其yiiGridView函数——从结构来看yiiGridView应该是一个Ajax的POST方式的update过程,最后返回false结束事件处理。显然,这部分是要在删除按钮点击后发送删除请求,在成功后刷新单元格显示。这里的关键是yiiGridView函数,并且它使用了两次。

registerClientScript就是注册客户端脚本,这个查阅权威指南可以了解客户端脚本的分布和注册的。

renderButton是根据参数如何渲染按钮,它在renderDataCellContent中被调用,最终渲染出整列。


我们从哪里来了解用于单元格的Javascript函数yiiGridView呢?

从框架的目录zii/widgets/assets/gridview下可以发现用于单元格组件的js脚本jquery.yiigridview.js文件,它是作为jQuery的插件形式设计的。

因为该插件的代码是针对整个单元格组件的功能来说的,我们只挑选对删除按钮相关的部分来了解:

	$.fn.yiiGridView = function (method) {
		if (methods[method]) {
			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
		} else if (typeof method === 'object' || !method) {
			return methods.init.apply(this, arguments);
		} else {
			$.error('Method ' + method + ' does not exist on jQuery.yiiGridView');
			return false;
		}
	};
yiiGridView函数是一个方法“调用器”,它的第一个参数method是被调用的方法(如果在方法列表中)或对象(不在方法列表中)。methods属性很长,我们挑键为update的部分来看:

		update: function (options) {
			var customError;
			if (options && options.error !== undefined) {
				customError = options.error;
				delete options.error;
			}

			return this.each(function () {
				var $form,
					$grid = $(this),
					id = $grid.attr('id'),
					settings = gridSettings[id];

				options = $.extend({
					type: settings.ajaxType,
					url: $grid.yiiGridView('getUrl'),
					success: function (data) {
						var $data = $('<div>' + data + '</div>');
						$.each(settings.ajaxUpdate, function (i, el) {
							var updateId = '#' + el;
							$(updateId).replaceWith($(updateId, $data));
						});
						if (settings.afterAjaxUpdate !== undefined) {
							settings.afterAjaxUpdate(id, data);
						}
						if (settings.selectableRows > 0) {
							selectCheckedRows(id);
						}
					},
					complete: function () {
						yiiXHR[id] = null;
						$grid.removeClass(settings.loadingClass);
					},
					error: function (XHR, textStatus, errorThrown) {
						var ret, err;
						if (XHR.readyState === 0 || XHR.status === 0) {
							return;
						}
						if (customError !== undefined) {
							ret = customError(XHR);
							if (ret !== undefined && !ret) {
								return;
							}
						}
						switch (textStatus) {
						case 'timeout':
							err = 'The request timed out!';
							break;
						case 'parsererror':
							err = 'Parser error!';
							break;
						case 'error':
							if (XHR.status && !/^\s*$/.test(XHR.status)) {
								err = 'Error ' + XHR.status;
							} else {
								err = 'Error';
							}
							if (XHR.responseText && !/^\s*$/.test(XHR.responseText)) {
								err = err + ': ' + XHR.responseText;
							}
							break;
						}

						if (settings.ajaxUpdateError !== undefined) {
							settings.ajaxUpdateError(XHR, textStatus, errorThrown, err);
						} else if (err) {
							alert(err);
						}
					}
				}, options || {});
				if (options.type === 'GET') {
					if (options.data !== undefined) {
						options.url = $.param.querystring(options.url, options.data);
						options.data = {};
					}
				} else {
					if (options.data === undefined) {
						options.data = $(settings.filterSelector).serialize();
					}
				}
				if(yiiXHR[id] != null){
					yiiXHR[id].abort();
				}
				//class must be added after yiiXHR.abort otherwise ajax.error will remove it
				$grid.addClass(settings.loadingClass);
				
				if (settings.ajaxUpdate !== false) {
					if(settings.ajaxVar) {
						options.url = $.param.querystring(options.url, settings.ajaxVar + '=' + id);
					}
					if (settings.beforeAjaxUpdate !== undefined) {
						settings.beforeAjaxUpdate(id, options);
					}
					yiiXHR[id] = $.ajax(options);
				} else {  // non-ajax mode
					if (options.type === 'GET') {
						window.location.href = options.url;
					} else {  // POST mode
						$form = $('<form action="' + options.url + '" method="post"></form>').appendTo('body');
						if (options.data === undefined) {
							options.data = {};
						}

						if (options.data.returnUrl === undefined) {
							options.data.returnUrl = window.location.href;
						}

						$.each(options.data, function (name, value) {
							$form.append($('<input type="hidden" name="t" value="" />').attr('name', name).val(value));
						});
						$form.submit();
					}
				}
			});
		},
我们知道索引为update的值是一个function,它的参数options对应jQuery的AJAX请求中的选项,所以我们前面的用法中看到和jQuery Ajax请求相似的格式。

update的作用是执行基于Ajax的单元格内容的更新,默认情况,请求的URL是产生当前单元格视图对应的那个URL,返回值为jQuery对象。

对于使用者来说,再细究细节没有太大意义。我们来了解$.ajax的有关选项。

type参数指定请求方式,默认是GET,上面的删除中使用POST方式。

url参数是发送请求的目的地址,默认当前页地址。注意,前面的代码中,url第一次是:

url: jQuery(this).attr('href'),$csrf    它是指向类似href="/blog/index.php?r=post/delete&amp;id=1"的URL的,所以,其实是执行actionDelete($id)。url第二次是:

jQuery('#{$this->grid->id}').yiiGridView('update');   它没有指定url,会使用url: $grid.yiiGridView('getUrl'), 即由方法getUrl确定:

        getUrl: function () {
            var sUrl = gridSettings[this.attr('id')].url;
            return sUrl || this.children('.keys').attr('title');
        },

应该就是admin视图

如果要处理 $.ajax() 得到的数据,则需要使用回调函数:beforeSend、error、dataFilter、success、complete。上面用到了success和error 。success的时候进行了上面第二个url的调用,即刷新过程。


admin视图中另一个Ajax操作是高级搜索表单:

<?php echo CHtml::link('高级搜索','#',array('class'=>'search-button')); ?>
<div class="search-form" style="display:none">
<?php $this->renderPartial('_search',array(
    'model'=>$model,
)); ?>
</div><!-- search-form -->

因为表单初始是不显示的,所以提供了链接按钮“高级搜索”来显示/隐藏切换。对应注册的客户端Javascript脚本包含了显示/隐藏切换代码和Ajax方式提交表单的代码:

Yii::app()->clientScript->registerScript('search', "
$('.search-button').click(function(){
    $('.search-form').toggle();
    return false;
});
$('.search-form form').submit(function(){
    $('#user-grid').yiiGridView('update', {
        data: $(this).serialize()
    });
    return false;
});
");

在表单提交中,我们再次见到了方法调用器yiiGridView和被调用的函数update,指定了选项data为对表单数据的序列化打包。


分析了上面的特点,我们可以制作Ajax方式的增加和修改了。




你可能感兴趣的:(Yii中CGridView单元格组件delete之Ajax特性)