作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal提供了一套表单API,用来生成、验证和处理HTML表单。表单API将表单抽象为一个关联数组,里面包含了各种属性和对应的值。在生成表单页面时,呈现引擎会在适当的时候将数组呈现出来。表单API为我们带来了很多好处,由于我们将表单表示成为了结构化的数组,所以我们可以添加、删除、重新排序、和修改表单。当我们想用一种干净的方式,来对其它模块创建的表单进行修改时,这会特别方便。此外,表单API还对表单操作进行了保护,从而能够有效的防止表单注入攻击。对于任意的表单,我们可以使用表单API为其添加附加的验证和处理函数。
当然,表单API再给我们带来很多便利和灵活性的同时,也给我们带来了高昂的学习成本,除此以外,定制表单的外观,通常也是非常琐碎和费时的工作。
我们在前面已经接触过Drupal的表单了,并且用到了hook_form_alter、hook_form_FORM_ID_alter。对于表单、表单元素应该有了感性的认识。如果以前,就使用过Drupal6下面的表单API写过模块,那么接下来的内容,就不难理解了。
在本章中,我们将迎难而上。首先通过两个实际的例子,来学习Drupal表单API。最后,我们列出来了Drupal核心自带的各种表单元素,及实例代码,并对呈现(render)API做了简单介绍。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
让我们来看第一个实例,我们先介绍一下,实例的背景。我们想建立一个联系我们的表单页面,我们知道有两种方法可用,一是使用Drupal核心自带的contact模块,另一个就是使用webform模块,通常这两个模块可以满足大多数的情况。但是假如,客户希望用户提交联系我们表单的时候,先确认一下,确认无误以后再提交。这在实际中,也是常见的需求。此时,contact模块和webform模块就不大适用了。每个客户对于表单元素的需求也各不相同,所以此时我们可以选择通过定制自己的模块来实现。
假定客户的具体需求是这样的,在联系表单页面,用户可以输入姓名、单位名称、电子邮件、电话号码,邮件正文、访问来源等信息,用户填写了这些信息后,提交表单,进入确认页面;在确认页面,用户可以检查填写的信息,如果信息有误,可以返回来修改,如果信息无误,正式提交;正式提交后,用户看到一个致谢页面,用来“感谢用户的来信”。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal7中,不再使用“AHAH”术语了,而是采用更通用的大家已经习以为常的“Ajax”。有时候,错误的表述被一而再,再而三的使用以后,也就成为了标准,Ajax就是这样。在7里面,表单API对AJAX提供了进一步的支持,我们可以方便的创建Ajax表单,实现动态表单的效果,而不需要借助于第三方模块,这比Drupal6进步了很多。
现在就让我们通过一个实例,来实际学习一下Ajax表单吧。我们在构建各种站点时,经常会遇到让用户选择他所在的地区。比如他的出生地,他的户口所在地,他的常住地址。当一个销售商品时,我们需要客户的配送地址。中国的地址信息,通常包含省、市、县这一信息,为了让网站更加友好,通常将省市县做成联动的表单。我们的这个例子,就是在Drupal中实现省市县三级联动。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
首先,我们先做一些准备工作。Drupal7中,分类术语被处理成了实体,与以前的版本相比,Drupal7下面的分类更好用了。我们不妨把省市县信息存储成为分类,这样以后用起来也更加方便。首先我们创建一个“地区”词汇表,如图所示。
在里面添加一些省市县测试信息。
注意,这里面,长垣县在2011年6月以前,是隶属于新乡市的,2011年6月1日改为省辖县(市)了,不过这里还是把它放在了新乡市下面,长垣是我老家。题外话。北京市由于是直辖市,所以这里给了一个中间层“区/县”。
实际的省市县信息,可以借助于第三方模块,比如feeds、migrate,导入到Drupal的分类系统中去。这里就不再详细介绍了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,就是创建我们的模块,不妨把这个模块叫做“shengshixian”,在sites/all/modules/custom目录下创建一个shengshixian目录,接着创建两个文件,shengshixian.info、shengshixian.module。我们向info文件添加以下内容:
name = 省市县
description = 中国省市县三级联动.
core = 7.x
接着向module文件添加逻辑代码:
<?php
/**
* @file
* 省市县三级联动实例代码,
*/
/**
* 实现钩子hook_menu().
*/
function shengshixian_menu() {
$items['ssx'] = array(
'title' => '省市县',
'page callback' => 'shengshixian_test_page',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* 路径“ssx”页面的回调函数
*/
function shengshixian_test_page(){
$render_array = array();
//我们为这个页面设置标题
drupal_set_title('省市县三级联动');
//页面的正文为一个表单,注意drupal_get_form返回的不是html,需要使用drupal_render呈现一下。
$render_array['#markup'] .= drupal_render(drupal_get_form('shengshixian_test_form'));
//Drupal7的页面回调,返回的应该是一个数组,只有在退化形式下,才返回字符串。
return $render_array;
}
我们首先建立一个测试菜单路径“ssx”,然后添加了它的回调函数shengshixian_test_page。这里强调一下,drupal_get_form返回的不是html,需要需要使用drupal_render呈现一下,这与Drupal6下的有所不同。现在我们来看看这个表单的逻辑:
/**
* 表单shengshixian_test_form的构建函数
*/
function shengshixian_test_form($form, &$form_state){
//设置省市县对应元素的默认值
$default_sheng = !empty($form_state['values']['sheng']) ? $form_state['values']['sheng'] : '';
$default_shi = !empty($form_state['values']['shi']) ? $form_state['values']['shi'] : '';
$default_xian = !empty($form_state['values']['xian']) ? $form_state['values']['xian'] : '';
//构建省份的选项数组,首先设置了一个提示语
$sheng_options = array(
'' => '请选择省份',
);
//向数据库中查询省份信息,
$query_sheng = db_select('taxonomy_term_data','ttd')
->fields('ttd', array('tid', 'name'));
//因为省份是第一级术语,分类术语的父亲为0
$query_sheng->leftJoin('taxonomy_term_hierarchy', 'tth', 'ttd.tid = tth.tid ');
$query_sheng->condition('tth.parent', 0);
//需要确定术语所在的词汇表,就是我们在前面创建的地区
$query_sheng->leftJoin('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid ');
$query_sheng->condition('tv.machine_name', 'diqu');
//按照tid排序,并执行
$result_sheng = $query_sheng->orderBy('tid')->execute();
//将返回的结果,进行迭代,为$sheng_options赋值。
foreach ($result_sheng as $record) {
$sheng_options[$record->tid] = $record->name;
}
//省份表单元素
$form['sheng'] = array(
'#title' => t('请选择您所在的省份?'),
'#type' => 'select',
'#options' => $sheng_options,
'#default_value' => $default_sheng,
//#ajax属性数组
'#ajax' => array(
'callback' => 'shengshixian_sheng_callback',
'wrapper' => 'shi-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
);
//构建市的选项数组,首先设置了一个提示语
$shi_options = array(
'' => '请选择市',
);
//在省份不为空的情况下,取该省份下的所有的市
if(!empty($default_sheng)){
//向数据库中查询术语信息,
$query_shi = db_select('taxonomy_term_data','ttd')
->fields('ttd', array('tid', 'name'));
//将其父术语限定在前面的省份的具体值上
$query_shi->leftJoin('taxonomy_term_hierarchy', 'tth', 'ttd.tid = tth.tid ');
$query_shi->condition('tth.parent', $default_sheng);
//由于省份信息里面,已经包含了词汇表信息,所以我们不再需要关联这个taxonomy_vocabulary表。
//$query_sheng->leftJoin('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid AND tv.machine_name = :machine_name', array(':machine_name' => 'diqu'));
//按照tid排序,并执行
$result_shi = $query_shi->orderBy('tid')->execute();
//将返回的结果,进行迭代,为$shi_options赋值。
foreach ($result_shi as $record) {
$shi_options[$record->tid] = $record->name;
}
}
/*
//测试代码,中间测试的时候用的,这里保留了,开发模块所用到的测试代码很多,多数都已删除。
$form['test'] = array(
'#markup' => '123456:'.$default_sheng
);
*/
//表单元素市
$form['shi'] = array(
'#title' => t('请选择您所在的市?'),
'#prefix' => '<div id="shi-wrapper-div">',
'#suffix' => '</div>',
'#type' => 'select',
'#options' => $shi_options,
'#default_value' => $default_shi,
'#ajax' => array(
'callback' => 'shengshixian_shi_callback',
'wrapper' => 'xian-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
);
//构建县的选项数组,首先设置了一个提示语
$xian_options = array(
'' => '请选择县',
);
//在市不为空的情况下,取该市下的所有的县
if(!empty($form_state['values']['shi'])){
//向数据库中查询术语信息,
$query_xian = db_select('taxonomy_term_data','ttd')
->fields('ttd', array('tid', 'name'));
//将其父术语限定在前面的市的具体值上
$query_xian->leftJoin('taxonomy_term_hierarchy', 'tth', 'ttd.tid = tth.tid AND tth.parent = :parent', array(':parent' => $form_state['values']['shi']));
$query_xian->condition('tth.parent', $default_shi);
//由于最前面省份信息里面,已经包含了词汇表信息,所以我们不再需要关联这个taxonomy_vocabulary表。
//$query_sheng->leftJoin('taxonomy_vocabulary', 'tv', 'ttd.vid = tv.vid AND tv.machine_name = :machine_name', array(':machine_name' => 'diqu'));
//按照tid排序,并执行
$result_xian = $query_xian->orderBy('tid')->execute();
//将返回的结果,进行迭代,为$xian_options赋值。
foreach ($result_xian as $record) {
$xian_options[$record->tid] = $record->name;
}
}
//表单元素县
$form['xian'] = array(
'#title' => t('请选择您所在的县/区?'),
'#prefix' => '<div id="xian-wrapper-div">',
'#suffix' => '</div>',
'#type' => 'select',
'#options' => $xian_options,
'#default_value' => $default_xian,
);
//提交按钮
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('提交'),
);
return $form;
}
/**
* 表单元素sheng,它的值变更时,对应的Ajax回调函数。
*/
function shengshixian_sheng_callback($form,&$form_state){
//根据当前省份,重新确定市的可选项。返回重新构建的表单元素shi
return $form['shi'];
}
/**
* 表单元素sheng,它的值变更时,对应的Ajax回调函数。
*/
function shengshixian_shi_callback($form,&$form_state){
//根据当前所选的市,重新确定县的可选项。返回重新构建的表单元素xian
return $form['xian'];
}
对于上面的代码,对于从分类相关表中读取对应的省市县信息的代码,里面有详细的注释,相关的数据库操作,可参看数据库API一章。对于省市县对应的三个表单元素,这里都选用了下拉选择框。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这里值得我们学习的地方有三点,一个是
$default_sheng = !empty($form_state['values']['sheng']) ? $form_state['values']['sheng'] : '';
$default_shi = !empty($form_state['values']['shi']) ? $form_state['values']['shi'] : '';
$default_xian = !empty($form_state['values']['xian']) ? $form_state['values']['xian'] : '';
还有一个是:
'#ajax' => array(
'callback' => 'shengshixian_sheng_callback',
'wrapper' => 'shi-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
'#ajax' => array(
'callback' => 'shengshixian_shi_callback',
'wrapper' => 'xian-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
最后便是Ajax的回调函数:
function shengshixian_sheng_callback($form,&$form_state){
//根据当前省份,重新确定市的可选项。返回重新构建的表单元素shi
return $form['shi'];
}
function shengshixian_shi_callback($form,&$form_state){
//根据当前所选的市,重新确定县的可选项。返回重新构建的表单元素xian
return $form['xian'];
}
这三处,是Ajax表单的关键所在。对于第一处,获取默认的表单元素的值,看似非常普通,但确实是一个关键的地方。我们先来描述一下大致的执行流程。当我们访问页面ssx时,我们看到了一个普通的表单。如图所示:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
此时,省份的可选项有“北京市”、“河南省”。市的可选项为空,县的可选项为空。也就是初次访问这个页面时,$default_sheng、$default_shi、$default_xian的值都为空。
接着,当我们在表单页面选择“河南省”的时候,系统进行了Ajax调用,如图所示:
我们看到这里出现了“请稍等”的提示,表明系统进行了Ajax调用。聪明的读者会想到,Ajax回调应该就是hengshixian_sheng_callback了,这样理解是对的,但是还要再补充一点,那就是Drupal在后端重新构建了整个表单,在构建完整个表单后,根据hengshixian_sheng_callback回调函数,向浏览器端只返回变化的元素$form['shi'],浏览器端根据服务器端的返回值,重新设置了 “市”这一表单元素。
在重新构建整个表单的过程中,此时$default_sheng的值,就是“河南省”的tid了,已经不再为空,所以此时“市”的可选项就不再为空了。
当我们选择“新乡市”,系统再次进行了Ajax调用,如图所示:
此时,Drupal在后端再次重新构建了整个表单,在构建完整个表单后,根据shengshixian_shi_callback回调函数,向浏览器端返回更新过的元素$form['xian'],浏览器端根据服务器端的返回值,重新设置了 “县”这一表单元素。
在重新构建整个表单的这次过程中,此时$default_sheng的值,就是“河南省”的tid了,此时“市”的可选项不为空。此时$default_shi的值,就是“新乡市”的tid了,所以此时“县”的可选项就不再为空了。如图所示:
当我们做完这样的流程分析以后,我们再来理解这段代码:
'#ajax' => array(
'callback' => 'shengshixian_sheng_callback',
'wrapper' => 'shi-wrapper-div',
'method' => 'replace',
'effect' => 'fade',
),
'callback'对应的是回调函数,通常这个回调函数非常简单,返回表单中的特定部分就可以了,注意这里是系统重新构建了整个表单后;'wrapper'表示动态更新的部分,这里对应于'#prefix' => '<div id="shi-wrapper-div">',也就是表单元素“市”的主内容;'method'表示对wrapper中内容的处理方法,可选值有'replace' (默认)、 'after'、 'append'、 'before'、'prepend',这里是'replace';'effect'表示Ajax特效,可选值有'none' (默认)、 'fade'、'slide'。
最后,我们做一下归纳与总结,Drupal 7 中的Ajax表单,用起来非常简单,不需要编写自己的JQuery代码,回调函数也极其简单,只要我们遵守Drupal的规范,其余的工作都有Drupal替我们完成。从本质上来讲,这里的Ajax表单,就是多步表单的一个特例,我在前面,多次强调,重新构建整个表单,就是为了突出它其实就是一个多步表单。
构建Ajax表单,需要注意三点:一是为带有ajax属性的元素提前设置默认值,二是正确的设置表单元素的ajax属性,三是编写ajax的回调函数。
有关表单元素Ajax属性的更多信息,可参考http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html#properties。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html#properties
在本节中,我们将通过例子来学习Drupal核心自带的表单元素。表单元素类型定义在
hook_element_info中,Drupal核心表单元素大部分都定义在systemk_element_info。使用element_info($type)函数可以查看元素类型的默认属性。为方便大家查阅,这里按照字母顺序排列。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
它是一个包装器(wrapper)元素,用来对其它按钮元素进行归类分组。把'actions'元素作为数组的键,可以方便设置主题样式,同时也方便其它模块修改该表单的动作。
示例代码,来自(node.admin.inc):
$form['filters']['status']['actions'] = array(
'#type' => 'actions',
'#attributes' => array('class' => array('container-inline')),
);
$form['filters']['status']['actions']['submit'] = array(
'#type' => 'submit',
'#value' => count($session) ? t('Refine') : t('Filter'),
);
常用属性: #access、 #after_build、#attributes、 #children、 #id、#parents、#post_render、 #pre_render、 #prefix、 #process、 #states、 #suffix、 #theme、#theme_wrappers、 #tree、 #type、#weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
按钮元素除了属性#executes_submit_callback默认为FALSE以外,其它属性与提交按钮元素完全相同。属性#executes_submit_callback告诉Drupal是否需要处理表单,为TRUE时处理表单,为FALSE时则简单的重新呈现表单。和提交按钮元素一样,可以将特定的验证和提交函数直接分配给这个按钮元素。
示例代码,来自(simpletest\tests\form_test.module):
$form['continue_button'] = array(
'#type' => 'button',
'#value' => 'Reset',
// Rebuilds the form without keeping the values.
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #button_type (默认为submit)、 #disabled、 #element_validate、 #executes_submit_callback ((默认为FALSE)、 #limit_validation_errors、 #name ((默认为op)、 #parents、 #post_render、 #prefix、#pre_render、#process、 #submit、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #validate、 #value、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
单个复选框。
$form['revision_information']['revision'] = array(
'#type' => 'checkbox',
'#title' => t('Create new revision'),
'#default_value' => $node->revision,
'#access' => user_access('administer nodes'),
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #return_value (默认为 1)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display (默认为after)、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一组复选框。属性#options是一个关联数组,它的键是复选框的#return_value,对应的值则是用来显示给用户的。#options数组的键不能为0,假如为0了,那么系统就无法判断是否选中了这个选项。
$types = array_map('check_plain', node_type_get_names());
$form['advanced']['type'] = array(
'#type' => 'checkboxes',
'#title' => t('Only of the type(s)'),
'#prefix' => '<div class="criterion">',
'#suffix' => '</div>',
'#options' => $types,
);
在验证和提交函数中,通常使用array_filter()函数来获取复选框的键。在上述代码对应的验证函数中,就使用了array_filter()。代码如下:
$form_state['values']['type'] = array_filter($form_state['values']['type']);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree (默认为TRUE)、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个html容器,用来封装子元素。把子元素放在一个<div>中,并可以为这个div设置类或ID属性。
$form['filters']['status'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('clearfix')),
'#prefix' => ($i ? '<div class="additional-filters">' . t('and where') . '</div>' : ''),
);
$form['filters']['status']['filters'] = array(
'#type' => 'container',
'#attributes' => array('class' => array('filters')),
);
foreach ($filters as $key => $filter) {
$form['filters']['status']['filters'][$key] = array(
'#type' => 'select',
'#options' => $filter['options'],
'#title' => $filter['title'],
'#default_value' => '[any]',
);
}
常用属性: #access、 #after_build、 #attributes #children、 #id、 #parents、 #post_render、 #pre_render、 #prefix、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
日期元素,它是一个由3个下拉选择框联合而成的元素。如果没有提供默认值,#default_value则默认为今天的日期。#default_value和#return_value的格式是一个包含三个元素的关联数组,对应的键为:'year'、month'、'day'。例如:
array('year' => 2011, 'month' => 7, 'day' => 26)
$fields[$category][$field->name] = array(
'#type' => 'date',
'#title' => check_plain($field->title),
'#default_value' => $edit[$field->name],
'#description' => _profile_form_explanation($field),
'#required' => $field->required
);
Date元素默认的显示顺序是“月,日,年”,对于中文用户来说,这个顺序不是很习惯,要把它改为“年,月,日”,只需要在“admin/config/regional/date-time”页面,简单的配置一下默认的日期格式就可以了,将其设置为中文用户习惯的日期格式,比如“Y/m/d H:i”。
常用属性: #access、 #after_build、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
属性#process默认设为form_process_date (),在该方法中年选择器被硬编码为从1900到2050。属性#element_validate默认设为date_validate()(两个函数都位于includes/form.inc中)。当你在表单中定义日期元素时,通过定义这两个属性,就可以使用你自己的代码来替代默认设置了。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
字段集元素是用来对其它表单元素进行归类分组的。可将其声明为可伸缩的,这样当用户查看表单并点击字段集标题时,由Drupal自动提供的JavaScript能够动态的打开和关闭字段集。
$form['visibility']['node_type'] = array(
'#type' => 'fieldset',
'#title' => t('Content types'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'visibility',
'#weight' => 5,
);
常用属性: #access、 #after_build、 #attributes、 #collapsed (默认为FALSE)、 #collapsible (默认为FALSE)、 #description、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
创建了一个文件上传字段。注意,如果你使用了文件元素,那么你需要在你表单的根部设置属性enctype:
$form['#attributes']['enctype'] = 'multipart/form-data';
代码enctype="multipart/form-data"在浏览器处理文件时是必须的。
示例代码,来自(user.module):
['picture']['picture_upload'] = array(
'#type' => 'file',
'#title' => t('Upload picture'),
'#size' => 48,
'#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')),
);
常用属性: #access、 #after_build、 #array_parents、 #attached、 #attributes、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
该元素把值存放在了一个隐藏的表单字段中。注意,如果你想使用JavaScript来修改隐藏元素的值,此时应该使用#default_value属性,而不是#value属性。
示例代码,来自(node.admin.inc):
$form['nodes'][$nid] = array(
'#type' => 'hidden',
'#value' => $nid,
'#prefix' => '<li>',
'#suffix' => check_plain($title) . "</li>\n",
);
常用属性: #access、 #after_build、 #ajax、 #default_value、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #value、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
图片形式的表单提交按钮。它与提交按钮元素属性基本相同,但有两点例外。首先,它有一个#src属性,使用一个图片的URL作为它的值。其次,它把内部表单属性#has_garbage_value设置为了TRUE,这样就会阻止使用#default_value属性,从而避免在微软IE浏览器中的bug。不要在图片按钮中使用#default_value属性。图片按钮的值可从$form_state['clicked_button']['#value']中取出。
示例代码,来自(field_ui.admin.inc):
$table[$name]['settings_edit'] = $base_button + array(
'#type' => 'image_button',
'#name' => $name . '_formatter_settings_edit',
'#src' => 'misc/configure.png',
'#attributes' => array('class' => array('field-formatter-settings-edit'), 'alt' => t('Edit')),
'#op' => 'edit',
// Do not check errors for the 'Edit' button, but make sure we get
// the value of the 'formatter type' select.
'#limit_validation_errors' => array(array('fields', $name, 'type')),
'#prefix' => '<div class="field-formatter-settings-edit-wrapper">',
'#suffix' => '</div>',
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #button_type (默认为'submit')、 #disabled、 #element_validate、 #executes_submit_callback (默认为TRUE)、 #limit_validation_errors、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #return_value (默认为TRUE)、 #src、 #submit、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #validate、 #value、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
条目元素的格式与其它输入表单元素相同,但有一点不同,那就是它是只读的,没有输入框。由于这个元素是只读的,所有#required属性基本没有用处,除非你为了好看,想在#title旁边添加一个表示必填的红色图标。
示例代码,来自(user.admin.inc):
$form['permission'][$perm] = array(
'#type' => 'item',
'#markup' => $perm_item['title'],
'#description' => theme('user_permission_description', array('permission_item' => $perm_item, 'hide' => $hide_descriptions)),
);
常用属性: #access、 #after_build、 #description、 #element_validate、 #markup、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
创建一个单行的文本字段,用来存储唯一的机读名字。对这个字段会进行验证,以确保它是唯一的,并且不包含非法字符。所有的非法字符将会通过JavaScript替换为有效字符。注意这个元素对中文的支持不是很友好。
它还包含一个非标准的元素属性#machine_name,它是一个关联数组,可包含以下键exists、source、label、replace_pattern、replace。exists是一个回调函数,用来检查机读名字是否存在;source指的是表单元素的#array_parents包含的用户可读名字,可用作机读名字的源,默认为array('name');label,机读名字的标签文本,默认为“机读名字”;replace_pattern,正则表达式,用来匹配机读名字中不允许的字符,默认为'[^a-z0-9_]+';replace,在机读名字中,用来替换非法字符的字符,默认为'_'。
$form['type'] = array(
'#type' => 'machine_name',
'#default_value' => $type->type,
'#maxlength' => 32,
'#disabled' => $type->locked,
'#machine_name' => array(
'exists' => 'node_type_load',
),
'#description' => t('A unique machine-readable name for this content type. It must only contain lowercase letters, numbers, and underscores. This name will be used for constructing the URL of the %node-add page, in which underscores will be converted into hyphens.', array(
'%node-add' => t('Add new content'),
)),
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #autocomplete_path (默认为FALSE)、 #default_value、 #description (默认为 '一个唯一的机读名字。只能包含小写的字母,数字和下划线。')、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为64)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required (默认为TRUE)、 #size (默认为60)、 #states、 #suffix、 #theme (默认为'textfield')、 #theme_wrappers (默认为'form_element')、 #title (默认为'机读名字')、 #title_display #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
提供一个完整的ajax控件,用来上传文件并将其保存到{file_managed}表中。它包含一组表单元素,有两个'#submit',一个用来上传,一个用于删除;一个'#file' 元素;多个'#hidden'、'#markup'元素,用于处理进度条和显示已上传的文件。
注意:新上传的文件,如果默认状态为0,那么它们会当作临时文件,每隔6个小时被清空一次。如果你的模块用到这个元素了,那么你需要自己负责修改$file对象的状态,将其改为FILE_STATUS_PERMANENT,并保存到数据库中。下面是参考代码:
//通过file.fid加载文件。
$file = file_load($form_state['values']['my_file_field']);
//将状态修改为持久化。
$file->status = FILE_STATUS_PERMANENT;
//保存。
file_save($file);
如果点击了删除按钮,将会把该字段的值设置为0,你的模块还需要使用file_delete()将文件从files表和文件系统中实际的删除。
它包含多个非标准的表单元素属性:#progress_indicator,可选值有'none'、'bar'、'throbber',默认为'throbber';#progress_message,文件正被上传时的进度信息,默认为NULL;#upload_validators,一组回调函数,用来验证上传的文件;#upload_location ,上传文件应被存储的位置,例如'public://files/my_files'。
示例代码,来自(image.field.inc):
$form['default_image'] = array(
'#title' => t('Default image'),
'#type' => 'managed_file',
'#description' => t('If no image is uploaded, this image will be shown on display.'),
'#default_value' => $field['settings']['default_image'],
'#upload_location' => 'public://default_images/',
);
常用属性: #access、 #after_build、 #array_parents、 #attached、 #attributes、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #tree、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
它用来在表单中引入一段文本或者HTML。如果一个元素没有设置属性#type,那么它就默认为标识文本元素。。
注意:如果你把文本输出在一个可伸缩的字段集的内部,那么要使用<div>或者<p>标签对其进行包装,这样当字段集被折叠起来时,你的文本也被折叠在了里面;否则你的文本将显示在字段集的外面。
示例代码,来自(user.pages.inc):
$form['help'] = array('#markup' => '<p>' . t('This login can be used only once.') . '</p>');
常用属性: #access、 #after_build、 #element_validate、 #markup、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight。
password(密码)
生成一个单行文本字段,在这里用户的输入不直接显示出来。
示例代码,来自(user.module):
$form['account']['current_pass'] = array(
'#type' => 'password',
'#title' => t('Current password'),
'#size' => 25,
'#access' => !empty($protected_values),
'#description' => $current_pass_description,
'#weight' => -5,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为128)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一个单行文本字段,在这里用户的输入不直接显示出来。
示例代码,来自(user.module):
$form['account']['current_pass'] = array(
'#type' => 'password',
'#title' => t('Current password'),
'#size' => 25,
'#access' => !empty($protected_values),
'#description' => $current_pass_description,
'#weight' => -5,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为128)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
该元素创建两个HTML密码字段,并附加一个验证器来检查两个密码是否匹配。
示例代码,来自(user.module):
$form['account']['pass'] = array(
'#type' => 'password_confirm',
'#size' => 25,
'#description' => t('Provide a password for the new account in both fields.'),
'#required' => TRUE,
);
常用属性: #access、 #after_build、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一个单选按钮。
示例代码,来自(user.pages.inc):
$form[$name] = array(
'#type' => 'radio',
'#title' => $method['title'],
'#description' => (isset($method['description']) ? $method['description'] : NULL),
'#return_value' => $name,
'#default_value' => $default_method,
'#parents' => array('user_cancel_method'),
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #return_value、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display (默认为after)、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一组单选按钮。
示例代码,来自(user.module):
$form['account']['status'] = array(
'#type' => 'radios',
'#title' => t('Status'),
'#default_value' => $status,
'#options' => array(t('Blocked'), t('Active')),
'#access' => $admin,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一个下拉选择框。
示例代码,来自(node.module):
$form['node_recent_block_count'] = array(
'#type' => 'select',
'#title' => t('Number of recent content items to display'),
'#default_value' => variable_get('node_recent_block_count', 10),
'#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #description、 #disabled、 #element_validate、 #empty_option、 #empty_value、 #field_prefix、 #field_suffix、 #multiple、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一个表单提交按钮。按钮内部显示的单词默认为“提交”,但是可以使用属性#value来修改它。
示例代码,来自(node.module):
$form['advanced']['submit'] = array(
'#type' => 'submit',
'#value' => t('Advanced search'),
'#prefix' => '<div class="action">',
'#suffix' => '</div>',
'#weight' => 100,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #button_type (默认为'submit')、 #disabled、 #element_validate、 #executes_submit_callback (默认为TRUE)、 #limit_validation_errors、 #name (默认为 'op')、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #submit、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #validate、 #value、 #weight。
生成一个表格形式的单选按钮或者复选框,#headers属性用来设置列,#options属性用来设置行。#multiple属性用来决定是用单选按钮还是复选框,如果#js_select属性设置为TRUE,那么还会添加一个基于JavaScript的全选按钮。
admin/content页面的节点内容列表,就用到了这个元素,如图所示:
示例代码,来自(node.admin.inc):
$form['nodes'] = array(
'#type' => 'tableselect',
'#header' => $header,
'#options' => $options,
'#empty' => t('No content available.'),
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #default_value、 #element_validate、 #empty、 #header、 #js_select、 #multiple、 #options、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
文本域启用了文本格式的版本。它还包含两个非标准属性:
· #format: 要应用的格式。如果你想使用默认格式,那么可将这个属性设置为NULL,剩下的工作便会交给过滤器系统。
· #base_type (optional): 默认为'textarea'。也可以将文本格式选择器附加在其它表单元素类型上,比如文本字段。
示例代码,来自(user.module):
$form['signature_settings']['signature'] = array(
'#type' => 'text_format',
'#title' => t('Signature'),
'#default_value' => isset($account->signature) ? $account->signature : '',
'#description' => t('Your signature will be publicly displayed at the end of your comments.'),
'#format' => isset($account->signature_format) ? $account->signature_format : NULL,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #cols (默认为 60)、 #default_value、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #resizable (默认为 TRUE)、 #rows (默认为 5)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、#type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一个多行的文本字段。
示例代码,来自(user.admin.inc):
$form['email_admin_created']['user_mail_register_admin_created_body'] = array(
'#type' => 'textarea',
'#title' => t('Body'),
'#default_value' => _user_mail_text('register_admin_created_body', NULL, array(), FALSE),
'#rows' => 15,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #cols (默认为60)、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #resizable (默认为TRUE)、 #rows (默认为5)、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
如果通过设置#resizable为TRUE,启用了动态的文本域调整器,那么属性#cols的设置将不起作用。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
生成一个单行文本字段。
示例代码,来自(user.admin.inc):
$form['anonymous_settings']['anonymous'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#default_value' => variable_get('anonymous', t('Anonymous')),
'#description' => t('The name used to indicate anonymous users.'),
'#required' => TRUE,
);
常用属性: #access、 #after_build、 #ajax、 #attributes、 #autocomplete_path (默认为FALSE)、 #default_value、 #description、 #disabled、 #element_validate、 #field_prefix、 #field_suffix、 #maxlength (默认为 128)、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #size (默认为60)、 #states、 #suffix、 #text_format、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
值元素用在drupal表单内部,它不会显示出来。
示例代码,来自(user.module):
$form['picture']['picture'] = array(
'#type' => 'value',
'#value' => isset($account->picture) ? $account->picture : NULL,
);
不要混淆了'#type' => 'value' 和'#value' => ’’。前者声明了正被描述的元素的类型,而后者声明了该元素的值。
常用属性: #type、 #value。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
把所有的子字段集显示为垂直的标签。如图所示:
示例代码,来自(block.admin.inc):
$form['visibility'] = array(
'#type' => 'vertical_tabs',
'#attached' => array(
'js' => array(drupal_get_path('module', 'block') . '/block.js'),
),
);
常用属性: #access、 #after_build、 #default_tab、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #states、 #suffix、 #theme、 #theme_wrappers、 #tree、 #type、 #weight。
weight(重量)
生成一个用来声明重量的下拉选择框。
示例代码,来自(taxonomy.admin.inc):
$form[$key]['weight'] = array(
'#type' => 'weight',
'#delta' => $delta,
'#title_display' => 'invisible',
'#title' => t('Weight for added term'),
'#default_value' => $term->weight,
);
常用属性: #access、 #after_build、 #attributes、 #default_value、 #delta (默认为10)、 #description、 #disabled、 #element_validate、 #parents、 #post_render、 #prefix、 #pre_render、 #process、 #required、 #states、 #suffix、 #theme、 #theme_wrappers、 #title、 #title_display、 #tree、 #type、 #weight。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果你认真读过systemk_element_info函数中的代码,你会发现这里还定义了一个元素page。它是一个表单元素么?当然不是。在Drupal中,表单的这种数组形式的结构,经过不断的实践,发现它给我们带来了多个方面的便利性,因此在Drupal7中,这种概念又作了进一步的扩充,把它抽象成为了“呈现数组”(Render Array)。所有的表单、表单元素都属于“呈现数组”。但不是每一个“呈现数组”都是表单。也就是说,“呈现数组”这个概念范畴更广一点。
在Drupal7中,对于各种创建的内容,基本上都采用“呈现数组”这种方式。在本章的第一个实例代码中,页面回调函数中,我们就采用了“呈现数组”。使用这种方式的好处时,以page为例,我们可以方便的修改其它模块创建的页面内容,就像我们修改其它模块创建的表单一样方便,所不同的是这里使用hook_page_alter钩子函数。例如:
function mymodule_page_alter(&$page) {
// 把搜索表单放在页脚.
$page['footer']['search_form'] = $page['sidebar_first']['search_form'];
unset($page['sidebar_first']['search_form']);
//删除"powered by Drupal"区块
unset($page['footer']['system_powered-by']);
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们不妨把模块的名字命名为contactus,在sites/all/modules/custom目录下创建一个contactus目录,接着创建三个文件,contactus.info、contactus.module、contactus.pages.inc。我们向info文件添加以下内容:
name = 联系我们
description = 联系我们页面
core = 7.x
接着,我们向contactus.module中添加hook_menu的实现:
<?php
/**
* @file
* 方便用户联系我们.
*/
/**
* 实现钩子hook_menu().
*/
function contactus_menu() {
$items = array();
//联系我们菜单项
$items['contactus'] = array(
'tilte' => '联系我们',
'page callback' => 'contactus_page',
'type' => MENU_CALLBACK,
'access callback' =>TRUE,
'file' => 'contactus.pages.inc',
);
//确认页面菜单项
$items['contactus/confirm'] = array(
'page callback' => 'contactus_confirm_page',
'type' => MENU_CALLBACK,
'access callback' =>TRUE,
'file' => 'contactus.pages.inc',
);
//致谢页面菜单项
$items['contactus/thanks'] = array(
'page callback' => 'contactus_thanks_page',
'type' => MENU_CALLBACK,
'access callback' =>TRUE,
'file' => 'contactus.pages.inc',
);
return $items;
}
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
我们创建了三个菜单项,分别对应于联系我们、确认页面、致谢页面。接下来,我们首先为联系我们页面添加回调函数,向contactus.pages.inc中添加以下内容:
<?php
/**
* @file
* 各种页面的回调函数.
*/
/**
* “联系我们”页面的回调函数
*/
function contactus_page(){
//我们为这个页面设置标题
drupal_set_title('联系我们');
$render_array = array(
'#markup' => '',
);
//该页面的正文为一个表单,注意对于表单,这里需要使用drupal_render呈现一下。
$render_array['#markup'] .= drupal_render(drupal_get_form('contactus_form'));
//Drupal7的页面回调,返回的应该是一个数组
return $render_array;
}
Drupal需要对表单进行唯一的标识,这样当一个页面包含多个表单时,它就可以判定被提交的是哪一个表单,并且可以将表单与处理该表单的函数关联起来。为了唯一的标识表单,我们为每个表单分配了一个表单ID。在drupal_get_form()的调用中,所用的就是表单ID,如下所示:
drupal_get_form('contactus_form');
对于大多数的表单,其ID本身就是Drupal的一个函数名,因此符合Drupal中函数名的命名规则:“模块名字”+“表单描述”。例如,用户模块中的用户登录表单,它的ID为user_login,就符合这样的规则。
在表单的默认验证、提交、主题函数中,都用到了表单ID。另外,Drupal中表单ID和该表单最终生成的HTML版<form>标签中的ID属性还存在对应关系,我们很容易通过HTML版的ID来得到表单ID,对于我们的这个例子,我们可以使用firebug来查看表单的html,对于我们的这个表单,对应的html为:
<form id="contactus-form" accept-charset="UTF-8" method="post" action="/thinkindrupal/contactus">
这里我们看到,id="contactus-form",这是HTML版的ID,我们将连字符替换为下划线,就得到了表单ID,“contactus_form”。此外,在表单的HTML内部,我们还可以找到一个名为form_id的隐藏域,这里也存储了表单ID。通过这两个地方,我们可以反向的获取表单的ID,如果我们想使用form_alter修改一个已有表单时,这会非常有用。
在我们的这个模块中,我们把表单命名为了contactus_form,它的描述性并不好,不过我们的这个模块中只有这么一个表单,所以这样命名也不会引起歧义,对于中文的开发者来说,如果找不到合适的英文单词,我们可以使用自己的拼音,然后加上注释,这样更可行一点,没有必要为了一个地道的英文名字而费上半天的功夫。
现在就让我们看看contactus_form表单,接下来添加该表单的构建函数:
/**
* “联系我们”表单的构建函数
*/
function contactus_form(){
//添加我们自己的CSS,用来控制表单的样式
drupal_add_css(drupal_get_path('module', 'contactus').'/contactus.css');
//提示信息,默认为markup类型。
$form['tips'] = array(
'#prefix' =>'<div id="tips">',
'#markup' => t('<span class="form-required">*</span> 号为必填项。'),
'#suffix' =>'</div>',
);
//表单元素“姓名”
$form['name'] = array(
//表单元素的#title属性,对应于实际输出中的label
'#title' => t('姓名'),
//表单元素的类型,这里为textfield
'#type' => 'textfield',
//这个表单元素是必填的
'#required' => TRUE,
//表单元素的默认值,这里使用了三位运算符和isset进行判定
'#default_value' => isset($_SESSION['contactus_form']['name']) ? $_SESSION['contactus_form']['name'] : "",
//表单元素的描述,
'#description' => t('例如:周星驰'),
);
//表单元素“单位名称”
$form['company_name'] = array(
'#title' => t('单位名称'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => isset($_SESSION['contactus_form']['company_name']) ? $_SESSION['contactus_form']['company_name'] : "",
'#description' => t('例如:北京无名信息技术公司'),
);
//表单元素“电子邮件”
$form['mail'] = array(
'#title' => t('电子邮件'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => isset($_SESSION['contactus_form']['mail']) ? $_SESSION['contactus_form']['mail'] : "",
'#description' => t('例如:info@example.com'),
);
//表单元素“电话号码”
$form['phone'] = array(
'#title' => t('电话号码'),
'#type' => 'textfield',
'#required' => TRUE,
'#default_value' => isset($_SESSION['contactus_form']['phone']) ? $_SESSION['contactus_form']['phone'] : "",
'#description' => t('例如:010-88888888'),
);
//表单元素“邮件正文”
$form['contact'] = array(
'#title' => t('邮件正文'),
//表单元素的类型,这里为textarea
'#type' => 'textarea',
'#required' => TRUE,
'#default_value' => isset($_SESSION['contactus_form']['contact']) ? $_SESSION['contactus_form']['contact'] : "",
);
//访问来源的可选项
$visit_options = array(
'baidu' =>t('百度'),
'google' =>t('谷歌'),
'sohu' =>t('搜狐'),
'other' =>t('其它'),
);
//表单元素“访问来源”
$form['visit'] = array(
'#title' => t('访问来源'),
//表单元素的类型,这里为radios
'#type' => 'radios',
//单选按钮的可选项。
'#options' => $visit_options,
'#default_value' => isset($_SESSION['contactus_form']['visit']) ? $_SESSION['contactus_form']['visit'] : "",
//为了便于控制radios的外观,我们使用#prefix、#suffix为其添加了一个带有ID的div
'#prefix' => '<div id="visit-radios">',
'#suffix' => '</div>',
);
//表单元素“其它”,它依赖于表单元素“访问来源”
$form['other'] = array(
'#title' => t('其它'),
'#type' => 'textfield',
'#default_value' =>isset($_SESSION['contactus_form']['other'])?$_SESSION['contactus_form']['other']:"",
//这里的意思是,当表单元素“访问来源”的值为“other”时,这个表单元素才显示出来
'#states' => array(
'visible' => array(
':input[name="visit"]' => array('value' => 'other'),
),
),
);
/*
//表单元素“确认”提交按钮
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('确认'),
);
*/
//表单元素“确认”提交按钮,这里采用了图片的形式
$form['image_submit'] = array(
//表单元素的类型,这里为image_button
'#type' => 'image_button',
//图片按钮特有的#src属性,
'#src' => drupal_get_path('module','contactus').'/images/button1-1.jpg',
'#value' => 'image_sub',
//为表单元素添加两个属性,onmouseout、onmouseover,为了在鼠标移到按钮上时,显示不同的图片效果
'#attributes' =>array(
'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-1.jpg'",
'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-2.jpg'",
),
//为了便于控制image_button的外观,我们使用#prefix、#suffix
//为其添加了一个带有ID的div
'#prefix' => '<div id="image-submit">',
'#suffix' => '</div>',
);
return $form;
}
我们在这个表单中,用到了markup、textfield、radios、image_button等常用的表单元素,关于这几个表单元素和其它表单元素的详细介绍,可参看后面的表单元素一节。另外,上面的代码中,有不少的注释,我们在这里也就不再过多地重复了。我们讲解一下,这里面的重点。
我们启用这个模块,访问页面contactus。此时我们就可以看到我们创建的表单的实际效果了。如图所示:
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
如果你细心一点的话,你就会发现,我们的这个表单与默认的表单,在样式上面有所不同。是的,我们单独的为这个表单定义了自己样式。注意代码中的:
drupal_add_css(drupal_get_path('module', 'contactus').'/contactus.css');
这一代码,表示向这个表单添加CSS文件contactus.css,当加载这个表单时,就会加载这个CSS文件。通过在CSS文件中定义样式规则,我们便可以控制80%的表单外观了。首先,通过CSS代码,我们实现了表单元素的label标签与input元素左右显示,默认情况下,是上下显示的。我们来看看对应的CSS代码:
#contactus-form .form-item{
position:relative;
left:10px;
top:5px;
margin-bottom:5px;
overflow:hidden;
}
#contactus-form .form-item label{
float: left;
position:relative;
width: 180px;
font-size:12px;
}
#contactus-form .form-item input{
width: 250px;
}
在这里,我们固定了label和input的宽度,并使label向左浮动,从而实现了左右排列的效果。contactus.css文件中的更多的CSS代码,我们就不再这里列出了,大家可以登录我的网站http://zhupou.cn,来下载所有的实例模块的完整代码。
为了控制具体表单元素的外观,我么还用到了属性#prefix和#suffix。使用这两个属性,我们可以方便的为表单元素添加更具有表述性的ID:
'#prefix' => '<div id="visit-radios">',
'#suffix' => '</div>',
我们看一下,表单的最下面,我们这里使用了图片按钮,来替代默认的表单提交按钮元素。我这里的图片按钮看起来有点单薄,这是因为作者的美工效果实在不怎么样。如果换一个好一点的美工,加上上面所给的代码,一定能够实现非常酷的效果。另外注意的是,鼠标移到图片按钮上时,会变色,我们看看前后对比:
前: 后:
这是因为我们作了两个图片,通过JS,在鼠标移进和移出时变换图片,来实现的这一效果。对应代码:
'#attributes' =>array(
'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-1.jpg'",
'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button1-2.jpg'",
),
最后,让我们看一下访问来源这个表单元素,当我们选择“其它”的时候,表单显示出来一个文本输入框,允许我们输入一个具体的访问来源:
其实这里,我们做了两个表单元素,一个是visit、一个是other,后一个是一个文本输入框。对于表单元素other,我们为其使用了'#states'属性:
'#states' => array(
'visible' => array(
':input[name="visit"]' => array('value' => 'other'),
),
),
这段代码的意思是说,当表单元素visit的值等于'other'时,显示这个表单元素。我们在这里没有使用任何JS,仅仅添加了'#states',就实现了这一效果。实际上,如果我们看一下,生成的html代码,我们就会发现表单元素other是存在的,只不过被隐藏了起来,当我们点击访问来源并选择“其它”时,该元素显示了出来。在这里,Drupal替我们做了很多工作。
此外,为了控制表单的外观,我们还可以为表单添加主题函数,我们也可以使用#theme属性为任意一个表单元素添加主题函数。在默认情况下,contactus_form对应的主题函数应该为theme_contactus_form。当然,对于使用#theme属性添加的主题函数,可以根据需要按照Drupal函数的命名规则加以命名。关于主题的更多知识,我们可以参看主题系统一章。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
定义好表单以后,通常接下来的工作就是为表单添加验证函数,用来检查用户的输入,让我们在代码中添加验证函数对应的代码:
/**
* contactus_form表单的验证函数
*/
function contactus_form_validate($form, &$form_state){
//这里使用正则表达式,来验证邮箱的有效性,注意,这里的正则表达式,包围在"/...../"之间。
if(!preg_match("/^[\w\-\.]+@[\w\-\.]+(\.\w+)+$/", $form_state['values']['mail'])){
form_set_error('mail',t('您输入的电子邮件地址格式不正确'));
}
}
在这里,对于用户的输入,我们只对电子邮件字段进行了检查。如果我们在电子邮件输入框中输入了非法的电子邮件,比如:
那么当我们提交表单时,系统就会给出错误提示:
通过上面的例子,我们可以看出,form_set_error的第一个参数,对应于表单元素的ID,第二个参数为显示给用户的错误提示。
在我们的这个默认的验证函数中,我们可以为所有的表单元素添加验证规则,因此在一般情况下,一个默认的验证函数就足够了。当然,我们可以使用#validate属性,为表单添加一组验证。我们在用户系统一章,讲到的用户登录表单,就用到了#validate属性。此外,对于单个的表单元素,我们也可以为其设置验证函数,这和表单的验证函数类似,不过需要使用表单元素的#element_validate属性。
如果我们在验证函数做了大量的处理,而你又想把结果保存下来,供后面的提交函数使用,此时有两种不同的实现方式。一是使用form_set_value(),二是使用$form_state。我们来看一下$form_state的这种方式,这种方式我用到过:
由于$form_state在验证和提交函数中都是通过引用传递的,所以在验证函数中,可以将数值存储在这里,而在提交函数中,就可以使用它了。最好在$form_state中加上你模块的命名空间,这样有利于避免命名冲突。
在实际的工作中,我就用到过这一点,在一个模块的验证函数中,我需要从远程获取一个图书的ID号,这个操作由于调用的是web service,所以比较耗费资源,而在提交处理函数中,我需要使用这个ID号。
//通过调用web service获取图书的唯一ID
$book_id = my_module_get_book_id_by_service($bsno);
//我们将在验证中获取的值保存起来,这样在提交函数中可以使用。
$form_state['my_module']['book_id'] = $book_id;
接着,在提交函数中,我这样访问该数据:
$book_id = $form_state['my_module']['book_id'];
表单完全通过验证后,接下来我们需要对通过表单提交的数据进行处理。让我们为表单添加对应的提交处理函数。
/**
* contactus_form表单的提交函数
*/
function contactus_form_submit($form, &$form_state){
//把表单的值存放在会话中去,由于这里涉及到了两个不同的表单之间传值。
$_SESSION['contactus_form'] = $form_state['values'];
//表单重定向到确认页面
$form_state['redirect'] = 'contactus/confirm';
}
在我们的这个处理函数,逻辑相当简单,我们仅仅把表单的提交值,保存到了会话信息中。然后将表单重定向到了确认页面。如果我们不修改$form_state['redirect']的话,表单提交后,仍然会回到当前表单所在的页面。另外,这里最好不要使用drupal_goto,尽量使用$form_state['redirect']的方式。如果存在多个提交处理函数的话,你使用了drupal_goto,那么接下来的提交处理函数可能就被跳过了。如果我们有多个提交处理函数,每个函数中都设置了$form_state['redirect'],那么最后起作用的应该是最后设置的那个。
在两个表单页面之间传值,可以通过URL,也可以先将值保存到数据库中,然后再读取。我们这里用到的会话方式,本质上就是使用的后一种方式。刚学Drupal的时候,对这一点还不是很熟悉,总以为不应该向会话信息中保存太多信息,Java中就是这样,但是Drupal与Java不同,这里的会话信息会保存在数据库中,而不是停留在内存中供下次调用。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
接下来,让我们看一下确认页面的回调代码:
/**
* “确认”页面的回调函数
*/
function contactus_confirm_page(){
//我们为这个页面设置标题
drupal_set_title('联系我们');
//这里首先作了判断,如果会话中没有设置contactus_form,返回contactus
if(empty($_SESSION['contactus_form'])){
drupal_goto('contactus');
}else{
}
$render_array = array(
'#markup' => '',
);
//该页面的正文为一个表单,注意对于表单,这里需要使用drupal_render呈现一下。
$render_array['#markup'] .= drupal_render(drupal_get_form('contactus_confirm_form'));
//Drupal7的页面回调,返回的应该是一个数组
return $render_array;
}
这段代码和前面的类似,只是在函数的前面作了一下检查,检查会话中没有设置contactus_form,如果没有,则返回到“联系我们”页面。如果设置了,则会呈现确认表单,我们来看一下确认表单的定义:
/**
* “确认”表单的构建函数
*/
function contactus_confirm_form(){
//添加我们自己的CSS,用来控制表单的样式
drupal_add_css(drupal_get_path('module', 'contactus').'/contactus.css');
//drupal_set_message(print_r($_SESSION['contactus_form']));
//表单元素“姓名”
$form['name'] = array(
'#title' => t('姓名'),
//表单元素的类型,这里为item
'#type' => 'item',
//'#default_value' => $_SESSION['contactus_form']['name'],
//表单元素的#markup,在Drupal6下面,我用的是#value,在7下面就无法工作,改为了#default_value,还是不行,
//最后改为#markup,才可以了
'#markup' => isset($_SESSION['contactus_form']['name'])?$_SESSION['contactus_form']['name']:"",
);
//表单元素“单位名称”
$form['company_name'] = array(
'#title' => t('单位名称'),
'#type' => 'item',
//这是我在调试的时候,使用#value、#default_value、#description分别测试时的代码,这里保留了。
//'#value' => $_SESSION['contactus_form']['company_name'],
//'#value' => '123456',
'#markup' => isset($_SESSION['contactus_form']['company_name'])?$_SESSION['contactus_form']['company_name']:"",
//'#description' => '123456',
);
//表单元素“电子邮件”
$form['mail'] = array(
'#title' => t('电子邮件'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['mail'])?$_SESSION['contactus_form']['mail']:"",
);
//表单元素“电话号码”
$form['phone'] = array(
'#title' => t('电话号码'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['phone'])?$_SESSION['contactus_form']['phone']:"",
);
//表单元素“邮件正文”
$form['contact'] = array(
'#title' => t('邮件正文'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['contact'])?$_SESSION['contactus_form']['contact']:"",
);
//表单元素“访问来源”
$form['visit'] = array(
'#title' => t('访问来源'),
'#type' => 'item',
'#markup' => isset($_SESSION['contactus_form']['visit'])?$_SESSION['contactus_form']['visit']:"",
);
//如果访问来源,我们选择了“其它”,此时使用other表单元素的值来替换$form['visit']['#markup']。
if( isset($_SESSION['contactus_form']['visit']) && $_SESSION['contactus_form']['visit'] == 'other'){
$form['visit']['#markup'] = isset($_SESSION['contactus_form']['other'])?$_SESSION['contactus_form']['other']:"";
}
/*
//表单元素“返回”按钮
$form['back'] = array(
'#type' => 'submit',
'#value' => t('返回'),
'#submit' => array('contactus_confirm_form_back'),
);
//表单元素“提交”按钮
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('提交'),
);
*/
//表单元素“返回”图片按钮
$form['image_back'] = array(
'#type' => 'image_button',
'#src' => drupal_get_path('module','contactus').'/images/button2-1.jpg',
//使用这个按钮提交时,对应的提交处理函数为contactus_confirm_form_back
'#submit' => array('contactus_confirm_form_back'),
'#executes_submit_callback' => TRUE,
//为表单元素添加两个属性,onmouseout、onmouseover,为了在鼠标移到按钮上时,显示不同的图片效果
'#attributes' =>array(
'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button2-1.jpg'",
'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button2-2.jpg'",
),
);
//表单元素“提交”图片按钮
$form['image_submit'] = array(
'#type' => 'image_button',
'#src' => drupal_get_path('module','contactus').'/images/button3-1.jpg',
'#executes_submit_callback' => TRUE,
//使用这个按钮提交时,对应的提交处理函数为contactus_confirm_form_submit
'#submit' => array('contactus_confirm_form_submit'),
'#attributes' =>array(
'onmouseout' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button3-1.jpg'",
'onmouseover' => "this.src='".base_path().drupal_get_path('module','contactus')."/images/button3-2.jpg'",
),
);
return $form;
}
这个确认表单,其实比前面的第一表单还要简单一些,这里主要就用到了item元素。而图片按钮的用法则和前面一致。对于item元素,我最初使用'#value'属性,后来改为'#default_value'属性,但总是显示不出来值,最后才改用'#markup'。注释里面讲到了这一点,这里再强调一下,因为在Drupal6里面,我用的是'#value'属性,能够正常工作,在Drupal7下面却显示不出来,所以这里给以前熟悉Drupal6的开发者提个醒,避免出现同样的错误。
对于这个确认页面,我们仍然为其添加了CSS文件,用来控制样式,当我们提交表单后,得到一个如下图所示的确认表单。
当我们点击“返回”按钮,就会回到前面的表单页面,当我们点击“提交”按钮时,就会正式的提交填写的数据。
这个表单,我们没有定义验证函数,因为这里没有输入框。对于“返回”按钮和“提交”按钮,我们分别为其使用'#submit'属性为其定义了对应的提交处理函数。我们接下来看看这两个处理函数:
/**
* 返回按钮对应的提交函数
*/
function contactus_confirm_form_back($form, &$form_state){
//简单的重定向到contactus页面
$form_state['redirect'] = 'contactus';
}
/**
* 提交按钮对应的提交函数
*/
function contactus_confirm_form_submit($form, &$form_state){
$values = NULL;
//从会话中获取用户最初提交的值,并将$_SESSION['contactus_form']置为空。
if(empty($_SESSION['contactus_form'])){
drupal_goto('contact');
}else{
$values = $_SESSION['contactus_form'];
unset($_SESSION['contactus_form']);
}
//收件人地址,这里为作者的邮箱
$to = 'g089h515r806@gmail.com';
//用户填写的邮箱地址
$from = $values['mail'];
//发送邮件
drupal_mail('contactus', 'contact', $to, language_default(), $values, $from);
//简单的重定向到致谢页面
$form_state['redirect'] = 'contactus/thanks';
}
“返回”按钮的提交处理函数contactus_confirm_form_back的逻辑非常简单,就是一个重定向。“提交”按钮的处理函数contactus_confirm_form_submit,则包含获取用户最初提交的数据、发送邮件、重定向页面等三个组成部分。这个提交处理函数,是我们整个模块中最复杂的一个了,很多实际的提交处理函数可能比这个还要复杂一点。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
这里我们用到了Drupal的邮件发送,drupal_mail是一个API函数,专门用来发送邮件的。我们这里做一下简单的介绍:
drupal_mail($module, $key, $to, $language, $params = array(), $from = NULL, $send = TRUE)
$module表示hook_mail()所触发的模块的名字,也就是我们将调用{$module}_mail();
$key是hook_mail中定义的键。
$to表示收件人地址。
$language表示合成邮件所用的语言对象,默认为当前语言。
$params,表示用于构建电子邮件的可选参数
$from,表示发件人地址。
$send,表示直接发送邮件。
下面是我们的hook_mail的具体实现:
/**
* 实现钩子hook_mail().
*/
function contactus_mail($key, &$message, $params){
$language = $message['language'];
switch ($key) {
case 'contact':
//邮件的标题
$message['subject'] = '联系我们';
//邮件正文,这里面包含:姓名、单位名称、电子邮件、电话号码、邮件正文、访问来源
$message['body'][] = '姓名:'.$params['name'];
$message['body'][] = '单位名称:'.$params['company_name'];
$message['body'][] = '电子邮件:'.$params['mail'];
$message['body'][] = '电话号码:'.$params['phone'];
$message['body'][] = '邮件正文:'.$params['contact'];
//对于访问来源,如果visit的值我们选择了“其它”,那么此时我们取$params['other'],否则取$params['visit']
$visit = "";
if($params['visit'] == 'other'){
$visit = $params['other'];
}else{
$visit = $params['visit'];
}
$message['body'][] = '访问来源:'. $visit;
}
}
这里$message['body']表示邮件的正文部分,这里把用户提交的信息全部加了进来。这里的$message是一个数组,除了上面用到的subject、body以外,还可以用的键有id、to、from、headers。$key表示邮件的标识。$params则是传递过来用于构建邮件的关联数组。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
最后让我们看一下,“致谢”页面的回调代码,这里仅仅用来显示一段感谢语:
/**
* “致谢”页面的回调函数
*/
function contactus_thanks_page(){
//我们为这个页面设置标题
drupal_set_title('联系我们');
$render_array = array(
'#markup' => '',
);
//Drupal7的页面回调,返回的应该是一个数组
$render_array['#markup'] .= '<div id="contactus-thanks">';
$render_array['#markup'] .= t('感谢您的来信,我们会在第一时间给您回复。');
$render_array['#markup'] .= '</div>';
return $render_array;
}
通过这个例子,希望大家能够熟悉表单的定义,验证函数、提交函数的编写,以及了解两部表单的实现方法。