What do you want to learn?
Skip to main content
HTML5 Web Components: Moving from jQuery to Polymer.js
by Craig Shoemaker
As your applications grow the need to break parts of the page up, maintainable components become more and more important. This course demonstrates how to take existing jQuery code and refactor it into stand-alone Web Components using Polymer.js.
Resume CourseBookmarkAdd to Channel
Table of contents
Well hello, this Craig Shoemaker, and welcome to HTML5 Web Components: Moving from jQuery to Polymer.js. We know the web has changed in so many ways in recent years, but not all of our existing projects have followed suit. In this course you learn to upgrade an existing jQuery site to introduce web components. And fear not, the approach used here doesn't assume that you'll do the whole site at once. In fact, I'll show you how you can take just a single feature and make it a full-fledged web component. Right at the beginning of the course you'll learn how to think of your website in terms of components, and you'll get to experience the transformation of everyday jQuery code in to a Polymer.js component. As with any transformation the change is a journey, and during these lessons you'll follow the process of removing jQuery, implementing the feature as a native web component, and finally, refactoring the code to use Polymer.js. By the end of course you'll have encapsulated all the logic for the navigation feature into a web component. Plus, you'll learn to use the Shadow DOM, take advantage of style isolation, build custom templates, and harness the power of Polymer's templating engine. Now this is a hands-on and very practical course where you can take the principles presented here and use them immediately in your projects. I truly look forward to our time together, and I'll see you inside HTML5 Web Components: Moving from jQuery to Polymer.js.
Hello, and welcome to Pluralsight's HTML5 Web Components: Moving from jQuery to Polymer.js. This is Craig Shoemaker, and during our time together I'm going to take you through a journey of building an interactive feature for a website, beginning in jQuery, and take you step-by-step through the thought process and syntax changes needed to transform this feature into a fully-fledged Web Component written in Polymer.js. Now the code and the demos in this course will flow through four different iterations. The first is the basis found within jQuery. Here we'll implement the feature and this will look a lot like what you're used to seeing, probably in your everyday code standard jQuery development. From there we'll move to remove jQuery and use the Native Selection API. Browser support for the Native Selection API is so widespread and robust, here we can safely remove jQuery in preparation for a native web component. Then we as we implement the component, we'll introduce the Shadow DOM. I'll show you how to use HTML Imports, HTML Templates, and create a custom element. And with that foundation set we'll be able to upgrade that component into a Polymer web component and take advantage of data binding and the full Polymer API. So the way these demos are set up are to look identical among one another. I don't want any of the functionality to change, I simply want the implementation to go from jQuery into Polymer. You'll notice for each one of these I have a little implementation flag that shows up at the top. So here you can see we're running it as jQuery, this one is using the Nave Selection API, here this is implemented as a Web Component, and then of course, the very last one shows that it's running as a Polymer component. The way this works is we'll go through and select an item, the navigation will slide in, and I can drill down in here and select an individual item. As I open the developer tools and click on a particular item within the navigation, you'll see that I have bubbled up into the console the MouseEvent information, as well as custom arguments showing the navPath, the fileName, and the title of the time that I clicked on. Now I can go back, and back up, and down, and do the same. So this interaction and the result of the click is exactly the same for each one of the implementations. And this is what you'll be building when you take the journey from jQuery to Polymer here in this course.
Journey from jQuery to Polymer.js
Understanding the journey from jQuery to Polymer is important because I'm betting that you're here because you have a ton of old jQuery code that you're looking to better organize and modularize. So to begin, you'll see how we make small changes to your code to remove jQuery calls that are no longer necessary because of the availability of a native HTML5 selection engine. The selection engine is so robust that in many cases you don't really ever need jQuery at all. So, once we've removed jQuery, we'll move onto the basics of Web Components. Now, the best understand Polymer you really need to have at least an overview understanding of how it's helping you. So, by working directly with the Web Components API you get to see how the components are made in all their glory. And finally, we do a few more refactorings in order to take the native Web Components and make a full-fledged Polymer component. But before we dive into the details let's review the anatomy of an HTML5 Web Component.
Anatomy of a Web Component
Okay, to wrap up this module you learned about the path from jQuery to Polymer, the benefits of Web Components, and the benefits of Polymer.js. Now we got your environment completely set up, so next we'll talk about the jQuery implementation and where to go from there.
Building the Slider Navigation Feature
In this module we'll build the basis of the slider navigation upon which the rest of the modules in this course is based. In order to show the journey from jQuery to Polymer we first need to create the feature in jQuery, so let's get started.
HTML and Data
Now as we dive into the jQuery version of the slidernav feature I'd like to start you off by going over the basic structure of the HTML. Now this has all the aspects of an HTML page that you would come to expect, so at the top we have the DOCTYPE. Inside that we have the head. Now the items that are most relevant here are the fact that I'm bringing in a couple style sheets, the first one is bootstrap. This just makes styling the page so much easier. I have some global styles, and then some styles are specific the slidernav feature itself. Now these styles, the styles within a slidernav are what we'll change as we change the implementation. In fact, what you'll see is that the selectors get simpler and simpler as we move from jQuery into components. So here within the body there's a div that acts as the basis for the entire feature. So this has the data-role of slidernav=container. This is the container that holds all the elements that will implement the slidernav. Inside that there's the slidernav-root, and then I have a title for Courses. Now, as we move into components this markup will get simpler and simpler, and in fact, the title for Courses will become an attribute of the component itself, rather than its own element here. Now if you recall we have the implementation flag, so here we're starting off by saying that it's for jQuery, and then I have a few script files. The first is for courses.js. We'll take a look at that in a moment, but that's the data that's being fed into the feature. Now under normal circumstances you would probably get this data through an ajax call. Here I'm just making it available in the page itself to make it easy to talk about the component. Then of course we're using jQuery, so that's referenced here on the page. And then we have the script itself for the slidernav feature. We'll be looking at that in detail within the next few clips. And then finally we have the script that initializes the feature itself. So here I have the jQuery ready function, then I can access the sliderNav module that's implemented within slidernav JS, call the init function, pass in the data that's off of the window object, and then the callback here, this is what's called when you click on an individual item. So it's returned up to is the MouseEvent args, as well as the custom args. So I can print out the information about which item you've clicked on the screen. So that's the basis for the HTML. Very quickly I'll show you the data, and then in the next clip we'll go over the style sheets. So here's the data for this application, and the best way to get access to this data is just to clone the repository, as I showed you in the introductory portion of this course. But I'm showing you this so that you can see that the array of data is set aside under window.navData. Again, normally, what you would do is have some sort of ajax call that would return an array of data and you would pass that into your feature, but here, just to keep things simple, I'm setting it aside as a variable off a window. Okay, now let's look at the style sheets.
jQuery Module Structure
Implementing the Module: init
The initialization function works to set up the component. So the first thing that happens is we'll take a look at the div that has the data-role of slidernav-container. And if you remember from the HTML, that was the root element, and inside of that is a div that has the role of slidernav-root, but here we'll take the container itself and set reference to it equal to this variable $navContainer. Now the data that comes in we'll set that aside under navData, and the callback that gets run eventually once an individual item is clicked, is set aside here into individualItemCallback. Now the root layer is bound by selecting the root of the component itself. So once we locate that div then we can tell it to bind the layer, and then we want to be able to respond to any user interaction that happens. So whether they close a layer or they click on a parent item of child item, these click handlers are set up in order to handle all of that interaction. So to close a layer we're looking for elements that have the data-role of close-layer, and this is generated during the bind layer operation. Just as items are created they're either parents or children. So here is we have parent=false itemClick is bound to that element, or if parent=true then parentClick is bound to that element. Let's take a look at this working in the browser. So here the component is being initialized, and what's being passed in is an array of data. So when we start talking about paths, a path of 1, 2, 0, will take up all the way down to this item right here that has the children of HTML5 Advanced Topics. So this is the full array of data that comes into the component. The callback that gets passed into the init function is the callback that's found in the HTML. Well currently all we're doing is console.log for the item info and the MouseEvent args. So here the nav container is selected, the data that comes into the initialization function is set aside under navData, and the callback is set aside here, we'll bind the layer, and then set up the click events for each one of those individual items. Okay, so now let's look at the inside of the bindLayer function.
Implementing the Module: bindLayer
Now as you just saw, binding to a layer requires you to have a layer and a path inside the array of data that you're going to bind to. So the first thing that we want to do is take a look at the path. If it's undefined then we know we're working with a fresh version of the component, nothing has been selected, no user interaction has taken place yet. So the items are made up of the data itself, the root level of the data, and the path is an empty array because nothing's been selected. However, if a comma-delimited string has been passed into this function, we can split that string creating a path to pass into the getItems function. Now getItems is responsible for getting the specific items for this layer based off of the path. So before we go any farther let's take a look at this working in the browser. So here navPath his undefined, because this is the first time we're interacting with the component, and so nothing is passed into bindLayer. The items are the root of the data itself, and the path is initialized as an empty array. Let's let this run for a moment, and then I'll open it back up, and then we'll select on something here so we have a path. So, let's click on AngularJS, we'll come back to parent item and add layer, but I want to return here to bindLayer, and now you'll notice that path is 0. Now this is 0 because this is the first item in the array that's bound to this layer, so that would be at index 0. Again, if I let this run again. So if I click on this one, and Advanced, now bindLayer is coming through with a path of 0, 2. Again, because that was the first element in the first layer, and the third element in the second layer. Now we'll come back to getItems in just one moment, but for right now, let's deal with an array of items that are pulled out of the data itself. For each one of the data items what we want to do is create a new layer for each one. So we'll iterate through each item, and as we're looping through that array, we're keeping track of the index. By having a new index value and concatenating it to the current path, essentially what we're doing is creating a new path that is being generated so when you click on that item it knows what its own path is. We'll initialize isParent to false, and then take a look at the item that's under this iteration and look to see if it has children, and if the children length is greater than 0. So if it has children well then it's a parent. Once we know this information then we can go ahead and start generating up the HTML for this layer element. It's a div, data-parent is set equal to whatever the Boolean value of isParent is, but we'll serialize that toString. The path of the data here is the new path that we've generated, but again we want to serialize this to a string so we'll take that array and join it with a comma. Now we want the title to show up if the user hovers over this element, so we'll set title equal to item.title. We'll add the class of slidernav-item to give it the style that we want, and then finally, render out the correct text to the user by setting the text equal to item.title. Once all of that is done we can take our new itemElement and append that to the layer and then return the layer up. So let's take a look at this working in the browser, and at the same time we'll take a look at the implementation for getItems. As we return to bindLayer I've clicked on the first item within the first layer and the third item within the second layer, so that gives us a path of 0, 2 once again. So now we can take this path and pass it into the getItems function in order to get the actual data out of data source that matches this path. So here we're passing in the path of 0, 2 because here on this line we've taken the serialized string and split it by a comma in order to make it an array. And then we pass in the root data, so getItems will get the specific data out of the full dataset that we need for this layer. Now the first thing that we want to do here is to clone the path, because in other areas of the code that path will be mutated. As we're generating UI elements we need to append to the path with the current index of the item that we're rendering, so that each one knows its specific path. And we want to protect against that, so here I'm just using a pretty basic approach in order to clone this value. So I'll do JSON.stringify, JSON.parse, so now I have the exact same value, it's just cloned and separated from the original variable. Now that we have the path, we can loop through each item and then drill into the actual data until we reach the level that we need in order to pass it up to the caller. So for each path we'll be working within an individual pathIndex and then also the loopIndex. If a loopIndex is 0 then we know we're working at a root level. So we'll always start from the root of the data and drill down into the children. So if it's a root we're going to find the next level that we need to work with by looking at the parent data, passing in that pathIndex, so we're looking at the first level of the data at this point, and setting that equal to level. So then as this runs, now we're in the next item, you'll see that pathIndex is 2. This is not the root, because the loop index is now 1 instead of 0, and now we can take the level that we found before, look at its children, pass in the index that we have here, and now we've found the level that we've selected from the UI. So ultimately what we can do is take a look at this level. So this is the Advanced item, and you can see that's what I clicked on over here, and what we want to do is grab its children, because once we've clicked on an item in the layer the next thing we want to do is show its children in the subsequent layer. So getItems returns now the items. These are the children that are selected for that advanced AngularJS category, and now we can take those items and create a new layer out of it. So just as you saw in the code editor, here's what we're doing, we're going to call path.concat, so our current path here is 0, 2, looking at the first item in that array, so our new path will now be 0, 2, 0. We'll figure out if this item is a parent, it is not, so we'll add that value into data-parents, we'll take the path, join it together by its comma in order to serialize the path in the UI elements, we'll take the fileName and stick that into data-name, the title is set to title we'll have the nice tooltip, style it with slidernav-item, CSS class, the text itself, which you'll see in the layer is AngularJS In-Depth, so that's the value of title, and we can take all of that and append it, and once we've done that for each layer we can return that up. So there we go. Now we have all the children rendered for the Angular Advanced category.
Implementing the Module: parentClick
Okay, so now we've initialized the component, we've called bindLayer and getItems in order to bind the layer. So the next thing to do is handle a parentClick. So this is clicking on an item, and then adding a layer, and then binding it. So what needs to happen when you click on a layer is the first that we'll do is take a look at the current target, and that's the target that we're working with. Now, we want to find its parent. So we'll take a look at the parent and look for an element that has the class of slidernav-item-selected. We need to remove that, because now we're going to select a new item within the navigation. And so we'll add slidernav-item-selected to the current one that we've just clicked on. Now we'll take the title out of the target, set that aside because we're going to pass it into the addLayer function. The reason we'll do this is because once we bind to the next layer there's this button up here at the top that shows you can go back. Rather than querying the data again for it, we'll just pass that title into the layer so we can render it easily there. Then the only other thing that we need to do is take a look at this element and grab its nav-path out of it. So this is the path that we serialized during bindLayer, and passed that into addLayer so it knows exactly what we're working with. Alright, well let's see it in action in the browser. Okay, here we are starting at the top here, this is the root level of our data, so from the Courses I've clicked on AngularJS. Now the target that's coming in, so e.currentTarget, that's the slidernav-item. So this is the UI element that I've clicked on. So we'll wrap that with a jQuery wrapper, and set that aside into $target. So this makes it easy to use the DOM, so we'll start with this target element, go up to its parents, and look for the element that has the class of slidernav-item-selected, and then remove it, so that we can then take this item here and add the selected class. And we'll get the title out of the element, so here we have AngularJS, and we'll pass that down into addLayer, making sure to bring the value of navPath along with it. Alright, so next let's look at addLayer.
Implementing the Module: addLayer
So while bindLayer handles the children of layer, addLayer is the function that we need to run when we're creating a brand new layer by clicking on a parent. So the first thing that we'll do is increment the module.id, we want to keep track of an ID for each one of the layers as we're going along. So the module itself just keeps track of the current ID at the time. Now we'll create the layerId itself by just appending layer- to the module.id. And so from there we can start building up the container div. So here we create a div, set the id attribute to layerId, and the navigation path to the current navPath that's been passed in here. Now each subsequent layer we need to appear at the top of the stack. So as we're going through each layer it needs to have a higher z-index value than the one before it. So here I'll take module.id, increment 100, and set that equal to the z-index. And then of course add the class of slidernav-layer so it gets the look that I need it to. Now in order to render out the closeButton I need to create a div for that, I'll add the pointer class to it so as you hover over it you get the pointer. Set the data-role to close-layer to help out with some styling, and then we'll set the title and the actual text of the element to parentTitle. Now I also want to render out a little arrow to the left there, so I'll create this span, set that next to parentTitle, and now I have a nice, little closeButton that shows up at the top of the layer. We'll append that to the layer itself, and then pass the layer down into bindLayer for the children to be rendered into the layer. So once the layer is ready, the children are rendered into it, then what we can do is take that layer and append it into navContainer. Now finally I have this little bit of somewhat odd syntax, I'm calling setTimeout, but it's executing at 0 ms. This is done in order to schedule it once everything above it is basically done running. So I want to make sure there's no processing left over after calling append when I add that layer into the container. So by scheduling that here I can then go and locate the layer based off the layerId, and add in the CSS class of slidernav-show. And that uses CSS animations in order to slide that layer in from the left. Okay, great, so now let's take a look at what happens when you click on that back button, and we need to hide a layer.
Implementing the Module: hideLayer
When calling a hideLayer what we want to do is to take that back button that's been rendered within a layer, take a look at it, and then find its parent, which is basically the layer itself, so then we can manipulate it in order to hide the layer. So here by interrogating e.current index we can find the span that we've clicked on, go to its parent, and get the id attribute from there. Now I've got a parentSelector. Using jQuery I can select the layer itself, remove the slidernav-show class, and then get the parent path and pass that into removeLastLayer. Now by removing the nav-path class, the CSS animation will slide that over to the left-hand side out of view, but what I don't want to have happen is for all of these extra layers to build up that we're not looking at. So by calling removeLastLayer we're able to remove it from the DOM. Let's take a look at that next.
Remove Last Layer Demo
Now I've gone into the code for a moment and removed the implementation for removeLastLayer so you can see why this is necessary. So as I click on some layers, I'll come in here and click on Angular, and then Intermediate, you'll see down here that I have these layers that are being added. So I have the path of 0 and then the path of 0, 1. When I go back, notice the slidernav-show class removed, the path of 0 still has slidernav-show, but when I go back, that class is removed as well. So now I have two layers, the one with the path of 0 and the one of path with 0, 1 that aren't being shown on the page, and they have z-indexes of 101 and 102. So if I come in and even click on the same ones, you'll notice that I have the same path here, but I have different z-index layers. And once I hide them, the show class is removed, and this list of layers continues to grow, and grow, and grow. So one of the easiest ways to deal with this problem is just to schedule the removal of the last layer that you've been working with from the DOM, and that's exactly what removeLastLayer does. Alright, now I'll put the logic back in and we'll look and see how it performs with the implementation working. Okay, I commented out the logic and refreshed the page, so now when we go in and click on Angular and Intermediate, you'll notice I have the two layers here, nav-path of 01 with a z-index of 101 and nav-path 0, 1 with a z-index of 102. So when I go back, after a second, you can see that each one of those layers is removed from the DOM. So the logic just schedules the removal of those elements after a second in order to clean up the DOM, but does it in such a way that it gives the animation enough time to complete, so you get a smooth transition of sliding the navigation before the final removal is done.
Implementing the Module: removeLastLayer
RemoveLastLayer is short and sweet, and its only purpose is to clean up the DOM. So you'll notice I have setTimeout set to 1000 ms, and as you can see in the comment, the purpose of this here is to wait enough time for the close animation to complete so we can remove that layer out of the DOM. So in order to do that, we'll go to module, to the navContainer and call find and look for slidernav-layer that has an attribute of data-nav-path and the parent path that we passed in here. So based off the value that's passed in for parentnavPath we find the one that we just closed and then we can call remove completely removing that item from the DOM. Okay, we're almost done. Next up, let's take a look at what happens when we click on individual item.
Implementing the Module: itemClick
For the final function in this module is the itemClick function. And the first thing that we want to do is take a look at the click event args and take the current target and wrap that in a jQuery wrapper. From there we can go to the parent and find all elements that have the class of slidernav-item-selected. Now there'll only be one, but we want to find that and remove the selected class so that now we can apply slidernav-item-selected to the current target. The next thing we want to do is build up an arguments object that has all the information that we'll need based off of the item that was clicked. So we'll grab the nav-path out of the data-attributes, as well as the name and set that equal to fileName. We can get the title from the text out of the element, and then we can call module.individualItemCallback passing in the click event args, as well as the custom arguments built up here in this function. Now if you remember, individualItemCallback starts out as a blank function and that's handled in the page itself. So if we look at the HTML page you can see here that during the initialization function a handler is created for the individual callback item, and at this point we're just logging that information all to the console. So let's just take a look at it in the browser. So from here you can see that once you click on an item over in the navigation area, it bubbles up and you have access to the click event arg information, as well as the custom argument values created in the function. Okay, so this completes the jQuery implementation. Now if order to get us all the way down to Web Components, the next step that we need to take is to remove jQuery and just use the native browser selection API. Let's do that next.
Move to the Native Selection API
Now the next step in the process is to take out all the calls to jQuery and replace it with the native selection API. Now, along with making some of those API changes there are a few stylistic changes that I've made to the code. For instance, by convention, any time something is set aside as a jQuery selector I usually prefix those variables with a dollar sign. Here you'll notice navContainer and a number of other different variables have changed simply be removing the dollar sign prefix. So let's take a look at each method one by one and talk about the changes that are made in order to use the native selection API. So here's the init function, which probably has some of the most extensive change when we're not using jQuery. So here, you'll notice we're using document.querySelector in order to query the DOM and using the native addEventListener in order to set up the click event. Inside the body of the click event then we're taking a look at the target and deciding which one of the functions to use based off of the data-role attribute that is applied to each element. So, if we have close-layer we'll call hideLayer, if parent is set to false then we'll do an itemClick, and if parent is set to true then we'll call parentClick. This was a little bit more separated when using jQuery, but here we can achieve the exact same thing with just a bit of a different syntax. Alright, let's take a look at bindLayer. For the most part, bindLayer remains the same, except for the syntax used in order to build up the elements to bind the layer. So down in this section you'll notice I'm using document.createElement and then setAttribute a number times in order to get the attributes that I need for that element, and then finally setting innerText = item.title. Once that element is created I can call layer.append passing in the itemElement, and finally can return that full layer up to the caller. Okay, now that we saw bindLayer let's take a look at getItems. GetItems remains untouched. So there's no jQuery done in here so we'll go ahead and move on to parentClick. When clicking on a parent item we'll start off by taking a look at the target and then go to the parentElement, the native DOM element of parentElement and then call querySelector from there in order to find the elements that have the class of slidernav-item-selected. Once we find those items we can go into the classList and remove that class and then add the class to the specific target, the actual item that we've clicked on. Then we can pull out the title from the element's innerText property and then grab the nav-path by calling getAttribute and passing that and the title down into addLayer. AddLayer has a few changes as well. The main change found in addLayer is again the process we use in order to create an element. So here we're calling createElement by id, and using setAttribute to give some values to the element's attribute, we'll access the style property directly here by setting the zIndex to the value that we want, and then use the classList collection in order to add the class to that element. The same type of syntax here is found for the closeButton, and once we have everything that we need ready for the closeButton we'll call layer.append passing in the closeButton, bind the layer, and then call append on the navContainer for the layer itself. The last change here is using document.querySelector in order to find the layer by its ID so that we can add to the classList the slidernav-show class name. Okay, I think you're starting to get the hang of it. HideLayer has similar types of changes. From here we're using the target's parent element and calling getAttribute in order to get its id, and then document.querySelector to find the element we're looking for, and then remove slidernav-show from the classList. From there we can get the path out of the element by calling getAttribute and then pass that path down into removeLastLayer. RemoveLastLayer just swaps out the jQuery selector by calling the native querySelector and then locating the element based off the nav-path and then simply calling remove in order to remove that item from the DOM. And lastly, we have itemClick where we're taking a look at the target's parentElement and calling querySelectorAll in order to get all the items that have been marked as selected. Then we'll iterate through each one of those items accessing its classList and calling remove in order to remove the class of slidernav-item-selected. Once that's done we can take a look at the specific target that's been clicked on and then add the class to its classList. Then from there we'll take the target and call getAttribute in order to get the path and the fileName, and then we have the args object that we need in order to pass into individualItemCallback. And that's it. So now that we've made this change we're poised to start building this feature out as a Web Component that uses no jQuery whatsoever. And we'll do that in the next module.
In this module you implemented the slider navigation feature in all of its glory in jQuery, and then moved beyond that by removing jQuery in favor of the native HTML5 selection API. And so from this point we've set the stage for Web Components. So coming up next we'll refactor the code and move to the native Web Components API.
Moving to Native Web Components
Now that we have the foundation set of the navigation feature implemented and we've moved off jQuery into using the Native Selection API, we can now move to building native Web Components. So in this module you'll learn about the Web Components API, how to import HTML, create custom elements, set up a template, and manage the Shadow DOM. Alright, let's go ahead and get started.
Update the Host Page
Define the Template
Implement the Component: bootstrap
Now as we take a look at the overview of the component, you'll notice that a lot of it should look very familiar to you. So for the most part we have all of the same function names, we've got an addition of bootstrap down here, which is called as the module loads, module.bootstrap is run immediately as the module is loaded, but beyond that everything else is named the same. The only other thing that you might notice that's different is that there's no longer an individual item callback, because instead of using callbacks now we're using events as the communication mechanism for the module to talk to anything outside of it. So let's begin by looking at this bootstrap function. The whole purpose of the bootstrap function is to get the component ready and set it up as a component to create the Shadow DOM and get everything ready so that you can begin working with it as a native HTML5 component. So the first thing that we do is go into Object and call create and create an object based off of the HTML element prototype. We'll use that prototype in a moment to finish setting everything up. Next what we need is reference to the current script on the owner document, so we'll do that by calling document.currentScript.ownerDocument, and then once the prototype is created and the created callback is called then we can start by querying the script and looking for the template. So this will find the template associated with this Web Component, and then we can create the official component itself by calling documents.importNode and passing the content of the template into it. Here I'm passing true into the deparameter, which tells import node to make a deep import of all the elements that are in the template. Once this is all set up we can call this.createShadowRoot off the prototype, and then I can set the components title by calling getElementById to find the element that has the id title and set its innerText to the title that was passed into the component. So if you recall, the markup for the component I had set a title, so I am passing the title from the component itself down into the element that has the id of title inside the component. Now once that's set I can call root.appendChild passing in the component and then I can add an event listener for data. Now if you recall from the host page, when the data event is raised it will take in what's passed to it and feed the data down into the init function. The last thing that has to happen, and this happens immediately as the bootstrap function is called, is register the custom element of slider-nav. And this is done by giving it a name here by calling registerElement and passing in the prototype. So when bootstrap runs the component is created, the shadow root is created, and the custom element is registered into the DOM. Now with this basis we can go ahead and look at the implementation for initializing the component.
Implement the Component: init
With bootstrap fully implemented now let's take a look at the init function. The functionality of the init function is the same as it was in the previous implementation with a few minor variations. The signature for this function, instead of taking in a callback now takes in the component itself. Once we have that component we can set it aside in the module by setting module.component equal to the component. Then we'll query the component and find the container within the component itself. Now, remember, before we would have a data-role of slidernav-container, now we just have container, which is great because inside of our component we can keep our class names very specific. We'll set aside the navData into the module and then bind the layer against the data-role element that equals root. Now, in order to handle the event, when a user clicks on an item, from the navContainer we'll add an event listener. So as soon as an item is clicked we'll take a look at the current target and set it aside. Evaluate whether or not the classList includes the item class, so we'll set up isItem equal to that, and also evaluate whether or not it's the close layer button by looking at current target and getting the attribute of data-role, if that equals close-layer. So, if it is an item, or it's a close layer, the first thing we can do is find out if it's within a parent. So here is a regular expression evaluation to test to see whether or not the data-parent attribute is equal it true. So now we know all about the element that's been clicked on, so if it's a close layer we'll call module.hideLayer, if it's a parent, module.parentClick, otherwise it's an itemClick, so that's module.itemClick. And that's the end of the function. So now, we can take a look and see what's changed within bindLayer.
Implement the Component: bindLayer
BindLayer remains largely the same, although the first differentiation is that we'll return execution from this function if there's no data set into the module at first. Then as we come down to the loop where we're creating the elements for the layer, instead of setting the class name equal to slidernav-item, here it's just item. And so once everything is done in order to create the element for the layer, layer.append is called and then return the layer back up to the caller. GetItems remains completely unchanged, so now let's take a look at the few minor variations found within parentClick. Once again the class name has changed that we're using in order to query the DOM. So here it's just item-selected looking for the collection of selected items. Once that class name has been removed then we'll add items selected to the classList of the target, and then take the target's navigation path and the title and pass it down to addLayer, just like we had before. Alright, let's take a look at addLayer. Again, the only difference here comes down to a class name. So here layer.classList.add, we're going to add layer, and then everything else remains the same until we get down to the setTimeout where the class name is just show. So here, you really get the sense of how much cleaner it is to write your code when you're working within the context of a component, because the styles are isolated to the component itself and so you can make your class names so much more natural. Alright, next let's look at hideLayer and the remaining few functions.
Implement the Component: hideLayer
The purpose of hideLayer is to take a look at the currently selected item. So the item that you clicked on, and then go up to its parent element and remove the show class name. Now, in the native selection API implementation we look to find that parent by calling document.querySelector. Here it's a little bit different though, because we're working within the context of a component, we're using module.component. So we're doing querySelector against the component itself, and there again, that's nice because class names and IDs are isolated to the components so they can be a little more general and make your code a little bit cleaner. So here, module.component.querySelector, passing in that parentSelector gives us the parent in order to remove the show class. Now removeLastLayer has just a minor change as well. Here it just comes down to that class name again. Instead of looking for slidernav-layer it's just the layer class and the data-nav-path attribute. Alright, let's finish things off now by looking at the itemClick function.
Implement the Component: itemClick
Here within the itemClick the logic is largely the same, the changes that we'll see here are basically against class names, and then instead of using a callback using an event to communicate with the application outside the component itself. So here querySelectorAll is looking for item-selected, and we'll remove item-selected from selected items. And of course we'll add that back in on the target in order to show the selected item itself. Now the args object remains exactly the same, except this time, instead of passing those args up through a callback, we'll create a custom event, name the items selected, and in the detail of that event, the UI information will be the native click event args represented by e and then the data will be the args object created up above. Then it's as easy as calling module.navContainer.dispatchEvent to signal to the hosting page that an item has been clicked. Alright, let's take a look at it working in the browser.
So here we are on the initial load of the page, module.bootstrap is called, and so let's step into the bootstrap function. From here we create a prototype from HTMLElement.prototype. And then create the script off document.currentScript.ownerDocument, and then set up the createdCallback off the prototype. Once the prototype is created then we can create the Shadow DOM and finish the setup. This registers the custom element on the page, and now that we're in the createdCallback we can set up the template, set up the component, create the shadow Root, set the title, based off the title that comes in from the component itself, append the component to the root, and then listen for the data as it comes in from the data event. So once that fires, so now execution is over on the host page and DOMContentLoaded is executed, we can find the slider on the page by calling getElementById and set up a listener for items selected. Since we have the data available off the window object, now this could have come in from any sort of asynchronous operation, an ajax call, pulled out of local storage, however you decide to get your data is fine, but in this case I'm just taking the available data and sending it into the component using an event. From there we're back into the slidernav module. This is the event handler for the data events, and now the module is initialized by passing in the data that comes in from the event arguments, and the root of the component, which was created up in the createdCallback. And after I let that run, the component is set up and now I can click on an item, and here you'll notice that the click event listener is being called. Based off of what type of item I'm looking at it'll go through and evaluate whether or not it's a closed layer, a parent, or an itemClick. In this case it's parent, and once again it will be a parent as well, until finally I get down to an item, and from there the item selected event is handled and I can logout the details. So you'll see here, e.detail is the data information of that item that was clicked, as well as the original MouseEvent argument. So I can log all that out to the console, and then you can see we have the exact same result from the previous implementation. Okay, at this point, we have a native Web Component, but things can get even easier for you. So now let's take a look at using Polymer.js in order to manage some of the boilerplate code needed to create a Web Component.
In this module you had the opportunity to implement the feature as a Web Component. You saw how to import HTML, create custom elements, use the templates, and manage the Shadow DOM. Next up, we'll move to Polymer.js, where life gets a little bit easier without having to deal with the low-level Web Component APIs. I'll see you there.
Moving to Polymer.js
Well, we're gaining some momentum, so now in this modules you'll see how easy it is to take an existing Web Component and refactor it to a full-fledged Polymer component. By using Polymer the boilerplate code fades away and you'll also gain the support of one-way and two-way data binding, computed properties, conditional and repeat templates, gesture events, and a much more structured, object-oriented approach. Okay, let's dive on it.
Update the Host Page
So as we have in the past, let's start the discussion about the Polymer component here in the host page. So right up here from the top you'll notice, we're still bringing in Bootstrap and our global styles, but then we have a script for the webcomponents-loader.js. And then from there we'll import polymer.html, so this gives us all the resources we need to start creating Polymer components. Now, at the bottom here you'll notice there's an import for slider-nav.html, and we'll get to that file in a moment, but that's where you'll find the template, styles, and the script, all for the web component. Within the body of the page you'll notice the custom element of slider-nav giving it the idea of slider and the title of Courses. Now my implementation flag shows that this is the Polymer implementation, and then just like I did in the past I'll bring in the data through a hard-coded reference, but of course, you would probably use ajax, or a service call, or some other means of getting this data onto your page, just keeping it simple here. Then we'll take a listen for DOMContentLoaded. Once the content has been loaded then we can get to the slider by calling getElementById and since the Polymer component is implemented as a class, I can simply call the init function by passing in the data. And then we'll add an event listener listening for items selected once again, and they can log out the details of what's selected. So now let's take a look at the styles, the template, and the class of the Polymer component.
Update the Style Sheet
Now the Polymer component is composed a little bit different than the native component. So you can see from a high-level we have the dom-module element, and I'll give the id of slider-nav and inside that it has a template, which includes styles and the markup for the components. Underneath that is a script block, which includes the implementation for the component, and all of this together within an HTML file is what creates a Polymer component. Now to kick everything off what we need to do at the very top is import polymer.html. And so you just have to find the path, in this case it's into the bower_components/polymer, and then the file of polymer.html. So as I've done in the past with the styles I'll open this up and we can take a look at it, and again, I won't explain line-by-line what's happening in the styles, but just as was true with the native component, but the style names are now very tightly defined because they're all isolated to the component itself. So here's the style for the layer itself, as well as anything that has the role of container. The left margin starts off, off the screen, by a -350 px, and of course, as we show at the margin-left is set to 0. We've got some background color changes, styling of individual items, and then some content definitions for parent items and the back button. Okay so that's the styles, now let's take a look at the template itself, which manages the data binding from the data that gets passed into the component.
Define the Template
Overview of Component Class
Implement the Component: init
Now as the data comes into the class, what we want to do is prep that information for data binding. So, we want to go ahead and calculate ahead of time some of the properties. So again, as the items come in, we'll got through each one of those items and then figure out its new path. So we'll start off with the base path of the item, and then concatenate in the index by calling toString and joining those, serializing that path to a string by joining it with a comma. This is the same exact functionality as what we've done before, it's just located in a little bit of a different spot. Then for each one of the items we can find out whether or not it's a parent. So if that item has children and the children length is greater than 0, then parent will evaluate to true, and then we'll serialize that to either true or false and set that into item.isParent. And the path also is set to the new path and once that's done for each one of the items we can return up the items. Now prepForTemplate is called from within init, so let's take a look at that now. So initialization at this point gets very simple. We take the data that's passed into init and set it aside for navData and then set up the root items by calling prepForTemplate, passing in the data, and an empty array. So the base path for this is empty because we haven't clicked on anything yet, so then it's an empty array. Id is seeded at 0, and we'll go about creating some layers in a moment, so that starts off as an empty array as well. So that's all we need to do in order to initialize the class. Now in previous approaches we provided the implementation for what happens when you click on something within the layer, here within init. But because we're doing it as a class we can separate that out. And if you remember from the markup, it's set to call the click method any time something is clicked within the component. So here's the click method, and once again the implementation here remains the same, we'll take a look at the current target and find out if it has the class of item to know whether or not it's an item, and also find out if it's a close button to see if the data-role is equal to close-layer. Then we know if it's an item or a close-layer, we find out if it's a parent by running this regular expression, and the based off what we know about the element now we can delegate either to hideLayer, parentClick, or itemClick. Now getItems remains absolutely identical to what it was before, so here I'll show you the implementation, and in the next clip we can start looking at parentClick and the rest of the methods here in the class.
Implement the Component: parentClick
Now parentClick is largely the same except for one major distinction, and that's a use of Polymer.dom. So when we're querying the DOM we need to use Polymer.dom instead of other approaches so that we know that we're finding elements that are created with inside the Polymer component. So here, in order to find the selected items we're doing Polymer.dom, scoping it to the target's parentElement and then running the query selector of item-selected. So this takes a look at the parent element and finds the items selected on it. So if we have items that are selected we'll go ahead and remove the item-selected class off of each one of those, and then can go to the target and add item-selected to its classList. So the rest of this is identical to what you've seen in the past, we'll take the target's innerText, set that aside as the title, as well as the path, and now we can addLayer based off the path that was clicked and the title. Alright, let's look at addLayer.
Implement the Component: addLayer
Implement the Component: removeLayer
Now, hideLayer will also use Polymer.dom in order to query the DOM, So we'll build up the querySelector by using the target's parent element in order to get the attribute of its id. So by gluing that and the pound sign together we now have a selector, we can call Polymer.dom, looking at the root of the component, and then selecting the layer that we want to hide. So by going to the parent.classList and removing the show class, then the CSS animation will take care of sliding it out of the way, and then we can get the parentnavPath off the data- attribute of data-nav-path. Once we have that we can call removeLastLayer in order to clean up the DOM. So removeLastLayer takes the parentnavPath and sets aside the root of the component. We want to be able to reference that after the async function of setTimeout runs. So after 1 second, setTimeout will fire and we can go to Polymer.dom, looking at the root and selecting for the root of the component itself. Once I have that I can select inside the root of the component and find the layer that has the data-nav-path of the current path. Then once I found my layer I can go to Polymer.dom, passing in the root and remove the child of that layer. So that keeps everything clean and in sync based off of the user interaction. Alright, we've got one more method left, let's take a look at itemClick.
Implement the Component: itemClick
When an item is clicked, just as before, the first thing that we want to do is find all the items that have the itemselected class on them, it should only be one, but we're going to select them all and remove it. So here by calling Polymer.dom, looking at the target's parentElement we can run that querySelector and get the selected items. For each one of those we can remove items selected from the classList. Then for the target that we've clicked on itself we'll add in item-selected, and then just as you're used to seeing we'll build up the args object by setting the navPath equal to data-nav-path, the fileName to data-name, and the title to the text that's inside the target element. Then we can create itemselected and dispatch that event so that now the host page or any other component that's subscribing to this event will know about what happened inside our Polymer component. Okay, well with that let's take a look at this running within the browser and debug through some of the logic a little bit.
So here's the Polymer host page during initial load. So DOMContentLoaded is fired and now I can find the slider by calling getElementById. Now, just by calling getElementById it recognizes that that is a Polymer component, so here I can directly call the method of the components by calling init and passing in the data. So here's the data as it has come in to the component, and I can set that aside into navData. PrepForTemplate each one of the items, so now when we take a look at the items you'll notice that I have a path and a value for isParent for each one of the items, and then set up id and layers as necessary. So once I click on an item, here the click method is called, and this is what decides whether or not to call hideLayer, parentClick, or itemClick. So in this case, this is a parent item so we'll call parentClick, and during parentClick we'll call Polymer.dom in order to do DOM selection, again scope that to the parent element and look for the itemselected. So we shouldn't have any of those right now, we'll add the item-selected class to the item that was clicked, and then get the title and path and then addLayer. Now remember, addLayer simply goes through the process of creating a layer, prepping the items for the template, and adding that to the layer collection within the component itself. So as I let that execution run, you saw the panel over here slide in, and that's all done through the data binding, simply because I added a layer that happened to have some items in it. So I'll click on another parent here. And so now with parentClick, again we'll be using Polymer.dom, scoping that to the parent element, and looking for the item-selected, and all the way down, as you saw before. Now when I click on an individual item, click will figure out which one that I need to call here, so in this case it's itemClick. And now with itemClick we'll be looking at the parentElement, scoping that to Polymer.dom and looking for all the items that are set as item-selected, remove them at item-selected to the individual item that I've clicked, create the args object, as you've seen in the past, so this gives me the fileName, the navPath, and the title, and then create the event and dispatch the event to say that an item is selected. Once that's run, then the event listener and the host page picks that up and you can see that we have the same values as we've had in the past, the MouseEvent information, as well as the custom arguments created within the component.
In this module you learned how to refactor a Web Component into using Polymer.js. You got to implement a component class and see how all of this culminated into some very simple registration for your Web Component. Well thank you so much for taking the time to spend with me during this course, I hope that you have an opportunity to build many Web Components, which makes your development and the development of other applications that much easier. I hope you can take what you've learned here and refactor much of your existing code in order to make your applications more granular and reusable in the future. Well once again, this is Craig Shoemaker, and thanks for joining me for HTML5 Web Components: Moving from jQuery to Polymer.js.
Craig Shoemaker is a developer, instructor, writer, podcaster, and technical evangelist of all things awesome.
Released19 Dec 2017