Angular binding to a read-only property - angularjs

How do you enforce read-only properties in a performant way in Angular?
Controller:
function MyCtrl($scope) {
$scope.clickCount = 0;
$scope.incrementCount = function() {
$scope.clickCount = $scope.clickCount + 1;
}
}
View:
<div ng-controller="MyCtrl">
Clicked {{clickCount}} times
<button ng-click="incrementCount()">Doober</button>
<input type="text" ng-model="clickCount" /><!-- how do I prevent this -->
</div>
I know I could make clickCount a getter function getClickCount(), but will that kill the performance since Angular will have to call this function on every digest cycle?
http://jsfiddle.net/zb05om1k/
Update
I'm looking for a way that makes it clear that the read only property should not be changed directly but instead through the provided function. Additionally, prevent the view from changing the property directly.

use the angular directive ng-readonly, it works just like it sounds...
https://docs.angularjs.org/api/ng/directive/ngReadonly

In your example, incrementCount() is called once and takes a negligible amount of time to execute, then Angular starts a digest cycle and the DOM rebuilds, which would happen anyway.
As a general rule, you don't need to optimize code whose time is bound by user input. Your example code looks fine to me.
If you really want to enforce separation of concerns by making the property read-only, you can use Object.defineProperty to prevent writes:
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function MyCtrl($scope) {
var clickCount = 0; // Private variable
Object.defineProperty($scope, 'clickCount', {
set: function() { // Prevent views from editing data
throw new Error('Operation not supported');
},
get: function() {
return clickCount;
}
});
$scope.incrementCount = function() {
clickCount++;
}
});
When you put data in the input, the controller throws an error and the change is immediately erased in the DOM.
Fiddle: http://jsfiddle.net/acbabis/5bq5oeq3/

Related

AngularJS - How do I modify variables in $rootScope?

I've got a potentially really dumb question, but how do I modify variables up in $rootScope in Angular? I've got a slide-in sidebar that I want to change the content on whenever someone clicks on a thumbnail, and I figured the easiest way to handle where the data in the sidebar comes from/the sidebar visibility would either be in global values, or in $rootScope. I'm trying to keep everything as simple as possible, but I just don't know how to handle modifying global variables.
My angular code surrounding this is:
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', '$rootScope',
function ($scope, $rootScope) {
$scope.isDetail = $rootScope.detail_visible.value;
$scope.url = $rootScope.currentUrl.value;
$scope.hide = function($rootScope) {
$rootScope.detail_visible.value = false;
};
}]);
and the connecting HTML is
<div id="detail_box" ng-class="{d_show: isDetail, d_hide: !isDetail}">
<div ng-include="url + 'detail.html'"></div>
</div>
In essence, I'm trying to make it so that when you click on a thumbnail, it changes the currentUrl value from 'visual/design/1/' to whatever they've clicked on (like, 'music/solo/2' or whatever) then changes the value of detail_visible to false, so that the classes on my sidebar switch and I get a nice little slide-in, with fresh content loaded via ng-include which I kind of love a thousand times more than I thought I would. I've been banging my head against this for about three hours now, breaking everything else on this app whenever I get the chance. What am I screwing up here? Alternatively, is there a better way of doing this?
My reason for using global variables is that I have multiple thumbnails in multiple controllers, and I want each one to be able to dynamically change the URL in my ng-include.
For your question, you change the $rootScope variable simple by referencing it with
$rootScope.detail_visible.value = newValue;
but you dont need to inject $rootScope to your function:
$scope.hide = function() { //without $rootScope
$rootScope.detail_visible.value = false;
};
But, I would suggest you to implement a service and not to pollute the rootscope for such task.
https://docs.angularjs.org/guide/services
Object properties of scopes are inherited -- in your controller, you should be able to modify $scope.detail_visible.value and see it affect the $rootScope. You still have to initialize it on the $rootScope in .run() though.
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', function ($scope, $rootScope) {
$scope.hide = function() { // don't need to pass an argument
$scope.detail_visible.value = false;
};
}]);
view:
<div id="detail_box" ng-class="{d_show: currentUrl.value, d_hide: !currentUrl.value}">
<div ng-include="currentUrl.value + 'detail.html'"></div>
</div>

Why the modifications i apply on a service don't impact the DOM on all the controllers it is involved

I am new using AngularJS, i am interesting about the fact that when we update a data, Angular automatically impacts the modifications everywhere the data is involving.
But unfortunately, i can't make it works.
The simple thing i am trying to do is to make a change on a controller B, and i want the changes to be achieve on the controller A, since the data is referering to the same Service.
The data is correctly impacting on the both controllers, but the DOM is not updating according to this modification, here is the test:
HTML
<body>
<div ng-controller="ACrtl">
<h1>{{is_logged}}</h1> <!-- Always false -->
<button ng-click="check()">Check</button> <!-- true/false -->
</div>
<div ng-controller="BCrtl">
<button ng-click="{{is_logged=!is_logged}}">Toggle throught the DOM</button> <!-- Doesn't change anything on the Javascript -->
<button ng-click="toggle()">Toggle throught the controller</button> <!-- Change the Javascript but doesn't impact the other controller's scope -->
</div>
</body>
JS
var app = angular.module('MyApp', []);
app.controller('ACrtl', function($scope, UserService) {
$scope.is_logged = UserService.is_logged;
$scope.check = function() {
console.log('is_logged='+UserService.is_logged); //The change is correctly made when changin is_logged on the controller B.
$scope.is_logged = UserService.is_logged;
};
});
app.controller('BCrtl', function($scope, UserService) {
$scope.is_logged = UserService.is_logged;
$scope.toggle = function() {
UserService.is_logged = !UserService.is_logged;
};
});
app.factory('UserService', function() {
var User = {
is_logged: false
};
return User;
});
I hope AngularJS is able to do this and it's something i am doing wrong in my code !
Here is a plunker
Primitive variables (like boolean) are passed by value in Javascript, and the variables $scope.is_logged are just copies of their values in the service. So, if the original service value is changed, then this won't affect any copies on the scopes.
A standard way or re-factoring this would be to share an object between the controllers, and not a primitive, so
app.factory('UserService', function() {
return {
status: {
is_logged: false
}
};
});
And then used in the controllers
$scope.status = UserService.status;
So the controller can change $scope.status.is_logged, and the changes will be seen in all the controllers.
You can see this at:
http://plnkr.co/edit/GLZmdsAnn3T5Xw80h4sV?p=preview
When you assign is_logged to the scope on each controller you are creating a new property on each controller, both of which are initialised to the value from UserService.
In your case what you can do is expose the service on the scope of each controller like so:
$scope.data = UserService
and in your view:
<h1>{{data.is_logged}}</h1>
Have a look at this answer and the links that it mentions.

Update scope value when service data is changed

I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>

angularJS $broadcast and $on

is there a way to make the $broadcast propagate the variable to the $on during initialization phase?
<div ng-app='test'>
<div ng-controller='testCtrl'> <span>{{testContent}}</span>
</div>
<div ng-controller="testCtrl2">
<input type='text' ng-change="updateContent()" ng-model="testContent2" />
</div>
</div>
var app = angular.module('test', []);
app.factory('sharedContent', function ($rootScope) {
var standardContent;
var resizeProportion;
return {
setStandardContent: function (newStandardContent) {
standardContent = newStandardContent;
$rootScope.$broadcast('updateContent');
console.log('broadcast');
},
getStandardContent: function () {
return standardContent;
},
setResizeProportion: function (newResizeProportion) {
$rootScope.$broadcast('updateResizeProportion');
},
}
});
app.run(function (sharedContent) {
sharedContent.setStandardContent('haha');
});
function testCtrl($scope, sharedContent) {
$scope.testContent;
$scope.$on('updateContent', function () {
console.log('receive');
$scope.testContent = sharedContent.getStandardContent();
});
}
function testCtrl2($scope, sharedContent) {
$scope.testContent2 = 'test';
$scope.updateContent = function () {
sharedContent.setStandardContent($scope.testContent2);
};
}
Sample fiddle : http://jsfiddle.net/jiaming/NsVPe/
The span will display the value as the input changes, which is due to the ng-change function.
However, at initialization phase, the value "haha" was not propagated to the $scope.testContent and thus, nothing was shown at first runtime. Is there a way to make the value "haha" appear at the first runtime?
Thank you.
Just provide a little delay using $timeout function. Just update the code in the factory it will start working.
Please refer the code below for the factory:
app.factory('sharedContent', function ($rootScope,$timeout) {
var standardContent;
var resizeProportion;
return {
setStandardContent: function (newStandardContent) {
standardContent = newStandardContent;
$timeout(function(){
$rootScope.$broadcast('updateContent');
},0)
console.log('broadcast');
},
getStandardContent: function () {
return standardContent;
},
setResizeProportion: function (newResizeProportion) {
$rootScope.$broadcast('updateResizeProportion');
},
}
});
The reason for this is that the ng-change triggers upon subsequent changes to the model identified by testContent2. When the controller initializes, the value "test" is assigned to it. ng-change then keeps a track of subsequent changes - the initial assignment does not qualify for this, only subsequent changes do.
http://jsfiddle.net/vZwy4/ - I updated the fiddle provided by you. Here you can see that the span tag is correctly populated with the data.
What you needed to do was instead of using ng-change, you should use the scope's $watch functionality. So remove the ng-change directive from the input box and remove the updateContent method. Instead, replace it with the following code wherein you watch the changes to the testContent2 model:
$scope.$watch('testContent2', function () {
if ($scope.testContent2 === undefined || $scope.testContent2 === null) {
return;
}
sharedContent.setStandardContent($scope.testContent2);
});
You can now see that the word "test" (I could not find anything to do with 'haha') appears the moment the page loads. Subsequent changes to the input are also updated in the span. Hope this is what you were looking for.
The thing that you are not taking into account that the run phase of the app gets executed before the controllers are initialized. Because broadcasted messages don't get buffered and are only served to the listeners that are listening in the moment the message is created, the haha value gets lost.
In your case, however, it's quite easy to make it work with a small change in your controller:
function testCtrl($scope, sharedContent) {
updateTestContent();
$scope.$on('updateContent', updateTestContent);
function updateTestContent(){
$scope.testContent = sharedContent.getStandardContent();
}
}
I forked your JSFiddle here http://jsfiddle.net/y3w5r01d/2/ where you can see on the console when each function (run and controllers) gets executed.

How to trigger a method when Angular is done adding scope updates to the DOM?

I am looking for a way to execute code when after I add changes to a $scope variable, in this case $scope.results. I need to do this in order to call some legacy code that requires the items to be in the DOM before it can execute.
My real code is triggering an AJAX call, and updating a scope variable in order to update the ui. So I currently my code is executing immediately after I push to the scope, but the legacy code is failing because the dom elements are not available yet.
I could add an ugly delay with setTimeout(), but that doesn't guarantee that the DOM is truly ready.
My question is, is there any ways I can bind to a "rendered" like event?
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}];
$scope.results = [];
$scope.loadResults = function(){
for(var i=0; i < resultsToLoad.length; i++){
$scope.results.push(resultsToLoad[i]);
}
}
function doneAddingToDom(){
// do something awesome like trigger a service call to log
}
}]);
angular.bootstrap(document, ['myApp']);
Link to simulated code: http://jsfiddle.net/acolchado/BhApF/5/
Thanks in Advance!
The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. -- http://docs.angularjs.org/guide/concepts#runtime
Okay, so what's a "stack frame"? A Github comment reveals more:
if you enqueue from a controller then it will be before, but if you enqueue from directive then it will be after. -- https://github.com/angular/angular.js/issues/734#issuecomment-3675158
Above, Misko is discussing when code that is queued for execution by $evalAsync is run, in relation to when the DOM is updated by Angular. I suggest reading the two Github comments before as well, to get the full context.
So if code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders. If you need to run something after the browser renders, or after a controller updates a model, use $timeout(..., 0);
See also https://stackoverflow.com/a/13619324/215945, which also has an example fiddle that uses $evalAsync().
I forked your fiddle.
http://jsfiddle.net/xGCmp/7/
I added a directive called emit-when. It takes two parameters. The event to be emitted and the condition that has to be met for the event to be emitted. This works because when the link function is executed in the directive, we know that the element has been rendered in the DOM. My solution is to emit an event when the last item in the ng-repeat has been rendered.
If we had an all Angular solution, I would not recommend doing this. It is kind of hacky. But, it might be an okey solution for handling the type of legacy code that you mention.
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [
{id: 1, name: "one"},
{id: 2, name: "two"},
{id: 3, name: "three"}
];
function doneAddingToDom() {
console.log(document.getElementById('renderedList').children.length);
}
$scope.results = [];
$scope.loadResults = function(){
$scope.results = resultsToLoad;
// If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console.
doneAddingToDom();
}
// If we run on doneAddingToDom here, we will find 3 list elements in the DOM.
$scope.$on('allRendered', doneAddingToDom);
}]);
myApp.directive("emitWhen", function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var params = scope.$eval(attrs.emitWhen),
event = params.event,
condition = params.condition;
if(condition){
scope.$emit(event);
}
}
}
});
angular.bootstrap(document, ['myApp']);
Using timeout is not the correct way to do this. Use a directive to add/manipulate the DOM. If you do use timeout make sure to use $timeout which is hooked into Angular (for example returns a promise).
If you're like me, you'll notice that in many instances $timeout with a wait of 0 runs well before the DOM is truly stable and completely static. When I want the DOM to be stable, I want it to be stable gosh dang it. And so the solution I've come across is to set a watcher on the element (or as in the example below the entire document), for the "DOMSubtreeModified" event. Once I've waited 500 milliseconds and there have been no DOM changes, I broadcast an event like "domRendered".
IE:
//todo: Inject $rootScope and $window,
//Every call to $window.setTimeout will use this function
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
$rootScope.$broadcast('domRendered')
}, 500)
});
//IE stupidity
} else {
document.attachEvent("DOMSubtreeModified", function (e) {
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
$rootScope.$broadcast('domRendered')
}, 500)
});
}
This event can be hooked into, like all broadcasts, like so:
$rootScope.$on("domRendered", function(){
//do something
})
I had a custom directive and I needed the resulting height() property of the element inside my directive which meant I needed to read it after angular had run the entire $digest and the browser had flowed out the layout.
In the link function of my directive;
This didn't work reliably, not nearly late enough;
scope.$watch(function() {});
This was still not quite late enough;
scope.$evalAsync(function() {});
The following seemed to work (even with 0ms on Chrome) where curiously even ẁindow.setTimeout() with scope.$apply() did not;
$timeout(function() {}, 0);
Flicker was a concern though, so in the end I resorted to using requestAnimationFrame() with fallback to $timeout inside my directive (with appropriate vendor prefixes as appropriate). Simplified, this essentially looks like;
scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) {
$window.requestAnimationFrame(function() {
scope.$apply(function() {
scope.height = element.height(); // OK, this seems to be accurate for the layout
});
});
});
Then of course I can just use a;
scope.$watch("height", function() {
// Adjust view model based on new layout metrics
});
interval works for me,for example:
interval = $interval(function() {
if ($("#target").children().length === 0) {
return;
}
doSomething();
$interval.cancel(interval);
}, 0);

Resources