Communicating between multiple layers of nested directives - javascript

It's a bit complex so bear with me. I've got three layers of directives:
Top layer - a popup directive
Middle layer - a switch-pane directive *
Bottom layer - one of several views
The top layer is just some popup that represents a wizard in my app.
The middle layer is a directive I've made that acts as a stack of views - you can "push" and "pop" views. The "top" view is displayed and the rest are pushed aside and blurred.
The bottom layer is a bunch of views that are normally unrelated to each other, which are dynamically loaded and displayed in the switch-pane according to what the user does.
So far, this works, BUT: currently, the top layer's $scope has an array property that represents all the views the switch-pane should display, passed to the switch-pane directive as an attribute, and the switch-pane directives $watches it and updates itself.
This is OK but I don't think it's good enough - I'd like the switch-pane directive itself to manage it's stack of views, and only expose a push and pop API.
Here are a few ways I thought of:
Using $broadcast / $emit - the top layer will $broadcast a "push" event and the switch-pane will catch it and do whatever it needs
Using a service (to subscribe and fire the "push" event - this is just like using $broadcast but doesn't propagate throughout the scope tree
Using a service that allows the switch-pane directive to register an API of it's own. Using some way of identifying it such as an attribute or even element ID
Using angular.element().scope() to get access to the switch-pane's inner workings
Frankly, I don't like any of these methods much. Certainly I want to avoid being tied to the DOM, so the last 2 are worst.
Any other ways to do this? Which is the most Angular-ish way to expose a directive's API, considering we don't really have access to a certain instance of a directive except via DOM?

You could check how ng-form, ng-model and ng-input is implemented. Basically if the form has a name, for instance <form name="foo" ...> its controller is published to the current scope, in this case under $scope.foo variable. Once the controller is published you could use its API outside the ng-form directive.
You could also access this controller from the other directive if require: "^ngForm" option is specified.
Here is an example taken from my project. It's a sort of wrapper for jqGrid grid plugin written in jQuery: https://github.com/9ci/angle-grinder/blob/06856b0d940b572960025f06f470c2f40fdc0ceb/app/scripts/modules/gridz.coffee#L12

Related

What is the ideal way of communicating within the same level sibling directives?

I have a code segment in a controller which define directives within a ng-repeat statement.
<div class="Value" ng-repeat="value in valueList">
<my-directive inner-value="value"></my-directive>
</div>
Here I want to communicate between the directives (e.g. - the button in the 2nd directive to be appeared when click on the button of the 1st directive)
What would be the ideal way of communicating between the same level directives through the controller
There are two ways I know of :
Events
Child Components trigger events .
Parent receives them and decide what to do
Service
All components inject the service
Service has a subject which all components listen to
When button is clicked any component can call the service to update
For each update each component is aware of the change
Which I prefer ?
Service based
Why ?
Disadvantages of Events
Having events complicates the flow, all the components are fixed in their places. We also need to worry about sharing data between them . Multiple inputs , their state etc., big mess.
If there are lot of changes its difficult to track
Logic resides in Component, difficult to test and maintain . I love dummy components
Advantages of Service
All logic resides in service, easy to test and maintain
Components can move across , change their position still works great
Adding new components is breeze, don't need to rewrite the whole logic again
Utilize the power of rx programming and simplify any complex logic

How to correctly interact with a view which is managed from another controller?

The question might seem too vague but I could not think of a better way to describe the idea, so I'll try to explain it in details.
I have MasterController attached to <html> tag of my SPA application. This MasterController contains all the logic and models for controlling the following UI elements:
page title (<title> tag)
subheader which displays the title of current page (like Customers, Orders, Settings etc.)
name of the currently logged-in user
some commonly used action buttons which will be used for all pages in the system. To be specific, these buttons are Show filters, Export data to Excel and Add new record.
While the first two items on this list can be managed through detection of current ui-router state (through its $stateChangeSuccess event), the last two (username and buttons) are somewhat problematic, especially the buttons.
I can manage the button actions using $broadcast, so every controller can be notified about clicks on any button. But the tricky part here is that the buttons might be needed in different combinations - one page might need all of them, and another one might need none.
Let's say, ui-router loads some CustomersController. At that point MasterController receives $stateChangeSuccess event and by default hides all the buttons.
But now, how does CustomersController tell to MasterController that CustomersController will need two specific buttons from the very beginning?
Theoretically, I could use $emit from CustomersController to send an event to MasterController, but it somehow feels ugly. Events are meant for, well, events and not for sending requests like "hey, MasterController, if you are somewhere up the scope, can you please show the following buttons?".
Of course, I might be wrong and maybe there is some way to use Angular event system to manage this scenario in clean way.
What came to my mind is that maybe in the $stateChangeSuccess event I could somehow detect if there are currently any listeners for my button click events and then I could hide buttons which do not have any listeners attached, but I'm not sure how to do it, and I'm not sure whether it will work as expected - whether old listeners will be detached when ui-router recreates the view with another controller.
If you are just nesting controllers, their corresponding scopes actually make use of prototypical inheritance. So you could just define a function $scope.configureButtons in your MasterController and call this function from the $scope in your nested CustomerController.
If Controllers are not nested you would probably need to resort to $rootScope.$broadcast for setting up your buttons.
Why not just simply using diferent controllers for each view? Maybe generalize a bit the CustomerController and extend it (specialize it) for every combination of buttons you need. Using the $stateChangeSuccess feels like avoiding polymorphism to me.
Today I got a tricky idea based on #Diego Castaño Chillarón 's answer. I thought - but is it possible to use ui-router to swap controller of existing view and will it rebind also the $scope? And will I still be able to replace inner parts of the loaded view?
It turned out that it is doable! Now I don't have to control the common view fragments from the master control, and I don't need also to inherit or duplicate them - I just switch the controller to the required one through ui-router.
Like this:
$stateProvider
.state("customers", {
url: "^/customers",
views: {
"controller": {
controller: "CustomerController as cntrlr"
},
"page#customers": // <- this is important, absolute name required for ui-router to find nested view
{
templateUrl: "customers"
}
}
}) // other routes follow in the same manner
And my HTML looks like this:
<div id="routes-root" ui-view="controller">
<div id="content-header-buttons">
<button type="button" ng-click="master.toggleFilter()">Filter data</button>
<button type="button" ng-click="cntrlr.exportClicked()">Export</button>
<button type="button" ng-click="cntrlr.createNewClicked()">Create</button>
</div>
<div id="view-content" ui-view="page"></div>
</div>
As you see, I left master controller to control only visibility for filters block, which won't change.
But controller itself is attached to #routes-root element, preserving inner content, and ui-router (or Angular) is smart enough to attach $scope and cntrlr variable to the loaded controller. And then I load inner view into #view-content, which also gets attached to the already loaded controller.

How do I handle interaction between the DOM and the Controller?

The more I read (and try to test) Angular apps, I'm seeing that it is bad practice for a controller to refer to the DOM. (e.g. this blog post).
I must be missing something big, because if the controller can't access the DOM (e.g. by "regular" javascript calls like document.getElementsByClassName), then I don't understand how to do a lot of things I'd consider very basic.
Here's a contrived, simple example that has some of the same problems that my app does:
I have a directive that is simply a red box (a div with some styles applied), and uses ng-transclude. So I'd use it in my html file like <red-box>Text that goes in the red box</red-box>
A button, when clicked, changes the color of all red boxes to blue. I would have something like this in my html file: <intput type="button" value="Make Them Blue" ng-click="makeThemBlue"/>
In the controller's makeThemBlue function, I'd (for example) find all of the divs by class name, and change the class to something else which makes them blue
Now consider that my real app is much more complicated - many "boxes" consisting of nested directives, that can be dragged around, and have their positions saved. The controller reads all of the saved settings, and lays everything out according to how the user saved it.
How would I do something like either of the above examples without having the controller access the DOM?
Here are my key rules:
Directives - For solid components and for DOM manipulation.
Services - For business logic and saving state. Directives, Controllers, Services etc. should use them.
Controllers - A views helper. No business logic should be executed inside. For complicated issues use a service.
In your case a box should be a directive.
You directive will use an observable service and register for the click event.
When the click event occurs, the observer will notify all the registered directive instances that it was clicked, and you should apply to that in your directive.
You should use Directives for this purpose.
See the documentation for Directive
Angular Directives
It gives you a built in jquery like functionality to Access Dom
the link function in Directive is amazing to Manipulate Dom with the same syntax(almost) as jquery.
Further You can Maintain Chunks of Functionality By Making Services , So you can separate each login plus you have Access to Dom and can manipulate them easily
Injecting services to controller function of directive will give you to maintain code reuseability.
By having the DOM access the controller (or natively from within the directive). This is what the declarative paradigm is all about.
If your box needs to change it's color, have it read that value, or a class, or whatever you need, from a value in the controller, or in the directive itself.
In a very basic sense:
<my-directive color="getColor()"></my-directive>

angularjs when i should use a directive

i am working with javascript and angularjs for about a month.
But i'm still not sure, when i should use a directive or not.
Example:
I want to show appointments in a table with the date as tableheader.
On the left of the table i have a button, where i can load the appointments
of the previous week and on the right for the next week.
My first approach would be:
I have a viewmodel/controller which holds the data for the actual week.
Then i add two methods on the controller: loadPreviousWeek, loadNextWeek which get's
the data from a appointmentProvider.
In my view i add a ng-click directive to the buttons which call the functions of the controller.
After i read a bit about directives this would be my second approach:
I have a AppointmentService which holds the actual week. This service
can load the next week and the previous week. Then i create a directive, loadWeek,
which adds the onClick event to the element who uses's it. This onClick
method call's the methods from the appointmentService. And my view directly
binds to the appointmentService througt the controller.
Which approach would be better? Correct me please, if i am completely wrong.
And can you give me some other examples from the practice for directives?
Sincerely,
Cristan
This doesn't sound like a use case for a custom directive to me. Directives are used to link your controllers/scopes to the DOM (remember, all the ng-xy bindings are directives provided by AngularJS). A directive mostly is somehow linked to a scope (and it's controller therefore).
Defining the information which you want to show and changing them based on the user inputs is a typical task for the controller. Do you have any problems implementing the controller or are you considering directives?
I believe a directive could be what what you're looking for, but you may also just want a controller in this situation. The controller handles getting the loadPrevious/next events and uses the service to get the data.
Where you would want to involve the directive, in my understanding, is if you wanted to create an html template that went along with it which can do cool stuff like transclude other elements.
For convenience: https://docs.angularjs.org/guide/directive

Is there a good way to 'Freeze' Angular scopes and children that are not being used

I am working on a project that basically consists of complicated form containers repeated in an ng-repeat. Each of these forms has enough functionality that it could easily be its own angular app, but instead they are repeated on the screen, and there can be up to 30 of them.
With 5 or more of these on the screen, performance becomes very slow. Although I tried to disbale some of our heavier features, it seems that the big performance wins just come from disabling the 'uninteractable' scopes.
I am looking for a good pattern to 'freeze' the unused scopes. I want them to maintain their state, but I don't want them to listen or be attached to anything until the user focuses on the form that they apply to.
Without seeing your code my first thought is to use a directive to control that.
You could use the bind to the forms, and or use $watch(using $watch would depend on your setup) so that you have finer control over the scope.
here are some links:
for directives in general
for $watch and other ways of controlling the scope
Are all the forms in the view port at the same time? If not, you could add / remove forms to the DOM (using ng-include f.e.) based on whether they are visible to the user. This way watches are removed and added again when needed.
Otherwise, if you have a lot of watches you specify in your form controller, you can unwatch when not focused and rewatch when getting back the focus. This won't reduce the number of watches created by the (native) directives inside your form. This number can perhaps be reduced by using bind-once.

Categories

Resources