Search code examples
javascriptjqueryarraysangularjs-ng-repeat

Pure javascript/ jquery solution to Angular's 'ng-repeat' function with data-attributes?


I'm trying to create a custom repeater function similar to Angular JS's ng-repeat to iterate over data. I want to do this purely with javascript/ jquery and make use of html data-attributes.

So for example, let's say I have this html

    <tr data-repeat="datas">
      <td data-repeat-value="data.name">--</td>
      <td data-repeat-value="data.email">--</td>
    </tr>

I'm wanting to create a function that's smart enough to see this html and map over however many <tr>'s and <td>'s it needs to and spit out the data correctly.

Without the data-attributes, a js solution would look this

    ${datas.map(data => 
        `<tr>
            <td>${data.name}</td>
            <td>${data.email}</td>
        </tr>`).join("")
    }`;

But how would I write the js to make use of the data-attributes?

I've got a codepen showing the working version without data-attributes along with a non-working version with data-attributes.

Any help or guidance will be appreciated!


Solution

  • Here is the solution:

    getApiData(url, function(apidata){
        const $template = $('[data-repeat-new]');
        const $container = $template.parent();
    
        $template.remove();
    
        $container.append(
            apidata.map(data =>{
                const $snippet = $template.clone(true);
                $snippet.find('[data-repeat-value-new]').each((i, el) => {
                    $(el).text(eval($(el).data('repeat-value-new')));
                });
                return $snippet;
            })
        );
    });
    

    Full example is here: https://codepen.io/anon/pen/JQWVGG?editors=0010 (the only change is in // Not Working block).

    What it does:

    • finds a DOM fragment which is going to be our template (one that matches [data-repeat-new] selector)
    • stores a reference to parent element of that fragment -- the container where we're going to insert new content
    • removes that fragment, as we don't want to see template in final output
    • for each data element it does the following:
      • clones template
      • searches for all nodes that needs a value to be inserted (i.e. ones that match [data-repeat-value-new] selector)
      • and for every such node it replaces node's contents with evaluated expression (expression comes from value of data-repeat-value-new attribute, and we have data variable in eval's context, so, things just work)
    • afterwards, all these cloned templates are appended to container

    That eval call might look dangerous, so:

    • you can make simple expression parser that will reduce possible expressions to some subset of JS, e.g. it could be able to only get elements from fixed array (i.e. expressions like data.property or data.property[0])
    • or you can leave it as is, because this eval happens on client side, and code source is DOM node -- if attacker can set arbitrary value in your DOM, he probably can execute any other code too

    And a note re value of data-repeat-new: you get your API data in your code, and you never assign it, so, I'm not sure how one could interpret value of that attribute. One way would be provide some repository of data, where there are arrays with their names, like this:

    const repo = {
        data1: [{id: 'rec1'}, {id: 'rec2'}, {id: 'rec3'}]
    };
    

    And then in your code you could do this:

    const $template = $('[data-repeat-new]');
    const $container = $template.parent();
    
    $template.remove();
    
    const data = repo[$template.data('repeat-new')];
    
    $container.append(
        data.map(data =>{
            const $snippet = $template.clone(true);
            $snippet.find('[data-repeat-value-new]').each((i, el) => {
                $(el).text(eval($(el).data('repeat-value-new')));
            });
            return $snippet;
        })
    );