emberjs / ember.js

Ember.js - A JavaScript framework for creating ambitious web applications
https://emberjs.com
MIT License
22.46k stars 4.21k forks source link

buffer.string() doesn't work #5534

Closed amk221 closed 9 years ago

amk221 commented 10 years ago

Appending a view renders the buffer to a string, but one cannot access the string() directly.

example failure: http://jsbin.com/hafubakokape/1/edit?js,output related forum post: http://discuss.emberjs.com/t/how-to-render-a-template-to-a-string/6136/7

mmun commented 10 years ago

The error is correct. You can't manually render a view like this outside of the rendering process.

You probably don't want to do template({ name: 'Fred' }); with an Ember template. Most helpers that Ember provides expect there to be a view to be present (via options.data.view) in order to setup observers and bindings.

In theory, you could try

template({ name: 'Fred' }, {
  data: {
    buffer: someBuffer
  }
});

// use someBuffer.string();

Instead, I recommend compiling your own templates with Handlebars.compile (not Ember.Handlebars.compile) these are better suited for what you want to do.

amk221 commented 10 years ago

Ah ok, sorry for the trouble.

mmun commented 10 years ago

@amk221 No trouble at all. :)

DougPuchalski commented 10 years ago

@mmun Would it be reasonable to Ember.View to have a public function that would allow its template to be rendered to a string, using unbound properties from a given context? Third party libraries that need a string of HTML seem inevitable. In 1.0.7 I was happily sharing my view templates.

mmun commented 10 years ago

@aceofspades That sounds reasonable. Could you explain the use case more? Why do you want to use an Ember Handlebars template for this, rather than a vanilla Handlebars template? Are you using it both the "standard" way as well as for string serialization?

DougPuchalski commented 10 years ago

I assume vanilla would be just fine. Adapting your example

buffer = Ember.RenderBuffer();
App.__container__.lookup('template:item')({}, {data: {buffer: buffer}})

bombs on _triageMustache which is calling EmberHandlebars and expects a container.

Is there some other way to fetch the raw template markup that I'm missing so handlebars could be used directly?

mmun commented 10 years ago

Vanilla Handlebars-compiled templates don't use a buffer. That's a construct unique to the Ember Handlebars compiler and Ember's view layer. Here's an example of what I meant by a vanilla template

var template = Handlebars.compile("Hey {{name}}!");
var output = template({name: "aceofspaces"});
DougPuchalski commented 10 years ago

Even better--all I am missing now is how to fetch the raw markup from the template. Am I missing something blatantly obvious?

mmun commented 10 years ago

@aceofspades not sure what you mean. The output is a string of HTML

var template = Handlebars.compile("Hey <em>{{name}}</em>!");
var output = template({name: "aceofspaces"}); // Hey <em>aceofspaces</em>!
DougPuchalski commented 10 years ago

@mmun The input is the problem, i.e. getting "Hey <em>{{name}}</em>!" from the source template.hbs. Maybe this an ember-cli thing, which precompiles all the templates up front?

mmun commented 10 years ago

The template source is not accessible. Yes, you'll need to set up ember-cli to precompile them with Handlebars.precompile vs Ember.Handlebars.precompile but I don't think such an add-on exists.

amk221 commented 10 years ago

Ah yes - I think the confusion comes from the work ember-cli kindly does. One assumes that the precompiled ember-handlebars-templates would be useable for non-ember stuff. My particular use case is:

didInsertElement: function() {
  template = this.container.lookup('template:suggestion');
  this.$().typeahead { minLength:2 }, { templates: { suggestion: template } ... }
  // But instead I am using _.template('{{foo}}');
}
DougPuchalski commented 10 years ago

It seems there is a feature loss, not listed as a breaking change, from 1.0.7 to 1.0.8, that a precompiled template cannot be rendered to a string outside the context of the DOM (maybe this can be stated more accurately by someone more familiar with internals). Several posts on discourse and elsewhere show how to render to a string and no longer work.

This seems like an important feature to have, it certainly breaks my app.

@mmun thanks for taking the time to comment.

miguelcobain commented 10 years ago

My use case is to integrate with ember-selectize, an integration between ember and selectize.

Selectize accepts render functions to render its options, items, etc. These functions have to return strings. It would be great if we could have handlebars templates to customize rendering. I basically need to render a template with a context, get it's string and give it to selectize. I've got that covered, but not the part of getting a string.

I understand the problem. I'm using bindings in my template, I can't expect the bindings to work with a string. But would it be possible to render an Ember template without bindings (just using the current context)? Later, if the properties changed, i could trigger a re-render on selectize to make it call this render function again.

So, i guess this feature may be important to integrate with other libraries (like i've seen here with typeahead.js).

Related issue: https://github.com/miguelcobain/ember-selectize/issues/13

mixonic commented 10 years ago

@amk221 "One assumes that the precompiled ember-handlebars-templates would be useable for non-ember stuff." <- This is not true, and has never been suggested.

@aceofspades Still unsure what you are referring to. You can ask Ember for a buffer's string. In general, Ember is supposed to manage the rendering of your templates into DOM. What is your use-case for doing something different?

@miguelcobain that is the kind of behavior we would want to see provided by interoperable web components. This is easiest to do when the components all speak DOM, since that is the universal UI language of of the web. That selectize expects a string of HTML will make it challenging to use with Ember templates. Using buffer.string() is likely close, but you would also need to change render to avoid Ember outputting the DOM itself. Fwiw selectize expects strings here. You could open a ticket there to add support for optionally returning DOM instead of a string. Also fwiw selectize supports an API (addItem and family) for programmatically setting the same values you would with the strings.

Another option to consider is authoring a helper instead of a view. This would be a bit lower-level, and allow you to bypass Ember's views and data binding. With HTMLBars, you will need to get the outerHTML or innerHTML of the helper's block (since the inner block would return DOM). In Handlebars the raw string would be available to you.

At the end of the day, regardless of how well we can support strings, Ember is going to have a DOM based rendering engine internally. A string-based rendering engine is far too slow to be viable in the long-term. Using DOM will also enable a bunch of fantastic new syntaxes that make authoring templates easier. I urge you to keep in mind that although supporting strings of HTML is still something we will do for existing APIs in 1.x, you should consider avoiding string HTML in your own code for the same reasons.

miguelcobain commented 10 years ago

@mixonic very informative.

I'm also a contributor to ember-leaflet, and we had this problem when trying to use Ember views for some marker popups in a map. Things worked great there because leaflet accepts DOM elements.

I'm aware that selectize expects strings and that's the problem. I'll open a ticket there, or maybe I'll try to implement that myself. Don't know how difficult that might be.

My first thought was that if selectize just took the DOM element from our function and take a string from it and continue, wouldn't it work? I know that the ideal approach would be to make it actually use the DOM element. Could you possibly outline a "flow" for this kind of things to work?

Thanks!

DougPuchalski commented 10 years ago

@mixonic There are posts on github and discourse where core members have shown how to render a view to a string. This procedure no longer works in 1.8.0 so I'm saying this is a breaking change.

I am confused why this would be opposed. Aren't there are not tons of libraries out there that work with HTML strings, and other reasons we might want to leverage a template and render to something other than the browser DOM? Ember shouldn't cripple developers, expect every library to accommodate ember's "better way", or reinvent the wheel and rewrite already proven code.

My current pain point is also selectize. Render hooks are used to generate markup that is used elsewhere in the app to display a common metaphor. I'd really prefer to be able to keep one template that I can render manually as needed to make the plugin happy, so I can concentrate on my business logic.

Personally I don't think this is selectize's problem, and there are 217 open tickets there anyway.

image

miguelcobain commented 10 years ago

@aceofspades, are you using ember-selectize? We should try to encapsulate whatever the outcome of this is in that repo.

Ember always encouraged best practices. Since "using html strings" isn't one of them, I'm not surprised it isn't easy to do with ember. But I would love a solution for this.

mixonic commented 10 years ago

@mixonic There are posts on github and discourse where core members have shown how to render a view to a string. This procedure no longer works in 1.8.0 so I'm saying this is a breaking change.

Yup, I agree the RenderBuffer is public. buffer.string() is public. There are a few things getting conflated here, but I agree with that statement. Let us focus on that then. The JSBin in the original comment looks like valid usage to me.

I think I explained why we do not encourage strings in my last comment. It is a better way, and our goal is definitely not to "cripple" developers :-( Let's keep the emotions steady :-)

mixonic commented 10 years ago

Fwiw, that JSBin looks correct for rendering a string, but it doesn't seem to in 1.7 or 1.6.

mixonic commented 10 years ago

Can we get a JSBin with this dis-connected render buffer behavior working in 1.7 or before?

DougPuchalski commented 10 years ago

Agreed ember should do things the better way, within its scope. We just need an escape hatch to leverage all the code that's out there.

Cloned jsbin, working with 1.7.0: http://jsbin.com/poqus/1/edit

mixonic commented 9 years ago

Ok, several points. @aceofspades I don't think I can make you happy here, but I'm going to lay things out in as much detail as possible.

The last point highlights what I think may be the crux here: buffer.string() is not an API for building a string of HTML for all child views at the moment it is run. It is an API for generating the string of a buffer. And as the meaning of a buffer has evolved, so has the API with it.

http://emberjs.com/api/classes/Ember.RenderBuffer.html#method_string

buffer.string() "Generates the HTML content for this buffer."

A far more sane way to achieve similar behavior is to use createElement and fetch the outerHTML:

http://emberjs.com/api/classes/Ember.View.html#method_createElement

createElement "Creates a DOM representation of the view and all of its child views by recursively calling the render() method." (The recursive part is maybe inaccurate now, but you get the idea. This API includes child views).

A JSBin using this API: http://jsbin.com/newub/4/edit?html,js,output

I suggest this with a caveat: Ember's view layer is not designed to be used in an ad-hoc manner like this. In-fact this codepath (createElement) is not used in the course of a normally running application. It is does not allow the passing of a contextual element, and is really only used in tests. I'm wary to suggest any solution for ad-hoc rendering, but this seems closest to what you want.

I am again closing this issue. buffer.string was a poor suggestion to be made for ad-hoc rendering (regardless of who made the suggestion), and we will not be changing the meaning of "buffer" to accommodate the use of a specific method in a specific circumstance.

DougPuchalski commented 9 years ago

Thanks @mixonic for the detailed explanation.

My original use-case was a bit different. Forgetting about the original workaround, and stated more simply, I would like to fetch a component's simple, raw template and compile it with handlebars at runtime. Since ember-cli precompiles templates this appears not to be possible. The alternative seems to be to declare markup in javascript rather than hbs.

MiguelMadero commented 9 years ago

@mixonic thanks this information has been really useful. It makes sense why we're moving in that direction.

The examples helped but I still haven't been able to solve this problem. We have two different use cases with external libraries.

  1. We have static elements generated by Ember/HTMLBars and an external library needs to consume it, e.g. displaying items in an autocomplete.
  2. We have live elements, that the external library adds to parts of their DOM. We need those to raise actions in their corresponding context and for bindings to work.

All of this was working just fine in Ember 1.4 (yes we're behind I know - trying to catch up).

Your example of using createElement worked great for simple cases. Since we don't care about updates we simply get the HTML. jsbin and we don't have to change the library to work with markup instead of strings.

However, if we need to use nested components it can't find them. jsbin. So we have to use createChildView instead of calling create directly. jsbin. That seems to render the templates, but it never calls didInsertElement jsbin. Looking deeper, createElement calls into Renderer.renderTree, which won't call didInsertElement, which is essential for the nested components to work.

For the second scenario, it seems to be working fine, but without the nested components working I've not been able to verify all instances. For this to work, I'll also need to tweak/hack a few of our external libraries, but it's certainly doable. Before I go down that path, has anything changed since Oct 27?

MiguelMadero commented 9 years ago

This seems hacky, but actually better than what we had with 1.4. It makes sense that render doesn't call didInsertElement, so we simply call it manually see jsbin after the element is inserted into the DOM. Not included in this jsbin, but we do something equivalent for other events like willDestroy.

There's another jsbin that tests bindings and actions. Simple change was to use the element instead of the string. Now I need to look at the real world scenarios.

MiguelMadero commented 9 years ago

Oh, I found another problem with this approach. Even though, BoundViews work, {{#if}} doesn't. I'll dig more into it and branch off this thread, since this went beyond the original context already.

miguelcobain commented 9 years ago

I was trying to use the createElement API to get a string from a template/view. Using Ember 1.10 I got the following error:

Uncaught TypeError: Cannot set property '_elementInserted' of null

Here is a JSBin: http://emberjs.jsbin.com/dumiboroju/1/edit?html,js,console,output

However, the string is still rendered. Is there anything new we should consider when doing this?