I need to build a dialog to be used with any item on a list of items. The dialog is pretty much the same regardless of the item except for the values of the fields which are obviously item dependent.
The directive I am building is reading the template from a file, compiles it with $compile and then binds (links) it to the scope of the item. The result of the binding is a DOM tree. To make the dialog visible I need to append this tree to some element in the existing DOM. The nature of my dialog is such that it makes sense to append it directly to the body tag. The dialog will be used many times in combination with different items on the list
So here is my question: How much of this process (compile, bind, append) can be done in advance? I certainly can run compile once. I can also bind the compilation result to the $rootscope and append (hidden) it to the body tag. This way I can later just turn on visibility and show the dialog.
But if it is already bound and attached to DOM, is it kosher to re-bind it to some other scope, if so - what's the right way to do it? Another question is is it even worth it? might be just re-insert it every time it is needed?
If you're only ever going to display one dialog like that at a time and you will use it frequently, you don't have to re-bind it to another scope, just change the data on the scope. Something like this:
Create a service for your dialog
Create the directive and inject your service into it. When the linking function executes, pass something like $scope.dialogData to the service so that the service can update the data.
Create a controller that gets the service injected. Set the dialog data through the service to display the dialog. Since you're modifying data in your controller that's on the directives scope, Angular notices that and updates your dialog.
Add ng-show on your dialogs wrapper to make it simple to implement open()/close() methods on your service.
Now you have a dialog that can be used from anywhere in your system, and you're just re-using the same directive without having to mess with the DOM or compilation.
This is indeed excellent question and I'm happy to see that more and more people are starting to approach dialogs as services.
Regarding your particular questions, here are some of my thoughts:
You can "cache" linking function (that is - function that is returned from the $compile call) and then call this function as needed (passing in scope variables).
Instead of inserting (hidden) compiled element you could only attach it on demand, when a dialog gets opened. On top of this I would rather attach modal element to the $rootElement instead of <body> just not to touch DOM elements above where ng-app was defined. Just not to touch parts of the DOM that AngularJS is not controlling.
IMO dialogs are really close to AngularJS routes (as they provide different "views") and as such it would be very nice to have ability to resolve promises before modal is shown (as with routes).
In fact there are number of things to consider when designing a good, generic dialog service and I hope that those advice, alongside with excellent input provided by others, will get you started. But this all is a bit theoretical so if you are looking at the implementation of what was discussed here you can have a look at this implementation. ($dialog service from http://angular-ui.github.com/bootstrap/ - it is fully customizable so can be used with CSS other than Bootstrap's. Documentation here).
It can be seen in action in this plunk: http://plnkr.co/edit/PG0iHG?p=preview
Excellent question I think. You're wondering if one can "hot swap" the scope of an element. I don't know if there's a way to do that, or even if there is, if that's the Angular way. I take it you looked at how ng-view works to get as far as you've gotten?
My advice is to do the $compile once, keep the result, the link or transclusion function or whatever it's called in Angular parlance, around somewhere. And call it for each needed instance of the dialog.
Related
I have a dropdown element directive to display in essence a styled dropdown list with additional capabilities.
My dropdown controller has a function called openDropdownItems that get's executed when the list should be displayed.
Then I also have another attribute directive called setInViewWhen that provides an expression when the element should be scrolled into view when condition is true.
<x set-in-view-when="something.item === selectedItem">
This is just an example of some X element with my attribute directive applied.
The thing is that I would like my dropdown list items (LIs namely) to have this directive on them so when user navigates over them using a keyboard, it would automatically scroll them in view when they get passed visible viewport. Whether these items are displayed within a scrollable container or as a whole in a list longer than the browser viewport isn't really relevant.
The main idea is for scrolling to follow dropdown list selection. Whether that should scroll the main window.
The problem
I can make my setInViewWhen directive completely independent but that means that I would have to search for the closest scrollable container whenever condition changes on an item. This seems to be quite a bit of processing that I would like to avoid to repeat (I need to traverse the DOM upwards, checking each node's calculated stylesheet property OverflowY + some additional checks.
This basically seems redundant because whenever I get the closest scrolling ancestor all sibling elements with the same directive could reuse the calculation result.
Question 1
How can I share this knowledge between sibling directives? If I was to fire an event I could not know whether receivers are siblings or not without any additional processing.
Question 2
Instead of checking for scrollable container every time when my directive's condition becomes true I could theoretically change dropdown parent's openDropdownItems to first complete it's original execution an then also execute the scrollability check and use the result of it along with my directive's condition.
I can gain access to dropdown's controller in my directive via directive requires property and adjust it in the post-link phase.
But this also means that I couldn't use my directive outside of dropdown which I would like to as it is a generally usable directive I could attach on several elements in my app to scroll elements into view under certain conditions.
What would you suggest how to do this?
To sum up the comments:
For Question 1, there is no direct way for sibling directives to communicate with each other. I usually create a wrapper parent directive to relay the messages.
As per the comments, the condition something.item === selectedItem introduces a watch; for a list of select items this can be many watches and detrimental to performance. Even if it doesn't occur now, it is a trap and someone may be lured into using this for a long list of items in the future. I would rather have a single watch in the parent of the <li>s and add the "scroll into view" logic there.
With the previous point in mind, you could still have a standalone setInViewWhen directive (seems useful) and have the controller of your X-select replacement directive share code with it. E.g.:
a service containing the common functionality,
an angular value containing a base class (Typescript or JS) and have the controller of both the setInViewWhen and X directives extend from it
or any other solution that is convenient for your case
Related to Question 2: The children could require their parent and change a method in it (this technique is even endorsed by Angular, see ng-model and custom controls where they override ngModel.$render() by replacing it). If you do that however, there would be many children changing the method of the parent, which could lead to a mess.
Ok, so I'll just start off saying I'm almost certain this bug is either arising from my lack of understanding of isolate scoping in angular directives or some weird interaction between directives and the angular google maps markers directive.
Anyways, the issue is that I have an instance of the angular-google-maps stuff in my main controller, and I have a tag within that. I also have a directive which is captures user inputs and uses them to populate a list in the main controller for the tag to do its thing. I pass in an object and function from the main controller into the directive's isolate scope.
The weird thing is that within my directives, I have bound this function onto a button click for a form, and for some reason, it takes 2 clicks to get the marker to appear. The function fires the first click, it has all the right values and everything, but the marker doesn't appear. Another weird I checked was simply calling the method twice in a row for one click which didn't work (still required 2 button presses).
The function modifies the array in the main controller (I have a feeling this is the root of the issue) which is used by the tag as a list of markers. Any ideas on why this could be happening? If it doesn't seem like an obvious misunderstanding of Angular scoping, I can post code.
My first thought is your data is happening outside of the digest loop. And if that's the case then adding a $scope.$digest() after your data is updated might solve it (or give you a console error).
I'm in need of some pointers in my landingPage-builder project. (i'm currently stuck!).
The main issue is as follows:
Each element in the template (like the h1 and the paragraph) has attached a directive. What I need to get the directive to do is: create a template of HTML with some other directives attached like ng-click, ng-options etc, keep the bindings to the model intact (currently far away from working), update the model when changed.
I'm not trying to append to, or replace the element the directive is on, but make a html-template and inserting it into the DOM (almost like another view) so that the model on the left can be updated from the "settings" box on the right.
The project can be viewed here: http://193.107.29.196/~stian123/landingPageV3/app/#/pagebuilder/2
You may need Allow-Control-Allow-Origin for Chrome: https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi/related
I'm a bit confused about $compile and doesn't really know when I need to use this part of the directives api.
Any suggestions?
Thank you!
If I understood your question correctly, you want to dynamically create templates, some of which have Angular attributes in them, then attach them to the DOM.
First, to (hopefully) answer your question, about when to call $compile:
Whenever you load in HTML from outside Angular's template system (like trying to set $(element).html(myHtmlString)), you need to let Angular compile it before you attach it to the DOM. In other words:
elem.append($compile(yourHTMLString)(scope));
This lets Angular traverse the DOM and parse any directives and bindings and attach them to the provided scope. If you don't $compile, Angular has no idea about those intended bindings at all, the HTML is never read by Angular.
Second, I don't know how flexible you want your templates to be, but if they're relatively fixed, but with some fixed customizable options (text, color, font-size etc), you might be better off creating a directive for each 'view', with the view options bound to the scope of the directive. Then you can just change the fields on the scope of the directive in the panel on the right side, and the view will update directly. You wouldn't even have to use $compile in this case.
If you want the user to be able to manually add the template HTML code, you will have to compile the HTML as described above.
I am using a directive "slideable" which creates a slideout area and has a toggle. This code that was not written by me but it demonstrates a larger issue for me. When I changing views (most commonly /user/:id type), slideable is a directive used on the template. The directive searches for an element during its link function and binds a click event. The issue is that when I am changing routes and the new view ( same type but different id ) is being loaded the directive is re-binding to the old view. If I stop the browser in chrome during the link then I will see two ng-views on the dom and the issue is it binds to the one that is leaving.
I also have other issues that appear to be related to this phenomenon. Is it normal that the old view would still be on the dom while the new view is being formulated?? Why wouldnt the old-view be destroyed before the new one is rendered? How do I get around this issue in a directive like this?
Thanks.
I am looking to understand conceptually what is happening. I already modified the directive to select the latest view and to appropriately search and bind to the correct element. But I am a bit perplexed as to why there would be a state where both co-exist on the dom.
One definitive reason why the old HTML fragment is briefly present along with the new one is to support animation of transitions from the old to the new. Take a look at the ngView documentation and you'll see an example of an animated transition, and it'll be clear that this is not a bug or a design flaw.
Usually when someone has problems with binding to the right element or element's event, it's because they are selecting the element without limiting the scope of the selector to the HTML fragment being added or updated, or trying to target parts of the DOM outside of the directive. So that's the first place to check, that the directive is doing things right, but like I said we'll need code to check on that.
I'm trying to build web app that dynamically load interfaces, using angularJS.
I found that it was possible to bootstrap some portions of my code after the initial bootstrap of Angular (HTML template + Controller).
My problem is that, doing so, the 2-way data-binding doesn't work. See for yourself:
http://plnkr.co/edit/MtAWP6
Any idea? Am I seeking for something to do the wrong way?
Thanks!
Your problem isn't a bootstraping one (although you really shouldn't be using bootstrap to instantiate a controller, but rather $compile, imo - see this answer). It is a scope problem. You define a "mymodel" model in your controller, but then define it again in your form, for which angular automatically creates it's own scope. While the form's scope inherits from the parent scope, and thus seems to be "binding" the model, the inverse doesn't happen.
You need to either establish a binding between both scopes (or $watch the form's variable, or define the for in the surronding controller), or just assign the controller you want to the form, directly.
See your problem exposed here (see that while your $timeout changes both models, manually setting the model only changes one)
See it resolved here (by basically assigning your controller to the generated form, rather than to a enclosing div of said form)
I think maybe you should take another look at routing/ deep linking. You should be able to specify both a template url and a controller.
Check out this video
And the api docs