1

Hello Stackoverflow 커뮤니티!

저는 AngularJS 및 jQuery와 함께 Symfony 3 프로젝트를 진행하고 있습니다. Symfony 양식 빌더와 인터페이스하여 컬렉션 필드 유형에 대한 행을 추가 및 제거하는 콜렉션 지시문을 작성했습니다. 이 지시문은 prototypeControl이라는 양방향 바인딩 변수를 설정하는 격리 된 범위를가집니다. 나뭇 가지 템플릿 사이트에서 prototype-control="{{ form.vars.id|camel_case }}Prototype"을 호출하면 컬렉션 필드의 고유 ID를 가져와 단일 양식의 여러 컬렉션 필드에서 작동합니다. prototype-control 속성에서 prototypeControl에 변수 이름을 설정하면 모든 것이 잘 동작합니다. 추가 및 삭제 버튼은로드시 존재하는 콜렉션 행에 대해 작동하며 삭제 버튼은 동적으로 추가 된 행에 대해 작동합니다. 사용자 정의 변수 이름을 사용하려면이 작업이 필요하므로 페이지 컨트롤러에서 분리 된 범위 함수로 작업 할 수 있습니다.

간단히 말해 양식 ID로 변수를 사용하여 고유하게 만들면 javscript 콘솔에서 함수를 트리거 할 수 있지만 동적으로 추가되는 필드는 삭제 버튼이 영향을 미치지 않습니다. delete 버튼을 작동시킬 수있는 유일한 방법은 지시문에 $ compile을 삽입하고 directvie 요소를 컴파일하는 것입니다.

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

() 메소드와 상기 .contents없이 시도했다. 이 방법으로 모든 것이 잘 보이고 삭제는 기존 요소와 동적으로 추가 된 요소 모두에서 작동합니다. 추가 단추가 작동하지만 어떤 이유로 든 추가 단추를 누를 때마다 청취자가 늘어납니다. 다음 번에 버튼을 클릭하면 두 행이 추가 된 다음 행이 추가되고 네 행이 추가됩니다.

다른 수준의 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 -%} 

다음은 제가 테스트중인 paricular 컬렉션의 실제 템플릿입니다.

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

난 아직 답을 찾으려면이를 통해 쾅거야 :이 동적으로 add() 방법으로 DOM에 추가됩니다 data-collection-prototype의 내용이다. 알아 내면 여기서 다시 게시 할 것입니다.

누군가가 이걸 한두 번 뛰었습니다.

감사합니다.

+0

누가 아래로 질문을 투표합니까? –

답변

0

글쎄 나는 약간의 조정을 거쳐 마침내 작동하게되었다. 나는 마침내 두 개의 지시문을 사용하여 추가 버튼이 상위 지시문에만 존재하고 추가 된 요소가 하위 지시문 2 개이므로 곱하기에서 더하기를 방지 할 수 있다고 생각했습니다. 따라서 .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 사업부에서 살고 페이지로드에 저장 및로드 프로토 타입 행을 이동 . 이것은 가장 이해하기 쉽고 지시문의 삭제 버튼이 두 인스턴스 모두에서 작동하도록 허용합니다. 여기

는 서로 통신을 모두 지침이 포함 된 업데이트 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; 
    } 
} 

이 문제의 해결을 돕기 위해이 문제를 재현하려는 누군가에게 감사 드리며, 이로 인해 거의 모든 주말 내 책상에 머리가 벅차 오르게되었습니다.

건배!