I have the below HTML on a page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="myCart">
<head>
<title>AngularJS Shopping Cart</title>
<link href="css/jsonstore.css" rel="stylesheet" />
</head>
<body>
<div id="container">
<div id="header">
<h1>The JSON Store</h1>
<div class="cart-info">
My Cart (<span class="cart-items">{{item.basketCount}}</span> items)
</div>
</div>
<div id="main" ng-view>
</div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular-resource.js"></script>
<script src="js/routing.js"></script>
<script src="js/dataresource.js"></script>
<script src="js/basketinfo.js"></script>
<script src="js/index.js"></script>
<script src="js/detail.js"></script>
</body>
</html>
The div "main" gets replaced by HTML templates by my routes however I would like to update the header section with a shopping basket count.
I have tried model binding it as shown in the HTML and below:
function DetailController($scope, item, basketDetail) {
$scope.item = item;
$scope.item.basketCount = basketDetail.getCount();
//more code
}
I've also tried just injecting the service and calling it from the HTML. Both ways do not do anything.
Can someone help please?
Thanks
Your header div is really a view, just like the other views you've defined for use with ng-view. Someday, you might want to show more than just a basketCount model in that header view. But the fact that you are projecting even one piece of model data into that header section makes that section a view. So, I would recommend that be given its own $scope to project that model, hence its own controller.
What remains then is where to put the basketCount model? And we must consider that multiple views may allow the user to do something that need to affect that model. Angular's normal answer for "many need access" is dependency injection. So, I would put the basketCount model into a service. Views/controllers that need access to it can inject it. Someday your app may have additional views that don't need access to these models, so those views would not inject the service.
Potentially, the entire basket could be modeled in this service:
app.factory('basketService', function() {
return {
items: [],
itemCount: 0,
...
}
});
function HeaderCtrl($scope, basketService) {
$scope.basket = basketService;
...
}
function DetailCtrl($scope, basketService) {
$scope.basket = basketService;
...
}
<div id="header" ng-controller="HeaderCtrl">
<h1>The JSON Store</h1>
<div class="cart-info">
My Cart (<span class="cart-items">{{basket.itemCount}}</span> items)
</div>
You'll need to inject $rootScope, then update it:
function DetailController($scope, $rootScope, basketDetail) {
$rootScope.item = $rootScope.item || {};
$rootScope.item.basketCount = basketDetail.getCount();
//more code
}
There are a lot more ways to do this, but this is my first suggestion, because it's probably the easiest.
EDIT: per your request, other ways to do this...
You could use $parent to push to your parent scope. The upside is it's pretty quick and clean... the downside is it's a little sloppy in that one of your controllers makes assumptions about what it's parent is to some degree (but that's still not terrible, really):
{{item.basketCount}}
<div ng-controller="InnerCtrl">
</div>
function InnerCtrl($scope, basketDetail) {
$scope.$parent.item = $scope.$parent.item || {};
$scope.$parent.item.basketCount = basketDetail.getCount();
}
Then there's the method #asgoth mentioned above where you use nested controller and a method on the parent scope to update the parent scope. Valid, but like my other solution in this "other ways to do it" section, it relies on assumptions made about the controller's container, and it also relies on you creating an additional controller.
Finally, you could create a service. Now services aren't generally used this way, but you could use one this way.. Where you could take your basketDetail service, and use it to pass the value back and forth. Like so:
app.factory('basketDetail', function() {
return {
items: { basketCount: 0 },
getCount: function() {
//return something here
return 123;
}
}
});
function FooCtrl($scope, basketDetail) {
$scope.items = basketDetail.items;
$scope.items.basketCount = basketDetail.getCount();
}
function BarCtrl($scope, basketDetail) {
$scope.items = basketDetail.items;
}
<div ng-controller="FooCtrl">
{{items.basketCount}}
</div>
<div ng-controller="BarCtrl">
{{items.basketCount}}
</div>
This works because the $scope in both controllers is keeping a reference to the same object, which is maintained by your basketDetail service. But again, this isn't really the recommended way.
All of that said: $rootScope, IMO is most likely what you're looking for.
It doesn't require the creation of an additional controller.
It doesn't require the creation of any additional function references.
Will not cause the creation of any additional parent/child Scope nesting and subsequent watches.
No real need for $rootScope. Create a parent controller (e.g. RootController) with a function on its scope. The child scopes will automatically inherit it:
<div id="container" ng-controller="RootController">
...
function RootController($scope) {
$scope.item = {};
$scope.setBasketCount = function (detail) {
$scope.item.basketCount = detail.getCount();
}
}
In your detail controller you just use the setBasketCount() function:
function DetailController($scope, item, basketDetail) {
$scope.item = item;
$scope.setBasketCount(basketDetail);
//more code
}
Related
I'm trying to use AngularJS in my cfm files to display data from cfquery resultset.
I used below code in my cfm file.I'm not able to see any output.
P.S. I'm really new to AngularJS. So if anyone can please help me out here it would be great.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html ng-app="Demo">
<head>
<link rel="stylesheet" href="/Applications/_Common/style.css" type="text/css">
</head>
<body>
<cfsetting enablecfoutputonly="Yes">
<CF_GetProjectData project_number=349246 query_name="GetProject">
<div ng-controller="DemoController">
<div ng-repeat="number in project">
{{number.project_number}} - {{number.project_name}}
</div>
<!-- <input name="imprint" type="text" size="10" ng-model="first">
<p>{{first}}</p> -->
</div>
<cfoutput>
<script language="JavaScript" src="/CFIDE/scripts/wddx.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script language="text/javascript">
var theProjectArray; < cfwddx action = "CFML2JS"
input = "#GetProject#"
toplevelvariable = "theProjectArray" >
</script>
<script>
var Demo = angular.module("Demo", []);
Demo.controller("DemoController", function($scope) {
$scope.project = theProjectArray;
alert(theProjectArray);
});
</script>
</cfoutput>
<cfsetting enablecfoutputonly="No">
</body>
</html>
I am not sure about Angular.js but based on the code you have posted, it seems, you need to wrap ng-controller div in cfoutput. This is because you have set enablecfoutputonly="Yes". So only the content inside cfoutput will be rendered.
I'm a CF developer that's currently learning Angular as well. First of all Angular is a MVC framework and will work for you best of you follow the rules of Separation of Concern (SoC). I know unless you are using Object Relational Mapping (ORM) in CF this is counter intuitive but it will save you so much hassle and trouble shooting later.
For your problem right now though i would
combine both script blocks.
your variable theProjectArray is defined with var so it's not
global. Are you sure it's making it into your controller?
the line toplevelvariable = "theProjectArray" > ... is the greater
than sign a type-o?
After you've done those i would console.log(theProjectArray); right after it's defined. Then console.log(theProjectArray); again in your controller to ensure it's getting passed in properly.
Just for your reference here is a very basic example of a controller and a factory in angular calling a CFC. Since i've been doing things this way the only time i use ColdFusion is to retrieve data and model it. It's simplified my code and logic quite a bit and has allowed me to do a lot more now that i'm not leaning on ColdFusion.
var myapp = angular.module("test.myapp", [])
myapp.controller("MyController", function($scope, getevent) {
$scope.myData = {};
$scope.myData.doUpdate = function(item, event) {
getevent.GetProgram(item).success(function(data, status, headers, config) {
console.log(data.DATA);
$scope.test = data.DATA;
$scope.test.DATE = new Date(data.DATA.DATESTART);
});
getevent.GetProgram(item).error(function(data, status, headers, config) {
console.log("FAILED: "+item);
alert("AJAX failed!");
});
}
});
myapp.factory('getevent', function($http){
return {
GetProgram: function(item) {
var programInfo = '/foundation/cfc/calendar.cfc?method=getevent&eventid='+item;
return $http.get(programInfo);
}
};
});
I'm trying to get my head around scopes and after dredging through a number of blogs, stack overflow answers and the docs I am still stuck.
angular.module('app', [])
.factory('alphabet', function () {
data = [
'c',
'b',
'a'
];
return {
get : function () {
return data;
},
set : function (val) {
data.push(val);
}
};
})
.controller('AlphaCtrl', function (alphabet) {
this.alphabet = alphabet;
})
.directive('sortableTable', function () {
return {
scope : {
"param" : '#'
},
link : function (scope) {
console.log(scope.param);
}
};
})
;
HTML:
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div ng-controller="AlphaCtrl as alpha">
<table sortable-table param="{{alpha.alphabet}}">
<thead>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</body>
</html>
What I would like to do is be able to access a service/factory that's being used in an outer controller from within a directive. So for example when I handle a click event I can add items to the data. That seems to be a good way of keeping things decoupled but I am open to suggestions there.
The problem at hand is that 'param' is undefined. I've also tried using '&' but that's not doing anything for me. Could someone put me on the path to Angular righteousness?
I would inject the service directly into the directive:
.directive('sortableTable', function (alphabet) {
return {
scope : {},
link : function (scope) {
console.log(alphabet);
}
};
})
This is indeed a good way of keeping things decoupled, if this is a directive that is used across controllers and views.
Edit for a bit more complex solution: It is possible to do it by injecting the service into the scope of the directive, though I would not recommend it if you don't need to switch services on the fly, since the method above is easier. I could see some use cases though, if you would want to input a different service (with the same get/set structure) in another controller for example. Here's how you could do it via scope:
.controller('myController', function($scope, alphabet) {
$scope.alphabet = alphabet;
})
.directive('myDirective', function(){
return {
scope: {
service: '='
},
template: '<div ng-bind="service.get()"></div>'
}
})
And in the template:
<div data-my-directive service="alphabet"></div>
The trick here is using service: '=' as this creates a two-way binding between the scope-variable in the controller (which is bound to the service) and the scope-variable in the directive. http://jsfiddle.net/vt52bauu/2/
I don't think the get/set is going to work the way you are expecting in an Angular factory.
this.alphabet = alphabet.get();
(simple plunkr demo here)
SUMMARY:
There is a leak using ng-repeat after the 2nd wave iterating over an 'array' of custom objects like this :
<div ng-repeat="d_sampleObject in mySampleObjects">
{{d_sampleObject.description}}
</div>
Memory profile reveals an extra 'd_sampleObject' left over and not de-referenced. More details (via a controller and an injected service) below. A simple demonstration also in the provided plunkr link. Any thoughts and help greatly appreciated in advance!
NOTE: 'mySampleObjects' is an array of the following instances:
ml.MySampleObject = function (id) {
this.id = id;
this.description = 'this is object #:' + ' '+id;
}
DETAILS:
I have a custom object model that reflects the business domain objects that we utilize in our AngularJS app. I have found that when I pass an instance of one of my custom objects to ng-repeat, a reference is kept to it (I think by Angular) and memory is not freed. This happens on the second 'wave' (click on 'refresh') of the ng-repeat as it iterates, again, over its array of objects. This leak is exposed in my Profile tests (in Chrome) . Here is a short example in plunkr. Click on 'refresh' button once (or more) to see the extra 'd_sampleObject' object instance that is leaked (in Chrome Profile Inspection). Note, the 'd_sampleObject' name is only used when passed to ng-repeat. I have included screenshots of the extra object instance ('d_sampleObject') that is being leaked further below. Why is there a leak and how can it be avoided?
(Note, I have found if I don't iterate over my object collection (JS array) thru an object but rather thru a primitive index ('integer'), there is no leak. The leak seems to only happen when I use an object reference as a result of ng-repeat iterations)
SIMPLE HTML:
<!DOCTYPE html>
<html ng-app="memoryleak">
<head>
<meta charset="utf-8" />
<title>Memory Leak Test</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<script data-require="angular.js#1.3.x" src="https://code.angularjs.org/1.3.13/angular.min.js" data-semver="1.3.13"></script>
<script src="app.js"></script>
<script src="dataservice.js"></script>
</head>
<body ng-controller="MainCtrl">
<div ng-repeat="d_sampleObject in mySampleObjects">
{{d_sampleObject.description}}
</div>
<br>
<button ng-click="redo()">Number of refreshes: {{numRedos}}!</button>
</body>
</html>
SIMPLE APP.JS
(function(ml) {
var app = angular.module('memoryleak',['servicemodule']);
app.controller('MainCtrl', ['$scope', 'dataservice', function($scope, dataservice) {
$scope.redo = function () {
$scope.numRedos++;
$scope.mySampleObjects = dataservice.myObjectCollection;
dataservice.redo();
}
$scope.redo();
}]);
}(window.MEMLEAK = window.MEMLEAK || {}));
SIMPLE dataservice.js
(function(ml) {
'use strict';
var serviceModule = angular.module('servicemodule',[]);
serviceModule.factory('dataservice', ['$rootScope', '$http',
function ($rootScope, $http) {
this.myObjectCollection = [];
this.redo = function () {
this.numRedos++;
// that.myObjectCollection = [];
this.myObjectCollection.length = 0;
for (var i = 0; i < 10; i++) {
var sampleObject = new ml.MySampleObject(i);
that.myObjectCollection.push(sampleObject);
}
sampleObject=null;
}
ml.MySampleObject = function (id) {
this.id = id;
this.description = 'this is object #:' + ' '+id;
}
return this; //return the entire service to make methods accessible to dependents
}]);
}(window.MEMLEAK = window.MEMLEAK || {}));
SCREENSHOT 1: (FIRST PAGE LOAD--there are 10 'mySampleObjects' created)
SCREENSHOT 2: (CLICKED ON REFRESH--there is an 11th mySampleObject created/leaked with a reference to the instance name of 'd_sampleObject' passed to ng-repeat.)
There is acknowledgement by the AngularJS folks that this is indeed a bug in the framework. A fix and pull request has been posted.
I have also asked what the timeframe is for a formal fix.
I have a cordova app in which I want to show the details of a location. For some reason when I try to display a variable in HTMl which is being successfully assigned in JS, nothing appears.
JS controller:
app.controller('placeCtrl', function($scope, LocDat){
LocDat.async().then(function(d){
$scope.item= places.selectedItem;
$scope.locs = [];
for(var i=0; i<d.length; i++){
if(d[i].attributes.Joint.id === places.selectedItem.id){
getDistance(d[i]);
$scope.locs.push(d[i]);
}
}
$scope.showSite = function(){
//var ref = navigator.app.loadUrl($scope.item.attributes.Website, '_blank');
var ref = window.open($scope.item.attributes.Website,'_blank','location=yes');
}
$scope.showDetail = function(index){
var selectedItem = d[index];
d.selectedItem = selectedItem
$scope.l = selectedItem;
console.log($scope.l.attributes.City);
$scope.ons.navigator.pushPage('location_detail.html', { title : d.selectedItem.attributes.Address });
}
});
HTML:
<!DOCTYPE html>
<html>
<body>
<div ng-controller="placeCtrl">
<ons-page class="center" ng-device-backbutton="myNavigator.popPage()">
<ons-toolbar>
<div class="left"><ons-back-button ons-if-platform="ios">Back</ons-back-button></div>
<div id="title" class="center">{{l.attributes.City}}, {{l.attributes.State}}</div>
<!--<div class="left" onclick=".myNavigator.popPage()"><ons-back-button>Back</ons-back-button></div>-->
<!--<div class="center">Page 2</div>-->
</ons-toolbar>
<h2 align="center">Location Details Go Here</h2>
<!--enter more content here-->
</ons-page>
</div>
</body>
</html>
Image of the Console output:
Apparently my reputation is too low to post images... Seriously? Anyway, it displays the City name in the console successfully, but the html only shows the comma
Services that make async calls, such as your LocDat, do not automagically trigger a digest event when they return. If you're writing a service it should call a $scope.$apply() chained to the end of the promise. Alternatively you can wrap any changes to $scope variables in an apply and that should get you where you need.
$scope.$apply( function() { $scope.l = selectedItem; } );
In angularjs data binding, if the data type is list or object, it will pass by reference value in view.
When you do like $scope.l = selectedItem, the reference is changed, but the watched reference is previous one. So it will be always better to bind by an attribute on an object, but not the object itself. like:
<div id="title" class="center">{{obj.l.attributes.City}}, {{obj.l.attributes.State}}</div>
And update in controller with:
$scope.obj.l = selectedItem;
The issue was that the scope changed when I loaded the new page. I'm now passing the data through the parameters of onsenui's pushpage function and assigning them to the scope variables in a separate controller.
I have some trouble identifying a view update that is not happening neither when I set a property on the $scope nor when I set a property on the controller prototype.
I have create a quick example of what I am doing to illustrate the issue.
I have, for instance the following view:
<html data-ng-app="test">
<head>
<script data-require="angular.js#1.2.18" data-semver="1.2.18" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<h1>Hello Plunker!</h1>
<div data-catalog="{ 'type': 'Category1', 'key': '252' }">
<span class="text-danger">{{query | json}}</span>
<span class="text-danger">{{catalog.model | json}}</span>
</div>
</body>
</html>
And a simple module with a directive and a controller:
angular
.module("test", [])
.controller("CatalogCtrl", ["$scope", "$parse", "$location", "$q", function ($scope, $parse, $location, $q) {
var catalog = this,
listener = function () {
var query = {},
params = $location.search();
try
{
query = $parse($scope.query)();
}
catch (exception)
{
console.error(exception);
}
if (_.isEmpty(query))
{
return;
}
$q
.all([
scroll({ top: 0 }),
Catalog.searchProducts(_.assign(query, params))
])
.then(function (response) {
catalog.model = response[1];
console.log(catalog.model, response[1]);
})
.catch(function () {
catalog.model = {};
})
.finally(function () {});
};
catalog.model = {test:""};
//$scope.listen("$locationChangeSuccess", listener);
}])
.directive("catalog", function () {
return {
controller: "CatalogCtrl as catalog",
scope: {
query: "#catalog"
},
restrict: "A",
replace: false
};
});
The issue is that neither {{query | json}} nor {{catalog.model | json}} are rendered in the view and I am not sure what is the cause of it. It might be something I am missing or doing wrong, but I could use some help with it if anyone spots my mistake :)
There are a few issues with your code...
1.
Your directive has it controlelr attached and you place some values on its scope (query, catalog etc). Then you try to access those values from an element that is outside of the directive (and thus has a different scope that knows nothing about query, catalog etc. E.g.
<!-- Let's say somehow DIV#1 has scope $001 -->
<div id="1">
<!-- Somehow DIV#1.1 creates a new scope ($002) -->
<div id="1.1" some-directive>
<!-- DIV#1.1.1 will have access to scope $002 -->
<!-- (under certain cirsumstances) -->
<div id="1.1.1"></div>
</div>
<!-- DIV#1.2 (which is outside of DIV#1.1) -->
<!-- will have access to scope $001 (but not $002) -->
<div id="1.2"></div>
</div>
2.
To make things even more complicated, your directive creates an isolate scope, which means that any content it has will not see your directive's scope, but its parent scope. I.e. in the example above, DIV#1.1.1 will have access to scope $001, not $002.
What you can do about it (which basically mean, explicitly state that your directive's content should be included (transcluded) into its template. This gives you greater control on what's going on and allows you to bind the content of your directive to the scope you want (i.e. your directive's isolate scope).
The resulting code should look like this:
<div data-pj-catalog="{ 'type': 'Category1', 'key': '252' }">
...
<div class="col-12">
...
<span class="text-danger">{{query | json}}</span>
<span class="text-danger">{{catalog.model | json}}</span>
...
<div>
</div>
.directive('pjCatalog', function () {
return {
restrict: 'A',
transclude: true,
template: '<div ng-transclude></div>',
controller: 'CatalogCtrl',
scope: {
query: '#pjCatalog'
},
link: function (scope, elem, attrs, ctrls, transFn) {
transFn(scope, function (cloned) {
elem.empty().append(cloned);
});
}
};
})
See, also, this short demo.
Note:
This is considered "advanced" directive stuff (so it sounds (and is) much more complicated than most directive stuff) and should be required in rae cases only.
I am pretty sure there is a much easier way to achieve what you want (e.g. using a non-isolate scope) with slight modifications (but I am not sure what you want in order to help further.