在$ compile

时间:2017-12-09 00:45:32

标签: angularjs angularjs-directive twig symfony-forms symfony-3.4

Hello Stackoverflow社区!

我正在使用AngularJS和jQuery开发Symfony 3项目。我创建了一个集合指令,以与Symfony表单构建器进行交互,以添加和删除集合字段类型的行。该指令有一个独立的范围,它设置一个名为prototypeControl的双向绑定变量。在twig模板网站上,我调用prototype-control="{{ form.vars.id|camel_case }}Prototype"来获取集合字段的唯一ID,这样就可以在单个表单中使用多个集合字段。奇怪的是,如果我在prototype-control属性中将变量名称设置为prototypeControl,一切正常。添加和删​​除按钮适用于加载时存在的集合行,而删除按钮适用于动态添加的行。我需要这个以使用自定义变量名称,以便我可以使用页面控制器中的隔离范围函数。

长话短说,当我使用带有表单ID的变量使其唯一时,我可以从javscript控制台触发函数,但无论字段是动态添加的,删除按钮都不会影响到什么。我可以让删除按钮工作的唯一方法是将$ compile注入指令并编译directvie元素。

$compile(element.contents())(scope);

尝试使用和不使用.contents()方法。使用这种方法一切看起来都不错,删除适用于现有元素和动态添加元素,添加按钮有效,但无论出于何种原因,每次添加按钮上的ng-click似乎都会成倍增加。因此,下次单击按钮时,它会添加两行,然后添加四行,依此类推。

我尝试在不同级别的DOM上编译,但删除按钮永远不会工作。我尝试过的示例元素是删除按钮本身,存储在本地容器变量中的DOM,尝试了原型HTML本身和.prototype-row。这些似乎都没有来自$ 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方法动态添加到DOM的add()的内容:

<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>

我仍然会通过这个来找到答案。如果我搞清楚,我会在这里回复。

希望有人在那里遇到过一两次。

谢谢!

1 个答案:

答案 0 :(得分:0)

我经过一些调整后终于得到了它。我终于弄清楚使用两个指令会阻止add乘以,因为add按钮仅存在于parent指令中,而添加的元素只有两个child指令。所以我决定为.prototype-container div创建一个指令,让原型行生效。这样编译.prototype-container元素就会注册动态添加的ng-click事件,而不必改变原型的方式模板存储在数据属性中。

我很想知道是否有人会看到更清洁的方式。

我将为可能有兴趣使用AngularJS处理带有Symfony集合类型的添加和删除按钮的任何人发布修复程序。我还会为想要在他们的项目上尝试的人发布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中。这只是最有意义的,并允许指令上的删除按钮与两个实例一起使用。

这是更新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;
    }
}

特别感谢任何可能尝试重现此问题的人,以帮助解决这个问题,导致我几乎整个周末都在我的桌子上敲我的头。

干杯!