groue / GRMustache

Flexible and production-ready Mustache templates for MacOS Cocoa and iOS
http://mustache.github.com/
MIT License
1.44k stars 190 forks source link

Computing a value out of several objects inside a template #73

Closed martinjuhasz closed 10 years ago

martinjuhasz commented 10 years ago

my template looks like the following

<h1>{{project.title}}</h1>
{{#questions}}
    <div class="question">{{title}}</div>
    <div class="answer"> </div>
{{/questions}}

to get an answer, i would do something like this in obj-c:

Answer *answer = [question getAnswerForProject:project];
NSLog("%@", answer.text);

is this possible in any way using grmustache?

groue commented 10 years ago

Hi Martin. So the answer div should display a text that comes from the current question of the current project. Is it true? If so, if the project global for the whole template rendering, or not?

martinjuhasz commented 10 years ago

yes thats what i want to do. the project is global for the template. in fact, questions and project are all subclasses of NSManagedObject.

groue commented 10 years ago

OK. And the Question class can not have any answer method, since the only available method is getAnswerForProject: which requires a project, and has no clue about the global project of the template. Right?

martinjuhasz commented 10 years ago

yes. a question can not have a answer property because there are many projects that all have an answer for this question.

in my template i want the answer of a specific project to this question.

if it helps, here is my core data model: https://dl.dropboxusercontent.com/u/80699/model.png

groue commented 10 years ago

Update your template with <div class="answer">{{ answerTextForProject }}</div>, and fetch inspiration from the sample code below.

The goal is to have a key which returns an object that:

// {{ answerTextForProject }}
//
// {{# question }}{{ answerTextForProject }}{{/ question }} should render
// [question getAnswerForProject:project].text
//
// So let's have answerTextForProject return an object which simply...
// renders [question getAnswerForProject:project].text :-)
//
// As soon as an object should render in a custom way, GRMustacheRendering
// helps a lot:

id extraKeys = @{ @"answerTextForProject": [GRMustache renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError **error) {
    // load the question, and the project from the context stack
    MJUProject *project = [context valueForMustacheKey:@"project"];
    MJUQuestion *question = [context valueForMustacheKey:@"question"];
    // answer
    MJUAnswer *answer = [question getAnswerForProject:project];
    // render
    return answer.text;
}]};

// Insert this new key in the rendering
GRMustacheTemplate *template = nil;
[template renderObjectsFromArray:@[ data, extraKeys ] error:NULL];

Tell me if you have any question.

martinjuhasz commented 10 years ago

ok, while trying to implement this, i can't get the current question. since i loop through multiple questions [context valueForMustacheKey:@"question"]; does not work.

{{#sortedQuestions}}
    <div class="question">{{title}}</div>
    <div class="answer">{{answerForProject}}</div>
{{/sortedQuestions}}

sortedQuestions is a method that returns an array of question objects. how do i get the value of the current object of the loop while i'm defining the extraKeys?

groue commented 10 years ago

How, you are right - sorry, I did not test my code. Try question = [context topMustacheObject] instead.

martinjuhasz commented 10 years ago

This works, thanks!

is this the only way i can "call" methods with parameters?

groue commented 10 years ago

Well, here is another way to do it, by altering the MJUQuestion class, this time :

// MJUQuestion support for GRMustache
@interface MJUQuestion(GRMustache)
@end
@implementation MJUQuestion(GRMustache)

- (id)answerTextForProject
{
    return [GRMustacheFilter filterWithBlock:^id(MJUProject *project) {
        MJUAnswer *answer = [self getAnswerForProject:project];
        return answer.text;
    }];
}

@end

And the template would look like {{#sortedQuestions}}{{ answerTextForProject(project) }}{{/ sortedQuestions}}.

You may prefer this solution.

is this the only way i can "call" methods with parameters?

Genuine Mustache is all about nested sections which extend a context stack, and extracting values, searching from the top of the context stack.

Your need is quite different: you want to mix several objects taken from different levels in the context stack. This goes well beyond Mustache.

GRMustache won't let you down, but you have to go custom. GRMustacheFilter and GRMustacheRendering are nice and powerful protocols. You may appreciate reading https://github.com/groue/GRMustache/blob/master/Guides/rendering_objects.md and https://github.com/groue/GRMustache/blob/master/Guides/filters.md

groue commented 10 years ago

Reusing the extraKeys object, you can also write:

// Support for {{ answerText(question, project) }}
id extraKeys = @{ @"answerText": [GRMustacheFilter variadicFilterWithBlock:^id(NSArray *arguments) {
    MJUQuestion *question = arguments[0];
    MJUQuestion *project = arguments[1];
    return [question getAnswerForProject:project].text;
}];

And in the template: {{# questions }} {{ answerText(., project) }}{{/ }}

The choice is yours, depending on how much do you want your custom key to be reusable in other parts of your template, how explicit should be your arguments in the template, how much do you want to pollute your CoreData objects, etc.

I'm sorry there is no unique answer.

groue commented 10 years ago

Should other attributes of the answer be used, you might also go with:

// Support for `answer(question, project)`
// This filter returns an MJUAsnwer instance.
id extraKeys = @{ @"answer": [GRMustacheFilter variadicFilterWithBlock:^id(NSArray *arguments) {
    MJUQuestion *question = arguments[0];
    MJUQuestion *project = arguments[1];
    return [question getAnswerForProject:project];
}];

And in the template:

{{# questions }}
    {{ answer(., project).text }}
    {{ dateFormat(answer(., project).date) }}
    {{! or... }}
    {{# answer(., project) }}
        {{ text }}
        {{ dateFormat(date) }}
    {{/ }}
{{/ }}

Happy Mustache :-)

martinjuhasz commented 10 years ago

ok nice. one more thing if you have the time.

i now only want to show this if a project has an answer to a question.

{{#sortedQuestions}}
    {{# <!-- [question hasAnswerForProject:project] -->}}
        <div class="question">{{title}}</div>
        <div class="answer">{{answerForProject}}</div>
    {{/ <!-- whatever -->}}
{{/sortedQuestions}}

if i do the same thing here with returning nil if it doesn't exist and a string if it does this (of course) doesn't work.

how would you implement a if statement that needs a object passed to it (project) to determine if it works? also, something like {{# abc}} would change the scope for title and `ànswerForProject`` so that after this addition they wouldn't work anymore, am i right?

what can i do about that?

groue commented 10 years ago

Doesn't using the answer(question, project) filter (see https://github.com/groue/GRMustache/issues/73#issuecomment-37429597) solves this?

{{# sortedQuestions }}
    {{# answer(., project) }}   {{! if there is an answer, put it on the stack }}
        <div class="question">{{ title }}</div>   {{! loaded from the question, assuming answer has no title }}
        <div class="answer">{{ text }}</div>      {{! loaded from the answer }}
    {{/ }}
{{/ }}
martinjuhasz commented 10 years ago

pwew, yeah this works. pretty much stuff to generate this template. i'm not sure that GRMustache is the right approach in my specific case.

anyway, thanks for this great and fast help!

groue commented 10 years ago

You are welcome, Martin!

GRMustache is a designed to be a Mustache engine which won't let you down.

What does it mean? Mustache (available in Obj-C, Ruby, Javascript, Python, PHP, etc.) is a very minimalistic template engine, which usually force you to prepare your data, which mean preparing a dedicated, maybe complex, object on the side. This object is filled with all the keys expected by the template, and building it eventually turns into a chore, as your templates increase their complexity.

GRMustache adds a layer on top, which allows you to inject directly your raw models, without preparing this dedicated complex object. Increasing the complexity of your template, step after step, is possible without being forced to refactor everything. You can add little touches and helpers, one after the other. This is what I call "not letting your down".

However, eventually, you may want to build a dedicated ViewModel object which encapsulates all your non-trivial needs (see the ViewModel Guide).

Thanks for your nice comments, cheers :-)

martinjuhasz commented 10 years ago

yes, i'm probably going to build extra view models for generating my html using GRMustache. For now, this helper functions work perfectly, so thanks again.

groue commented 10 years ago

About ViewModels : GRMustache7 is about to ship, which removes the support for subclassing GRMustacheContext: you should ignore this part of the guide.