web-dev-qa-db-fra.com

Comment lier correctement la portée entre la directive et le contrôleur avec angularJS

J'essaie de générer une liste hiérarchique non ordonnée de niveau n avec anugularJS, et j'ai réussi à le faire. Mais maintenant, j'ai des problèmes de portée entre la directive et le contrôleur. J'ai besoin de changer une propriété de portée du parent à partir d'une fonction appelée via ng-click dans le modèle de directive.

Voir http://jsfiddle.net/ahonaker/ADukg/2046/ - voici le JS

var app = angular.module('myApp', []);

//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});

function MyCtrl($scope) {
    $scope.itemselected = "None";
    $scope.organizations = {
        "_id": "SEC Power Generation",
        "Entity": "OPUNITS",
        "EntityIDAttribute": "OPUNIT_SEQ_ID",
        "EntityID": 2,
        "descendants": ["Eastern Conf Business Unit", "Western Conf Business Unit", "Atlanta", "Sewanee"],
        children: [{
            "_id": "Eastern Conf Business Unit",
            "Entity": "",
            "EntityIDAttribute": "",
            "EntityID": null,
            "parent": "SEC Power Generation",
            "descendants": ["Lexington", "Columbia", "Knoxville", "Nashville"],
            children: [{
                "_id": "Lexington",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 10,
                "parent": "Eastern Conf Business Unit"
            }, {
                "_id": "Columbia",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 12,
                "parent": "Eastern Conf Business Unit"
            }, {
                "_id": "Knoxville",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 14,
                "parent": "Eastern Conf Business Unit"
            }, {
                "_id": "Nashville",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 4,
                "parent": "Eastern Conf Business Unit"
            }]
        }]
    };

    $scope.itemSelect = function (ID) {
        $scope.itemselected = ID;
    }
}

app.directive('navtree', function () {
    return {
        template: '<ul><navtree-node ng-repeat="item in items" item="item" itemselected="itemselected"></navtree-node></ul>',
        restrict: 'E',
        replace: true,
        scope: {
            items: '='
        }
    };
});

app.directive('navtreeNode', function ($compile) {
    return {
        restrict: 'E',
        template: '<li><a ng-click="itemSelect(item._id)">{{item._id}} - {{itemselected}}</a></li>',
        scope: {
            item: "=",
            itemselected: '='
        },
        controller: 'MyCtrl',
        link: function (scope, Elm, attrs) {
            if ((angular.isDefined(scope.item.children)) && (scope.item.children.length > 0)) {
                var children = $compile('<navtree items="item.children"></navtree>')(scope);
                Elm.append(children);
            }
        }
    };
});

et voici le HTML

<div ng-controller="MyCtrl">
    Selected: {{itemselected}}

    <navtree items="organizations.children"></navtree>
</div>

Notez que la liste est générée à partir du modèle. Et ng-click appelle la fonction pour définir la propriété d'étendue parent (itemselected), mais la modification ne se produit que localement. Le comportement attendu, lorsque je clique sur un élément, est que "Sélectionné: Aucun" doit se transformer en "Sélectionné: xxx" où xxx est l'élément sur lequel vous avez cliqué.

Suis-je pas lier la propriété entre la portée parent et la directive de manière appropriée? Comment transmettre la modification de propriété à la portée parent?

J'espère que c'est clair.

Merci d'avance pour votre aide.

18
user2165994

Veuillez jeter un œil à ce violon qui fonctionne, http://jsfiddle.net/eeuSv/

Ce que j'ai fait était d'exiger le contrôleur parent à l'intérieur de la directive navtree-node, Et d'appeler une fonction membre définie dans ce contrôleur. La fonction membre est setSelected. Veuillez noter que c'est this.setSelected Et non $scope.setSelected. Définissez ensuite une méthode d'étendue navtree-nodeitemSelect. Pendant que vous cliquez sur les balises d'ancrage, il appellera la méthode itemSelect sur la portée navtree-node. Ce tour appellera la méthode membre des contrôleurs setSelected en passant l'id sélectionné.

scope.itemSelect = function(id){ myGreatParentControler.setSelected(id) }

18

Maxdec a raison, cela a à voir avec la portée. Malheureusement, c'est un cas suffisamment compliqué pour que les documents AngularJS puissent être trompeurs pour un débutant (comme moi).

Avertissement: préparez-vous à ce que je sois un peu de longue haleine alors que j'essaie d'expliquer cela. Si vous voulez juste voir le code, allez à ceci JSFiddle . J'ai également trouvé les vidéos egghead.io inestimables pour en savoir plus sur AngularJS.

Voici ma compréhension du problème: vous avez une hiérarchie de directives (navtree, navitem) et vous voulez transmettre des informations du navitem "en haut de l'arborescence" au contrôleur racine. AngularJS, comme Javascript bien écrit en général, est configuré pour être strict quant à la portée de vos variables, afin de ne pas gâcher accidentellement d'autres scripts également en cours d'exécution sur la page.

Il y a une syntaxe spéciale (&) dans Angular qui vous permet à la fois de créer une étendue isolée et d'appeler une fonction sur la portée parent:

// in your directive
scope: {
   parentFunc: '&'
}

Jusqu'ici tout va bien. Les choses deviennent délicates lorsque vous avez plusieurs niveaux de directives, car vous voulez essentiellement effectuer les opérations suivantes:

  1. Avoir une fonction dans le contrôleur racine qui accepte une variable et mettre à jour le modèle
  2. Une directive de niveau intermédiaire
  3. Une directive au niveau enfant qui peut communiquer avec le contrôleur racine

Le problème est que la directive au niveau enfant ne peut pas voir le contrôleur racine. Je crois comprendre que vous devez mettre en place une "chaîne" dans votre structure de directive qui agit comme suit:

Premièrement: Avoir une fonction dans votre contrôleur racine qui renvoie une fonction (qui fait référence à la portée du contrôleur de vue racine):

$scope.selectFunctionRoot = function () {
    return function (ID) {
        $scope.itemselected = ID;
    }
}

Deuxièmement: Configurez la directive de niveau intermédiaire pour avoir sa propre fonction de sélection (qu'elle transmettra à l'enfant) qui renvoie quelque chose comme ce qui suit. Remarquez comment nous devons sauver la portée de la directive de niveau intermédiaire, car lorsque ce code sera réellement exécuté, ce sera dans le contexte de la directive de niveau enfant:

// in the link function of the mid-level directive. the 'navtreelist'
scope.selectFunctionMid = function () {
    // if we don't capture our mid-level scope, then when we call the function in the navtreeNode it won't be able to find the mid-level-scope's functions            
    _scope = scope;
    return function (item_id) {
        console.log('mid');
        console.log(item_id);

        // this will be the "root" select function
        parentSelectFunction = _scope.selectFunction();
        parentSelectFunction(item_id);
    };
};

Troisièmement: Dans la directive au niveau enfant (navtreeNode) liez une fonction à ng-click qui appelle une fonction locale qui, à son tour, "appellera la chaîne" jusqu'au contrôleur racine:

// in 'navtreeNode' link function
scope.childSelect = function (item_id) {
    console.log('child');
    console.log(item_id);

    // this will be the "mid" select function  
    parentSelectFunction = scope.selectFunction();
    parentSelectFunction(item_id);
};

Voici la mise à jour fork de votre JSFiddle , qui a des commentaires dans le code.

11
Benmj

C'est peut-être parce que chaque directive crée son propre champ d'application (en fait, vous leur dites de le faire).
Vous pouvez en savoir plus sur les directives ici , en particulier le chapitre "Écrire des directives (version longue)".

scope - Si défini sur:

true - alors une nouvelle portée sera créée pour cette directive. Si plusieurs directives sur le même élément demandent une nouvelle étendue, une seule nouvelle étendue est créée. La nouvelle règle d'étendue ne s'applique pas à la racine du modèle car la racine du modèle obtient toujours une nouvelle étendue.

{} (hachage d'objet) - alors une nouvelle portée 'isoler' est créée. La portée "isoler" diffère de la portée normale en ce qu'elle n'hérite pas de manière prototypique de la portée parent. Cela est utile lors de la création de composants réutilisables, qui ne doivent pas lire ou modifier accidentellement des données dans la portée parent.

Ainsi, les modifications que vous effectuez ne sont pas reflétées dans la portée MyCtrl car chaque directive a sa propre portée "isolée".

C'est pourquoi un clic ne modifie que la variable locale $scope.itemselected Et non "toutes".

3
maxdec