I'm new to Angular. I'm trying to use components (1.6). In the parent, I have an $http.get that gets data from a service and then assigns the response to a $scope variable. That scope variable is passed to a child component using one-way binding <. In the JavaScript, if I alert the variable passed in, I get "undefined", however, the html template in the child does show the variable. It's like there is a race condition happening and I don't know how to tell it to wait until the data from the service is loaded.
In my parent.js:
(function (angular) {
'use strict';
$http.get("http://localhost:52422/api/PayOffYourCc")
.then(function mySucces(response) {
$scope.baseline = response.data;
}
,
function myError(respone) {
$scope.baseline = response.statusText;
}
);
})(window.angular);
In my parent HTML template:
<thermometer baseline="baseline"></thermometer>
In my child component:
(function (angular) {
'use strict';
function drawChart(baselineVal) {
alert(baselineVal);
}
function ThermometerController($scope) {
var ctrl = this;
ctrl.$onInit = function () {
drawChart(ctrl.baseline);
};
}
angular.module('payOffYourCcApp').component('thermometer', {
templateUrl: '../PayOffYourCC/partials/thermometer.html',
transclude: true,
controller: ThermometerController,
bindings: {
baseline: '<'
}
});
})(window.angular);
In my child html template:
<div>
baseline:{{$ctrl.baseline}}
</div>
In the html, {{$ctrl.baseline}} is displayed fine, but when I alert it in the .js, it's undefined. Why is that? How can I make sure the {{$ctrl.baseline}} is in scope before the javascript loads?
Use the $onChanges life-cycle hook:
function ThermometerController($scope) {
var ctrl = this;
/* REPLACE THIS
ctrl.$onInit = function () {
drawChart(ctrl.baseline);
}; */
// WITH THIS
ctrl.$onChanges = function (changesObj) {
if (changesObj.baseline && changesObj.baseline.currentValue) {
drawChart(changesObj.baseline.currentValue);
};
};
}
The controller needs to wait for the data to come from the server. By using the $onChanges life-cycle hook, the drawChart function will be called when the data becomes available and will be called on subsequent updates.
For more information, see AngularJS Comprehensive Directive API Reference - Life-Cycle Hooks.
With Angular component, you should privilege communication from the child back to the parent since it allows a very cheap binding (&)
You can communicate from the parent to the child but it is more expensive (=).
I will give you an example on how to do it.
It is a not tested solution but you should have the idea.
You should change your parent to have a child api to transmit the data :
JS Parent :
(function (angular) {
'use strict';
$http.get("http://localhost:52422/api/PayOffYourCc")
.then(function mySucces(response) {
$scope.baseline = response.data;
$ctrl.apiChild.transmit(response.data);
}
,
function myError(respone) {
$scope.baseline = response.statusText;
}
);
})(window.angular);
HTML Parent :
<thermometer api="$ctrl.apiChild"></thermometer>
Change your child to have a function to receive the data from the parent and also change the binding to "=" :
JS Child :
(function (angular) {
'use strict';
function drawChart(baselineVal) {
alert(baselineVal);
}
function ThermometerController($scope) {
var ctrl = this;
ctrl.$onInit = function () {
drawChart(ctrl.baseline);
ctrl.api = {};
ctrl.api.transmit = ctrl.transmitData;
};
this.transmitData = function transmitData(data){
// here you get the data from the parent to the child
}
}
angular.module('payOffYourCcApp').component('thermometer', {
templateUrl: '../PayOffYourCC/partials/thermometer.html',
transclude: true,
controller: ThermometerController,
bindings: {
api : '='
}
});
})(window.angular);
This is the result of the fact that $http requests are asynchronous. The alert that happens when the child component initializes prints undefined, because at that instance the $http request that is retrieving the data has not returned yet. Since you're using > binding however, the template in your child component will update with the correct value as soon as the request resolves (which is pretty darn fast), so you don't ever see undefined actually printed in the template. In fact, I don't think angular will print undefined, I think it's just blank. So to your eye, it looks like it has the right value right away, when, in reality, it was momentarily undefined while the $http request was resolving.
Related
I am currently using Angular 1.5. I am using ui-router as my primary navigation mechanism. I am leveraging Angular components.
I understand that I can use .resolve on my states to instantiate services which are then passed down through my component hierarchy (mostly using one-way bindings).
One of my components is called literatureList and is used in more than one route/state. The literatureList component makes use of a specific service called literatureListService. literatureListService is only used by literatureList. literatureListService takes a while to instantiate, and uses promises etc.
In each of the .state definitions then I need to have a .resolve that instantiates literatureListService. This means that I need to refer to this literatureListService in each of the .state.resolve objects. This doesn't seem very DRY to me.
What I'd really like to do is remove the literatureListService references from the .state.resolve objects and 'resolve' the service from 'within' the literatureList component itself.
How do I code a 'resolve-style' mechanism within the literatureList component that will handle the async/promise nature of literatureListService? What is best practice for doing this?
Code snippets follow:
state snippets:
$stateProvider.state({
name: 'oxygen',
url: '/oxygen',
views: {
'spofroot': { template: '<oxygen booklist="$resolve.literatureListSvc"></oxygen>' }
},
resolve:{
literatureListSvc: function(literatureListService){
return literatureListService.getLiterature();
}
}
});
$stateProvider.state({
name: 'radium',
url: '/radium',
views: {
'spofroot': { template: '<radium booklist="$resolve.literatureListSvc"></radium>' }
},
resolve:{
literatureListSvc: function(literatureListService){
return literatureListService.getLiterature();
}
}
});
literatureListService:
angular.module('literature')
.factory('literatureListService',function($http,modelService){
// Remember that a factory returns an object, whereas a service is a constructor function that will be called with 'new'. See this for a discussion on the difference: http://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html
console.log('literatureListService factory is instantiating - this will only happen once for each full-page refresh');
// This is a factory, and therefore needs to return an object containing all the properties that we want to make available
var returnObject = {}; // Because this is a factory we will need to return a fully-formed object (if it was a service we would simply set properties on 'this' because the 'context' for the function would already have been set to an empty object
console.log('service instantiation reporting that modelManager.isDataDirty='+modelService.isDataDirty);
// The getLiterature method returns a promise, and therefore can only be consumed via a promise-based mechanism
returnObject.getLiterature = function($stateParams){
console.log('literatureService.getLiterature will now return a promise (via a call to $http)');
return $http({method: 'GET', url: 'http://localhost:3000/literature/'});
};
return returnObject;
});
oxygen component html:
<div>
This is the OXYGEN component which will now render a literature list, passing in bookList.data as books
<literature-list books="$ctrl.booklist.data"></literature-list>
</div>
oxygen component js
angular.module('frameworks')
.component('oxygen',{
templateUrl:"frontend/framework/frameworks/oxygenComponent.html",
controller:function($http){
var $ctrl = this;
console.log('Hello from the oxygen component controller with literatureListSvc='+$ctrl.booklist); // Bound objects NOT YET AVAILABLE!!!!!!
this.$onInit = function() {
//REMEMBER!!!! - the bound objects being passed into this component/controller are NOT available until just before the $onInit event fires
console.log('Hello from the oxygen component controller onInit event with bookList='+JSON.stringify($ctrl.booklist));
};
}
,bindings:{ // remember to make these lowercase!!!
booklist:'<'
}
});
literatureList component html:
<div>
{{$ctrl.narrative}}
<literature-line ng-repeat="literatureItem in $ctrl.books" wizard="fifteen" book="literatureItem" on-tut="$ctrl.updateItemViaParent(itm)">555 Repeat info={{literatureItem.title}}</literature-line>
</div>
literatureList component js
angular.module('literature')
.component('literatureList',{
templateUrl:'frontend/literature/literatureListComponent.html',
//template:'<br/>Template here33 {{$ctrl.listLocalV}} wtfff',
// controller:function(literatureListService){
controller:function(){//literatureListService){
var $ctrl=this;
this.narrative = "Narrative will unfold here";
this.updateItemViaParent = function(book){
this.narrative = 'just got notified of change to book:'+JSON.stringify(book);
};
this.$onInit = function(){
console.log('literatureList controller $onInit firing with books='+JSON.stringify($ctrl.books));
};
this.$onChanges = function(){
console.log('literatureList controller $onChanges firing');
};
},
bindings: {
books:'<'
}
});
As JavaScript in reference based, you can crete object in your service and access it in all three controllers that you need.
For Example:
function serviceA() {
var vm = this;
vm.testObject = {};
vm.promise1().then(function(response) {
vm.testObject = response;
})
}
function ControllerA($scope, serviceA) {
$scope.testA = service.testObject;
}
In this case, as soon as the promise is resolved, all the controllers will get the value of the response and can be used in the partials respecively
I have small problem to solve.
I have modal controller rejectIssueModalCtrl.js
(function () {
'use strict';
function rejectIssueModalCtrl($modalInstance, issue, $rootScope) {
var self = this;
self.cancel = function () {
$modalInstance.dismiss('cancel');
};
self.reject = function ($rootScope) {
$modalInstance.close(self.reason);
console.log(self.reason);
};
$rootScope.reasono = self.reason;
}
rejectIssueModalCtrl.$inject = ['$modalInstance', 'issue', '$rootScope'];
angular
.module('app')
.controller('rejectIssueModalCtrl', rejectIssueModalCtrl);
})();
When I click the button I can open this modal and write a reason. I want to show this reject reason in table in other controller.
Here's my code from other controller issueDetailsCtrl.js
$scope.reasonoo = $rootScope.reasono;
function rejectIssue() {
var rejectModal = $modal.open({
templateUrl: '/App/Issue/rejectIssueModal',
controller: 'rejectIssueModalCtrl',
controllerAs: 'rejectModal',
size: 'lg',
resolve: {
issue: self.issueData
}
});
rejectModal.result.then(function (reason) {
issueSvc
.rejectIssue(self.issueData.id, reason)
.then(function (issueStatus) {
changeIssueStatus(issueStatus.code);
getIssue();
}, requestFailed);
});
};
and html code
<div>
<span class="right" ng-bind="$root.reasono"></span>
</div>
As you can see I tried to use $rootScope. I can console.log the reason but I cant make it to show in this html. Any help?
We're missing some context, but I believe this is your problem:
self.reject = function ($rootScope) {
$modalInstance.close(self.reason);
console.log(self.reason);
};
$rootScope.reasono = self.reason;
Assuming self.reason is bound to the input in your modal, it won't be defined outside of the reject callback - that's the nature of async. You're able to log to console because you're doing so within the callback.
Define $rootScope.reasono inside of the callback like so:
self.reject = function () {
$modalInstance.close(self.reason);
console.log(self.reason);
$rootScope.reasono = self.reason;
};
Edited to show that $rootScope should be removed as a named parameter in the reject function definition.
Using root scope is not recommended. For this reason it is recommended to create a service for intercommuncation with variable to store reject reason, then inject this service for each controller - that way you will be able to read/write reason from different controllers.
I'm trying to figure out the "preferred" or "angular-way" of sharing properties or state between controllers/directives. There are several methods to implement this, but I want to keep with best-practice. Below are some banal examples of how this can be implemented:
1. Using $scope.$watch
// The parent controller/scope
angular.module('myModule').controller('parentController', ['$scope', function($scope) {
$scope.state = {
myProperty: 'someState'; // Default value to be changed by some DOM element
};
}]);
// The child controller/scope.
angular.module('myModule').controller('childController', ['$scope', function($scope) {
$scope.$watch('state.myProperty', function (newVal) {
// Do some action here on state change
});
}]);
Edit: Based on answers below, this is bad practice and should be avoided. It is untestable and places an unwanted DOM dependancy.
2. Using $broadcast
// The parent controller
angular.module('myModule').controller('parentController', ['$scope', function($scope) {
var myProperty = 'someState';
$scope.setState = function (state) {
myProperty = state; // Set by some other controller action or DOM interaction.
$scope.$broadcast('stateChanged', state); // Communicate changes to child controller
}
}]);
// The child controller.
angular.module('myModule').controller('childController', ['$scope', function($scope) {
$scope.$on('stateChanged', function (evt, state) {
// Do some action here
}
}]);
Edit: Equally bad practice as you need to know the placement of the controllers in the DOM in order to determine weather to use $broadcast (down the DOM) or $emit (up the DOM).
3. Using service
angular.module('myModule').factory('stateContainer', [function () {
var state = {
myProperty: 'defaultState'
},
listeners = [];
return {
setState: function (newState) {
state.myProperty = newState;
angular.forEach(listeners, function (listener) {
listener(newState);
});
},
addListener: function (listener) {
listeners.push(listener);
}
}
}]);
// The parent controller
angular.module('myModule').controller('parentController', ['$scope', 'stateContainer', function($scope, stateContainer) {
$scope.setState = function (state) {
stateContainer.setState(state);
};
}]);
// The child controller.
angular.module('myModule').controller('childController', ['$scope', 'stateContainer', function($scope, stateContainer) {
stateContainer.addListener(function (newState) {
// Do some action here
});
}]);
There are probably some approaches I've missed here, but you get the idea. I'm trying to find the best approach. Although verbose, I personally lean towards #3 in the list here. But I come from a Java and jQuery background where listeners are widely used.
Edit: Answers below are insightful. One talks of sharing state between parent/child directives using the require directive configuration. The other talks of sharing service or service properties directly to the scope. I believe that depending on the need, they are both right in what is or is not best practice in Angular.
Any of these will work if done correctly, but a variant on service is the preferred way AFAIK.
The question is, do you even need a listener in the service case? Angular itself will update any views (which is the purpose of the controller), so why do you need a listener or watch? It is sufficient to change the value itself for the view to be changed.
app.factory('stateService',function() {
return {
myState: "foo"
}
})
.controller('one',function($scope,stateService) {
$scope.changeState = function() {
stateService.myState = $scope.state;
};
})
.controller('two',function($scope,stateService) {
$scope.svc = stateService;
})
You can then do the following in your view (incomplete):
<div ng-controller="one">
<input name="state" ng-model="state"></input>
<button type="submit" ng-click="changeState()">Submit</button>
</div>
<div ng-controller="two">{{svc.myState}}</div>
Truth is, you don't even need to go that far with having a button and a function. If you just tie the ng-model together it will work:
<div ng-controller="one">
<input name="state" ng-model="svc.myState"></input>
</div>
<div ng-controller="two">{{svc.myState}}</div>
Try the following jsfiddle http://jsfiddle.net/cwt9L6vn/1/
There is no such thing as parent and child controllers in AngularJS. There are only parent and child directives, but not controllers. A directive can have a controller that it exposes as an API to other directives.
Controllers are not related to the DOM hierarchy so they can't have children. They also don't create their own scope. So you never know if you have to $broadcast or $emit to talk to other controllers.
If you start using $broadcast from a controller, then you're going to get stuck not knowing if the other controller is up or down. That's when people start doing stuff like $rootScope.$broadcast(..) which is a very bad practice.
What you are looking for are directives that require other directives.
var app = angular.modeul('myApp',[]);
// use a directive to define a parent controller
app.directive('parentDir',function() {
return {
controller: function($scope) {
this.myFoo = function() {
alert("Hello World");
}
}
});
// use a directive to enforce parent-child relationship
app.directive('childDir',function() {
return {
require: '^parentDir',
link: function($scope, $el, $attr, parentCtrl) {
// call the parent controller
parentCtrl.myFoo();
}
});
Using the require feature of a directive does two important things.
Angular will enforce the relationship if it's not optional.
The parent controller is injected into the child link function.
There is no need to $broadcast or $emit.
Another option that is also effective is to use directives to expose an API.
// this directive uses an API
app.directive('myDir',function() {
return {
scope: {
'foo': '&'
},
link: function($scope, $el, $attr) {
// when needed, call the API
$scope.foo();
}
});
// in the template
<div ng-controller="parentController">
<div my-dir foo="parentController.callMyMethod();"></div>
</div>
At what stage of the compile / link process are variables from the isolated scope of a directive bound to the parent (controller) scope? I have an application in which I want to call a directive api automatically, as soon as the view is loaded.
I understood that scope binding happens in the directive linking phase, so that post linking, the variables exposed on the isolated scope should be available on the parent scope.
However, I find that this is not the case, as demonstrated in the code below (plunker here).
//plunker code
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.buttonClick = function() {
console.log("In buttonClick function, call is: " + this.call);
this.call();
}
$scope.$on("LinkComplete", function(event) {
console.log("In LinkComplete, call is: " + event.currentScope.call);
//event.currentScope.call();
});
console.log("In Constructor, call is: " + this.call);
})
.directive('myDirective', function(){
return {
scope: {
myMethod: '='
},
controller: function ($scope) {
$scope.myMethod = function() {
alert("method called");
};
},
link: function postLink(scope)
{
scope.$emit("LinkComplete");
}
};
});
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<div my-directive my-method="call"></div>
<button ng-click="buttonClick()">Call</button>
</body>
Note that the code attempts to access the linked variable (which points to a method on the directive controller) twice during the view initialisation, and on both occasions, the variable is undefined. I wouldn't expect the variable to be available during the main controller constructor, but I would expect it to be available during the post-link event handler. Once the view is loaded, the bound variables are available (click Call button to witness).
How can I access the bound variables from the controller, without requiring the user to click on a button or the like?
That's a good question, when you see some words like 'x is y in z stage' you need to be careful on the accuracy, always dig into the source code to prove it.
Your plunker is using v1.2.27, checkout this line:
https://github.com/angular/angular.js/blob/v1.2.27/src/ng/compile.js#L1492
isolateScope.$watch(function parentValueWatch() {
var parentValue = parentGet(scope);
if (!compare(parentValue, isolateScope[scopeName])) {
// we are out of sync and need to copy
if (!compare(parentValue, lastValue)) {
// parent changed and it has precedence
isolateScope[scopeName] = parentValue;
} else {
// if the parent can be assigned then do so
parentSet(scope, parentValue = isolateScope[scopeName]);
}
}
return lastValue = parentValue;
}, null, parentGet.literal);
This will be evaluated in next $digest cycle and by then parentScope.call will be assigned. At the same time, postLink function is executed synchronously right below it:
https://github.com/angular/angular.js/blob/v1.2.27/src/ng/compile.js#L1575
// POSTLINKING
for (i = postLinkFns.length - 1; i >= 0; i--) {
try {
linkFn = postLinkFns[i];
linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn);
} catch (e) {
$exceptionHandler(e, startingTag($element));
}
}
After postLink has been executed, controller got the event but parentScope.call has not been initialized yet via $digest.
So if you add a setTimeout to check, it looks like what you want:
$scope.$on("LinkComplete", function(event) {
setTimeout(function () {
console.log("In LinkComplete, call is: " + event.currentScope.call);
//event.currentScope.call();
});
});
Cannot call method 'push' of undefined
I receive that error when my AngularJS runs the following:
$scope.ok = function () {
$modalInstance.close();
$scope.key.push({ title: '', gps:'', desc:''});
};
I declared my $scope.key = []; right after my .controller as I need to be able to use the $scope.key in other parts of the application. Could someone please point out where I should be declaring this?
$scope.ok is my Save Button which pulls the data from my input fields and $scope.plotmarkers is what I am using to pull the data from the inputs that have been pushed.
app.controller('MenuSideController', ['$scope','$modal','$log', function($scope, $modal, $log) {
$scope.key = [];
$scope.createmarker = function () {
var modalInstance = $modal.open({
templateUrl: 'template/modal-add-marker.html',
controller: ModalInstanceCtrl,
resolve: {}
});
modalInstance.result.then(function (selectedItem) {
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
var ModalInstanceCtrl = function ($scope, $modalInstance) {
$scope.ok = function () {
$modalInstance.close();
$scope.key.push({ title: '', gps:'', desc:''});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
};
$scope.plotmarkers = function() {
console.log($scope.key);
};
}]);
Don't forget to pass a scope to $modal.open! If you don't, it will default to the root scope, which is not a child of the controller's scope, so key is not defined on it or its parents. You can use { scope: $scope.$new(), ... } as a parameter of $model.open to pass it a child of the controller's scope. See the docs for details. Good luck!
I think you are using $modal a bit improperly.
So you have two controller here - one for the application logic and one for the modal window itself. According to best practice you shouldn't interact between different controllers directly (the case with parent-child directive is exclusion but honestly speaking it not direct interaction - rather your linker function used the controller from parent directive). Instead of interaction between controller in general we should use services. It is just additional note.
What is related to your question - you have two things here to keep in mind:
if you want to pass the information from the controller to the modal window you should use resolve property which actually specifies multiple functions which are called to get the data and then injected to the modal windows controller as a function parameter. This way you can pass some data from the main controller.
if you need to pass the result back you should use result property of the modal instance which is the promise (by using your $modalInstance.result.then(function(result){ ... }); ) To pass this object from the modal you can close it with the result as a parameter like this: $modalInstance.close(result);
Hope this helps. For further details you can look at the documentation for the $modal: Angular Bootstrap