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

Render view from string #35

Closed
RWOverdijk opened this issue Mar 17, 2015 · 64 comments
Closed

Render view from string #35

RWOverdijk opened this issue Mar 17, 2015 · 64 comments

Comments

@RWOverdijk
Copy link

I'm currently trying to create a custom component that allows you to create forms (including serializing, setting values, validating and easy templating) based on objects. The catch, is that I want to generate the markup (performance and code readability reasons).

Currently, I see no simple way of doing this. I was pointed to this code: https://github.com/YoloDev/mimosa-aurelia-skeleton/blob/c5799bf35a68d26f4a8137b28b93ce3897ce3fd1/src/app/docs/behaviors/doc.js but that's not what I'd like to call simple.

So, in a nutshell: I'd like to be able to render a view based on either a string or DocumentFragment (preferably string, but I can live with the latter) for the ViewModel I'm in in stead of using a view file, which would consequently include data binding,

@ghost
Copy link

ghost commented Mar 17, 2015

Indeed, the code you mentioned is really not simple and it would be nice to have an abstraction that would handle this scenario.

I think that ES6 template strings would be ideal in the scenario of handling templates in javascript code. The developer could also generate the template markup from objects via this functionality too.

About the same wish of being able to render a view from a string: #33

@RWOverdijk
Copy link
Author

@Ab34 They do sound pretty much the same. I too want to render a view from string for the ViewModel.

Any pointers as to how to get this going?

@ghost
Copy link

ghost commented Mar 18, 2015

Yes, we are facing the limitations of actual Aurelia's design in this case, which is tightly coupled to browser imports mechanisms. It would require a custom Loader for use into the ViewEngine. Presently, the ViewEngine only supports one unique loader, the default one based on HTML imports. So I would also import the Router inside the ViewEngine, in order to decide what instance of Loader to use per view via the Router config . It would be possible, by correctly implementing a custom Loader, to not have to extend ViewStrategy nor to use getViewStrategy() method inside our module so that only the Router intelligently decides what Loader to use according to its configuration map.

@EisenbergEffect
Copy link
Contributor

What is needed is a view strategy that lets you provide a string. The strategy would then use the view engine to load its dependencies and then use the compiler to compiler the string with its resources. All the pieces are there to do this today, it just takes a bit of work. We can create a view strategy that does this by default, so it can be as simple as supplying the metadata or using getViewStrategy.

@RWOverdijk
Copy link
Author

@EisenbergEffect So the default view strategy would load the actual view file, and something like a generatedView strategy could then say "hey, don't worry about it, I'll tell you what view to use, there's no need to download anything". Right? A build tool could use this to also bundle the views with compiled code. Perhaps even in a bundledViews file, much like JST. But I digress. So the idea would be to add a new strategy which would then... depend on a specific method to be implemented by the ViewModel?

@ghost
Copy link

ghost commented Mar 20, 2015

Well, it is not obvious to go through the code of the ViewEngine as there is no comment nor type information in it. Difficult to understand exactly what it is doing and with what. I wish there was more documentation per module explaining their internal workings.

I was wondering if it would work to pass the template as a Data URI to the component Loader via a ViewStrategy communicating with the ViewEngine. Not sure that it would work nor if it would work cross-browser though.

@RWOverdijk
Copy link
Author

Can't this pull request help us?

@EisenbergEffect
Copy link
Contributor

That PR is part of the solution. But if your view has imports, those need to be extracted and loaded so that they can be passed into the compile call. Ultimately, what I will do is create a custom view strategy that lets you provide the string. It will transform it to a template. Then it will use existing apis to discover the imports and load them. Finally, it would pass all of that to the compiler. Does that make sense?

@ghost
Copy link

ghost commented Mar 20, 2015

I'm wondering, what about the TemplateRegistry which is maintained by the Loader. In this case the template would not be part of the registry, as the ViewStrategy would directly create a fragment from the string ? Anyway, I need more time to better understand the framework to make better statements about it.

@EisenbergEffect
Copy link
Contributor

No, it wouldn't be in the registry, but it would be a one-off anyway if it's embedded in a string inside a behavior, so it's not a big deal.

@ghost
Copy link

ghost commented Mar 20, 2015

That makes sense now. If there is no side-effect that the provided template is not in the registry, then that's fine. Thanks for the explanations.

@EisenbergEffect
Copy link
Contributor

Accidentally closed this. Sorry. Reopening.

@csaloio
Copy link

csaloio commented Apr 13, 2015

Just wanted to say that I too would greatly desire the ability to take a string containing custom elements/bindings and have it injected into the DOM and compiled.

@lneffa
Copy link

lneffa commented Apr 15, 2015

Also would really desire this functionality. Would definitely provide new flexibility from a Rails app standpoint.
Rails and other tied gems precompile views to serve through responses of type text/html. These views and the time at which the views are compiled for serving can be configured.
No big news here, however the functionality found in other gems can precompile views and serve them through the browser on demand using 'javascript templates'; JST['my-view'] or JST['second-view'] for example.

Right now in the app, I tie in a Gulp build process prior to Rails running its build process for assets.
My Gulp build process takes .jade files I have and compiles them to .html. These .html files are then placed inside a directory within the Rails app. The Rails app feeds off this directory to serve back the text/html responses as the V part of the MVC backend.

It would be great to be able to use the same Rails views and have Rails precompile and serve them through 'javascript templates'. This will keep code dry for partial views in the Rails app that have to be replicated as .jade files for the Gulp build process. AND the Rails app, on compiling views to html, have many helpers that tie to presenting and working with data from backend to produce html files. Way cleaner for testing.

-- edit ---

Also want to throw in that with other frameworks like Ember, Backbone, or Angular, I have seen 'inlining' the templates as the most convenient way to integrate with Rails.

-- another update --

Without having to involve Gulp makes managing cached assets on a Heroku deploy easier for a team working on Rails. During a Rails app deploy to Heroku, cached versions of compiled assets by the Rails asset pipeline are kept alive for 3 versions/releases (by default) during the Ruby buildpack step of a Heroku deploy. Having to compile assets with Gulp involves making sure they are digested and cleared out by Heroku as they get cached on each deploy.

@EisenbergEffect
Copy link
Contributor

The latest version of the ViewCompiler in master now allows you to pass a string and receive a ViewFactory which you can use to generate View instances. To use this, in a custom element, declare that the element has @noView. Then make sure the ViewCompiler and ViewSlot are injected into the constructor. If your generated view also will use custom elements, you will need to have the ViewResources injected as well. Generate your html as a string and pass it to the view compiler, along with the resources. Then use the view factory to crate a view. Add that view to the view slot.

@maxgrv
Copy link

maxgrv commented Jul 7, 2015

Would it be possible, with the new ViewCompiler, to do this directly from a ViewModel setting a suitable ViewStrategy?

@EisenbergEffect
Copy link
Contributor

Yes, it would be possible. The view strategy would need to have a way to include the resource dependencies though. The compiler needs those in order to properly compile all custom element, attributes, etc in the view.

@maxgrv
Copy link

maxgrv commented Jul 7, 2015

We could inject the ViewResources into the ViewStrategy created inside the VM constructor, would that work?

@EisenbergEffect
Copy link
Contributor

Doing something like that would only give you the global view resources. If you have anything that is view-specific, you would need to provide those to the compiler. That's mainly what I'm referring to.

@maxgrv
Copy link

maxgrv commented Jul 7, 2015

Ok, so limiting to using globally-registered custom elements/attributes/value converters should do, wouldn't it?

@EisenbergEffect
Copy link
Contributor

That would be the easy way :)

@maxgrv
Copy link

maxgrv commented Jul 17, 2015

Is the bc464a4 commit related to this use case?

@EisenbergEffect
Copy link
Contributor

@maxgrv You can use the ViewCompiler to compile a string to a ViewFactory and have been able to for a while. That new commit is for advanced scenarios where you need to provide views, usually from a database and you've already got the data loaded but want to integrate that into the standard component load cycle.

@maxgrv
Copy link

maxgrv commented Jul 17, 2015

Ok, I thought this was a shortcut and a way to specify arbitrary dependencies :)

@maxgrv
Copy link

maxgrv commented Jul 17, 2015

We will begin looking into dynamic view creation based on the VM content next week, for a "dynamic form" scenario type.

@EisenbergEffect
Copy link
Contributor

It can assist with providing dependencies for views that are created dynamically from a string. You could construct a form via a string, then create the view strategy, supplying it with the dependencies and use that, yes.

@Infuser
Copy link

Infuser commented Jul 23, 2015

Thanks for the quick response so I have tried it again without a view and sure enough I have found it is calling the code but its not working because InlineViewStrategy is undefined.

Not sure if this is because I am using an old version of aurelia
"github:aurelia/framework@^0.13.2",

@maxgrv
Copy link

maxgrv commented Jul 23, 2015

You should try to update. I am using

  "aurelia-framework": "github:aurelia/framework@0.13.3",
  "aurelia-templating": "github:aurelia/templating@0.13.11",

@zewa666
Copy link
Member

zewa666 commented Jul 23, 2015

@Infuser the InlineViewStrategy was introduced in one of the more recent commits. Please always update to the latest version while we are in pre beta state. It happens a lot of times that things get fixed behind the scenes :)

@Infuser
Copy link

Infuser commented Jul 23, 2015

Thanks guys got the example working now and aurelia is updated.

I am new to using GITH, Gulp and JSPM I went into package.json changed the numbers and ran JSPM install if there is an easier way I would love to know what that is.

I am using one of the Aurelia skeletons

Okay for anyone else not sure how to update aurelia I have found a couple of posts

http://blog.durandal.io/2015/04/09/aurelia-update-with-decorators-ie9-and-more/

https://wipdeveloper.com/2015/04/10/updating-aurelia/

Sorry to hijack this issue/thread a bit

@EisenbergEffect
Copy link
Contributor

If your view is static, you can also use the @inlineView decorator:

import {inlineView} from 'aurelia-framework';

@inlineView('<button click.delegate="greet()">${text}</button>')
export class Test {
    constructor(){
        this.text = "Hello world";
    }

    greet(){
        alert('test');
    }
}

@kevmeister68
Copy link

Regarding Rob's piece of code from earlier:

@noview
@Inject(ViewCompiler, ViewSlot, Container, ViewResources)
export class Test {
constructor(vc, vs, container, resources){

If the HTML string needs to be asynchronously loaded, should some of this code move into the activate method and return a Promise? I'm still coming to grips with the whole JS/Aurelia ecosystem.

@EisenbergEffect
Copy link
Contributor

It all depends. You can create a custom view strategy. That has the ability to asynchronously provide the view. If your component is being controlled by a router, then you can use the activate callback to load the string. You would then also implement the getViewStrategy() callback and return an instance of InlineViewStrategy. The constructor is the same as the decorator above.

@EisenbergEffect
Copy link
Contributor

Quick update for anyone who comes across this thread. As of the latest version of Aurelia, if you use inline views or compile from string, you need to wrap your string in a template element. Like this:

import {inlineView} from 'aurelia-framework';

@inlineView('<template><button click.delegate="greet()">${text}</button></template>')
export class Test {
    constructor(){
        this.text = "Hello world";
    }

    greet(){
        alert('test');
    }
}

@heruan
Copy link

heruan commented Oct 20, 2015

I'm also very interested on this, since I'm writing an Aurelia plugin which builds tables and forms from a JSON Schema description of entities. I didn't know the existence of inlineView, ViewCompiler, ViewSlot, etc. Where can I find documentation about these (or just a reference to know their existence)?

@EisenbergEffect
Copy link
Contributor

Full api reference docs are coming for the beta in a couple of weeks.
On Oct 20, 2015 4:05 AM, "Giovanni Lovato" notifications@github.com wrote:

I'm also very interested on this, since I'm writing an Aurelia plugin
which builds tables and forms from a JSON Schema description of entities. I
didn't know the existence of inlineView, ViewCompiler, ViewSlot, etc.
Where can I find documentation about these (or just a reference to know
their existence)?


Reply to this email directly or view it on GitHub
#35 (comment).

@RWOverdijk
Copy link
Author

I can't wait for the docs. Is there some code I can read in the meantime?

@insidewhy
Copy link

@RWOverdijk I found many examples online, I could get the template inserted but the model binding doesn't seem to work, all my ${variables} evaluate to the empty string. If I work it out I'll show you my code. On the other hand, if you've managed to get it to work, I'd love to see your code.

@kplatter
Copy link

I got binding working after reading comment in http://blog.durandal.io/2015/11/10/aurelia-pre-beta-release/

thanks to jmjf for pointing out that ViewFactory.create() no longer accepts bindingContext as the second parameter. You must call view.bind(bindingContext) after creating the view. Calls to ViewFactory.create() with more than one parameter (i.e., 2-4 parameters) need to be adjusted accordingly.

@RWOverdijk
Copy link
Author

@kplatter I still have no idea how to set this up. I only see bits and pieces; do you have an example / links to docs?

@kplatter
Copy link

Here's how I am using it and it is working. I am sure that there are improvements that could be made.

interfaces is just an array of objects with optional view properties (string), if the object has a view property it is rendered.

====

    <tbody repeat.for="interface of interfaces">
        ...
        <tr if.bind='interface.view != undefined'><td colspan='8' style="padding-left:50px"><inline-view model.bind='interface'></inline-view></td></tr>
    </tbody>

====

import {inject, noView, ViewCompiler, ViewSlot, Container, ViewResources, bindable} from 'aurelia-framework';

@noView
@inject(ViewCompiler, ViewSlot, Container, ViewResources)
export class InlineView {

    constructor(viewCompiler, viewSlot, container, viewResources){
      this.viewCompiler = viewCompiler;
      this.viewSlot = viewSlot;
      this.container = container;
      this.viewResources = viewResources;
    }

    attached() {
      // Compile an html template, dom fragment or string into ViewFactory instance, capable of instantiating Views.
      var viewFactory = this.viewCompiler.compile('<template>' + this.bindingContext.interface.view + '<template>', this.viewResources);

      // Creates a view or returns one from the internal cache, if available
      var view = viewFactory.create(this.container);

      // Bind the view and it's children
      view.bind(this.bindingContext);

      // Add the view to the slot
      this.viewSlot.add(view);

      // Trigger the attach for the slot and its children.
      this.viewSlot.attached();
    }

    bind(bindingContext) {
      this.bindingContext = bindingContext;
    }

}

@kplatter
Copy link

Also documentation is here http://aurelia.io/docs.html#/aurelia/templating/1.0.0-beta.1.0.2/doc/api/overview but nothing in the way of examples or use cases.

@eriktim
Copy link

eriktim commented Jan 11, 2016

@kplatter Thanks for your example. This works for me as well. I only had to change view.bind(this.bindingContext); to view.bind(this); as I wanted to bind to the model itself (not its parent) and initially failed on using click delegates.

@RWOverdijk
Copy link
Author

@kplatter Do you mean </template> for the second one in var viewFactory = this.viewCompiler.compile('<template>' + this.bindingContext.interface.view + '<template>', this.viewResources);?

@StrahilKazlachev
Copy link
Contributor

For this I use <compose> and InlineViewStrategy - just binding an InlineViewStrategy instance to the view property of <compose>. Not sure for the performance implications, but for my use case it does the job and is simple.

<template>
    <compose view.bind="viewStrategy"></compose>
</template>
export class DynamicCustomElement {
    viewStrategy: InlineViewStrategy;
    ...
    bind(bindingContext: any, overrideContext: any) {
        ...
        this.viewStrategy = new InlineViewStrategy(`<template><${editorName} data.bind="data"></${editorName}></template>`, [dep1, dep2, ...]);
        ...
    }
    ...
}

@kplatter
Copy link

@RWOverdijk thanks for catching that, it seems to work either way but would probably come back to bite me later.

@StrahilKazlachev thanks for the example, I tried something similar and failed, but may try again using your example

@geleto
Copy link

geleto commented Sep 26, 2016

Why is this not working? If I uncomment @inlineView( html ) it works.

import {InlineViewStrategy} from 'aurelia-framework';
import {inlineView} from 'aurelia-framework';

const html = '<template> <button click.delegate="greet()">${text}</button> </template>';

//@inlineView( html )
export class TestCustomElement{
    constructor(){
    }

    text = "Hello world";

    greet(){
        alert('test');
    }

    getViewStrategy(){  
        return new InlineViewStrategy(html);
    }
}

Here's a gist:
https://gist.run/?id=5e9313a0af2d0084ed032c05bad1b5e3

@StrahilKazlachev
Copy link
Contributor

@geleto See the description here.

@benhathaway
Copy link

All,

I am doing almost exactly the same thing - I want to use an InlineViewStrategy (with a dynamically generated string) in a custom element. InlineViewStrategy seems to work for views - but not for custom elements (unless I am missing something).

Can anyone help?

@benhathaway
Copy link

OK. I found an answer:

#360

Can't use getViewStrategy with a custom element as they are static. Use a nested compose element to get dynamic composition inside of a custom element.

@rockResolve
Copy link

Beware of IE. IE does not build template elements with a content property, which can cause viewCompiler to go into an infinite loop.

The workaround is to use (Typescript has to use "as any")

if (!FEATURE.htmlTemplateElement) {
    (FEATURE as any).ensureHTMLTemplateElement(childTemplate);
}

Note aurelia-pal-browser only publishes createTemplateFromMarkup which takes markup input, but hidden ensureHTMLTemplateElement takes element input

@trusktr
Copy link

trusktr commented Jan 19, 2018

How do we do this on codepen.io? I have this pen:

https://codepen.io/anon/pen/qpLNZd

But due to the fact that the Custom Elements (i-scene, i-node) are receiving the raw strings (f.e. strings containing ${...}), it throws an error.

Instead of having those elements in the DOM then using enhance on them, I'd like to mount a component to the DOM and give it a string-based template so that the actual DOM instances will not ever receive non-interpolated values.

How can I modify that pen so that there's nothing in the HTML editor, and the template is given to the view as a string?


Basically I'm trying to achieve the same as in this React pen:

https://codepen.io/trusktr/pen/8d798dc31bc8ed9a53f068848bc62768

or as in this Vue pen:

https://codepen.io/anon/pen/XVoKPb

How can I make the same demo with Aurelia on Codepen?

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