creating your own framework part #2

Tue Jan 29 2019 22:48:31 GMT+0100 (Hora estándar romance), David Ibáñez

Create Your Own Framework In Less Than 300 Lines Of Code. Part #2.

Do you know you can build your own framework, with no dependencies, compatible with all browser since IE8+ in less than 300 lines of code?

Using vanilla Javascript, no third party code, without compilers, transpilers, npm, webpack, ... Just your code! Compatible with any server technology.

This is the part #2 of the tutorial. You can find the first article here.

Let's recap what we did so far.

  • Created a DynamicTemplate module
  • Created the HTML templates with dynamic variables using curly braces.
  • Created a function to render the templates using dynamic data.
  • Added to our original code and applied to render our picture results from Pixabay.

This is what we got with our version so far.
pixabay-browser.jpg

It's time to modify the template system allowing more versatile features!

What we'll see in this article.

  • Add conditionals statements to the templates.
  • Add computed functions, allowing to apply a function to the data before apply to the template.
  • Add iterations allowing the template system to render array of objects

Conditionals statements

Conditional statements allow us to use comparison operators into our templates. Imagine you need to set a class depending if a value it's true or not in your data.

We will define the conditional statements in our templates using the same curly braces we used so far.

 {{if:some-value:operator:comparing-value:option1:option2}}

A real use case would be:

 {{if:item.type:==:photo:photo-class:video-class}}

Because we want a full featured template system, we will allow to define dynamic data in the result values as well. This means we will add a way to define variables inside the conditional statement. Like this.

 {{if:item.user:is:true:{@item.user.name@}:not known user}}

In the case above, if item has a user property, then set the user.name, else set the string 'not known user'.

Let's create the methods called getValueFromConditional() in our template module to add this feature. We create first the method to check the condition and execute the operation.

TRY FOR YOURSELF - Before going further it's time for you to try.

  1. Define a method getValueFromCondition() that receive two arguments. The string containing the conditional statement like if:some-value:operator:comparing-value:option1:option2 and the object with the data.
  2. Split the if:some-value:operator:comparing-value:option1:option2 into an array of ['if', 'some-value', 'operator', 'comparing-value', 'option1', 'option2'] - Hint: Array split method.
  3. Use your function getValueFromObject() we coded in the last article to receive the value of some-value you will compare to.
  4. As we receive the pieces as string, we need to convert some values to javascript primitives. "null", "true" and "false" to null, true, false.
  5. Now check if option1 or option2 are string values or they are dynamic data defined as {@object.property@}. Hint - Create a function and use to check both values. Use a regular expression similar as we did to find the {{double-braces}}expressions. If Regex matches, then replace the definition by its value using the getValueFromObject() function. I provide you the Regex before giving you the entire solution.
  6. Execute the condition to find the correct value. Hint - Create a switch statement using the operator as the parameter to check.
  7. You can return the result value.

If you want to test your new method, I will add you some useful stuff. You can read until you find a STOP HERE.

Apply your new method to our template system

You should update your original code to distinct when you face with a plain dynamic value or a conditional statement.
Maybe you find a {{item-value}} or a {{if:some-value: ... }}. When we checked dynamic value we used a method called getValueFromObject(). Now we'll implement a function in the middle to differentiate if we're facing a plain dynamic value or a conditional statement. Let's call this method getValue(). This method will call getValueFromObject() or getValueFromConditional() after checking if if: string is in the definition.

The getValue() function

Functionality

  • Receive the dynamic definition and chose what method to use depending if it's a conditional or not.
  • Returns the value received by the method selected.
function getValue(var_string, object){

    // Check if the dynamic variable is the type {{if:condition:value1:value2}} 
    // looking if there's an 'if:' in the string
    if (var_string.indexOf('if:') > -1) {

        // Retrieves the value from the conditional statement
        return getValueFromConditional(var_string, object);
    } else {

        // Retrieves the value directly from the object
        return getValueFromObject(var_string, object);
    }
}

Finally we only need to modify our first applyDynamicValue() method to call the getValue() method instead of getValueFromObject(). You'll find on the line 33 of your dynamic-template.js

From

var value = getValueFromObject(dyn_value, object);

to

var value = getValue(dyn_value, object);

Finally, add one conditional statement in your HTML template in order to check your new function. I added a conditional statement to check if the response from Pixabay is a photo or a video {{if:item.type:==:photo:photo-class:video-class}}.

<script type="text/template" id="template-item">
    <div class="item panel {{if:item.type:==:photo:photo-class:video-class}}"> <!-- New conditional statement -->
        <div class="image" style="background-image: url({{item.preview}})"></div>
        <div class="details">
            <div class="user">
                <div class="thumb" style="background-image: url({{item.user_img}})"></div>
                <div>{{item.user}}</div>
            </div>
            <div class="views"><span class="fa fa-eye"></span> {{item.views}}</div>
            <div class="likes"><span class="fa fa-heart"></span> {{item.likes}}</div>
        </div>
    </div>
</script>

Regular Expression

In this function we need to use a regular expression to find {@value@}.

// Regex Explanation:
// Check if the values contains expressions to evaluate expressed
// as {@object@} or {@object.property@}
// Starts with a {@ -> '{@'
// Retrieve any character until the end ->  (.*?) - Here the '?' means to get as minimum as possible
// Ends with a @} -> '@}'
var expr = condition.match(/{@(.*?)@}/);

If you're going to try for yourself STOP HERE. Come back to this point when tried.

The code solution

I hope you tried for yourself. Great! Let's see what solution I'm thinking of.

The getValueFromConditional() function

Functionality

  • Gets the string containing the conditional statement definition.
  • Splits the conditional statement into its parts.
  • Gets the value from the data.
  • Checks if the condition contains "null" or "true" to convert to null or true.
  • Checks if the returned values are dynamic values or plain strings.
  • Executes the conditional operation based on the operator used in the definition.
  • Returns the value.
// Analyze the condition: {{if:some-value:operator:value:option1:option2}}
function getValueFromConditional(str_condition, object){

    // Splits conditional parameters into arrey
    var condition = str_condition.split(':');

    // Gets the value from the object to compare {{if:SOME-VALUE:operator:value:option1:option2}}
    var variable_value = getValueFromObject(condition[1], object);

    // converts some possible string values to the javascript primitives
    // {{if:some-value:operator:COMPARING-VALUE:option1:option2}}
    switch(condition[3]){
        case "null": condition[3] = null; break;
        case "false": condition[3] = false; break;
        case "true": condition[3] = true; break;
    }

    // for the conditional values we can use objects indicated by {@object.property@}
    // on {{if:some-value:operator:value:OPTION1:OPTION2}}
    condition[4] = checkVariableInContent(condition[4], object);
    condition[5] = checkVariableInContent(condition[5], object);

    // Finally we compute the conditional based on the operator {{if:some-value:OPERATOR:value:option1:option2}}
    // We use 'is' to compare values to true like: item.name ? 'value if true' : 'value if false'
    switch(condition[2]){
        case 'is':
            return variable_value ? condition[4] : condition[5];
        case '==':
            return (condition[3] == variable_value) ? condition[4] : condition[5];
        case '!=':
            return (condition[3] != variable_value) ? condition[4] : condition[5];
        case '>':
            return (condition[3] > variable_value) ? condition[4] : condition[5];
        case '<':
            return (condition[3] < variable_value) ? condition[4] : condition[5];
        case '<=':
            return (condition[3] <= variable_value) ? condition[4] : condition[5];
    }
}

// We check if condition values has reference to the object to get dynamic data
function checkVariableInContent(condition, object){

    // Check if the values contains expressions to evaluate expressed
    // as {@object@} or {@object.property@}
    var expr = condition.match(/{@(.*?)@}/);

    // Replace the expression by its value found in the object or return the string as it is
    return expr ? condition.replace(expr[0],getValueFromObject(expr[1], object)) : condition;
}

This is the result. I added a class in out CSS called video-class with blue border and some radius on the corners only when searching for videos.

Video Class.PNG

Fantastic! Frameworks out there start to feel anxious with your template system!

Computed functions

Computed function will allow you to include methods in your template. You will be able to manipulate data before applying a value to your HTML.

Why is this useful?
If you remember the first version of the Pixabay API we seen, the items found showed the number of likes and views in short format. The API tells us that a photo has 375000 views and we show 375K instead of the large number. Including a method on our templates allow us to make this transformations.

We will add a new format in our dynamic variables definition.

 {{compute:someMethod(data)}}

We would accept the following formats:

 {{compute:someMethod(object.method)}}

or

 {{compute:someMethod('My string')}}

or

 {{compute:someMethod('My string', data, data.property)}}

It seems a little bit complicated to accept too much options. It is! ;)

TRY FOR YOURSELF - It's your turn.

  1. Create a function getValueFromCompute() to call when you find a compute function.
  2. As getValueFromConditional() you'll receive two arguments. The string containing the conditional statement like compute:someMethod('My string', data, data.property) and the object with the data.
  3. Try to break the statement in two parts. One the method name and the other the arguments wrapped by parenthesis. Hint Use a Regex. I provide one below before going for the solution.
  4. Now you have a variable with a string containing all the arguments. Can be one or more, separated by a comma. You have to split these arguments into an array.
  5. Iterate through the array and get the values for every argument. Each argument can be just a string or a dynamic value. Find out which one it is. If it is dynamic use getValueFromObject() to retrieve the value. You need to end up with another array, but with the converted values.
  6. The next steps can be a little bit tricky. You have to find the method that compute is calling. First try if in the global scope window the function exists. Hint - typeof *** === "function"
  7. If it is a function. Then just call the function using your array of parameters as arguments. Hint - Use apply function prototype method. This allows you to pass the arguments as an array, and this is how we have the parameters values right now.
  8. If not. There're two options. The method can be:
  9. item.myMethod -> Referencing the object received -> This would be ObjectReceived["item"]["myMethod"]
  10. someObject.method -> It's calling some method of an object that's in the global scope window -> This would be window["globalObject"]["method"]
  11. When you'll find the correct function to be called, call it. Hint - Use apply function prototype method. This allows you to pass the arguments as an array, and this is how we have the parameters values right now. Remember using apply you need to supply the function context, _also known as_, who is this. You can find more information on MDN
  12. Finally, return the value obtained invoking the function.

Wow! some hard work here! Good luck! Read how can you test your method before start coding it.

To test your method, you need to follow the next steps to prepare the template using a compute function. You can read until you find a STOP HERE.

Apply your new method to your template system

Add this new method to our previous getValue function. We'll check if the dynamic variable contains the new compute:.

From

function getValue(var_string, object){

    // Checks if the dynamic variable is the type {{if:condition:value1:value2}}
    // looking if there's an 'if:' in the string
    if (var_string.indexOf('if:') > -1) {

        // Retrieves the value from the conditional statement
        return getValueFromConditional(var_string, object);
    } else {

        // Retrieves the value directly from the object
        return getValueFromObject(var_string, object);
    }
}

to

function getValue(var_string, object){

    //Check if the dynamic variable is the type {{if:condition:value1:value2}}
    if (var_string.indexOf('if:') > -1) {
        // Retrieves the value from the conditional
        return getValueFromConditional(var_string, object);
    } else if (var_string.indexOf('compute:') > -1) {
        return getValueFromCompute(var_string, object);
    } else {
        return getValueFromObject(var_string, object);
    }
}

NOTE - At this point we have to consider one thing. When we get the dynamic values, we replace the original dynamic variables/expressions by these values. We used regular expressions to do the task. In this new feature we added parenthesis in the expressions {{compute:method(data)}}. Parenthesis are special characters in RegEx. We need to scape them to convert to regular characters. Let's do that before going further.

In the function applyDynamicValues() at the top part of our dynamic-template.js file, you'll find the place where we replace the dynamic variables by its value. We have to modifiy the following code.

From

var regexp = new RegExp("{{" + dyn_value + "}}", 'g');

to

var regexp = new RegExp("{{" + dyn_value.replace('(','\\(').replace(')','\\)') + "}}", 'g');

Almost ready to start coding your new method. you only has to include a compute function on your template. In app.js you'll find you already have a function to convert long number of views and likes to short ones. I called the function formatTotals(). I'm just dividing and rounding the long number.

function formatTotals(views){
    if(views > 1000000){
        return Math.floor(views / 10000) + 'M'
    }
    if(views > 1000){
        return Math.floor(views / 1000) + 'K'
    }
    return views
}

Apply the new compute method to your template.

<script type="text/template" id="template-item">
    <div class="item panel {{if:item.type:==:photo:photo-class:video-class}}">
        <div class="image" style="background-image: url({{item.preview}})"></div>
        <div class="details">
            <div class="user">
                <div class="thumb" style="background-image: url({{item.user_img}})"></div>
                <div>{{item.user}}</div>
            </div>
            <div class="views"><span class="fa fa-eye"></span> {{compute:formatTotals(item.views)}}</div>
            <div class="likes"><span class="fa fa-heart"></span> {{compute:formatTotals(item.likes)}}</div>
        </div>
    </div>
</script>

Regular Expression

In this function we need to use a regular expression to find {@value@}.

// Regex Explanation:
// compute:function_name(arguments) -> we create 2 group matches,
// one for the fn name an another for the arguments.
// ([\w.]*) Finds any alphanumeric character until a (
// \((.*)\) Finds everything inside parenthesis
var regex = /compute:([\w.]*)\((.*)\)/;  

It's time to STOP HERE if you want to try for yourself.

The code solution

Again, thank you for your effort if you tried for yourself! Let's see how we could do it.

The getValueFromCompute() function

Functionality

  • Uses regex to find the parts of the compute definition.
  • We split the arguments separated by comma.
  • Iterate over the arguments to get the value of each one. If it is a string, get the argument itself. If it's a variable, we get the value using our previous method getValueFromObject.
  • Getting the method name it checks if window global object has the method to invoke.
  • If not, try to find if it's a method of the object received or some method of any object on the global scope.
function getValueFromCompute(str_compute, object) {

    // compute:function_name(arguments) -> we create 2 group matches,
    // one for the fn name an another for the arguments.
    // ([\w.]*) Finds any alphanumeric character until a (
    // \((.*)\) Finds everything inside parenthesis
    var regex = /compute:([\w.]*)\((.*)\)/;     

    // We split into two parts. part[1] will contain the method name and part(2) the arguments
    var parts = regex.exec(str_compute);
    var fn = parts[1];

    // Analize the parameters passed to the function
    var values = parts[2].split(',').map(function(parameter, index){

        // cleaning white spaces
        parameter = parameter.trim();

        // Returning the value. If string, removing the `'`, if object, get the property of the object
        return parameter.indexOf('\'') > -1 ? parameter.replace(/\'/g, '') : getValueFromObject(parameter, object);
    });

    if(typeof window[fn] === "function"){
        return window[fn](values);
    }else {
        if(fn.indexOf('.')>-1){

            // we break function into properties based on dot notations. object.props0.props1
            var props = fn.split('.');

            // We verify if object.firstField exists and object.firstField.secondField if it is a function
            // If not we check if window.firstField exists and window.firstField.secondField if it is a function
            if (object[props[0]] && typeof object[props[0]][props[1]] === "function") {
                return object[props[0]][props[1]].apply(object[props[0]], values);
            } else if(window[props[0]] && typeof window[props[0]][props[1]] === "function") {
                return window[props[0]][props[1]].apply(window[props[0]], values);
            } else {
                // We create an advice that we are using a not existing function on this template
                console.log("Error: " + str_compute + " is not a function");
            }
        }
        console.log("Error: " + str_compute + " is not a function");
    }
}

And the result.

ViewsLikes.PNG

Iterations

Now, when we receive the data from the API we're creating a loop to render the results. The idea including iterations in our templates is that we could include this loops on it. Then any object or property of the object we receive that is an array of items, the dynamic-template will render for us.

Let's see how to do it. First we need to define how we will tell to the render system that an HTML has to be iterated.

This is our template to render picture items so far.

<script type="text/template" id="template-items">
    <div class="item panel {{if:item.type:==:photo:photo-class:video-class}}">
        <div class="image" style="background-image: url({{item.preview}})"></div>
        <div class="details">
            <div class="user">
                <div class="thumb" style="background-image: url({{item.user_img}})"></div>
                <div>{{item.user}}</div>
            </div>
            <div class="views"><span class="fa fa-eye"></span> {{compute:formatTotals(item.views)}}</div>
            <div class="likes"><span class="fa fa-heart"></span> {{compute:formatTotals(item.likes)}}</div>
        </div>
    </div>
</script>

And we are using a loop to iterate through our API results to render each item.

// If we received results and there are items
if (pictures && pictures.items){

    // We iterate for the entire items
    list.innerHTML = pictures.items.reduce(function(item) {
        // Appends the new item to the list
        return list.innerHTML + dt.render('template-item', { item : item });
    }, '');
}

If we add new iteration capabilities our javascript to render the results will end like this.

// If we received results and there are items
if (pictures && pictures.items){
    list.innerHTML = dt.render('template-items', { items : pictures.items });
}

That's great, isn't it? Let's see what we need to add to our HTML template to tell to the template system how to iterate on certain HTMLElements.

<script type="text/template" id="template-items">
    <div class="item panel {{if:item.type:==:photo:photo-class:video-class}} dt-iterate" dt-data="item in items">
        <div class="image" style="background-image: url({{item.preview}})"></div>
        <div class="details">
            <div class="user">
                <div class="thumb" style="background-image: url({{item.user_img}})"></div>
                <div>{{item.user}}</div>
            </div>
            <div class="views"><span class="fa fa-eye"></span> {{compute:formatTotals(item.views)}}</div>
            <div class="likes"><span class="fa fa-heart"></span> {{compute:formatTotals(item.likes)}}</div>
        </div>
    </div>
</script>

We'll only need to add a class dt-iterate to an element to say to the template engine that it has to be iterated. Then we add an attribute to set what data will be used to render that iteration. dt-data="item in items". That's it!

Here we are using the iteration for the entire template, but the solution works for iterations inside templates, let's say, something like this, where the iteration is in the third DIV Element of the template.

<script type="text/template" id="template-items">
    <div class="title">Some Amazing Pictures</div>
    <div class="subtitle">The search query: {{items.search_query}}</div>
    <div class="item panel {{if:item.type:==:photo:photo-class:video-class}} dt-iterate" dt-data="item in items">
        <div class="image" style="background-image: url({{item.preview}})"></div>
        <div class="details">
            <div class="user">
                <div class="thumb" style="background-image: url({{item.user_img}})"></div>
                <div>{{item.user}}</div>
            </div>
            <div class="views"><span class="fa fa-eye"></span> {{compute:formatTotals(item.views)}}</div>
            <div class="likes"><span class="fa fa-heart"></span> {{compute:formatTotals(item.likes)}}</div>
        </div>
    </div>
    <div class="footer">Picture found: {{items.count}}</div>
</script>

Before going further we'll add another feature. We'll add a new concept. Template Components. Template components will be a templates that can be called by another template. Imagine you want to show an array of results on your pages based on an array, but later on you want to add new items individually. You create a template for the items. Then when rendering the whole results you'll use a template that will use iteration calling the new component to render these iterations. Let me show you some code to explain me better.

This is what the item template will look like. As you can see, is almost exactly the same one that we are using before diving into iterations.

<script type="text/template" id="template-component-item">
    <div class="item panel {{if:item.type:==:photo:photo-class:video-class}}">
        <div class="image" style="background-image: url({{item.preview}})"></div>
        <div class="details">
            <div class="user">
                <div class="thumb" style="background-image: url({{item.user_img}})"></div>
                <div>{{item.user}}</div>
            </div>
            <div class="views"><span class="fa fa-eye"></span> {{compute:formatTotals(item.views)}}</div>
            <div class="likes"><span class="fa fa-heart"></span> {{compute:formatTotals(item.likes)}}</div>
        </div>
    </div>
</script>

There's only one minor difference. We changed the id from id="template-items" to id="template-component-item". This is because this template will be a component to be used by another templates.

NOTE - the word component is not required at all. Is to make things clear. You can call the name you want to your component templates.

Now, we add a new template that will be used to render the array of items. We'll use a new attribute name dt-component if we want to use a component template for the iteration.

<script type="text/template" id="template-items">
    <div class="dt-iterate" dt-data="item in items" dt-component="template-component-item"></div>
</script>

This template is very simple. It's a <div> element that will be replaced by the iterations of the item using the component defined in the new attribute dt-component="template-item"

Let's recap. We add three new concepts to our templates.

  • Added a class name dt-iterate to indicate an iteration
  • Added an attribute dt-data to set what data will be used for the iteration.
  • Added an optional attribute name dt-component to set an alternative template to use on the iterations.

Finally, before going to TRY IT YOURSELF, let me show you some tricks to make it easier to start on iterations. I'm going to modify the dynamic-template.js.

Our main render method looked like this.

function render(template_selector, object){
    ...
    return applyDynamicValues($template.innerHTML, object);
}

Now, we need to apply iterations and to apply dynamic values. Then we'll add a middle function to make both steps inside this function. Let's call it applyTemplate. Instead of passing the template innerHTML to that function, we will pass the complete DOM Element.

function render(template_selector, object){
    ...
    return applyTemplate($template, object); // We pass the complete DOM element, not just innerHTML
}

And the middle function will call applyIterations, a new function you'll have to create for your own. After that we'll call applyDynamicValues as we did so far.

function applyTemplate($template, object) {
    var str_html = $template.outerHTML.substr(0,30).indexOf('script')>-1 ? $template.innerHTML : $template.outerHTML;
    str_html = applyIterates(str_html, object);
    return applyDynamicValues(str_html, object);
}

NOTE 1 - Why do we apply iterations before applying dynamic values?
If we apply dynamic values first, we'll replace all dynamic values inside the iterations as well. Then at the point we'll go for the iterations the {{dynamic-values}} for the iterations will be replaced.
My approach is to look for iterations first and then going to replace the dynamic values at the rest of the template.

NOTE 2 - We are using the var str_html = $template.outerHTML.substr(0,30).indexOf('script')>-1 ? $template.innerHTML : $template.outerHTML; to get the HTML string of the template. We're checking if the template is embedded in a <script> tag or not. We'll use this function for our iterations to render each item, and then the templates will be directly the HTML Elements not nested in a <script> tag. If we receive a <script> tag, we will get the only innerHTML (the string of the child elements). If there's no <script> tag, we will get outerHTML that will include the whole template on the string.

It's your time now! Do you want to try for yourself?

TRY FOR YOURSELF - Let's see what you need to implement

  1. Create a method named applyIterates() that receives two parameters. A string str_html containing the html content of the template and the object containing the data.
  2. Create a new Div element using the DOM. HINT - createElement('div')
  3. Set the content of this new div to the html string received as an argument.
  4. Now, you have a DOM with your template in it. Now you can find if there's any iteration selecting the class dt-iterate on your DOM. HINT - querySelectorAll()
  5. Now you have to iterate over the iterations found.
  6. For every iteration you have found you need to
  7. Get the HTML Element to iterate
  8. Get the dt-data attribute and find what data uses the iteration. The proposed format is "[item] in [array_items]". HINT - Array.split().
  9. Use the existing function getValueFromObject() to retrieve the values from the data object. HINT - The name you found in [array_items] is the object property where you'll find the array.
  10. The iteration you've found is your template for every iteration. I recommend you to scroll up a bit and check how we create the templates and how where we use dt-iterate class. What we're getting are the HTML element that has to be repeated.
  11. Before going on, you have to check if the template defined another template to use on the iterations. Remember, we set an attribute dt-component to define an alternative template.
  12. If you find the attribute, find the new template in the main HTML using the DOM.
  13. Now you have a template and an array of objects. Now you have to loop over the array items and apply the template to each one. Sounds it familiar? You have a template and an object. We have a function for that! applyTemplate($template, object). Yes! you can call on every item the function we are creating to render the templates. Invoke that function on every item and concatenate the results on a variable.
  14. Finally, apply to the resulting html string to the original template HTML element that contained the dt-iterate.
  15. Return the innerHTML of the resulting HTML element.

It's not an easy task, I wish you good luck on it! At the end of every article you can find my contact details if you stuck in any point.

The code solution

I hope you tried for yourself. If you solved the entire function, congratulations!!! It's harder when you try to follow the way another coder decided to do things.

Let's see my proposition.

The applyIterates() function

Functionality

  • Gets the string containing the html of the template and the object.
  • Creates a virtual DOM to apply the template. see the note after the code.
  • Creates a new div and sets the HTML to it.
  • Find all the HTML elements that has dt-iterate class to find all the iterations in the template. Can be multiple iterations
  • What the code will do is a loop where will get the first element found and remove the class dt-iterate, find the object data values and apply on each item of the array. Then removes the class dt-iterate and find again if there's any HTML with the class. And starts the loop again until no dt-iterate class found. We are using reduce array method to render all items. You can find more information about reduce in the Part #1 of this tutorial. Inside the loop we check if the template uses another template to render every item.
  • Finally returns the result.

Here's the code.

function applyIterates(str_html, object) {
    // Creates dummy DOM to apply work with the template
    var newHTMLDocument = document.implementation.createHTMLDocument('preview');
    var $html = newHTMLDocument.createElement('div');

    //Sets the HTML content to the new dummy div
    $html.innerHTML = str_html;

    // Search for iterations on the HTML code. We define iterates using a class <div class="dt-iterate" ...></div>
    var $iterates = $html.querySelectorAll('.dt-iterate');

    // We iterate through all iterations in the template
    while($iterates.length) {

        var $iterate = $html.querySelectorAll('.dt-iterate')[0];
        // Avoid to repeat the iteration inside iterations removing the attribute
        $iterate.classList.remove('dt-iterate');

        // We get the data object to iterate with
        var iteration_data = $iterate.attributes["dt-data"].value.split(' in ');

        // we set the template to use as the iteration but check if there's a component to use as template
        var $template = $iterate;

        if ($iterate.attributes["dt-component"] && $iterate.attributes["dt-component"].value) {
            var $component = document.getElementById($iterate.attributes["dt-component"].value);
            if (!$component) {
                console.error('Component not found!: ' + $iterate.attributes["dt-component"].value);
                return $html;
            } else {
                $template = $component.cloneNode(true);
            }
        }

        // iterate over the object array
        // We use reduce to write every iteration to $temp_div
        var iterations_html = getValueFromObject(iteration_data[1], object).reduce(function (iterations_html, element) {
            // Creates a temp object that will be used on the iteration
            var item = {};

            // set the name to this object based in the expression 'item in items'. -> item.item = element
            item[iteration_data[0]] = element;

            // concatenates the new html created in this iteration into the one obtained in previous iterations
            iterations_html += applyTemplate($template, item);

            // Returns the HTML obtained in the iterations so far
            return iterations_html;
        }, '');

        // Sets the result HTML string to the original template object when we found the .dt-iterate class
        $iterate.parentNode.innerHTML = iterations_html;

        // We check if there're some more iteration to apply
        $iterates = $html.querySelectorAll('.dt-iterate');
    }

    // Returns the full HTML when all iterations finished
    return $html.innerHTML;
}

At this point, if you completed all this content, congratulations! You have coded a full template system from scratch. It seems a lot of time, but if you start to apply to your projects you'll see how fast you can create things from scratch just importing your dynamic-template.js file to your project!

undraw_festivities_tvvj.png

Thank you for your effort!

I'm David. If you get stuck in some point, please get in touch with me at Twitter, by email at kriyeng on a gmail account or leave a comment in this post. I'm open to do pair sessions if you want help to follow the tutorial or anything you're stuck.


Tags

Follow us

Latest blogs
My Notes About Intro to Tensorflow Course
Sat Jun 22 2019 19:43:02 GMT+0200 (Hora de verano romance)
How to train YOLOv3 using Darknet on Colab notebook and speed up load times
Wed Apr 10 2019 14:57:55 GMT+0200 (Hora de verano romance)
Creating a banking chat bot hosted at IBM Cloud connected to IBM Watson using No
Fri Mar 15 2019 15:24:18 GMT+0100 (Hora estándar romance)
Getting back to #100DaysOfCode
Wed Mar 06 2019 22:30:42 GMT+0100 (Hora estándar romance)
50 days of the 100DaysOfCode challenge
Sun Feb 17 2019 16:47:31 GMT+0100 (Hora estándar romance)

Latest comments