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:
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:
- leftController -> mainApp -> rightController
- part a: leftController -> mainApp
mainApp.$scope.onLeftItemSelected
(e.g. via{ scope: '&' }
)leftController.$scope.$emit
withinleftController.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)
- part a: leftController -> mainApp
- leftController -> mainApp model -> rightController.$watch
- share parent scope variable between leftController and rightController
- transmit isolated scope from leftController to mainApp using
{ scope: '=' }
; this is Jesús Quintana's solution, below, and here: http://plnkr.co/edit/tyZPziT9VoWDFqHdF8A5?p=preview
- 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
- leftService -> rightService
- Inject the
nominationService
also into the siblingpositionService
. CallnominationService.load(positionid)
when setting thepositionService.activeRecord
. Pro: already working. Con: It's wrong. This sibling dependency makes the parent list unreusable. - add a custom Pub/Sub service (https://stackoverflow.com/questions/16235689#16235822, http://jsfiddle.net/ThomasBurleson/sv7D5/)
- Inject the
- leftService -> $rootScope.broadcast -> rightService
- not ideal, assuming our event string will be unique in the root namespace (but performance issues have been addressed, as discussed here https://stackoverflow.com/questions/11252780#19498009 )
- leftService -> mainApp -> rightService
- Inject the
positionService
andnominationService
into themainApp
. Add a simple event list topositionService
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)
- Inject the
- 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?