如何嵌入表单集合
正在修复中: 阅读时请注意,本文档以下部分正待修复。我们将尽快完成校对。
在此章,你将会学习到如何创建一个多集合的表单。他可能会很有用,例如,你有一个Task
(任务)类并且你还想在一个表单中,编辑/创建/删除任务类下的多个Tag
(标签)对象。
Note
在这一章中,假设你使用doctrine在操作数据库存储。但是,如果你不是使用doctrine(如你使用的是 Propel 或者只是使用数据库连接)这些都差不多。persistence仅仅是这个教程的一部分。
如果你正在使用Doctrine,你需要去添加Doctrine元数据,让Task(任务)和Tag
(标签)实体形成ManyToMany
映射关系。
首先,假设每一个 Task 有多个 Tag
对象。开始建立简单的 Task
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Task
{
protected $description;
protected $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getTags()
{
return $this->tags;
}
} |
Note
ArrayCollection
是 Doctrine 特有的并且基本上和使用 array
一样(但是如果你使用 Doctrine 必须是ArrayCollection
)。
现在,创建一个Tag
类。正如你看到的,一个Task
可以有很多的Tag
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// src/AppBundle/Entity/Tag.php
namespace AppBundle\Entity;
class Tag
{
private $name;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
} |
然后,创建一个Tag
对象的表单类让用户能够修改他:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/AppBundle/Form/Type/TagType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tag',
));
}
} |
有了这个,它就能自己渲染tag表单。但是最终目标是要让 Task
的 tag 可以在 task
表单中自己修改,所以还要创建Task的form类
请注意,你嵌入的TagType
表单集合要使用collectionType 字段类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Task',
));
}
} |
在你的控制器中,你创建一个TaskType
的新表单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// src/AppBundle/Controller/TaskController.php
namespace AppBundle\Controller;
use AppBundle\Entity\Task;
use AppBundle\Entity\Tag;
use AppBundle\Form\Type\TaskType;
use Symfony\Component\Httpfoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class TaskController extends Controller
{
public function newAction(Request $request)
{
$task = new Task();
// dummy code - this is here just so that the Task has some tags
// otherwise, this isn't an interesting example
$tag1 = new Tag();
$tag1->setName('tag1');
$task->getTags()->add($tag1);
$tag2 = new Tag();
$tag2->setName('tag2');
$task->getTags()->add($tag2);
// end dummy code
$form = $this->createForm(TaskType::class, $task);
$form->handleRequest($request);
if ($form->isValid()) {
// ... maybe do some form processing, like saving the Task and Tag objects
}
return $this->render('AppBundle:Task:new.html.twig', array(
'form' => $form->createView(),
));
}
} |
现在相应的模板能够渲染task(任务)表单下的description
字段以及TagType
表单下的所有tag(标签)都已经关联到了这个Task
。上面的控制器,我添加了一些伪代码所以你能够在action中看到(因为 Task
在最初创建时并没有 tag
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{# src/AppBundle/Resources/views/Task/new.html.twig #}
{# ... #}
{{ form_start(form) }}
{# render the task's only field: description #}
{{ form_row(form.description) }}
<h3>Tags</h3>
<ul class="tags">
{# iterate over each existing tag and render its only field: name #}
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
{{ form_end(form) }}
{# ... #} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!-- src/AppBundle/Resources/views/Task/new.html.php -->
<!-- ... -->
<?php echo $view['form']->start($form) ?>
<!-- render the task's only field: description -->
<?php echo $view['form']->row($form['description']) ?>
<h3>Tags</h3>
<ul class="tags">
<?php foreach($form['tags'] as $tag): ?>
<li><?php echo $view['form']->row($tag['name']) ?></li>
<?php endforeach ?>
</ul>
<?php echo $view['form']->end($form) ?>
<!-- ... --> |
当用户提交表单时,tags
字段的提交的数据将会用于构造 Tag
对象的 ArrayCollection
,用来设置Task
实例的tag
字段。
这个tags集合你就理解为$task->getTags()并保存在数据库中,你随时都可以使用。
到目前为止,这些工作都很重要,但是这些并没有允许你去动态的添加一个新的tag(标签)或者删除现有的tag(标签)。所以,编辑现有tag(标签)是非常好用的,可是你的用户不能添加任何新的tag(标签)。
Caution
在这一章,你仅仅嵌入了一个集合,但你不会局限于此。你也能嵌入你想要的集合。但如果你在开发过程中使用Xdebug,你可能会有一个Maximum function nesting level of '100' reached, aborting!
错误。这是由于php设置的xdebug.max_nesting_level默认为100。PHP中当函数调用层数超过限制的时候就会出现。
他直接将嵌套循环限制在100,如果你一次渲染整个表单(如form_widget(form)
)那么在渲染表单模板时可能不够用。为了避免这样的情况,你可以设置一个较高的值(通过php.ini
文件或者在app/autoload.php
通过ini_set
)或者手动使用form_row
去渲染每一个表单。
允许“prototype”的“新”Tag ¶
允许用户动态添加新的 tag,这就意味着你需要使用一些 JavaScript。之前你在控制器中添加了两个tag(标签)到你的表单。现在让用户在浏览器直接添加更多的tag(标签)到表单。你将会使用一些javascript来完成。
你需要做的第一件事就是去让你的表单知道tag(标签)的数量是未知的。到目前为止,你已经添加了两个tag(标签)并且你的表单类型期望就是两个,否则会抛出一个错误:This form should not contain extra fields
。为了让他更加灵活,添加allow_add
配置选项到你的集合字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/AppBundle/Form/Type/TaskType.php
// ...
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class,
'allow_add' => true,
));
} |
告诉这个字段去接收多个对象之外,allow_add
也为你给出了一个“prototype”变量。这个“prototype”是一个小“模板(template)”它包含了所有渲染tag表单的html。为了渲染他,你的模板要做出以下修改:
1 2 3 |
<ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
...
</ul> |
1 2 3 4 5 |
<ul class="tags" data-prototype="<?php
echo $view->escape($view['form']->row($form['tags']->vars['prototype']))
?>">
...
</ul> |
Note
如果你一次性渲染你的“tag”子表单(如,form_row(form.tags)
),那么prototype会在你自定生成的div中添加data-prototype属性,和上面的类似。
Tip
form.tags.vars.prototype 是表单元素,他看起来就像是form_widget(tag)
元素里的for
循环。这意味着你可以调用form_widget
, form_row
和 form_label
。你甚至可以选择只渲染它的一个字段(如 name
字段):
1 |
{{ form_widget(form.tags.vars.prototype.name)|e }} |
在这个渲染页面,出来的结果如下:
1 2 3 |
<ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
...
</ul> |
本章的目的就是使用javascript去读取这个属性并且当用户点击 "Add a tag" 链接是,动态加载新的tag表单。为了让他简单些,这个例子中使用了jquery并且你的项目已经包含了这个包。
在页面上添加script
标签,你就可以开始编写一些javascript了。
首先,在“tag”列表的底部用javascript添加一个链接。然后,绑定链接的点击事件,这样你就能添加一个新的tag表单了(addTagForm
函数将会在下一段代码展示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var $collectionHolder;
// setup an "add a tag" link
var $addTagLink = $('<a href="#" class="add_tag_link">Add a tag</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
$collectionHolder = $('ul.tags');
// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTagForm($collectionHolder, $newLinkLi);
});
}); |
这个addTagForm
函数的工作就是当链接被点击的时候,使用data-prototype
属性动态添加一个新的表单。data-prototype
包含了名为task[tags][__name__][name]
的tag text
input 元素,并且id为 task_tags___name___name
。这个 __name__
是一个小的“占位符”,你可以用一个唯一标识来替换它,如递增的数字(task[tags][3][name]
)。
实际的代码可能会相当多样化,幸运的是这里有一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function addTagForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
} |
Note
最好把你的javascript代码分离到javascript文件中去,会比直接写在HTML中要好。
现在,一个用户每次点击 Add a tag
连接,一个新的子表单就会出现在页面上。当这个表单被提交,所有的新tag表单都会被转换为一个新的tag
对象并且会添加到Task
对象的tags
属性上。
Seealso
你可以在JSFiddle中找到这个例子。
Seealso
如果你想自定义prototype中的html代码,请阅读:如何定义一个集合prototype
为了处理这些新的tag(标签)容易些,添加一个tag的“添加”和“移除”方法到Task
类里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
// ...
class Task
{
// ...
public function addTag(Tag $tag)
{
$this->tags->add($tag);
}
public function removeTag(Tag $tag)
{
// ...
}
} |
下一步,添加一个by_reference
配置选项到tag
字段并设置它为false:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Form/Type/TaskType.php
// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('tags', CollectionType::class, array(
// ...
'by_reference' => false,
));
} |
由于这两处更改,当表单被提交时,每一个新的Tag
对象都是通过调用addTag
方法添加到Task
类的。再做这个改变之前,他们是通过调用表单$task->getTags()->add($tag)
来内部添加的。这样很好,但强制使用“添加”方法会是处理新的tag
对象容易些(特别是如果你使用的是Doctrine,你会在下面学习到)。
Caution
你必须创建addTag
和removeTag
方法,否则这个表单将继续使用setTag
,即便你已经设置了by_reference
为false
。你将在后面学习更多关于removeTag
的文章。
允许移除Tag ¶
下一步就是允许删除集合的特定条目。解决方法类似于允许tag添加的方法。
开始在添加allow_delete
配置选项到表单类型:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Form/Type/TaskType.php
// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('tags', CollectionType::class, array(
// ...
'allow_delete' => true,
));
} |
现在,你需要在 Task 的 removeTag 方法中加入一些代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Entity/Task.php
// ...
class Task
{
// ...
public function removeTag(Tag $tag)
{
$this->tags->removeElement($tag);
}
} |
模板修饰 ¶
allow_delete选项有一个使用原则:如果集合中的某个元素没有在提交时被发送出去,那么在服务器端,该元素对应的数据将从集合中被删除。因此这种解决方案,是要删除DOM中的表单元素的。
首先,给每个tag表单添加一个“delete this tag”(删除这个标签)链接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
$collectionHolder = $('ul.tags');
// add a delete link to all of the existing tag form li elements
$collectionHolder.find('li').each(function() {
addTagFormDeleteLink($(this));
});
// ... the rest of the block from above
});
function addTagForm() {
// ...
// add a delete link to the new form
addTagFormDeleteLink($newFormLi);
} |
这个addTagFormDeleteLink
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
function addTagFormDeleteLink($tagFormLi) {
var $removeFormA = $('<a href="#">delete this tag</a>');
$tagFormLi.append($removeFormA);
$removeFormA.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// remove the li for the tag form
$tagFormLi.remove();
});
} |
当一个tag表单从DOM中移除,并提交数据,这个移除的Tag
对象将不会传递到setTags
集合中。根据你的持久层来判断,他可能或者可能不足以删除被移除tag
和Task
对象之间的关系。