elements
20 | { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } },
21 |
22 | // Zoom in and out with Alt+click
23 | { src: 'plugin/zoom-js/zoom.js', async: true },
24 |
25 | // Speaker notes
26 | { src: 'plugin/notes/notes.js', async: true },
27 |
28 | // MathJax
29 | { src: 'plugin/math/math.js', async: true }
30 | ]
31 | });
32 |
33 | event.target.removeEventListener(event.type, init);
34 | }
35 |
36 | document.addEventListener('DOMContentLoaded', init);
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-components-are-coming",
3 | "version": "0.0.0",
4 | "main": "src/index.ts",
5 | "scripts": {
6 | "build": "webpack --config ./webpack/webpack.dev.js",
7 | "clean": "rimraf dist/*",
8 | "serve": "webpack-dev-server --config ./webpack/webpack.dev.js --port 3000 --host 0.0.0.0",
9 | "start": "yarn serve"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/haydenbr/web-components-are-coming"
14 | },
15 | "author": "haydenbr",
16 | "license": "MIT",
17 | "devDependencies": {
18 | "ajv": "^5.0.0",
19 | "autoprefixer": "^7.2.6",
20 | "chalk": "^2.3.1",
21 | "copy-webpack-plugin": "^4.4.1",
22 | "css-loader": "^0.28.9",
23 | "extract-loader": "3.1.0",
24 | "file-loader": "4.1.0",
25 | "html-loader": "^0.5.5",
26 | "node-sass": "^4.7.2",
27 | "pngquant-bin": "3.1.1",
28 | "postcss": "^6.0.17",
29 | "postcss-loader": "3.0.0",
30 | "rimraf": "^2.6.2",
31 | "sass-loader": "^6.0.6",
32 | "source-map": "^0.7.0",
33 | "ts-loader": "^6.0.4",
34 | "typescript": "^3.5.3",
35 | "webpack": "4.39.1",
36 | "webpack-dev-server": "3.7.2",
37 | "write-file-webpack-plugin": "^4.2.0"
38 | },
39 | "dependencies": {
40 | "lit-element": "^2.2.1",
41 | "lit-html": "1.0.0",
42 | "reveal.js": "3.6.0",
43 | "webpack-cli": "^3.3.6"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/examples/cool-tab-group.lit-html.component.js:
--------------------------------------------------------------------------------
1 | import { html, render } from 'lit-html';
2 |
3 | const style = `
4 | :host {
5 | align-items: center;
6 | display: flex;
7 | justify-content: center;
8 | }
9 | `;
10 |
11 | class CoolTabGroup extends HTMLElement {
12 | constructor() {
13 | super();
14 |
15 | this._value = undefined;
16 | }
17 |
18 | connectedCallback() {
19 | this.attachShadow({ mode: 'open' });
20 |
21 | this.addEventListener('tabselect', (event) => this.value = event.target.value);
22 | this.value = this.defaultValue;
23 | // setTimeout(() => this.render());
24 | this.render()
25 | }
26 |
27 | disconnectedCallback() {
28 | this.removeEventListener('tabselect');
29 | }
30 |
31 | get defaultValue() {
32 | return this.getAttribute('value') || '';
33 | }
34 |
35 | get value() {
36 | return this._value;
37 | }
38 |
39 | set value(newValue) {
40 | if (newValue === this.value) {
41 | return;
42 | }
43 |
44 | let oldValue = this._value;
45 | this._value = newValue;
46 | this.render();
47 | this.dispatchTabChange({ oldValue, newValue });
48 | }
49 |
50 | get tabButtons() {
51 | return Array.from(this.querySelectorAll('cool-tab'));
52 | }
53 |
54 | dispatchTabChange(payload) {
55 | this.dispatchEvent(new CustomEvent('tabchange', { bubbles: true, detail: payload }));
56 | }
57 |
58 | render() {
59 | this.tabButtons.forEach((tab) => tab.selected = (tab.value === this.value));
60 |
61 | render(html`
62 |
63 |
64 | `, this.shadowRoot);
65 | }
66 | }
67 |
68 | // window.customElements.define('cool-tab-group', CoolTabGroup);
69 |
--------------------------------------------------------------------------------
/src/examples/cool-tab-group.vanilla.component.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | const style = `
3 | :host {
4 | align-items: center;
5 | display: flex;
6 | justify-content: center;
7 | }
8 | `;
9 |
10 | template.innerHTML = `
11 |
12 |
13 | `;
14 |
15 | class CoolTabGroup extends HTMLElement {
16 | constructor() {
17 | super();
18 |
19 | this._value = undefined;
20 | }
21 |
22 | connectedCallback() {
23 | const shadowRoot = this.attachShadow({ mode: 'open' });
24 | shadowRoot.appendChild(template.content.cloneNode(true));
25 |
26 | this.addEventListener('tabselect', (event) => this.value = event.target.value);
27 | this.value = this.defaultValue;
28 | setTimeout(() => this.render());
29 | }
30 |
31 | disconnectedCallback() {
32 | this.removeEventListener('tabselect');
33 | }
34 |
35 | get defaultValue() {
36 | return this.getAttribute('value') || '';
37 | }
38 |
39 | get value() {
40 | return this._value;
41 | }
42 |
43 | set value(newValue) {
44 | if (newValue === this.value) {
45 | return;
46 | }
47 |
48 | let oldValue = this._value;
49 | this._value = newValue;
50 | this.render();
51 | this.dispatchTabChange({ oldValue, newValue });
52 | }
53 |
54 | get tabButtons() {
55 | return Array.from(this.querySelectorAll('cool-tab'));
56 | }
57 |
58 | dispatchTabChange(payload) {
59 | this.dispatchEvent(new CustomEvent('tabchange', { bubbles: true, detail: payload }));
60 | }
61 |
62 | render() {
63 | this.tabButtons.forEach((tab) => tab.selected = (tab.value === this.value));
64 | }
65 | }
66 |
67 | window.customElements.define('cool-tab-group', CoolTabGroup);
68 |
--------------------------------------------------------------------------------
/src/styles/font.scss:
--------------------------------------------------------------------------------
1 | $roboto-font-path: 'assets/fonts';
2 |
3 | @font-face {
4 | font-family: 'Roboto';
5 | font-style: normal;
6 | font-weight: 300;
7 | src: local('Roboto'), local('Roboto-Light'),
8 | url('#{$roboto-font-path}/roboto-light.woff2') format('woff2'),
9 | url('#{$roboto-font-path}/roboto-light.woff') format('woff'),
10 | url('#{$roboto-font-path}/roboto-light.ttf') format('truetype');
11 | }
12 |
13 | @font-face {
14 | font-family: 'Roboto';
15 | font-style: normal;
16 | font-weight: 400;
17 | src: local('Roboto'), local('Roboto-Regular'),
18 | url('#{$roboto-font-path}/roboto-regular.woff2') format('woff2'),
19 | url('#{$roboto-font-path}/roboto-regular.woff') format('woff'),
20 | url('#{$roboto-font-path}/roboto-regular.ttf') format('truetype');
21 | }
22 |
23 | @font-face {
24 | font-family: 'Roboto';
25 | font-style: normal;
26 | font-weight: 500;
27 | src: local('Roboto Medium'), local('Roboto-Medium'),
28 | url('#{$roboto-font-path}/roboto-medium.woff2') format('woff2'),
29 | url('#{$roboto-font-path}/roboto-medium.woff') format('woff'),
30 | url('#{$roboto-font-path}/roboto-medium.ttf') format('truetype');
31 | }
32 |
33 | @font-face {
34 | font-family: 'Roboto';
35 | font-style: normal;
36 | font-weight: 700;
37 | src: local('Roboto Bold'), local('Roboto-Bold'),
38 | url('#{$roboto-font-path}/roboto-bold.woff2') format('woff2'),
39 | url('#{$roboto-font-path}/roboto-bold.woff') format('woff'),
40 | url('#{$roboto-font-path}/roboto-bold.ttf') format('truetype');
41 | }
42 |
43 | .noselect {
44 | -webkit-touch-callout: none;
45 | -webkit-user-select: none;
46 | -khtml-user-select: none;
47 | -moz-user-select: none;
48 | -ms-user-select: none;
49 | user-select: none;
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/slides.scss:
--------------------------------------------------------------------------------
1 | section {
2 | img {
3 | border: 0 !important;
4 | background-color: transparent !important;
5 | box-shadow: none !important;
6 | }
7 |
8 | pre {
9 | box-shadow: none !important;
10 | width: 100% !important;
11 |
12 | .hljs {
13 | box-shadow: 0px 0px 6px rgba(0,0,0,0.3);
14 | }
15 | }
16 |
17 | .row {
18 | display: flex;
19 | align-items: center;
20 | }
21 |
22 | &.content-slide {
23 | text-align: left;
24 | top: 0 !important;
25 | }
26 |
27 | &.image-slide {
28 | padding: 0 !important;
29 |
30 | img {
31 | margin: 0 !important;
32 | }
33 | }
34 |
35 | &.title-slide {
36 | text-align: left;
37 | top: 0 !important;
38 |
39 | img {
40 | background: none !important;
41 | box-shadow: none !important;
42 | }
43 | }
44 |
45 | .section-title {
46 | color: $white;
47 | font-size: 60px;
48 | font-weight: bold;
49 | text-transform: uppercase;
50 | }
51 |
52 | .section-subtitle {
53 | color: $primary;
54 | font-size: 60px;
55 | font-weight: bold;
56 | text-transform: uppercase;
57 | }
58 |
59 | .page-title {
60 | color: $white;
61 | font-size: 60px;
62 | margin-bottom: 20px;
63 | // text-transform: capitalize;
64 | }
65 | }
66 |
67 | .slide-background {
68 | &.present {
69 | background-size: 100% 100%;
70 | }
71 | }
72 |
73 | .reveal {
74 | .slides {
75 | img {
76 | background: none;
77 | }
78 | }
79 | }
80 |
81 | #closing-image {
82 | cursor: pointer;
83 | display: block;
84 | margin-bottom: 40px;
85 |
86 | img {
87 | border-radius: 15px;
88 | margin: 0;
89 | width: 500px;
90 | }
91 | }
92 |
93 | #banana {
94 | // bottom: -100px;
95 | top: -550px;
96 | left: 100%;
97 | position: relative;
98 | width: 8px;
99 | }
100 |
101 | #unboxed-logo {
102 | width: 200px;
103 | position: absolute;
104 | }
105 |
--------------------------------------------------------------------------------
/src/examples/kitchen-sink.component.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | const styles = `
3 | :host {
4 | --color: #f8981c
5 | }
6 |
7 | p {
8 | color: var(--color, #6f7dbc);
9 | }
10 |
11 | slot::slotted(small) {
12 | color: yellow;
13 | }
14 |
15 | slot[name="secondary-slot"]::slotted(small) {
16 | color: red;
17 | }
18 |
19 | slot[name="secondary-slot"]::slotted(.cool) {
20 | color: #6f7dbc;
21 | }
22 | `;
23 | const templateString = `
24 |
25 | is cool!!!
26 |
27 |
28 |
29 | `;
30 | template.innerHTML = templateString;
31 |
32 | class KitchenSink extends HTMLElement {
33 | constructor() {
34 | super();
35 |
36 | this._$name = undefined;
37 | this._name = '';
38 | }
39 |
40 | handleNameChange(oldValue, newValue) {
41 | if (oldValue === newValue) {
42 | return;
43 | }
44 |
45 | this.name = newValue;
46 | }
47 |
48 | connectedCallback() {
49 | // this.appendChild(template.content.cloneNode(true))
50 | // this._$name = this.querySelector('#name');
51 | this.attachShadow({ mode: 'open' });
52 | this.shadowRoot.appendChild(template.content.cloneNode(true));
53 | this._$name = this.shadowRoot.querySelector('#name');
54 |
55 | this.name = this.getAttribute('name') || 'Bob';
56 | }
57 |
58 | get name() {
59 | return this._name;
60 | }
61 |
62 | set name(newName) {
63 | if (newName === this.name) {
64 | return;
65 | }
66 |
67 | this.setAttribute('name', newName);
68 | this._name = newName;
69 | this.render();
70 | }
71 |
72 | static get observedAttributes() {
73 | return ['name'];
74 | }
75 |
76 | attributeChangedCallback(attrName, oldValue, newValue) {
77 | if (oldValue !== newValue) {
78 | this[attrName] = newValue;
79 | }
80 | }
81 |
82 | disconnectedCallback() {
83 | // or do something useful like clean up event listeners
84 | alert(`I'll get you Eh Steve, if it's that last thing I DOOOOOOOOOOO!`);
85 | }
86 |
87 | render() {
88 | this._$name.textContent = this.name;
89 | }
90 | }
91 |
92 | window.customElements.define('kitchen-sink', KitchenSink);
93 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 | const WriteFilePlugin = require('write-file-webpack-plugin');
5 |
6 | const config = {
7 | devtool: 'inline-source-map',
8 | entry: {
9 | main: ['./src/main.js']
10 | },
11 | output: {
12 | filename: '[name].js',
13 | path: path.resolve(__dirname, '..', 'dist')
14 | },
15 | resolve: {
16 | extensions: ['.ts', '.js', '.json'],
17 | modules: [path.resolve('node_modules')]
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.(ts)$/,
23 | use: 'ts-loader',
24 | exclude: /node_modules/
25 | },
26 | {
27 | test: /\.(scss)$/,
28 | use: [
29 | { loader: 'file-loader', options: { name: '[name].css' } },
30 | { loader: 'extract-loader' },
31 | { loader: 'css-loader' },
32 | { loader: 'postcss-loader' },
33 | { loader: 'sass-loader' }
34 | ]
35 | },
36 | {
37 | test: /\.html$/,
38 | use: [
39 | { loader: 'file-loader', options: { name: '[name].html' } },
40 | { loader: 'extract-loader' },
41 | { loader: 'html-loader', options: { attrs: [ 'section:data-background-image', 'img:src' ] } }
42 | ]
43 | },
44 | {
45 | test: /\.(jpg|gif|png)$/,
46 | use: [
47 | { loader: 'file-loader', options: { name: 'assets/images/[name].[ext]' } }
48 | ]
49 | },
50 | {
51 | test: /\.(ttf|woff|woff2)$/,
52 | use: [
53 | { loader: 'file-loader', options: { name: 'assets/fonts/[name].[ext]' } }
54 | ]
55 | }
56 | ]
57 | },
58 | plugins: [
59 | new WriteFilePlugin(),
60 | new CopyWebpackPlugin([
61 | {
62 | from: path.resolve(__dirname, '..', 'node_modules', 'reveal.js', 'plugin'),
63 | to: path.resolve(__dirname, '..', 'dist', 'plugin')
64 | },
65 | {
66 | from: path.resolve(__dirname, '..', 'node_modules', 'reveal.js', 'lib', 'js', 'head.min.js'),
67 | to: path.resolve(__dirname, '..', 'dist', 'lib', 'js', 'head.min.js')
68 | },
69 | {
70 | from: path.resolve(__dirname, '..', 'node_modules', 'reveal.js', 'js', 'reveal.js'),
71 | to: path.resolve(__dirname, '..', 'dist', 'js', 'reveal.js')
72 | }
73 | ])
74 | ],
75 | devServer: {
76 | contentBase: 'dist',
77 | overlay: true
78 | }
79 | };
80 |
81 | module.exports = config;
82 |
--------------------------------------------------------------------------------
/src/examples/cool-stop-watch.lit-element.component.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, customElement, css, property, } from 'lit-element';
2 |
3 | @customElement('cool-stop-watch-lit-element')
4 | export class CoolStopWatch extends LitElement {
5 | static get styles() {
6 | return css`
7 | :host {
8 | border: 1px solid black;
9 | background-color: white;
10 | color: black;
11 | display: inline-block;
12 | padding: 10px;
13 | }
14 |
15 | button {
16 | border-radius: 5px;
17 | font-size: 16px;
18 | padding: 5px 10px;
19 | }
20 |
21 | #stop-watch-controls {
22 | text-align: center;
23 | }
24 | `
25 | }
26 |
27 | @property({ type: Number, reflect: false, attribute: 'current-time' }) private currentTime = 0;
28 | @property() private stopWatchInterval: any;
29 | private millisecondsInterval = 5;
30 |
31 | disconnectedCallback() {
32 | clearInterval(this.stopWatchInterval);
33 | }
34 |
35 | start = () => {
36 | if (this.stopWatchIsRunning) {
37 | return;
38 | }
39 |
40 | this.stopWatchInterval = setInterval(() => {
41 | this.currentTime = this.currentTime + this.millisecondsInterval;
42 | }, this.millisecondsInterval);
43 | }
44 |
45 | stop = () => {
46 | clearInterval(this.stopWatchInterval);
47 | this.stopWatchInterval = undefined;
48 | }
49 |
50 | reset = () => this.currentTime = 0;
51 |
52 | get formattedTime() {
53 | let minutesRaw = this.currentTime / 60000;
54 | let secondsRaw = (minutesRaw % 1) * 60;
55 | let millisecondsRaw = (secondsRaw % 1) * 1000;
56 |
57 | let minutes = Math.floor(minutesRaw);
58 | let seconds = Math.floor(secondsRaw).toString().padStart(2, '0');
59 | let milliseconds = Math.floor(millisecondsRaw).toString().padStart(3, '0');
60 |
61 | return `${minutes}:${seconds}.${milliseconds}`;
62 | }
63 |
64 | get stopWatchIsRunning() {
65 | return !!this.stopWatchInterval;
66 | }
67 |
68 | render = () => html`
69 | ${this.formattedTime}
70 |
71 | ${!this.stopWatchIsRunning
72 | ? html``
73 | : null
74 | }
75 | ${this.stopWatchIsRunning
76 | ? html``
77 | : null
78 | }
79 |
80 |
81 | `;
82 | }
83 |
--------------------------------------------------------------------------------
/src/examples/cool-tab.lit-html.component.js:
--------------------------------------------------------------------------------
1 | import { render, html } from 'lit-html';
2 |
3 | const componentStyles = `
4 | :host {
5 | --tab-button-border-radius: 5px;
6 | --tab-button-border-width: 3px;
7 | --tab-button-color: #6f7dbc;
8 |
9 | border: var(--tab-button-border-width) solid var(--tab-button-color);
10 | display: block;
11 | flex-grow: 1;
12 | overflow: none;
13 |
14 | background-color: #fff;
15 | color: var(--tab-button-color);
16 | cursor: pointer;
17 | font-size: 18px;
18 | padding: 10px;
19 | text-align: center;
20 | user-select: none
21 | transition: all 200ms ease-in-out;
22 | }
23 |
24 | :host([selected]) {
25 | background-color: var(--tab-button-color);
26 | color: #fff;
27 | transition: all 200ms ease-in-out;
28 | }
29 |
30 | :host(:not(:first-of-type)) {
31 | border-left-width: 0;
32 | }
33 |
34 | :host(:first-of-type) {
35 | border-top-left-radius: var(--tab-button-border-radius);
36 | border-bottom-left-radius: var(--tab-button-border-radius);
37 | }
38 |
39 | :host(:last-of-type) {
40 | border-top-right-radius: var(--tab-button-border-radius);
41 | border-bottom-right-radius: var(--tab-button-border-radius);
42 | }
43 | `;
44 |
45 | class CoolTab extends HTMLElement {
46 | constructor() {
47 | super();
48 |
49 | this._selected = false;
50 | this._value = '';
51 | }
52 |
53 | connectedCallback() {
54 | this.attachShadow({ mode: 'open' });
55 | this._value = this.getAttribute('value');
56 | this.clickListener = this.addEventListener('click', () => this.selected = true);
57 | render();
58 | }
59 |
60 | get value() {
61 | return this._value;
62 | }
63 |
64 | get selected() {
65 | return this._selected;
66 | }
67 |
68 | set selected(selected) {
69 | if (selected === this.selected) {
70 | return;
71 | }
72 |
73 | this._selected = selected;
74 | this.render();
75 |
76 | if (this.selected) {
77 | this.dispatchTabSelect();
78 | }
79 | }
80 |
81 | dispatchTabSelect() {
82 | this.dispatchEvent(new CustomEvent('tabselect', { bubbles: true }));
83 | }
84 |
85 | render() {
86 | if (this.selected) {
87 | this.setAttribute('selected', '');
88 | this.set
89 | } else {
90 | this.removeAttribute('selected');
91 | }
92 |
93 | render(html`
94 |
95 |
96 | `, this.shadowRoot);
97 | }
98 | }
99 |
100 | // window.customElements.define('cool-tab', CoolTab);
101 |
--------------------------------------------------------------------------------
/src/examples/cool-tab.vanilla.component.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | const style = `
3 | :host {
4 | --tab-button-border-radius: 5px;
5 | --tab-button-border-width: 3px;
6 | --tab-button-color: #6f7dbc;
7 |
8 | border: var(--tab-button-border-width) solid var(--tab-button-color);
9 | display: block;
10 | flex-grow: 1;
11 | overflow: none;
12 |
13 | background-color: #fff;
14 | color: var(--tab-button-color);
15 | cursor: pointer;
16 | font-size: 18px;
17 | padding: 10px;
18 | text-align: center;
19 | user-select: none
20 | transition: all 200ms ease-in-out;
21 | }
22 |
23 | :host([selected]) {
24 | background-color: var(--tab-button-color);
25 | color: #fff;
26 | transition: all 200ms ease-in-out;
27 | }
28 |
29 | :host(:not(:first-of-type)) {
30 | border-left-width: 0;
31 | }
32 |
33 | :host(:first-of-type) {
34 | border-top-left-radius: var(--tab-button-border-radius);
35 | border-bottom-left-radius: var(--tab-button-border-radius);
36 | }
37 |
38 | :host(:last-of-type) {
39 | border-top-right-radius: var(--tab-button-border-radius);
40 | border-bottom-right-radius: var(--tab-button-border-radius);
41 | }
42 | `;
43 |
44 | template.innerHTML = `
45 |
46 |
47 | `;
48 |
49 | class CoolTab extends HTMLElement {
50 | constructor() {
51 | super();
52 |
53 | this._selected = false;
54 | this._value = '';
55 | }
56 |
57 | connectedCallback() {
58 | const shadowRoot = this.attachShadow({ mode: 'open' });
59 | shadowRoot.appendChild(template.content.cloneNode(true));
60 |
61 | this._value = this.getAttribute('value');
62 | this.clickListener = this.addEventListener('click', () => this.selected = true);
63 | }
64 |
65 | get value() {
66 | return this._value;
67 | }
68 |
69 | get selected() {
70 | return this._selected;
71 | }
72 |
73 | set selected(selected) {
74 | if (selected === this.selected) {
75 | return;
76 | }
77 |
78 | this._selected = selected;
79 | this.render();
80 |
81 | if (this.selected) {
82 | this.dispatchTabSelect();
83 | }
84 | }
85 |
86 | dispatchTabSelect() {
87 | this.dispatchEvent(new CustomEvent('tabselect', { bubbles: true }));
88 | }
89 |
90 | render() {
91 | if (this.selected) {
92 | this.setAttribute('selected', '');
93 | this.set
94 | } else {
95 | this.removeAttribute('selected');
96 | }
97 | }
98 | }
99 |
100 | window.customElements.define('cool-tab', CoolTab);
101 |
--------------------------------------------------------------------------------
/src/examples/cool-stop-watch.lit-html.component.js:
--------------------------------------------------------------------------------
1 | import { html, render } from 'lit-html';
2 |
3 | const componentStyles = `
4 | :host {
5 | border: 1px solid black;
6 | background-color: white;
7 | color: black;
8 | display: inline-block;
9 | padding: 10px;
10 | }
11 |
12 | button {
13 | border-radius: 5px;
14 | font-size: 16px;
15 | padding: 5px 10px;
16 | }
17 |
18 | #stop-watch-controls {
19 | text-align: center;
20 | }
21 | `;
22 |
23 | class CoolStopWatch extends HTMLElement {
24 | connectedCallback() {
25 | this._currentTime = 0;
26 | this._stopWatchInterval;
27 | this._millisecondsInterval = 5;
28 |
29 | let initialTime = Number(this.getAttribute('current-time'));
30 | if (initialTime && !isNaN(initialTime)) {
31 | this._currentTime = initialTime;
32 | }
33 |
34 | this.attachShadow({ mode: 'open' });
35 | this.render();
36 | }
37 |
38 | disconnectedCallback() {
39 | clearInterval(this._stopWatchInterval);
40 | }
41 |
42 | start() {
43 | if (this.stopWatchIsRunning) {
44 | return;
45 | }
46 |
47 | this._stopWatchInterval = setInterval(() => {
48 | this.currentTime = this.currentTime + this._millisecondsInterval;
49 | }, this._millisecondsInterval);
50 | }
51 |
52 | stop() {
53 | clearInterval(this._stopWatchInterval);
54 | this._stopWatchInterval = undefined;
55 | this.render();
56 | }
57 |
58 | reset() {
59 | this.currentTime = 0;
60 | }
61 |
62 | get currentTime() {
63 | return this._currentTime;
64 | }
65 |
66 | set currentTime(value) {
67 | this._currentTime = value;
68 | this.render();
69 | }
70 |
71 | get formattedTime() {
72 | let minutes = this.currentTime / 60000;
73 | let seconds = (minutes % 1) * 60;
74 | let milliseconds = (seconds % 1) * 1000;
75 |
76 | minutes = Math.floor(minutes);
77 | seconds = Math.floor(seconds).toString().padStart(2, '0');
78 | milliseconds = Math.floor(milliseconds).toString().padStart(3, '0');
79 |
80 | return `${minutes}:${seconds}.${milliseconds}`;
81 | }
82 |
83 | get stopWatchIsRunning() {
84 | return !!this._stopWatchInterval;
85 | }
86 |
87 | render() {
88 | render(html`
89 |
90 |
91 | ${this.formattedTime}
92 |
93 | ${!this.stopWatchIsRunning
94 | ? html``
95 | : null
96 | }
97 | ${this.stopWatchIsRunning
98 | ? html``
99 | : null
100 | }
101 |
102 |
103 | `, this.shadowRoot);
104 | }
105 | }
106 |
107 | window.customElements.define('cool-stop-watch-lit-html', CoolStopWatch);
108 |
--------------------------------------------------------------------------------
/src/examples/cool-stop-watch.vanilla.component.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | const componentStyles = `
3 | :host {
4 | border: 1px solid black;
5 | background-color: white;
6 | color: black;
7 | display: inline-block;
8 | padding: 10px;
9 | }
10 |
11 | button {
12 | border-radius: 5px;
13 | font-size: 16px;
14 | padding: 5px 10px;
15 | }
16 |
17 | #stop-watch-controls {
18 | text-align: center;
19 | }
20 | `;
21 |
22 | const templateString = `
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | `;
32 | template.innerHTML = templateString;
33 |
34 | class CoolStopWatch extends HTMLElement {
35 | connectedCallback() {
36 | this._currentTime = 0;
37 | this._stopWatchInterval;
38 | this._millisecondsInterval = 5;
39 |
40 | let initialTime = Number(this.getAttribute('current-time'));
41 | if (initialTime && !isNaN(initialTime)) {
42 | this._currentTime = initialTime;
43 | }
44 |
45 | this.attachShadow({ mode: 'open' });
46 | this.shadowRoot.appendChild(template.content.cloneNode(true));
47 | this.$timeDisplay = this.shadowRoot.querySelector('#time');
48 | this.$startButton = this.shadowRoot.querySelector('#start');
49 | this.$stopButton = this.shadowRoot.querySelector('#stop');
50 | this.$resetButton = this.shadowRoot.querySelector('#reset');
51 |
52 | this.start = this.start.bind(this);
53 | this.stop = this.stop.bind(this);
54 | this.reset = this.reset.bind(this);
55 |
56 | this.$startButton.addEventListener('click', this.start);
57 | this.$stopButton.addEventListener('click', this.stop);
58 | this.$resetButton.addEventListener('click', this.reset);
59 |
60 | this.render();
61 | }
62 |
63 | disconnectedCallback() {
64 | clearInterval(this._stopWatchInterval);
65 | this.$startButton.removeEventListener('click', this.start);
66 | this.$stopButton.removeEventListener('click', this.stop);
67 | this.$resetButton.removeEventListener('click', this.reset);
68 | }
69 |
70 | start() {
71 | if (this.isStopWatchRunning) {
72 | return;
73 | }
74 |
75 | this._stopWatchInterval = setInterval(() => {
76 | this.currentTime = this.currentTime + this._millisecondsInterval;
77 | }, this._millisecondsInterval);
78 | }
79 |
80 | stop() {
81 | clearInterval(this._stopWatchInterval);
82 | this._stopWatchInterval = undefined;
83 | this.render();
84 | }
85 |
86 | reset() {
87 | this.currentTime = 0;
88 | }
89 |
90 | get currentTime() {
91 | return this._currentTime;
92 | }
93 |
94 | set currentTime(value) {
95 | this._currentTime = value;
96 | this.render();
97 | }
98 |
99 | get formattedTime() {
100 | let minutes = this.currentTime / 60000;
101 | let seconds = (minutes % 1) * 60;
102 | let milliseconds = (seconds % 1) * 1000;
103 |
104 | minutes = Math.floor(minutes);
105 | seconds = Math.floor(seconds).toString().padStart(2, '0');
106 | milliseconds = Math.floor(milliseconds).toString().padStart(3, '0');
107 |
108 | return `${minutes}:${seconds}.${milliseconds}`;
109 | }
110 |
111 | get isStopWatchRunning() {
112 | return !!this._stopWatchInterval;
113 | }
114 |
115 | render() {
116 | this.$timeDisplay.textContent = this.formattedTime;
117 |
118 | if (this.isStopWatchRunning) {
119 | this.$startButton.style.display = 'none';
120 | this.$stopButton.style.display = 'inline';
121 | } else {
122 | this.$startButton.style.display = 'inline';
123 | this.$stopButton.style.display = 'none';
124 | }
125 | }
126 | }
127 |
128 | window.customElements.define('cool-stop-watch', CoolStopWatch);
129 |
--------------------------------------------------------------------------------
/src/examples/cool-side-menu.component.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | const styles = `
3 | :host {
4 | --menu-primary-color: #f8981c;
5 | --menu-primary-contrast-color: #fff;
6 | --menu-width: 400px;
7 | --title-font-size: 1.5em;
8 | --menu-item-font-size: 1.2em;
9 | }
10 | .frame {
11 | position: fixed;
12 | top: 0;
13 | bottom: 0;
14 | width: 100%;
15 | overflow: hidden;
16 | pointer-events: none;
17 | z-index: 1000;
18 | transition: background-color 300ms ease-in;
19 | }
20 | .frame[open] {
21 | pointer-events: auto;
22 | background-color: rgba(0,0,0,0.25);
23 | }
24 | .frame[open] .container {
25 | transform: translate3d(0, 0, 0);
26 | }
27 | .container {
28 | width: var(--menu-width);
29 | background: #FFF;
30 | height: 100%;
31 | transform: translate3D(-100%, 0, 0);
32 | will-change: transform;
33 | transition: transform 300ms ease-in;
34 | box-shadow: 1px 0 3px rgba(51,51,51,0.25);
35 | }
36 | .title {
37 | display: flex;
38 | flex-direction: row;
39 | min-height: 3.2em;
40 | font-size: var(--title-font-size);
41 | background-color: var(--menu-primary-color);
42 | color: var(--menu-primary-contrast-color);
43 | }
44 | .title .title-content {
45 | flex-grow: 1;
46 | display: flex;
47 | align-items: center;
48 | padding-left: 1em;
49 | }
50 | .close {
51 | align-items: center;
52 | flex-basis: 100px;
53 | flex-grow: 0;
54 | flex-shrink: 0;
55 | cursor: pointer;
56 | display: flex;
57 | justify-content: center;
58 | user-select: none;
59 | }
60 | .menu-items::slotted(a) {
61 | display: block;
62 | font-size: var(--menu-item-font-size);
63 | text-decoration: none;
64 | line-height: 2.5em;
65 | padding: 0.5em;
66 | border-bottom: solid 1px #F1F1F1;
67 | color: #665;
68 | }
69 | .menu-items::slotted(a:hover) {
70 | color: var(--menu-primary-color);
71 | }
72 | :host([backdrop="false"]) .frame[open] {
73 | pointer-events: none;
74 | background-color: inherit;
75 | }
76 | :host([backdrop="false"]) .frame[open] .container {
77 | pointer-events: auto;
78 | }
79 | `;
80 | // #6f7dbc
81 | template.innerHTML = `
82 |
83 |
84 |
95 |
96 | `;
97 |
98 | class CoolSideMenuComponent extends HTMLElement {
99 | constructor() {
100 | super();
101 |
102 | this._$frame = null;
103 | this._isOpen = false;
104 | }
105 |
106 | connectedCallback() {
107 | const shadowRoot = this.attachShadow({ mode: 'open' });
108 | shadowRoot.appendChild(template.content.cloneNode(true));
109 |
110 | this._$frame = shadowRoot.querySelector('.frame');
111 | this._$frame.addEventListener('click', (event) => this.onCloseClick(event));
112 | }
113 |
114 | set isOpen(value) {
115 | const result = !!value;
116 | if (this._isOpen === result) {
117 | return;
118 | }
119 | this._isOpen = result;
120 | this.render();
121 | }
122 |
123 | get isOpen() {
124 | return this._isOpen;
125 | }
126 |
127 | open() {
128 | this.isOpen = true;
129 | }
130 |
131 | close() {
132 | this.isOpen = false;
133 | }
134 |
135 | onCloseClick(event) {
136 | if (event.target.dataset.close === 'true') {
137 | this.close();
138 | }
139 | }
140 |
141 | render() {
142 | if (this._$frame) {
143 | if (this.isOpen) {
144 | this._$frame.setAttribute('open', '');
145 | this.dispatchEvent(new CustomEvent('menuopen'));
146 | } else {
147 | this._$frame.removeAttribute('open');
148 | this.dispatchEvent(new CustomEvent('menuclose'));
149 | }
150 | }
151 | }
152 | }
153 |
154 | window.customElements.define('cool-side-menu', CoolSideMenuComponent);
155 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Intro to Web Components 📦
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
244 |
245 |
246 |
247 |
--------------------------------------------------------------------------------