How to solve lack of promise unwrapping when writing nested directives - javascript

I've got a few nested directives:
<first-directive>
<second-directive user="user">
<third-directive name="name">
<fourth-directive description="description">
The template of the first directive has the second directive, template of the second directive has the third directive and so on. All directives have isolated scopes.
The first directive sets user object on its own scope. If it does it in an synchronous way ($scope.user = {name: "Alice"}, then everything is fine. However, if it loads user in an asynchronous way (e.g. $http.get(url).then(function (user) { $scope.user = user }), then all other directives won't work, because e.g. when the controller function of the second directive is evaluated, user hasn't been set yet on its scope.
The only solution I've found so far is to use $scope.$watch in all nested promises (e.g. $scope.$watch("user", function (user) { $scope.name = user.name} for the second promise), which sucks a lot, because if I want to use directive that I already use somewhere else inside <first-directive>, I need to rewrite it to use $scope.$watch as well...
Is there any other way to fix it? I know that I could load data necessary for the first directive e.g. in resolve function of the route it's used in, but I'd like to be able to put this directive in any page without modifying any routes or controllers.

Not 100% sure it fits your use case, but if the first directive always loads the user asynchronously it can use ng-if in it's template:
template: '<second-directive ng-if="user" user="user"></second-directive>'
This way the second directive will not be compiled until the user is actually available (nor will the third or fourth directive).
This also makes it possible to use other directives in the chain without having to add $watch.
And if you need to use the second directive somewhere else where the user is already available you can just use it without the ng-if.

Related

How do I change one Directive when I click another Directive?

So here's my problem:
I have a page which displays two different graphs. Each of these graphs are there own Directives which has their own isolate scope.
When a user clicks on one of the bar's in the chart in Directive #1, I need the graph in Directive #2 to change.
Currently both Chart Directives are being fed their respective data sets from the Controller of this page.
Now from what I've seen I really have about three options:
Pass a callback function into Directive #1 which be called when the chart is selected. This callback function will exist on the Controller of the page and then can change the necessary data in order to get Directive #2 to update via data-binding.
Events. Fire an event on $rootScope inside of Directive #1 when the chart is selected. I can then listen to this event on the Controller and change the data in Directive #2 to update it via data-binding.
Use a Library like Rx.JS in order to make an observable inside of Directive #1. I haven't used Rx.JS with Angular that much so to be honest I have no idea if this would even work or what it would look like. But if I could expose this Observable to page's Controller from within Directive #1 then I should be able to subscribe to it and update Directive #2 when necessary.
Now I have a good understanding of Solution #1 and #2 but they have their own issues:
This very quickly could turn into "callback hell" and doesn't seem to be a very "Angular" solution. This also creates a bit of a tight dependency between the page's Controller and this very generic Chart Directive. Out of my options I think this is the best solution but I would love a better one.
I have to build a way to specify id's on the event names that are unique to that explicit instantiation of the directive, since theoretically there could be more than one of these Chart Directives on the page.
I would love to know if anyone has any other ideas that I haven't thought of or a better approach? Maybe even something that I'm not aware of that Rx.JS offers with Observable's?
TLDR: I need to click on Directive #1 and have it effect what is currently being displayed in Directive #2.
I think this can be done by using two binding scopes in your directive like,
.directive('graphOne', function () {
return {
template: blah/blah.html,
scope: {
scopeToPass: '='
}
}
})
and
.directive('graphTwo', function () {
return {
template: blah1/blah1.html,
scope: {
scopeToGet: '='
}
}
})
and in html
<graph-one scope-to-pass="uniqueScope"></graph-one>
<graph-two scope-to-get="uniqueScope"></graph-two>
Since we are assign $scope.uniqueScope to both directives, and the scopeToPass is two way binding, when the value of scopeToPass get changed it will be passed to uniqueScope and from uniqueScope it will be passed to scopeToGet.

In AngularJS, how does $scope get passed to scope?

I'm a bit confused with the use of $scope in controllers and of scope in directives. Please verify if my understanding is correct (and also provide some alternative ways how to do this).
Let's say I have an html:
<div ng-controller="app1_Ctrl">
.
.
.
<input type="text" ng-model="value"/>
<input type="checkbox" />
<button ng-click="submit()"></button>
</div>
And my main.js
(function() {
angular.module('mainApp', ['app1']);
})();
And my app1 looks like this (based on official AngularJS documentation here)
(function() {
var app = angular.module('app1', []);
app.controller('app1_Ctrl', ["$scope", function($scope) {
.
.
.
}]);
app.directive('app1_Dir1', [function() {
function link(scope, element, attr) {
scope.$watch(attr.someAttrOfCheckBox, function() {
// some logic here
});
function submit() {
// some logic here
}
}
return link;
}]);
})();
How does $scope.value passed in scope in directive so that I can do some manipulations there? Will ng-click fire the function submit() in the directive link? Is it correct to use scope.$watch to listen for an action (ticked or unticked of course) in checkbox element?
Many thanks to those who can explain.
By default, directive scope is controller $scope; but it means the directive is directly dependent on your controller and you need a different controller for each instance of the directive you want to use. It is usually considered a best practice to isolate your directive scope and specifically define the variables you wish to pass it from your controller.
For this, you will need to add a scope statement to your directive :
scope {
label :'#',
context : '=',
function : '&'
}
and update your view :
<my-directive label="labelFromController" context="ctxtFromController" function="myFunction()" ></my-directive>
The symbols denote the kind of thing you wish to pass through : # is for one-way binding (as a string in your directive), = is for two-way binding of an object (which enables the directive to update something in your controller), and & is for passing a function.
There are a lot of additional options and subtleties that are best explained by the Angular doc https://docs.angularjs.org/guide/directive. There are also some nice tutorials out there (e.g. http://www.sitepoint.com/practical-guide-angularjs-directives/)
Your submit() function is not attached to anything, so you won't be able to call if from your viewer. You need to define it as scope.submit = function() ... in your link function if you wish to access it.
You can use $watch for this kind of thing, but there are usually other more elegant ways to achieve this by leveraging the fact that angular already "watches" the variables it is aware of and monitors any changes he can (this can be an issue when some external service changes data for exemple, because angular cannot listen to events it is not made aware of). Here, you can probably simply associate the ng-model directive to your input checkbox to store its true/fale (checked/unchecked) value, and the ng-change or ng-click directives to act on it. The optimal solution will mostly depend on the exact nature of your business logic.
Some additional thoughts :
The HTML insides of your directive should be packaged in an inline template field, or in a separate HTML file referenced by the templateUrl field in your directive.
In your HTML code above, your directive is not referenced anywhere. It should be an element, attribute or class (and your directive definition should reflect the way it can be called, with the restrict field). Maybe you have omitted the line containing the directive HTML, but as it stands, your directive doesn't do anything.
To my knowledge, you don't need to return link. Think of it as the "body" of your directive, where you define the variables and functions you will call in the HTML.
Your directive doesn't actually need HTML code and the above thoughts might be irrelevant if you are going in a different direction, but encapsulating some kind of view behaviour that you want to reuse is probably the most common use of directives.

Add AngularJS directive at runtime

I am creating a game where the first thing that needs to happen is some state is loaded in from an external JSON file - the contents of one of my directives are dependent on this data being available - because of this, I would like to delay applying the directive until after the data has loaded. I have written the following:
window.addEventListener('mythdataLoaded', function (e) {
// Don't try to create characters until mythdata has loaded
quest.directive('character', function() {
return {
restrict: 'A',
scope: {
character: '#'
},
controller: 'CharacterCtrl',
templateUrl: 'partials/character.html',
replace: true,
link: function(scope, element) {
$(document).on('click', '#'+scope.character, function () {
$('#'+scope.character+'-popup').fadeToggle();
});
}
};
});
});
// Load in myth data
var myth_data;
$.getJSON("js/mythdata_playtest.json", function(json) {
myth_data = json;
window.dispatchEvent(new Event('mythdataLoaded'));
});
However, it appears that my directive's link function never runs - I'm thinking this is because angular has already executed the part of it's cycle where directives are compiled/linked by the time this directive gets added. Is there some way to force angular to compile this directive after it is created? I googled around a bit, and some people suggested adding $compile to the link function for similar issues - but the link function is never run, so that doesn't work for this case. Thanks!
It seems to me it would be better to always configure the directive, to do the JSON call in the directive, and attach logic to the element in the JSON call's success handler. This would, if I understand you correctly, do what you want.
AngularJS is meant as a framework, not a library, so using it in the way you mentioned is not recommended. Exactly as you mentioned, AngularJS does a lot of things for you when it runs. AngularJS, by default, runs on document loaded, and your $.getJSON callback arrives after that. When AngularJS runs it does all its magic with compiling the content and all that.
As a sidenote, it's also more the Angular way to use $http over $.getJSON.
I think you're thinking about this the wrong way. A major ideology in angular is that you set up declarative elements and let it react to the state of the scope.
What I think you might want to do is pass in what you need through the directive scope, and use other angular built in directives to hide or show your default ("non directive") state until the scope gets set from the controller for example.
Example:
You want a box to be hidden until an api call comes back. Your directive sets special styles on your element (not hidden). Instead of delaying to dynamically set your directive, you can pass in a scope var with a default value and use something like ng-show="data.ready" in your directive template to handle the actual dom stuff.

How to dynamically inject controller in AngularJS

How to set ng-controller as an expression from the $scope?
According to the documentation:
ngController – {expression} – Name of a globally accessible
constructor function or an expression that on the current scope
evaluates to a constructor function.
But how to evaluate scope expression as a controller for controllers that have been registered with module .controller?
For example:
Layout:
<div ng-controller="myExpr"></div>
JavaScript (define controller):
app.controller('myCtrl', ['$scope', '$timeout', function () { ... }];
JavaScript (parent scope):
$scope.myExpr = ...;
What should be in myExpr to use myCtrl as a controller via expression?
I've tried $controller('myCtrl')... not working...
P.S. If controller has been defined via globally accessible function.. it's possible to provide it as myExpr. But what to do if it has been defined so?
The expressions that ng-controller accept are a bit wierd. So you can do this by writing your controller slightly differently (but read below as for why you probably don't want to).
function myCtrl($scope) {
$scope.value = 'Stuff';
}
This is a controller and will work like normal for this case. Like in this example: http://jsbin.com/ubevel/2/edit
So why not do it?
First of all this is not a good way to define things from a testing perspective. Secondly, this allows you to set the controller dynamically on load but it won't allow you to change things after that. If you change the value of myExpr after the page has loaded the controller will not change.
So what to do?
I would highly suggest looking at using a service instead. Swap out your actions by supplying your outer controller with a service that you then change in the same manner you are now trying to change the inner controller. So something like: http://jsbin.com/ubevel/5/edit
This service can be swapped out on the fly, changing it will change the actions that are available in the scope.
You could also use an include, but this would result in duplicate html. I personalty am fine with this since I am against reusing html for two different types objects (sooner or later you want to change one but not the other and it becomes a mess). But a lot of people would object to that.
An extra note: There are probably nicer ways to do it with controllers, I probably haven't looked at all angles, but I just don't think controllers are the right tool for this case.

Access Attributes / $element context from function called by Angular directive

Within a directive definition, there's an API for accessing $element and $attrs, so you can refer back to the element from which the directive was called. How would I access $element and $attrs when calling a custom function using a standard directive like ng-class?
EDIT: I understand that this is not an idiomatic approach. This question is applicable to rapid prototypes, which is a great use for many of Angular's features. Just not the ones that are all about sustainability, separation of concerns, etc. My primary concern is velocity. Thus, being able to bang something out quickly with the built-in directives and a quick controller method can, in fact, be a virtue, and can win the opportunity to do a fuller and more proper implementation down the road...
In this case, I'm just adding a contextual .active class to a nav element, based on the value of $location.path(), as per this post and this one. However, in those examples you need to explicitly and redundantly pass a copy of the contents of the href attribute as an argument to the getClass() function you're calling from ng-class. But isn't there a way to programmatically access the href attribute from within getClass() and avoid redundantly passing identical content as an arg?
For example, a controller with a getClass() function the way I'd imagine it could work:
function navCtrl($scope, $routeParams) {
$scope.getClass = function($element, $attrs) {
if ($location.path().substr(0, path.length) == $attrs.href) {
return "active"
} else {
return ""
}
}
}
Which you could then call simply and elegantly with:
<a ng-class="getClass()" href="/tasks">Tasks</a>
rather than:
<a ng-class="getClass('/tasks')" href="/tasks">Tasks</a>
(I recognize another option is to create a custom directive that does this, but for now I'd just like to figure out if it's possible to access $attrs and/or $element from within a controller function that's been called by a directive. Thanks!)
You actually can do this... BUT YOU SHOULD NOT DO THIS. lol...
Here's how to do what you're asking...
In your markup, pass $event into your function.
<a ng-click="test($event)" href="whatever">Click Me</a>
Then in your controller, get the target element of the event:
$scope.test = function($event) {
alert($event.target.href);
};
Now here is why you probably shouldn't do this:
If you reference the DOM, or manipulate the DOM in your controller, you're endangering dependency injection and also the separation of concerns from within the Angular structure. Sure you could still inject $event as a dependency if you were testing your function, but depending on what you're doing inside of that function, you might still have ruined your DI, and you're trying to make a controller do a directive's work (which is to try to keep your controller from being tightly-coupled to your markup)
That said, if ALL your doing is just getting a value, it's probably fine, but you're still coupling your controller to your markup to some degree. If you're doing anything else with that DOM element, you're off the reservation.
I don't think you can do that. The angular philosophy is to avoid accessing the DOM directly from your controller. You have already identified your two options for doing this in Angular:
pass in the href as a param i.e.. getClass('/tasks')
or, write a custom directive
Alternatively, if the class is purely presentational and doesn't affect how your application runs then you could ignore angular and use a quick and dirty jQuery function to do the job for you.
The lack of direct interaction with the DOM can be a bit strange at first but it's a godsend in the long term as your codebase grows, gets more complicated and needs more tests.
Here's a completely different answer: you don't need a Angular to change styling based on href. You can use CSS selectors for that.
In your styles:
a[href="/something"] {
background-color: red;
}
That in combination with anything you might be doing with ng-class should be everything you need.

Categories

Resources