I'm trying to bind some values in an angular 1.6 component that should be available to the controller code.
I must be misunderstanding it, but the variables aren't available when the controller runs. The only way I've managed it is by putting a $timeout in to push the code into the next digest cycle.
What am I doing wrong here?
The relevant section is here:
var SelectorCtrl = ['$scope', '$http', '$timeout',
function ($scope, $http, $timeout) {
var self = this;
alert("1: " + self.hierarchyId);
// I'm not 100% sure why this has to be in the next digest cycle
$timeout(function(){
$scope.categories = self.categories;
alert("2: " + self.hierarchyId);
});
}
app.component('categorySelector', {
templateUrl: 'categorySelector.html',
controller: SelectorCtrl,
bindings: {
hierarchyId: "#",
disabled: "=",
categories: "=",
onSelectionChanged: "&"
}
});
See plunker: https://plnkr.co/edit/8rtDuCawdHaiXzQU5VBR
This is because of $compileProvider.preAssignBindingsEnabled(flag) introduced in Angular 1.6, you can configure it on config cycle on $compileProvider
If disabled (false), the compiler calls the constructor first before
assigning bindings.
The default value is true in Angular 1.5.x but will switch to false in
Angular 1.6.x.
You will get all bindings inside $onInit lifecycle event of Angular component, where all the bindings would be available(if binding passed synchronously).
self.$onInit = function() {
$scope.categories = self.categories;
alert("2: " + self.hierarchyId);
};
Note: It's bad practice to mix $scope with this. Rather avoid using $scope to make your code Angular 2 migration proof.
If you want binding to be available when controller function instantiate then you could set $compileProvider.preAssignBindingsEnabled(true). Which will make self.categories(bindings) value.
app.config(function($compileProvider){
$compileProvider.preAssignBindingsEnabled(true)
});
Similar answer
Angular 1.7.x update
As of Angular 1.7.x, the $compileProvider.preAssignBindingsEnabled(flag) is gone, and it is no longer possible to assign bindings before the constructor.
To work around this, you need to define a link function in your directive definition. You can make this function call a method on your controller like this:
app.directive("directive", function() {
return {
controller: DirectiveController, // bind controller any way you want
controllerAs: "ctrl",
bindToController: true,
link: function(scope) {
scope.ctrl.init(); // this will call an init() function on your controller
}
}
});
Related
I am using directives to create a component library in AngularJS 1.5. Hence, my directives need to have isolate scopes.
Some of my directives have callbacks so you can pass in a function to get invoked by the directive. However, when that callback is invoked by the directive, it doesn't seem like the changes to $scope attributes are fully updated like I would expect them to be.
Here is a Plunker that shows this behavior:
http://embed.plnkr.co/Rg15FHtHgCDExxOYNwNa/
Here is what the code looks like:
<script>
var app = angular.module('myApp', []);
app.controller('Controller', ['$scope',function($scope) {
// initialize the value to something obvious
$scope.clickersValue = "BEFORE";
// when this call back is called we would expect the value to be updated by updated by the directive
$scope.clickersCallback = function() {
//$scope.$apply(); // $apply is not allowed here
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
};
}
]);
app.directive('clicker', [function() {
return {
restrict: 'EA',
template: '<div ng-click="clicked()">click me!</div>',
controller: ['$scope', function($scope) {
$scope.clicked = function() {
console.log("you clicked me.");
$scope.newValue = 'VALID';
$scope.myUpdate();
}
}],
scope: {
"newValue": "=",
"myUpdate": "&"
}
};
}]);
</script>
So when clickersCallback gets invoked the clickersValue attribute still has the old value. I have tried using $scope.$apply but of course it isn't allowed when another update is happening. I also tried using controller_bind but got the same effect.
Wrap the code inside clickersCallback function in a $timeout function.
$timeout(function() {
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
});
Updated plunker
The $timeout does not generate error like „$digest already in progress“ because $timeout tells Angular that after the current cycle, there is a timeout waiting and this way it ensures that there will not any collisions between digest cycles and thus output of $timeout will execute on a new $digest cycle.
source
Edit 1: As the OP said below, the user of the directive should not have to write any "special" code in his callback function.
To achieve this behavior I changed the $timeout from de controller to the directive.
Controller callback function (without changes):
$scope.clickersCallback = function() {
$scope.clickersValueRightAfterCall = $scope.clickersValue;
console.log("clickersCallback: scope.clickersValue", $scope.clickersValue);
};
Directive code (inject $timeout in the directive):
$scope.clicked = function() {
console.log("you clicked me.");
$scope.newValue = 'VALID';
$timeout(function() {
$scope.myUpdate();
});
}
Updated plunker
I'm pretty new with angular and I've read a lot of threads here and googled this topic but I cannot get a clear answer. what i am really trying to achieve is. lets suppose I have a controller A, this is a actual source for data. I passed it to one directive through binding it to a HTML. From this directive I am acually getting the source at another controller.
So I need to find out the way where I can change the data of controller when the data of controller A gets changed.
Controller A
angular.module('page.leadAndOpportunity.ctrl', []).controller('LeadAndOpportunityController', ['$scope', '$rootScope', '$timeout', function ($scope, $rootScope, $timeout, leadAndOpportunityService) {
$scope.selectDataSource = function (condition) {
var dataSource = [];
var dataSource = $scope.leadsDataSource.filter(function (item) {
return item.typeName === condition;
});
$scope.leadsDataSource = [];
$scope.leadsDataSource = dataSource;
console.log($scope.leadsDataSource);
}
}]);
HTML
<ng-senab-grid datasource="{{ leadsDataSource }}" defaultdata="{{defaultColumns}}" skipdata="{{ skipColumns }}" enablepersonalisation="true"></ng-senab-grid>
Directive
angular.module('page.gridView.drct', []).directive("ngSenabGrid", ["$rootScope", function ($rootScope) {
return {
restrict: "E",
templateUrl: "pages/gridView/page.gridView.tpl.html",
scope: {
enablePersonalisation: "#enablepersonalisation",
datasource: "#datasource",
defaultdata: "#defaultdata",
skipdata: "#skipdata"
},
}
}]
);
Controller B
var _datasource = JSON.parse($scope.datasource);
//rest the data follows
So when $scope.leadsDataSource gets changes on Controller A, then the
var _datasource = JSON.parse($scope.datasource);
also should get changed
I dont know if it is possible or not. But I need to change the data
Thanks in advance
remove the curly brackets of the variable.since this is a directive no need to add curly brackets
<ng-senab-grid datasource="leadsDataSource" defaultdata="defaultColumns" skipdata="skipColumns" enablepersonalisation="true"></ng-senab-grid>
if u want to get the value of the variable then use "=" if u use "&" it will only get the string
scope: {
enablePersonalisation: "=enablepersonalisation",
datasource: "=datasource",
defaultdata: "=defaultdata",
skipdata: "=skipdata"
},
also inject the directive module to ur angular module
angular.module('page.leadAndOpportunity.ctrl', ['page.gridView.drct'])
A simple explanation to keep in mind about different types of scopes would be below.
# Attribute string binding (String)
= Two-way model binding (model)
& Callback method binding (method)
According this you should be using Two-way binding instead of Attribute string binding because The model in parent scope is linked to the model in the directive's isolated scope. Changes to one model affects the other, and vice versa.
I would prefer using bindToController property definition in the directive. When set to true in a directive with isolated scope that uses controllerAs, the component’s properties are bound to the controller rather than to the scope.
That means, Angular makes sure that, when the controller is instantiated, the initial values of the isolated scope bindings are available on this, and future changes are also automatically available.
Check the Below sample fiddle example for more understanding
var myApp = angular.module('myApp', []);
myApp.controller('MyController', function($scope) {
$scope.change = function() {
$scope.fullname = 'Keshan';
}
$scope.reset = function() {
$scope.fullname = 'Fill Your Name';
}
});
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
name: '='
},
controller: function($scope) {
this.name = 'Fill Your Name';
},
controllerAs: 'ctrl',
bindToController: true,
template: '{{ctrl.name}}',
};
});
<script src="https://code.angularjs.org/1.3.7/angular.js"></script>
<div ng-app="myApp" ng-controller="MyController">
<button ng-click="change()">Change</button>
<button ng-click="reset()">Reset</button>
<my-directive name="fullname"></my-directive>
</div>
Further Reading
everybody. I am new to AngularJS and find it very interesting, but I am a bit unclear about the following situation.
app.controller("myCtrl", ['$scope', '$http', '$filter', function ($scope, http, filter)
{
$http({
method: CTHocUri,
url: 'get',
async: true,
}).then(function (response) {
$scope.CTH = response.data; //response.data=two Object
})
}])
app.directive("myCustom1",['$http', function ($compile,$http) {
return {
link: function (scope, element, attr) {
console.log(scope.CTH); // I can't get... scope.CTH=undefined
}
}])
I can't get value scope.CTH. ??
There is a VERY simple way to SEE what the issue is:
In your html, merely surround your directive with an ng-if conditional based on CTH:
<span ng-if="CTH">
<my-custom-1></my-custom-1>
</span>
That's it.
What this does is that your directive will only be born/instantiated when CTH is set to non-null/non-undefined, i.e. when $http returns asynchronously. With this, your code will work. As such, there is no need for watching or broadcasting for this type of simple serialization of asynchronous events when you can simply leverage Angular's built-in '$watch's.
NOTE 1: I do not know what your architecture is and am not suggesting what you need to do. I am merely showing you why your code won't work and how you have been caught in a simple asynchronicity trap.
NOTE 2: I assume your directive is 'as -is'. In other words you have access to the parent's scope (i.e. the controller's scope). If your directive's scope were isolated (i.e. you had a scope:{..(attrs)..} defined in the directive) you will not have 'simple' access to the parent scope. Your code will be different--eg you can pass bits and pieces of your scope to the directive attrs. However, the ng-if will still work since it is on the controller's scope.
I hope this helps.
The directive and the controller are two completely different entities. If it helps you can think of them as different classes. They will not share the same scope.
You could create an isolated scope on the directive and pass the CTH variable into it. Conceptually something like this:
app.directive("myCustom1",['$http', function ($compile,$http) {
return {
scope { cth : "=" },
link: function (scope, element, attr) {
console.log(scope.cth);
}
Then in your HTML, do something like this:
<div ng-controller="myCtrl">
<my-Custom1 cth="CTH">
</div>
when the directive initializes, the scope.CTH is still not initialized since its initialization accurses inside an $http call.
one way to overcome this is to broadcast and event from the controller and catch it from inside the directive. see this plnkr and angularjs scope's docs
app.controller('MainCtrl', function($scope, $timeout) {
$scope.name = 'World';
$timeout(function() {
$scope.test = "test";
$scope.$broadcast('MyEvent')
}, 500);
});
app.directive('test', function() {
return {
link: function(scope, elm, attr) {
scope.$on('MyEvent', function() {
console.log(scope.test);
})
}
}
})
I have a directive with require property:
require: '^testBox'
now I want to get testBox controller inside controller of my directive. How should I do it?
I was trying to do so:
controller: function(){
this.testBox.user
}
but looks like it does not work.
It's clear for me how to get required controller inside link function. But is there a way to get it inside controller without using link?
Code on plunker.
This is still an open issue. So at the moment you can not just inject the required controller into your directive controller. I have updated your Plunker. It's definitely a bit hacky but the problem is; You cannot expose the TextBoxCtrl to the UserCtrl in either the pre or post link function because the controller gets executed first. So my idea is to use a watcher to observe a scope varibale called textBox. Once the value is defined I declare a variable on the UserCtrl and remove the watcher. Now you can simply use it in your template like so:
{{ user.textBox.name }}
Here is the code for the link function and the controller of the user directive:
link: function($scope, $element, $attrs, ctrl) {
$scope.textBox = ctrl
},
controller: function($scope) {
var vm = this;
var watcher = $scope.$watch('textBox', function(newVal) {
if(newVal) {
vm.textBox = newVal;
watcher();
}
});
}
However, you can also go with a link function instead. The required controller will be injected as the fourth parameter.
When you use controllerAs it's just added as a property of the underlying scope object (using the name you've defined). Knowing this, you can attach the parent controller instance as a property of your child controller instance as follows:
function exampleDirective() {
return {
require: '^testBox',
link: function (scope, element, attrs, testBox) {
scope.example.testBox = testBox;
},
controllerAs: 'example',
controller: function() {
// silly example, but you get the idea!
this.user = this.testBox.user;
}
}
};
I have the following directive:
offerListSorters.directive('offersSorter', ['myState', '$templateCache', function (myState, $templateCache){
return {
scope: {},
controller: function($scope, $element, $attrs, $transclude) {
[...]
},
restrict: 'E',
//templateUrl: 'partials/offersSorterDirective.html',
template: $templateCache.get('partials/offersSorterDirective.html'),
replace: true,
transclude: true
};
}]);
And I use Karma + Jasmine to test this code and it works. But now if I switch to the templateUrl (currently commented out), it doesn't work. I've created a simple Plunker to show this issue. When you compare sorter and bsorter directives, it looks as if the isolateScope() call on the compiled element breaks when I use templateUrl instead of template. Any ideas?
It's the weirdest thing, I think this is actually a bug where if you're using a templateUrl, you don't get an isolated scope. The template itself is loaded correctly, but the scope is never loaded. I've updated the plnkr with some additional logging, checkout out the console and you'll see what I mean, bsorter doesn't get the ng-isolate-scope class and the scope is returned as undefined.
Edit:
I've updated the plnkr further to log a console message when the compile function is called. This is pretty much the limit of my javascript/angularJS knowledge, but the bsorter compile function is logged as being called after the scope should be returned, unlike the sorter compile function which is called before.
You should call isolateScope() in the it() function, not in beforeEach().
describe("Testing...", function(){
var element, isoScope;
beforeEach(function(){
module("myApp");
inject(function($rootScope, $compile){
var scope = $rootScope.$new();
element = $compile(angular.element("<sorter></sorter>"))(scope);
// isoScope = element.isolateScope(); <- Move from here
$rootScope.$digest();
});
});
it("something...", function(){
isoScope = element.isolateScope(); // <- to here
expect(isoScope.someProp).toBe("someValue");
});
});
You have to call $rootScope.$digest() after creating the directive via $compile, then it should work (right now you're calling the $digest() on the on your variable parentScope with is a new scope created with $rootScope.$new())