ractivejs / ractive

Next-generation DOM manipulation
http://ractive.js.org
MIT License
5.94k stars 397 forks source link

Suggestion: Allow components to render in the "el" #1131

Closed cfenzo closed 7 years ago

cfenzo commented 10 years ago

Right now, if you set the el option for a component, it will render in the element specified (like body, or #main), but crash the whole Ractive instance:

Ractive.components.modal = Ractive.extend({
  el:'body',
  append:true
});
// gives: Uncaught Error: You cannot call ractive.render() on an already rendered instance! Call ractive.unrender() first 

I'm not sure if this is a bug, or intended behavior..

Why would you want to render anywhere but where the tag is? one might say.. In some cases, like with a modal or a dropdown, it would be very nice to be able to render the component outside the template (like appended to the body), or outside a list (could be done in more ways).

Rendered Ractive components can be moved outside the template/into any element, by using a combination of complete, detach and insert:

Ractive.components.Modal = Ractive.extend({
    template:'<div class="modal">{{>content}}</div>',
    data:{
      el:false
    },
    append:true,
    complete:function(){
      this.reattach();
    },
    init:function(){
      this.observe('el',function(n,o){
        this.reattach();
      },{init:false});
    },
    reattach:function(){
      if(this.get('el')){
        this.detach();
        this.insert(this.get('el'));
      }
    }
  });

(jsbin example)[http://jsbin.com/xusez/19] But this workaround will first render the component inside the template (and run all intro animations), and then move the rendered component outside to the specified element. Which can cause glitches..

I hope "rendering components in the element referenced by the el option" is a feature worth adding (it looks like it's doable), maybe there should be an option to toggle the behavior?

But if components shouldn't be able to render in the el, it would be nice if the el option where ignored for components.

martypdx commented 10 years ago

1089 fixed #1072 and #1010 (the bug) in edge, so el is ignored for inline components.

All mark this issue as an enhancement so that when we get to progressive rendering (rendering on top of an existing DOM), we can remember to consider this case. Ideally ractive's virtual DOM can be outside of 1:1 with exact DOM tree, but there's some work to get there...

cfenzo commented 10 years ago

Thanks @martypdx, I thought it was a bit weird (and that el for components didn't do anything in some previous version)..

I've updated my jsbin: http://jsbin.com/xusez/21/ with a workaround that sets an empty template before moving the component and setting the correct template.. It's a bit fugly, but it'll work until there's a way to set the el for components :)

martypdx commented 10 years ago

I'm about to tackle a similar problem of how to display a popup that needs to be above all the other DOM stuff so it can't live inline with the launching component (I assume that's sortof what's going on for you here).

I think I'll use more of a service approach and run a popup container higher up in the component heirarchy, and use events. I'm using magic: true, so I'm hoping the data stays in sync without having to call .update()

cfenzo commented 10 years ago

Yeah, sounds like the same problem. We started off with something along what you describe, but as we write most components as generic as possible (for re-use in a lot of small projects/ad campaigns), having a popup container just didn't fit the bill :)

I also tried to get it working by using a dynamic partial, but the solution I posted turned out to be the one working best for what we needed.

martypdx commented 10 years ago

Here's an example dynamic component I'm using now:

component.base = 'board'
component.exports = {
    template: function(data){
        return '<' + data.board.type + '-board id="' + data.id + '"/>'
    }
}

I'll see how it goes with a popup service...

martypdx commented 10 years ago

@cfenzo fill under "off-label use", but ractive seems to hold up well just moving the nodes. Here's simple example using component and decorator approach: http://jsfiddle.net/wnwev90g/3/ (different than your workaround in that I'm swapping out DOM nodes, not detaching ractive)

martypdx commented 9 years ago

Having spent some time doing this, I agree with @cfenzo that specifying el on a component should render to that element.

This is my currently implementaton:

component.exports = {
    data: {
        ready: false
    },
    oncomplete: function(){
        this.detach()
        this.host = document.getElementById('drag-host')
        this.insert(this.host)
        this.set('ready', true)
    },
    onteardown: function(){
        if(this.host) { this.host.innerHTML = '' }
    }
}

It seems silly to have to wait (until complete!) then detach and insert. And I have to put a block in the template to prevent flicker on initial render before it gets moved (but still have a root element so something actually renders):

<div>
{{#if ready}}
<!-- actual content -->
{{/if}}
</div>

Seems much better to just render to el:

component.exports = {
    el: document.getElementById('drag-host')
}

Or exploit an option function:

component.exports = {
    el: function() {
                var host = document.getElementById('drag-host')
                if(!host){ /*create it*/ }
                return host
        }
}

@Rich-Harris Is there any reason why actual DOM nodes shouldn't be loosely coupled with virtualDOM nodes?

evs-chris commented 7 years ago

In 0.9 builds, you can now attach external instances, that can handle rendering themselves or let the parent instance handle it. That should cover cases like this, where you need manual control but still want to maintain the parent -> child relationship.