你好,斯塔克溢出社区!
我正在用AngularJS和jQuery做一个Symfony 3项目。我创建了一个集合指令,用于与Symfony表单生成器接口,以添加和删除集合字段类型的行。该指令有一个独立的作用域,它设置一个名为prototypeControl的双向绑定变量。在小枝模板站点上,我调用prototype-control="{{ form.vars.id|camel_case }}Prototype"来获取集合字段的唯一ID,这样就可以在一个表单中处理多个集合字段。奇怪的是,如果我在prototype-control属性中将变量名设置为prototypeControl,那么一切都正常。“添加”和“删除”按钮用于加载中存在的集合行,“删除”按钮处理动态添加的行。我需要它来使用自定义变量名,这样我就可以使用来自页面控制器的隔离作用域函数。
长话短说,当我使用带有表单ID的变量使其惟一时,我可以从javscript控制台触发函数,但是无论动态添加什么字段,delete按钮都不会影响任何情况。使delete按钮工作的唯一方法是将$compile注入指令并编译directvie元素。
$compile(element.contents())(scope);
尝试使用和不使用.contents()方法。使用这种方法看起来一切都很好,delete对现有元素和动态添加元素都有效,但是由于任何原因,每次单击add按钮似乎都会增加侦听器的数量。所以下一次我点击这个按钮时,它会添加两行,然后是后面,四行等等。
我试着在不同级别的DOM上编译,但delete按钮从未起作用。我尝试过的示例元素是delete按钮本身、存储在本地容器变量中的DOM、原型HTML本身和. prototype行。所有这些似乎都没有受到$compile的影响。似乎只有编译元素变量才能工作。
以下是指令:
($_ => {
$_.app.directive('formCollection', [
'$compile',
($compile) => ({
restrict: 'C',
require: '^form', // Grab the form controller from the parent <form> element,
scope: {
prototypeControl: '=',
},
link(scope, element, attr, form) {
// Declare prototypeControl as an object
scope.prototypeControl = {};
// Store the prototype markup in the scope (the template generated by Symfony)
scope.prototype = attr.collectionPrototype;
// Determine what the the next row id will be on add
let row = element.find('.prototype-row').last().data('row');
// Set the nextRow scope variable
if (typeof row !== 'undefined') {
// Next number in the sequence
scope.nextRow = row + 1;
}
else {
// There are no rows on page load. Setting the default to zero
scope.nextRow = 0;
}
// Add prototype row (add button)
scope.prototypeControl.add = ($event) => {
if (typeof $event !== 'undefined') {
// Prevent Default
$event.preventDefault();
}
// Get the element that will contain dynamically added prototype form rows
let container = element.find('.prototype-container');
// Replace the __name__ placeholder with the row id (typically the next number in the sequence)
let prototype = scope.prototype.replace(/__name__/g, scope.nextRow);
// Appened the prototype form row to the end of the prototype form rows container
angular.element(prototype).appendTo(container);
// Re-compiles the entire directive element and children to allow events like ng-click to fire on
// dynamically added prototype form rows
$compile(element.contents())(scope);
// Increase the nextRow scope variable
scope.nextRow++;
};
// Remove prototype row (remove button)
scope.prototypeControl.remove = ($event) => {
// Prevent Default
$event.preventDefault();
// Get the button element that was clicked
let el = angular.element($event.target);
// Get the entire prototype form row (for removal)
let prototypeRow = el.parents('.prototype-row');
// Remove the row from the dom (If orphan-removal is set to true on the model, the ORM will automatically
// delete the entity from the database)
prototypeRow.remove();
};
// Manual control to add a row (omits the $event var)
scope.prototypeControl.addRow = () => {
scope.prototypeControl.add();
};
// Manual control to remove a row by passing in the row id
scope.prototypeControl.removeRow = (row) => {
// Find the prototype form row by the row id
let el = angular.element(`.prototype-row[data-row="${row}"]`);
// If the element is found, remove it from the DOM
if (el.length) {
el.remove();
}
};
}
})]);
})(Unicorn);下面是服务器端集合的小枝模板块。
{%- block collection_widget -%}
{% if prototype is defined and prototype %}
{% set prototypeVars = {} %}
{% set prototypeHtml = '<div class="prototype-row" data-row="__name__">' %}
{% set prototypeHtml = prototypeHtml ~ form_widget(prototype, prototypeVars) %}
{% if allow_delete is defined and allow_delete %}
{% set prototypeHtml = prototypeHtml ~ '<div class="input-action input-action-delete">' %}
{% set prototypeHtml = prototypeHtml ~ '<a href="#" class="btn btn-secondary btn-destructive btn-small prototype-remove" ng-click="' ~ form.vars.id|camel_case ~ 'Prototype.remove($event)" data-field="' ~ prototype.vars.id|camel_case ~ '">' ~ deleteButtonText|trans({}, translation_domain)| raw ~ '</a>' %}
{% set prototypeHtml = prototypeHtml ~ '</div>' %}
{% endif %}
{% set prototypeHtml = prototypeHtml ~ '</div>' %}
<div class="form-collection" prototype-control="{{ form.vars.id|camel_case }}Prototype" data-collection-prototype="{{ prototypeHtml|e('html') }}">
{% for field in form %}
<div class="prototype-row" data-row="{{ field.vars.name }}">
{{ form_widget(field) }}
{{ form_errors(field) }}
{% if allow_delete is defined and allow_delete %}
<div class="input-action input-action-delete">
<a href="#" class="btn btn-secondary btn-destructive btn-small prototype-remove" ng-click="{{ form.vars.id|camel_case }}Prototype.remove($event)" data-field="{{ field.vars.id|camel_case }}">{{ deleteButtonText|trans({}, translation_domain)| raw }}</a>
</div>
{% endif %}
</div>
{% endfor %}
<div class="prototype-container"></div>
{% if allow_add is defined and allow_add %}
<div class="input-action input-action-add">
<a href="#" class="btn btn-secondary btn-small" ng-click="{{ form.vars.id|camel_case }}Prototype.add($event)" data-collection="{{ form.vars.id|camel_case }}">{{ form.vars.addButtonText|trans({}, translation_domain) }}</a>
</div>
{% endif %}
{{ form_errors(form) }}
</div>
{% else %}
{{- block('form_widget') -}}
{% endif %}
{%- endblock collection_widget -%}下面是我正在测试的针状集合的实际模板。这是data-collection-prototype的内容,它使用add()方法动态地添加到DOM中:
<div class="prototype-row" data-row="__name__">
<div id="proposal_recipients___name__Container">
<div class="form-item form-item-contact">
<div id="proposal_recipients___name___contactContainer">
<div class="form-item form-item-first-name"><label class="control-label required"
for="proposalRecipientsNameContactFirstName">First
Name<span class="field-required">*</span></label>
<input type="text" id="proposalRecipientsNameContactFirstName"
name="proposal[recipients][__name__][contact][firstName]" required="required"
ng-model="proposalDetails.proposal.recipients[__name__]._contact.firstName"
ng-init="proposalDetails.proposal.recipients[__name__]._contact.firstName=''" class="input"/>
</div>
<div class="form-item form-item-last-name"><label class="control-label required"
for="proposalRecipientsNameContactLastName">Last
Name<span class="field-required">*</span></label>
<input type="text" id="proposalRecipientsNameContactLastName"
name="proposal[recipients][__name__][contact][lastName]" required="required"
ng-model="proposalDetails.proposal.recipients[__name__]._contact.lastName"
ng-init="proposalDetails.proposal.recipients[__name__]._contact.lastName=''" class="input"/>
</div>
<div class="form-item form-item-email"><label class="control-label required"
for="proposalRecipientsNameContactEmail">Email
Address<span class="field-required">*</span></label> <input type="email"
id="proposalRecipientsNameContactEmail"
name="proposal[recipients][__name__][contact][email]"
required="required"
ng-model="proposalDetails.proposal.recipients[__name__]._contact.email"
ng-init="proposalDetails.proposal.recipients[__name__]._contact.email=''"
class="input"/></div>
<div class="form-item form-item-phone"><label class="control-label"
for="proposalRecipientsNameContactPhone">Phone</label>
<input type="phone" id="proposalRecipientsNameContactPhone"
name="proposal[recipients][__name__][contact][phone]"
ng-model="proposalDetails.proposal.recipients[__name__]._contact.phone"
ng-init="proposalDetails.proposal.recipients[__name__]._contact.phone=''" class="input"/>
</div>
</div>
</div>
<div class="form-item form-item-company"><label class="control-label required"
for="proposalRecipientsNameCompany">Company<span
class="field-required">*</span></label>
<input type="text" id="proposalRecipientsNameCompany" name="proposal[recipients][__name__][company]"
required="required" ng-model="proposalDetails.proposal.recipients[__name__]._company"
ng-init="proposalDetails.proposal.recipients[__name__]._company=''" class="input"/>
</div>
<div class="form-item form-item-title"><label class="control-label required" for="proposalRecipientsNameTitle">Title<span
class="field-required">*</span></label>
<input type="text" id="proposalRecipientsNameTitle" name="proposal[recipients][__name__][title]"
required="required" ng-model="proposalDetails.proposal.recipients[__name__]._title"
ng-init="proposalDetails.proposal.recipients[__name__]._title=''" class="input"/>
</div>
<div class="form-item form-item-role"><label class="control-label required" for="proposalRecipientsNameRole">Role<span
class="field-required">*</span></label>
<select id="proposalRecipientsNameRole" name="proposal[recipients][__name__][role]" required="required"
ng-model="proposalDetails.proposal.recipients[__name__]._role"
ng-init="proposalDetails.proposal.recipients[__name__]._role=''" class="hide-search"
data-show-search="0" chosen="chosen" data-allow-single-deselect="true" data-placeholder="Select"
tabindex="-1">
<option value="" selected="selected">Select</option>
<option value="ROLE_PROPOSAL_SIGNER">Signer</option>
<option value="ROLE_PROPOSAL_READER">Reader</option>
</select>
</div>
</div>
<div class="input-action input-action-delete"><a href="#"
class="btn btn-secondary btn-destructive btn-small prototype-remove"
ng-click="proposalRecipientsPrototype.remove($event)"
data-field="proposalRecipientsName">Remove Recipient</a></div>
</div>为了找到答案,我还是要好好研究一下。如果我搞清楚了,我就把它发回来。
希望外面有人遇到过一两次这样的事。
谢谢!
发布于 2017-12-10 21:42:04
嗯,经过一点调整,我终于开始工作了。最后,我发现使用两个指令可以防止add乘法,因为add按钮只存在于父指令中,而添加的元素只有两个子指令。因此,我决定为原型行创建一个.prototype-container div指令。这样,编译.prototype-container元素将注册动态添加的ng单击事件,而不必更改原型模板存储在数据属性中的方式。
不过,我很好奇,看看是否有人认为这样做更干净。
我将为可能对使用AngularJS处理使用Symfony集合类型的add和remove按钮感兴趣的任何人发布修复。我也会为那些想在他们的项目中尝试它的人张贴CollectionTypeExtension。
以下是工作原型中更新的表单主题块:
{%- block collection_widget -%}
{% if prototype is defined and prototype %}
{% set prototypeVars = {} %}
{% set prototypeHtml = '<div class="prototype-row" data-row="__name__">' %}
{% set prototypeHtml = prototypeHtml ~ form_widget(prototype, prototypeVars) %}
{% if allow_delete is defined and allow_delete %}
{% set prototypeHtml = prototypeHtml ~ '<div class="input-action input-action-delete">' %}
{% set prototypeHtml = prototypeHtml ~ '<a href="#" class="btn btn-secondary btn-destructive btn-small prototype-remove" ng-click="' ~ form.vars.id|camel_case ~ 'Prototype.remove($event)" data-field="' ~ prototype.vars.id|camel_case ~ '">' ~ deleteButtonText|trans({}, translation_domain)| raw ~ '</a>' %}
{% set prototypeHtml = prototypeHtml ~ '</div>' %}
{% endif %}
{% set prototypeHtml = prototypeHtml ~ '</div>' %}
<div class="form-collection" prototype-control="{{ form.vars.id|camel_case }}Prototype" data-collection-prototype="{{ prototypeHtml|e('html') }}">
<div class="prototype-container">
{% for field in form %}
<div class="prototype-row" data-row="{{ field.vars.name }}">
{{ form_widget(field) }}
{{ form_errors(field) }}
{% if allow_delete is defined and allow_delete %}
<div class="input-action input-action-delete">
<a href="#" class="btn btn-secondary btn-destructive btn-small prototype-remove" ng-click="{{ form.vars.id|camel_case }}Prototype.remove($event)" data-field="{{ field.vars.id|camel_case }}">{{ deleteButtonText|trans({}, translation_domain)| raw }}</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% if allow_add is defined and allow_add %}
<div class="input-action input-action-add">
<a href="#" class="btn btn-secondary btn-small" ng-click="{{ form.vars.id|camel_case }}Prototype.add($event)" data-collection="{{ form.vars.id|camel_case }}">{{ form.vars.addButtonText|trans({}, translation_domain) }}</a>
</div>
{% endif %}
{{ form_errors(form) }}
</div>
{% else %}
{{- block('form_widget') -}}
{% endif %}
{%- endblock collection_widget -%}我在模板中所做的更改就是将已经存储并加载到页面加载中的原型行移动到.prototype-container div中。这是最有意义的,并且允许指令上的delete按钮处理这两个实例。
下面是更新的JS,它包括两个相互通信的指令:
($_ => {
$_.app.directive('formCollection', [
() => ({
restrict: 'C',
require: '^form', // Grab the form controller from the parent <form> element,
scope: {
prototypeControl: '=',
},
link: function(scope, element, attr, formController) {
scope.formController = formController;
},
controller: function($scope, $element, $attrs) {
// Register the child directive scope
this.register = (element) => {
$scope.prototypeContainerScope = element.scope();
};
// Store the prototype template from the form theme in the controller prototype variable
this.collectionPrototype = $attrs.collectionPrototype;
// Determine what the the next row id will be on add
let row = $element.find('.prototype-row').last().data('row');
// Set the nextRow $scope variable
if (typeof row !== 'undefined') {
// Next number in the sequence
$scope.nextRow = row + 1;
}
else {
// There are no rows on page load. Setting the default to zero
$scope.nextRow = 0;
}
// Controller method to get the next row from the child directive
this.getNextRow = () => {
return $scope.nextRow;
};
// Set next row from the child directive
this.setNextRow = (nextRow) => {
$scope.nextRow = nextRow;
};
// Prototype control methods from the page controller
$scope.prototypeControl = {
add: ($event) => {
$event.preventDefault();
$scope.prototypeContainerScope.add();
},
remove: ($event) => {
$event.preventDefault();
$scope.prototypeContainerScope.remove($event);
}
};
}
})
]).directive('prototypeContainer', [
'$compile',
($compile) => ({
restrict: 'C',
require: '^formCollection', // Grab the form controller from the parent <form> element,
link: function(scope, element, attr, formCollectionController) {
formCollectionController.register(element);
scope.collectionPrototype = formCollectionController.collectionPrototype;
scope.nextRow = formCollectionController.getNextRow();
scope.increaseNextRow = () => {
let nextRow = scope.nextRow + 1;
scope.nextRow = nextRow;
// Set next row on the parent directive controller
formCollectionController.setNextRow(nextRow);
};
},
controller: function($scope, $element, $attrs) {
$scope.add = () => {
// Replace the __name__ placeholder with the row id (typically the next number in the sequence)
let prototype = $scope.collectionPrototype.replace(/__name__/g, $scope.nextRow);
// Appened the prototype form row to the end of the prototype form rows container
angular.element(prototype).appendTo($element);
// Re-compiles the entire directive $element and children to allow events like ng-click to fire on
// dynamically added prototype form rows
$compile($element)($scope);
// Increase the nextRow $scope variable
$scope.increaseNextRow();
};
$scope.remove = ($event) => {
// Get the button $element that was clicked
let el = angular.element($event.target);
// Get the entire prototype form row (for removal)
let prototypeRow = el.parents('.prototype-row');
// Remove the row from the dom (If orphan-removal is set to true on the model, the ORM will automatically
// delete the entity from the database)
prototypeRow.remove();
};
}
})
]);
})(Unicorn);此外,如果这对希望将此代码用于他们的项目的任何人都有帮助,那么CollectionTypeExtension.php内容:
<?php
namespace Unicorn\AppBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CollectionTypeExtension extends AbstractTypeExtension
{
/**
* @param FormView $view
* @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['addButtonText'] = $options['add_button_text'];
$view->vars['deleteButtonText'] = $options['delete_button_text'];
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'add_button_text' => 'Add',
'delete_button_text' => 'Delete',
'prototype' => false,
])
->setAllowedTypes('add_button_text', 'string')
->setAllowedTypes('delete_button_text', 'string')
->setAllowedTypes('prototype', 'boolean');
}
/**
* Returns the name of the type being extended.
*
* @return string The name of the type being extended
*/
public function getExtendedType()
{
return CollectionType::class;
}
}特别感谢任何人,谁可能一直试图复制这个问题,以帮助解决这个问题,这使我的头几乎在我的桌子上几乎整个周末。
干杯!
https://stackoverflow.com/questions/47724149
复制相似问题