在WEB项目中多维下拉菜单的实现技巧和方法
作者:tttk
来源:CSDN
点击次数:11382
2003-6-12 8:44:36
〓简介〓
【摘要】对于web项目中下拉菜单的设计,尤其涉及到复杂的多维菜单,许多web开发的入门者往往不知所措,网络中关于此类问题的讨论也是屡见不鲜。文章旨在通过一个实例的分析,帮助初学者掌握web项目中多维动态下拉菜单的实现原理和方法,帮助入门者熟悉javascript、DHTML、ASP等相互结合的技巧,从而找到一种动态下拉菜单的通用解决方案。
〓正文〓
前 言
笔者日前涉及一个大型ASP项目的开发,其中多次遇到多维下拉菜单(对于WEB项目而言,这里专指网页中的<SELECT>元素)的问题,菜单中的数据均需要从数据库中取出,并动态的生成和变化。笔者以前曾发表过如何利用PHP和JavaScript制作二维下拉菜单的文章,目前这类文章在网络上也颇为多见,思路也有很多创新,但对于本文中谈到的多维下拉菜单,很少有人谈及。笔者无意班门弄斧,只是想把开发中的一点经验和技巧总结出来,希望能给广大的读者一点启示。
多维下拉菜单,顾名思义,也就是根据一个下拉菜单的选择,来控制其它一个或多个下拉菜单中显示的数据。举个例子来说明,在一个WEB管理系统中,用户要求通过选择单位名称,进而选择部门名称,最后选择员工。也就是说,需要提供三个下拉列表,每个下拉列表之间需要建立关联。通过第一个能选择第二个,并同时选择第三个,第四个等等。那么每一个下拉列表的显示数据之间如何建立关联,关联起来的数据又如何通过事件驱动,这正是本文所要讨论的主要内容。
熟悉VB、Delphi等RAD开发工具的朋友可能会感到疑惑。的确,在这些所谓的RAD开发工具中,我们可以利用Combo Boxes控件很容易的实现下拉菜单,进而实现他们之间的关联。但是由于WEB项目中的下拉菜单是利用HTML中的<SELECT>元素来实现的,而由于HTML的局限性,无论是对象的属性还是事件模型,都远没有RAD工具那么强大。所以,开发WEB项目,这种问题只能有一个解决途径,那就是利用JavaScript(也可能有人说可以利用java来实现,那我也只好说,您是高手)。
关于JavaScript的使用,不是本文的重点,所以不了解或不熟悉JavaScript的读者请先参考JavaScript的相关资料以获取相关信息。
好,言归正传,下面我们就一起来探讨多维下拉菜单的设计问题。为了讨论的方便,我们就以上文提到的三维下拉菜单为例,向大家一步一步的讲述设计的思路。
一、分析菜单的运作流程
首先,用户会选择单位列表,并从中选出一个单位名称,我们假定为单位A。这时,另外两个下拉列表应该做些什么?对,我们希望第二个下拉菜单能立即反映第一个下拉菜单的选择,显示并仅显示单位A中的所有部门,我们再假定菜单中第一项为部门A(默认的显示项)。那么可能有读者会问,第三个下拉菜单不是应该同时选择与部门A对应的员工数据吗?这是个很好的想法,是的,我们也应该立即改变第三个下拉菜单中的数据为部门A中的员工列表。同样,当用户选择部门时,又会改变员工列表。依次类推。
以上是一种思路,可能有一些特殊的情况,例如,在改变部门列表时,并不希望立即就选择一个部门,而是显示一个"请选择"字样的提示条目。
思路已经有了,下面就是如何实现的问题了。
二、菜单数据的容器
根据常规想法,当用户从第一个下拉菜单中选择单位名称,我们可以从数据库中选出与之相关的部门数据,并显示出来,这似乎也不无可行。但有经验的开发者就会发现,由于Web页面的无状态性,当你再次连接数据库时,Web页面必须得再次刷新。这是一个头疼的问题,一方面我们想连接数据库,可另一方面我们必须得保持用户已输入数据不被破坏。即使如此,估计用户也并不希望看到一个每次选择都刷新一次的局面。难道就没有更好的办法?
有,那就是利用JavaScript的多维数组。我们为什么不可以把需要显示的数据在第一次连接数据库时全取出来,放到数组中去?这样在每次改变菜单数据时,只要从数组中取得数据,不就可以大大的提高效率了吗?这是个令人振奋的方法,这个方法中提到的JavaScript数组,我们暂且称之为菜单数据的容器。
您的思路是不是一下子豁然开朗?可是跃跃欲试一番以后,是不是感到事情好像并不是那么简单?问题又来了,容器的结构该如何设计,数据之间的关联又如何实现呢?别急,其实这正是问题之所在。
三、数据容器结构的设计
说起容器结构的设计,我们得感谢数据结构中的链表给我们的启示--链表是通过指针联系在一起的。虽然JavaScript中没有指针的概念,但我们为什么不可以模拟一下。
为了讨论方便,我们假定数据库的结构如下:
1、 单位信息表:(unit_id, unit_name, …)
2、 部门信息表:(dept_id, unit_id, dept_name, …)
3、 员工信息表:(emp_id, dept_id, emp_name, …)
利用这个数据库结构,我们可以很容易的推导出数组的结构。您说的没错,这应该是一个多维数组。其定义方法应该象下面这样(以部门为例):
var arrDept = new Array();
arrDept[0] = new Array(unit_id0, dept_id0 dept_name0);
arrDept[1] = new Array(unit_id1, dept_id1, dept_name1);
…
arrDept[n] = new Array(unit_idn, dept_idn, dept_namen);
n的大小视实际数据量而定,例如在单位下拉菜单中,n代表单位的总数。但读者必须明白,正是由于n的不确定性,以上的代码必须通过程序动态的产生。例如对于ASP程序,我们可以在<script></script>之间嵌入这样的一段代码:
<%
Dim rs, i
'[连接数据库,取出数据]
response.write "var arrDept = new Array();" & vbNewLine
i = 0
while not rs.EOF
response.write "arrDept[" & i & "] = new Array('" & rs(unit_id) & "', '" & _
rs(dept_id) & "', '" & rs(dept_name) & "');" & vbNewLine
rs.MoveNext
i = i +1
wend
…
%>
以上这段代码用来从部门表中取出数据,并产生相关的JavaScript多维数组。这只是笔者的一种演示,读者完全可以使用更灵活的方法来提取数据。
说来说去,我们还是要回到JavaScript数组的结构定义上来。聪明的读者应该已经从上述的代码中发现了数组的定义方法,但笔者还是要不厌其烦的再补充一遍:
我们把数组的第一个元素定义为指针,用来指向其"父结点"。等等,什么是父结点?父结点说明白了就是上一级结点,例如,部门的上一级是单位,员工的上一级是部门。那么第二个元素是什么?让我们来看一下下面的一段<SELECT>定义:
<SELECT NAME="s1" onChange=" SetSubMenu(this)">
<OPTION Value="1">单位1</OPTION>
<OPTION Value="2">单位2</OPTION>
….
</SELECT>
<OPTION>元素的Value属性从哪里来呢?对,就是第二个元素,依此类推,第三个元素指的就是显示在菜单中的数据喽,即上面的"单位1"、"单位2"…
读者到这里可能有些糊涂了,说这么多,这个数组到底是什么样?别急,让我们以部门为例,给出一段根据部门库中的数据动态生成的数组模拟代码:
<SCRIPT LANGUAGE="JAVASCRIPT">
<!-
…
var arrDept = new Array();
arrDept[0] = new Array('u01', 'd01', '部门1');
arrDept[1] = new Array('u01', 'd02', '部门2');
…
arrDept[8] = new Array('u06', 'd08', '部门8');
…
arrDept[15] = new Array('u08', 'd15', '部门15');
…
->
</SCRIPT>
数组终于真相大白。以"u"开头数据的代表单位的编号,即,指向单位的指针,也就是说,我们可以通过这个编号来确定该单位所属的部门;以"d"开头的数据代表部门的编号,用来供下一级选单(即员工选单)的指针使用。(注:实际使用中,数据格式根据情况而定)
有一个问题,象单位这样没有父结点的数组该如何定义?很简单,把数组的第一个元素全部置为0就行了。
下一步,是到我们编写JavaScript代码来控制菜单的显示的时候了。我们就假定您生成的三个数组分别命名为arrUnit,arrDept,arrEmp。
四、编写JavaScript代码,控制菜单的显示
其实有经验的程序员,读到这里应该知道如何进行下去。但您不妨读下去,也许,笔者的方法对您未必不是一种新的尝试。而且,据我猜测,读我这篇文章的大多数都是没有经验的程序员,呵呵,帮人帮到底吧。Come On, Let's Go.
让菜单显示出来,其实有好几种思路。利用ASP等程序直接生成<SELECT>结构、利用OPTION对象的ADD和Remove方法动态添加和改变等等,都是可以使用的方法。但,经过笔者的多次实践和摸索,有一种方法更为有效,那就是利用Script代码动态的改写整个<SELECT>框架。
好,就让我们从加载页面(document)开始,一步一步的讲解JavaScript代码到底是如何控制菜单的显示的。
既然有三个菜单,那么我们就得事先设计出这样的HTML代码(其实要不要无所谓,放在那里只是为了便于理解):
<BODY BGCOLOR="#FFFFFF" ONLOAD="body_onload()">
...
<TD>
<SELECT NAME="s0" ONCHANGE="SetSubmenu(this)"></SELECT>
</TD>
<TD>
<SELECT NAME="s1" ONCHANGE="SetSubmenu(this)"></SELECT>
</TD>
<TD>
<SELECT NAME="s2" ONCHANGE="SetSubmenu(this)"></SELECT>
</TD>
</BODY>
您有可能要问,这里怎么什么数据都没有?不要奇怪,等一下您自然就会明白。我们来看一下<BODY>对象的ONLOAD事件body_onload()做了些什么工作?
function body_onload(){
var TD = GetParent(document.all("s0"), "TD");
TD.innerHTML = MakeMenu(arrUnit, 0, 0, "s0", 1);
TD = GetParent(document.all("s1"), "TD");
TD.innerHTML = MakeMenu(arrDept, GetSelectValue(document.all("s0")), 0, "s1", 1);
TD = GetParent(document.all("s2"), "TD");
TD.innerHTML = MakeMenu(arrEmp, GetSelectValue(document.all("s1")), 0, "s2", 1);
}
让我们来研究一下。首先程序利用GetParent()函数取得s0的容器TD对象句柄,然后,利用MakMenu()函数产生菜单代码,并把代码赋值给刚才取得的TD对象;然后是s1,接着是s2.。GetParent()函数定义如下:
function GetParent(src, tag){
if (src && src.tagName!=tag){
return(GetParent(src.parentElement, tag));
}
return src;
}
这里的tag参数必须大写,例如TD、TR、TABLE,函数返回的是离src指定的元素最近的由tag标签定义的父对象。
我们要特别说明一下MakeMenu()函数,这个函数的作用不言而喻--产生菜单的HTML定义,先看看函数定义:
function MakeMenu(arrSub, pValue, cValue, name, bulSkip){
var sHTML = "<select name='" + name + "' οnchange='SetSubMenu(this)' >";
if (bulSkip) sHTML += "<option value=0><未选择></option>";
for (var i=0; i < arrSub.length; i++){
if (arrSub[i][0]==pValue){
var tag = (arrSub[i][1]==cValue)?" selected>":">";
sHTML += "<option value='" + arrSub[i][1] + "'" + tag + arrSub[i][2] + "</option>";
}
}
sHTML += "</select>";
return sHTML;
}
来看一下参数的含义。arrSub,指的是菜单数据的来源,其实就是我们上文定义的数组;pValue,指定父结点的编号,根据这个编号,我们可以找出所有的子结点数据;cValue,指定菜单的默认显示项;name,指定产生的<SELECT>菜单的名称;bulSkip,指定菜单的默认显示项是"<未选择>"还是具体数据。
GetSelectValue()函数的目的,就是取得<SELECT>对象当前显示的值。如果没有显示任何值,函数返回0。
function GetSelectValue(oSelect){
if (oSelect.selectedIndex < 0) return 0;
return oSelect.options(oSelect.selectedIndex).value;
}
那么,当用户加载页面之后,首先运行的就是body_onload()函数,该函数根据已经产生的JavaScript多维数组,利用MakeMenu()函数动态的生成菜单的HTML代码,并根据DHTML的原理加载到页面中。OK,运行一下页面,看看菜单是否正常显示?如果有什么问题,抓紧时间好好调试一下,例如数据库的连接是否正常,javascript代码的大小写是否正确,数组的定义是否有什么问题…
下一步,选择菜单…等一下,好像还有什么遗漏,对了,我们还必须为<SELECT>对象的onchange事件添加程序代码:
function SetSubMenu(pSelect){
var oOption, sValue;
if (pSelect.selectedIndex < 0) return;
switch (pSelect.name){
case "s0":
var TD = GetParent(document.all("s1"), "TD");
TD.innerHTML = MakeMenu(arrDept, GetSelectValue("s0"), "0", "s1", 0);
TD = GetParent(document.all("s2"), "TD");
TD.innerHTML = MakeMenu(arrEmp, GetSelectValue("s1"), "0", "s2", 0);
break;
case "s1":
var TD = GetParent(document.all("s2"), "TD");
TD.innerHTML = MakeMenu(arrEmp, GetSelectValue("s1"), "0", "s2", 0);
break;
default:
}
}
好了,我们再检查一下,还有没有什么遗漏。从第一个下拉菜单中选择单位,立即,第二个下拉菜单和第三个下拉菜单都发生了变化,看看是不是想要的。(不是?呵呵,回头好好检查);再在第二个下拉菜单中选择部门,看看员工的下拉菜单是否跟着改变?
恭喜你,你已经成功的实现了三维下拉菜单。其实,对于二维菜单,实现的方法完全一致。读者完全可以利用本文的方法实现WEB项目菜单的全攻略。以后再遇到类似的问题,我想这回你一定可以毫不犹豫的说,让我来搞定它。
文章到此,也应该结束,但我还想补充两句。本文所提供的只是一种解题思路,或者说是一种方法。我不喜欢硬搬照抄的做法,相反,我希望读者能够举一反三,衍生出更多的新思路、新功能,那将是我愿意看到的。如果你有什么想法,欢迎您同我交流。 tttk2000@hotmail.com
< html>
< head>
< /head>
< body>
< script language="JavaScript">
< !--
var subcat = new Array();
subcat[0] = new Array('10','1','=1')
subcat[1] = new Array('10','2','=2')
subcat[2] = new Array('10','3','=3')
subcat[3] = new Array('10','4','=4')
subcat[4] = new Array('10','5','=5')
subcat[5] = new Array('10','6','=6')
subcat[6] = new Array('10','7','=7')
subcat[7] = new Array('10','8','=8')
subcat[8] = new Array('10','9','=9')
subcat[9] = new Array('10','10','=10')
subcat[10] = new Array('20','11','=11')
subcat[11] = new Array('20','12','=12')
subcat[12] = new Array('20','13','=13')
subcat[13] = new Array('20','14','=14')
subcat[14] = new Array('20','15','=15')
subcat[15] = new Array('20','16','=16')
subcat[16] = new Array('20','17','=17')
subcat[17] = new Array('20','18','=18')
subcat[18] = new Array('20','19','=19')
subcat[19] = new Array('20','20','=20')
function changeselect1(locationid)
{
document.form1.s2.length = 0; //初始化下拉列表 清空下拉数据
document.form1.s2.options[0] = new Option('==请选择==',''); //给第一个值
for (i=0; i<subcat.length; i++) //legth=20
{
if (subcat[i][0] == locationid) //[0] [1] 第一列 第二列
{document.form1.s2.options[document.form1.s2.length] = new Option(subcat[i][1], subcat[i][2]);} //建立option
//第一次 length=1 因为有==请选择==
//i=9时 length= 10 值有11个 因为从0数起 subcat[i][0] == locationid屏蔽了再写
}
}
//-->
< /script>
< form name="form1" method="post" runat="server">
二级联动:
< select name="s1" onChange="changeselect1(this.value)">
< option>==请选择==</option>
< option value="10">1-10</option>
< option value="20">11-20</option>
< /select>
< select name="s2" onChange="alert(this.value)">
< option>==请选择==</option>
< /select>
< /form>
<?php
/*******************************************
**********功能:php二级联动菜单*************
**********作者:冲星*************************
**********Email:njj@nuc.edu.cn**************
**********日期:2004/10/02******************
**********请转载时保留版权信息**************
*******************************************/
require_once('db.inc.php');//数据库连接
$db=new hq_online;
$db1=new hq_online;
$db->query("select * from news_bclass order by id desc");
$fMenu="";
$fValue="";
while($db->next_record()){
$fMenu.="/"".$db->Record["bname"]."/",";
$fValue.="/"".$db->Record["id"]."/",";
}
$fMenu=substr($fMenu,0,(strlen($fMenu)-1));
$fMenu="[".$fMenu."]";//*****************************得到var fMenu
$fValue=substr($fValue,0,(strlen($fValue)-1));
$fValue="[".$fValue."]";//*****************************得到var fValue
//得到*****************************var sMenu
$db->query("select * from news_bclass order by id desc");
while($db->next_record()){
$parentid=$db->Record["id"];
$db1->query("select * from news_sclass where parentid='$parentid' order by parentid desc");
while($db1->next_record()){
$num=$db1->num_rows();
$i++;
$sMenu.="/"".$db1->Record["sname"]."/",";
if($i==$num){
$sMenu="[".$sMenu."],[";
}
}
// $sMenu1=$sMenu."],[";
}
$sMenu.="]";
$sMenu=str_replace("/",]","/"]",$sMenu);
$sMenu="[".$sMenu."]";
//得到*****************************var sValue
$db->query("select * from news_bclass order by id desc");
while($db->next_record()){
$parentid=$db->Record["id"];
$db1->query("select * from news_sclass where parentid='$parentid' order by parentid desc");
while($db1->next_record()){
$nums=$db1->num_rows();
$j++;
$sValue.="/"".$db1->Record["id"]."/",";
if($j==$nums){
$sValue="[".$sValue."],[";
}
}
}
$sValue.="]";
$sValue=str_replace("/",]","/"]",$sValue);
$sValue="[".$sValue."]";
//echo $fMenu."<br>";
//echo $sMenu."<br>";
//echo $fValue."<br>";
//echo $sValue."<br>";exit;
?>
<div id="tar"></div>
<SCRIPT LANGUAGE="JavaScript" DEFER>
var fMenu = <?php echo $fMenu; ?>;
var fValue = <?php echo $fValue; ?>;
var sMenu = <?php echo $sMenu; ?>;
var sValue = <?php echo $sValue; ?>;
var oWhere = document.all.tar;
var ofMenu = document.createElement("<SELECT name='bigclass'>");
var osMenu = document.createElement("<SELECT name='smallclass'>");
with(oWhere)appendChild(ofMenu),appendChild(osMenu);
createMainOptions();
createSubOptions(0);
ofMenu.onchange = function() {createSubOptions(this.selectedIndex);};
function createMainOptions() {
for(var i=0;i<fMenu.length;i++)ofMenu.options[i] = new Option(fMenu[i],fValue[i]);
}
function createSubOptions(j) {
with(osMenu) {
length=0;
for(var i=0;i<sMenu[j].length;i++)osMenu.options[i] = new Option(sMenu[j][i],sValue[j][i]);
}
}
</SCRIPT>
<?php
/*
调用方法:
将此代码保存为文件ld2.php
例如要在write.php页面放一个二级联动,则在write.php页面
要放置二级联动的地方加一句require_once("ld2.php");
若write.php要向save.php页面提交数据则在save.php中使用
$bigclass=$_POST["bigclass"];//取得大类的id值
$smallclass=$_POST["smallclass"];//取得小类的id值
接下来就知道该怎么做了吧……
附表的结构:
可能有的表的结构和我有些不同,但基本点都一样,修改程序
中对应字段和表名就ok
程序写的有点乱,不过绝对可以放心的使用
欢迎与我交流探讨!
---------------------------------------------------------
大类的表news_bclass结构
CREATE TABLE news_bclass (
id int(11) NOT NULL auto_increment,
bname varchar(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY id_2 (id),
KEY id (id)
) TYPE=MyISAM;
字段说明:bname为大类中文名称
------------------------------
小类的表news_sclass结构
CREATE TABLE news_sclass (
id int(11) NOT NULL auto_increment,
sname varchar(10) NOT NULL default '',
parentid int(10) NOT NULL default '0',
bname varchar(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY id_2 (id),
KEY id (id)
) TYPE=MyISAM;
字段说明:sname为小类中文名称
parendid为大类中的id值
bname为大类中文名称
-----------------------------------------------------------
*/
?>
db.inc.php文件的内容
<?php
class hq_online extends db_sql{
var $Host="localhost";
var $Database="hq";
var $User="root";
var $password="";
}