<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>webdev &amp;mdash; Nat Knight</title>
    <link>http://natknight.xyz/tag:webdev</link>
    <description>Reflections, diversions, and opinions from a progressive ex-physicist programmer dad with a sore back.</description>
    <pubDate>Fri, 22 May 2026 14:38:29 -0700</pubDate>
    <item>
      <title>TIL: Copying to the clipboard from a webpage is easy now</title>
      <link>http://natknight.xyz/til-copying-to-the-clipboard-from-a-webpage-is-easy-now</link>
      <description>&lt;![CDATA[#til #webdev #javascript #clipboard&#xA;&#xA;I remember a time when there were whole libraries dedicated to clipboard management.&#xA;&#xA;Turns out in browsers released after ~2020 you can do it with one line:&#xA;&#xA;navigator.clipboard.writeText(text).then(console.log).catch(console.error);&#xA;&#xA;!--more--&#xA;&#xA;Some caveats:&#xA;&#xA;this only works in a secure context (which mostly means either &#34;a site served over HTTPS&#34; or localhost).&#xA;the user needs to have interacted with the page before you can actually call the method.&#xA;this was only added to certain browsers in 2020, so it&#39;s new enough that it might cause problems if your constituents have older devices or don&#39;t update often.&#xA;&#xA;Usually when I do this I&#39;m implementing something like a &#34;copy to clipboard&#34; button:&#xA;&#xA;span id=&#39;copy-target&#39;This text is going into a clipboard/span&#xA;button onclick=&#39;copyToClipboard(&#34;copy-target&#34;)&#39;Copy/button&#xA;script&#xA;    const copyToClipboard = (target) =  {&#xA;        const element = document.getElementById(target);&#xA;        const value = element?.innerText&#xA;        if (value) {&#xA;            navigator.clipboard.writeText(value).then(console.log).catch(console.error);&#xA;        }&#xA;    }&#xA;/script&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:til" class="hashtag"><span>#</span><span class="p-category">til</span></a> <a href="http://natknight.xyz/tag:webdev" class="hashtag"><span>#</span><span class="p-category">webdev</span></a> <a href="http://natknight.xyz/tag:javascript" class="hashtag"><span>#</span><span class="p-category">javascript</span></a> <a href="http://natknight.xyz/tag:clipboard" class="hashtag"><span>#</span><span class="p-category">clipboard</span></a></p>

<p>I remember a time when there were <a href="https://github.github.com/clipboard-copy-element/">whole</a> <a href="https://clipboardjs.com/">libraries</a> dedicated to clipboard management.</p>

<p>Turns out in <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/clipboard#browser_compatibility">browsers released after ~2020</a> you can do it with one line:</p>

<pre><code class="language-javascript">navigator.clipboard.writeText(text).then(console.log).catch(console.error);
</code></pre>



<p>Some caveats:</p>
<ul><li>this only works in a <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">secure context</a> (which mostly means either “a site served over HTTPS” or <code>localhost</code>).</li>
<li>the user needs to have interacted with the page before you can actually call the method.</li>
<li>this was only added to certain browsers in 2020, so it&#39;s new enough that it might cause problems if your constituents have older devices or don&#39;t update often.</li></ul>

<p>Usually when I do this I&#39;m implementing something like a “copy to clipboard” button:</p>

<pre><code class="language-html">&lt;span id=&#39;copy-target&#39;&gt;This text is going into a clipboard&lt;/span&gt;
&lt;button onclick=&#39;copyToClipboard(&#34;copy-target&#34;)&#39;&gt;Copy&lt;/button&gt;
&lt;script&gt;
    const copyToClipboard = (target) =&gt; {
        const element = document.getElementById(target);
        const value = element?.innerText
        if (value) {
            navigator.clipboard.writeText(value).then(console.log).catch(console.error);
        }
    }
&lt;/script&gt;
</code></pre>
]]></content:encoded>
      <guid>http://natknight.xyz/til-copying-to-the-clipboard-from-a-webpage-is-easy-now</guid>
      <pubDate>Mon, 10 Mar 2025 21:19:49 +0000</pubDate>
    </item>
    <item>
      <title>TIL: Accessing Content in Custom HTML Elements</title>
      <link>http://natknight.xyz/til-accessing-content-in-custom-html-elements</link>
      <description>&lt;![CDATA[#til #javascript #webdev #customelements&#xA;&#xA;I was working on a custom element today that replaces a textarea with CodeMirror in the UI while still updating the textarea in the background so that it can be submitted in a form. I ran across a wild footgun in custom elements.&#xA;&#xA;sStackOverflow eventually provided the solution./s There&#39;s a solution on stack overflow, but Danny Engleman wrote up a more thorough explanation.&#xA;&#xA;!--more--&#xA;&#xA;When you&#39;re creating a custom element you do most of your setup in a method called connectedCallback. Here you can read the content of the custom element, replace it with different elements, set up callback handlers, etc.&#xA;&#xA;Or so I thought.&#xA;&#xA;You can read the attributes of the node just fine, but if you try to read its contents you&#39;ll find they aren&#39;t there. The reason is that the connectedCallback fires when the opening tag of the custom element is parsed, so the contents aren&#39;t in the DOM yet.&#xA;&#xA;The solution is breathtakingly simple and silly: call setTimeout to defer initialization until after the DOM has been fully parsed.&#xA;&#xA;export class MyCustomElement extends HTMLElement {&#xA;  initialize () {&#xA;    // Fancy custom element stuff goes here; you can safely access&#xA;    // this.innerHTML and stuff.&#xA;  }&#xA;&#xA;  connectedCallback() {&#xA;    // Be sure to use an arrow function here or &#39;this&#39; will be&#xA;    // messed up in `initialize.&#xA;    setTimeout(() =  this.initialize(), 0)&#xA;  }&#xA;}&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:til" class="hashtag"><span>#</span><span class="p-category">til</span></a> <a href="http://natknight.xyz/tag:javascript" class="hashtag"><span>#</span><span class="p-category">javascript</span></a> <a href="http://natknight.xyz/tag:webdev" class="hashtag"><span>#</span><span class="p-category">webdev</span></a> <a href="http://natknight.xyz/tag:customelements" class="hashtag"><span>#</span><span class="p-category">customelements</span></a></p>

<p>I was working on a custom element today that replaces a <code>textarea</code> with <a href="https://codemirror.net/">CodeMirror</a> in the UI while still updating the <code>textarea</code> in the background so that it can be submitted in a form. I ran across a <em>wild</em> footgun in custom elements.</p>

<p><s>StackOverflow eventually provided <a href="https://stackoverflow.com/questions/64169068/obtain-this-textcontent-during-custom-element-construction">the solution</a>.</s> There&#39;s a solution on stack overflow, but Danny Engleman wrote up a <a href="https://dev.to/dannyengelman/web-component-developers-do-not-connect-with-the-connectedcallback-yet-4jo7">more thorough explanation</a>.</p>



<p>When you&#39;re creating a custom element you do most of your setup in a method called <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_a_custom_element"><code>connectedCallback</code></a>. Here you can read the content of the custom element, replace it with different elements, set up callback handlers, etc.</p>

<p><em>Or so I thought.</em></p>

<p>You can read the <em>attributes</em> of the node just fine, but if you try to read its <em>contents</em> you&#39;ll find they aren&#39;t there. The reason is that the <code>connectedCallback</code> fires when the <em>opening</em> tag of the custom element is parsed, so the contents aren&#39;t in the DOM yet.</p>

<p>The solution is breathtakingly simple and silly: call <code>setTimeout</code> to defer initialization until after the DOM has been fully parsed.</p>

<pre><code class="language-javascript">export class MyCustomElement extends HTMLElement {
  initialize () {
    // Fancy custom element stuff goes here; you can safely access
    // `this.innerHTML` and stuff.
  }

  connectedCallback() {
    // Be sure to use an arrow function here or &#39;this&#39; will be
    // messed up in `initialize.
    setTimeout(() =&gt; this.initialize(), 0)
  }
}
</code></pre>
]]></content:encoded>
      <guid>http://natknight.xyz/til-accessing-content-in-custom-html-elements</guid>
      <pubDate>Sun, 09 Mar 2025 21:59:00 +0000</pubDate>
    </item>
    <item>
      <title>Replace a textarea with CodeMirror in 30 lines of code and 30 minutes</title>
      <link>http://natknight.xyz/replace-a-textarea-with-codemirror-in-30-lines-of-code-and-30-minutes</link>
      <description>&lt;![CDATA[#codemirror #webdev #vanillajs #esbuild&#xA;&#xA;When you write an HTML form, [textarea] is the standard way to support multi-line input, but it&#39;s not suitable for every application. [CodeMirror] describes itself as a &#34;code editor component for the web&#34;, and might be a suitable replacement for a textarea if you want something more like a code editor.&#xA;&#xA;[textarea]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea&#xA;[CodeMirror]: https://codemirror.net/&#xA;&#xA;This article describes how to replace a textarea with a CodeMirror editor in under 30 lines of code and (hopefully) under 30 minutes of effort. It uses some very basic tooling to create an artifact that&#39;s no harder to deploy than vanilla JavaScript and HTML.&#xA;&#xA;You can also jump straight to the [source code] if you prefer.&#xA;&#xA;[source code]: https://github.com/nathanielknight/codemirror-in-30&#xA;&#xA;!--more--&#xA;&#xA;Tooling&#xA;&#xA;In the spirit of [A simple stack for today&#39;s web hacks] (which was part of the inspiration for this guide) we&#39;ll try to keep our tooling to a minimum:&#xA;&#xA;npm to get the pieces we&#39;ll need&#xA;[esbuild] to put the pieces together for us&#xA;&#xA;We&#39;ll even install esbuild using npm, so that&#39;s all you&#39;ll need in order to get started (there are many ways to install it; I happen to like [Volta]).&#xA;&#xA;[Volta]: https://volta.sh/&#xA;&#xA;[A simple stack for today&#39;s web hacks]: https://neugierig.org/software/blog/2022/12/simple-web-hacks.html&#xA;[esbuild]: https://esbuild.github.io/&#xA;&#xA;Dependencies&#xA;&#xA;In the project you want to add your CodeMirror editor to, run&#xA;&#xA;npm init&#xA;npm add codemirror&#xA;npm add --save-dev esbuild&#xA;&#xA;This will let you set up a very basic editor and bundle it so it&#39;s easy to deploy. If you want to add features (syntax highlighting, Vim or Emacs keybindings, integration with Vue or Svelte, etc.) there are many extensions (both [first party] and [community developed]). They can also be installed with npm add and added to the editor initialization script below. For this example, we&#39;ll add [markdown syntax highlighting]&#xA;&#xA;[first party]: https://codemirror.net/docs/extensions/&#xA;[community developed]: https://codemirror.net/docs/community/&#xA;[markdown syntax highlighting]: https://www.npmjs.com/package/@codemirror/lang-markdown&#xA;&#xA;npm add @codemirror/lang-markdown&#xA;&#xA;Code&#xA;&#xA;Once your packages are installed its time to set up the editor. Create a file with these contents (I called my main.js).&#xA;&#xA;import { basicSetup, EditorView } from &#34;codemirror&#34;&#xA;import { markdown } from &#34;@codemirror/lang-markdown&#34;&#xA;&#xA;// Hide the existing textarea&#xA;let textarea = document.querySelector(&#39;textarea&#39;);&#xA;textarea.style.display = &#34;none&#34;;&#xA;&#xA;// Create a CodeMirror editor with the textarea&#39;s contents&#xA;let view = new EditorView({&#xA;    doc: textarea.value,&#xA;    extensions: [&#xA;        basicSetup,&#xA;        markdown({}),&#xA;    ],&#xA;});&#xA;// Insert the editor into the document&#xA;textarea.insertAdjacentElement(&#34;afterend&#34;, view.dom);&#xA;&#xA;// When submitting the form, update the textarea with the editor&#39;s&#xA;// contents so that they&#39;re included with the form submission.&#xA;textarea.parentElement.onsubmit = function () {&#xA;    textarea.value = view.state.doc;&#xA;}&#xA;&#xA;This is what we&#39;re going to &#34;build&#34; with esbuild and eventually add to our web page.&#xA;&#xA;Build&#xA;&#xA;To create the final JavaScript file that we&#39;re going to include on our web page, run&#xA;&#xA;npm run esbuild --minify --bundle main.js --outfile=editor.js&#xA;&#xA;npm run is a helper command that executes programs installed with NPM, so really we&#39;re running esbuild --minify --bundle main.js --outfile=editor.js.&#xA;&#xA;esbuild will parse the JavaScript module we wrote, find the code it depends on (that we installed with npm) and pull it all together&#xA;the --minify flag tells esbuild to minify our code&#xA;the --bundle flag tells esbuild to concatenate all the packages (in our case, codemirror and any plugins we installed) into a single file&#xA;main.js is the name of our script&#xA;--outfile=editor.js tells esbuild where to put all this code once it&#39;s compiled&#xA;&#xA;Publish&#xA;&#xA;Finally, let&#39;s publish our editor. Here&#39;s a basic HTML file to load it:&#xA;&#xA;!DOCTYPE html&#xA;html lang=&#34;en&#34;&#xA;&#xA;head&#xA;    meta charset=&#34;UTF-8&#34;&#xA;    meta http-equiv=&#34;X-UA-Compatible&#34; content=&#34;IE=edge&#34;&#xA;    meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&#xA;    titleCodeMirror in 30/title&#xA;/head&#xA;&#xA;body&#xA;    form action=&#34;&#34;&#xA;        label for=&#34;editor&#34;Editor Input/label&#xA;        textarea name=&#34;editor&#34;/textarea&#xA;    /form&#xA;    script src=&#34;editor.js&#34;/script&#xA;    /form&#xA;/body&#xA;&#xA;/html&#xA;&#xA;This document has:&#xA;&#xA;a form with a textarea that we&#39;re going to submit&#xA;the editor.js script that esbuild built for us&#xA;&#xA;When the page is first loaded, we mark the textarea as hidden and append a CodeMirror editor to the form. As the form gets submitted, we copy the contents of the editor into the textarea so that it gets sent along with the rest of the form.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="http://natknight.xyz/tag:codemirror" class="hashtag"><span>#</span><span class="p-category">codemirror</span></a> <a href="http://natknight.xyz/tag:webdev" class="hashtag"><span>#</span><span class="p-category">webdev</span></a> <a href="http://natknight.xyz/tag:vanillajs" class="hashtag"><span>#</span><span class="p-category">vanillajs</span></a> <a href="http://natknight.xyz/tag:esbuild" class="hashtag"><span>#</span><span class="p-category">esbuild</span></a></p>

<p>When you write an HTML form, <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea"><code>textarea</code></a> is the standard way to support multi-line input, but it&#39;s not suitable for every application. <a href="https://codemirror.net/">CodeMirror</a> describes itself as a “code editor component for the web”, and might be a suitable replacement for a <code>textarea</code> if you want something more like a code editor.</p>

<p>This article describes how to replace a <code>textarea</code> with a CodeMirror editor in under 30 lines of code and (hopefully) under 30 minutes of effort. It uses some very basic tooling to create an artifact that&#39;s no harder to deploy than vanilla JavaScript and HTML.</p>

<p>You can also jump straight to the <a href="https://github.com/nathanielknight/codemirror-in-30">source code</a> if you prefer.</p>



<h2 id="tooling" id="tooling">Tooling</h2>

<p>In the spirit of <a href="https://neugierig.org/software/blog/2022/12/simple-web-hacks.html">A simple stack for today&#39;s web hacks</a> (which was part of the inspiration for this guide) we&#39;ll try to keep our tooling to a minimum:</p>
<ul><li><code>npm</code> to get the pieces we&#39;ll need</li>
<li><a href="https://esbuild.github.io/">esbuild</a> to put the pieces together for us</li></ul>

<p>We&#39;ll even install <code>esbuild</code> using <code>npm</code>, so that&#39;s all you&#39;ll need in order to get started (there are many ways to install it; I happen to like <a href="https://volta.sh/">Volta</a>).</p>

<h2 id="dependencies" id="dependencies">Dependencies</h2>

<p>In the project you want to add your CodeMirror editor to, run</p>

<pre><code class="language-shell">npm init
npm add codemirror
npm add --save-dev esbuild
</code></pre>

<p>This will let you set up a very basic editor and bundle it so it&#39;s easy to deploy. If you want to add features (syntax highlighting, Vim or Emacs keybindings, integration with Vue or Svelte, etc.) there are many extensions (both <a href="https://codemirror.net/docs/extensions/">first party</a> and <a href="https://codemirror.net/docs/community/">community developed</a>). They can also be installed with <code>npm add</code> and added to the editor initialization script below. For this example, we&#39;ll add <a href="https://www.npmjs.com/package/@codemirror/lang-markdown">markdown syntax highlighting</a></p>

<pre><code class="language-shell">npm add @codemirror/lang-markdown
</code></pre>

<h2 id="code" id="code">Code</h2>

<p>Once your packages are installed its time to set up the editor. Create a file with these contents (I called my <code>main.js</code>).</p>

<pre><code class="language-javascript">import { basicSetup, EditorView } from &#34;codemirror&#34;
import { markdown } from &#34;@codemirror/lang-markdown&#34;

// Hide the existing textarea
let textarea = document.querySelector(&#39;textarea&#39;);
textarea.style.display = &#34;none&#34;;

// Create a CodeMirror editor with the textarea&#39;s contents
let view = new EditorView({
    doc: textarea.value,
    extensions: [
        basicSetup,
        markdown({}),
    ],
});
// Insert the editor into the document
textarea.insertAdjacentElement(&#34;afterend&#34;, view.dom);

// When submitting the form, update the textarea with the editor&#39;s
// contents so that they&#39;re included with the form submission.
textarea.parentElement.onsubmit = function () {
    textarea.value = view.state.doc;
}
</code></pre>

<p>This is what we&#39;re going to “build” with <code>esbuild</code> and eventually add to our web page.</p>

<h2 id="build" id="build">Build</h2>

<p>To create the final JavaScript file that we&#39;re going to include on our web page, run</p>

<pre><code class="language-shell">npm run esbuild --minify --bundle main.js --outfile=editor.js
</code></pre>

<p><code>npm run</code> is a helper command that executes programs installed with NPM, so really we&#39;re running <code>esbuild --minify --bundle main.js --outfile=editor.js</code>.</p>
<ul><li><code>esbuild</code> will parse the JavaScript module we wrote, find the code it depends on (that we installed with <code>npm</code>) and pull it all together</li>
<li>the <code>--minify</code> flag tells <code>esbuild</code> to minify our code</li>
<li>the <code>--bundle</code> flag tells <code>esbuild</code> to concatenate all the packages (in our case, <code>codemirror</code> and any plugins we installed) into a single file</li>
<li><code>main.js</code> is the name of our script</li>
<li><code>--outfile=editor.js</code> tells <code>esbuild</code> where to put all this code once it&#39;s compiled</li></ul>

<h2 id="publish" id="publish">Publish</h2>

<p>Finally, let&#39;s publish our editor. Here&#39;s a basic HTML file to load it:</p>

<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;

&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta http-equiv=&#34;X-UA-Compatible&#34; content=&#34;IE=edge&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;CodeMirror in 30&lt;/title&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;form action=&#34;&#34;&gt;
        &lt;label for=&#34;editor&#34;&gt;Editor Input&lt;/label&gt;
        &lt;textarea name=&#34;editor&#34;&gt;&lt;/textarea&gt;
    &lt;/form&gt;
    &lt;script src=&#34;editor.js&#34;&gt;&lt;/script&gt;
    &lt;/form&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre>

<p>This document has:</p>
<ul><li>a form with a textarea that we&#39;re going to submit</li>
<li>the <code>editor.js</code> script that <code>esbuild</code> built for us</li></ul>

<p>When the page is first loaded, we mark the <code>textarea</code> as hidden and append a CodeMirror editor to the form. As the form gets submitted, we copy the contents of the editor into the <code>textarea</code> so that it gets sent along with the rest of the form.</p>
]]></content:encoded>
      <guid>http://natknight.xyz/replace-a-textarea-with-codemirror-in-30-lines-of-code-and-30-minutes</guid>
      <pubDate>Sat, 07 Jan 2023 08:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>