ufront / ufront-mvc

The core MVC framework that powers ufront
MIT License
17 stars 15 forks source link

Supporting partials. #6

Closed tiagolr closed 8 years ago

tiagolr commented 9 years ago

Hi, now that i have the templating engine supporting partials (using Stipple) I'm having difficulties splitting the page processing to different classes.

Ideally i would have different classes like NavBar.hx taking care of the navigation bar, SideBar.hx taking care of the side bar, Footer.hx taking care of the footer etc...

The way I'm doing this is by assigning the partials to viewResult global values:

ViewResult.globalValues.set( "navBar", function(_,_) {  return NavBar.generate();  } );
ViewResult.globalValues.set( "sideBar", function(_,_) {  return SideBar.generate();  } );
ViewResult.globalValues.set( "footer", function(_,_) {  return Footer.generate();  } );

Now the trouble is implementing the NavBar.generate() method, there is no ready solution to process a template file and return its result to be used as a partial in the layout. ViewResult is meant to be used once and its result returned as an HTTP response.

What would be a good solution to this problem? Think something like ViewResult (template engine agnostic, asynchronous, file path inference) can be worked out to only process layout files ? I can implement it if there is a good solution.

tiagolr commented 9 years ago

For example in expressJs there is res.render() that renders a view and sends it to the user, and there is also app.render that only renders a view: http://stackoverflow.com/questions/15403791/in-express-js-app-render-vs-res-render-whats-the-difference

jasononeil commented 9 years ago

Hi @ProG4mr,

Thanks for looking into this - I have the same need, in my case for rendering views for the purpose of sending emails.

I see at least 2 options:

  1. Refactor ViewResult and add a renderTemplate() method that works without an ActionContext, and returns a String of the rendered template. The existing execute() method could then use this new renderTemplate() method.
  2. Create a new class, perhaps ufront.view.TemplateRenderer or similar, and then get ViewResult to use that. This has the advantage of being a bit cleaner, if you just want to render a template not do a whole ViewResult.

Either way, we'll probably need to use dependency injection, because that's how we'll find the UFViewEngine etc.

If we can agree on a preferred API then either you or me can work on it.

It would be a great addition to have.

tiagolr commented 9 years ago

Hey, i fixed this problem with greatness using stipple and viewResult globalVariables:

// Partials.hx
public static var navbar = CompileTime.readFile('app/partials/navbar.html');
static public function create(tpl:String) { return new Template().fromString(tpl);}

// HomeController.hx
@post public function init() {
    ViewResult.globalValues.set( "navbar", Partials.create(Partials.navbar));
}
<!-- layout.html -->
<body>
    {{>navbar}}
</body>

It works like a charm, the contest is passed down to partials, so they can even be nested, they always render context variables, helpers, global vars etc...

Still partials could be supported in ViewResult by having an array of views to render instead of only one main view before the layout.

I think option 2 is more clean and it works well like that in ExpressJs, who needs it first implements it sounds good?

PS - there is a pull request to this repo adding stipple support in case you didn't noticed.

jasononeil commented 9 years ago

Nice work combining Template.fromString and CompileTime.readFile, that's a very clean solution.

I still think creating a TemplateRenderer class, and then refactoring ViewResult to depend on it, sounds like a good move. I'll leave the issue open until one of us implements it :)

jasononeil commented 8 years ago

I'll write up some more full documentation later, but these were my notes in designing this feature...

I'm pretty happy with the outcome, but will test it a bit more in real projects over the next few weeks.


layout.html

<html>
    <head>
        <title>@title</title>
    </head>
    <body>
        @nav()
        @viewContent
    </body>
</html>

view.html

<div class="container">
    <h1>@title</h1>
    @toolbar()
    <ul>
        @for( post in posts ) {
            <li>@post.title</li>
        }
    </ul>
</div>

nav.html

<ul role="nav" class="nav">
    <li><a href="">Link 1</a></li>
    <li><a href="">Link 2</a></li>
    @if ( user==null ) {
        <li><a href="">Log in</a></li>
    }
    else {
        <li><a href="">Your Profile</a></li>
        <li><a href="">Log out</a></li>
    }
</ul>

toolbar.html

<div role="toolbar" class="btn-toolbar">
    @if( posts.length>0 ) { @button({link:"#", name:"View Posts"}) }
    @if( user.can(WritePost) ) { @button({link:"#", name:"New Post"}) }
    @if( user.can(ModerateComments) ) { @button({link:"#", name:"Comments"}) }
    @if( user.can(ViewStats) ) { @button({link:"#", name:"Blog Statistics"}) }
</div>

AdminController.hx

return new ViewResult({
    title: "Blog Admin",
    description: "Take care of all the things",
    posts: posts,
    user: context.currentUser
})
.addPartial( "nav", "nav.html", TemplatingEngines.erazor )
.addPartial( "toolbar", "toolbar" )
.addPartialString( "btn", "<a href='::link::' class='btn'>::name::</a>", TemplatingEngines.haxe );

What it will do in ViewResult.executeResult()

var partialFutures = new Map();
for ( partialName in partials.keys() ) {
    var partialPath = partials[partialName];
    partialPath = addViewFolderToPath( partialPath );
    var template = loadTemplateFromSource( path, templatingEngine?? );
    partialFutures()
}

var templateReady = loadTemplateFromSource()...
var layoutReady = loadTemplateFromSource()...
var partialsReady = Future.fromMany( partialFutures.array() );

return FutureTools.when( templateReady, layoutReady, partialsReady ).map(function(viewOutcome,layoutOutcome,partialArray) {
    var combinedData = getCombinedData();
    var helpers = getCombinedHelpers();
    for ( partialName in partialFutures ) {
        var partialTplOutcome = partialFutures[partialName];
        var partialFn = function( partialData:TemplateData ) {
            if ( partialData==null )
                partialData = new TemplateData();
            partialData.merge( combinedData );
            executeTemplate( 'Partial[$partialName]', partialTplOutcome, partialData );
        }
        combinedHelpers[partialName] = partialFn;
    }
    var viewOut = executeTemplate( "view", viewOutcome, combinedData, combinedHelpers );
    var finalOut = executeTemplate( "layout", layoutOutcome, combinedData, combinedHelpers );
});

Now this means that ViewResult.executeTemplate() and UFTemplate.execute() need to be updated to support helpers.

We could have a type, like:

abstract Helper({ numArgs:Int, fn:Function }) {
    new( numArgs:Int, fn:Function ) {
        this = { numArgs:Int, fn:Function };
    }

    @:from( Void->Void ) new( 0, fn );
    @:from( T1->Void ) new( 1, fn );
    @:from( T1->T2->Void ) new( 2, fn );
    @:from( T1->T2->T3->Void ) new( 3, fn );
    @:from( T1->T2->T3->T4->Void ) new( 4, fn );

    function call0() Reflect.callMethod( {}, fn, [] );
    function call1(arg1) Reflect.callMethod( {}, fn, [arg1] );
    function call2(arg1,arg2) Reflect.callMethod( {}, fn, [arg1,arg2] );
    function call3(arg1,arg2,arg3) Reflect.callMethod( {}, fn, [arg1,arg2,arg3] );
    function call4(arg1,arg2,arg3,arg4) Reflect.callMethod( {}, fn, [arg1,arg2,arg3,arg4] );
}

With: helpers:Map<String,Helper>.

We can then use this to retrieve a function that is compatible with haxe.Template macros, in that it expects the first argument to be a resolve function.


While we're doing all this, can we make ViewResult available outside of a request?

function executeResult(actionContext) {
    // infer layout if needed
    // infer template if needed
    var viewFolder = getViewFolder( actionContext );
    var viewEngine = actionContext.injector.getInstance( UFViewEngine );

    var finalOut = renderViewResult( viewEngine, viewFolder, baseUri );

    ContentResult.replaceRelativeLinks( actionContext, finalOut );
    writeResponse( finalOut, combinedData, actionContext );
}
function renderViewResult() {
    // check view is set
    // check layout is set

    templateSource = addViewFolderToPath( templateSource, viewFolder );
    layoutSource = addViewFolderToPath( layoutSource, viewFolder );

    // Begin to load the templates (as Futures).
    var templateReady = loadTemplateFromSource( templateSource, viewEngine );
    var layoutReady = loadTemplateFromSource( layoutSource, viewEngine );
}