Reputation: 1018
I am writing a SPA using angular (1.4.7) and to keep the complexity down I'd been attempting to abstract the persistence logic to a factory/repository.
This is nothing special and seems to work fine.
One feature I'd like to implement is the ability for a "parent" scope to update when the user updates some personal information.
See the example https://jsfiddle.net/h1r9zjt4/
I've taken a look at various ways of implementing this and a few ways I've seen are:
controllerAs
syntax. This seems to be the advised solution to keep a strict/robust separation between controller and view.My ideal scenario would be to have a pub/sub scenario.
My user updating their details will be handled by the repository which in-turn sends a command or fulfils a promise to all subscribers of that repository.
Is this a standard angular pattern? If not, what would a suitable alternative be?
Upvotes: 2
Views: 4840
Reputation: 1867
I use postaljs and inject a $bus to $scopes as show the blog An angular.js event bus with postal.js
Note that code snippet at blog throws an Unable to get property 'length' of undefined, I fixed it as:
app.config(function($provide) {
$provide.decorator('$rootScope', [
'$delegate',
function($delegate) {
Object.defineProperty($delegate.constructor.prototype,
'$bus', {
get: function() {
var self = this;
return {
subscribe: function() {
var sub = postal.subscribe.apply(postal, arguments);
self.$on('$destroy',
function() {
sub.unsubscribe();
});
},
//Fix to avoid postaljs v 2.0.4:513 Unable to get property 'length' of undefined
channel: function() { return postal.channel.apply(postal,arguments); },
publish: function() { postal.publish.apply(postal,arguments); }
};
},
enumerable: false
});
return $delegate;
}
]);
Subscribe controller:
var subscription = $scope.$bus.subscribe({
channel: "organizations",
topic: "item.changed",
callback: function(data, envelope) {
// `data` is the data published by the publisher.
// `envelope` is a wrapper around the data & contains
// metadata about the message like the channel, topic,
// timestamp and any other data which might have been
// added by the sender.
}
});
Publish controller:
channel = $scope.$bus.channel('organizations');
channel.publish("item.changed",data);
Upvotes: 0
Reputation: 17930
Using $on/$emit
is definitely a viable option, but you need to be careful not to overuse it, since it might lead to a very complex application that is hard to debug and track.
Another way (which i think is better in most cases) is to use services. Since services are singletons by nature, data on a service will be shared across all application.
So you can have a service that is injected in both the parent and child controller and once a change is made on the child, it will update a property on the service and the parent will $watch
that attribute and act upon change:
var app = angular.module('myApp', []);
app.factory('sharedService', function() {
return {
sharedAttr: ''
}
});
app.controller('childCtrl', function($scope, sharedService) {
$scope.onAttrChange = function() {
sharedService.sharedAttr = 'Value Changed';
}
});
app.controller('parentCtrl', function($scope, sharedService) {
$scope.$watch(function() {
return sharedService.sharedAttr;
},
function(newVal, oldVal) {
//do something with newValue
});
});
Upvotes: 1
Reputation: 29989
Although it's mostly associated with the React world, what you are looking for is Flux. It's even been ported to Angular in the form of flux-angular.
Flux enforces a pattern for how you think about data flowing through your application.
The shared models that allow you to publish and subscribe to changes with are called stores. However, you don't speak to them in the conventional pubsub way.
A store is responsible for looking after some data and handling any actions that you trigger. For instance a store for a counter might look something like this:
app.store('CounterStore', function() {
return {
count: 0,
increment: function() {
this.count = this.count + 1;
this.emitChange();
},
decrement: function() {
this.count = this.count - 1;
this.emitChange();
},
exports: {
getCount: function() {
return this.count;
}
}
};
});
Then inject your store into a controller or directive to listen for changes.
Think of this as the subscription part of a pub/sub architecture.
app.directive('Counter', function() {
return {
template: '<div ng-bind='count'></div>',
controller: function($scope, CounterStore) {
$scope.listenTo(CounterStore, function() {
$scope.count = CounterStore.getCount();
});
}
};
});
The other piece in the Flux puzzle is dispatching actions. This is the publishing part of a pub/sub architecture, on steroids.
Rather than emitting events like you could do with the root scope's event emitter, you dispatch serializable actions and Flux does the rest for you.
Let's define a final directive to control the counter in the previous directive, using Flux.
app.directive('CounterControls', function() {
return {
template: '<button ng-click="inc()">+</button>' +
'<button ng-click="dec()">-</button>',
controller: function($scope, flux) {
$scope.inc = function() {
flux.dispatch('increment')
};
$scope.dec = function() {
flux.dispatch('decrement');
};
}
};
});
This code doesn't even know about the store! It just knows that these are the actions that should be dispatched when these buttons are clicked.
Once these actions have been dispatched, Flux uses the name of the action to call the appropriate functions within the stores. These stores update their data and if necessary, they emit a change, notifying the subscribers so that they can update their data too.
It might seem like a lot of code for sharing a counter between two directives, but it's a very powerful idea and in the long term will keep the architecture of your application clean and concise.
Flux is a pretty cool architecture. Here's a run down of why it might suit you better than the other solutions you mentioned.
Flux allows you to move all state management code out into loosely coupled modules called stores. This way none of your controllers will ever have to know about any other controllers.
If you make sure that you only dispatch actions that can be serialized, then you can keep a track of every action that's fired in your application, meaning it's possible to recreate any state, by simplying re-playing the same actions again.
To get some idea of just how cool this can be, check out this video about time travel with a Flux implementation called Redux.
It's easier to reason about your program when data only flows in one direction. When you use Flux, there's no reason to ever communicate with any components other than your children.
A---+
/ \ |
/ v |
B D |
| / |
| / |
C-----+
In a more traditional pub/sub architecture, if directive C wanted to communicate with directive A and D it would have to maintain a complex entangled hierarchy, which gets more and more difficult to manage each time you let one directive or controller know about another.
It's not clear which way the data is flowing because directives can communicate with eachother, regardless of where they are.
A <------------+
/ \ |
v v |
B D <----- [store]
| ^
v |
C --> [action] --+
With Flux, your directives only communicate with their children and with stores — data flows in one direction round your application, making it much easier to work out how a value got somewhere, or why a function was called.
Upvotes: 4