I might be missing something conceptually but I understand that ng-repeat creates child scopes but for my scenario this is undesirable. Here is the scenario. I have a 3way bind to a firebase dataset. The object is an object with n sub objects. In my current code structure I use ng-repeat to iterate and render these objects with a custom directive. The issue is that these objects are meant to be "live" ( meaning that they are 3-way bound. The highest level object is bound with angularfire $bind ).
So the simple scenario in my case would be where the ng-repeat created scope was not isolated from the scope that it was created from.
I am looking for ideas on how to do this? Or suggestions on other approaches.
This won't be a complete answer, but I can help with the angularFire portion, and probably an angular guru can fill in the blanks for you (see //todo).
First of all, don't try to share scope. Simple pass the variables you want into the child scope. Since you'll want a 3-way binding, you can use & to call a method on the parent scope.
For example, to set up this pattern:
<div ng-repeat="(key,widget) in widgets">
<data-widget bound-widget="getBoundWidget(key)"/>
</div>
You could set up your directive like this:
.directive('dataWidget', function() {
return {
scope: {
boundWidget: '&boundWidget'
},
/* your directive here */
//todo presumably you want to use controller: ... here
}
});
Where &boundWidget invokes a method in the parent $scope like so:
.controller('ParentController', function($scope, $firebase) {
$scope.widgets = $firebase(/*...*/);
$scope.getBoundWidget = function(key) {
var ref = $scope.widgets.$child( key );
// todo, reference $scope.boundWidget in the directive??
ref.$bind(/*** ??? ***/);
};
});
Now you just need someone to fill in the //todo parts!
You still have access to the parent scope in the repeat. You just have to use $parent.
Controller
app.controller('MainCtrl', ['$scope', function ($scope) {
$scope.someParentScopeVariable = 'Blah'
$scope.data = [];
$scope.data.push({name:"fred"});
$scope.data.push({name:"frank"});
$scope.data.push({name:"flo"});
$scope.data.push({name:"francis"});
}]);
HTML
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="item in data | filter: search">
<td>
<input type="text" ng-model="$parent.someParentScopeVariable"/>
<input type="text" ng-model="item.name">
</td>
</tr>
</table>
</body>
Plunker
Related
How can I know the Parent controller name in controller chain of Angular?
I'm new to Angular so I have one basic question.
Suppose, In Angular, I have a controller's chain like below.
<div ng-controller="parentController">
... something in the parent element
<div ng-controller="childController">
... something in the child element
</div>
</div>
Is there any way to write the code in the child element so that I can know the parent controller name in the output (In this case output should be 'parentController')?
I need this because I have a too big project and want to know the parent of each controller because someone has wrote the code like
googleOAuth= $scope.$parent.$parent.$parent.$parent.status.googleOAuth
and I'm not able to understand so want to know the parent of each $scope.
On approach is to use "controller as" syntax:
<div ng-controller="parentController as top">
<!-- ... something in the parent element -->
<div ng-controller="childController">
<!-- ... something in the child element -->
{{top.status.googleOAuth}}
</div>
</div>
This requires the controller be written using the this context instead of $scope.
Another approach is to use a property of an object in the parent scope:
app.controller("parentController", function($scope) {
$scope.top = {status: {} };
$scope.top.status.googleOAuth = value;
});
<div ng-controller="parentController">
<!-- ... something in the parent element -->
<div ng-controller="childController">
<!-- ... something in the child element -->
{{top.status.googleOAuth}}
</div>
</div>
Because scopes use prototypical inheritance, the top property is available to child scopes.
See AngularJS Developer Guide - Scope Hierarchies.
As georgeawg said, using $parent is not optimal because it relies on a constant number of scopes.
Instead, you could write a service to deal with your googleOAuth.
The service can then be injected in each controller and will function as a single source of truth because services are singletons in AngularJS.
e.g. something like this
angular.module('appModule', [])
.factory('googleOAuthService', [function() {
var googleOAuth = {
// your googleOAuth stuff here
};
return {
get: get,
set: set,
stuff: stuff
}
function get () {
return googleOAuth;
}
function set (newGoogleOAuth) {
googleOAuth = newGoogleOAuth;
}
function stuff () {
// Do stuff to googleOAuth
}
}])
.controller('parentController', ['googleOAuthService', function(googleOAuthService) {
googleOAuthService.stuff();
}])
.controller('childController', ['googleOAuthService', function(googleOAuthService) {
googleOAuthService.stuff();
}]);
For more info, see https://docs.angularjs.org/guide/services
I have two controllers where the first one contains a list of items:
$scope.items = [{id:1, desc:'desc1'},{id:2, desc:'desc2'}...];
The second one binds to a template which displays a list of items selected:
$scope.myitems = [1,2,3,...]; // ids only
<div ng-repeat="item in myitems">
{{item.id}} / {{item.desc}}
</div>
How can I look up the item desc in the ng-repeat of the second controller from the item list of the first controller?
Sharing data between controllers is best achieved via services/factories, an alternative is to use scope inheritance.
1. Factory/Service
angular.module('yourModule').factory('itemService', itemService);
function itemService() {
var items = [{id:1, desc:'desc1'},{id:2, desc:'desc2'}...];
return {
findItemById: findItemById
}
function findItemById(id) {
//logic to find item by id
}
}
Inject this factory in your controllers and add more functions if needed.
2. Scope inheritance
The key here is to nest your childcontroller, which I presume is the one with the ids.
<div ng-controller="topCtrl">
<div ng-controller="childCtrl">
<div ng-repeat="item in myitems">
{{item.id}} / {{item.desc}}
</div>
</div>
</div>
With this option, any controller that is nested within the topCtrl in the view has access to the topCtrl scoped variables.
A third option would be to store the data in the $rootScope, which is actually also a sort of scope inheritance (all scopes except for isolated directive scopes inherit from the rootScope), but that's probably not a good idea for your usecase.
You can try to use inheritance concept:
var app = angular.module(....);
app.controller('FirstCtrl ', function($scope) {
$scope.items = [{id:1, desc:'desc1'},{id:2, desc:'desc2'}...];
});
app.controller('SecondCtrl', function($scope, $controller) {
$controller('FirstCtrl', {$scope: $scope}); // $scope.items now available
});
Advantage over the $rootScope solution:
second controller will always have access to items, even if first controller is not instantiated
items array may be changed only by these two controllers, and not anyone other
Main flaw of this approach is that SecondCtrl will have access to any scope variable of the FirstCtrl and not only items array
EDIT. IMHO Factory/Service approach mentioned by #NexusDuck is the best one (composition over inheritance).
Can you assign values to $rootScope.items in first controller, then try to access in second controller.
You can use $emit and $on.
EX:
In First controller add
$scope.$emit('eventName', $scope.items);
};
And also pass $scope.items.
In Second Controller Add
$scope.$on('eventName', function (event, args) {
$scope.items = args;
});
args hold $scope.items value and is assign to $scope.items in second controlle, so now ng-repeat display value of $scope.items of First Controller.
I have a directive set up here http://jsfiddle.net/screenm0nkey/8Cw4z/3 which has two bindings to the same scope property, but for some reason the binding in the directive's template property doesn't update when the model changes (after typing in the input).
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count">
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
}
};
});
But if I move the input position in the markup it works and both bindings update.
<input type="text" ng-model="count">
<test>
<h3>Inner {{count}}</h3>
</test>
http://jsfiddle.net/screenm0nkey/dCvZk/3
Can anyone explain why the position of the input containing the binding, would have an affect the bindings. I assumed that during the digest loop the watchers for both binding would be updated regardless of the position of the markup.
Many thanks
To me, this seems purely to be a scope issue. Lets take a look at the markup that is generated by both:
Not working:
<body ng-app="App" class="ng-scope">
<h1 class="ng-binding">Outer1 <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner 1</h3>
<input type="text" ng-model="count" class="ng-scope ng-pristine ng-valid">
</div>
</h1>
</body>
Working:
<body ng-app="App" class="ng-scope">
<input type="text" ng-model="count" class="ng-valid ng-dirty">
<h1 class="ng-binding">Outer <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner </h3>
</div>
</h1>
</body>
The ng-scope class is a useful marker for where Angular is declaring a new scope.
You can see by the markup that the in the working example both the count properties are enclosed in the scope that is attached to body. So, in this case, the directive scope is a child of the body scope (and therefore has access to it).
However, In the example that is not working, the Outer1 property is sitting outside of the scope that the input is in.
The Angular Scope documentation covers this well. The scopes are arranged in a hierarchy with child scopes having access to parent scopes (but not the other way around):
The application can have multiple scopes, because some directives
create new child scopes (refer to directive documentation to see which
directives create new scopes). When new scopes are created, they are
added as children of their parent scope. This creates a tree structure
which parallels the DOM where they're attached
Long story short - as others have said, this is a scope issue. Using the "ng-transclude" directive creates a new scope. When a new scope is created values from the old scope will be accessible in the new scope (hence the first replace) but after that only objects that are shared between the old/new scope will be updated. That is why using an object would work, but using a value will not.
In your case placing the input field inside of the ng-transclude causes this to only edit the value in that scope, not the value in the parent scope (which is where the count for the "test" directive is pulled from).
Incidentally, this can be an issue with repeaters (ng-repeat) as well as other directives. Its best to use a tool such as "Batarang" in order to find issues such as this. It allows you to look at what is in each scope and determine why the screen isn't showing the "correct" data. Hope that helps explain further!
Add ng-change to input , it should work. The problem is that controller into directive doesn't know about count change.
JS
var App = angular.module('App', []);
App.directive('test', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer {{this.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
$scope.onChange = function(count){
$scope.count = count;
}
}
};
});
HTML
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count" ng-change="onChange(count)">
</test>
Demo Fiddle
The order matters because of the difference between creating a property on the scope versus actually using an object bound to the scope (especially when a transclude creates a new child scopr). Best practice is to use an object on the scope and bind properties to that object when scope issues can come into play with directives and transcludes.
If you change your code to this, it will work as you were expecting and order does not matter. Notice that I am creating a scope object and placing the count as a property on that object.
<test>
<h3>Inner {{data.count}}</h3>
<input type="text" ng-model="data.count"/>
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{data.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.data = {};
$scope.data.count = 1;
}
};
});
This is a great tutorial on this subject. Props to EggHead. https://egghead.io/lessons/angularjs-the-dot
It's a scoping issue.
$scope.count = 1; adds the property count to the scope that <test> is in. Let's call it parent scope.
ng-transclude creates a new scope, let's call it child scope. When <h3>Inner {{count}}</h3> is evaluated, the child scope has no property count so it's read from the parent scope.
<input type="text" ng-model="count"> binds the value of the input to the property count in the child scope. As soon as you enter something the property will be created if it's not there yet. From this point on <h3>Inner {{count}}</h3> gets its value from the child scope.
Scopes in angular are simple JavaScript objects and are connected to their parents via prototypes. So before you enter something the child scope looks something like
{
prototype: { // = parent scope
count: 1
}
}
When you change the value to, say, 5, the scope looks something like
{
count: 5,
prototype: { // = parent scope
count: 1
}
}
Because data binding does something like scope.count = 5.
Here's a work around
Change $scope.count to
$scope.helper = {
count: 1
}
and refactor the rest.
Check this video out for an explanation.
It seems that we cannot override this since ngTransclude will use $transclude function directly.
See: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
and: http://docs.angularjs.org/api/ng.$compile
transcludeFn - A transclude linking function pre-bound to the correct transclusion scope. The scope can be overridden by an optional first argument. This is the same as the $transclude parameter of directive controllers. function([scope], cloneLinkingFn).
I ran into an issue with ng-form not setting up the form on scope when its nested within an ng-scope.
For example
<div ng-controller='TestCtrl'>
<ng-switch on="switchMe">
<div ng-switch-default>Loading...</div>
<div ng-switch-when="true">
<form name="nixTest">
<input placeholder='In switch' ng-model='dummy'></input>
<button ng-click="test()">Submit</button>
</form>
</div>
</ng-switch>
</div>
Controller:
controllers.TestCtrl = function ($scope) {
$scope.switchMe = true;
$scope.test = function () {
if ($scope.nixTest) {
alert('nixTest exists')
} else {
alert('nixTest DNE')
}
}
}
Are there any work arounds to this ? Test fiddle can be found here
ng-switch creates a child scope and the form is created on this scope. Hence the child scope form would not be available on the parent scope.
To get access to it, you can pass it to the method test() like ng-click=test(nixTest). So the scope method signature would also need to updated to support the input parameter.
I ran into the same issue. Unfortunately I could not easily apply Chandermani's solution, because I need to access the form name from an $on call, within the ng-switch parent scope.
Thus, I resorted to creating a directive that sends the form's name to the $rootScope:
.directive("globalName", ["$rootScope", function($rootScope) {
return function(scope, element) {
$rootScope["get_" + element.attr('name')] = function() {
return scope[element.attr('name')];
};
}
}]);
Usage is like this:
<form name="whatever" novalidate global-name>...</form>
and then you access the form in controllers e.g. like this:
$scope.get_whatever().$setPristine();
$scope.get_whatever().$setUntouched();
Being the name in the $rootScope, it does not depend anymore on your DOM structure.
I understand this is not an optimal solution, as it pollutes the global namespace, but either I feel uncomfortable with form name visibility depending on the DOM structure, in somewhat unexpected ways.
I was trying to create a table with a filter at the top of each column.
I am unable to figure out why the Any filter is working but not the individual colum filters.
I have the code # http://plnkr.co/edit/6ghH02
I have also tried to compile the element after setting the ng-model but it did not work.
Can anyone please help?
You can do this if you get rid of the directive and use the ng-change attribute of the input[text] like so:
function TestController ($scope) {
$scope.valueChanged = function (name, value) {
if (!$scope.search) {
$scope.search = {};
}
$scope.search[name] = value;
}
};
<body ng-controller="TestController">
<th ng-repeat="sortName in columnNames">
<input type=text ng-model="value" ng-change="valueChanged(sortName, value)">
</th>
The short version of the answer as to why your original code doesn't work is that ngRepeat creates a new scope and the value in ng-model must be an assignable variable.
As #Ryan pointed out, ng-repeat's nested scopes are the trick. An alternative to writing a controller method is to use the $parent property of the child scope to access the search property in the parent scope:
<th ng-repeat="sortName in columnNames">
<input type=text ng-model="$parent.search[sortName]"></th>
As in #Ryan's solution, a directive is not needed.
However, the additional search properties needed to be created/initialized:
<div ng-init="search.name = ''; search.phone = ''"></div>
http://plnkr.co/edit/wT34vh?p=preview