├── Part 1 ├── UserCard │ ├── UserCard.css │ ├── UserCard.html │ └── UserCard.js └── index.html ├── Part 2 ├── components │ └── PeopleController │ │ ├── PeopleController.css │ │ ├── PeopleController.html │ │ ├── PeopleController.js │ │ ├── PeopleList │ │ ├── PeopleList.html │ │ └── PeopleList.js │ │ └── PersonDetail │ │ ├── PersonDetail.css │ │ ├── PersonDetail.html │ │ └── PersonDetail.js └── index.html └── README.md /Part 1/UserCard/UserCard.css: -------------------------------------------------------------------------------- 1 | .card__user-card-container { 2 | text-align: center; 3 | display: inline-block; 4 | border-radius: 5px; 5 | border: 1px solid grey; 6 | font-family: Helvetica; 7 | margin: 3px; 8 | width: 30%; 9 | } 10 | 11 | .card__user-card-container:hover { 12 | box-shadow: 3px 3px 3px; 13 | } 14 | 15 | .card__hidden-content { 16 | display: none; 17 | } 18 | 19 | .card__block-content { 20 | display: block !important; 21 | } 22 | 23 | .card__details-btn { 24 | background-color: #dedede; 25 | padding: 6px; 26 | margin-bottom: 8px; 27 | } 28 | -------------------------------------------------------------------------------- /Part 1/UserCard/UserCard.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /Part 1/UserCard/UserCard.js: -------------------------------------------------------------------------------- 1 | const currentDocument = document.currentScript.ownerDocument; 2 | 3 | class UserCard extends HTMLElement { 4 | constructor() { 5 | // If you define a constructor, always call super() first as it is required by the CE spec. 6 | super(); 7 | 8 | // Setup a click listener on 9 | this.addEventListener('click', e => { 10 | this.toggleCard(); 11 | }); 12 | } 13 | 14 | // Called when element is inserted in DOM 15 | connectedCallback() { 16 | const shadowRoot = this.attachShadow({ mode: 'open' }); 17 | 18 | // Select the template and clone it. Finally attach the cloned node to the shadowDOM's root. 19 | // Current document needs to be defined to get DOM access to imported HTML 20 | const template = currentDocument.querySelector('#user-card-template'); 21 | const instance = template.content.cloneNode(true); 22 | shadowRoot.appendChild(instance); 23 | 24 | // Extract the attribute user-id from our element. 25 | // Note that we are going to specify our cards like: 26 | // 27 | const userId = this.getAttribute('user-id'); 28 | 29 | // Fetch the data for that user Id from the API and call the render method with this data 30 | fetch(`https://jsonplaceholder.typicode.com/users/${userId}`) 31 | .then((response) => response.text()) 32 | .then((responseText) => { 33 | this.render(JSON.parse(responseText)); 34 | }) 35 | .catch((error) => { 36 | console.error(error); 37 | }); 38 | } 39 | 40 | render(userData) { 41 | // Fill the respective areas of the card using DOM manipulation APIs 42 | // All of our components elements reside under shadow dom. So we created a this.shadowRoot property 43 | // We use this property to call selectors so that the DOM is searched only under this subtree 44 | this.shadowRoot.querySelector('.card__full-name').innerHTML = userData.name; 45 | this.shadowRoot.querySelector('.card__user-name').innerHTML = userData.username; 46 | this.shadowRoot.querySelector('.card__website').innerHTML = userData.website; 47 | this.shadowRoot.querySelector('.card__address').innerHTML = `

Address

48 | ${userData.address.suite},
49 | ${userData.address.street},
50 | ${userData.address.city},
51 | Zipcode: ${userData.address.zipcode}` 52 | } 53 | 54 | toggleCard() { 55 | let elem = this.shadowRoot.querySelector('.card__hidden-content'); 56 | let btn = this.shadowRoot.querySelector('.card__details-btn'); 57 | 58 | // if elem has "card __block-content" class, remove class, otherwise add it. 59 | elem.classList.toggle('card__block-content'); 60 | // if elem has "card __block-content" class, the text of button will be "Less Details", 61 | // otherwise will be "More Details" 62 | btn.innerHTML = elem.classList.contains('card__block-content') ? 'Less Details' : 'More Details'; 63 | } 64 | } 65 | 66 | customElements.define('user-card', UserCard); 67 | -------------------------------------------------------------------------------- /Part 1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Component 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PeopleController.css: -------------------------------------------------------------------------------- 1 | #people-list { 2 | width: 45%; 3 | display: inline-block; 4 | } 5 | #person-detail { 6 | width: 45%; 7 | display: inline-block; 8 | } 9 | -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PeopleController.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PeopleController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const currentDocument = document.currentScript.ownerDocument; 3 | 4 | function _fetchAndPopulateData(self) { 5 | let peopleList = self.shadowRoot.querySelector('#people-list'); 6 | fetch(`https://jsonplaceholder.typicode.com/users`) 7 | .then((response) => response.text()) 8 | .then((responseText) => { 9 | const list = JSON.parse(responseText); 10 | self.peopleList = list; 11 | peopleList.list = list; 12 | 13 | _attachEventListener(self); 14 | }) 15 | .catch((error) => { 16 | console.error(error); 17 | }); 18 | } 19 | function _attachEventListener(self) { 20 | let personDetail = self.shadowRoot.querySelector('#person-detail'); 21 | 22 | //Initialize with person with id 1: 23 | personDetail.updatePersonDetails(self.peopleList[0]); 24 | 25 | self.shadowRoot.addEventListener('PersonClicked', (e) => { 26 | // e contains the id of person that was clicked. 27 | // We'll find him using this id in the self.people list: 28 | self.peopleList.forEach(person => { 29 | if (person.id == e.detail.personId) { 30 | // Update the personDetail component to reflect the click 31 | personDetail.updatePersonDetails(person); 32 | } 33 | }) 34 | }) 35 | } 36 | 37 | class PeopleController extends HTMLElement { 38 | constructor() { 39 | super(); 40 | this.peopleList = []; 41 | } 42 | 43 | connectedCallback() { 44 | const shadowRoot = this.attachShadow({ mode: 'open' }); 45 | const template = currentDocument.querySelector('#people-controller-template'); 46 | const instance = template.content.cloneNode(true); 47 | shadowRoot.appendChild(instance); 48 | 49 | _fetchAndPopulateData(this); 50 | } 51 | } 52 | 53 | customElements.define('people-controller', PeopleController); 54 | })() -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PeopleList/PeopleList.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PeopleList/PeopleList.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const currentDocument = document.currentScript.ownerDocument; 3 | 4 | function _createPersonListElement(self, person) { 5 | let li = currentDocument.createElement('LI'); 6 | li.innerHTML = person.name; 7 | li.className = 'people-list__name' 8 | li.onclick = () => { 9 | let event = new CustomEvent("PersonClicked", { 10 | detail: { 11 | personId: person.id 12 | }, 13 | bubbles: true 14 | }); 15 | self.dispatchEvent(event); 16 | } 17 | return li; 18 | } 19 | 20 | class PeopleList extends HTMLElement { 21 | constructor() { 22 | // If you define a constructor, always call super() first as it is required by the CE spec. 23 | super(); 24 | 25 | // A private property that we'll use to keep track of list 26 | let _list = []; 27 | 28 | // Use defineProperty to define a prop on this object, ie, the component. 29 | // Whenever list is set, call render. This way when the parent component sets some data 30 | // on the child object, we can automatically update the child. 31 | Object.defineProperty(this, 'list', { 32 | get: () => _list, 33 | set: (list) => { 34 | _list = list; 35 | this.render(); 36 | } 37 | }); 38 | } 39 | 40 | connectedCallback() { 41 | // Create a Shadow DOM using our template 42 | const shadowRoot = this.attachShadow({ mode: 'open' }); 43 | const template = currentDocument.querySelector('#people-list-template'); 44 | const instance = template.content.cloneNode(true); 45 | shadowRoot.appendChild(instance); 46 | } 47 | 48 | render() { 49 | let ulElement = this.shadowRoot.querySelector('.people-list__list'); 50 | ulElement.innerHTML = ''; 51 | 52 | this.list.forEach(person => { 53 | let li = _createPersonListElement(this, person); 54 | ulElement.appendChild(li); 55 | }); 56 | } 57 | } 58 | 59 | customElements.define('people-list', PeopleList); 60 | })(); -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PersonDetail/PersonDetail.css: -------------------------------------------------------------------------------- 1 | .card__user-card-container { 2 | text-align: center; 3 | border-radius: 5px; 4 | border: 1px solid grey; 5 | font-family: Helvetica; 6 | margin: 3px; 7 | } 8 | 9 | .card__user-card-container:hover { 10 | box-shadow: 3px 3px 3px; 11 | } 12 | 13 | .card__hidden-content { 14 | display: none; 15 | } 16 | 17 | .card__details-btn { 18 | background-color: #dedede; 19 | padding: 6px; 20 | margin-bottom: 8px; 21 | } -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PersonDetail/PersonDetail.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /Part 2/components/PeopleController/PersonDetail/PersonDetail.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const currentDocument = document.currentScript.ownerDocument; 3 | 4 | class PersonDetail extends HTMLElement { 5 | constructor() { 6 | // If you define a constructor, always call super() first as it is required by the CE spec. 7 | super(); 8 | 9 | // Setup a click listener on 10 | this.addEventListener('click', e => { 11 | this.toggleCard(); 12 | }); 13 | } 14 | 15 | // Called when element is inserted in DOM 16 | connectedCallback() { 17 | const shadowRoot = this.attachShadow({ mode: 'open' }); 18 | const template = currentDocument.querySelector('#person-detail-template'); 19 | const instance = template.content.cloneNode(true); 20 | shadowRoot.appendChild(instance); 21 | } 22 | 23 | // Creating an API function so that other components can use this to populate this component 24 | updatePersonDetails(userData) { 25 | this.render(userData); 26 | } 27 | 28 | // Function to populate the card(Can be made private) 29 | render(userData) { 30 | this.shadowRoot.querySelector('.card__full-name').innerHTML = userData.name; 31 | this.shadowRoot.querySelector('.card__user-name').innerHTML = userData.username; 32 | this.shadowRoot.querySelector('.card__website').innerHTML = userData.website; 33 | this.shadowRoot.querySelector('.card__address').innerHTML = `

Address

34 | ${userData.address.suite},
35 | ${userData.address.street},
36 | ${userData.address.city},
37 | Zipcode: ${userData.address.zipcode}` 38 | } 39 | 40 | toggleCard() { 41 | let elem = this.shadowRoot.querySelector('.card__hidden-content'); 42 | let btn = this.shadowRoot.querySelector('.card__details-btn'); 43 | btn.innerHTML = elem.style.display == 'none' ? 'Less Details' : 'More Details'; 44 | elem.style.display = elem.style.display == 'none' ? 'block' : 'none'; 45 | } 46 | } 47 | 48 | customElements.define('person-detail', PersonDetail); 49 | })() -------------------------------------------------------------------------------- /Part 2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Component Part 2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Components tutorials 2 | 3 | This repository contains the code I wrote to go along with [web component posts](https://ayushgp.github.io/html-web-components-using-vanilla-js/). To run the code, you need a static server. I use [static-server](https://github.com/nbluis/static-server) for it. Go to either directory and run: 4 | 5 | ``` 6 | $ static-server 7 | ``` 8 | 9 | Then navigate to [localhost](http://localhost:9080). 10 | 11 | Links to tutorials: 12 | 1. Part 1: [HTML Web Component using Vanilla JavaScript](https://ayushgp.github.io/html-web-components-using-vanilla-js/) 13 | 2. Part 2: [Making Higher Order Components](https://ayushgp.github.io/html-web-components-using-vanilla-js-part-2/) 14 | 3. Part 3: [Using Attributes to define web components' behavior](https://ayushgp.github.io/html-web-components-using-vanilla-js-part-3/) 15 | 16 | ### Components Created for examples: 17 | #### Part 2: 18 | ![Part 2 Component](https://user-images.githubusercontent.com/7992943/32207972-794dd8de-be25-11e7-8333-37aece4c030c.gif) 19 | 20 | #### Part 3: 21 | ![Part 3 Component](https://user-images.githubusercontent.com/7992943/32566632-8b030bde-c4de-11e7-98ff-9be1534c2c2b.gif) 22 | 23 | ### About Web Components: 24 | The HTML and DOM standards define four new standards/APIs that are helpful for defining Web Components. These standards are: 25 | 26 | 1. [Custom Elements](https://www.w3.org/TR/custom-elements/): With Custom Elements, web developers can create new HTML tags, beef-up existing HTML tags, or extend the components other developers have authored. This API is the foundation of Web Components. 27 | 2. [HTML Templates](https://www.html5rocks.com/en/tutorials/webcomponents/template/#toc-pillars): It defines a new `