I'm fairly new to angular and directives but while I was making a custom directive for my app I realized I was using variables and scope interchangeably with no issue.
For example I have scope.onBreak = false and var completedSessions = 0
My question is, when should I use scope and when should I use variables inside a directives and will this end up causing issues in the future if I don't use them accordingly .
scope.onBreak = false;
scope.onLongBreak = false;
// starts countdown from current work/break time
scope.timerText = "Work Timer";
var completedSessions = 0;
var timeSet;
var setBreak = function() {
$interval.cancel(timeSet);
scope.workTime = MY_TIMES.break;
scope.buttonText = "START";
scope.onBreak = true;
scope.timerText = "Break Timer";
};
scope.countdown = function() {
if (scope.workTime <= 0) {
//if countdown reaches 0 and is on break , set time to 25m (work)
if (scope.onBreak) {
console.log("currently working");
setWork();
} else {
setBreak();
}
}
} else {
//countdown
scope.workTime--;
}
};
I think a good way of looking at it is to say that 'scope' is the malleable (and manipulatable) link between controller and view (template). Variables are internal to the class you're working on be it controller, directive, service etc., and ONLY seen by those classes (a template file cannot access a declared variable called 'foo' on the controller). There are probably more eloquent explanations, but since no-one had posted anything, thought I would.
You should use variables whenever you don't need to pass the data to the view, as it's faster.
If you need to use that data in a view, then use scope.
scope variables bind to the DOM view. But var can't bind, And functionality wise only we can use within JS.
Related
Edit: the code below was made up on the spot to show how I was going about what I was doing. It definietely won't run, it is missing a lot of things.
Here is a working example in codepen: https://codepen.io/goducks/pen/XvgpYW
much shorter example: https://codepen.io/goducks/pen/ymXMyB
When creating a function that is using call or apply, the this value stays null when using getPerson. however, when I use apply or call with getPerson it returns the correct person.
Please critique, I am really starting to learn more and more. I am in the middle of a project section so it might be hard to change all the code, but my next project could implement this better.
call and apply are setting to the window and not the object.
I will provide code that is much simpler with the same concept of what I am talking about.
function createPerson(){
this.manager = null;
this.teamManager = null;
this.setTeamManager = function(val){
this.teamManager = val;
}
this.setManager = function(val){
console.log('setting manager to',val);
this.teamManager = val;
}
this.getTeamManager = function(){
console.log('setting team manager to',val);
return this.teamManager ;
}
this.getManager = function(){
return this.manager;
}
this.appendSelect = function(elem){
var that = this;
createOtherSelects(that,elem);
}
//some functions that create selects with managers etc
//now assume there are other selects that will filter down the teams,
//so we might have a function that creates on change events
function createOtherSelects(that){
//code that creates locations, depending on location chosen will
//filter the managers
$('#location').on('change',function(){
//do some stuff
//... then call create management
createManagement(that,elem);
});
}
function createManagement(that,elem){
var currentLocation = that.location; //works
var area = that.area;//works ... assume these are set above
//code that returns a filter and unique set of managers back
that.teamManager = [...new Set(
data.map(person=>{
if(person.area==area &&
person.currentLocation==currentLocation
){
return person;
}
})
)].filter(d=>{if(d){return d}});
if(elem.length>0){
var selectNames = ['selectManager','selectTeamManager'];
var fcns = [that.setManager,that.setTeamManager];
for(var i = 0; i < selectNames.length;i++){
//do stuff
if(certainCriteriaMet){
// filter items
if(filteredManager == 1){
fcns[i].call(null,currentManager);//
}
}
}
}
}
}
var xx = new createPerson()
In console I see setting manager and setting team manager to with the correct values.
however when I call xx in console, I see everything else set except for
xx.teamManager and xx.manager
instead it is applying to the window, so if I type teamManager in the console, it will return with the correct person.
If I straight up say
that.setManager('Steve')
or even it works just fine.
xx.setManager('steve')
the this value in setManager is somehow changing from the current instance of the object to this window. I don't know why, and I would like to learn how to use apply and call using that for future reference.
I think the issue is with your following code
fcns[i].call(null,currentManager)
If you are not supplying "this" to call, it will be replaced with global object in non-strict mode.
fcns[i].call(that,currentManager)
See mdn article here
From your codepen example, you need to change that line
fcnset[0].apply(that,[randomName]);
The first argument of the apply method is the context, if you are not giving it the context of your method it's using the global context be default. That's why you end up mutating the window object, and not the one you want !
I have two functions, one working, the other not.
They are equal, except that the one is looping through a variable, in which the global object of the scope is saved (hope this makes sense), and the other tries to loop through the text directly, but fails, because it throws the error:
Uncaught TypeError: Cannot read property '0' of undefined
Here is the fiddle:
http://jsfiddle.net/4p1p4wjy/2/
In my understanding, the 2nd version of the function is not working, because it somehow can't access the this.splittedText, from within the callback of the function.
First Working Function:
loopThroughSplittedText: function() {
// delete this
var locationInString = 0;
var splittedText = this.splittedText;
function delayedOutput() {
document.getElementById('output').innerHTML = splittedText[locationInString];
locationInString++;
if(locationInString < splittedText.length) {
setTimeout(delayedOutput, 200);
}
}
delayedOutput();
},
Second Not Working Function:
loopThroughSplittedTextNotWorking: function() {
// delete this
var locationInString = 0;
function delayedOutput() {
document.getElementById('output').innerHTML = this.splittedText[locationInString];
locationInString++;
if(locationInString < this.splittedText.length) {
setTimeout(delayedOutput, 200);
}
}
delayedOutput();
}
How do I make the 2nd function work, without saving the object inside a local variable first? I'd like to use the two-way databinding as best as possible.
How do I make the 2nd function work, without saving the object inside a local variable first?
You can't. this is a variable that is always local to the function it is used in, and its value depends on how the function is called. If you want to use its value in a different function, then you need to copy it into another variable.
The bind method provides a shorthand for doing that.
setTimeout(delayedOutput.bind(this), 200);
Simple answer, you don't.
Because your function is called through timeout, it's not in the same context anymore and 'this' will not refer to the same object anymore.
You can do this:
loopThroughSplittedTextNotWorking: function() {
// delete this
var locationInString = 0;
var that = this;
function delayedOutput() {
document.getElementById('output').innerHTML = that.splittedText[locationInString];
locationInString++;
if(locationInString < that.splittedText.length) {
setTimeout(delayedOutput, 200);
}
}
delayedOutput();
}
By saving the "this" variable into a local variable, you can access it in your "delayedOutput" function.
I realize it's basically just like your working example, just phrased a little different, but that's usually how I do it.
Suppose I am working with a directive that is given a date in form of a unix timestamp via two-way binding, but also offers a calendar widget to change the selection.
The calendar widget works with a date object, and I am unable to change the input data format and I do not want to rework the calendar to support unix timestamp. Also this is just an example and the question is about general way of working with circular watchers.
The scope would look like this:
scope.selectedUnixTimestamp; // this comes from the outside
scope.selectedDate;
scope.$watch('selectedUnixTimestamp', function(newV, oldV) {
$scope.selectedDate = new Date(newV*1000);
});
scope.$watch('selectedDate', function(newV, oldV) {
$scope.selectedUnixTimestamp = Math.floor(newV.getTime()/1000 + 0.000001);
});
My question is: what do I do in order to avoid extra calls to $watch callbacks? Obviously if I choose a new date, the flow will be following:
Watcher #2 is called - it modifies selectedUnixTimestamp
Watcher #1 is called - it modifies selectedDate
Watcher #2 is called again (new object reference) - it modifies selectedUnixTimestamp
But I don't want any of those calls besides the first one. How do can I achieve it?
Obviously one way would be to do something like:
scope.selectedUnixTimestamp;
scope.selectedDate;
var surpressWatch1 = false;
var surpressWatch2 = false;
scope.$watch('selectedUnixTimestamp', function(newV, oldV) {
if(surpressWatch1) { surpressWatch1 = false; return; }
$scope.selectedDate = new Date(newV*1000);
surpressWatch2 = true;
});
scope.$watch('selectedDate', function(newV, oldV) {
if(surpressWatch2) { surpressWatch2 = false; return; }
$scope.selectedUnixTimestamp = Math.floor(newV.getTime()/1000 + 0.000001);
surpressWatch1 = true;
});
But it quickly becomes a hell to maintain a code like that.
Another way would be to do something like:
scope.selectedUnixTimestamp;
scope.selectedDate;
scope.$watch('selectedUnixTimestamp', function(newV, oldV) {
if(newV*1000 === scope.selectedDate.getTime()) { return; }
$scope.selectedDate = new Date(newV*1000);
});
scope.$watch('selectedDate', function(newV, oldV) {
if(scope.selectedUnixTimestamp*1000 === newV.getTime()) { return; }
$scope.selectedUnixTimestamp = Math.floor(newV.getTime()/1000 + 0.000001);
});
But it might be very costful if the data transformation is more complicated than * 1000
Another way would be to watch on primitive value instead of a date object:
scope.$watch('selectedDate.getTime()', function(newV, oldV) {
But this only works with this particular example and does not solve the general issue
How to work with circular watches? I guess answer is, try not to do it.
You can try this, although I am sure there are better solutions to your example.
Use only one watch function:
You can use a function as first parameter to the watch. This function will be called until the value it returns settles (is the same as last time). You can hence create a $watch like this:
$scope.$watch(function() {
return {
timestamp: scope.selectedUnixTimestamp,
date: scope.selectedDate
}
}, function(newVal, oldVal) {
// Note that newVal and oldVal here is on the form of the object you return in the watch function, and hence have properties: timestamp and date.
// You can compare newVal.date to oldVal.date (same with timestamp) to see which one has actually changed if you need to do that.
}
true); // You need a deep watch (the true param) to watch the properties on the object
The Angular framework is built on the following assumption:
The true and trustable value of something, ready to be synchronized with a REST service for example, exists once in the model.
Keeping this in mind, you never write circular watchers.
And in case you have two different ways to alter a model value, you would write directives requiring ngModelController instance and providing the right formatter and parser functions.
I am doing performance testing in Angular and I want to know exactly how many watches are there in my page. Turns out there is no easy way to do this. Has anyone tried it yet?
Any help will be highly appreciated!
I had the same question. I created a function that will do it:
// get the watch count
// scopeHash is an optional parameter, but if you provide one, this function will modify it for later use (possibly debugging)
function getWatchCount (scope, scopeHash) {
// default for scopeHash
if (scopeHash === undefined) {
scopeHash = {};
}
// make sure scope is defined and we haven't already processed this scope
if (!scope || scopeHash[scope.$id] !== undefined) {
return 0;
}
var watchCount = 0;
if (scope.$$watchers) {
watchCount = scope.$$watchers.length;
}
scopeHash[scope.$id] = watchCount;
// get the counts of children and sibling scopes
// we only need childHead and nextSibling (not childTail or prevSibling)
watchCount+= getWatchCount(scope.$$childHead, scopeHash);
watchCount+= getWatchCount(scope.$$nextSibling, scopeHash);
return watchCount;
}
It will calculate the number of watchers on any scope. It may be most useful to calculate on the root scope, but you can use it at any scope level (possibly to check watches on a component). Here is an example in action: http://jsfiddle.net/S7APg/
I have trouble to solve a scope issue. Actually I'm working on a project for an HMI browser frontend. It's should visualise variables from an automation system. For the HMI it's required that the user can switch between different pages. To solve the general process flow I have created a state machine function, which coordinates loading, drawing and interaction with user. My problem now is that I use setTimeout to call the run function (which is actually my state machine) and now run in trouble with var-scope.
Look at following code:
function frontend() {
// Public properties:
this.soundEnable = true;
// Private Properties:
var p1 = 0;
var p2 = [1,2,3];
var p3 = {a:1, b:2, c:3};
var runState = 1;
var runWait = false:
// Public Methods
// stops the state machine until m_continue is called
this.m_wait = function() {
runWait = true;
}
// continues the state machine
this.m_continue = function() {
if (runWait) {
runWait = false;
setTimeout(run, 100);
}
}
// Private Methods
function drawFrame(finish_callback) {
...<Drawing of HMI-Objects on the canvas>...
finish_callback();
}
function run() {
switch (runState) {
case 1:
this.m_stop();
drawFrame(this.m_continue());
case 2:
for(i=0; i<p3.length; i++) {
p2.push(externalObjectCreator(p3[i]));
}
}
if (!runWait) {
runState++;
setTimeout(run, 100);
}
}
// Constructor
...<code to assign public and private properties>...
// Finally call the state machine to activate the frontend
runState = 1;
run();
}
Problem is scope in run-Function. In case of the first call from end of constructor everything is ok. run can access all the private properties and manipulate them. But when it is called later on via setTimeout from m_continue or by itself I can't access the private properties. In firebug I can just see the public properties and functions and none of the private properties I need.
Using of global variables will help, but is not possible, because on multi monitor solution I have 2 separated canvas objects which need to show a separated version of the HMI - for that case I need 2 instances of frontend running parallel in one browser window.
Does anyone know a solution for that problem? I'm on the end of my knowledge and totally confused.
The easiest way will be to define your scope like. Any many renound javascript libraries also use this technique.
this.m_continue = function() {
that = this;
if (runWait) {
runWait = false;
setTimeout(that.run, 100);
}
}
Otherwise you may also use scope binding using apply
You should bind the run function in each setTimeout, since run uses this.
setTimeout(run.bind(this), 100);