My question is about the communication between components in the Maquette Javascript framework.
Imagine I have a Menu sub-component used in an Application component: the Application instance would like to know when a menu item is selected in the Menu instance in order to swap the main content displayed in the application (for example). In other words, I need a way to communicate between a child component and its parent component. How can this be achieved in Maquette?
Sure, I can pass a callback owned by the application instance to the menu instance, which will be called when an item is selected. But I'm a bit reluctant to do that because the "selection" event is just a "rendering-side" aspect of the menu component, so I would prefer the event not to leak into my Menu API, but to stay inside the render function/method instead.
So, I would like to deal with events at the "rendering-side". But I guess it means I have to send a CustomEvent from the Menu's render function and register a CustomEvent handler inside the Application's render function, right? Is this use-case supported in maquette? Are there other alternatives to CustomEvent for my use-case?
Thanks!
PS: question reposted here from ticket #71 in order to get more answers.
What we usually do is pass callbacks to components that get invoked when items get clicked. A Menu could be constructed with a callback menuItemClicked(menuItem: MenuItem) for example. Imho this does not leak any application-specific knowledge into the menu/menuitem. The creator of the menu could do anything inside the callback like routing or changing a variable.
An event system is also certainly possible. I do not recommend using the DOM hierarchy to bubble the events though. I can imagine the Application creating the menu using menu = createMenu(...) and afterwards calling menu.addEventListener('itemClicked', navigate) or something.
Related
I am just wondering, why do we define every component function in one place (e.g index.js) and then pass those functions down the components? Does that make any sense?
E.g I want to define a click handler for a list button component, and another click handler for some forms button component, why would I define them all in one place? Isn't the divide-and-conquer approach better?
I mean, why would I not just define my click handlers in the components themselves, where they belong? beside the fact, that everything in one place gets messy and hard to upkeep...
I want to define a click handler for a list button component, and another click handler for some forms button component, why would I define them all in one place?
Because of the nature of the unidirectional data flow that React employs, state flows down the tree and events (i.e. changes) are passed back up via handlers such as the ones you mention. Components deep down in the tree can (or rather, should) only propagate changes back up via clickHandlers and the like.
The reason that handlers are often defined together in only a few places is because those places typically represent points where common state is shared, so the events that update that state need to be handled there.
The custom component
I've created a custom component for the navigation of my app. It consists of an ul element and all its li elements are dynamically created based on the items in the router's navigation list. This is happening in the attached event of the component. Nothing special is going on here.
The custom attribute
But because I want to have a good looking fancy menu I also created a custom attribute and implemented it into the root ul element of the custom component. In the attached event of the custom attribute I'd like to do some dom manipulations so that my menu looks like a menu created by those cool kids.
The problem
Although the attached event of the custom attribute is fired AFTER the attached event of the custom component, the dynamically created li items are not part of the dom in the attached event of the custom attribute yet.
The question
My assumption was that on attached event the view of the actual component is attached to dom and that all components before are also attached to the dom. And when I am done with the attached event the html that has been dynamically created here is also attached. Am I mistaken?
On a side note
I am aware of using TaskQueue could solve my problem. However, I would like to know if there's a different approach/solution first because I believe that moving things in time could cause a chain of paradoxes leaving you in a maintenance nightmare.
Allow me take away your misconception about the TaskQueue.
Your idea of moving things in time causing maintenance issues would certainly apply to using setTimeout() because that incurs an actual delay, and forces execution onto the next event loop. queueTask() is more or less the same thing (it uses setTimeout() internally).
However, the TaskQueue's queueMicroTask() method works very differently.
When you call queueMicroTask(), unlike queueTask() and setTimeout(), the task is scheduled for immediate execution on the same event loop. This is a very robust mechanism with a guaranteed execution order, and it's generally considered good practice to use it within attached() before doing any DOM manipulation.
In fact, queueMicroTask() is used internally by Aurelia in various places (mostly in binding and templating-resources). Two notable places are:
Property- and collection observers use it to "delay" notifying subscribers until all other bindings have completed their internal updating work
The repeat attribute uses it to set an ignoreMutations flag (and unset it after the queue is flushed) to prevent infinite recursion while updating its inner collection
You can generally consider there to be two "phases" to the bind() and attached() hooks: a non-queued, and a queued phase. The queued phase is when components do work that relies on the whole component graph to first be done with some other (usually recursive) process.
queueMicroTask() does not delay execution, just pushes it to the end of the call stack
It's the functional equivalent of passing the function as a callback to the end of the call stack, but saves you the trouble of writing the spaghetti code required to locate that last call and wire it all up. It's super clean.
all its li elements are dynamically created based on the items in the
router's navigation list. This is happening in the attached event of
the component
See, whenever you create anything during attached(), you can't rely on that thing being there during another component's attached() as this depends on the order of compilation/composition. That's an internal matter. This is especially true for custom attributes. Custom attributes (particularly those in style libraries) use the TaskQueue all over the place because it's the only way they can rely on the DOM being done.
Using queueMicroTask() here will guarantee two things:
It's executed when aurelia is completely done with the "first pass" of attacheds and rendering
It's executed immediately when aurelia is done with that - not even a microsecond delayed.
The best (and perhaps only correct) way to address this is indeed by using the TaskQueue - I promise :)
We can follow this approach Detect click outside element?
in Angular as well with HostListner.
In my component I have registered a host listener which listens on document click and hide the menu inside my component. This approach works fine with a single component.
However I am a bit concerned about the performance when the component is instantiated(in a list) 100 times each component registers document on click and do some processing to hide the menu opened from its component if any.
I can move the code outside the component list but then I need to hold references of each component to detect which one has opened menu and should be closed.
None of these two approach is perfect is there some other way to handle this efficiently as well as without leaking the component code in its parent?
Instead of setting the HostListener on the component, use it in a directive and set it on that component's wrapper. The wrapper could be an ng-container if you wish to not have template modification.
I am trying to make a tabbed windowing system within a webpage using om-bootstrap's "pills" navigation by adding tabs when links get clicked and removing tabs when an X button on the tabs is clicked.
I need to know how to add and remove data from the global state/store and create a macro that can be used to declare a tab app component and make it remove itself when it is no longer alive.
What is the best way to reference the global state? How can I make a component remove/unmount itself when it gets closed?
Since removal of a subcomponent affects its owner, you should let the owner (i. e. the "tab system") know that this tab needs to be closed/destroyed/obliterated.
I've digged through todomvc example (live) assuming your process of destroying tab panes is pretty much the same as destruction of TODO items there. I see nothing ocnflicting so far. Here are my findings:
A channel is used.... When application starts (IWillMount), a (chan) (from core.async) is written into application state at :comm key.
...for event handling.... Events from the channel are handled in the loop following that code, in go-form, asynchronously with the block it appears in (with <! being a "kinda blocking" operation). Well, you may know it, I didn't, still learning what is CLJS all about.
...that is passed to all child items' init states.... So it becomes a way for children to send events to the root. I'm starting to like this.
...so they can send events to their parent! This is done in put! calls with the comm channel, fetched in the linked line. Events put there are handled by the loop defined in (2), which delegates them to appropriate functions depending on type (accompanying keyword).
I'm nowhere near a ClojureScript pro, but I'm learning. So if the above doesn't make sense, this is normal and means I didn't understand something. If that turns out to be the case, putting me back on track would be much appreciated.
I'm working on a web app in Google closure where the structure is something like this:
App
+ Control Pane
| + Search Box
| + Search Button
+ Result Pane
+ Results
+ Next Page Link
The actual component structure is quite a bit more complex. The important point is that there are many different components all over the component tree that can initiate certain actions. In this example, hitting enter in Search Box, pressing Search Button, or hitting Next Page all require a query to be made.
This is simple enough to handle. Any child anywhere in the component tree can do
this.dispatchEvent(App.EventType.ACTION, ...)
App will be able to listen to it when the event propagates upwards. The problem is the other direction: When App receives data from its query, it must push it to all children.
It seems very brittle for App to try to push directly to Search box and Results, as their location in the component tree is subject to change. What I'd like to do is fire an App.EventType.DATA_RECEIVED event and have all children (and sub-children, etc.) hear it.
The only way to do this in google closure that I've been able to find is to make a global public singleton instance of App and use as the source for all App.EventType.DATA_RECEIVED events, or to plumb App through to all children and subchildren.
Both of these are messy and brittle in their own way.
Is there a simple way in closure to dispatch events that bubble downwards?
It's not a very satisfactory answer, but it's what I settled on:
There's no good way to communicate such things down the component tree. Even closure itself bumps into this problem, passing opt_domHelper down the tree to every subcomponent.
My suggestion is to subclass goog.ui.Component for your app and create a myapp.Environment class, which contains both opt_domHelper and other environment variables, such as one event listener designated as the application's event channel.
It's not a good solution, per se, but it's the least of all possible evils. And if you've already been dutifully passing opt_domHelper everywhere, then it's no worse a problem: that plumbing becomes more extensible, and the opt_domHelper itself is hidden from implementors (who now pass around environment instead).