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

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.

Related

Passing keys of object to directive

I have created a directive as a wrapper for md-autocomplete so that it's easier to re-use. In the parent controller, I have an object. I want to pass the keys of the object to my custom directive, but I'm having trouble. Simplified code, without md-autocomplete:
Here's the script
var app = angular.module('myApp',[])
.controller('parentController', function(){
var parent = this;
parent.things = {item1: {color: "blue"}, item2: {color: "red"}};
})
.directive('childDirective',function(){
return {
scope: {},
bindToController: {
items:'&'
},
controller: childController,
controllerAs: 'child',
template: '<pre>{{child.items | JSON}}<pre>' //should be [item1,item1]
}
function childController(){
//Just a dummy controller for now
}
})
HTML
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="Object.keys(parent.things)">
</my-directive>
</div>
TL;DR: How do I pass the keys of an object defined in the parent controller to a child directive? I need to pass just the keys, not the object itself, because my directive is designed to deal with an array of strings.
Try using a directive with local scope from user attribute (=)
app.directive('childDirective', function() {
return {
replace: true,
restrict: 'E',
scope: {
items: '='
},
template: '<pre>{{items | JSON}}<pre>'
};
});
Using the directive, object in attribute "items" is passed "as is" , as a scope variable "items"
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="getKeys(parent.things)">
</my-directive>
</div>
Using Object.keys(obj) as source will cause an infinite loop digest (the function is always returning a new different object). You need a function to save the result to a local updatable object, like in this example:
https://jsfiddle.net/FranIg/3ut4h5qm/3/
$scope.getKeys=function(obj){
//initialize result
this.result?this.result.length=0:this.result=[];
//fill result
var result=this.result;
Object.keys(obj).forEach(function(item){
result.push(item);
})
return result;
}
I'm marking #Igor's answer as correct, because ultimately it led me to the right place. However, I wanted to provide my final solution, which is too big for a comment.
The search for the answer to this question led me to create a directive that is more flexible, and can take several different types of input.
The real key (and my actual answer to the original question) was to bind the items parameter to a proxy getter/setter object in the directive. The basic setup is:
app.directive('myDirective',function(){
return {
...
controller: localControl,
bindToController: {
items: '<' //note one-way binding
}
...
}
function localControl(){
var child = this;
child._items = [],
Object.defineProperties(child,{
items: {
get: function(){return child._items},
set: function(x){child._items = Object.keys(x)}
}
});
}
});
HTML
<my-directive items="parent.items">
<!-- where parent.items is {item1:{}, item2:{}...} -->
</my-directive>
Ultimately, I decided I wanted my directive to be able to accept a variety of formats, and came up with this plunk as a demonstration.
Please feel free to offer comments/suggestions on improving my code. Thanks!

Angular - Bind directive value to controller object

I'm trying to pass an array from a controller to a directive and for some (probably obvious to you lot!) reason when the array values are updated in the controller it does not reflect in the directive. The controller obtains data from a service into an array and I want to pass that array to the directive to create a bar graph. I've put the key parts of the code below.
Here is my top level HTML
<div dash-progress
graph-data="{{dashCtrl.myProgress}}">
</div>
<div>
Other Stuff
</div>
My template HTML for the directive:
<div class="boxcontent" ng-show="dashCtrl.showProgress">
<div class="chart-holder-lg">
<canvas tc-chartjs-bar
chart-data="progress"
chart-options="options"
height="200"
auto-legend>
</canvas>
</div>
</div>
Controller:
angular
.module('myApp')
.controller('dashCtrl',['mySvc',
function(mySvc) {
var self = this;
this.myProgress = [];
this.getProgress = function() {
//logic must be in the service !
mySvc.getProgress().then(function(success) {
self.myProgress = mySvc.progress;
});
};
}]);
and the directive:
angular
.module('myApp')
.directive('dashProgress', [function() {
return {
restrict: 'AE',
templateUrl: 'components/dashboard/progress.html',
scope: {
graphData: '#'
},
link: function(scope,el,attrs) {
scope.progress = {
labels: ['Duration','Percent'],
datasets: [
{
label: 'Duration',
data: [scope.graphData.duration]
},
{
label: 'Percent',
data: [scope.graphData.percent]
}
]
};
scope.options = { };
}
}
}]);
If I set an initial values of the myProgress object in the controller then these do get reflected in the directive, but I don't get the real values that I need when they are returned to the controller from the service.
In your directive's scope, instead of this:
scope: {
graphData: '#'
}
try using this:
scope: {
graphData: '='
}
Don't use {{ }} when passing array to the directive with =. It will render the array in the view instead of passing a reference to directive's scope.
As far as I know, # is not only one-way binding, but also one-time binding and should be used mostly for string values (e.g. setting an html attribute while initializing directive). If you'd like to use #, you should firstly convert data to JSON, then pass it to directive with {{ }}, then parse it again in directive and after any change - manually recompile the directive. But it would be a little overkill, wouldn't it?
Conclusion
Just remove the curly brackets from the view and use = to bind value to directive's scope.
View
<div dash-progress
graph-data="dashCtrl.myProgress">
</div>
Directive
scope: {
graphData: '='
},
Update
Try one more thing. In dashCtrl, wrap myProgress with an object (you can change names to be more self-explaining - this is just an example):
this.graphData = {
myProgress: []
}
this.getProgress = function() {
mySvc.getProgress().then(function(success) {
self.graphData.myProgress = mySvc.progress;
});
}
Then, pass graphData to directive:
<div dash-progress
graph-data="dashCtrl.graphData">
</div>
Finally, substitute every scope.graphData with scope.graphData.myProgress. This way you make sure that scope.graphData.myProgress always refers to the same data because it's a property of an object.
If this still doesn't work, you will probably have to use a watcher and update properties of scope.progress manually.

Use this instead $scope on controller with provider

I´m learning about Providers. On a common controller I would use
modu.controller("thecontrol", [function()
{
this.something = "Hello";
]});
and on the HTML
<div ng-controller="thecontrol as ctrl">
{{ ctrl.something }}
</div>
But... I´m trying to the the same with this code and, I really could not, even when I tried all the ways I "know".
Here´s the code...
What I want? Use THIS instead of $scope
<div ng-app="myApp" ng-controller="showingName">
{{theNameIs}}
</div>
<script src="angular.js"></script>
<script>
var myApp = angular.module("myApp", []);
myApp.provider("showingName", function() {
this.name = "Luis";
this.$get = function() {
var name = this.name;
return {
showName: function() {
return name;
}
}
};
this.setName = function(name) {
this.name = name;
};
});
myApp.config(function(showingNameProvider){
showingNameProvider.setName("Juan");
});
myApp.controller("showingName", function($scope, showingName)
{
$scope.theNameIs = showingName.showName();
});
</script>
And yes... It works, but I would like to know if it´s possible to do it with THIS.
Thanks!
I think it's because when you don't name the controller, then the {{ }} has to be scope, since this and $scope can be different depending on the context. Say for instance in an ng-repeat, 1 controller yet essentially 2 scopes.
Name the controller like you did on the first, ctrl as showingName. Make the variable this.theNameIs and then use {{ ctrl.theNameIs }}
Also, personally I don't think you should name the controller and provider the same name, appreciate this is probably just an example.
More information on $scope and this:
'this' vs $scope in AngularJS controllers

editable with ngrepeat: automatically editing the latest added item

I need to add new items to a collection, that gets rendered with ngrepeat and using xeditable make it automatically editable.
BTW, I'm using the "manual trigger" method for xeditable.
Here it is the HTML
<h4>Angular-xeditable demo</h4>
<div ng-app="app" ng-controller="Ctrl" style="margin: 50px">
<div class="btn btn-default" ng-click="addNew()">+</div>
<ul>
<li ng-repeat="item in array | orderBy:'-value'">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
</div>
and here the controller:
var app = angular.module("app", ["xeditable"]);
app.run(function(editableOptions) {
editableOptions.theme = 'bs3';
});
app.controller('Ctrl', function($scope, $filter) {
$scope.array = [
{value: 1, field: 'status1'},
{value: 2, field: 'status2'},
{value: 3, field: 'status3'},
{value: 4, field: 'status4'}
];
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
//MAKE IT EDITABLE????????
}
});
Take a look to the issue in this fiddle: http://jsfiddle.net/dpamio/hD5Kh/1/
Here is a updated fiddle that works. Because of how the directive was written, and how ng-repeat works, it required an extremely hacky solution...
app.controller('Ctrl', function($scope, $filter, $timeout) {
$scope.itemForms = {};
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
// Use timeout to force evaluation after the element has rendered
// ensuring that our assignment expression has run
$timeout(function() {
$scope.itemForms[0].$show(); // last index since we sort descending, so the 0 index is always the newest
})
}
Background on how ng-repeat works: ng-repeat will create a new child scope for each element that is repeated. The directive assigns a variable on that scope using the string passed into e-form for its name (in this case itemForm). If it was smarter, it'd allow for expression evaluation for assignment. (Then we could assign it to the parent scope, and access it in the controller, but that's a different matter).
Since we don't have any way to access this child scope outside of the directive, we do something very bad. We use the mustache expression in a span of display none to assign the itemForm variable to the parent scope so that we can use it later. Then inside our controller we use the look up value to call the itemForm.$show() method that we expect.
Abstracting that bit of nastyness into an angular directive, we could write the following:
.directive('assignFromChild', function($parse) {
return {
restrict: 'A',
link: function(scope, el, attrs) {
scope.$watch(function() { return $parse(attrs.assignFromChild)(scope); }, function(val) {
$parse('$parent.' + attrs.toParent).assign(scope, val);
})
}
};
});
Allowing our HTML to go back down to:
<ul>
<li ng-repeat="item in array | orderBy:'-value'" assign-from-child="itemForm" to-parent="itemForms[{{$index}}]">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
Here is a fiddle with my final solution
I found a very simple solution using ng-init="itemForm.$show()", that will activate the xeditable form when the new item is inserted.
Here's the updated jsFiddle answering the question: http://jsfiddle.net/hD5Kh/15/

ng-include causes the controller block to re render

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).

Resources