Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

buffer.string() doesn't work #5534

Closed
amk221 opened this issue Sep 2, 2014 · 28 comments
Closed

buffer.string() doesn't work #5534

amk221 opened this issue Sep 2, 2014 · 28 comments

Comments

@amk221
Copy link
Contributor

amk221 commented Sep 2, 2014

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
Copy link
Member

mmun commented Sep 2, 2014

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
Copy link
Contributor Author

amk221 commented Sep 2, 2014

Ah ok, sorry for the trouble.

@amk221 amk221 closed this as completed Sep 2, 2014
@mmun
Copy link
Member

mmun commented Sep 2, 2014

@amk221 No trouble at all. :)

@DougPuchalski
Copy link
Contributor

@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
Copy link
Member

mmun commented Sep 2, 2014

@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
Copy link
Contributor

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
Copy link
Member

mmun commented Sep 3, 2014

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
Copy link
Contributor

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

@mmun
Copy link
Member

mmun commented Sep 3, 2014

@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
Copy link
Contributor

@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
Copy link
Member

mmun commented Sep 3, 2014

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
Copy link
Contributor Author

amk221 commented Sep 3, 2014

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
Copy link
Contributor

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
Copy link
Contributor

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: miguelcobain/ember-selectize#13

@mixonic
Copy link
Member

mixonic commented Sep 18, 2014

@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
Copy link
Contributor

@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
Copy link
Contributor

@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
Copy link
Contributor

@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
Copy link
Member

mixonic commented Sep 18, 2014

@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 mixonic reopened this Sep 18, 2014
@mixonic
Copy link
Member

mixonic commented Sep 18, 2014

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

@mixonic
Copy link
Member

mixonic commented Sep 18, 2014

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

@DougPuchalski
Copy link
Contributor

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
Copy link
Member

mixonic commented Oct 26, 2014

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 JSBin uses renderToBuffer, which was a private API that was removed. Private APIs are definitely subject to change, I'm sure you can agree. This one is gone.
  • Updating the JSBin to remove the use of {{name}} results in a working string, as I believe you expect things to work: http://jsbin.com/newub/1/edit?html,js,output
  • Please note that your comment of "You would expect a template to be able to do this:" is incorrect. Ember reserves the right to change the behavior of internal templates and their call signatures. Indeed we will change them in 1.x for HTMLBars.
  • With the {{name}} re-introduced, two changes are required to avoid an error firing. The view must be placed in the inBuffer state, and the buffer must be attached to the view. See http://jsbin.com/newub/3/edit?html,js,output. We can maybe relax the requirement that the buffer be attached to the view by doing that in render. I worry that relaxing the inBuffer restriction will cause more errors in the wild than it will enable good behavior.
  • Lastly, even without errors it is important to note that the output of buffer.string() is "Hello ". The name is not present, because it must be updated by the binding view later in the runloop.

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.

@mixonic mixonic closed this as completed Oct 26, 2014
@DougPuchalski
Copy link
Contributor

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
Copy link
Contributor

@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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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?

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

No branches or pull requests

6 participants