@storybook-tiny/webcomponent 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,7 @@ document.getElementById('app').appendChild(storybook)
53
53
  Then run with vite:
54
54
 
55
55
  ```sh
56
- npx vite
56
+ npx vite --open /stories/
57
57
  ```
58
58
 
59
59
  # license
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'
5
5
 
6
6
  const config = {
7
7
  rootDir: fileURLToPath(new URL('..', import.meta.url)),
8
- devDependencies: ['@storybook-tiny/webcomponent', 'vite'],
8
+ devDependencies: ['@storybook-tiny/webcomponent', 'mi-element', 'vite'],
9
9
  files: ['stories/*', 'vite.config.js'],
10
10
  post: [
11
11
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook-tiny/webcomponent",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "A tiny storybook for webcompoments",
5
5
  "keywords": [
6
6
  "storybook",
@@ -33,6 +33,7 @@
33
33
  "vite.config.js"
34
34
  ],
35
35
  "dependencies": {
36
+ "mi-element": "^0.1.0",
36
37
  "@storybook-tiny/setup": "1.0.0"
37
38
  },
38
39
  "devDependencies": {
package/src/Storybook.js CHANGED
@@ -1,22 +1,21 @@
1
1
  import styles from './Storybook.module.css'
2
- import { WcElement, define, cssUnit } from './WcElement'
2
+ import { MiElement, define, refsBySelector, esc, refsById } from 'mi-element'
3
3
 
4
4
  const getLocationHash = () => decodeURIComponent(location.hash.substring(1))
5
5
 
6
- const template = `
7
- <main class="${styles.storybook}">
8
- <aside>
9
- <h4><a></a></h4>
10
- <div></div>
11
- </aside>
12
- <section class="stories">
13
- </section>
14
- </main>
15
- `
6
+ /**
7
+ * convert number to css unit
8
+ * @param {numbers|string} unit
9
+ * @returns {string}
10
+ */
11
+ const cssUnit = (unit) => {
12
+ const n = Number(unit)
13
+ return isNaN(n) ? unit : `${n}px`
14
+ }
16
15
 
17
16
  const defaultStory = `
18
17
  <p class="${styles.storybookSectionP}">
19
- The tiny storybook for<span> </span>
18
+ The tiny storybook for
20
19
  <a
21
20
  href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components"
22
21
  target="_blanc"
@@ -27,50 +26,71 @@ const defaultStory = `
27
26
  </p>
28
27
  `
29
28
 
30
- class Storybook extends WcElement {
31
- $ = {}
29
+ class Storybook extends MiElement {
32
30
  state = {}
33
31
 
34
- static attributes = {
35
- header: 'Storybook Tiny',
36
- href: '/stories/index.html',
37
- width: 130,
38
- stories: []
39
- }
32
+ static shadowRootOptions = null
40
33
 
41
- connectedCallback() {
42
- this.innerHTML = template
43
- const { $ } = this
44
- $.aside = this.querySelector('main > aside')
45
- $.h4 = $.aside.querySelector('h4 a')
46
- $.titles = $.aside.querySelector('div')
47
- $.story = this.querySelector('section')
48
- this.addEventListener(window, 'hashchange', () => this._renderStory())
49
- this.render()
34
+ static get attributes() {
35
+ return {
36
+ header: 'Storybook Tiny',
37
+ href: '/stories/index.html',
38
+ width: 130,
39
+ stories: []
40
+ }
50
41
  }
51
42
 
43
+ static template = `
44
+ <main class="${styles.storybook}">
45
+ <aside>
46
+ <h4><a></a></h4>
47
+ <nav></nav>
48
+ </aside>
49
+ <section class="stories">
50
+ </section>
51
+ </main>
52
+ `
53
+
52
54
  render() {
53
- const { $ } = this
54
- $.h4.textContent = this.header
55
- $.h4.href = this.href
56
- $.aside.style.flexBasis = cssUnit(this.width)
57
- $.titles.innerHTML = ''
55
+ this.refs = refsBySelector(this.renderRoot, {
56
+ aside: 'main > aside',
57
+ h4: 'main > aside h4 a',
58
+ nav: 'main > aside nav',
59
+ story: 'main > section'
60
+ })
61
+ this.on('hashchange', () => this._updateStory(), window)
62
+ }
63
+
64
+ update() {
65
+ const { refs } = this
66
+ refs.h4.textContent = this.header
67
+ refs.h4.href = this.href
68
+ refs.aside.style.flexBasis = cssUnit(this.width)
69
+ refs.nav.innerHTML = ''
58
70
  for (const story of this.stories) {
59
71
  const $el = document.createElement('storybook-tiny-story')
60
72
  $el.story = story
61
- $.titles.appendChild($el)
73
+ refs.nav.appendChild($el)
62
74
  }
63
- this._renderStory()
75
+ this._updateStory()
64
76
  }
65
77
 
66
- _renderStory() {
78
+ _updateStory() {
79
+ const { refs } = this
67
80
  const locHash = getLocationHash()
81
+
68
82
  if (this.state.title === locHash) {
69
83
  return
70
84
  }
71
85
 
86
+ // update active state on nav
87
+ for (const $el of refs.nav.childNodes) {
88
+ $el.active = $el.story?.title === locHash
89
+ }
90
+
72
91
  let renderStory = defaultStory
73
92
 
93
+ // find active story
74
94
  for (const story of this.stories) {
75
95
  if (typeof story === 'object') {
76
96
  const { title, component } = story
@@ -81,15 +101,15 @@ class Storybook extends WcElement {
81
101
  }
82
102
  }
83
103
 
84
- const { $ } = this
85
- $.story.innerHTML = ''
104
+ // try rendering the story
105
+ refs.story.innerHTML = ''
86
106
  try {
87
107
  switch (typeof renderStory) {
88
108
  case 'string':
89
- $.story.innerHTML = renderStory
109
+ refs.story.innerHTML = renderStory
90
110
  break
91
111
  case 'function':
92
- $.story.appendChild(renderStory())
112
+ refs.story.appendChild(renderStory())
93
113
  break
94
114
  default:
95
115
  throw new Error(
@@ -101,55 +121,66 @@ class Storybook extends WcElement {
101
121
  const error = document.createElement('storybook-tiny-error')
102
122
  error.message = err.message
103
123
  error.stack = err.stack
104
- $.story.appendChild(error)
124
+ refs.story.appendChild(error)
105
125
  }
106
126
  }
107
127
  }
108
128
 
109
129
  define('storybook-tiny', Storybook)
110
130
 
111
- class Story extends WcElement {
112
- connectedCallback() {
131
+ class Story extends MiElement {
132
+ static shadowRootOptions = null
133
+
134
+ static get attributes() {
135
+ return {
136
+ active: false,
137
+ story: ''
138
+ }
139
+ }
140
+
141
+ update() {
113
142
  if (typeof this.story === 'string') {
114
- this.innerHTML = this.story
143
+ this.renderRoot.innerHTML = this.story
115
144
  return
116
145
  }
117
146
 
118
- this.addEventListener(window, 'hashchange', () => this.render())
119
147
  const { title } = this.story
120
- this.innerHTML = `
121
- <div>
122
- <a
123
- href="#${title}"
124
- tabindex="0"
125
- role="button"
126
- >${title}</a>
127
- </div>
128
- `
129
- this.render()
130
- }
131
148
 
132
- render() {
133
- const { title } = this.story
134
- const locHash = getLocationHash()
135
- this.querySelector('a').className = locHash === title ? styles.active : ''
149
+ this.renderRoot.innerHTML = esc`
150
+ <div>
151
+ <a href="#${title}">${title}</a>
152
+ </div>
153
+ `
154
+ this.refs = { a: this.querySelector('a') }
155
+ this.refs.a.className = this.active ? styles.active : ''
136
156
  }
137
157
  }
138
158
 
139
159
  define('storybook-tiny-story', Story)
140
160
 
141
- class StoryError extends WcElement {
142
- static attributes = { message: '', stack: '' }
161
+ class StoryError extends MiElement {
162
+ static shadowRootOptions = null
163
+
164
+ static get attributes() {
165
+ return { message: '', stack: '' }
166
+ }
167
+
168
+ static template = `
169
+ <div class="${styles.error}">
170
+ <h2>Error</h2>
171
+ <p id="message"></p>
172
+ <p> </p>
173
+ <pre style="white-space: pre-wrap" id="stack"></pre>
174
+ </div>
175
+ `
143
176
 
144
177
  render() {
145
- this.innerHTML = `
146
- <div class="${styles.error}">
147
- <h2>Error</h2>
148
- <p>${this.message}</p>
149
- <p> </p>
150
- <pre style="white-space: pre-wrap">${this.stack}</pre>
151
- </div>
152
- `
178
+ this.refs = refsById(this.renderRoot)
179
+ }
180
+
181
+ update() {
182
+ this.refs.message.textContent = this.message
183
+ this.refs.stack.textContent = this.stack
153
184
  }
154
185
  }
155
186
 
@@ -1,12 +1,14 @@
1
+ import { refsBySelector } from "mi-element/refs"
2
+
1
3
  // define some custom elements
2
4
  window.customElements.define(
3
5
  'x-button',
4
6
  class extends HTMLElement {
5
7
  connectedCallback() {
6
- this.shadow = this.attachShadow({ mode: 'open' })
8
+ this.renderRoot = this.attachShadow({ mode: 'open' })
7
9
  this.$ = document.createElement('button')
8
10
  this.$.addEventListener('click', () => alert('Hi'))
9
- this.shadow.appendChild(this.$)
11
+ this.renderRoot.appendChild(this.$)
10
12
  this.$.textContent = this.childNodes?.[0]?.textContent || 'Click Me'
11
13
  }
12
14
  }
@@ -15,7 +17,6 @@ window.customElements.define(
15
17
  window.customElements.define(
16
18
  'x-counter',
17
19
  class extends HTMLElement {
18
- $ = {}
19
20
  init = 0
20
21
 
21
22
  static observedAttributes = ['init']
@@ -29,8 +30,8 @@ window.customElements.define(
29
30
  }
30
31
 
31
32
  connectedCallback() {
32
- this.shadow = this.attachShadow({ mode: 'closed' })
33
- this.shadow.innerHTML = `
33
+ this.renderRoot = this.attachShadow({ mode: 'closed' })
34
+ this.renderRoot.innerHTML = `
34
35
  <style>
35
36
  :host {
36
37
  font-size: 1.5em;
@@ -50,18 +51,21 @@ window.customElements.define(
50
51
  <div>
51
52
  <span class="count"></span>
52
53
  <span class="heart"></span>
53
- <button>👍</button>
54
- <button>👎</button>
54
+ <button id="up">👍</button>
55
+ <button id="down">👎</button>
55
56
  </div>
56
- `
57
- this.$.count = this.shadow.querySelector('.count')
58
- this.$.heart = this.shadow.querySelector('.heart')
59
- this.$.buttons = this.shadow.querySelectorAll('button')
60
- this.$.buttons[0].addEventListener('click', () => {
57
+ `
58
+ this.refs = refsBySelector(this.renderRoot, {
59
+ count: '.count',
60
+ heart: '.heart',
61
+ up: '#up',
62
+ down: '#down'
63
+ })
64
+ this.refs.up.addEventListener('click', () => {
61
65
  this.init += 1
62
66
  this.render()
63
67
  })
64
- this.$.buttons[1].addEventListener('click', () => {
68
+ this.refs.down.addEventListener('click', () => {
65
69
  this.init -= 1
66
70
  this.render()
67
71
  })
@@ -69,8 +73,8 @@ window.customElements.define(
69
73
  }
70
74
 
71
75
  render() {
72
- this.$.count.textContent = this.init
73
- this.$.heart.textContent =
76
+ this.refs.count.textContent = this.init
77
+ this.refs.heart.textContent =
74
78
  this.init === 0 ? '🤍' : this.init > 0 ? '❤️' : '💔'
75
79
  }
76
80
  }
@@ -86,8 +90,8 @@ export const storyCounter = {
86
90
  component: () => document.createElement('x-counter')
87
91
  }
88
92
  export const storyError = {
89
- title: 'Error',
93
+ title: 'Hello 🌍 Error',
90
94
  component: () => {
91
- throw new Error('baam')
95
+ throw new Error('Hello 🌍 Error')
92
96
  }
93
97
  }
package/src/WcElement.js DELETED
@@ -1,145 +0,0 @@
1
- /**
2
- * class extening HTMLElement to enable deferred rendering on attribute changes
3
- * either via `setAttribute(name, value)` or `this[name] = value`.
4
- * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
5
- * @example
6
- * class Example extends WcElement {
7
- * // internal references go here
8
- * $ = {}
9
- * // define all observed attributes.
10
- * // attributes are accessible via `this[prop]`
11
- * // avoid using attributes which are HTMLElement properties
12
- * static attributes = {
13
- * text: 'Hi'
14
- * }
15
- * // as with HTMLElement
16
- * connectedCallback() {
17
- * // do initial mount of component here
18
- * this._shadow = this.shadow = this.attachShadow({ mode: 'closed' })
19
- * this._shadow.innerHTML = `<div></div>`
20
- * this.$.div = this._shadow.querySelector('div')
21
- * this.render()
22
- * }
23
- * // render method called every time an attribute changes
24
- * render() {
25
- * this.$.div.textContent = this.text
26
- * }
27
- * }
28
- * // create a custom element with the `define` function (see below)
29
- * define('x-example', Example)
30
- * // create a DOM node and re-render via attribute or property changes
31
- * const elem = document.createElement('x-example')
32
- * elem.setAttribute('text', 'set attribute')
33
- * elem.text = 'set property'
34
- */
35
- export class WcElement extends HTMLElement {
36
- _attr = {}
37
- _removers = []
38
-
39
- constructor() {
40
- super()
41
- this._attr = structuredClone(this.constructor.attributes || {})
42
- observedAttributes(this)
43
- }
44
-
45
- /**
46
- * set attribute and re-render
47
- * @param {string} name
48
- * @param {any} value
49
- */
50
- setAttribute(name, value) {
51
- this._attr[name] = value
52
- this.isConnected && this.deferredRender()
53
- }
54
-
55
- /**
56
- * component must implement render()!
57
- */
58
- connectedCallback() {
59
- this.render()
60
- }
61
-
62
- /**
63
- * @param {string} name change attribute
64
- * @param {any} _oldValue
65
- * @param {any} newValue new value
66
- */
67
- attributeChangedCallback(name, _oldValue, newValue) {
68
- this._attr[name] = newValue
69
- this.deferredRender()
70
- }
71
-
72
- /**
73
- * helper function to remove event listeners when component gets disconnected
74
- * @param {HTMLElement} node
75
- * @param {string} event
76
- * @param {function} fn
77
- */
78
- addEventListener(node, event, fn) {
79
- node.addEventListener(event, fn)
80
- this._removers.push(() => node.removeEventListener(event, fn))
81
- }
82
-
83
- /**
84
- * remove possible event listeners
85
- */
86
- disconnectedCallback() {
87
- this._removers.forEach((remover) => remover())
88
- }
89
-
90
- /**
91
- * deferred render
92
- */
93
- deferredRender() {
94
- window.requestAnimationFrame(() => {
95
- this.render()
96
- })
97
- }
98
- }
99
-
100
- /**
101
- * re-render component when property changes
102
- * @param {HTMLElement} elem
103
- */
104
- const observedAttributes = (elem) => {
105
- for (const prop of Object.keys(elem._attr)) {
106
- Object.defineProperty(elem, prop, {
107
- get() {
108
- return elem._attr[prop]
109
- },
110
- set(newValue) {
111
- const oldValue = elem._attr[prop]
112
- if (oldValue === newValue) return
113
- elem._attr[prop] = newValue
114
- elem.isConnected && elem.deferredRender()
115
- }
116
- })
117
- }
118
- }
119
-
120
- /**
121
- * defines a custom element adding observedAttributes from default static
122
- * attributes
123
- * @param {string} name custom element tag
124
- * @param {HTMLElement} Element
125
- */
126
- export const define = (name, Element) => {
127
- Element.observedAttributes =
128
- Element.observedAttributes || Object.keys(Element.attributes || [])
129
- window.customElements.define(name, Element)
130
- }
131
-
132
- const toNumber = (n) => {
133
- const _n = Number(n)
134
- return !isNaN(_n) ? _n : undefined
135
- }
136
-
137
- /**
138
- * convert number to css unit
139
- * @param {numbers|string} unit
140
- * @returns {string}
141
- */
142
- export const cssUnit = (unit) => {
143
- const n = toNumber(unit)
144
- return typeof n === 'number' ? `${n}px` : unit
145
- }