Angular Scope, mystery? - javascript

I have two directives, Isolated and Shared, the Isolated directive pass the two-way binding directly to the Shared directive but the Shared directive is not using the Isolated scope, is creating its own.
The objective is that the Isolated directive should respond to changes in the two-way bindings when the Shared directive changes them.
<body ng-app="app">
<div ng-controller="main as $ctrl">
<h3>Main data: {{$ctrl.data.bind}}</h3>
<isolated bind="$ctrl.data.bind"></isolated>
</div>
</body>
angular.module("app", [])
.controller("main", function() {
this.data = {
bind: 123
}
})
.directive("isolated", function() {
return {
scope: {
bind: '='
},
bindToController: true,
template: '<div><h3>Parent directive data: {{$ctrl.bind}}</h3> </div>'
+ '<input type="text" shared ng-model="$ctrl.bind" />',
controller: function() {
this.changed = function() {
console.log('Data changed: ' + this.bind);
}
},
controllerAs: '$ctrl',
link: {
pre: function($scope) {
console.log("Parent data: " + $scope.$ctrl.bind);
}
}
}
})
.directive("shared", function() {
return {
restrict: 'A',
require: {
ngModel: '^'
},
bindToController: true,
link: function($scope) {
console.log('Current data in shared: ' + $scope.$ctrl.bind)
},
controller: function() {
this.$postLink = function() {
this.ngModel.$modelValue = 321;
}
},
controllerAs: '$ctrl'
}
});
Here I have a Plunker

Gourav Garg is correct. Due to the shared scope, the second directive declaration is overriding the $scope.$ctrl field. The controllerAs property in the second declaration is unneeded anyways, as you never access the controllers properties within the template. If you do end up needing the second directives controller information within your template, you need to declare it's name as something other than $ctrl, or, better yet, use require syntax to require the second directive on the first directive. That will bind the second directive's controller to a property on the first directive's controller.
For more information on require, see the "Creating Directives That Communicate" section of the angular directive guide here.
Best of luck!

Related

Angular: Using “require” in Directive to require a parent Controller and call parent function

I'm using the require attribute in directive to create a link between 2 controllers, everything works great but when I want to invoke a parent function from the child scope I need to use the ugly syntax of $parent.ParentCtl.func() and I would like to know if I can avoid this syntax and invoke the function without explicitly writing the $parent notation.
The child doesn't have isolated scope.
I've look around and didn't find an answer to that question.
At the moment I'm using a factory to bind that functions from parent.
Thanks
The good way
If you're using Angular >=1.5 you can use the require syntax in combination with the controllerAs syntax, so instead of using the scope to access the parent, you use a direct reference to the parent which is created by angular, when you explicitly require another directive. For example:
angular.module("test", [])
.directive("foo", function() {
return {
restrict: "E",
scope: {},
bindToController: true,
controllerAs: "fooController",
controller: function() {
var controller = this;
controller.something = "Foo Initial Value";
controller.setSomething = function(something) {
controller.something = something;
console.log("Foo Changed:" + something);
}
}
}
})
.directive("bar", function() {
return {
scope: {},
restrict: "E",
require: {
"parent": "^^foo"
},
controllerAs: "barController",
template: "Bar <a href='' ng-click='barController.parent.setSomething(\"from template\")'>Change directly on parent</a>",
bindToController: true,
controller: function() {
var controller = this;
this.$onInit = function() {
// We can access the parent after the $onInit.
console.log(controller.parent.something);
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0/angular.js"></script>
<body ng-app="test">
<foo>
<bar></bar>
</foo>
</body>
In this example, the bar directive requires the foo controller, which will get binded to the parent property of the bar controller.
The Not-So-Good way
I do not like this way, since the directive's are tied together by the coincidence of they being used in the proper order.
angular.module("test", [])
.directive("foo", function() {
return {
restrict: "E",
scope: true,
controller: ["$scope", function($scope) {
$scope.something = "Foo Initial Value";
$scope.setSomething = function(something) {
$scope.something = something;
console.log("Foo Changed:" + something);
}
}]
}
})
.directive("bar", function() {
return {
scope: true,
restrict: "E",
template: "Bar <a href='' ng-click='setSomething(\"from template\")'>Change directly on parent</a> Something: {{something}}"
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0/angular.js"></script>
<body ng-app="test">
<foo>
<bar></bar>
</foo>
</body>
From my experience, I recommend using the ControllerAs syntax.

Angular component won't display

I have piece of html I want to show as a component, as I'm not manipulating the DOM.
As a directive it works fine, but as a component it doesn't. I have made components before with no problem, just can't see what the issue is here.
If I comment in the component code, and the directive out, it doesn't work.
Any idea what I've done wrong?
(function() {
"use strict";
angular
.module('x.y.z')
// .component('triangularStatus', {
// bindings: {
// value: '=',
// dimension: '=?'
// },
// templateUrl: '/path/to/triangular-status.html',
// controller: TriangularStatusController,
// controllerAs: 'vm'
// });
.directive('triangularStatus', triangularStatus);
function triangularStatus() {
var directive = {
scope: {
value: '=',
dimension: '=?'
},
replace: true,
templateUrl: '/path/to/triangular-status.html',
controller: TriangularStatusController,
controllerAs: 'vm',
};
return directive;
}
TriangularStatusController.$inject = [];
function TriangularStatusController() {
var vm = this;
}
})();
Here is the working code, most probably you are not using vm.values to access data.
Just be sure you are using right version of angular js ~1.5
(function(angular) {
angular.module('x.y.z', [])
.component('triangularStatus', {
bindings: {
value: '=',
dimensions:'=?'
},
template: '{{vm.value}} <br/> {{vm.dimensions}}' ,
controller: TriangularStatusController,
controllerAs: 'vm'
});
TriangularStatusController.$inject = [];
function TriangularStatusController() {
}
})(window.angular);
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.js"></script>
<div ng-app = "x.y.z">
<triangular-status value="24" dimensions="348"></triangular-status>
</div>
The definition of your component, using bindings, is not directly equivalent to the definition of your directive, using scope, even though both are defined to use controllerAs. This is because your component will be binding directly to the controller, and your directive will be binding to $scope (by default).
I've used your code in the snippet below, slightly modified to allow the component and directive(s) to be used together. I've also added an additional directive that makes use of bindToController:true to demonstrate a directive that behaves a little more like a component in binding its attribute values directly to the controller, rather than to $scope.
I've also used a very basic shared template that attempts to show the bound attribute values by looking for them on $scope, followed by looking for them on vm (the ControllerAs).
(function() {
"use strict";
var templateBody = '<h2>$scope</h2>' +
'<p>value: {{value}}</p><p>dimension: {{dimension}}</p>' +
'<h2>vm</h2>' +
'<p>vm.value: {{vm.value}}</p><p>vm.dimension: {{vm.dimension}}</p>';
angular
.module('x.y.z', [])
.component('triangularStatusComponent', {
bindings: {
value: '=',
dimension: '=?'
},
template: '<div><h1>Triangular Status Component</h1>' + templateBody + '</div>',
controller: TriangularStatusController,
controllerAs: 'vm'
})
.directive('triangularStatusDirective', triangularStatusDirective)
.directive('triangularStatusDirectiveBound', triangularStatusDirectiveBound);
function triangularStatusDirective() {
var directive = {
scope: {
value: '=',
dimension: '=?'
},
replace: true,
template: '<div><h1>Triangular Status Directive</h1>' + templateBody + '</div>',
controller: TriangularStatusController,
controllerAs: 'vm',
};
return directive;
}
function triangularStatusDirectiveBound() {
//https://docs.angularjs.org/api/ng/service/$compile#-bindtocontroller-
var directive = {
scope: {
value: '=',
dimension: '=?'
},
bindToController: true,
replace: true,
template: '<div><h1>Triangular Status Directive Bound</h1>' + templateBody + '</div>',
controller: TriangularStatusController,
controllerAs: 'vm',
};
return directive;
}
TriangularStatusController.$inject = [];
function TriangularStatusController() {
var vm = this;
}
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="x.y.z">
<triangular-status-component value="'componentValue'" dimension="'componentDimension'">
</triangular-status-component>
<hr>
<triangular-status-directive value="'directiveValue'" dimension="'directiveDimension'">
</triangular-status-directive>
<hr>
<triangular-status-directive-bound value="'directiveValueBound'" dimension="'directiveDimensionBound'">
</triangular-status-directive-bound>
</div>
If you're finding that your code works as a directive, where your values are bound to $scope, but not as a component, where your values are bound to the controller, I would assume either your template html (most likely?) or your controller function are relying on trying to access your values as though they were on $scope. To confirm this, you may notice there are errors being logged to your javascript console that will help you zero in.
I think the only problem is, that your missing the brackets:
angular.module('x.y.z')
change to
angular.module('x.y.z', [])
https://docs.angularjs.org/api/ng/function/angular.module
As was mentioned in the comment, I need to clarify, the problem can be how are your JS files ordered or bundled, some other JS file executed later can overwrite this module and therefor you will not see any tag rendered.

Bind to model from child directive where model has not been resolved at compile-time

I am using angular ui for bootstrap for its modals:
http://angular-ui.github.io/bootstrap/#/modal
I am opening a modal with a controller and templateUrl with:
var modalInstance = $uibModal.open({
animation: true,
templateUrl: $scope.templateUrl,
controller: $scope.controller,
size: 'lg',
resolve: {
formModel: item
}
});
where formModel is the model I will use in the modal.
Here is the controller for the modal:
app.controller('commentCtrl', ['$scope', '$modalInstance', 'formModel', function ($scope, $modalInstance, formModel) {
$scope.formModel = {};
var loadFormModel = function () {
if (formModel !== undefined) {
$scope.formModel = formModel;
}
};
loadFormModel();
}]);
This modal has child directives and needs to pass properties of formModel to them
template:
<div>
<child model="formModel.Comment"></child>
</div>
but child is created before the modal's controller has loaded formModel. Inside the child I want to use model as:
app.directive('child', function () {
return {
restrict: 'E',
template: '<textarea ng-model="model"></textarea>',
link: linkFn,
controller: controllerFn,
scope: {
model: '='
}
};
});
Edit:
I've found that I can do:
<div>
<child model="formModel" property="Comment"></child>
</div>
...
app.directive('child', function () {
return {
restrict: 'E',
template: '<textarea ng-model="model[property]"></textarea>',
link: linkFn,
controller: controllerFn,
scope: {
model: '=',
property: '#'
}
};
});
Is there a better way to do this without the extra attribute?
Edit 2:
I have found where the bug is:
http://plnkr.co/edit/kUWYDvjR8YArdqtQRHhi?p=preview
See fItem.html for some reason having any ng-if causes the binding to stop working. I have put a contrived ng-if='1===1' in for demonstration
This happens because ng-if directive creates new inherited scope, so the bug is just a common scope prototypal inheritance pitfall.
The most concise way to get around it is to use controllerAs syntax in conjunction with bindToController, the avoidance of undesirable scope inheritance side effects is the most common use case for them. So it will be
app.directive('fItem', function () {
return {
restrict: 'E',
replace: true,
templateUrl: 'fItem.html',
controller: ['$scope', function ($scope) {
}],
controllerAs: 'vm',
bindToController: true,
scope: {
model: '='
}
};
});
and
<div class="input-group">
<textarea ng-if='1===1' ng-model="vm.model" class="form-control"></textarea>
</div>
Not sure what you are trying to do. If you need just to wait until variable is resolved, you need just use promise: (Here is simple one using $timeout, if you use i.e. $http - you ofc dont need $q and $timeout)
resolve: {
something: function () {
return $q(function(resolve, reject) {
setTimeout(function() {
resolve('Hello, world!');
}, 3000);
});
Then modal will be opened only after promise is resolved.
http://plnkr.co/edit/DW4MzIO4ej0JorWRgWIK?p=preview
Also keep in mind that you can wrap you object, so if in scope you have $scope.object = {smth : 'somevalue'} :
resolve: {
object: function () {
return $scope.object;
});
And in modal controller:
$scope.object = object;
Now object in initial controller scope and object in modal scope point to same javascript object, so any time you change one - another changes. You are free to use object.smth in modal template as usual property. And as soon as it will change you will see changes.

How to specify which $scope a directive uses?

I have a directive that is watching something on $scope, and it is getting the wrong $scope.
so I have a state setup that specifies a controller:
.state('app.mystate', {
url: 'search/:searchText',
views: {
'mainPane#': {
templateUrl: 'views/content/search.html',
controller: 'ABCController'
}
},
resolve: {
searchPromise: ['$http', '$stateParams', function ($http, $stateParams) {
console.log($stateParams.searchText);
return $http.get(...blah...blah).then(blahblah);
}]
}
})
When this state gets activated, it goes to this view:
<ul ...>
<div ng-repeat="...">
<li ...>
<div ng-include="'views/widgets/somewidget.html'"></div>
</li>
</div>
</ul>
the ng-include loads this, which has a specific controller specified. And a directive.
<div ng-controller="XYZController">
<my-chart chart="chart" ...></my-chart>
</div>
Here is my directive:
angular.module('app.directive')
.directive('myChart', function () {
return {
template: '<div></div>',
scope: {
chart: '='
},
// wrong scope!
scope.$watch('chart', function (chart) { }
I figured that the <div> that contains the my-chart directive, that directive would get the $scope that goes into XYZController.
But the directive is getting the scope that is injected into ABCController, not XYZController as I want/expected. I can see XYZContoller getting activated.
How do I get the $scope that is injected into XYZController be the same scope that the my-chart directive sees?
The sample code for you directive seems to have been truncated/broken during copy/paste so it's hard to see where that scope.$watch call is at. If it is in the directive definition, after the return block, then it's never getting called but I would expect it to be in the controller.
Having said that, you can specify the controller to be used by the directive in the directive attributes which would seem appropriate here, unless you want to use the directive with different scopes..
<div>
<my-chart chart="chart"></my-chart>
</div>
angular.module('app.directive')
.directive('myChart', function () {
return {
controller: "XYZController",
bindToController: true
template: '<div></div>',
scope: {
chart: '='
}
})
.controller('XYZController', function($scope){
$scope.$watch('chart', function (chart) { });
});
scope: true will create a scope that is prototypically inerited from the parent scope, so you should be able to $watch a property on the parent scope.
angular.module('app.directive')
.directive('myChart', function () {
return {
template: '<div></div>',
scope: true,
link: function(scope){
scope.$watch('chart', function (chart) { });
});

AngularJS : Nested Directives and Scope Inheritance

preamble: It seems like this question has been asked and answered before, but I cannot seem to get it working, so if my question boils down to "can you debug my code?", I apologize.
I would like to write the following code:
<radio-set ng-model="obj.prop" name="obj_prop">
<radio-set-button ng-value="'public'">Public</radio-set-button>
<radio-set-button ng-value="'protected'">Protected</radio-set-button>
<radio-set-button ng-value="'private'">Private</radio-set-button>
</radio-set>
This renders a bunch of radio buttons and labels which need to populate whatever is passed to the <radio-set>'s ngModel. I'm missing something scope-related.
.directive("radioSet", function () {
return {
restrict: 'E',
replace: true,
scope: {
ngModel: '=?',
ngChange: '&',
name: '#'
},
transclude: true,
template: '<div class="radio-set" ng-transclude></div>',
controller: function () {}
};
})
.directive("radioSetButton", function () {
return {
restrict: 'E',
replace: true,
require: ['^radioSet', '?ngModel'],
scope: {
ngModel: '=?', // provided by ^radioSet?
ngValue: '=?',
ngChange: '&', // provided by ^radioSet?
name: '#' // provided by ^radioSet?
},
transclude: true,
link: function (scope, element, attr) {
element.children().eq(0).attr("name", scope.name); // scope.name is null
},
template: '<label class="radio-set-button">' +
'<input type="radio" name="name" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
'<div class="radio-content" ng-transclude></div>' +
'</label>'
};
})
Both the parent and child directives need their own scope definition, but it is unclear to me how to access to the radioSet's scope from within radioSetButton.
thanks for the help.
fiddle: http://jsfiddle.net/pmn4/XH5K2/2/
Transclusion
I guess i have to tell you that the transclusion you used in your directive does not work as you expect because in short: The transcluded directive doesn't inherit the scope you'd expect, actually it inherits the scope of the outer controller, but there are plenty of answers on this topic:
Access Parent Scope in Transcluded Directive
How to solve
To access a parents directive there are basically two ways:
1.) Require the parents directive's controller and create some API to do stuff on the parent
2.) Use the fifth parameter of the link function to access the transclude function, here you could change the injected scope and you could set it to the parents directive scope
Since the first solution is more intuitive i will go with this one:
On the radioSetdirective i set up a bidirectional databinding to the object in my Controller and i create a getter and setter method to interact with the value.
In the "child"'s directive i require the parent directive's controller which i get passed as the fourth parameter in my link function. I setup a click handler on the element to get the click and here i call the parents setter method with my value. To visualize the current selected object i add an ng-class directive which conditionally adds the active class.
Note: This way you can use the ngModel directive as well. It has an API to interact with the model.
The second solution uses the transclude function which you can use to pass in a scope. As i dont have time right now and as it adds more complexity i'd recommend using the first solution.
Recommendation
For your example transclusion might not be the right choice, use one directive and add the choices to the template or pass them into the directive. As i dont know what your intentions are i provided this solution. (I didn't know what the purpose of this name property is?)
The Code
Fiddle: http://jsfiddle.net/q3nUk/
Boilerplate:
var app = angular.module('myApp', []);
app.controller('MainController', function($scope) {
$scope.object = {
'property' : 'public'
};
});
The Directives:
app.directive('radioSet', function() {
return {
scope : {
radioValue : '='
},
restrict : 'E',
transclude : true,
replace : true,
template : '<div class="radioSet" ng-transclude></div>',
controller : function($scope) {
this.getRadioValue = function() {
return $scope.radioValue;
}
this.setRadioValue = function(val) {
$scope.$apply(function() {
$scope.radioValue = val
});
}
}
};
});
app.directive('radioSetButton', function() {
return {
restrict : 'E',
transclude : true,
replace : true,
scope : true,
template : '<div class="radioSetButton" ng-class="{active:isActive()}" ng-transclude></div>',
require : '^radioSet',
link : function(scope, elem, attrs, radioSetController, transclude) {
scope.isActive = function() {
return attrs.buttonValue === radioSetController.getRadioValue();
};
elem.on('click', function() {
radioSetController.setRadioValue(attrs.buttonValue);
});
}
};
});
The HTML:
<html>
<body ng-app="myApp">
<div ng-controller="MainController">
<p>{{ object.property }}</p>
<radio-set radio-value="object.property">
<radio-set-button button-value="public">Public</radio-set-button>
<radio-set-button button-value="private">Private</radio-set-button>
<radio-set-button button-value="protected">Protected</radio-set-button>
</radio-set>
</div>
</body>
</html>
CSS:
.radioSetButton {
display : block;
padding : 10px;
border : 1px solid black;
float : left;
}
.radioSetButton:hover {
cursor : pointer;
}
.radioSetButton.active {
background-color : grey;
}

Categories

Resources