Cannot perform operations on a Metamorph that is not in the DOM. #752

Closed
i-fail opened this Issue Apr 27, 2012 · 30 comments

Comments

Projects
None yet
4 participants

i-fail commented Apr 27, 2012

We've decided to use Ember.js for a fairly large app. After two months of development, we started seeing this error pop up randomly:

"Cannot perform operations on a Metamorph that is not in the DOM"

This is EXTREMELY frustrating, since now the whole development process is in jeopardy.

So it normally works, but then if you go back-and-forth between different parts of the app, Ember suddenly drops this error and everything breaks.

We load and render the templates dynamically. We load them with Ajax and then compile and create a view:

    $(d).select('script[type="text/x-handlebars"]').each(function() {
        Ember.TEMPLATES[name] = Ember.Handlebars.compile($(this).html());
    });
    Views[name] = Ember.View.create({
        templateName: name,
        APPName: APPName
    });

After that, if we need to render the template into an element with id="target", we do this:

    Views[name].appendTo(target);

And if that template was rendered previously, we do

    Views[name].remove();

I tracked it down to Views[name].appendTo(target); not inserting the template into the target element. Everything looks normal, but the element just remains empty after this call.

So normally, I can back and forth between various "pages", and they re-render as many times as I want, just fine. Then I would go to some other "pages", come back, and it would trow this error.

We're using the latest version of Ember.js.

Owner

wagenet commented Apr 27, 2012

Why are you doing things this way? This is definitely not a standard setup.

Owner

wagenet commented Apr 27, 2012

I am pretty sure this is not actually a bug. And that these issues stem from the unusual and unsupported architecture of your app. However, it might make sense for us to provide some better documentation so that it's more clear up front what you should and shouldn't do.

Member

lukemelia commented Apr 27, 2012

Something to look for is whether the target element you are appending to (or any of its parent nodes) are already being "managed" by another Ember view. If that's the case, that other view can be destroyed and remove the element out from under the other view you have appended.

As Peter said, the code you provided is an unusual approach that is not recommended. I'd be curious to hear if you came to this approach to solve a particular problem.

Owner

wagenet commented Apr 27, 2012

Appending or replacing existing views is now explicitly disallowed: c209f12

i-fail commented Apr 27, 2012

We're doing this because to load every single template and to compile them would take forever, so we're loading the templates with ajax only when they are needed.

What's the right and approved way of doing this?

Member

lukemelia commented Apr 27, 2012

@emberfail if you set('templateName', 'new_template_name') on a view, it will re-render with the new template. So if you don't have different view classes, you could just leave the one attached and update what template is in use.

For more complex scenarios, use an Ember.ContainerView, which will let you manage the 'childViews' property programmatically. When you want to switch pages, load your template via ajax and register it in Ember.TEMPLATES, then update childViews to add a new view using your new template. The API docs for ContainerView will be helpful in understanding how to manage childViews.

Owner

krisselden commented Apr 27, 2012

It is ok to programmatically load views and/or templates, but you should add them to a view using a method that will make the parent view aware it has a child view. This is typically a ContainerView and you push and remove child views. You can also extend view and use addChild during render.

You use append() methods on your root view.

Owner

krisselden commented Apr 27, 2012

You can also precompile you templates to JS and load them as script.

Owner

wagenet commented Apr 27, 2012

I'm closing this since we added some basic documentation and are now warning against incorrect use of appendTo. @emberfail If you have more questions you can continue in this thread though you might have better luck on Stack Overflow.

wagenet closed this Apr 27, 2012

i-fail commented Apr 27, 2012

@lukemelia

I just tried doing what you suggested and it doesn't work:

        var x = Ember.View.extend({templateName: 'layout'})
        x.set('templateName', 'layout')
        TypeError: Object (subclass of Ember.View) has no method 'set'
Member

lukemelia commented Apr 27, 2012

You're trying to call set on a subclass. It would need to be called on an instance.

i-fail commented Apr 27, 2012

@wagenet I really need a way to switch the view between different templates.

ContainerView sort-of achieves that, though it means I have to shuffle the views array instead of just setting the template.

Owner

wagenet commented Apr 27, 2012

@emberfail You should be able to set the template or templateName property of the view and then call rerender. Ideally you shouldn't even have to call rerender but for now you will need to. See #399.

Member

lukemelia commented Apr 27, 2012

Here's a simple example: http://jsfiddle.net/Qpkz5/384/

Member

lukemelia commented Apr 27, 2012

And a fork from @kselden showing proper use of the append method: http://jsfiddle.net/x3VWZ/

i-fail commented Apr 27, 2012

@lukemelia

In your example. this.simpleView is also a subclass, yet you can call .set() on it. I don't get why it works in your example, and doesn't work in the example I posted above.

And by the way, thanks to all of you for helping me out, I'm really stressed about this, but it looks like I will be able to do this with a relatively simple rewrite.

Member

lukemelia commented Apr 27, 2012

this.simpleView is not actually a subclass, but you're totally forgiven for thinking that. ContainerView does a few things to let you assign a view subclass to property names matching what you supply to childViews, and them turn them into view instances after the ContainerView is instantiated. I shouldn't have used that trick in the example.

No worries re: help. Hope your project gets back on track.

Member

lukemelia commented Apr 27, 2012

Here's an update of the fiddle without the ContainerView property shortcuts: http://jsfiddle.net/x3VWZ/2/

i-fail commented Apr 27, 2012

@lukemelia That makes sense.

One last question, hopefully.

In your example you set() the new template not listed in childViews. Is that valid or is it better to push 'goodbye' into that array?

Owner

krisselden commented Apr 27, 2012

You are getting view instances and templates confused, he is reusing the view instance by setting the template name on it and calling rerender.

Owner

krisselden commented Apr 27, 2012

This is if you wanted to replace the existing view http://jsfiddle.net/x3VWZ/4/

i-fail commented Apr 27, 2012

I'm getting more and more confused. So there are view classes and view instances, and Ember.js documentation uses Ember.View.extend and Ember.View.create interchangeably. I'm not sure which one to use.

Since I'd like to call .rerender(), I switched my Ember.View.extend calls to Ember.View.create, and I immediately get strange errors coming from the depths of Ember:

Uncaught TypeError: Object <(subclass of Ember.View):ember187> has no method 'extend'
e.ViewHelper.Ember.Object.create.viewClassFromHTMLOptionscombine.php:19
e.ViewHelper.Ember.Object.create.helpercombine.php:19
(anonymous function)combine.php:19
(anonymous function)
a.VM.templatecombine.php:15
Ember.View.Ember.Object.extend.rendercombine.php:18
Ember.View.Ember.Object.extend.renderToBuffercombine.php:18
Ember.View.Ember.Object.extend.createElementcombine.php:18
Ember.View.states.preRender.insertElementcombine.php:18
Ember.View.Ember.Object.extend.invokeForStatecombine.php:18
ccombine.php:16
kcombine.php:16
Ember.ArrayUtils.forEachcombine.php:16
f.flushcombine.php:16
f.endcombine.php:16
Ember.run.endcombine.php:16
i
Member

lukemelia commented Apr 27, 2012

@emberfail create and extend definitely cannot be used interchangeably. create creates an instance of the class it is called on and extend creates a subclass inherited from the class it is called on. For the most part, you can think of it as a typical classical object-oriented system. extend == inheriting, create == instantiating.

There are places in Ember where the framework allows for convenience you to pass either a view class or a view instance. Beyond those places, the two cannot be treated interchangeably.

i-fail commented Apr 27, 2012

I made a simple app reflecting what I want to do

http://jsfiddle.net/X6xPn/2/

So there are a few issues:

  1. Basically, it lets me swap the views at the beginning, but it just doesn't work on "click" events

  2. I have use Ember.view.create for the root view, otherwise I can't append

  3. I have use Ember.view.extend (so classes, not instances) for other views, so I can't call .rerender()

  4. If I change Ember.view.extend to Ember.view.create for any of the child views, I get this strange error from somewhere inside Ember:

    Uncaught TypeError: Object <Ember.View:ember135> has no method 'extend'

Why it works for the RootView, but not for child views is not clear at all.

  1. If I add RootView.rerender(), it works, but re-renders the whole app instead of just the views I want to update
$('#show_landing').live('click', function(){
    App.Content.set('view', App.Content.get('view_landing'));
    App.RootView.rerender();
});

$('#show_product').live('click', function(){
    App.Content.set('view', App.Content.get('view_product'));
    App.RootView.rerender();
});
​```

i-fail commented Apr 27, 2012

@lukemelia I totally understand that one is a class, another one is an instance, but what's not clear at all is which one Ember wants. In the example I posted above it wants an instance for the RootView, and classes for the nested views. It makes little sense to me.

Owner

krisselden commented Apr 29, 2012

extend creates subclasses and create creates instances, rerender and append are instance methods, so you need a view instance. ContainerView childViews is a little overloaded in that before instantiation it expects childViews to contain classes and not instances because its init turns the classes into instances, after creation it expects instances to be pushed or inserted into childViews.

Owner

krisselden commented Apr 29, 2012

Here is a working example of your fiddle, that tries to illustrate some of the above concepts. Hope this helps!

http://jsfiddle.net/krisselden/hs9DK/

i-fail commented May 1, 2012

@kselden Thank you for the example. Don't you think it's a bit insane, the amount of code and indirection required for such a simple task?

Owner

wagenet commented May 1, 2012

@emberfail There's actually not all that much unusual going on. If you read the code, you'll see that @kselden has implemented a lot of the pieces of your app and that the only really unusual part is currentState which actually isn't all that complex. The rest of it is just some standard (and pretty basic) view definitions and a basic state manager. He's gone beyond just providing you with the minimal solution and actually given you a basic app framework.

Owner

krisselden commented May 1, 2012

@emberfail The simplest example would be just to show/hide, but your question as I understood it, is you want to defer creating views and templates then append them to an existing ember view.

Here is a simple example that defers creating the view and template until needed, and takes the view in and out of the DOM in a manner that won't cause issues like append().

http://jsfiddle.net/krisselden/gYBhh/

If you are loading templates via ajax, you are going to need to do more than that, and for loading states, I find stateManagers useful, so I started one for you. I also thought dealing with currentView would be simpler than childViews so I made a computed property for you, to make it so you just had to set a single property currentView.

So the idea was to give you some building blocks to build from, wasn't meant to be the simplest possible example.

Hope this other one works better for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment