mitchlloyd / ember-islands

Render Ember components anywhere on a server-rendered page to create "Islands of Richness"
MIT License
236 stars 24 forks source link

Dynamically adding component #41

Closed sigmer closed 7 years ago

sigmer commented 7 years ago

I'm just getting started with ember and I've successfully added some ember components to my server-rendered app using ember islands! But now I have a case where a fragment of html is fetched asynchronously from the server and added to the page. In that html I'd like to define an ember island component. Of course it does not render when inserted into the dom where ember has already booted. Any thoughts on a way to render a component when it's inserted into the page after ember has booted?

sigmer commented 7 years ago

I guess this was more of a general ember question. I did it by resetting the app with Ember.Application.reset() when new components are dynamically added. Probably not the best thing to do.

mitchlloyd commented 7 years ago

That's an interesting solution. It might be a little heavy since it clears out all the objects that Ember has, but in this case that might not be much.

Someone else asked this same question recently (maybe it was you!), so I'm wondering if we should come up with a better solution. The first thing that comes to mind is exposing some global function EmberIslands.rerender() that would check the list of rendered elements, tearing down any whose DOM element has been removed and adding new components whenever an new marker is found.

What do you think?

irenezyj commented 7 years ago

Mitch, I asked the same question and thanks for your response. Look forward to your solution for it.

sigmer commented 7 years ago

A global function that handles removed/added island elements would be very useful in this situation. I think it's better than resetting the ember application. Thanks!

Your addon is a great bridge to introduce ember in a large, server rendered app.

adambedford commented 7 years ago

I'd also love to see something like this! I'm loading remote modal content and I'd like to be able to have an ember component rendered in the modal

mitchlloyd commented 7 years ago

Note to self or someone else wanting to tackle this:

This can probably be accomplished while still adhering to the public API of Ember's components.

  1. Add an option to install and remove a global reference to the component on init and destroy. It might even make sense to leave this up to users and add documentation. Users can then call the public rerender method on the component.

  2. In the didUpdate or willUpdate hook (not sure which is better)

    • run queryIslandComponents again
    • destroy() any components that no longer have an element
    • rerender() components that are still on the page
    • call renderComponent for any new components

Edit: This approach changed quite a bit. Overloading these hooks had some complications and didn't provide much value given that users just need some global function to call and shouldn't have to know much about the ember-islands component itself.

acorncom commented 7 years ago

I'm interested in this as well 😀

mitchlloyd commented 7 years ago

Took a shot at this tonight. The implementation in Ember Islands simple, but there is an issue with how Ember handle's destroying root components that currently makes this infeasible.

As of this commit Ember does not remove root elements from the renderer when they are "cleaned up". This means that if you remove DOM, you'll be leaking memory since the list of components referenced by this._roots will never get smaller. In Ember Islands tests with manifests with a error:

Error: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Which comes when Glimmer is trying to clear the root component which is no longer in the DOM.

As always, landing splat in Ember would mean we could stop using multiple root elements which would fix this problem. Also I'm curious if the new Component Manager API could help us get away from rendering multiple root nodes.

In any event the next task here is probably to add a test to this file that shows you can use appendTo, call destroy and remove a root component:

https://github.com/emberjs/ember.js/blob/69c9f1e3b3e3c55a3ca0be9921d209394df00c7e/packages/ember-glimmer/tests/integration/components/append-test.js#L466

mitchlloyd commented 7 years ago

You can try out the new "rerendering" branch by adding this to your package.json:

"dependencies": {
  ...
  "ember-islands": "mitchlloyd/ember-islands#rerendering"
}

Then inside of an initializer, app.js, or any file that is imported in your Ember app:

import { reconcile } from 'ember-islands';
window.whateverNameYouWant = reconcile;

Then whenever you want to "update" your page you can call this global method:

window.whateverNameYouWant();

The reconcile function does 3 things:

  1. Any placeholder elements that remain from the previous render will get a chance to update if the data-attrs have changed.
  2. Any new placeholder elements will be populated with components.
  3. Any missing placeholder elements will cause the removed components to be destroyed.

Let me know if it works out!

A few caveats:

  1. You can't manipulate the DOM of a rendered component and expect things to behave in a sane way. I'm not sure what happens when you change or remove parts of the DOM from an Ember component and rerender it, but it might be weird. So you'll want to operate at the level of adding, removing, and updating Ember Island placeholders.
  2. This implementation uses the identity of the placeholder elements to determine whether to rerender or initially render a component there. So if you render with a placeholder element, remove that element, and then replace it with an identical looking element, you'll end up with a new instance of that component. Likewise, changing the name of a placeholder (data-component="new-name") isn't supported (but could be). Updating the data-attrs property of a placeholder is supported.
  3. With current versions of Ember, I believe you'll be leaking memory whenever you teardown and rerender components. Based on the way your application works (e.g. if you periodically get a full-page refresh) this might not be an issue. If this is an issue, you may be better off using Application.reset() until the next version of Ember lands.
felixibel commented 7 years ago

Hey @mitchlloyd, your rerendering branch is great! It's quite useful when not all JS lives within the Ember world. Many thanks for that one!

Are you going to open a PR of your rerendering branch? I would love to see it in master, if possible.

mitchlloyd commented 7 years ago

@felixibel Were you able to get this working on a project? If so great to hear! I definitely need to get feedback on this change since I don't have this use case myself.

I opened a PR (#46) with a few todos on it. I'm expecting to have time to tackle this by next week.

sigmer commented 7 years ago

@mitchlloyd This is working in my project. Very excited for this change! I can confirm components are rendered/destroyed when the placeholder divs are added/removed from the page.

I'm not currently dynamically modifying the data-attrs on the div, but that is a cool idea and definitely useful.

Thanks for this!!

mitchlloyd commented 7 years ago

Published as 1.3.0