This is kind of a weird question, but here's the idea:
Let's say I have a complex JSON object coming back from an HTTP call and attaching to the $scope. Something like this:
$scope.obj = {
user: {
id: 10,
name: { first: 'Joe', last: 'Smith' },
contact: {
home: {
street: '101 First St.',
city: 'Myville',
state: 'Jokelahoma',
zip: '98765'
},
email: 'joeshmoe#gmail.com',
phone: '+12345678901'
}
},
purchase_hist: [
{ item_id: 11004, date: 'Thu, 06 Aug 2015 13:51:17 GMT' },
{ item_id: 97020, date: 'Fri, 31 Jul 2015 18:57:57 GMT' }
]
}
Now, if I wanted to display an overview of purchase history in an AngularJS partial, I could do something like this:
<table>
<tr ng-repeat="p in obj.purchase_hist">
<td>{{p.item_id}}</td>
<td>{{p.date}}</td>
</tr>
</table>
The really convenient thing about this format (though it's not super evident here with so few props) is that the purchase being described is aliased as p. I don't have to do obj.purchase_hist[0].item_id, I can just do p.item_id.
But what about when I go to show the user's home address? Do I really have to do this?:
<div>
<p>{{obj.user.contact.home.street}}</p>
<p>{{obj.user.contact.home.city}}</p>
<p>{{obj.user.contact.home.state}}</p>
<p>{{obj.user.contact.home.zip}}</p>
</div>
That's really verbose. I would much rather use something akin to the controller as ... syntax, something like this:
<div ng-alias="obj.user.contact.home as uhome">
<p>{{uhome.street}}</p>
<p>{{uhome.city}}</p>
<p>{{uhome.state}}</p>
<p>{{uhome.zip}}</p>
</div>
Is there such a thing that exists in AngularJS ? Unfortunately I'm not very able to use plugins in my environment, so I'm specifically looking for a part of angular core that will work this way.
Thanks!
I've written this little directive, which allow you to perform what you want :
Directive ngAlias
(function(){
function ngAlias($compile) {
return {
restrict: "A",
link: function(scope, element, attrs) {
var args = attrs.ngAlias.split('as').map(function(elm){return elm.replace(/ /g,'')});
scope[args[0]] = '';
var dot = args[1].split('.');
var object = {};
dot.forEach(function(value, index){
index === 0
? object = scope[value]
: object = object[value] === null ? object[value] = {} : object[value];
});
console.log(object)
scope[args[0]] = object;
}
};
}
angular
.module('app')
.directive('ngAlias', ngAlias);
})();
For example, set your object in your controller
Controller
(function(){
function Controller($scope) {
$scope.obj = {
toto: {
nom: 'toto',
prenom: 'tata'
}
};
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
And you can use it :
HTML
<body ng-app="app" ng-controller="ctrl">
<div ng-alias="toto as obj.toto">
{{toto.nom}}
</div>
</body>
#PaulBoutes provided the answer I needed, and he should get the credit; I just wanted to add the version of the directive that I settled on based on his answer.
app.directive('alias', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var splits = attrs['alias'].trim().split(/\s+as\s+/);
scope.$watch(splits[0], function(val) {
scope.$eval(splits[1]+'=('+splits[0]+')');
});
}
};
});
Same basic idea as Paul's, just cleaned up a little and made a little more flexible in terms of whitespace and such.
Usage example:
<div data-alias="obj.user.contact.home as uhome">
<p>{{uhome.street}}</p>
<p>{{uhome.city}}</p>
<p>{{uhome.state}}</p>
<p>{{uhome.zip}}</p>
</div>
Certainly there's no shortage of workable solutions here, but there shouldn't be harm in adding another.
I threw this together the other day to save myself some typing and make my html easier to read. It creates a new child scope, so it doesn't pollute the existing scope with these aliases. It simply works by binding the parent scope's property to the new child scope with whatever name you specify.
app.directive('with', function() {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
if (attrs.as) {
scope[attrs.as] = scope.$eval(attrs.with);
} else {
angular.extend(scope, scope.$eval(attrs.with));
}
}
};
});
So, instead of doing this:
<div>{{some.deeply.nested.model.id}} - {{some.deeply.nested.model.name}}</div>
Do this:
<div with="some.deeply.nested.model" as="t">{{t.id}} - {{t.name}}</div>
Or, do it without using as:
<div with="some.deeply.nested.model">{{id}} - {{name}}</div>
I came across this just now and tested out the ngAlias directive, which may or may not work fine, I don't know because I had issues with it, probably due to some other unrelated error, and I decided on this simpler method:
<div ng-repeat="uhome in [obj.user.contact.home]">
<p>{{uhome.street}}</p>
<p>{{uhome.city}}</p>
<p>{{uhome.state}}</p>
<p>{{uhome.zip}}</p>
</div>
Tada! "obj.user.contact.home" is now aliased to "uhome". I believe this solution is actually exactly what you wanted when you asked this question, too, because it literally uses ng-repeat and therefore has all the benefits of ng-repeat's built-in aliasing mechanism.
<div ng-init="uhome = obj.user.contact.home">
<p>{{uhome.street}}</p>
<p>{{uhome.city}}</p>
<p>{{uhome.state}}</p>
<p>{{uhome.zip}}</p>
</div>
https://code.angularjs.org/1.3.16/docs/api/ng/directive/ngInit
As far as i know there is nothing really simple.
There are some hack. You can remap object to new scope using new controller
<div ng-controller="Ctrl as c" ng-init="c.makeAlias(obj.user.contact.home)">
<p>{{c.home}}</p>
</div>
where Ctrl just store makeAlias argument to it's scope as home
this.makeAlias(home) {
this.home = home;
}
Another solution is use $scope.$watch and remap object to the more convenient one.
I don't think Angular provides out of box supports for such things. But You can definitely create Domain object.Convert your http response into domain object. Within your domain object you can have helper method to get the required information.
For example in your case you can have Domain object like this
factory('User', function () {
return function (id,name, contact, purchase_hist) {
this.id = id;
this.name = name;
this.contact = contact;
this.purchase_hist = purchase_hist;
this.getContact = function(){
return this.contact.home;
};
};
})
you can use this domain object in your controller
$scope.user = // domain object return from your service
in html you can use
{{user.getContact().street}}
You can redefine your domain to make thing easier.It might be additional work. But when you have to do same thing again and again.One can enjoy writing code this way
Related
We're developing a set of (ideally) flexible, component-based re-usable templates in angularjs 1.2 to develop a series of e-learning modules.
Part of the spec requires the tracking of 'completable' components. At the moment the main controller looks like this:
app.controller('mainCtrl', ['$scope', function($scope) {
$scope.completables = [];
$scope.completed = [];
$scope.addCompletable = function (object) {
$scope.completables.push(object);
// also set correlating completed property to 'false' for each completable added
$scope.completed.push(false);
}
$scope.componentCompleted = function(id) {
// Set complete to 'true' for matching Sscope.completed array index
// We COULD use .indexOf on the completables array, but that doesn't work with IE8
var tempArray = $scope.completables;
var matchingIndex = -1;
for (var i=0; i<tempArray.length; i++) {
if (tempArray[i]==id) {
matchingIndex = i;
}
}
if (i>-1) {
$scope.completed[matchingIndex] = true;
}
}
}]);
We have a eng-completable attribute that triggers the following directive:
app.directive('engCompletable', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// add the id of this element to the completables array in the main controller
scope.$parent.addCompletable(attrs.id);
}
}
});
So every time angular encounters an 'eng-completable' attribute on an element, it calls addCompletable on the parent scope which adds the element id to the 'completables' array and 'false' to the corresponding index of the 'completed' array.
In the eng-popup attribute directive, we have a function to check if it has been made visible:
app.directive('engPopup', function() {
return {
restrict: 'A',
replace: true,
templateUrl: 'components/popup.html',
link: function(scope, element, attrs) {
scope.$watch(function() { return element.is(':visible') }, function() {
scope.$parent.componentCompleted(attrs.id);
});
}
};
});
Which also uses the parent scope to trigger the 'componentCompleted' function. I've been told that referring to the parent scope is bad practise, and it is also messing up our unit tests, apparently.
I'd like to know what is the alternative. How can I let my app know that a specific component has been completed? And where should this state be tracked?
I'd really like to know HOW to do this - not just be told that I'm doing it the wrong way. Please let me know what the alternative is.
But, as always, any help will be much appreciated.
One alternative would be to create a Service to be responsible to track all the components and keep their states (complete/not completed).
It will remove the need for $scope.parent and the service can be injected into any controller or directive you need.
:)
If that completables list is application-wide yo could consider adding it to your $rootScope along with the addCompletable method —and any other relate methods— instead of adding it to your mainController's $scope.
This way you could substitude your scope.$parent.componentCompleted(attrs.id); with $rootScope.componentCompleted(attrs.id); and avoid to make calls to scope.$parent.
I can find bits and pieces of how to solve this, but no concrete way to make it work.
I have an asynchronous call to a server to fetch data in AngularJS and wish to store it in a variable. This variable then needs to be accessible to all the directives in the app, but they obviously all need to wait for the variable to be assigned before they can use it. I'm also using TypeScript and its export functionality to spin directives from their own functions.
Controller
export class MainController{
fundData: Object;
constructor(scope, FundService) {
FundService.fetchData('some_param').then(d => {
let data = d[0],
fundProps = data.properties_pub;
this.fundData = {
'isin': data.clientCode,
'nav': fundProps.nav.value,
'nav_change': fundProps.nav_change.value.toFixed(2),
'nav_change_direction': change,
'total_aum': fundProps.net_asset.value.toFixed(2)
};
scope.ctrl = this;
});
}
}
Directive
class OverviewController {
scope: ng.IScope;
constructor(scope){
scope.$watch('data', newVal => {
console.log(newVal);
});
}
}
OverviewController.$inject = ['$scope'];
export function overview(): ng.IDirective {
return {
restrict : "C",
controller : OverviewController,
controllerAs : "overview",
template : require("../templates/overview"),
bindToController :{
data: '='
}
}
}
HTML
<div ng-controller="MainController">
<div class="overview" data="ctrl.fundData"></div>
</div>
Bootstrap Process
let module = angular.module(MODULE_NAME,[])
.controller('MainController', ['$scope','FundService', MainController])
.service('FundService', FundService)
.directive('overview', overview);
Things I've Tried:
$rootScope
I can set something static and share it, so this works:
$rootScope.data = 2;
This doesn't:
someFunction().then(data => { $rootScope.data = data });
Maybe there's something about promises in $rootScope I don't understand.
Setting in controller
I can set the result of the call to a variable in the controller, set that to an attribute in the directive, and then bind the attribute to its controller, but this doesn't work either, even if I use $watch on the variable.
What I would do is fetch the data, store it in a service (which I think you are already doing) and then broadcast an event when the data in the service is updated. Here's an example (in raw javascript)
module.service('DataService', function(rootScope) {
...
var data;
services.setData = function(newData) {
data = newData;
rootScope.$broadcast('DataUpdated');
};
...
});
And then in your directives all you would need to do is listen for the 'DataUpdated' event:
scope.$on('DataUpdated', ...);
Hope that helps!
I cannot get a binded service value to update when it is changed. I have tried numerous methods of doing so but none of them have worked, what am I doing wrong? From everything I have seen, this seems like it should work...
HTML:
<div class="drawer" ng-controller="DrawerController">
{{activeCountry}}
</div>
Controller:
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService', function($scope, mapService) {
$scope.$watch(function() { return mapService.activeCountry }, function(newValue, oldValue) {
$scope.activeCountry = mapService.activeCountry;
});
}]);
Service:
angular.module('worldboxApp').
service('mapService', function(dbService, mapboxService, userService) {
this.init = function() {
this.activeCountry = {};
}
this.countryClick = function(e) {
this.activeCountry = e.layer.feature;
};
this.init();
});
I put a break point to make sure the mapService.activeCountry variable is being changed, but all that ever shows in the html is {}.
If you work with objects and their properties on your scope, rather than directly with strings/numbers/booleans, you're more likely to maintain references to the correct scope.
I believe the guideline is that you generally want to have a '.' (dot) in your bindings (esp for ngModel) - that is, {{data.something}} is generally better than just {{something}}. If you update a property on an object, the reference to the parent object is maintained and the updated property can be seen by Angular.
This generally doesn't matter for props you're setting and modifying only in the controller, but for values returned from a service (and that may be shared by multiple consumers of the service), I find it helps to work with an object.
See (these focus on relevance to ngModel binding):
https://github.com/angular/angular.js/wiki/Understanding-Scopes
If you are not using a .(dot) in your AngularJS models you are doing it wrong?
angular.module('worldboxApp', []);
/* Controller */
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService',
function($scope, mapService) {
//map to an object (by ref) rather than just a string (by val), otherwise it's easy to lose reference
$scope.data = mapService.data;
$scope.setCountry = setCountry; //see below
function setCountry(country) {
// could have just set $scope.setCountry = mapService.setCountry;
// however we can wrap it here if we want to do something less generic
// like getting data out of an event object, before passing it on to
// the service.
mapService.setCountry(country);
}
}
]);
/* Service */
angular.module('worldboxApp')
.service('mapService', ['$log',
function($log) {
var self = this; //so that the functions can reference .data; 'this' within the functions would not reach the correct scope
self.data = {
activeCountry: null
}; //we use an object since it can be returned by reference, and changing activeCountry's value will not break the link between it here and the controller using it
_init();
function _init() {
self.data.activeCountry = '';
$log.log('Init was called!');
}
this.setCountry = function _setCountry(country) {
$log.log('setCountry was called: ' + country);
self.data.activeCountry = country;
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="worldboxApp">
<div ng-controller="DrawerController">
<button ng-click="setCountry('USA')">USA</button>
<br />
<button ng-click="setCountry('AUS')">AUS</button>
<br />Active Country: {{data.activeCountry}}
</div>
</div>
In some case $watch is not working with factory object. Than you may use events for updates.
app.factory('userService',['$rootScope',function($rootScope){
var user = {};
return {
getFirstname : function () {
return user.firstname;
},
setFirstname : function (firstname) {
user.firstname = firstname;
$rootScope.$broadcast("updates");
}
}
}]);
app.controller('MainCtrl',['userService','$scope','$rootScope', function(userService,$scope,$rootScope) {
userService.setFirstname("bharat");
$scope.name = userService.getFirstname();
$rootScope.$on("updates",function(){
$scope.name = userService.getFirstname();
});
}]);
app.controller('one',['userService','$scope', function(userService,$scope) {
$scope.updateName=function(){
userService.setFirstname($scope.firstname);
}
}]);
Here is the plunker
Note:- In Some case if broadcast event is not fired instantly you may use $timeout. I have added this in plunker and time depends on your needs. this will work for both factories and services.
I have an angular directive uses the = operator to two-way bind an isolated scope with an attribute of its parent scope:
app.directive('nestedDirective', function(){
return {
scope: {
model: '='
},
link: function($scope){
...
}
}
});
I understand that any changes to $scope.model will propagate to the parent scope. However, deleting $scope.model does not propagate. delete($scope.model) My question thus: How do I delete the referenced variable and propagate the removal to the parent scope.
This codepen should illustrate what I'm trying to do not (Not even watching the scope fires an event)
This question gets asked very, very often, so I'll start by referring to the wiki article.
Basically, follow the "dot rule" : if you need to modify a property (directly), scope it under another property so that JS prototypal inheritance can kick in :
var model = {prop: "val"};
var a = {model: model};
model = null;
console.log(a.model.prop); // prints val
var b = {a: a};
a.model = null;
console.log(b.a.model); // prints null
This is the same here (even if this doesn't use a prototypal inheritance to keep things simple).
I've edited your code pen source below, i'm sure there's a simpler way of doing this but i've just tried this and it works, it should start you on the right path:
<ul ng-app="app" ng-controller="ctrl">
<dir model="data.children" child="child" ng-repeat="child in data.children"></dir>
</ul>
var app = angular.module('app', []);
app.controller('ctrl', function($scope){
$scope.data = {};
$scope.data.children = [
{name: 'Ben'},
{name: 'Heffler'},
{name: 'Schubert'}
];
$scope.$watchCollection('data.children', function(){
console.log("children shallow watch", $scope);
});
$scope.$watch('data.children', function(){
console.log("children deep watch",$scope);
}, true);
});
app.directive('dir', function(){
return {
restrict: 'E',
scope: {
model: '=',
child:'='
},
replace: true,
template: '<div>{{child.name}} <button ng-click="remove()">Remove</button></div>',
link: function(scope, element, attrs){
scope.remove = function(){
// I'm just deleting the first one as an example.
delete(scope.model[0]);
console.log("children inner scope", scope)
}
}
};
});
I'm not sure why you would want to delete the properties but i'm sure you have your reasons, just to show you it is possible.
EDIT
Here is the edited code pen (see the console logs to see the deleted items in scope). http://cdpn.io/Ghmvk
I'm having some headaches with scope, not Angular scope but JavaScript itself.
I have a factory called chartParsingHelpers it's something like this:
angular.module('myApp')
.factory('chartParsingHelpers',['dateUtils', function(dateUtils){
return {
getInitArray : function(){
return [ 'Stuff', 'Stuff 2' ];
},
doFancyStuff : function (data){
var initArray = this.getInitArray();
}
};
}]);
That works great if I inject chartParsingHelpers anywhere and call doFancyStuff it will work. However, I wanted to have some config for my fancy charts since they share code structure and I think is great to share that config around directives and all friends so, I created something like this:
angular.module('visits')
.factory('ChartTypes',['dateUtils', 'chartParsingHelpers', function(dateUtils, chartParsingHelpers){
return {
config : [
{
name : 'Chart 1',
xAxisFormatter : dateUtils.getFullHour,
parseMethod : chartParsingHelpers.doFancyStuff
}
]
};
}]);
My plan is to inject ChartTypes into a Controller and into a directive (for now) but then I receive this error:
TypeError: Object #<Object> has no method 'getInitArray'
If I look into the value of this then... well, it's the value of the object itself with name, xAxisFormatter and so on...
How could I approach this? Is this the right way?
The context of this in chartParsingHelpers.doFancyStuff is probably the issue. I would create the factory like this:
angular.module('myApp')
.factory('chartParsingHelpers',['dateUtils', function(dateUtils){
var self = {};
self.getInitArray = function() {
return ['Stuff', 'Stuff 2'];
};
self.doFancyStuff = function(data) {
var initArray = self.getInitArray();
};
return self;
}]);
That way you make sure that you're always referencing that object.