Skip to content

Conversation

@chadhietala
Copy link
Contributor

@chadhietala chadhietala commented Aug 1, 2018

Copy link
Member

@rwjblue rwjblue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you for putting this together @chadhietala!

Some things:

  • Can you update the bounds section to make it clear that the bounds object itself is stable?
  • Can you show a few examples of component templates, and show what bounds would be for them?


In [RFC#2013](https://github.com/emberjs/rfcs/blob/master/text/0213-custom-components.md#detailed-design) we outlined the component lifecycle and how a custom component is associated with a manager. This design builds off off of that existing work and concepts.

### `elementHook` capability
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elementHook doesn't really seem to fit with the other capability names. Suggestions:

  • elementAccess
  • domAccess
  • domAccessCallbacks
  • domCallbacks
  • elementCallbacks
  • bounds (I like this one best 😄)

Thoughts?

});
```

### `didRenderLayout` hook
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this is the name internally in the glimmer-vm, but I don't think its particularly relevant as a name.

Possibly: didCreateBounds?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, can we avoid creating a new hook here and instead add bounds as a second argument to the already existing didCreateComponent hook? didCreateComponent is part of the asyncLifecycleCallbacks capability, and per the prior RFC is where didInsertElement should go.

The name seems much better, and the intent seems very much in line with what we are doing here...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind bike shedding about the name, but I don't think we want to bundle it with didCreateComponent. There may be situations that you need one but not the other, and we want to keep things as granular as possible. Plus, bundling the hooks forces their timing to be the same, which we probably don't want (and they are in fact different today).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I agree. Still would prefer another name than didRenderLayout it really doesn't feel cohesive with the other component manager method names.


didRenderLayout(component, bounds) {
component.bounds = bounds;
component.didInsertElement();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the prior RFC didInsertElement is more appropriately called in didCreateComponent (which was an optional asyncLifecycleCallbacks capability), I don't think we should recommend changing that...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I think we need to recommend against calling any kind of user hooks here.

```

### `didRenderLayout` hook
Once the rendering engine has constructed the bounds for the component the `didRenderLayout` hook will be called with the `component` and a `Bounds` instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This phrasing is slightly off, the hook does not receive "the component" it receives the opaque result of calling createComponent (aka the "state bucket"). Different component managers may return the instance itself but it may also return some other POJO/state bucket.


In other words, components **should not** rely on the timing of DOM insertion outside of the component's own bounds.

### `willDestroyLayout` hook
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming we can keep the bounds as a second arg to createComponent (one of my suggestions above), this can become willDestroyComponent...


### `willDestroyLayout` hook

To balance `didRenderLayout` we will expose a hook called `willDestroyLayout` that is invoked during destruction. It will receive the `component` instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't get the component instance, it gets the "return value from createComponent" (may be a state bucket, may be a component instance)

type FirstNode = Node;
type LastNode = Node;

interface Bounds {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the lack of timing guarantees of construction of the parent do think this actually useful?

Copy link
Member

@chancancode chancancode Aug 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to provide access to the parent. What we cannot guarantee is that the parent's children (i.e. your siblings) are fully constructed. (And possibly that the grandparent may not be construed yet.)

By the way, I think you meant to say:

interface Bounds {
  firstNode: Node | Bounds;
  lastNode: Node | Bounds;
}

@chadhietala
Copy link
Contributor Author

I think I incorporated the feedback.

Copy link
Member

@rwjblue rwjblue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, I'm 👍 here...

@lennyburdette
Copy link

Has anyone proposed a react-style “refs” api? Instead of relying on the framework to create special properties, we could have a declarative api that allows decorating the template with special attributes that create JavaScript-accessible node references.

Something like <div ref="a" /> to be accessed as this.refs.a in lifecycle hooks.

I’m just curious. Both APIs could exist simultaneously.

@JCBarry
Copy link

JCBarry commented Aug 10, 2018

Not frequently, but I've had the need for this in more complex components and the workarounds aren't pretty or easy to test. 👍

@chancancode
Copy link
Member

We discovered a problem with this design in the core team meeting last week. The intent of this is to offer functionality akin to classic component's this.element, didInsertElement and willDestroyElement. The common use case is to attach and cleanup jQuery or native event listeners.

On the surface, it seems like this gets the job done. However, consider the following component template:

{{#if this.isSelected}}
  <strong>selected</strong>
{{else}}
  <em>select me</em>
{{/if}}

Ignoring the whitespace nodes, the bounds object will point to the strong or em element depending on the component's this.isSelected state, which could change back and forth multiple times during the lifecycle of the component. Therefore, if you attached an event listener on the element at initial render time, you may not get a chance to detach it before the element goes away, thus leaking memory. Dereferencing the nodes referenced by the bounds and then saving them off or closing over them with a closure has the same issue.

Of course, in a sense this is no different than attaching event listeners directly to descendants of this.element, since they are also subject to change without notice. However, as there is a stable root element, the event listeners are almost always attached there in practice.

There is still a class of use cases that is somewhat compelling and does not have the problem. For example, a component may use the bounds to "autofocus" the first input element. This only involve accessing the bounds' content synchronously and does not require any cleanup. In general, these kind of "snapshotting" use cases are fine. However, they may call for more hooks, like one that runs after the bounds' content has (potentially) been updated so they could reapply the logic.

So we can adapt this proposal for that kind of "snapshotting" use cases, perhaps adding the update hook and possibly removing the teardown hook (cause it is not needed, and may be misleading). We can then explore alternatives for the original use case, perhaps a modifier/ref-based design or an additional capability that ensures (at runtime) that the component has a stable root element. However, my worry is that since the original use case is the most common and compelling one, having this API around without a solution for that use case will mislead our users and causing them to misuse this API.

@chancancode
Copy link
Member

Relevant: emberjs/ember.js#12500

@chadhietala
Copy link
Contributor Author

Closing in favor of #353

@chadhietala chadhietala mentioned this pull request Aug 25, 2018
@rwjblue rwjblue deleted the bounds branch January 23, 2019 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants