Can directives expose methods to code outside the directive?

4,848 views
Skip to first unread message

Dave Merrill

unread,
Jan 6, 2013, 2:09:10 PM1/6/13
to ang...@googlegroups.com
(Just want to thank the folks who've been answering my incessant questions as I ramp up here, and also the bystanders who put up with them. Hopefully some of this clarifies something for somebody besides me...)

Two semi-related questions:

- Can directives have methods that code outside the directive can call? For instance, say it renders some fields. How could you have a button outside the directive's template or original element call a method/ defined by the directive that clears those fields?
  You could move that method to an enclosing controller, but in this case it needs to reach into the directive's scope and touch the vars defined there

- Is it possible to configure a directive globally, in such a way that it affects all instances of it? For instance, say a directive renders some buttons whose texts you can configure via attributes. How could you configure those texts once, and have it apply to all instances, at least ones created after that, if that's easier?

Thanks again,
Dave

Peter Bacon Darwin

unread,
Jan 6, 2013, 2:54:44 PM1/6/13
to ang...@googlegroups.com
First question.  How would you refer to the directive so that you could call anything on it?  This would require knowledge of the DOM element on which it is applied, which is against Angular Zen.  In AngularJS one works with the model and the model only in the application, letting the directives take their queues from changes to the model.  In the example you offer, your external button would clear the model to which the fields refer, which in turn would cause the inputs to clear themselves.

A global directive?  In Angular-UI, there is a "global" service, called uiConfig, which is injected into all angular-ui directives.  They look for default values in uiConfig and then use attributes on the directive itself to override these. (Actually uiConfig is a "value", which is a service that doesn't have a factory to create it.)

Pete


--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To post to this group, send email to ang...@googlegroups.com.
To unsubscribe from this group, send email to angular+u...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en-US.
 
 

Dave Merrill

unread,
Jan 6, 2013, 5:22:02 PM1/6/13
to ang...@googlegroups.com
Thanks for getting back Peter, as always.


Re how you'd refer to that function, that's the question, isn't it? Re manipulating the model in question directly, the model that needs to be updated is internal to the directive, so methods to touch it really need to be part of the directive too.

Maybe a better example is a video player. It renders its own controls, but ideally, it also exposes an API that other elements on the page can use.

Best I've been able to figure out is to create the method on the scope as usual, then do something like this:
  angular.element($('#idOfDirectiveElement')).scope().someMethod(whatever);
It does mean you need the id of the element created by the directive instance you want to call, or some other way to access it. That's not ideal, but I don't see any other way. The directive's methods and vars only exist within its own scope (good), and there's no other handle to it.

I don't absolutely have to do this in my actual use case, just trying to understand what's possible/reasonable. I think I would have if there had been a happy way.


Re global defaults, it sounds like Angular-UI has the feature I'm looking for, but only for its own directives. Some angular services expose their own defaults too, like $http. 

So I guess if I really wanted this, I'd wrap my directive in a service, with one or more config methods that stored the defaults you set in service-scoped vars, and have the directive itself look there first. Not sure I care that much in this case.


Dave

Joshua Miller

unread,
Jan 6, 2013, 6:06:30 PM1/6/13
to angular
Hi!

I see a couple ways to do this but, as Peter said, using the DOM is not a good idea. Here are two solutions to think about.

You could use events. If your directive listens for events (`$scope.$on`) then your controller can $broadcast events (with optional arguments) to your directive. This has tradeoffs and won't be good in every case. But you could potentially "expose" an API through event listeners.

But I imagine in many cases this will be unnecessary and overly-complicated. In your video player example, some methods normally exposed as an API work really well as attributes. You don't need something like `directive.play(file)` because you can have a `file` attribute, and the directive can respond to changes in it. You could also have state, timecodes, etc., as attributes and, with bi-directional binding, you get a much more seamless interface between controller and directive. Plus, you need to know neither anything about the internals of the directive nor anything about its DOM structure. For many directive use cases, attributes _are_ the exposed API.

Anyone have other thoughts?

Josh

ganaraj p r

unread,
Jan 6, 2013, 6:13:18 PM1/6/13
to ang...@googlegroups.com
I was about to say the same thing josh.. You just said it better. 

@Dave : At the end of the day directives are either attributes or tags.. If you want to configure them with multiple arguments, then I guess they have to be tags.. 
I wouldnt recommend the event's approach.. Though it does decouple the caller and the callee.. I think it should be used only when you rule out all other options. 
Regards,
Ganaraj P R

Tony Polinelli

unread,
Jan 6, 2013, 11:07:28 PM1/6/13
to ang...@googlegroups.com

It seems to make sense to me that you can reference the scope of a directive, and access methods too. 


This is basically binding the scope of the directive to the variable parsed in as an attribute - test="myVar"  

It seems that the controller is created before the child directives (makes sense) so we need to wait till its init'd - could be done better?  Is this really a break of  some zen rules? to me it seems pretty handy

cheers

Dave Merrill

unread,
Jan 6, 2013, 11:12:35 PM1/6/13
to ang...@googlegroups.com
As long as everything that needs to talk to the directive is inside it, life is simple, configurable through attributes, scriptable through methods on the directive's scope. 

But suppose another element outside the directive has to control what video is playing?

Seems like the options are events, DOM access, or probably best, wrapping the directive in a service that can be injected into whoever needs it.

Dave


On Sunday, January 6, 2013 6:06:30 PM UTC-5, Joshua Miller wrote:
Hi!

I see a couple ways to do this but, as Peter said, using the DOM is not a good idea. Here are two solutions to think about.

You could use events. If your directive listens for events (`$scope.$on`) then your controller can $broadcast events (with optional arguments) to your directive. This has tradeoffs and won't be good in every case. But you could potentially "expose" an API through event listeners.

But I imagine in many cases this will be unnecessary and overly-complicated. In your video player example, some methods normally exposed as an API work really well as attributes. You don't need something like `directive.play(file)` because you can have a `file` attribute, and the directive can respond to changes in it. You could also have state, timecodes, etc., as attributes and, with bi-directional binding, you get a much more seamless interface between controller and directive. Plus, you need to know neither anything about the internals of the directive nor anything about its DOM structure. For many directive use cases, attributes _are_ the exposed API.

Anyone have other thoughts?

Josh

Dave Merrill

unread,
Jan 6, 2013, 11:16:51 PM1/6/13
to ang...@googlegroups.com, to...@touchmypixel.com
May be a plunker thing, but this doesn't work for me. Console shows "Uncaught TypeError: Cannot call method 'method1' of null". I tried lengthening the timeout, no difference, didn't have time to look further.

Dave

Joshua Miller

unread,
Jan 6, 2013, 11:51:45 PM1/6/13
to angular
Hello!

This is a great discussion.

@Tony - I would say this violates Angular Zen and it's kludgey. But more than that, it's unnecessary. Directives are enhancements to HTML. You shouldn't need to "call a function on a directive"; in the vast majority of cases, needing to do so means that the approach to the problem was fundamentally flawed. And like Dave, I got the same error on Plunker.

@Dave - Here is an example of one directive setting the "video" on a second: http://plnkr.co/edit/JeMiv4?p=preview. In most cases, we just don't need services or events (and definitely not knowledge of the DOM) to communicate between components. The code is simpler and cleaner than it would otherwise have been. But of course there are good use cases for services and events. And on very rare occasions, we can't get around _some_ knowledge of the DOM. But they're rarer than one might think.

This is more difficult to talk about in the abstract, and it seems counter-intuitive, but once we start thinking the "angular way", a lot of the confusion drops. That said, if anyone wants to suggest a real-world use case, we should talk about it.

Josh

Tony Polinelli

unread,
Jan 7, 2013, 4:02:11 AM1/7/13
to ang...@googlegroups.com
Interesting that it didnt work - that worked in chrome. not FF. Im now following the way that angular-ui parses in variables. Should work. 


I agree that it might not be the way to go- but in more complex circumstances, it at least its an option. 


Peter Bacon Darwin

unread,
Jan 7, 2013, 4:37:00 AM1/7/13
to ang...@googlegroups.com
The more coupling between the different layers in the application the more complex it becomes, which makes it harder to test, harder to maintain and more likely to have bugs.

The Angular Way is to have all the application logic in Controllers and Services.  The View (i.e. the HTML) is purely declarative and does not have any business logic.

If you want to "send" a message, which is what you are really talking about here, to a directive from a Controller then that Controller must know about the directive somehow.  This couples the Controller to the directive in a way that adds complexity.

AngularJS goes to great lengths to prevent this kind of coupling.  Instead all communication between the business logic and the view is through the scope.  If the directive must do something in response to something happening in the business logic then the directive must watch something on the scope and act when it changes, then the Controller, Service or other Directive that triggers the action simply changes the scope and voila!

So instead of things "knowing" about each other, we have an idea of shared knowledge.

In the case of a video player with external buttons, we would have a "video" object on the scope at some place to which all the directives have access.  Then the video object would have a state, such as "playing", "paused", "stopped", "position", etc.  External buttons would update this state and the Player directive would change what it is doing in terms of displaying the video, based on changes to this state.

This makes it really easy to unit test in isolation.  For instance, to test the external buttons you simply click them and check that the video object has been updated.  You don't need to mock up a player or even worse have a real player and check that the video is actually playing.  To test the player, you make changes to the video object and check that the player acts accordingly.

Does that make sense?

Pete

Dave Merrill

unread,
Jan 7, 2013, 7:32:56 AM1/7/13
to ang...@googlegroups.com
Good discussion.

My real world case is a set of recordset navigation buttons (first-previous-next-last). Mostly this is easy, but there are edge cases I was thinking of how to handle. For instance, say I'm looking at records 51-100 out of 257, change my search criteria to one that (I don't know yet) will find only 23 rows, and click Next. The server can deal with this is some semi-intuitive way, and when the new data comes back the btns can adjust themselves. However, the reason _any_ server behavior in that situation is only sort of logical is that that action shouldn't have been allowed, because its meaning is ambiguous and/or counter-intuitive.

What I was thinking is that modifying the search criteria, which are unknown to the nav btns, and probably should be, should disable all navigation, so you can only click Search again, not navigate through a recordset that's changing out from under you. The question is, what's the Angular way to do that. 

I don't like the btns observing the fields of the search form, feels like wrong coupling to me. 

Having the parent controller observe them (which it'll probably do anyway to send search requests to the server), and call an enable(false) method of the nav btns instance feels better. Doing that means that there has to be some entity somewhere that has access to that nav btns instance.
 
In the real real world, I might well just handle it on the server, and call it Done. But I'm still trying to get my head around the Angular way, and what's possible and not, so I'm banging on this more than the use case probably deserves.

Thoughts are welcome.

Dave Merrill

Peter Bacon Darwin

unread,
Jan 7, 2013, 7:38:49 AM1/7/13
to ang...@googlegroups.com
You need to stand on your head!  Instead of telling the buttons whether they should be enabled or not, you provide a function on the scope called, say, hasNext(), which returns true if it is allowed to go to the next item.  This function can check whether there is a next record to go to or return false if a request to the server is currently in progress.  Then your button can have ng-disabled="!hasNext()" attribute on it.

You model (scope) is the truth and everything works of that.

Pete

Witold Szczerba

unread,
Jan 7, 2013, 11:40:36 AM1/7/13
to ang...@googlegroups.com
Hi,
i think the root cause of your confusion is exposed in the citation below. You do not think the MVC way. I was writing about this several times on this group and will write it again: in MVC the "View" part does not "think", it does not tell anyone what to do, because "View" is supposed to reflect model state on screen. What do you mean by: my directive plays video and another view component is controlling that video is playing? View components (directives) are not the source of truth about what is going on in the system. Each directive can inject any service it wants, it has access to methods exposed in the embedded scope, and it can listen to events. But the event source, the scope methods and services does not come from another directives but the Model-Controller buddies. I always ask a question:

WOULD you application still be able to work when you remove the "View" layer? Of course it does not make sense to remove "View" from web application, but you get the idea.

If your directives are actually making decisions then this is not MVC and you are in embroiled environment, see:
http://bit.ly/XDRGqQ

:-)

Regards,
Witold Szczerba

Dave Merrill

unread,
Jan 7, 2013, 11:46:28 AM1/7/13
to ang...@googlegroups.com
Hmmm, could well be I'm inside out, maybe.

As it stands, the directive observes some of the passed attributes (lastStartRow, lastEndRow, rowsPerPage, and rowCount'), and calls its internal updateBtnStates function when they change. That function updates scope.canPrev, scope.canNext, scope.show, and scope.rowsShowingMsg (scope here is the directive). All of that is working, though maybe that's inside out too; poke me if I'm thinking about this wrong.

The missing piece is that once a search has been run and the nav btns are showing, any change to the search criteria should probably disable all nav btns. 

As I understand it, you're proposing a new function in the calling scope, outside the directive, that the directive would call, for an additional level of "should my buttons be enabled" check. Am I understanding you right? If so,I don't think I can use the result of that function directly in ng-disable, since the existing logic in updateBtnStates needs to be honored too, to disable prev if you're already at the first row, disable next if you're already showing the last row, and hiding the whole thing if there aren't enough records that navigation is needed. All of that is separate from the question of whether the search form, outside the directive, has been modified since the last search (getting new results back is what updates those observed attributes mentioned above, setting the button states).

There could be a disableFunction attribute, assigned something like disableFunction="areNavButtonsDisabled()", and the internal updateBtnStates function would disable all btns if it was true, otherwise us its existing logic.

Does that sound like a better way to go about this?

Thanks, 
Dave

Dave Merrill

unread,
Jan 7, 2013, 12:00:35 PM1/7/13
to ang...@googlegroups.com
I get that some model is the state the view should render from. 

My intent was that the directive should contain and manage the parts of the model that enable and disable the buttons, in a self-contained way, depening only on passed attributes. It calculate those directive-scoped model fields based on the passed attributes, which tell it the start and end rows, number of rows in the result set, and the number of rows to show per page. The controller shouldn't have to manage the btns explicitly itself, just pass those pieces of state into the directive. They  come from the UI (rows per page) or the server (everything else), all controller scope vars.

The problem is that the state of the search form is also relevant for this semi-edge case. Making the directive aware of the search form fields seems wrong, hence my initial desire for a callable API the controller could use. If I understood Pete correctly, he's proposing an additional function in the calling (controller) scope that would watch for modifications to the search form, and return isDisabled if it had changes since the last search.

Does that still seem inside out to you?

Dave

Witold Szczerba

unread,
Jan 7, 2013, 7:12:32 PM1/7/13
to ang...@googlegroups.com

The solution seems to be ok, but the final judgement would be possible after looking at implementation.

All I wanted to say is that you have to place the model-controller always in the privileged position.

Question: do you write unit tests? If not, do you write your code as if you were writing them? In either case you would never let the directives drive your user stories.

Thinking this way: let you write,in list, all your expectations about how would you like user to use your application. Could you codify all that as the expectations and mocks against controllers, services, events and  whatever else there is, excluding view artifacts?

Of course without being paranoic. For example: you have simple paginator. It knows how many items there is, which page you are on and what is the number of items per page. There is no need to expose trivial methods on scope like
isFirstButtonEnabled()
isNextButtonEnabled()
etc...
Why? Because that methods are so trivial you would't like to waste your time writing unit tests for them. You can go ahead and explicitly implement them on the view:
<a href... ng-enabled="page>1">...</a>
But once it is getting tricky like when other parties do participate in the situation, you would certainly like to hide/encapsulate the logic in controller and use all the greatness of AngularJS to decouple that logic from DOM/view stuff, staying st the simple level of pure JavaScript (the language), so you can test-drive the use cases in isolated environment. Your view will stay simple no matter what.

Regards,
Witold Szczerba
---
Sent from my mobile phone.

Reply all
Reply to author
Forward
0 new messages