user663031
user663031

Reputation:

Ember.js: sending actions to components?

I have a mixin for Ember components, named in-view, the job of which is to request that that the element be brought in view. It is provided an attribute whose value is an piece of content to be brought into view, and if that attribute matches the component's content then I call scrollIntoView or the equivalent. The code looks something like this:

// calling template
{{#each items as |item|}}
   {{my-item content=item inViewItem=inViewItem}}
}}

// mixins/in-view.js
scrollIntoView() {
  if (this.get('content') === this.get('inViewItem'))
    this.get('element').scrollIntoView();
}.on('didInsertElement')

// components/my-item/component.js
import InView from 'mixins/in-view';
export default Ember.Component.extend(InView, 

This works fine. The question I have arises when I want to change the item in view. I can have the in-view mixin observe the inviewItem attribute:

}.on('didInsertElement').observes('inViewItem')

and this also works, but seems like a bit of a code smell.

In addition, my actual code structure is that there is a controller which knows which item is supposed to be in view, and then its template calls a my-item-list component which displays the scrollable div containing the item list, and that in turn calls the my-item component. This means I have to pass the inViewItem attribute from the controller down through two levels, as in

// resource/controller.js
inViewItem: something

// resource/template.js
{{my-item-list items=item inViewItem=inViewItem}}

// components/my-item-list/template.js
{{#each items as |item|}}
   {{my-item content=item inViewItem=inViewItem}}
}}

I could avoid this by having the my-item template hard-wired to access the inViewItem attribute on the controller:

scrollIntoView() {
  if (this.get('content') === this.get('controller.inViewItem'))
    this.get('element').scrollIntoView();
}.on('didInsertElement')

but that's another code smell; I don't want to build this kind of dependency on a specific controller field into the mixin. Instead I could possibly pass the component the name of the controller attribute to watch, but this seems unduly clumsy, and it's tricky to observe an attribute whose name is variable. More importantly, I don't think this will work when controllers go away in 2.0.

What I want essentially is a way to "ping" or somehow send a message to a template. I know that in principle this violates the DDAU principle, but in this particular case what I need is exactly to somehow send an "action down"--an action telling the component to adjust itself to bring itself into view.

Of course, I could give up on the entire idea of the in-view mixin and simply have the controller dig down into the generated HTML to find the item to bring into view and issue the scrollIntoView on it directly. However, this seems to violate some principle of separation of concerns; the my-item template would no longer be in complete control of itself.

What is the recommended design pattern for this kind of case?

Upvotes: 1

Views: 311

Answers (1)

runspired
runspired

Reputation: 2693

The solution here is to go the opposite direction that you have. Your component here is a localized scope, and the pain you are feeling is that your localized scope needs to access and mutate global state (the app's scroll position).

Some people use a scroll-service for keeping track of and mutating state, I've used several variations on that myself.

It sounds though like you're dealing with a scrollable list, perhaps a div, and that what item is in view isn't merely a function of page state, but programmatically may change. For instance, a new item has been inserted and you want to scroll the new item into view.

A plugin like jquery.scrollTo or similar (collectively "scroller") would be better for that than simply jumping to the new position as it preserves the user's contextual awareness to where they are on page.

With a scrollable div or list or similar, you might choose to have your top level component control scroll state. The scroll state is still localized in this case, but instead of being localized to each item it's been localized to the scrollable region as a whole, which is where it better belongs.

There are a number of patterns for list items to register themselves with a parent list-component. In a robust scenario, I might do so, but a quick and not very dirty approach is to do something wherein on didInsertElement the new child emits an action to the parent containing it's context, which the parent then uses to check if it's the active item and if so triggers the scrollTo.

Upvotes: 1

Related Questions