We use tons of tools every day. Different libraries and frameworks are a part of our daily job. We use them because we don’t want to reinvent the wheel for every project, even if we don’t understand what’s going on under the hood. In this article, we will reveal some of the magical processes happening in the most popular libraries. We’ll also see if we can replicate their behaviour.
With the rise of single page applications, we are doing a lot of things with JavaScript. A big part of our application’s logic has been moved to the browser. It is a common task to generate or replace elements on the page. Code similar to what is shown below has become very common.
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
The result is a new
<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
We defined our own utility method stringToDom that creates a temporary
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
Visually, on the page, there are no differences. However, if we check the generated markup with Chrome’s developer tools we will get an interesting result:
It looks like our stringToDom function created just a text node and not the actual
jQuery successfully solves the problem by creating the right context and extracts only the needed part. If we dig a bit into the code of the library we will see a map like this one:
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
Every element that requires special treatment has an array assigned. The idea is to construct the right DOM element and to depend on the level of nesting to fetch what we need. For example, for the
Having a map, we have to find out what kind of tag we want in the end. The following code extracts the tr from
<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
The rest is finding the proper context and returning the DOM element. Here is the final variant of the function stringToDom:
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
Notice that we are checking if there is a tag in the string – match != null. If not we simply return a text node. There is still usage of a temporary
Here is a CodePen showing our implementation:
See the Pen xlCgn by Krasimir Tsonev (@krasimir) on CodePen.
Let’s continue by exploring the wonderful AngularJS dependency injection.
When we start using AngularJS it impresses with its two-way data binding. The second thing which we notice is its magical dependency injection. Here is a simple example:
<span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span><span>}; </span>wrapMap<span>.optgroup = wrapMap.option; </span>wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span>wrapMap<span>.th = wrapMap.td;</span>
That’s a typical AngularJS controller. It performs an HTTP request, fetches data from a JSON file, and passes it to the current scope. We don’t execute the TodoCtrl function – we don’t have a chance to pass any arguments. The framework does. So, where do these $scope and $http variables came from? It’s a super cool feature, that highly resembles black magic. Let’s see how it is done.
We have a JavaScript function that displays the users in our system. The same function needs access to a DOM element to put the generated HTML, and an Ajax wrapper to get the data. In order to simplify the example, we will mock-up the data and the HTTP requesting.
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
We will use the
tag as a content holder. ajaxWrapper is the object simulating the request and dataMockup is an array containing our users. Here is the function that we will use:<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
And of course, if we run displayUsers(body, ajaxWrapper) we will see the three names displayed on the page and /api/users requested in our console. We could say that our method has two dependencies – body and ajaxWrapper. So, now the idea is to make the function working without passing arguments, i.e. we have to get the same result by calling just displayUsers(). If we do that with the code so far the result will be:
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
And that’s normal because ajax parameter is not defined.
Most of the frameworks that provide mechanisms for dependency injecting have a module, usually named injector. To use a dependency we need to register it there. Later, at some point, our resource is provided to the application’s logic by the same module.
Let’s create our injector:
<span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span><span>}; </span>wrapMap<span>.optgroup = wrapMap.option; </span>wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span>wrapMap<span>.th = wrapMap.td;</span>
We need only two methods. The first one, register, accepts our resources (dependencies) and stores them internally. The second one accepts the target of our injection – the function that has dependencies and needs to receive them as parameters. The key moment here is that the injector should not call our function. That’s our job and we should be able to control that. What we can do in the resolve method is to return a closure that wraps the target and invokes it. For example:
<span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span><span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, '');</span>
Using that approach we will have the chance to call the function with the needed dependencies. And at the same time we are not changing the workflow of the application. The injector is still something independent and doesn’t hold logic related functionalities.
Of course, passing the displayUsers function to the resolve method doesn’t help.
<span>var stringToDom = function(str) { </span> <span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span> <span>}; </span> wrapMap<span>.optgroup = wrapMap.option; </span> wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span> wrapMap<span>.th = wrapMap.td; </span> <span>var element = document.createElement('div'); </span> <span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span> <span>if(match != null) { </span> <span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, ''); </span> <span>var map = wrapMap[tag] || wrapMap._default, element; </span> str <span>= map[1] + str + map[2]; </span> element<span>.innerHTML = str; </span> <span>// Descend through wrappers to the right content </span> <span>var j = map[0]+1; </span> <span>while(j--) { </span> element <span>= element.lastChild; </span> <span>} </span> <span>} else { </span> <span>// if only text is passed </span> element<span>.innerHTML = str; </span> element <span>= element.lastChild; </span> <span>} </span> <span>return element; </span><span>}</span>
We still get the same error. The next step is to find out what the passed target needs. What are its dependencies? And here is the tricky part that we can adopt from AngularJS. I, again, dug a bit into the code of the framework and found this:
<span>function <span>TodoCtrl</span>($scope<span>, $http</span>) { </span> $http<span>.get('users/users.json').success(function(data) { </span> $scope<span>.users = data; </span> <span>}); </span><span>}</span>
We purposely skipped some parts, because they are more like implementation details. That’s the code which is interesting for us. The annotate function is something like our resolve method. It converts the passed target function to a string, removes the comments (if any), and extracts the arguments. Let’s use that and see the results:
<span>var dataMockup = ['John', 'Steve', 'David']; </span><span>var body = document.querySelector('body'); </span><span>var ajaxWrapper = { </span> <span>get: function(path<span>, cb</span>) { </span> <span>console.log(path + ' requested'); </span> <span>cb(dataMockup); </span> <span>} </span><span>}</span>
Here is the output in the console:
If we get the second element of argDecl array we will find the names of the needed dependencies. That’s exactly what we need, because having the names we will be able to deliver the resources from the storage of the injector. Here is the version that works and successfully covers our goals:
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
Notice that we are using .split(/, ?/g) to convert the string domEl, ajax to an array. After that we are checking if the dependencies are registered and if yes we are passing them to the target function. The code outside the injector looks like that:
<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
The benefit of such an implementation is that we can inject the DOM element and the Ajax wrapper in many functions. We could even distribute the configuration of our application like that. There is no need to pass objects from class to class. It’s just the register and resolve methods.
Of course our injector is not perfect. There is still some room for improvements, like for example the supporting of scope definition. The target function right now is invoked with a newly created scope, but normally we will want to pass our own. We should support also sending custom arguments along with the dependencies.
The injector becomes even more complicated if we want to keep our code working after minification. As we know the minifiers replace the names of the functions, variables and even the arguments of the methods. And because our logic relies on these names we need to think about workaround. One possible solution is again coming from AngularJS:
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
Instead of only the displayUsers we are passing the actual dependencies’ names.
Our example in action:
See the Pen bxdar by Krasimir Tsonev (@krasimir) on CodePen.
Ember is one of the most popular frameworks nowadays. It has tons of useful features. There is one which is particularly interesting – computed properties. In summary, computed properties are functions that act as properties. Let’s see a simple example taken from the Ember’s documentation:
<span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span><span>}; </span>wrapMap<span>.optgroup = wrapMap.option; </span>wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span>wrapMap<span>.th = wrapMap.td;</span>
There is a class that has firstName and lastName properties. The computed property fullName returns the concatenated string containing the full name of the person. The strange thing is the part where we use .property method against the function applied to fullName. I personally didn’t see that anywhere else. And, again, quick look of the framework’s code reveals the magic:
<span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span><span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, '');</span>
The library tweaks the prototype of the global Function object by adding a new property. It’s a nice approach to run some logic during the definition of a class.
Ember uses getters and setters to operate with the data of the object. That simplifies the implementation of the computed properties because we have one more layer before to reach the actual variables. However, it will be even more interesting if we are able to use computed properties with the plain JavaScript objects. Like for example:
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
name is used as a regular property but in practice is a function that gets or sets firstName and lastName.
There is a build-in feature of JavaScript that could help us realize the idea. Have a look at the following snippet:
<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
The Object.defineProperty method could accept a scope, name of a property, getter, and setter. All we have to do is to write the body of the two methods. And that’s it. We will be able to run the code above and we will get the expected results:
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
Object.defineProperty is exactly what we need, but we don’t want to force the developer to write it every time. We may need to provide a polyfill, run additional logic, or something like that. In the ideal case we want to provide an interface similar to Ember’s. Only one function is part of the class definition. In this section, we will write a utility function called Computize that will process our object and somehow will convert the name function to a property with the same name.
<span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span><span>}; </span>wrapMap<span>.optgroup = wrapMap.option; </span>wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span>wrapMap<span>.th = wrapMap.td;</span>
We want to use the name method as a setter, and at the same time as a getter. This is similar to Ember’s computed properties.
Now let’s add our own logic into the prototype of the Function object:
<span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span><span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, '');</span>
Once we add the above lines, we will be able to add .computed() to the end of every function definition:
<span>var stringToDom = function(str) { </span> <span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span> <span>}; </span> wrapMap<span>.optgroup = wrapMap.option; </span> wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span> wrapMap<span>.th = wrapMap.td; </span> <span>var element = document.createElement('div'); </span> <span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span> <span>if(match != null) { </span> <span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, ''); </span> <span>var map = wrapMap[tag] || wrapMap._default, element; </span> str <span>= map[1] + str + map[2]; </span> element<span>.innerHTML = str; </span> <span>// Descend through wrappers to the right content </span> <span>var j = map[0]+1; </span> <span>while(j--) { </span> element <span>= element.lastChild; </span> <span>} </span> <span>} else { </span> <span>// if only text is passed </span> element<span>.innerHTML = str; </span> element <span>= element.lastChild; </span> <span>} </span> <span>return element; </span><span>}</span>
As a result, the name property doesn’t contain function anymore, but an object that has computed property equal to true and func property filled with the old function. The real magic happens in the implementation of the Computize helper. It goes through all the properties of the object and uses Object.defineProperty where we have computed properties:
<span>function <span>TodoCtrl</span>($scope<span>, $http</span>) { </span> $http<span>.get('users/users.json').success(function(data) { </span> $scope<span>.users = data; </span> <span>}); </span><span>}</span>
Notice that we are deleting the original property name. In some browser Object.defineProperty works only on properties that are not defined yet.
Here is the final version of the User object that uses .computed() function.
<span>var dataMockup = ['John', 'Steve', 'David']; </span><span>var body = document.querySelector('body'); </span><span>var ajaxWrapper = { </span> <span>get: function(path<span>, cb</span>) { </span> <span>console.log(path + ' requested'); </span> <span>cb(dataMockup); </span> <span>} </span><span>}</span>
A function that returns the full name is used for changing firstName and lastName. That is the idea behind the checking of passed arguments and processing the first one. If it exists we split it and apply the values to the normal properties.
We already mentioned the desired usage, but let’s see it one more time:
<span>var displayUsers = function(domEl<span>, ajax</span>) { </span> ajax<span>.get('/api/users', function(users) { </span> <span>var html = ''; </span> <span>for(var i=0; i < users.length; i++) { </span> html <span>+= '<p>' + users[i] + '</p>'; </span> <span>} </span> domEl<span>.innerHTML = html; </span> <span>}); </span><span>}</span>
The following CodePen shows our work in practice:
See the Pen ahpqo by Krasimir Tsonev (@krasimir) on CodePen.
You’ve probably heard about Facebook’s framework React. It’s built around the idea that everything is a component. What is interesting is the definition of component. Let’s take a look at the following example:
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
The first thing that we start thinking about is that this is a JavaScript, but it is an invalid one. There is a render function, and it will probably throw an error. However, the trick is that this code is put in <script> tag with custom type attribute. The browser doesn’t process it which means that we are safe from errors. React has its own parser that translates the code written by us to a valid JavaScript. The developers at Facebook called the XML like language <em>JSX. Their JSX transformer is 390K and contains roughly 12000 lines of code. So, it is a bit complex. In this section, we will create something way simple, but still quite powerful. A JavaScript class that parses HTML templates in the style of React.</script>
The approach that Facebook took is to mix JavaScript code with HTML markup. So, lets say that we have the following template:
<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
And a component that uses it:
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
The idea is that we point out the id of the template and define the data that should be applied. The last piece of our implementation is the actual engine that merges the two elements. Let’s call it Engine and start it like that:
<span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span><span>}; </span>wrapMap<span>.optgroup = wrapMap.option; </span>wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span>wrapMap<span>.th = wrapMap.td;</span>
We are getting the content of the
Now, let’s write our parse function. Our first task is to distinguish the HTML from the expressions. By expressions, we mean strings put between . We will use a RegEx to find them and a simple while loop to go through all the matches:
<span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span><span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, '');</span>
The result of the above code is as follows:
<span>var stringToDom = function(str) { </span> <span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span> <span>}; </span> wrapMap<span>.optgroup = wrapMap.option; </span> wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span> wrapMap<span>.th = wrapMap.td; </span> <span>var element = document.createElement('div'); </span> <span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span> <span>if(match != null) { </span> <span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, ''); </span> <span>var map = wrapMap[tag] || wrapMap._default, element; </span> str <span>= map[1] + str + map[2]; </span> element<span>.innerHTML = str; </span> <span>// Descend through wrappers to the right content </span> <span>var j = map[0]+1; </span> <span>while(j--) { </span> element <span>= element.lastChild; </span> <span>} </span> <span>} else { </span> <span>// if only text is passed </span> element<span>.innerHTML = str; </span> element <span>= element.lastChild; </span> <span>} </span> <span>return element; </span><span>}</span>
There is only one expression and its content is title. The first intuitive approach that we can take is to use the JavaScript’s replace function and replace with the data from the passed comp object. However, this will work only with the simple properties. What if we have nested objects or even if we want to use a function. Like for example:
<span>function <span>TodoCtrl</span>($scope<span>, $http</span>) { </span> $http<span>.get('users/users.json').success(function(data) { </span> $scope<span>.users = data; </span> <span>}); </span><span>}</span>
Instead of creating a complex parser and almost invent a new language we may use pure JavaScript. The only one thing that we have to do is to use the new Function syntax.
<span>var dataMockup = ['John', 'Steve', 'David']; </span><span>var body = document.querySelector('body'); </span><span>var ajaxWrapper = { </span> <span>get: function(path<span>, cb</span>) { </span> <span>console.log(path + ' requested'); </span> <span>cb(dataMockup); </span> <span>} </span><span>}</span>
We are able to construct the body of a function that is later executed. So, we know the position of our expressions and what exactly stands behind them. If we use a temporary array and a cursor our while cycle will look like that:
<span>var text = $('<div>Simple text</div>'); </span> <span>$('body').append(text);</span>
The output in the console shows that we are on the right track:
<span>var stringToDom = function(str) { </span> <span>var temp = document.createElement('div'); </span> temp<span>.innerHTML = str; </span> <span>return temp.childNodes[0]; </span><span>} </span><span>var text = stringToDom('<div>Simple text</div>'); </span> <span>document.querySelector('body').appendChild(text);</span>
The code array should be transformed to a string that will be a body of a function. For example:
<span>var tableRow = $('<tr><td>Simple text</td></tr>'); </span><span>$('body').append(tableRow); </span> <span>var tableRow = stringToDom('<tr><td>Simple text</td></tr>'); </span><span>document.querySelector('body').appendChild(tableRow);</span>
It’s quite easy to achieve this result. We may write a loop that goes through all the elements of code array and checks if the item is a string or object. However, this again covers only part of the cases. What if we have the following template:
<span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span><span>}; </span>wrapMap<span>.optgroup = wrapMap.option; </span>wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span>wrapMap<span>.th = wrapMap.td;</span>
We can’t just concatenate the expressions and expect to have the colors listed. So, instead of appending a string to string we will collect them in an array. Here is the updated version of the parse function:
<span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span><span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, '');</span>
Once the code array is filled we start constructing the body of the function. Every line of the template will be stored in an array r. If the line is a string, we are clearing it a bit by escaping the quotes and removing the new lines and tabs. It’s added to the array via the push method. If we have a code snippet then we check if it is not a valid JavaScript operator. If yes then we are not adding it to the array but simply dropping it as a new line. The console.log at the end outputs:
<span>var stringToDom = function(str) { </span> <span>var wrapMap = { </span> <span>option: [1, '<select multiple="multiple">', '</select>'], </span> <span>legend: [1, '<fieldset>', '</fieldset>'], </span> <span>area: [1, '<map>', '</map>'], </span> <span>param: [1, '<object>', '</object>'], </span> <span>thead: [1, '<table>', '</table>'], </span> <span>tr: [2, '<table><tbody>', '</tbody></table>'], </span> <span>col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], </span> <span>td: [3, '<table><tbody><tr>', '</tr></tbody></table>'], </span> <span>_default: [1, '<div>', '</div>'] </span> <span>}; </span> wrapMap<span>.optgroup = wrapMap.option; </span> wrapMap<span>.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; </span> wrapMap<span>.th = wrapMap.td; </span> <span>var element = document.createElement('div'); </span> <span>var match = <span>/<<span>\s*\w.*?></span>/g</span>.exec(str); </span> <span>if(match != null) { </span> <span>var tag = match[0].replace(<span>/</g</span>, '').replace(<span>/>/g</span>, ''); </span> <span>var map = wrapMap[tag] || wrapMap._default, element; </span> str <span>= map[1] + str + map[2]; </span> element<span>.innerHTML = str; </span> <span>// Descend through wrappers to the right content </span> <span>var j = map[0]+1; </span> <span>while(j--) { </span> element <span>= element.lastChild; </span> <span>} </span> <span>} else { </span> <span>// if only text is passed </span> element<span>.innerHTML = str; </span> element <span>= element.lastChild; </span> <span>} </span> <span>return element; </span><span>}</span>
Nice, isn’t it? Properly formatted working JavaScript, which executed in the context of our Component will produce the desired HTML markup.
The last thing that is left is the actual running of our virtually created function:
<span>function <span>TodoCtrl</span>($scope<span>, $http</span>) { </span> $http<span>.get('users/users.json').success(function(data) { </span> $scope<span>.users = data; </span> <span>}); </span><span>}</span>
We wrapped our code in a with statement in order to run it in the context of the component. Without that we need use this.title and this.colors instead of title and colors.
Here is a CodePen demonstrating the final result:
See the Pen gAhEj by Krasimir Tsonev (@krasimir) on CodePen.
Behind the big frameworks and libraries are smart developers. They found and use tricky solutions that are not trivial, and even kinda magical. In this article, we revealed some of that magic. It’s nice that in the JavaScript world we are able to learn from the best and use their code.
The code from this article is available for download from GitHub
Magic methods in JavaScript are special methods that provide hooks into a class’s behavior. They are not called directly but are invoked when certain actions are performed. For instance, the toString() method is a magic method that is automatically called when an object needs to be represented as a text value. Another example is the valueOf() method, which is called when an object is to be represented as a primitive value.
Magic methods in JavaScript can be used by defining them in your object or class. For example, you can define a toString() method in your object to customize how your object will be represented as a string. Here’s a simple example:
let person = {
firstName: "John",
lastName: "Doe",
toString: function() {
return this.firstName " " this.lastName;
}
};
console.log(person.toString()); // "John Doe"
Magic functions in JavaScript are important because they allow you to control and customize how your objects behave in certain situations. They can make your code more intuitive and easier to understand, as well as provide a way to encapsulate and protect your data.
Sure, here are some examples of magic functions in JavaScript:
While magic methods can be very useful, they also have some limitations. For one, they can make your code more complex and harder to debug, especially if you’re not familiar with how they work. They can also lead to unexpected behavior if not used correctly.
Unlike some other programming languages, JavaScript does not have a formal concept of “magic methods”. However, it does have certain methods that behave similarly, such as toString() and valueOf(). These methods are automatically called in certain situations, much like magic methods in other languages.
Some best practices for using magic methods in JavaScript include understanding when and why to use them, using them sparingly to avoid complexity, and always testing your code thoroughly to ensure it behaves as expected.
Yes, magic methods can be used with JavaScript frameworks like React or Vue. However, the way they are used may vary depending on the framework. It’s always best to refer to the specific framework’s documentation for guidance.
There are many resources available to learn more about magic methods in JavaScript. You can start with the official JavaScript documentation, as well as online tutorials and courses. You can also practice using them in your own projects to gain hands-on experience.
There are many libraries and tools that can help with using magic methods in JavaScript. For example, Lodash is a popular JavaScript utility library that provides helpful methods for working with arrays, objects, and other types of data.
The above is the detailed content of Revealing the Magic of JavaScript. For more information, please follow other related articles on the PHP Chinese website!