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

Support for maps, sets, and custom iterables in built-in "each" helper? #1418

Closed
jasonh-brimar opened this issue Jan 10, 2018 · 6 comments
Closed

Comments

@jasonh-brimar
Copy link

When using Handlebars in an ES6 environment, the built-in each helper's limitation of supporting only arrays and generic objects becomes inconvenient. To work around this, I started registering my own version of the each helper that supports arrays, maps, sets, custom iterables, and generic objects. That helper is below.

Is there a plan or willingness to introduce support for these types of lists in the built-in each helper? I ask because I understand that Handlebars aims to avoid polyfills and I imagine that the only way of making the new helper work without compromising on browser support would be to progressively enable support for the different list types dependent on the environment's native or pre-polyfilled support for Set, Map, and Symbol.

Handlebars.registerHelper("each", function (contexts, options) {

    // Throw a runtime exception if options were not supplied.
    if (!options) {
        throw new Handlebars.Exception("Must pass iterator to #each");
    }

    // If the "list of contexts" is a function, execute it to get the actual list of contexts.
    if (typeof contexts === "function") {
        contexts = contexts.call(this);
    }

    // If data was supplied, frame it.
    const data = options.data ? Object.assign({}, options.data, { _parent: options.data }) : undefined;

    // Create the string into which the contexts will be handled and returned.
    let string = "";

    // Create a flag indicating whether or not string building has begun.
    let stringExtensionStarted = false;

    // Create a variable to hold the context to use during the next string extension. This is done to
    // allow iteration through the supplied list of contexts one step out of sync as they are looped
    // through later in this helper, ensuring a predictable sequence of value retrieval, string
    // extension, value retrieval, string extension...
    let nextContext;

    // Create a function responsible for expanding the string.
    const extendString = (final = false) => {

        // If other contexts have been encountered...
        if (nextContext) {

            // Expand the string using the block function.
            string += options.fn(nextContext.value, {
                data: data ? Object.assign(data, {
                    index: nextContext.index,
                    key: nextContext.key,
                    first: !stringExtensionStarted,
                    last: final
                }) : undefined,
                blockParams: [nextContext.key, nextContext.value]
            });

            // Note that string extension has begun.
            stringExtensionStarted = true;

        // If no contexts have been encountered and this is the final extension...
        } else if (final) {

            // Expand the string using the "else" block function.
            string += options.inverse(this);

        }

    };

    // If a list of contexts was supplied...
    if (contexts !== null && typeof contexts !== "undefined") {

        // Start a counter.
        let index = 0;

        // If an array list was supplied...
        if (Array.isArray(contexts)) {

            // For each of the possible indexes in the supplied array...
            for (const len = contexts.length; index < len; index++) {

                // If the index is in the supplied array...
                if (index in contexts) {

                    // Call the string extension function.
                    extendString();

                    // Define the context to use during the next string extension.
                    nextContext = {
                        index: index,
                        key: index,
                        value: contexts[index]
                    };

                }

            }

        // If a map list was supplied...
        } else if (contexts instanceof Map) {

            // For each entry in the supplied map...
            for (const [key, value] of contexts) {

                // Call the string extension function.
                extendString();

                // Define the context to use during the next string extension.
                nextContext = {
                    index: index,
                    key: key,
                    value: value
                };

                // Increment the counter.
                index++;

            }

        // If an iterable list was supplied (including set lists)...
        } else if (typeof contexts[Symbol.iterator] === "function") {

            // Get an iterator from the iterable.
            const iterator = contexts[Symbol.iterator]();

            // Create a variable to hold the iterator's next return.
            let next;

            // Do the following...
            do {

                // Iterate and update the variable.
                next = iterator.next();

                // If there is anything left to iterate...
                if (!next.done) {

                    // Call the string extension function.
                    extendString();

                    // Define the context to use during the next string extension.
                    nextContext = {
                        index: index,
                        key: index,
                        value: next.value
                    };

                    // Increment the counter.
                    index++;

                }

            // ... until there is nothing left to iterate.
            } while (!next.done);

        // If a list other than an array, map, or iterable was supplied...
        } else {

            // For each key in the supplied object...
            for (const key of Object.keys(contexts)) {

                // Call the string extension function.
                extendString();

                // Define the context to use during the next string extension.
                nextContext = {
                    index: index,
                    key: key,
                    value: contexts[key]
                };

                // Increment the counter.
                index++;

            }

        }

    }

    // Call the string extension a final time now that the last supplied context has been encountered.
    extendString(true);

    // Return the fully-extended string.
    return string;

});
@nknapp
Copy link
Collaborator

nknapp commented Apr 5, 2020

Should be possible now, with #1557

@nknapp nknapp closed this as completed Apr 5, 2020
@karlvr
Copy link

karlvr commented Apr 16, 2020

@nknapp it appears that the implementation in #1557 doesn't support Map properly. It currently produces an iterated item being the entry in the Map, which is a tuple of [key, value], whereas the example code above makes the iterated item the value and sets @key, which I think is preferable. It's preferable to me!

Also, it seems that expressions don't currently support Map, so you can't say {{person.myMap.myMapKey}}. I'm delving more into this issue now.

@karlvr
Copy link

karlvr commented Apr 16, 2020

With an addition in lookupProperty in runtime.js we can lookup properties in Maps:

    lookupProperty: function(parent, propertyName) {
      if (parent instanceof Map) {
        return parent.get(propertyName)
      }

Is there any appetite to add support like this?

@nknapp
Copy link
Collaborator

nknapp commented Apr 16, 2020

@karlvr I think your proposal is worth looking into. But I would like to discuss it.

@nknapp
Copy link
Collaborator

nknapp commented Apr 16, 2020

@karlvr could you start a new issue for Map support. Parts of this issue is already resolved and I would like to have a clean start.

@nknapp nknapp closed this as completed Apr 16, 2020
@karlvr
Copy link

karlvr commented Apr 16, 2020

@nknapp thank you very much for your speedy response; I've just made a PR with the suggested changes. Could we discuss there? #1679

jaylinski added a commit that referenced this issue Sep 4, 2023
Also support Map-keys in lookup-expressions.

See #1418 #1679
jaylinski added a commit that referenced this issue Sep 6, 2023
Also support Map-keys in lookup-expressions.

See #1418 #1679
jaylinski added a commit that referenced this issue Sep 6, 2023
Also support Map-keys in lookup-expressions.

See #1418 #1679
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

3 participants