Bind scope variable from controller to directives without using $watch - angularjs

For Angular 1, I have an AJAX call that is made in the controller (as it is dependent on another set of data that is returned during the route resolve) and once the response is back, the data is passed down to directives. Since the data doesn't come back until after the directives are compiled, the directives initially gets an undefined for that data passed from the controller and would only get the data if I am watching that scope value inside each of the directive. Is there a better way where I don't have to use $scope.$watch or any event listeners, such as $broadcast/$on? I don't want to exhaust the digest cycle with too much watchers.
Here's a mock structure:
<parent>
<directive1 data="manipulateDataReturnedFromAJAXCall"></directive1>
</parent>
//template for directive1
<div>
<directive2 ng-if="data" attr1="data.field1"></directive2>
<div>

What about using a callback in your http service.
//Controller
MyHTTPService.getData(function(data){
$scope.manipulateDataReturnedFromAJAXCall = data;
})
//MyHTTPService service
return{
getData : function(fct){
$http.get("url/to/data").then(function(response){
fct(response.data);
})
}
}

Related

AngularJS : Testing directive, HTML not re-rendered after promise completion

I am writing some basic unit tests for my AngularJS app. I have some bindings on the UI with a scope variable inside my directive whichis populated on the completion of a promise.
HTML:
<div id="parent">
<div id="child" ng-repeat="l in aud">
// Other Stuff
</div>
</div>
Directive:
link: function(scope){
service.getArray().$promise.then(function(data){
scope.aud = data;
}
Test:
describe('my module', function () {
    var $compile: ICompileService, $rootScope: IScope, directive: JQuery<HTMLElement>;
    // Load the myApp module, which contains the directive
    beforeEach(angular.mock.module('my-module'));
    beforeEach(angular.mock.module(($provide) => {
        $provide.service('service', () => {
            return {
                getArray: () => {
                    return Promise.resolve(
                        ["item1", "item2"]
                    );
                }
            }
        });
        // Store references to $rootScope and $compile
        // so they are available to all tests in this describe block
        beforeEach(inject(($httpBackend: IHttpBackendService, _$compile_: ICompileService, _$rootScope_: IRootScopeService) => {
            $compile = _$compile_;
            $rootScope = _$rootScope_.$new();
            directive = $compile('<my-directive></my-directive>')($rootScope)
            $rootScope.$apply();
        }));
        describe('account-utility directive', function () {
            it('account utility directive details panel is shown on click', function () {
                let list = directive.find("parent"); // Finds this
                let listItems = list.find("child"); // Cannot find this. Throws error. 
                console.log(list); // innerHTML still shows ngrepeat unsubstituted by divs
                expect(listItems.length).toBe(2);
            });
        });
});
I debugged the whole thing and the promise is resolved and the data is assigned to the scope variable 'aud'. However seems like my copy of scope for the test and the app are different. Whats going on here?
beforeEach((done) => {
directive = $compile('<my-directive></my-directive>')($rootScope);
$rootScope.$digest();
setTimeout(() => {
$rootScope.$digest();
done();
});
});
Done helps you wait till all asynchronous tasks are picked up from the stack.
apply()
works too
When the Angular's promise is resolved you need to notify it so it will run its dirty check.
In order to do that you need to invoke $rootScope.apply() inside yours it clause.
Think of it like that, your $rootScope.apply() call in the before each clause invokes your directive's link function, the that function registers a Promise resolvment in Angulars queue, but it not got processed.

AngularJS - How to pass data through nested (custom) directives from child to parent

I am looking to find the best way of sending scope through nested directives.
I have found that you can do $scope.$parent.value, but I understood that's not a best practice and should be avoided.
So my question is, if I have 4 nested directives like below, each with it's own controller where some data is being modified, what's the best way to access a value from directive4 (let's say $scope.valueFromDirective4) in directive1?
<directive1>
<directive2>
<directive3>
<directive4>
</directive4>
</directive3>
</directive2>
</directive1>
For the "presentational" / "dumb" components (directive3 and directive4), I think they should each take in a callback function which they can invoke with new data when they change:
scope: {
// Invoke this with new data
onChange: '&',
// Optional if you want to bind the data yourself and then call `onChange`
data: '='
}
Just pass the callback down from directive2 through directive4. This way directive3 and directive4 are decoupled from your app and reusable.
If they are form-like directives (similar to input etc), another option is to look into having them require ngModel and have them use ngModelController to update the parent and view. (Look up $render and $setViewValue for more info on this). This way you can use them like:
<directive4 ng-model="someObj.someProp" ng-change="someFunc()"></directive4>
When you do it like this, after the model is updated the ng-change function is automatically invoked.
For the "container" / "smart" directives (directive1 and directive2), you could also have directive2 take in the callback which is passed in from directive1. But since directive1 and directive2 can both know about your app, you could write a service which is injected and shared between directive1 and directive2.
Nested directives can always have an access to their parents' controllers via require. Let's say you want to change value from the directive1's scope from any of its nested directives. One of the possible ways to achieve that is to declare a setter in the directive1's controller setValue(value). Then in any of nested directives you need to require the directive1's controller and by doing that you'll get an access to the setter setValue(value) and other methods the controller provides.
angular
.module('yourModule')
.directive('directive1', function() {
return {
controller:['$scope', funciton($scope) {
return {
setValue: setValue
};
funciton setValue(value) {
$scope.value = value;
}
}]
// The rest of the directive1's configuration
};
})
.directive('directive4', function() {
return {
require: '^^directive1',
link: (scope, elem, attrs, directive1Ctrl) {
// Here you can call directive1Ctrl.setValue() directly
}
// The rest of the directive4's configuration
};
})
Another way is to $emit events from a child directive's controller whenever value is changed by the child. In this case the parent directive's controller should subscribe to that event and handle the data passed along with it.

Angularjs directive, maintaining internal state for each of the directive instance

I have a directive which has dependency on a service, I am maintaining some directive state in the service.
So whenever same directive is used in two places, it needs to instantiate the new service.
for example,
<body>
First directive instance: <custom-directive> 1 </custom-directive>
Second directive instance: <custom-directive> 2 </custom-directive>
</body>
In this case, both the controllers are populated with the same service object, but I need the different service objects to maintain their states internally.
Can someone help.
You can still use a service/factory, but expose a getInstance method inside your service that returns a new instance, allowing you to inject and subsequently create a new instance for each directive.
angular.module('app', []).factory('sharedService', sharedService);
function sharedService() {
return {
getInstance: function () {
return new instance();
}
}
function instance() {
// functionality
}
}

AngularJS:AppLevel controller possible?

I have a controller that is the controller for my page but i was wondering if its possible to have a AppLevel controller i.e. something that is accessible from every page... so each page would actually have more than 1 controller assigned.
I know i can probably do this with a service and inject the service but I was hoping for some kind of applevel controller that can be assigned.
If this possible, how would i communicate between the 2? I presume using dependency injection and just pass the applevel controller to my main page?
Anyone have an idea about this?
thanks
AngularJS leverages JavaScript prototypical inheritance in order for scopes to access properties on the parent scope. You can define the controllers nested in the HTML and access the parent from the child. However I would strongly urge you not to rely on this fact for your 'AppCtrl'. In some cases the scope you are working on will be isolated and will not be a part of the inheritance hierarchy that has access to the AppCtrl's scope.
I would suggest creating a service for this, or you could use pub/sub with $rootScope.$on and $rootScope.$broadcast.
To show the service example I'll use the words shellCtrl and shell service instead of app to make the example a little clearer.
The 'shell' service's job is to allow any other controller, directive or service in your app to interact with the shellController, and therefore the host view container.
<div ng-app="myApp" ng-controller="ShellCtrl">
<div ng-controller="SomeOtherCtrl"></div>
</div>
// parent controller defined on the same element as ng-app
function ShellCtrl($scope, shell) {
// I've just made the shell accessible to the $scope of shellctrl, but you can do
// this in various ways.
$scope.shell = shell;
}
// any other controller
function SomeOtherCtrl($scope, shell) {
shell.setTitle('Some title');
}
// basic example of the shell service
angular.module('myApp').factory('shell', function () {
return {
title = 'No title set',
setTitle = function (title) {
this.title = title;
}
}
});
Now you can set properties on the parent controller in a detached fashion without relying on the scope hierarchy.
When you have a child controller in Angular it inherits from the parent scope. So if you have one top level controller that contains functions that some descendant controller doesn't have then the top level function (or scope object) will be referenced. If one of the child controllers defines a local version of the function (or property on the scope) then it will no longer inherit from it's parent controller.
The Fiddle: http://jsfiddle.net/Y9yEQ/
The HTML
<div ng-app="myApp" ng-controller="TopLevelCtrl">
<button ng-click="testing()">Yo top level!</button>
<button ng-click="testing2()">Yo top level 2!</button>
<div ng-controller="ChildCtrl">
<button ng-click="testing()">Yo child!</button>
<button ng-click="testing2()">Yo child2!</button>
</div>
</div>
The JS
angular.module("myApp",[]).controller("TopLevelCtrl", function($scope){
$scope.testing = function() {
alert("just testing");
}
$scope.testing2 = function() {
alert("just testing parent");
}
}).controller("ChildCtrl", function($scope){
$scope.testing2 = function() {
alert("just testing child");
}
})
If you need to share some data between multiple controllers (since controller instances may be created or destroyed to support views as they are added/removed) you'll want to use a service. If you have a strict structure for your controllers you can $emit to bubble an event up scopes or $broadcast to send events down through scopes.

Angular - data not available inside ng-init

Angular partial - HTML.
BaseCtrl
<div ng-controller="SelectTagCtrl">
<input type="text" ng-init="setTags(viewData['users'])" ui-select2="tagAllOptions" ng-model="tagsSelection" name="users" />
{{viewData['users']}} ECHOES CORRECTLY.
But undefined when passed inside ng-init callback.
</div>
<input type="text" class="span12" placeholder="Brief Description" name="description" value="{{viewData['description']}}">
ECHOES CORRECTLY.
Controller.js
function SelectTagCtrl(){
$scope.setTags = function(data){
// data is undfined when viewData['users'] is used. <-- PROBLEM
// correct when I pass some static string.
}
}
//POPULATING viewData to be used inside view partial.
function BaseCtrl(){
$http.get(url).success(function(data){
$scope.viewData = data.data || [];
$scope.view_type = $scope.viewData['view_type'];
$scope.fields = data.data.fields;
console.log($scope);
}).error();
}
Using timeout would be a workaround, instead I would take a $scope variable inside the controller to know if the ajax call has completed.
The problem is ng-init might get called before ajax completion.
I already had ui-if directive configured in my angular project, so I used it with the combination of $scope variable to get the things working.
<div ng-controller="SelectTagCtrl" ui-if="ajax_done">
<input type="text" ng-init="setTags(viewData['users'])" ui-select2="tagAllOptions" ng-model="tagsSelection" name="users" />
</div>
And inside controller,
$http.get(gurl + '.json').success(function(data,status){
// some stuff
$scope.ajax_done = true;
}).error();
Because of angular's magical two-way binding, the element will get updated. Now it sees that ajax request is completed, ui-if will get a truthy value and ng-init directive of the element will get a chance to execute its callback.
EDIT: ui-if was removed from Angular UI in favour of ng-if which is now built in.
Here are two different changes to your fiddle that appear to work.
Fiddle 1 - this version uses $scope.$apply(exp) as described in the documentation here and is useful when you are modifying angular bound data outside of the angular framework. In this example setTimeout is the culprit.
setTimeout(function(){
console.log("updateVal" );
$scope.$apply(function() {
$scope.updateVal2();
});
console.log($scope.tagsSelection);
},5000);
Fiddle 2 - this version uses angular's wrapper for setTimeout called the $timeout service.
$timeout(function(){
console.log("updateVal" );
$scope.updateVal2();
console.log($scope.tagsSelection);
},5000);

Resources