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

Get element from array by index #80

Closed NightFox7 closed 10 years ago

NightFox7 commented 10 years ago

Is there a way to get an element from an array using its index?

groue commented 10 years ago

Hi

No, there is no such feature today. You can use the first and last keys of NSArray, though.

I never had such a need... Would you mind giving an example where such a possibility would be useful?

groue commented 10 years ago

All right. I'm closing this issue right now.

GRMustache implements the mustache templating language as defined by https://github.com/mustache/spec. And the spec, so far, as no support for accessing array elements by index.

I recommend you open an issue in the very repository of the spec: https://github.com/mustache/spec/issues

NightFox7 commented 10 years ago

Sorry I didn't answer you sooner. As for the example that you demanded. Well, let's say I have 2 NSArray. Each has some information about a certain object like so:

array1 = [ ['name': 'John', 'lastname': 'doe'], ['firtname': 'John2', 'lastname': 'doe2'] ];
array2 = [ ['age': 20], ['age': 25] ];

I want to display:

John doe is 20 years old.
John2 doe2 is 25 years old.

So my idea was to go through the first array then use the index to get the required object from the second array.

Do you have an alternative way without using the index?

groue commented 10 years ago

I get it. We could imagine some zip filter, that you could use in a template like:

{{# zip(array1, array2) }}
  {{ name }} {{ lastname }} is {{ age }} years old.
{{/ }}

It's quite possible, using [GRMustacheFilter variadicFilterWithBlock:]. Yet the usual advice in these cases is to give the template data it can render. The template would look like:

{{# zipped_array1_array2) }}
  {{ name }} {{ lastname }} is {{ age }} years old.
{{/ }}

Where zipped_array1_array2 would be pre-computed, or the result of a computed property based on array1 and array2. It is simpler to read and simpler to understand.

Yet a zip filter would be handy in the GRMustache standard library, thanks for the idea :-) You may try to write it if you want :-)

NightFox7 commented 10 years ago

Thanks @groue for your answer. I am gonna try to implement the zip filter that you suggested and get back to you when it's done.

groue commented 10 years ago

GRMustache 7.2 has shipped, with built-in zip filter (documentation). Thanks for your contribution :smile:

groue commented 10 years ago

The introduction of the zip filter in the standard library has generated backward compatibility issues (see issue #82).

GRMustache v7.3.0 has shipped without it, in order to restore compatibility.

Below you'll find the documentation for the discontinued zip filter, and its Objective-C code:

zip

Usage:

The zip filter iterates several lists all at once. On each step, one object from each input list enters the rendering context, and makes its own keys available for rendering.

Document.mustache:

{{# zip(users, teams, scores) }}
- {{ name }} ({{ team }}): {{ score }} points
{{/}}

data.json:

{
  "users": [
    { "name": "Alice" },
    { "name": "Bob" },
  ],
  "teams": [
    { "team": "iOS" },
    { "team": "Android" },
  ],
  "scores": [
    { "score": 100 },
    { "score": 200 },
  ]
}

Rendering:

- Alice (iOS): 100 points
- Bob (Android): 200 points

In the example above, the first step has consumed (Alice, iOS and 100), and the second one (Bob, Android and 200):

The zip filter renders a section as many times as there are elements in the longest of its argument: exhausted lists simply do not add anything to the rendering context.

id zipFilter = [GRMustacheFilter variadicFilterWithBlock:^id(NSArray *arguments) {

    // GRMustache generally identifies collections as objects conforming
    // to NSFastEnumeration, excluding NSDictionary.
    //
    // Let's validate our arguments first.

    for (id argument in arguments) {
        if (![argument respondsToSelector:@selector(countByEnumeratingWithState:objects:count:)] || [argument isKindOfClass:[NSDictionary class]]) {
            return [GRMustacheRendering renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError **error) {
                if (error) {
                    *error = [NSError errorWithDomain:GRMustacheErrorDomain code:GRMustacheErrorCodeRenderingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"zip filter in tag %@ requires all its arguments to be enumerable. %@ is not.", tag, argument] }];
                }
                return nil;
            }];
        }
    }

    // Turn NSFastEnumeration arguments into enumerators. This is
    // because enumerators can be iterated all together, when
    // NSFastEnumeration objects can not.

    NSMutableArray *enumerators = [NSMutableArray array];
    for (id argument in arguments) {
        if ([argument respondsToSelector:@selector(objectEnumerator)]) {
            // Assume objectEnumerator method returns what we need.
            [enumerators addObject:[argument objectEnumerator]];
        } else {
            // Turn NSFastEnumeration argument into an array,
            // and extract enumerator from the array.
            NSMutableArray *array = [NSMutableArray array];
            for (id object in argument) {
                [array addObject:object];
            }
            [enumerators addObject:[array objectEnumerator]];
        }
    }

    // Build an array of objects which will perform custom rendering.

    NSMutableArray *renderingObjects = [NSMutableArray array];
    while (YES) {

        // Extract from all iterators the objects that should enter the
        // rendering context at each iteration.
        //
        // Given the [1,2,3], [a,b,c] input collections, those objects
        // would be [1,a] then [2,b] and finally [3,c].

        NSMutableArray *objects = [NSMutableArray array];
        for (NSEnumerator *enumerator in enumerators) {
            id object = [enumerator nextObject];
            if (object) {
                [objects addObject:object];
            }
        }

        // All iterators have been enumerated: stop

        if (objects.count == 0) {
            break;
        }

        // Build a rendering object which extends the rendering context
        // before rendering the tag.

        id<GRMustacheRendering> renderingObject = [GRMustacheRendering renderingObjectWithBlock:^NSString *(GRMustacheTag *tag, GRMustacheContext *context, BOOL *HTMLSafe, NSError **error) {
            for (id object in objects) {
                context = [context contextByAddingObject:object];
            }
            return [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];
        }];
        [renderingObjects addObject:renderingObject];
    }

    return renderingObjects;
}];