1

I have approximately four main pairs of related directives, simultaneously displayed in my AngularJS UI. Generally speaking, each pair includes a parent list directive on top with an associated detail editor directive beneath it, as shown in this diagram:

master-detail UI

Each right-hand list has a many-to-one relationship with the active left-hand selection, and related data must always be displayed. How should I drive related list (the left-to-right association) refreshes?


Currently, each master-detail directive-pair, or "stack", share a service. That service holds the itemState.active (active detail record) and itemState.headers (query master results list). Activity in either the master or detail panel call the service, which directly affect the service state. Then, the master/detail association is operated via simple declarative Angular watches on this common positionService.state; almost no controller code is required. I expect that using the service as the single point of truth here will make it easy for me to integrate near-realtime display in the future, for example via SignalR. This master-detail implementation is only provided here for background, although I welcome improvements:

Master Directive, e.g. position-list.js

templateUrl: "position/position-list.html",
controller: ['$scope', 'positionSvc', function ($scope, positionService) {
    $scope.positions = positionService.itemState();
    $scope.select = function (position) {
        positionService.read(position.id)
    };
}

Master Template, e.g. position-list.html

<li ng-repeat="i in itemState.headers" ng-click="select(i)">{{i.title}}</li>

Service, e.g. position-svc.js

this.itemState = {
    headers: [],
    active: { id: 0 },
    pending: httpSvc.pending
};

this.create = function (detail) {
    httpSvc.remote("post", detail).then(function (header) {
        itemState.headers.unshift(header);
        read(detail.id);
    });
}

this.read = function (id) {
    httpSvc.remote("get", id).then(function (detail) {
        itemState.active = detail;
    });
}

A similar directive and template exists for the detail. It shares the same service. You get the idea.

Now I'm looking for a maintainable way to handle this left->right eventing following good design practices and well-known AngularJS patterns. I'd like to retain composability of my directives.


To demonstrate I'm not looking for you to do my work for me (and to organize my own thoughts), here are some approaches I came up with, although none have seemed right so far. I'm continuing to document these, check back if you need this section cleaned-up. They are grouped by message path:

  1. leftController -> mainApp -> rightController
    • part a: leftController -> mainApp
      • mainApp.$scope.onLeftItemSelected (e.g. via { scope: '&' })
      • leftController.$scope.$emit within leftController.watch
      • leftController.$parent.onLeftItemSelected (e.g. AngularJS access parent scope from child controller, but incorrectly creates bottom-up dependency)
    • part b: mainApp -> rightController
      • mainApp.$scope.$broadcast
      • mainApp.rightService.load(leftItemId)
  2. leftController -> mainApp model -> rightController.$watch
  3. leftController -> $route -> rightController
    • implementing this could be ideal, allowing me to provide deep-linking/bookmarking into my single-page-application, but requires enabling the page to load the parent hierarchy when a child is selected, rather than just the converse (loading the child records of a selected parent item). this requires a greater initial investment than I can currently justify. http://embed.plnkr.co/DBSbiV/preview
  4. leftService -> rightService
  5. leftService -> $rootScope.broadcast -> rightService
  6. leftService -> mainApp -> rightService
    • Inject the positionService and nominationService into the mainApp. Add a simple event list to positionService to transmit notification upwards. PRO: this retains the leftService's role as single point of truth for the active record throughout the application (as noted by Martin Cortez, in comments below)
  7. leftService -> $rootScope.$route -> rightController -> rightService
    • see notes for approach 3

References pass data between controllers How to communicate between controllers while not using SharedService between them?

Community
  • 1
  • 1
shannon
  • 8,664
  • 5
  • 44
  • 74
  • 1
    I like number 6 the best. It seems to meet your goals of minimizing controller code (emphasizing the service instead). It also maintains the organization that you've already created. Along this line, perhaps something akin to a `leftToRightService(leftService, rightService)` that holds on to the event list can help. The `leftToRightService` might be reusable if you allow it to accept and execute a function that would be provided as a parameter. – Marty Cortez Oct 17 '14 at 23:30
  • 1
    @MartinCortez: +1, thank you. I'm deciding between that (which maintains more of a "single point of truth"), and the solution provided by Jesús Quintana below (which is probably what most angular developers would expect). – shannon Oct 18 '14 at 06:33

1 Answers1

3

Based on angular pattern design the best way is both directive bind the scope by '=' (Two way binding), and inside the directives make a $scope.#watch to detect changes of the element binding and execute the action function given the variable.

Is the more clean way to communicate inside both directives, angular detect the change and spread out to bot directives.

Sorry , if the answer is useless but the question is not well explain .

Jesús Quintana
  • 1,803
  • 13
  • 18
  • +1 Thank you for the advice. Could you please describe what was unclear about the question? It would help me clarify it, if I understood why it wasn't well explained. – shannon Oct 18 '14 at 02:16
  • 1
    The question was complex, the main issue must be highlighted, i had to read 3 times to try understand the real question. – Jesús Quintana Oct 18 '14 at 05:05
  • The main issue, the "real question", is highlighted, directly beneath the image. Is that what you mean? – shannon Oct 18 '14 at 05:06
  • Is the real problem possibly that there's just so much text in my initial post? – shannon Oct 18 '14 at 05:11
  • Regarding your answer, does `{ scope: '=' }` work for sibling directives? If so, List A would need to bind upwards, to the parent application, and then back downwards, to List B. – shannon Oct 18 '14 at 05:17
  • Maybe to much information is the issue. In this plunker show my example: http://plnkr.co/edit/tyZPziT9VoWDFqHdF8A5?p=preview – Jesús Quintana Oct 18 '14 at 05:47
  • That does indeed work, thank you. I'll mark it the answer because it's exactly the sort of suggestion I was looking for, although I think it's a toss-up between your suggestion (which obviously follows angular patterns) and another method to skip the controllers, going instead from service to service via the parent. – shannon Oct 18 '14 at 06:23
  • I just thought I'd mention, I realized the two-way binding actually benefits me, if I want to do deep-linking to a child record in the future. Now I can automatically load parent records with no additional code. – shannon Oct 18 '14 at 07:25
  • Yo maybe want check Angular ui bootstrap accordion code, there show parent directives with children sharing the parent scope, now I'm in the phone and cannot send you the link but check the github – Jesús Quintana Oct 18 '14 at 07:34
  • The accordion here? http://angular-ui.github.io/bootstrap/ If so, it performs pretty poorly on my laptop. :( – shannon Oct 18 '14 at 07:38
  • This code is a good approach https://github.com/angular-ui/bootstrap/blob/master/src/accordion/accordion.js for multiple directive communicate each – Jesús Quintana Oct 18 '14 at 07:43
  • I hit a snag with doing it this way. In the right-hand master-detail (the slave list), when I create a new detail I need to know the active ID of the left-hand list. That means I need to also bind that ID into other places in my application. It seems like the original design, having it "bind" into the service somehow, may be back on the table. – shannon Oct 18 '14 at 23:29