ng-include causes the controller block to re render - angularjs

I am trying to use ng-switch with ng-include below. The problem is with ng-init and the whole controller block getting re-rendered on any ng-includes change.
In the login_form.html, when a user logins, I set the isLoggedIn = true, in the LoginCtrl. However this causes the re-rendering of the full html below, which causes the ng-init again.
How do I avoid this cycle?
<div ng-controller="LoginCtrl" ng-init="isLoggedIn = false" class="span4 pull-right">
<div ng-switch on="isLoggedIn">
<div ng-switch-when="false" ng-include src="'login_form.html'"></div>
<div ng-switch-when="true" ng-include src="'profile_links.html'"></div>
</div>
</div>
Below is the HTML for the login form -
<form class="form-inline">
<input type="text" placeholder="Email" ng-model="userEmail" class="input-small"/>
<input type="password" placeholder="Password" ng-model="userPassword" class="input-small"/>
<button type="submit" ng-click="login(userEmail, userPassword)" class="btn">Sign In</button>
</form>
Below is the controller -
angularApp.controller('LoginCtrl', function($scope, currentUser){
$scope.loginStatus = function(){
return currentUser.isLoggedIn();
};
/* $scope.$on('login', function(event, args) {
$scope.userName = args.name;
});
$scope.$on('logout', function(event, args) {
$scope.isLoggedIn = false;
});*/
$scope.login = function(email, password){
currentUser.login(email, password);
};
$scope.logout = function(){
currentUser.logout();
};
});
Blow is the service -
angularApp.factory('currentUser', function($rootScope) {
// Service logic
// ...
//
var allUsers = {"rob#gmail.com": {name: "Robert Patterson", role: "Admin", email: "rob#gmail.com", password: "rob"},
"steve#gmail.com":{name: "Steve Sheldon", role: "User", email: "steve#gmail.com", password: "steve"}}
var isUserLoggedIn = false;
// Public API here
return {
login: function(email, password){
var user = allUsers[email];
var storedPass = user.password;
if(storedPass === password){
isUserLoggedIn = true;
return true;
}
else
{
return false;
}
},
logout: function(){
$rootScope.$broadcast('logout');
isUserLoggedIn = false;
},
isLoggedIn: function(){
return isUserLoggedIn;
}
};
});

I believe your problem is a result of the way prototypal inheritance works. ng-include creates its own child scope. Assigning a primitive value in a child scope creates a new property on that scope that shadows/hides the parent property.
I'm guessing that in login_form.html you do something like the following when a user logs in:
<a ng-click="isLoggedIn=true">login</a>
Before isLoggedIn is set to true, this is what your scopes look like:
After isLoggedIn is set to true, this is what your scopes look like:
Hopefully the pictures make it clear why this is causing you problems.
For more information about why prototypal inheritance works this way with primitives, please see What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
As the above link explains, you have three solutions:
define an object in the parent for your model, then reference a property of that object in the child: parentObj.isLoggedIn
use $parent.isLoggedIn in login_form.html -- this will then reference the primitive in the $parent scope, rather than create a new one. E.g.,
<a ng-click="$parent.isLoggedIn=true">login</a>
define a function on the parent scope, and call it from the child -- e.g., setIsLoggedIn(). This will ensure the parent scope property is being set, not a child scope property.
Update: in reviewing your HTML, you may actually have two levels of child scopes, since ng-switch and ng-include each create their own scopes. So, the pictures would need a grandchild scope, but the three solutions are the same... except for #2, where you would need to use $parent.$parent.isLoggedIn -- ugly. So I suggest option 1 or 3.
Update2: #murtaza52 added some code to the question... Remove ng-init="isLoggedIn = false" from your controller (your service is managing the login state via its isUserLoggedIn variable) and switch on loginStatus() in your controller: <div ng-switch on="loginStatus()">.
Here is a working fiddle.

I've have a working example. The trick is that the scope variable to be evaluated has to be an object, not a primitive type. It looks like $scope.$watch() is not watching primitive types properly (which is a bug). The jsFiddle has a parent controller with two child controllers, but it would also work with only one (parent) controller.
HTML:
<div ng-controller="LoginCheckCtrl">
<div ng-switch on="user.login">
<div ng-switch-when="false" ng-include="'login'"></div>
<div ng-switch-when="true" ng-include="'logout'"></div>
</div>
</div>
Controller:
function LoginCheckCtrl($scope) {
$scope.user = {
login: false
};
}
Note: this will also work with only one controller:
function LoginCheckCtrl($scope) {
$scope.user = {
login: false
};
$scope.login = function() {
console.log($scope.user.login ? 'logged in' : 'not logged in');
$scope.user.login = true;
};
$scope.logout = function() {
console.log($scope.user.login ? 'logged in' : 'not logged in');
$scope.user.login = false;
};
}

You could store the state that needs to survive the reinitialization of the controller in a parent scope. I don't think it's strange to put isLoggedIn on the $rootScope even.
Also, you could initialize inside the controller, that would be cleaner in this case (but it doesn't solve your problem).

Related

syncing variable across pages using angularJs

I have a html page with a link as follows:
<div ng-if="!adminCtrl.valid">
<div><a target="_blank" ng-href="https://api.instagram.com/oauth/authorize/?client_id=xxx&redirect_uri=http://localhost:8888/igSuccess.html&response_type=token">Authorize to Instagram</a><br/></div>
</div>
This goes to redirect page on success where the code is
<div ng-controller="AdminController">
<h2>You can close this tab/window</h2>
</div>
The control is same for both pages as follows:
app.controller('AdminController', ['$scope','$routeParams','$location', function($scope,$routeParams,$location){
var actrl = this;
actrl.valid = false;
var token = $location.absUrl();
if(token.indexOf('access_token') > -1){
console.log('found token so will do special');
actrl.valid = true;
$scope.$apply();
}
}}
I am expecting the link to disappear once the new page opens as i am updating the valid variable value.
i know the flaw seems to be the cross page communication. so how to deal with it?
Controllers are 'flushed' when you change views. To keep data from a view/controller to another, store your data within a Service.
UPDATE
controller:
app.controller('AdminController', [
'$scope', '$routeParams', '$location', 'ExampleService', function ($scope, $routeParams, $location, ExampleService) {
var actrl = this;
// Watches the service's value for changes and applies it to the controller
$scope.$watch(function(){return ExampleService.valid}, function(newValidValue){
actrl.valid = ExampleService.valid;
});
var token = $location.absUrl();
if (token.indexOf('access_token') > -1) {
console.log('found token so will do special');
ExampleService.valid = true;
// No need for this
// $scope.$apply();
}
}
}
Service:
app.service('ExampleService', [
function () {
//All properties here are kept through-out your app's life time
this.valid = false; // Init to false
}
}
To share data between Controllers in Angular JS, use a named Service to encapsulate the data. In your case, I would typically define an Auth service that provides a few methods for getting and setting the access_token for a user:
module.factory('Auth', function(){
return {
isValid: function(){ /* Check that a User is authenticated... */ },
setToken: function(token){ /* Store the token somewhere... */ },
getToken: function(){ /* Fetch the token from somewhere... */ }
};
});
To share data across "pages" -- tabs or windows in your browser -- even in a Single Page Application (SPA) like this, store the data in cookies or localStorage. You can use angular-local-storage by grevory (GitHub) to abstract the details of using localStorage with a cookie fall-back in non-compatible browsers.
The reason that one page cannot see the valid value defined in the other is because each page gets a separate instance of AdminController, each of which get their own separate instance of $scope tied to their respective DOM elements. Setting valid on the $scope of the redirect landing page has not effect on the completely detached $scope instance in the originating page.
You'd encounter similar difficulties with a trivial same-page example (CodePen):
angular.module('scope-example', [])
.controller('ExampleCtrl', function($scope) {
$scope.value = 'Initial Value';
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<form class="pure-form" ng-app="scope-example">
<fieldset ng-controller="ExampleCtrl">
First instance of <code>ExampleCtrl</code>:
<br>
<input ng-model="value">
<label>{{value}}</label>
</fieldset>
<fieldset ng-controller="ExampleCtrl">
Second instance of <code>ExampleCtrl</code>:
<br>
<input ng-model="value">
<label>{{value}}</label>
</fieldset>
<fieldset ng-controller="ExampleCtrl">
Third instance of <code>ExampleCtrl</code>:
<br>
<input ng-model="value">
<label>{{value}}</label>
</fieldset>
</form>
Even though each of the <fieldset> elements have identical ng-controller directives associated, each gets its own instance of ExampleCtrl and $scope, so the value property isn't shared between them. This holds true for any directive.

Is this the correct way of binding factory to controller?

I have the following code in my module:
.controller('ModalInstanceCtrl', function($rootScope, $scope, emailService) {
$scope.emailService = emailService; // Good or not; if not, why?
$scope.showed = false;
$rootScope.$watch('showed', function () { $scope.showed = $rootScope.showed; }); // In case you wonder why I did this - I'm using this trick to prevent watch from firing twice, because that would happen if I remove the watch below and put its code here.
$scope.$watch('showed', function () {
if (!$rootScope.showed) return;
$scope.selected = 0;
$scope.primary = true;
$scope.verified = true;
if (emailService.emails.length == 0) emailService.load();
});
$scope.EmailSelected = function () {
emailService.setCurrent($scope.selected);
$scope.primary = emailService.emails[$scope.selected].primary;
$scope.verified = emailService.emails[$scope.selected].verified;
};
});
.factory('emailService', function($resource, $http) {
var emails = []; // [{email: 'sample#email.dom', verified: true, primary: false}, ...]
var selected = 0;
function sendreq(action, email){
$http({
method: 'POST',
url: '/email/',
data: "action_" + action + "=&email=" + email,
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}).then(function(response) {
console.log(response.data);
return true;
}, function(data){
return data;
});
}
return {
emails: emails,
selected: selected,
setCurrent: function(curr){
selected = curr;
},
load: function(){
$resource('/api/email/?format=json').query({},
function success(result) {
emails.push.apply(emails, result);
});
},
add: function(email) {
for (var e in emails) if (emails[e].email == email) return false;
return sendreq('add', email);
},
remove: function() {
sendreq('remove', emails[selected].email);
}
}
})
And this code in my HTML template:
<div ng-repeat="e in emailService.emails">
<input type="radio" ng-model="$parent.selected" ng-value="$index" ng-change="EmailSelected()" id="email_{{ $index }}" name="email">
<label for="email_{{ $index }}" ng-bind='e.email'></label> <span ng-show="e.verified">Verified</span> <span ng-show="e.primary">Primary</span>
</div>
<div><button ng-disabled="primary" ng-click="emailService.remove()">Remove</button></div>
<form novalidate>
<input class="form-control" type="email" name="email" ng-model="email" placeholder="Email">
<input type="submit" ng-disabled="email === undefined" ng-click="emailService.add(email)" value="Add Email Address">
</form>
And I want to ask, whether I've correctly assembled the module and template because I'm working with AngularJS for the first time. Specifically, I want to ask if it's correct to bind the whole factory to the scope? Also if someone has more time he can look at the other code to see if everything is right or not. Feel free to write any suggestions about my code.
Thanks in advance!
It always depends on particular case.
This way boilerplate wrapper methods
$scope.add = (...args) => emailService.add(...args);
can be omitted, as well as their tests in controller spec.
Another benefit is that it provides existing object for proper data binding and scope inheritance of scalar scope properties:
<parent-scope>
<p ng-init="emailService.selected = 0"></p>
<child-scope>
<p ng-init="emailService.selected = 1"></p>
{{ emailService.selected === $parent.emailService.selected }}
</child-scope>
</parent-scope>
This certainly would not work as expected if there's no emailService object. This is particularly useful when controllerAs syntax isn't used.
There's nothing wrong with exposing a service to scope - if its API matches the controller scope. And this may indicate an antipattern if it doesn't - or if there are too many services that are abused like that.
Why do you want to bind the entire service ? I do not see a need to that in your code. You are invoking parts of the service using the service handler, there is no specific need to put the entire service on scope.

Pass toggling Boolean from service to controller

I want to show/hide an element based on the Boolean value that is changing in my service. I want the change of this Boolean to happen in my service so multiple controllers can access the true or false value, but I am having trouble returning this value to one or more controllers. Currently I'm only able to pass one value which is false, although the value does show it's changing in my service. Here is an example of my controller...
angular.module('myApp')
.service('ThisService', function(){
function toggleDisplay(){
return displayElement = !displayElement;
}
});
.controller('ThisCtrl', function (thisService, $scope) {
function init(){
$scope.displayElement = ThisService.toggleDisplay();
}
$scope.toggleElement = function(){
$scope.displayElement = ThisService.toggleDisplay();
}
init();
});
My HTML...
<div ng-show="displayElement">Show hide me</div>
<button ng-click='toggleElement()'></button>
Can you please tell me how to return the true/false value to my controller correctly?
You can use a value and then toggle that in your service. However, your service definition is not valid, you have a semi-colon in the middle of your chain of modules and you define your service with the name "ThisService", but then you try to reference it in your controller as "thisService" (it's case sensitive).
JS:
angular.module("myApp", [])
.value("DisplayElement", { value: true })
.service("ThisService", function(DisplayElement) {
this.toggleDisplay = function() {
return DisplayElement.value = !DisplayElement.value;
}
})
.controller("ThisCtrl", function(ThisService, $scope) {
function init() {
$scope.displayElement = ThisService.toggleDisplay();
}
$scope.toggleElement = function() {
$scope.displayElement = ThisService.toggleDisplay();
}
init();
});
HTML:
<div ng-app="myApp">
<div ng-controller="ThisCtrl">
<div ng-show="displayElement">Show hide me</div>
<button ng-click="toggleElement()">Toggle Display</button>
</div>
</div>
jsFiddle
You could even eliminate the service and just access the value directly in your controller (you'd have to inject it first).

angularjs: scope value doesn't get updated in view

there are buttons in detail.html file:
<div ng-controller="test.views.detail">
<div data-ng-repeat="item in details" scroll>
<button ng-click="showDetails(item)">The details</button>
in detail.js file
angular.module('test')
.controller('test.views.detail', function($scope) {
$scope.detailsClicked = false;
$scope.showDetails = function(item){
$scope.detailsClicked = true;
}....
in formDetail.html code:
<div ng-controller="test.views.detail">
{{detailsClicked}}
<div ng-if="detailsClicked">...
Initially it shows false for detailsClicked, when I click on button it goes to showDetails function but value of $scope.detailsClicked never get updated! It is straight forward not sure why it doesn't work:(
This is because you're using the same controller at two places and expecting the scope object to be the same which it is not. Everytime you call ng-controller in your markup a new scope object will be created. If you want them to be based off the same data then use a service.
Here is an example
app.controller('test.views.detail', function($scope, detailsClicked) {
$scope.detailsClicked = detailsClicked;
$scope.showDetails = function(item){
$scope.detailsClicked.isClicked = true;
}
});
Create a factory/service which will retain the data, make sure the data is a
app.factory('detailsClicked', function(){
var data = {
isClicked: false
}
return data;
});

Angular model doesn't scope to variable within child DOM element?

I have this markup:
<div data-ng-model="currentUser.attributes">
<div>{{username}}</div>
</div>
And this is a stripped down version of my controller:
$scope.username = "Alice";
$scope.currentUser = {
attributes: {
username: "Bob"
}
};
I want Bob to display, but instead, I am getting Alice. It works just fine if I use this:
{{currentUser.attributes.username}}
But I don't want to have to scope down to this variable's properties every time I want to access something. How can I get the element to exist within the scope of currentUser.attributes?
While I don't think you should really do this, it is what you're asking for. You can essentially mimic with by using ng-repeat on an array that you populate with the relevant object. For example:
<div ng-repeat="user in [currentUser.attributes]">
{{ user.username }}
</div>
Working plunker: http://plnkr.co/edit/svwYEeWMQXjuAnLkr9Vz?p=preview
Other possible solutions would be to have a service or controller that has functions to get the attributes and return them, cleaning up the syntax of your HTML and making it easier to change backend stuff without breaking your frontend. Your choice.
Edit: I noticed you actually expect to be able to do {{ username }} and get the relevant info, if that's really what you want then I suggest my second proposal. Create functions that return the relevant info.
<div>
{{ getCurrentUserName() }}
</div>
$scope.getCurrentUserName = function() {
return $scope.currentUser.attributes.username;
};
Your call, take it or leave it.
If you want Bob just do the the following in your HTML.
<div>{{current user}}</div>//IGNORE THIS
<div>{{currentUser.attributes.username}}</div>//UPDATED CORRECTED
UPDATED based on clarification.
So in Knockout you do this
<p data-bind="with: currentUser.attributes">
<div data-bind="text: userName></div>
<div data-bind="text: login></div>
<div data-bind="text: bhalBlah></div>
<div data-bind="text: yaddaYadda></div>
</p>
<script type="text/javascript">
ko.applyBindings({
currentUser: {
attributes: {
userName : 'Bob',
login : 't#e',
blahBlah : 'ttttt',
yaddaYadda: 'x'
}
}
});
</script>
Same thing in AngularJS would be
<p ng-controller="myCtrl">
<div>{{currentUser.attributes.userName}}</div>
<div>{{currentUser.attributes.login}}</div>
<div>{{currentUser.attributes.blahBlah}}</div>
<div>{{currentUser.attributes.yaddaYadda}}</div>
</p>
<script type="text/javascript">
angular.module('myApp',[]).controller('myCtrl',function($scope){
$scope = {
currentUser: {
attributes: {
userName : 'Bob',
login : 't#e',
blahBlah : 'ttttt',
yaddaYadda: 'x'
}
};
});
</script>
In this the question is how to avoid how not to repeat the part the full property paths between ** as shown below in angular.
**currentUser.attributes.**userName
**currentUser.attributes.**login
**currentUser.attributes.**blahBlah
**currentUser.attributes.**yaddaYadda
Here is one way see plnkr using ng-init which reduces 'currentUser.attributes' to just 'attr'.
With just attr.<properties> repeated
{{attr.userName}}
{{attr.login}}
{{attr.blahBlah}}
{{attr.yaddaYadda}}
Another way is you restructure your object and flatten it on the $scope.
This is not recommended because now you are putting primitives on to the $scope and are widening the scope with $scope.userName = currentUser.attributes.username. Also your 'repetitive' code is still there just in the Javascript.
In lieu of ng-init
ng-init="attr = currentUser.attributes"
You could also do this in controller
$scope.attr = currentUser.attributes;
This post really got me thinking. I had a theory on how to accomplish this using a directive.
Came up with a proof of concept on plnkr: http://embed.plnkr.co/OJDhpJ1maEdSoPvlbiRA/
If I understand correctly, you want to only display the properties within a given block of your struct.
Given the following struct:
$scope.currentUser = {
attributes: {
username: 'Batman',
age: '99',
address: {
street: 'Bat Cave'
}
}
};
You want to scope things down with something like:
<div scope-with="currentUser.attributes">
Username: {{username}}<br />
Age: {{age}}
<div scope-with="address">
Street: {{street}}
</div>
</div>
Directive:
angular.module('mymodule', [])
.directive('scopeWith', function($interpolate){
return {
restrict: 'A',
scope: {
scopeWith: '='
},
transclude: 'element',
compile: function(tElement, tAttrs, linker) {
return function( scope, element, attr) {
var childScope,
parent = element.parent(),
withBlock = null
;
scope.$watch('scopeWith', function(val){
childScope = scope.$new();
angular.forEach(val, function(val, prop){
childScope[prop] = val;
});
if(withBlock) {
withBlock.el.remove();
withBlock.scope.$destroy();
}
linker(childScope, function(clone){
withBlock = {};
parent.append(clone);
withBlock.el = clone;
withBlock.scope = childScope;
});
}, true);
};
}
};
Use {{currentUser.username}} to show Bob.
The ng-model on the div is irrelevant as it only applies to input elements.

Resources