From 7b19c9dc7553912a041e3a9f43f4adf405fa3659 Mon Sep 17 00:00:00 2001 From: Jake Lazaroff Date: Fri, 5 Jan 2024 15:51:46 -0500 Subject: [PATCH] Dispatch to todayilearned on push --- .github/workflows/build.yml | 160 ------------------ .github/workflows/deploy.yml | 16 ++ .github/workflows/readme.yml | 81 ++++++++++ rss.xml | 306 ----------------------------------- 4 files changed, 97 insertions(+), 466 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/readme.yml delete mode 100644 rss.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 607af1e..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,160 +0,0 @@ -# This script creates a nice README.md with an index of all TIL files. -# TIL files are Markdown files named anything other than README.md. -# -# The readme is split into two sections: the "header" and the "index", -# separated by three hyphens on their own line. -# Anything above the three hyphens is the "header" and will be kept as is. -# Anything below will be replaced by the "index". - -name: Build README - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Check out repo - uses: actions/checkout@v3 - with: - fetch-depth: 0 # get full history or else it'll be overwritten - - - name: Regenerate README - uses: actions/github-script@v6 - with: - script: | - const { readFile, writeFile } = require("node:fs/promises"); - - console.log("Building index…"); - - // load readme - let readme = await readFile("README.md").then(file => file.toString()); - - // add header separator - const separator = "\n---\n"; - const index = readme.indexOf(separator); - if (index === -1) readme += separator; - else readme = readme.substring(0, index + separator.length); - - // collect entries - const files = await glob.create("./**/*.md").then(globber => globber.glob()); - const entries = files - .filter(name => !name.endsWith("/README.md")) // exclude README.md - .sort() - .map(name => name.split("/").slice(-2)); - - // add summary - readme += `\n${entries.length} TILs so far:\n`; - - // create category map - const categories = new Map(); - for (const [category, file] of entries) { - const list = categories.get(category) || []; - categories.set(category, [...list, file]); - } - - // create a section for each category - for (const [category, entries] of categories.entries()) { - // write category header - readme += `\n## ${category}\n\n`; - - // write link for each file - for (const file of entries) { - const filepath = [category, file].join("/"); - const contents = await readFile(filepath).then(file => file.toString()); - const [, title] = contents.match(/^# (.+)$/m); - readme += `- [${title}](/${filepath})\n`; - } - } - - // write readme - await writeFile("README.md", readme); - - - name: Install NPM libraries - run: npm install marked he - - - name: Generate RSS - uses: actions/github-script@v6 - with: - script: | - const { readFile, writeFile, stat } = require("node:fs/promises"); - const { promisify } = require("node:util"); - const run = promisify(require("node:child_process").exec); - - const marked = require("marked").marked; - const he = require("he"); - - console.log("Building feed…"); - - // collect entries - const files = await glob.create("./**/*.md").then(globber => globber.glob()); - const modified = files - .filter(name => !name.includes("/node_modules/")) // exclude node_modules - .filter(name => !name.endsWith("/README.md")) // exclude README.md - .map(name => name.split("/").slice(-2).join("/")) - .map(async file => [file, await run("git log --follow --format=%ad --date=unix " + file)]); - - const entries = await Promise.all(modified); - const sorted = entries - .map(([name, created]) => [name, created.stdout.trim().split("\n").at(-1)]) - .sort(([, a], [, b]) => b.localeCompare(a)) - .slice(0, 10) - .map(async ([name, date]) => ({ - name, - date: new Date(Number(date) * 1000), - content: await readFile(name).then(buf => buf.toString()) - })); - const feed = await Promise.all(sorted); - - const user = context.repo.owner; - const repo = context.repo.repo; - const branch = context.payload.repository.default_branch; - - const link = `https://github.com/${user}/${repo}`; - - const rss = []; - rss.push(``); - rss.push(``); - rss.push(``); - rss.push(`${user} TIL`); - rss.push(`A collection of useful things I've learned.`); - rss.push(`${link}`); - - const dateOptions = { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: "short" }; - for (const item of feed) { - rss.push(``); - - const itemLink = `${link}/blob/${branch}/${item.name}`; - rss.push(`${itemLink}`); - rss.push(`${itemLink}`); - - const category = item.name.split("/")[0]; - const [, title] = item.content.match(/^# (.+)$/m); - rss.push(`TIL (${category}): ${he.escape(title)}`); - - const pubDate = new Intl.DateTimeFormat("en-US", dateOptions).format(item.date); - rss.push(`${item.date.toUTCString()}`); - - const content = item.content.split("\n").slice(1).join("\n").trim(); - const encoded = he.escape(marked(content)); - rss.push(`${encoded}`); - - rss.push(``); - } - - rss.push(``); - rss.push(``); - - - // write feed - await writeFile("rss.xml", rss.join("\n")); - - - name: Commit and push if README or RSS changed - run: |- - git config --global user.email "actions@users.noreply.github.com" - git config --global user.name "tilbot" - git diff --quiet || git commit --all --message "Update README.md or rss.xml" - git push diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..11ea2b3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,16 @@ +name: Deploy +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Dispatch to workflows + run: | + curl -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token ${{ secrets.PERSONAL_ACCESS_TOKEN }}" \ + --request POST \ + --data '{ "event_type": "deploy" }' https://api.github.com/repos/jakelazaroff/todayilearned/dispatches diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml new file mode 100644 index 0000000..0697230 --- /dev/null +++ b/.github/workflows/readme.yml @@ -0,0 +1,81 @@ +# This script creates a nice README.md with an index of all TIL files. +# TIL files are Markdown files named anything other than README.md. +# +# The readme is split into two sections: the "header" and the "index", +# separated by three hyphens on their own line. +# Anything above the three hyphens is the "header" and will be kept as is. +# Anything below will be replaced by the "index". + +name: Build README + +on: + push: + branches: + - main + +jobs: + readme: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 # get full history or else it'll be overwritten + + - name: Regenerate README + uses: actions/github-script@v6 + with: + script: | + const { readFile, writeFile } = require("node:fs/promises"); + + console.log("Building index…"); + + // load readme + let readme = await readFile("README.md").then(file => file.toString()); + + // add header separator + const separator = "\n---\n"; + const index = readme.indexOf(separator); + if (index === -1) readme += separator; + else readme = readme.substring(0, index + separator.length); + + // collect entries + const files = await glob.create("./**/*.md").then(globber => globber.glob()); + const entries = files + .filter(name => !name.endsWith("/README.md")) // exclude README.md + .sort() + .map(name => name.split("/").slice(-2)); + + // add summary + readme += `\n${entries.length} TILs so far:\n`; + + // create category map + const categories = new Map(); + for (const [category, file] of entries) { + const list = categories.get(category) || []; + categories.set(category, [...list, file]); + } + + // create a section for each category + for (const [category, entries] of categories.entries()) { + // write category header + readme += `\n## ${category}\n\n`; + + // write link for each file + for (const file of entries) { + const filepath = [category, file].join("/"); + const contents = await readFile(filepath).then(file => file.toString()); + const [, title] = contents.match(/^# (.+)$/m); + readme += `- [${title}](/${filepath})\n`; + } + } + + // write readme + await writeFile("README.md", readme); + + - name: Commit and push if README changed + run: |- + git config --global user.email "actions@users.noreply.github.com" + git config --global user.name "tilbot" + git diff --quiet || git commit --all --message "Update README.md" + git push diff --git a/rss.xml b/rss.xml deleted file mode 100644 index fff41ad..0000000 --- a/rss.xml +++ /dev/null @@ -1,306 +0,0 @@ - - - -jakelazaroff TIL -A collection of useful things I've learned. -https://github.com/jakelazaroff/til - -https://github.com/jakelazaroff/til/blob/main/css/use-grid-template-to-set-grid-columns-rows-and-areas.md -https://github.com/jakelazaroff/til/blob/main/css/use-grid-template-to-set-grid-columns-rows-and-areas.md -TIL (css): Use `grid-template` to set grid columns, rows and areas -Sun, 31 Dec 2023 16:01:17 GMT -<p>I&#39;m always forgetting how the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template">grid-template</a> shorthand works, so here&#39;s a quick reference.</p> -<p>The simplest usage is just as a shorthand for <code>grid-rows</code> and <code>grid-columns</code>:</p> -<pre><code class="language-css">grid-template: 2rem 1fr / 20rem 1fr; -</code></pre> -<p>Before the slash is rows; after the slash is columns. It&#39;s equivalent to this longhand:</p> -<pre><code class="language-css">grid-template-rows: 2rem 1fr; -grid-template-columns 20rem 1fr; -</code></pre> -<p>Things get a little more interesting when you also use it as a shorthand for <code>grid-template-areas</code>:</p> -<pre><code class="language-css">grid-template: - &quot;toolbar toolbar&quot; 2rem - &quot;sidebar content&quot; 1fr - / 20rem 1fr; -</code></pre> -<p>Each line before the slash contains the grid area names and height of a row; after the slash contains the column sizes. It&#39;s equivalent to this longhand:</p> -<pre><code class="language-css">grid-template-rows: 2rem 1fr; -grid-template-columns: 20rem 1fr; -grid-template-areas: - &quot;toolbar toolbar&quot; - &quot;sidebar content&quot;; -</code></pre> - - - -https://github.com/jakelazaroff/til/blob/main/rust/link-against-a-cpp-file.md -https://github.com/jakelazaroff/til/blob/main/rust/link-against-a-cpp-file.md -TIL (rust): Link against a C++ file -Tue, 26 Dec 2023 06:28:28 GMT -<p>While Rust natively supports linking against C, it needs an extra binding layer in order to link against C++. Although a library called <a href="https://rust-lang.github.io/rust-bindgen/introduction.html">bindgen</a> can generate those bindings automatically, I wanted to see how to do it myself.</p> -<p>Rust has extensive documentation on writing a <a href="https://doc.rust-lang.org/nomicon/ffi.html">foreign function interace</a>; here&#39;s a minimal example of how to do it.</p> -<p>Let&#39;s say we want to call this function in <code>lib/inc.cpp</code> from Rust:</p> -<pre><code class="language-cpp">#include &quot;inc.h&quot; - -int twice(int x) { return x * 2; } -</code></pre> -<p>In the header file <code>lib/inc.h</code>, <code>extern &quot;C&quot;</code> <a href="https://stackoverflow.com/a/1041880">makes the function name linkable from C code</a>:</p> -<pre><code class="language-cpp">extern &quot;C&quot; { -int twice(int x); -} -</code></pre> -<p>We can compile that function into a shared library <code>lib/libinc.so</code>:</p> -<pre><code class="language-bash">g++ -shared lib/inc.cpp -o lib/libinc.so -</code></pre> -<p>(Note that in this case, we&#39;re defining a C-linkable interface in the C++ file itself rather than adding an extra layer between C++ and Rust. If we couldn&#39;t change the C++ source code, we could add another file in C or C++ that calls the C++ library, and then expose the C-linkable interface from <em>that</em> file.)</p> -<p>In Rust, we can define a list of foreign functions within an <code>extern</code> block, and then call into them from Rust code. Rust treats all foreign functions as unsafe, so we need to call it from within an <code>unsafe</code> block:</p> -<pre><code class="language-rs">extern &quot;C&quot; { - fn twice(x: i32) -&gt; i32; -} - -fn main() { - unsafe { - println!(&quot;{}&quot;, twice(2)); - } -} -</code></pre> -<p>Trying to build or run this will result in a linker error about undefined symbols. We need to tell Rust how to find the library, which we can do by placing a <a href="https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib"><code>build.rs</code> file</a> in the package root. TL;DR from the docs:</p> -<blockquote> -<p>Build scripts communicate with Cargo by printing to stdout. Cargo will interpret each line that starts with cargo: as an instruction that will influence compilation of the package. All other lines are ignored.</p> -</blockquote> -<p>Here&#39;s our <code>build.rs</code>:</p> -<pre><code class="language-rs">fn main() { - println!(&quot;cargo:rustc-link-search=lib&quot;); - println!(&quot;cargo:rustc-link-lib=inc&quot;); -} -</code></pre> -<ul> -<li><code>cargo:rustc-link-search=lib</code> tells Cargo to look in the directory <code>lib</code> for C libraries</li> -<li><code>cargo:rustc-link-lib=inc</code> tells Cargo to link against <code>libinc</code> (the <code>lib</code> prefix is implied)</li> -</ul> -<p>Boom! Running <code>cargo run</code> should now correctly print <code>4</code> to the console.</p> - - - -https://github.com/jakelazaroff/til/blob/main/c/prevent-clangformat-from-breaking-before-curly-braces.md -https://github.com/jakelazaroff/til/blob/main/c/prevent-clangformat-from-breaking-before-curly-braces.md -TIL (c): Prevent ClangFormat from breaking before curly braces -Fri, 22 Dec 2023 22:30:45 GMT -<p>For some unfathomable reason, C and C++ developers sometimes put line breaks before curly braces:</p> -<pre><code class="language-c">int main() -{ - for (int i = 0; i &lt; 10; i++) - { - printf(&quot;%d\n&quot;, i); - } -} -</code></pre> -<p>It&#39;s also the default formatting style in the code formatter <a href="https://clang.llvm.org/docs/ClangFormat.html">ClangFormat</a>.</p> -<p>Fortunately, it&#39;s configurable by setting the <a href="https://clang.llvm.org/docs/ClangFormatStyleOptions.html#breakbeforebraces"><code>BreakBeforeBraces</code> option</a> to <code>Attach</code> in your <code>.clang-format</code> file:</p> -<pre><code>BreakBeforeBraces: Attach -</code></pre> -<p>Much more readable:</p> -<pre><code class="language-c">int main() { - for (int i = 0; i &lt; 10; i++) { - printf(&quot;%d\n&quot;, i); - } -} -</code></pre> - - - -https://github.com/jakelazaroff/til/blob/main/htmx/load-modal-content-when-shoelace-dialog-opens.md -https://github.com/jakelazaroff/til/blob/main/htmx/load-modal-content-when-shoelace-dialog-opens.md -TIL (htmx): Load modal content when a Shoelace dialog opens -Fri, 08 Dec 2023 05:45:20 GMT -<p>This is pretty idiosyncratic to <a href="https://htmx.org">HTMX</a> and <a href="https://shoelace.style">Shoelace</a>, but it&#39;s a neat pattern so I&#39;m documenting it here.</p> -<p>HTMX lets you make an HTTP request in response to an event and insert it elsewhere into the DOM. Shoelace&#39;s <a href="https://shoelace.style/components/dialog">Dialog</a> component fires an <code>sl-show</code> event when the dialog opens. These can be combined to automatically load modal content when the modal opens:</p> -<pre><code class="language-html">&lt;sl-dialog hx-get=&quot;/modal/content/&quot; hx-trigger=&quot;sl-show&quot;&gt;&lt;/sl-dialog&gt; -</code></pre> -<p>If parts of the modal don&#39;t need to be loaded via HTTP — for example, the title — <code>hx-target</code> can be used to replace only the modal content:</p> -<pre><code class="language-html">&lt;sl-dialog hx-get=&quot;/modal/content/&quot; hx-trigger=&quot;sl-show&quot; hx-target=&quot;find .content&quot;&gt; - &lt;span slot=&quot;label&quot;&gt;My Modal&lt;/span&gt; - &lt;div class=&quot;content&quot;&gt; -&lt;/sl-dialog&gt; -</code></pre> - - - -https://github.com/jakelazaroff/til/blob/main/typescript/assert-that-a-variable-is-not-null-or-undefined.md -https://github.com/jakelazaroff/til/blob/main/typescript/assert-that-a-variable-is-not-null-or-undefined.md -TIL (typescript): Assert that a variable is not `null` or `undefined` -Wed, 06 Dec 2023 20:23:27 GMT -<p>TypeScript uses <code>!</code> as a <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator">non-null assertion operator</a>:</p> -<pre><code class="language-ts">const foo = document.querySelector(&quot;#foo&quot;); // Element | null; -const bar = document.querySelector(&quot;#bar&quot;)!; // Element; -</code></pre> -<p>I avoid it because it&#39;s the same as JavaScript&#39;s unary not operator, which makes it difficult to grep. Instead, I generally prefer to assert types with <code>as TypeName</code>.</p> -<pre><code class="language-ts">const foo = document.querySelector(&quot;#foo&quot;) as Element; -</code></pre> -<p>That has a couple drawbacks. Other than being more verbose, it&#39;s possible to cast the type to something incorrect:</p> -<pre><code class="language-ts">const foo = document.querySelector(&quot;svg&quot;) as HTMLElement; -</code></pre> -<p>But! You can chain the <code>!</code> non-null assertion operator as many times as you want:</p> -<pre><code class="language-ts">const foo = document.querySelector(&quot;#foo&quot;)!!!; // Element -</code></pre> -<p>I prefer that to <code>as</code> assertions. It&#39;s greppable (since using three consecutive unary nots is unlikely), it&#39;s less verbose, there&#39;s no chance of accidentally changing the type and a bunch of repeated exclamation marks definitely calls out that something unusual is happening with that code.</p> - - - -https://github.com/jakelazaroff/til/blob/main/tailwind/style-shadow-trees-from-the-light-dom.md -https://github.com/jakelazaroff/til/blob/main/tailwind/style-shadow-trees-from-the-light-dom.md -TIL (tailwind): Style shadow trees from the light DOM -Tue, 14 Nov 2023 22:29:00 GMT -<p>I&#39;ve been really enjoying the web component library <a href="https://shoelace.style">Shoelace</a> as a replacement for framework-specific UI libraries like <a href="https://www.radix-ui.com">Radix</a>.</p> -<p>Shoelace uses the shadow DOM to encapsulate its markup from the light DOM (aka the rest of the page), which means that class selectors can&#39;t reach it. This presents a problem for CSS utility frameworks like Tailwind, which use classes for everything.</p> -<p>Web components can use the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part"><code>part</code> attribute</a> to let outside stylesheets select elements in shadow DOM. For example, here&#39;s the shadow tree of <a href="https://shoelace.style/components/switch">Shoelace&#39;s Switch component</a>:</p> -<pre><code class="language-html">&lt;label part=&quot;base&quot; class=&quot; switch switch--medium &quot;&gt; - &lt;input class=&quot;switch__input&quot; type=&quot;checkbox&quot; role=&quot;switch&quot; title name aria-checked=&quot;false&quot; /&gt; - - &lt;span part=&quot;control&quot; class=&quot;switch__control&quot;&gt; - &lt;span part=&quot;thumb&quot; class=&quot;switch__thumb&quot;&gt;&lt;/span&gt; - &lt;/span&gt; - - &lt;div part=&quot;label&quot; class=&quot;switch__label&quot;&gt; - &lt;slot&gt;&lt;/slot&gt; - &lt;/div&gt; -&lt;/label&gt; -</code></pre> -<p>CSS in the light DOM can&#39;t select these elements using classes like <code>switch__control</code> — it needs to select these elements using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::part"><code>::part()</code> pseudo-element</a>:</p> -<pre><code class="language-css">sl-switch::part(control) { - background-color: red; -} - -sl-switch::part(thumb) { - background-color: white; -} -</code></pre> -<p>Although Tailwind can&#39;t target these parts out of the box, you can write a plugin to do it using <a href="https://tailwindcss.com/docs/plugins#dynamic-variants">dynamic variants</a> (Tailwind classes that accept arbitrary values):</p> -<pre><code class="language-js">const plugin = require(&quot;tailwindcss/plugin&quot;); - -module.exports = { - plugins: [ - plugin(function ({ matchVariant }) { - matchVariant(&quot;part&quot;, value =&gt; `&amp;::part(${value})`); - }) - ] -}; -</code></pre> -<p>This adds a <code>part</code> modifier that you can use like this:</p> -<pre><code class="language-html">&lt;sl-switch class=&quot;part-[control]:bg-red-500 part-[thumb]:bg-white part-[thumb]:rounded&quot;&gt; - label -&lt;/sl-switch&gt; -</code></pre> - - - -https://github.com/jakelazaroff/til/blob/main/github/run-github-actions-locally.md -https://github.com/jakelazaroff/til/blob/main/github/run-github-actions-locally.md -TIL (github): Run GitHub Actions locally -Wed, 27 Sep 2023 04:21:36 GMT -<p>A common pain point with GitHub Actions is that the feedback loop is so long: make a change, push, wait for it to run, find an error, try to debug, repeat. Which is why I was so happy to discover <a href="https://github.com/nektos/act"><code>act</code></a>, a tool for running GitHub Actions locally! The only prerequisite is Docker, which <code>act</code> uses to pull the appropriate images to run your actions.</p> -<p>By default, <code>act</code> will run the action for the <code>push</code> event, although you can configure it to run specific events or jobs:</p> -<pre><code class="language-bash"># run the `push` event: -act - -# run a specific event: -act pull_request - -# run a specific job: -act -j test -</code></pre> -<p>If your action needs a GitHub token (for example, if you&#39;re checking out your code with <a href="https://github.com/actions/checkout"><code>actions/checkout</code></a>) you can supply it with the <code>-s</code> flag (for &quot;secrets&quot;) and the <code>GITHUB_TOKEN</code> environment variable. This is easiest if you have the <a href="https://cli.github.com/">GitHub CLI</a> installed:</p> -<pre><code class="language-bash">act -s GITHUB_TOKEN=&quot;$(gh auth token)&quot; -</code></pre> -<p>Note that the <a href="https://github.com/nektos/act#github_token">official docs</a> note that supplying your token in this way can leak it to the shell history.</p> - - - -https://github.com/jakelazaroff/til/blob/main/htmx/attach-attributes-to-dynamically-added-elements.md -https://github.com/jakelazaroff/til/blob/main/htmx/attach-attributes-to-dynamically-added-elements.md -TIL (htmx): Attach attributes to dynamically added elements -Mon, 25 Sep 2023 05:20:01 GMT -<p>It&#39;s barely mentioned within the HTMX documentation, but by default HTMX attributes only work on elements that were in the DOM when HTMX was loaded, or that HTMX itself added to the DOM. This means that if you add an element by some other means — say, AlpineJS — HTMX won&#39;t know about any attributes on it or its descendants.</p> -<p>In the below example with AlpineJS and HTMX, the toggle button can be clicked to show and hide the form — but the <a href="https://htmx.org/attributes/hx-boost/"><code>hx-boost</code> attribute</a> won&#39;t be detected, which means that it will be a normal form submission:</p> -<pre><code class="language-html">&lt;div x-data=&quot;{ show: false }&quot;&gt; - &lt;template x-if=&quot;show&quot;&gt; - &lt;form method=&quot;post&quot; action=&quot;/some/endpoint&quot; hx-boost=&quot;true&quot;&gt; - &lt;input name=&quot;firstname&quot; /&gt; - &lt;input name=&quot;lastname&quot; /&gt; - &lt;button&gt;Submit&lt;/button&gt; - &lt;/form&gt; - &lt;/template&gt; - &lt;button @click=&quot;show = !show&quot;&gt;Toggle&lt;/button&gt; -&lt;/div&gt; -</code></pre> -<p>HTMX provides a function <a href="https://htmx.org/api/#process"><code>htmx.process</code></a> that checks a given element for HTMX attributes. Call it like so:</p> -<pre><code class="language-js">htmx.process(document.body); -</code></pre> -<p>The above code snippet can be fixed with the <a href="https://htmx.org/attributes/hx-boost/">AlpineJS <code>x-effect</code> directive</a>, which runs an expression when the tag is added to the DOM:</p> -<pre><code class="language-html">&lt;div x-data=&quot;{ show: false }&quot;&gt; - &lt;template x-if=&quot;show&quot;&gt; - &lt;form x-effect=&quot;htmx.process($el)&quot; method=&quot;post&quot; action=&quot;/some/endpoint&quot; hx-boost=&quot;true&quot;&gt; - &lt;input name=&quot;firstname&quot; /&gt; - &lt;input name=&quot;lastname&quot; /&gt; - &lt;button&gt;Submit&lt;/button&gt; - &lt;/form&gt; - &lt;/template&gt; - &lt;button @click=&quot;show = !show&quot;&gt;Toggle&lt;/button&gt; -&lt;/div&gt; -</code></pre> - - - -https://github.com/jakelazaroff/til/blob/main/typescript/type-concrete-subclasses-of-an-abstract-class.md -https://github.com/jakelazaroff/til/blob/main/typescript/type-concrete-subclasses-of-an-abstract-class.md -TIL (typescript): Type concrete subclasses of an abstract class -Wed, 13 Sep 2023 19:01:29 GMT -<p>Let&#39;s say there&#39;s an an inheritance hierarchy consisting of an abstract base class and two concrete subclasses:</p> -<pre><code class="language-ts">abstract class Base { - constructor(exampleParam: number) {} - abstract foo(): void; -} - -class A extends Base { - foo() { - console.log(&quot;A&quot;); - } -} - -class B extends Base { - foo() { - console.log(&quot;B&quot;); - } -} -</code></pre> -<p>In an inheritance hierarchy, it&#39;s common to reference a group of subclasses by their base class (the <a href="https://en.wikipedia.org/wiki/Liskov_substitution_principle">Liskov substitution principle</a>):</p> -<pre><code class="language-ts">function doSomething(obj: Base) { - base.foo(); -} -</code></pre> -<p>One situation in which this gets thorny for abstract classes is where a subclass that won&#39;t be known until runtime must be instantiated. In this case, referring to the base class directly will result in errors about how TypeScript cannot create an instance of an abstract class:</p> -<pre><code class="language-ts">function instantiate(Class: typeof Base) { - return new Class(10); // Cannot create an instance of an abstract class. -} -</code></pre> -<p>Instead of using the abstract base class&#39;s type directly, instead create an object type in which a constructor (a <code>new</code> method) returns an instance of the abstract class. If the constructor takes any parameters, they can be extracted using the <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype"><code>ConstructorParameters</code></a> utility type:</p> -<pre><code class="language-ts">function instantiate(Class: { new (...params: ConstructorParameters&lt;typeof Base&gt;): Base }) { - return new Class(10); -} -</code></pre> - - - -https://github.com/jakelazaroff/til/blob/main/javascript/access-css-variables-from-javascript.md -https://github.com/jakelazaroff/til/blob/main/javascript/access-css-variables-from-javascript.md -TIL (javascript): Access CSS variables from JavaScript -Sat, 26 Aug 2023 17:33:14 GMT -<p>In building <a href="https://fxplayground.pages.dev/">fxplayground</a>, I wanted to use colors from the site&#39;s theme in a canvas visualization. The theme colors were all stored in CSS custom properties (variables), but the visualization drawing code was all JavaScript. I needed a way to read the CSS color variables from within JavaScript code.</p> -<p>Luckily, there&#39;s a function called <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle"><code>getComputedStyle</code></a> that can help with that:</p> -<pre><code class="language-js">const color = getComputedStyle(document.documentElement).getPropertyValue(&quot;--color-primary&quot;); -</code></pre> -<p><code>getComputedStyle</code> takes a DOM node and returns a live <code>CSSStyleDeclaration</code>, which contains all the styles applied to that element. Calling <code>getPropertyValue</code> returns the value for a given property, which includes CSS variable declarations. So if there&#39;s a variable defined on the <code>:root</code> selector, you can get the value by calling <code>getPropertyValue(&quot;--variable-name&quot;)</code>!</p> - - - - \ No newline at end of file