|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- <!doctype html><!-- This is a valid HTML5 document. -->
- <!-- Screen readers, SEO, extensions and so on. -->
- <html lang="fr">
- <!-- Has to be within the first 1024 bytes, hence before the `title` element
- See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
- <meta charset="utf-8">
- <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
- <!-- The viewport meta is quite crowded and we are responsible for that.
- See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <!-- Required to make a valid HTML5 document. -->
- <title>Web Components as Progressive Enhancement (archive) — David Larlet</title>
- <meta name="description" content="Publication mise en cache pour en conserver une trace.">
- <!-- That good ol' feed, subscribe :). -->
- <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
- <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
- <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
- <link rel="manifest" href="/static/david/icons2/site.webmanifest">
- <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
- <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
- <meta name="msapplication-TileColor" content="#f7f7f7">
- <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
- <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
- <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
- <!-- Documented, feel free to shoot an email. -->
- <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
- <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
- <script>
- function toggleTheme(themeName) {
- document.documentElement.classList.toggle(
- 'forced-dark',
- themeName === 'dark'
- )
- document.documentElement.classList.toggle(
- 'forced-light',
- themeName === 'light'
- )
- }
- const selectedTheme = localStorage.getItem('theme')
- if (selectedTheme !== 'undefined') {
- toggleTheme(selectedTheme)
- }
- </script>
-
- <meta name="robots" content="noindex, nofollow">
- <meta content="origin-when-cross-origin" name="referrer">
- <!-- Canonical URL for SEO purposes -->
- <link rel="canonical" href="https://cloudfour.com/thinks/web-components-as-progressive-enhancement/">
-
- <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
-
-
- <article>
- <header>
- <h1>Web Components as Progressive Enhancement</h1>
- </header>
- <nav>
- <p class="center">
- <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
- </svg> Accueil</a> •
- <a href="https://cloudfour.com/thinks/web-components-as-progressive-enhancement/" title="Lien vers le contenu original">Source originale</a>
- </p>
- </nav>
- <hr>
- <div class="u-bgGray u-pad1 u-pullSides1 u-spaceItems1 u-textGrow1"><p>Web components are a powerful tool for creating custom HTML elements, but you can run into challenges if you try to replace existing elements with web components. To get the best of both worlds, try wrapping existing elements in web components instead.</p></div>
- <p>I learned this the hard way… On a few recent projects we’ve wanted to create an auto-expanding <code>textarea</code>: When a user types its height would increase so that its content is never clipped.</p>
- <p>I was tired of rewriting this behavior across projects and frameworks and began thinking through how it could be written as a reusable web component. My first draft of an API looked like this:</p>
- <pre><code class="language-html"><elastic-textarea name="textarea-name" id="textarea-id" rows="4">
- Here is my textarea content
- </elastic-textarea>
- </code></pre>
- <p>This is pretty sweet. Developers can use this exactly like the <code>textarea</code> they’re already used to! But, as I was testing this out I noticed a couple of major downsides:</p>
- <ol>
- <li>Web components require JavaScript to render <sup id="fnref-6408-1"><a href="#fn-6408-1" class="jetpack-footnote" title="Read footnote.">1</a></sup>. This means that until the JavaScript is downloaded, parsed, and run, no <code>textarea</code> is displayed. Instead “Here is my textarea content” is displayed as an unstyled string. If a user has JavaScript disabled, or the JavaScript fails to load, the textarea just won’t work.</li>
- <li>The <code>textarea</code> element has its own complex behavior, APIs, and accessibility behaviors. I’d need to recreate a lot of this from scratch for my component. I’d need to apply the <code>name</code>, <code>id</code>, and <code>rows</code> to the actual <code>textarea</code> element that my JavaScript renders. I’d also need to add all of the <code>textarea</code> JavaScript APIs to my custom element (<code>.value</code>, <code>.is-valid</code>, etc.) <sup id="fnref-6408-2"><a href="#fn-6408-2" class="jetpack-footnote" title="Read footnote.">2</a></sup></li>
- </ol>
- <div class="HashHeading HashHeading--h2">
- <h2 id="the-solution">The Solution</h2>
- <a href="#the-solution"
- aria-label="Permalink for The Solution"
- class="HashHeading-link">
- <svg viewBox="0 0 24 24" width="24" height="24" class="Icon" role="presentation"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M13.77,10.09l-0.71-.71a4,4,0,0,0-5.65,0L3.16,13.63a4,4,0,0,0,0,5.66l1.4,1.4a4,4,0,0,0,5.67,0l1.41-1.41"/><path d="M10.23,13.62l0.71,0.71a4,4,0,0,0,5.65,0l4.25-4.25a4,4,0,0,0,0-5.66L19.43,3a4,4,0,0,0-5.67,0L12.35,4.43"/></g></svg>
-
- </a>
- </div>
-
- <p>We can avoid both of these drawbacks by wrapping and enhancing an existing element instead of replacing it. Here’s what that looks like:</p>
- <pre><code class="language-html"><elastic-textarea>
- <textarea name="textarea-name" id="textarea-id" rows="4">
- Here is my textarea content
- </textarea>
- </elastic-textarea>
- </code></pre>
- <p>This is a little more verbose, but it gets me the best of both worlds. Before JavaScript loads I have a fully functioning <code>textarea</code>. After JavaScript loads my <code>textarea</code> is progressively enhanced with additional functionality.</p>
- <p>It could get a little tedious to wrap every <code>textarea</code> on the page. To avoid this we could set the component up so it can wrap multiple <code>textarea</code>s and enhance all of them at once. This would allow you to wrap a whole form or page to enable this behavior:</p>
- <pre><code class="language-html"><elastic-textarea>
- <textarea name="textarea-name" id="textarea-id" rows="2">
- Here is my textarea content
- </textarea>
-
- <textarea name="textarea-2" id="textarea-id-2" rows="5">
- Here is another textarea
- </textarea>
- </elastic-textarea>
- </code></pre>
- <p>You can play with the finished component below:</p>
- <figure class="Figure" style="text-align: center;">
- <script type="module" src="https://unpkg.com/@cloudfour/elastic-textarea/index.min.js"></script><br />
- <elastic-textarea><br />
- <label for="textarea-id">Type in the textarea below to watch it expand and contract.</label><br />
- <textarea name="textarea-name" id="textarea-id" style="resize: horizontal;width:100%;margin-top: 1em;"></textarea><br />
- </elastic-textarea><figcaption class="Figure-caption">
- We released this as an <a href="https://www.npmjs.com/package/@cloudfour/elastic-textarea">open source component</a> you can use in your projects. Most of the JavaScript logic was borrowed from an excellent implementation my colleague <a href="https://cloudfour.com/is/scott">Scott Vandehey</a> wrote for a previous project.<br />
- </figcaption></figure>
- <div class="HashHeading HashHeading--h2">
- <h2 id="the-skys-the-limit">The Sky’s the Limit</h2>
- <a href="#the-skys-the-limit"
- aria-label="Permalink for The Sky’s the Limit"
- class="HashHeading-link">
- <svg viewBox="0 0 24 24" width="24" height="24" class="Icon" role="presentation"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M13.77,10.09l-0.71-.71a4,4,0,0,0-5.65,0L3.16,13.63a4,4,0,0,0,0,5.66l1.4,1.4a4,4,0,0,0,5.67,0l1.41-1.41"/><path d="M10.23,13.62l0.71,0.71a4,4,0,0,0,5.65,0l4.25-4.25a4,4,0,0,0,0-5.66L19.43,3a4,4,0,0,0-5.67,0L12.35,4.43"/></g></svg>
-
- </a>
- </div>
-
- <p>The <code>elastic-textarea</code> component is a single example of how web components can be used for progressive enhancement, but there are tons of other potential use cases for this technique. Here are a couple other examples to check out for inspiration:</p>
- <ul>
- <li><a href="https://www.zachleat.com/">Zach Leatherman</a> created an <em>excellent</em> <a href="https://www.zachleat.com/web/details-utils/"><code>details-utils</code></a> component that wraps and progressively enhances the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"><code>details</code> element</a>. (This was a big source of inspiration for my approach here.)</li>
- <li>I used this same technique to create <a href="https://cloudfour.com/thinks/building-an-accessible-image-comparison-web-component/">a progressively enhanced image comparison component</a>.</li>
- </ul>
- <div class="HashHeading HashHeading--h2">
- <h2 id="sharing-is-caring">Sharing is Caring</h2>
- <a href="#sharing-is-caring"
- aria-label="Permalink for Sharing is Caring"
- class="HashHeading-link">
- <svg viewBox="0 0 24 24" width="24" height="24" class="Icon" role="presentation"><g fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M13.77,10.09l-0.71-.71a4,4,0,0,0-5.65,0L3.16,13.63a4,4,0,0,0,0,5.66l1.4,1.4a4,4,0,0,0,5.67,0l1.41-1.41"/><path d="M10.23,13.62l0.71,0.71a4,4,0,0,0,5.65,0l4.25-4.25a4,4,0,0,0,0-5.66L19.43,3a4,4,0,0,0-5.67,0L12.35,4.43"/></g></svg>
-
- </a>
- </div>
-
- <p>I’m a big fan of this strategy and I’m excited to create and share more progressively enhanced components. At Cloud Four we work on a lot of different projects for a lot of different clients, and I’ve found myself rewriting the same functionality across several projects. Next time I catch myself rewriting the same component logic for the third or fourth time I’m going to make it a web component so I don’t have to write it a fifth time.</p>
- <p>By packaging these chunks of interactive logic as custom elements, we can make them easy to share and reuse across projects and frameworks. By enhancing native HTML instead of replacing it, we can provide a solid baseline experience, and add progressive enhancement as the cherry on top.</p>
- <div class="footnotes">
- <hr />
- <ol>
- <li id="fn-6408-1">
- There are various libraries that allow server side rendering of web components, but they usually require you to buy into a specific web component framework, and/or modify your build steps. These are useful tools for individuals projects, but don’t work as well for creating easily shareable components. <a href="#fnref-6408-1" title="Return to main content."><svg viewBox="0 0 24 24" width="24" height="24" class="Icon" role="presentation"><g fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M4,14H16a5.76,5.76,0,0,0,6-6,5.76,5.76,0,0,0-6-6"/><polyline points="8 8 2 14 8 20"/></g></svg>
- </a>
- </li>
- <li id="fn-6408-2">
- There’s a spec to allow <a href="https://web.dev/custom-elements-v1/#extending-native-html-elements">extending existing elements</a> but it looks unlikely that Safari will adopt it. On some projects, <a href="https://cloudfour.com/thinks/mighty-morphin-web-components/">recreating element APIs can have a big payoff</a> but it’s a lot of work if you’re just trying to add a little functionality to an existing element. <a href="#fnref-6408-2" title="Return to main content."><svg viewBox="0 0 24 24" width="24" height="24" class="Icon" role="presentation"><g fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M4,14H16a5.76,5.76,0,0,0,6-6,5.76,5.76,0,0,0-6-6"/><polyline points="8 8 2 14 8 20"/></g></svg>
- </a>
- </li>
- </ol>
- </div>
- </article>
-
-
- <hr>
-
- <footer>
- <p>
- <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
- </svg> Accueil</a> •
- <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
- </svg> Suivre</a> •
- <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
- </svg> Pro</a> •
- <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
- </svg> Email</a> •
- <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
- </svg> Légal</abbr>
- </p>
- <template id="theme-selector">
- <form>
- <fieldset>
- <legend><svg class="icon icon-brightness-contrast">
- <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
- </svg> Thème</legend>
- <label>
- <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
- </label>
- <label>
- <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
- </label>
- <label>
- <input type="radio" value="light" name="chosen-color-scheme"> Clair
- </label>
- </fieldset>
- </form>
- </template>
- </footer>
- <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
- <script>
- function loadThemeForm(templateName) {
- const themeSelectorTemplate = document.querySelector(templateName)
- const form = themeSelectorTemplate.content.firstElementChild
- themeSelectorTemplate.replaceWith(form)
-
- form.addEventListener('change', (e) => {
- const chosenColorScheme = e.target.value
- localStorage.setItem('theme', chosenColorScheme)
- toggleTheme(chosenColorScheme)
- })
-
- const selectedTheme = localStorage.getItem('theme')
- if (selectedTheme && selectedTheme !== 'undefined') {
- form.querySelector(`[value="${selectedTheme}"]`).checked = true
- }
- }
-
- const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
- window.addEventListener('load', () => {
- let hasDarkRules = false
- for (const styleSheet of Array.from(document.styleSheets)) {
- let mediaRules = []
- for (const cssRule of styleSheet.cssRules) {
- if (cssRule.type !== CSSRule.MEDIA_RULE) {
- continue
- }
- // WARNING: Safari does not have/supports `conditionText`.
- if (cssRule.conditionText) {
- if (cssRule.conditionText !== prefersColorSchemeDark) {
- continue
- }
- } else {
- if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
- continue
- }
- }
- mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
- }
-
- // WARNING: do not try to insert a Rule to a styleSheet you are
- // currently iterating on, otherwise the browser will be stuck
- // in a infinite loop…
- for (const mediaRule of mediaRules) {
- styleSheet.insertRule(mediaRule.cssText)
- hasDarkRules = true
- }
- }
- if (hasDarkRules) {
- loadThemeForm('#theme-selector')
- }
- })
- </script>
- </body>
- </html>
|