Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

157 lines (109 sloc) 6.011 kb

up, next

Collection Indexes

In a genuine Mustache way

Mustache is a simple template language. Its specification does not provide any built-in access to collection indexes. It does not provide any way to render a section at the beginning of a loop, and another section at the end. It does not help you render different sections for odd and even indexes.

If your goal is to design your templates so that they are compatible with other Mustache implementations, the best way to render indices and provide custom looping logic is to have each of your data objects provide with its index, regardless of how tedious it may be for you to prepare the rendered data.

For instance, instead of [ { name:'Alice' }, { name:'Bob' } ], you would provide: [ { name:'Alice', position:1, isFirst:true, isOdd:true }, { name:'Bob', position:2, isFirst:false, isOdd:false } ].

GRMustache solution

Download the code

You can have Mustache templates render positional keys like position or isFirst for you, and avoid preparing your data.

However, it may be tedious or impossible for other Mustache implementations to produce the same rendering.

So check again the genuine Mustache way, above. Or keep on reading, now that you are warned.

The rendering

Below we'll implement the special keys position, isFirst, and isOdd:

Document.mustache:

<ul>
{{# withPosition(people) }}
  <li class="{{# isOdd }}odd{{/ isOdd }} {{# isFirst }}first{{/ isFirst }}">
    {{ position }}:{{ name }}
  </li>
{{/ withPosition(people) }}
</ul>

Render.m:

id data = @{
    @"people": @[
        @{ @"name": @"Alice" },
        @{ @"name": @"Bob" },
        @{ @"name": @"Craig" },
    ],
    @"withPosition": [PositionFilter new]
};

NSString *rendering = [GRMustacheTemplate renderObject:data
                                          fromResource:@"Document"
                                                bundle:nil
                                                 error:NULL];

Final rendering:

<ul>
  <li class="odd first">
    1:Alice
  </li>
  <li class=" ">
    2:Bob
  </li>
  <li class="odd ">
    3:Craig
  </li>
</ul>

PositionFilter implementation

You may just skip the rest of this document, and download the PositionFilter class. It should be trivial to adapt, should you need the isLast property, for example.

Let's see how it is implemented.

Due to the parenthesis in the withPosition(people) expression, we know that it is a filter, an object that conforms to the GRMustacheFilter protocol:

/**
 * A filter that renders its array argument with the extra following keys
 * defined for each item:
 *
 * - position: returns the 1-based index of the item
 * - isOdd: returns YES if the position of the item is odd
 * - isFirst: returns YES if the item is at position 1
 */
@interface PositionFilter : NSObject<GRMustacheFilter>
@end

The protocol requires the transformedValue: method, that returns the result of the filter.

Since we need a custom rendering of the array, the result of the filter will conform to the GRMustacheRendering protocol (see the Rendering Objects Guide).

Rendering objects take full responsability of their rendering. Our will render the section tag as many times as the array has items, extending the context stack with both a dictionary containing the special keys, and the array items that will provide the name key.

@implementation PositionFilter

/**
 * The transformedValue: method is required by the GRMustacheFilter protocol.
 * 
 * Don't provide any type checking, and assume the filter argument is an array:
 */

- (id)transformedValue:(NSArray *)array
{
    // We want to provide custom rendering of the array.
    //
    // So let's return an object that does custom rendering.

    return [GRMustache renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError *__autoreleasing *error)
    {
        NSMutableString *buffer = [NSMutableString string];

        [array enumerateObjectsUsingBlock:^(id item, NSUInteger index, BOOL *stop) {

            // Have our "specials" keys enter the context stack,
            // so that the `{{ position }}` tags etc. can render:

            id specials = @{
                @"position": @(index + 1),
                @"isFirst" : @(index == 0),
                @"isOdd" : @(index % 2 == 0),
            };
            GRMustacheContext *itemContext = [context contextByAddingObject:specials];


            // Have the item itself enter the context stack,
            // so that the `{{ name }}` tag can render:

            itemContext = [itemContext contextByAddingObject:item];


            // Render the item:

            NSString *itemRendering = [tag renderContentWithContext:itemContext HTMLSafe:HTMLSafe error:error];
            [buffer appendString:itemRendering];
        }];

        return buffer;
    }];
}

@end

Writing filters that return rendering objects lead to code that is pretty close to the Handlebars.js block helpers. You may enjoy comparing the code above to the each_with_index Handlebars helper.

Download the code

up, next

Jump to Line
Something went wrong with that request. Please try again.