对于Web开发者来说,处理HTML表单是最常见的任务(也是挑战)之一。Symfony2集成了Form组件以使得表单处理更为容易。在本章中,你将从头开始构建一个复杂的表单,并且学会表单库中大部分的重要功能。
Symfony2表单组件是一个独立库,它可以在Symfony2项目之外使用。更多详情参见Github中的Symfony2表单组件。
创建一个简单的表单
假设你正在构建一个商店应用程序,它需要显示产品。因为你的用户需要编辑和创建产品,你将需要构建一个表单。但在你开始之前,让我们关注一下Product类,该类代表和保存单个产品的数据:
- // src/Acme/StoreBundle/Entity/Product.php
- namespace Acme\StoreBundle\Entity;
- class Product
- {
- public $name;
- protected $price;
- public function getPrice()
- {
- return $this->price;
- }
- public function setPrice($price)
- {
- $this->price = $price;
- }
- }
注:如果你要编写这个例子,那么请确保创建和启动AcmeStoreBundle。运行下列命令并按照屏幕的指示进行:
- php app/console init:bundle Acme/StoreBundle src/
这种类通常被称为“简单PHP对象”(POPO),因为到目前为止它并没有与Symfony2或其它任何库相关。它是个非常简单的,要直接为你应用程序解决问题的标准PHP对象(如它在你的应用程序里代表产品)。当然在本章最后,你将可以提供数据到Product实例(通过表单),验证它的数据,并将其持久化到数据库。
截止到现在,你并没有做任何与“表单”相关的操作,你只是简单地创建了一个PHP类,该类将帮助你解决你应用程序中的问题。本章表单构建的目标是允许你的用户与Product对象的数据交互。
构建表单
现在你已经创建了一个Product类,下一步就是创建并渲染实际的HTML表单。在Symfony2中,可以通过构建Form对象,然后在模板中渲染来实现。这一切都可以通过一个内部的控制器来实现:
- // src/Acme/StoreBundle/Controller/DefaultController.php
- namespace Acme\StoreBundle\Controller;
- use Symfony\Bundle\FrameworkBundle\Controller\Controller;
- use Acme\StoreBundle\Entity\Product;
- class DefaultController extends Controller
- {
- public function indexAction()
- {
- // create a product and give it some dummy data for this example
- $product = new Product();
- $product->name = 'Test product';
- $product->setPrice('50.00');
- $form = $this->createFormBuilder($product)
- ->add('name', 'text')
- ->add('price', 'money', array('currency' => 'USD'))
- ->getForm();
- return $this->render('AcmeStoreBundle:Default:index.html.twig', array(
- 'form' => $form->createView(),
- ));
- }
- }
稍后,当你为Product对象添加验证时,你将学会配置表单项更简便的方法。该方法将在后面表单项类型猜测一节说明。
在Symfony2中构建一个表单是简便的,因为Form对象是通过“表单构造器”来构造的。表单构造器是一个对象,你可以与之交互用来帮助你容易地创建表单对象。
在本例中,你已经为你表单添加了两个表单项,name和price,它们与Porduct类的name和price属性相对应。表单项name是文本类型,意味着用户可以提交简单本文到该项。price是money类型,这是个一特殊的文本类型,可以用区域货币格式显示并提交。Symfony2附带了许多内建类型,我们稍后讨论(参见内建表单项类型)
现在表单已经创建了,下一步就是去渲染它。这一点可以很容易做到。只需要通过发送特殊的表单“视图”对象到你的模板(参见上面控制器中的$from->createView()方法),并使用一些表单帮手函数:
- {# src/Acme/StoreBundle/Resources/views/Default/index.html.twig #}
- <form action="{{ path('store_product') }}" method="post" {{ form_enctype(form) }}>
- {{ form_widget(form) }}
- <input type="submit" />
- </form>
就是这样!通过打印from_widget(form),表单中的每个伴着标签和最终错误消息的表单项都被渲染。因为它如此容易,所以它(还)不是很灵活。稍后,你将学会如何自定义表单输出。
在继续之前,注意如何渲染name输入表单项,该表单项拥有来自$product对象(如:"Test product”)的name属性值。表单的第1项工作:从对象得到数据并将它转换成适当的格式,以便在HTML表单中渲染。
表单系统足够智能,可以通过Product类中的getPrice()和setPrice()方法来访问保护属性price的值。除非属性是公共的,否则它必须有"getter"和"setter“方法,以便表单组件可以取得和设置属性。对于布尔属性而言,你可以使用"isser"方法(如isPublished())来代替"getter"方法(如getPublished())。
处理表单提交
表单的第2项工作就是将用户提交的数据返回到对象的属性。为了做到这一点,用户提交的数据必须被限定。在你控制器中添加以下功能:
- public function indexAction()
- {
- // just setup a fresh $product object (no dummy data)
- $product = new Product();
- $form = $this->createFormBuilder($product)
- ->add('name', 'text')
- ->add('price', 'money', array('currency' => 'USD'))
- ->getForm();
- $request = $this->get('request');
- if ($request->getMethod() == 'POST') {
- $form->bindRequest($request);
- if ($form->isValid()) {
- // perform some action, such as save the object to the database
- return $this->redirect($this->generateUrl('store_product_success'));
- }
- }
- // ...
- }
现在,当提交表单时,控制器会将提交的数据绑定到表单,它将把数据送回给$product对象的name和price属性。这一切通过bindRequest()方法完成。
bindRequest()一被调用,提交的数据就被立即传送给底层对象。举个例子,假定 name表单项的值Foo被提交:
- $product = new Product();
- $product->name = 'Test product';
- $form->bindRequest($this->get('request'));
- echo $product->name;
上面的语句将echo Foo,因为bindRequest最终将提交的数据传返回给$product对象。
该控制器遵循一个处理表单的通用模式,有三种可能的路径:
- 当在浏览器中最初引导表单时,请求方法是GET,这意味着表单可以很方便地被创建和渲染(但没有限制);
- 当用户提交表单时(如方法是POST),但提供的数据是无效的(验证将在下一节说明),表单被限制,然后被渲染,这时显示所有的验证错误;
- 当用户提交有效数据的表单时,表单被限制,在重定向用户到其它页(如“谢谢”或“成功”页面)之前,你有机会用$Product对象去执行一些操作(如将其持久化到数据库)。
在成功提交表单之后将用户重定向可以防止用户去点击“刷新”按钮并再次提交数据。
表单验证
在上一节,你学到了用表单来提交有效数据或无效数据会如何。在Symfony2中 ,验证被应用于底层对象(如:Product)。换句话说,问题不是“表单”是否合法,而是表单提交数据给$product之后,该对象是否合法。调用$form->isValid()是询问$product对象是否拥有有效数据的快捷方法。
验证可以通过向类添加一个工具集(称为限制)来实现。在本例中,添加验证限制以便name表单项不能为空,price表单项不能为空且必须是非负数:
- Acme\StoreBundle\Entity\Product:
- properties:
- name:
- - NotBlank: ~
- price:
- - NotBlank: ~
- - Min: 0
就是这样!如果你使用非法数据重新提交表单,你将看到表单会打印出相应的错误。
如果你有看一下生成的HTML代码,你会发现Form组件会生成的HTML5新字段中包含了特殊的"required"属性,它可以通过Web浏览器来直接强制进行一些验证。一些现代浏览器如Firefox4、Chrome3.0或Opera9.5理解这一特殊的"required"属性。
验证是Symfony2一个非常强大功能,并拥有它自己独立的章节。
内建表单项类型
Symfony2标配了大量的表单项类型,它涵盖了所有的常见表单项以及你所遇到的数据类型:
表单项组
常用表单项类型选项
你也许注意到price表单项是附带着选项数组发送的:
- ->add('price', 'money', array('currency' => 'USD'))
每个字段类型有大量的选项是可以随之一起发送。对于每个类型而言,它们许多都指定了表单项的类型和细节,这样都可以在文档中找到。然而,一些选项也在大多数表单项中共享:
-
- required [type: Boolean, default: true]
-
required选项可以用来渲染成HTML5的required属性. 注意它是独立于表单验证的:如果你在表单项包含了required属性,但忽略了required验证,那么该对象将在你的应用程序中显示一个空值。换句话说,这是一个很好地功能,可以为支持HTML5的浏览器提供基于客户端的验证。然而,这并不能代替服务端验证。
-
- max_length [type: integer]
-
该选项被用于添加max_length属性,该属性通过浏览器来限制表单项中文本的长度。
表单类型猜测
现在你已经为Produce类添加了验证元数据,Symfony2已经知道一点关于你表单项的情况。如果你允许,Symfony2可以“猜”你表单项的类型,并为你设置它。在本例中,Symfony2将从验证规则中猜出name和price表单项是正常的文本选项。因为对于name表单项来说是对的,所以你可以修改你的代码以便Symfony2为你猜表单项:
- public function indexAction()
- {
- $product = new Product();
- $form = $this->createFormBuilder($product)
- ->add('name')
- ->add('price', 'money', array('currency' => 'USD'))
- ->getForm();
- }
name表单项的文本类型现在已被忽略,因为它能被验证规则正确地猜出。然而price表单项仍然保持money类型,因为它比系统能够猜出的(text)更特殊。
createBuilder()方法最多需要3个参数(但只有第一个是必须的):
- 字符串form代表你正在构建的(表单),也被用来当作表单名。如果你查看生成的代码,两个菜单项被命名为:name=form[name]和name=form[name];
- 缺省数据用于初始化表单项。该参数可以是一个关联数据或象本例中的POPO;
- 表单的选项数组。
本例非常简单,但表单项猜测却可以节省大量的时间。正如你稍后所见,添加Doctrine元数据可以更加提高系统猜表单项类型的能力。
在模板中渲染表单
到目前为止,你已经看到了整个表单是如何通过一行代码就被渲染的。当然,在渲染时你需要更多的灵活性:
- {# src/Acme/StoreBundle/Resources/views/Default/index.html.twig #}
- <form action="{{ path('store_product') }}" method="post" {{ form_enctype(form) }}>
- {{ form_errors(form) }}
- {{ form_row(form.name) }}
- {{ form_row(form.price) }}
- {{ form_rest(form) }}
- <input type="submit" />
- </form>
让我们看看每个部分:
- form_enctype(form) - 如果至少有一个文件上传的表单项,那么这个渲染就必须强制为enctype="multipart/form-data";
- form_errors(form) - 渲染整个表单的全局错误(特定表单项的错误将显示在该表单项的后面);
- form_row(form.price) - 渲染标签、错误和指定表单项(如price)的HTML表单部件;
- form_rest(form) - 渲染任何尚未渲染的表单项。这通常是个好主意:在每个表单的底部放置对该帮手函数的调用(如果你忘记输出一个表单项或者不想手工渲染隐藏表单项)。该帮手函数在利用自动进行CSRF攻击保护方面也是很有用的。
大部分工作已经通过form_row帮手函数完成了。在缺省状态下,该函数可以渲染在div标识中的标签、错误和每个表单项的HTML表单部件。在表单主题化一节,你将学习form_row输出是如何在许多不同等级上定制。
手工渲染每个表单项
form_row 帮手函数是有用的,因为它可以使你非常快速地渲染你表单中的每个表单项(为"row"使用的标识还可以自定义)但是生活并不总是简单的,你也可以手工渲染整个表单的表单项:
- {{ form_errors(form) }}
- <div>
- {{ form_label(form.name) }}
- {{ form_errors(form.name) }}
- {{ form_widget(form.name) }}
- </div>
- <div>
- {{ form_label(form.price) }}
- {{ form_errors(form.price) }}
- {{ form_widget(form.price) }}
- </div>
- {{ form_rest(form) }}
如果自动生成的表单项标签并不正确,你也可以指定它:
- {{ form_label(form.name, 'Product name') }}
最后,一些有着附加渲染选项的表单项类型被发送到部件。这些选项记录着每个类型,但一个常用的选项是attr,可以让你去修改表单元素的属性。下面的例子将name_field类添加到输入文本表单项中:
- {{ form_widget(form.name, { 'attr': {'class': 'name_field'} }) }}
Twig模板函数参考
如果你使用Twig,一个关于表单渲染函数的完全参考在参考手册中可以找到。
创建Form类
如你所见,可以在控制器中直接创建表单并使用它。然而更好地方法是在独立、隔离的PHP类中构建表单,然后该类可以在你应用程序的任何地方重用。创建一个新类将包含构建产品表单的逻辑:
- // src/Acme/StoreBundle/Form/ProductType.php
- namespace Acme\StoreBundle\Form;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormBuilder;
- class ProductType extends AbstractType
- {
- public function buildForm(FormBuilder $builder, array $options)
- {
- $builder->add('name');
- $builder->add('price', 'money', array('currency' => 'USD'));
- }
- }
这个新类包含所有创建产品表单所需要的语句。它也被用于在控制器中快速构建表单对象:
- // src/Acme/StoreBundle/Controller/DefaultController.php
- // add this new use statement at the top of the class
- use Acme\StoreBundle\Form\ProductType;
- public function indexAction()
- {
- $product = // ...
- $form = $this->createForm(new ProductType(), $product);
- // ...
- }
你也可以通过setData()方法在表单上设置数据:
- $form = $this->createForm(new ProductType());
- $form->setData($product);
如果你使用setData方法,并想利用表单项类型猜测,那么请确保在你的表单类中添加了下列内容:
- public function getDefaultOptions(array $options)
- {
- return array(
- 'data_class' => 'Acme\StoreBundle\Entity\Product',
- );
- }
这是必须的,因为在表单项类型猜测之后对象将被发送给表单。
将表单逻辑放置到它自己的类中意味着表单可在很容易地在你应用程序的任何地方重用。这是创建表单的最好方式,但选择权最终在你。
表单和Doctrine
表单的目标就是从对象(如Product)将数据转换成HTML表单,然后将用户提交的数据传回原始对象。因此将Product对象持久化到数据库和表单是完全无关的。如果你已经配置成通过Doctrine持久化Product类,那么当表单提交之后,如果表单有效,那么将实现持久化:
- if ($form->isValid()) {
- $em = $this->get('doctrine')->getEntityManager();
- $em->persist($product);
- $em->flush();
- return $this->redirect($this->generateUrl('store_product_success'));
- }
如果因为某种原因,你没有权限访问你的原始$product对象,你将从表单中实现它:
- $product = $form->getData();
更多信息请参见Doctrine ORM章节
关键是要理解当表单被绑定时,提交的数据被立即传送给底层对象。如果你想持久化数据,你只需要简单地去持久化对象本身(已经包含了提交的数据)即可。
如果表单的底层对象(如Product)被Doctrine ORM映射,那么表单框架可以利用信息,元数据验证,去猜特定的表单项类型。
内嵌表单
通常,你想要构建包含许多不同的对象的表单项。举个例子,注册表单也许包含隶属于User对象和许多Address对象的数据。幸运地是,通过表单组件可以很容易、自然地实现该功能。
内嵌单个对象
假设每个Product都属于单个Category对象:
- // src/Acme/StoreBundle/Entity/Category.php
- namespace Acme\StoreBundle\Entity;
- use Symfony\Component\Validator\Constraints as Assert;
- class Category
- {
- /**
- * @Assert\NotBlank()
- */
- public $name;
- }
Product类有一个新的$category属性,说明它属于哪个Category:
- use Symfony\Component\Validator\Constraints as Assert;
- class Product
- {
- // ...
- /**
- * @Assert\Type(type="Acme\StoreBundle\Entity\Category")
- */
- protected $category;
- // ...
- public function getCategory()
- {
- return $this->category;
- }
- public function setCategory(Category $category)
- {
- $this->category = $category;
- }
- }
现在更新你的应用程序以反映新的需求,创建一个form类以便Category对象可以被用户修改:
- // src/Acme/StoreBundle/Form/CategoryType.php
- namespace Acme\StoreBundle\Form;
- use Symfony\Component\Form\AbstractType;
- use Symfony\Component\Form\FormBuilder;
- class CategoryType extends AbstractType
- {
- public function buildForm(FormBuilder $builder, array $options)
- {
- $builder->add('name');
- }
- public function getDefaultOptions(array $options)
- {
- return array(
- 'data_class' => 'Acme\StoreBundle\Entity\Category',
- );
- }
- }
name表单项的类型已经从Category对象的元数据验证中被猜出(作为文本表单项)。
最终目标是让Product的Category在产品表单中修改正确。为了做到这一点,将category表单项添加到ProductType对象,它的类型是新建的CategoryType类的一个实例:
- public function buildForm(FormBuilder $builder, array $options)
- {
- // ...
- $builder->add('category', new CategoryType());
- }
CategoryType的表单项现在可以与来自ProductType类的其它表单项一起被渲染。与渲染原始Product表单项一样渲染Category表单项:
- {# ... #}
- {{ form_row(form.price) }}
- <h3>Category</h3>
- <div class="category">
- {{ form_row(form.category.name) }}
- </div>
- {{ form_rest(form) }}
- {# ... #}
当用户提交表单时,Category表单项所提交的数据被合并进Category对象中。换句话说,正如主Product对象一样,一切正常。Category实例可以通过$product->getCategory()访问,并且可以被持久化到数据库或按你所需来使用。
内嵌一个表单集
你也可以在一个表单中内嵌一个表单集。这可以通过使用collection表单项类型来实现。假设你有一个属性叫reviews,对应的类叫ProductReviewType,你可以在下面的ProductType中实现:
- public function buildForm(FormBuilder $builder, array $options)
- {
- // ...
- $builder->add('reviews', 'collection', array(
- 'type' => new ProductReviewType(),
- ));
- }
表单主题化
表单渲染的每一步都是可以自定义的。你可以自由改变每个表单“row“的渲染、改变用于渲染错误的标识,甚至自定义一个将被渲染的textarea标签。没有什么不能做到,并且不同的定制可以用于不同的地方。
Symfony2使用模板来渲染表单的每一部分。在Twig中,表单每一个不同的部分,如row、textarea标签和错误,是通过Twig的“区块”来显示的。要定制表单渲染的每一步,你只需要覆写相应的区块即可。
为了理解它是如何工作的,让我们定制一个form_row输出,并向包括每个row的div元素添加类属性。要做到这一点,创建一个有着新标识的新模板文件:
- {# src/Acme/StoreBundle/Resources/views/Form/fields.html.twig #}
- {% extends 'TwigBundle:Form:div_layout.html.twig' %}
- {% block field_row %}
- {% spaceless %}
- <div class="form_row">
- {{ form_label(form) }}
- {{ form_errors(form) }}
- {{ form_widget(form) }}
- </div>
- {% endspaceless %}
- {% endblock field_row %}
field_row区块是通过form_row函数渲染大多数表单项所使用的区域名。要使用该模板中定义的field_row区块,在渲染该表单的模板上方添加下列面容:
- {# src/Acme/StoreBundle/Resources/views/Default/index.html.twig #}
- {% form_theme form 'AcmeStoreBundle:Form:fields.html.twig' %}
- <form ...>
form_theme标签“导入”到模板,在渲染表单时可以使用所有与表单相关区块。换句话说,当模板的form_row被稍后调用时,它将从fields.html.twig模板中使用field_row区块。
要自定义表单的任何部分,你仅需要去覆写相应的区块。精确地知道要覆写哪个区块是下一节的内容。
在接下来的一节中,你将学到更多关于如何自定义表单的不同部分。为了能进行更加广泛地讨论,请参见在Twig模板中如何定制表单渲染。
表单模板区块
表单中被渲染的每一部分,HTML表单元素、错误、标签等,都被作为独立的Twig区块被定义在一个基本模板。缺省状态下,每个区块需要保存在核心TwigBundle的div_layout.html.twig文件中定义,在这个文件中,你可以看到表单中需要渲染的每个区块和缺省的表单项类型。
每个区块都遵循相同的基本模式,被单个下划线(_)分成两个部分,下面是一些示例:- field_row - 被form_row用来渲染大部分表单项;
- textarea_widget - 被form_widget用来渲染textarea表单项类型;
- field_errors - 被form_errors用来渲染表单项错误。
每个区块都遵循相同的基本模板:type_part。type部分对应被渲染的表单项类型(例如文本域或复选框),而part部分对应的是被渲染的是什么(如标签,部件)。缺省情况下,表单有7个部分可以被渲染:
label | (e.g. field_label) | 渲染表单项的标签 |
widget | (e.g. field_widget) | 渲染表单项的HTML表现 |
errors | (e.g. field_errors) | 渲染表单项的错误 |
row | (e.g. field_row) | 渲染表单项的整个row (label+widget+errors) |
rows | (e.g. field_rows) | 渲染表单的子row |
rest | (e.g. field_rest) | 渲染表单中未渲染的表单项 |
enctype | (e.g. field_enctype) | 渲染表单的enctype属性 |
知道表单项类型(如:textarea)和你想定制的那个部分(如:widget),你可以构建需要被覆写的区块名( 如textarea_widget)。定制区域的最好方式是从div_layout.html.twig拷到新模板,定制它,然后如先前的例子所显示的那样,使用form_theme标签。
表单类型区块继承
在某种情况下,你想要定制的区块没有显示。举个例子,如你在div_layout.html.twig文件中所见,你没有看到textarea_errors区块。所以如何为textarea表单项渲染错误呢?
答案是:通过field_error区块。当Symfony2为textarea类型渲染错误时,它首先查找textarea_errors区块(textarea的父类型是表单项),如果基本区块不存在的话,Symfony2将使用父类型区块。
所以,如果只是要覆写textarea表单项,那么拷贝field_errors区块,并将其重命名为textarea_errors,并定制它。要为所有表单项定义缺省的错误,直接拷贝并定制field_errors区块。
全局表单主题化
到目前为止,你已经知道如何在模板使用form_theme的Twig区块去自定义表单。你也可以告诉Symfony2在你的应用程序中的所有模板去自动使用某些自定义表单。要从先前创建的fields.html.twig模板中自动包含自定义区块,请修改你的配置文件:
- # app/config/config.yml
- twig:
- form:
- resources: ['AcmeStoreBundle:Form:fields.html.twig']
- # ...
任何在fields.html.twig模板中的区块现在被全局地用于定义表单输出。
在单个文件中自定义表单输出
你也可以在模板中拥有根据需要自定义表单区块的权力。但注意这种方法仅当模板通过{% extends %}来继承一些基本模板时才有用:
- {% extends '::base.html.twig' %}
- {% form_theme form _self %}
- {% use 'TwigBundle:Form:div_layout.html.twig' %}
- {% block field_row %}
- {# custom field row output #}
- {% endblock field_row %}
- {% block content %}
- {# ... #}
- {{ form_row(form.name) }}
- {% endblock %}
The {% form_theme form_self %} 标签允许在模板中直接定义表单区块。使用这一方法可以快速自定义表单输出,而这只需要在一个模板中定义。
use标签也是有帮助的,它可以让你访问在div_layout.html.twig中定义的所有区块。举个例子,要遵循下面表单自定义,使用use语句是必须的,因为它让你访问在div_layout.html.twig中的attributes区块:
- {% block text_widget %}
- <div class="text_widget">
- <input type="text" {{ block('attributes') }} value="{{ value }}" />
- </div>
- {% endblock %}
CSRF攻击保护
CSRF - 或 跨站请求伪造 - 是一种恶意用户尝试提交一个合法用户不知道也不想提交的数据。幸运的是,你可以通过在你表单中使用CSRF令牌来阻止CSRF攻击。
好消息是,缺省情况下,Symfony2为你自动内嵌了验证CSRF令牌。这意味着你可以无须做任何设置就可以使用CSRF保护。实际上,本章中的每一个表单都已经使用了CSRF保护!
CSRF保护通过在你表单中添加一个表单项来实现,这个表单项缺省被称为_token,它包含一个只有你和你用户才知道的值。这样就确保了用户,不是一些其它的实体,提交了数据。Symfony2自动验证令牌以及它的精度。
_token是一个隐藏表单项,如果你在模板中包含了form_rest()函数(该函数确保所有未渲染的表单项被输出),它将被自动渲染。
CSRF令牌可以被自定义,例如:
- class ProductType extends AbstractType
- {
- public function getDefaultOptions(array $options)
- {
- return array(
- 'data_class' => 'Acme\StoreBundle\Entity\Product',
- 'csrf_protection' => true,
- 'csrf_field_name' => '_token',
- 'intention' => 'product_creation',
- );
- }
- }
要禁用CSRF保护,将csrf_protection选项设为false。也可以在你项目中进行全局定制。更多信息请参见表单配置参考一节。
intention选项是可选的,但是通过在不同表单中设置不同的值,它可以极大地提高生成令牌的安全性。
最后一点
你现在知道了要为你应用程序构建复杂、多功能的表单就必须构建区块。当创建表单时,请记住表单的第一个目标是将数据从一个对象(Product)转换成一个HTML表单,以便用户修改数据。第二个目标是通过用户提交数据并把它重新应用于对象。
还可以学习更多关于强大的表单世界的内容,如怎样处理文件上传、怎样创建拥有大量动态子表单的表单(如待办列表,你需要在提交之前通过Javascript添加更多的表单项)。参见相关主题的食谱(cookbook)。
本文转自 firehare 51CTO博客,原文链接:http://blog.51cto.com/firehare/582026,如需转载请自行联系原作者