How to keep isolate scopes in sync with parent scope variable? - angularjs

I have a controller:
MyController
- $scope.value = 5
- $scope.list = [
{item: Apple, cost: 5},
{item: Bannana, cost: 2}
]
In my index.html I have an ng-repeat on:
<div ng-controller="MyController">
<mydirective ng-repeat="item in list" value="value"></mydirective>
</div>
In my directive, I have an isolate scope:
{
value:"="
}
In the linking function of the directive I have an bind to an event:
- Onclick, increment scope.value by "1"
The issue is it appears that despite the "=", each of the 2 directives generated from ng-repeat are merely "copies" of the actual MyController "value" variable. How do I make it such that they are linked together so that when I click, the $scope.value of myController gets updated and all the directive "scope.value" would match it? Or is this not possible?
I want to be able to watch on $scope.value of MyController from all the directives so that each of the directives can do something based on "value".

I suppose you hit the problem of your scopes inheriting primitive types.
Try using an object instead:
MyController
- $scope.valueObject = { value: 5 }
then increment valueObject.value by "1"
Each scope of your ng-repeat inherits from the controllers scope.
If you inherit a primitive type like your "value", then changing it in the child scope will only affect this child as it shadows the parent.
If you inherit an object instead, then changing the value would affect the inherited object = the value would be the same for all ng-repeat scopes.
The concept is described here: Understanding scopes
a similar SO question was answered here: Scope inheritance in angularjs

Related

angular 1.5: watch doesn't call when i toggle the ui

I my template i have:
<md-checkbox ng-model="$ctrl.isAdOps" aria-label="isAdOps">
isAdOps {{ $ctrl.isAdOps }}
</md-checkbox>
In my component:
(function (app) {
app.component('homeComponent', {
templateUrl: 'partials/home-partial.html',
bindings: {
isAdOps: '='
},
controller: ['$scope', '$state', function ($scope, $state) {
var self = this;
$scope.$watch(
self.isAdOps,
function (isAdOps) {
$scope.$broadcast('isAdOpsToggled', isAdOps);
}
);
}]
});
})(myApp);
why doesn't the watch called when i toggle the md-checkbox?
Angular $watch first parameter should of type string/function
string: Evaluated as expression
function(scope): called with current scope as a parameter.
replace self.isAdOps with 'isAdOps' in $watch or alternativly use a function syntax with $scope.
You are better off using the ng-change directive:
<md-checkbox ng-model="$ctrl.isAdOps"
ng-change="$ctrl.isAdOpsChanged($ctrl.isAdOps)"
aria-label="isAdOps>
isAdOps {{ $ctrl.isAdOps }}
</md-checkbox>
It avoids using $scope and adding a watcher.
For more information, see Writing Components without Watchers.
See also, AngularJS Developer Guide - Component-based application architecture.
Components should follow a few simple conventions:
Inputs should be using < and # bindings. The < symbol denotes one-way bindings which are available since 1.5. The difference to = is that the bound properties in the component scope are not watched, which means if you assign a new value to the property in the component scope, it will not update the parent scope. Note however, that both parent and component scope reference the same object, so if you are changing object properties or array elements in the component, the parent will still reflect that change. The general rule should therefore be to never change an object or array property in the component scope. # bindings can be used when the input is a string, especially when the value of the binding doesn't change.
Outputs are realized with & bindings, which function as callbacks to component events.
This will make the migration to Angular 2+ easier.

Along with Transclude element, can i pass its scope too to a directive?

Moment i felt i have understood enough about Transclude i came across this statement :
Transclude allows us to pass in an entire template, including its scope, to a directive.
Doing so gives us the opportunity to pass in arbitrary content and arbitrary scope to a directive.
Does this mean, if there is a scope attached to Transclude element and it can be passed on to the directive ? If that's true then am not able to access that scope property inside directive template.
Let me take couple of steps back and explain with code about what am trying to do :
JSFiddle Link
My directive is directive-box and transclude: true is defined in Directive Definition Object(DDO).
Now there is a Child Div, which is the element to be Transcluded
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
and it has controller TransCtrl attached to it.
Now am trying to access $scope.name property which is part of TransCtrl from directive level after defining this in DDO :
scope: {
title: '#directiveTitle',
name: '='
}
Is this possible ?
This is more like a Parent scope trying to access Child scope property, is this permitted in JavaScript Protoypical inheritance ? Or is there something else i need to know ??
If this is not possible what does first statement mean ?
Transclude allows us to pass in an entire template, including its scope, to a directive.
UPDATE 1 :
My primary concern is Controller should remain with Transclude element, still we should be able to pass its (Transclude element) scope to Directive and then Directive should be able to consume that scope i.e., name from TransCtrl controller .
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
Above line of code should remain as is.
I may be completely wrong with my question but please let me if this can be accomplished.
The problem seems to be with the way the controller is defined within the ng-transcluded html.
I have made it clearer by using
the bindToController construct
using a controller at the directive level
Refer this fiddle for a working example.
controllerAs: "TransCtrl",
bindToController: true
And your statement, 'Parent scope trying to access Child scope property' is incorrect right? Since we are trying to use the parent scope property, i.e. name from within the child (ng-transcluded content), which is possible with protypical inheritance, and not the other way around.
Does this answer your question: https://jsfiddle.net/marssfa4/4/?
In it I have created a new controller on the outside (effectively replacing the functionality of your rootScope for inside the directive) and I made the directive's controller be set inside your controller template.
The long and short of it is though that you can see that it is possible to transclude html along with its scope even into a directive with its own scope.
The html:
<div ng-app='myApp' ng-controller="OutsideScope">
<h1>{{externalWorld}}</h1>
<div directive-box directive-title='{{directiveWorld}}' name='name'>
<div>Inside Transclude Scope : {{name}}</div>
</div>
</div>
JS (includes Update 1):
angular.module('myApp', [])
.directive('directiveBox', function() {
return {
restrict: 'EA',
scope: {
title: '#directiveTitle',
name: '='
},
transclude: true,
template: '<div ng-controller="TransCtrl">\
<h2 class="header">{{ title }}</h2>\
<div class="dirContent">Directive Element</div>\
<div>Outside Transclude Scope : {{name}}</div>\
<div class="content" ng-transclude></div>\
</div>'
}
})
.controller('TransCtrl', function($scope) {
$scope.name = 'Transclude World'
})
.controller('OutsideScope', function($scope) {
$scope.name = 'External World'
})
.run(function($rootScope) {
$rootScope.externalWorld = 'External World',
$rootScope.directiveWorld = 'Here comes directive'
});
UPDATE 1: JSFIDDLE
I restored the original scope declarations as the scope: false was a mistake.
If I understand your comment correctly you want to leave the controller on the element to be transcluded but still have the {{name}} within that element ignore its immediate controller and use as controller its parent (i.e. the directive's) scope.
The reason I placed the controller within the template directive is because that is the only way to limit the directive's scope on the directive and not its transcluded elements. If you are explicitly placing a controller on an element, then regardless of whether it is contained within a directive with another scope, its closest scope will override whatever scope has been declared on the directive. In other words, regardless of what the directive's scope is, the {{name}} in
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
will always be whatever $scope.name is in TransCtrl.

ionic, pass scope variable to popover scope

I would like to pass a variable from the view scope ($scope.sort) into the popover scope.
Once set in the popover, the variable 'goes out' of it with no issue though, with code below in$scope.closeSortPopover`.
I thought the popover scope was the same as the scope of the controller it comes from.
But it seems it's not the case.
Where is the popover scope ?
If I understand correctly from here the popover scope is a child of the controller scope. So how can I access it... ?
FYI my popover is a list of <ion-radio>s offering different sorting opions for the big list in my main view.
html:
<button ng-click="openSortPopover($event, sort)">
controllers.js
$ionicPopover.fromTemplateUrl('templates/computeSortPopover.html', {
scope: $scope
}).then(function(popover) {
$scope.sortPopover = popover;
});
$scope.openSortPopover = function($event, sort) {
$scope.sortPopover.show($event);
};
$scope.closeSortPopover = function(sort) {
$scope.sortPopover.hide();
$scope.sort = sort;
};
In the controller js, you are assigning the scope of the controller to the scope of the popover. Thus all the methods in the controller scope are accessible via the popover view. In other words, both the controller view and popover view share the same scope.
In fact you might think this a violation of mvc as one controller cannot own two views, but with angular, you actually can't otherwise.
To create isolated scope, your only option is directives.
I discovered that due to javascript inheritence, the scope variable must be contained into an object to be passed to inherited scopes.
So instead of this:
$scope.sort = ....
I declared this in my controller:
$scope.data={};
$scope.data.sort = ....
and used in my html (both in the initial view and in the popover templates):
data.sort
Documented here apparently: https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance

AngularJS - Explain why transcluded scopes must be siblings of an isolate scope

In the Angular docs on directives, there is this paragraph:
However isolated scope creates a new problem: if a transcluded DOM is
a child of the widget isolated scope then it will not be able to bind
to anything. For this reason the transcluded scope is a child of the
original scope, before the widget created an isolated scope for its
local variables. This makes the transcluded and widget isolated scope
siblings.
Can someone please explain why "if a transcluded DOM is a child of the widget isolated scope then it will not be able to bind to anything"?
Imagine you have some markup like this:
<html ng-app="myApp">
<body>
<div ng-controller="myController">
<div ng-repeat="item in items">
<div my-widget>
{{item.name}}
</div>
</div>
</div>
</body>
</html>
That sets up a tree of scopes ($rootScope -> controller scope -> ng-repeat scope -> widget scope). Now say your controller has some things in it:
function myController($scope) {
$scope.items = [
{name: 'Stella Artois'},
{name: 'Red Stripe'}
];
}
You can read values from a scope any number of levels up, because they inherit from each other using prototypical inheritance. {{item}} doesn't exist in the widget scope, but it does in the parent ng-repeat scope, so it's found just fine.
If you use isolate scope, you get a brand new scope that doesn't inherit from anything. So if my-widget uses scope: {} for example, the scope tree looks more like:
$rootScope
└controller scope
└ng-repeat scope
widget scope
Then in the double curlies, "item" is unknown. Using transclusion, you can set the scope up as a sibling like so:
$rootScope
└controller scope
└ng-repeat scope
└widget contents
widget scope
Another subtlety that was mentioned in this talk is that if your transcluded content's scope is a child of the directive's, then the directive could clobber whatever variables the transcluded content was trying to reference in a parent scope. For example:
<body ng-controller="MainCtrl">
<my-directive>{{ name }}</my-directive>
</body>
JS:
app.controller("MainCtrl", function($scope) {
$scope.name = 'foo';
});
app.directive("myDirective", function() {
return {
scope: {},
transclude: true,
template: '<span ng-transclude></span>',
controller: function($scope) {
$scope.name = 'bar';
}
}
});
Transclusion ensures that {{name}} in the directive references 'foo' instead of 'bar'.

angularjs directive with ng-repeat not respecting = scope binding

I have created a directive with an isolated scope with two properties. One of them set to data-binding with the equal sign. If I manually insert the directive several times the html document, changes to the values are reflected, as expected, on the scope in the controller. But if I insert the elements with a repeater (ng-repeat) the connection to the scope on the controller no longer works. Any idea why?
The directive looks like this:
myApp.directive("phone", function(){
return{
restrict: "E",
scope:{
number:"#",
dirname:"="
},
template: '<div class="panel"> <input type="text" ng-model="dirname"><br>Number:{{number}} {{dirname}}</div> '
}
});
I'll guess (since you didn't provide any HTML or model data) that you have an array of dirnames, so inside the ng-repeat, you are trying to bind the ng-model to a primitive. Since each iteration of ng-repeat creates its own child scope, when you first type into a textbox, a dirname primitive property will be created on the child scope. (This is how JavaScript prototypal inheritance works.)
The fix is to use an object rather than a primitive.
$scope.names = [ {name: 'Superhero'}, {name: 'Julio'} ];
<li ng-repeat="nameObj in names">
<phone number="123" dirname="nameObj.name"></phone>
</li>
Fiddle.

Resources