<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[LeKoala Dev Blog]]></title><description><![CDATA[LeKoala Dev Blog]]></description><link>https://blog.lekoala.be</link><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 17:44:04 GMT</lastBuildDate><atom:link href="https://blog.lekoala.be/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Writing a Zero-Dependency CSV and XLSX Reader/Writer]]></title><description><![CDATA[Some projects start with a grand plan. Others start because you just need to read a file without thinking too much about it.
That was the case for spread-compat. I needed a common interface for CSV, X]]></description><link>https://blog.lekoala.be/writing-a-zero-dependency-csv-and-xlsx-reader-writer</link><guid isPermaLink="true">https://blog.lekoala.be/writing-a-zero-dependency-csv-and-xlsx-reader-writer</guid><category><![CDATA[PHP]]></category><category><![CDATA[php8]]></category><category><![CDATA[xlsx]]></category><category><![CDATA[csv]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 06 Mar 2026 12:32:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5f97ffc991c85e2865f5ec51/ba5beb77-d399-40c4-9e6d-82953aff19eb.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some projects start with a grand plan. Others start because you just need to read a file without thinking too much about it.</p>
<p>That was the case for <strong>spread-compat</strong>. I needed a common interface for CSV, XLSX, and sometimes legacy XLS files, without having to rewrite the same code depending on the underlying library. Sometimes I needed a "simple" parser, sometimes, I needed something that supports more complex files. The goal was simple: switch adapters, keep the rest of the code the same, and let the package deal with the details. That is still very much the idea behind spread-compat today: one facade over several backends, including native PHP for CSV, OpenSpout, League CSV, PhpSpreadsheet, and SimpleXLSX, with the package favoring the fastest available adapter by format.​</p>
<h2><strong>The first idea</strong></h2>
<p>At first, the main motivation was convenience.</p>
<p>CSV is everywhere, and it is often the fastest and easiest format to process. But in real applications, users also expect XLSX support because that is what they open in Excel. And every now and then, you still run into old XLS files as well. spread-compat was my way to smooth over those differences and expose a single API for reading and writing tabular data. The repository describes that goal very directly: importing and exporting CSV data is common, but users are often more comfortable with Excel files, so changing format should ideally be just a matter of changing an adapter.​</p>
<p>At that stage, I also realized something else: for many use cases, a simple CSV or XLSX reader can be much faster than a full-featured spreadsheet library.</p>
<h2><strong>The part that looks simple</strong></h2>
<p>This was the point where the project became more interesting.</p>
<p>Reading CSV or XLSX files sounds simple until you start listing edge cases. CSV alone has plenty of traps: separators, enclosures, escapes, line endings, encoding issues, multiline fields, streaming, malformed files, and small RFC details that are easy to overlook until a real file breaks your parser. The spread-compat repository itself shows that the package had to support configuration around separators, generators, streams, output handling, and multiple adapters, which is already a sign that “simple” file handling quickly grows into something more subtle.​</p>
<p>That is also why mature libraries such as League CSV, OpenSpout, and PhpSpreadsheet exist in the first place. They are not big by accident. spread-compat supports those libraries precisely because each of them solves a different part of the problem space: speed, compatibility, streaming, or richer spreadsheet features.​</p>
<h2><strong>Why baresheet happened</strong></h2>
<p>After working on the “native” adapters in spread-compat, I reached a point where it made sense to split that work into its own package.</p>
<p>I wanted something focused: a small library dedicated to the hard parts of simple spreadsheet reading and writing, while still reusing the experience and code paths I had already explored in spread-compat. That eventually became <strong>baresheet</strong>.</p>
<p>In a way, baresheet came from a contradiction I kept running into: I wanted something lightweight, but I also wanted it to behave correctly in the messy situations that real files bring. So the work was no longer just about making a fast reader. It became about handling RFC details properly, improving performance without cutting corners, thinking about security, benchmarking the results, and iterating until the package felt solid enough to stand on its own.</p>
<p>Today, that feels like the real value of baresheet. It is not just “native code extracted from another package”. It is the result of many rounds of refinement around the deceptively small problem of reading and writing tabular files well.</p>
<h2><strong>How the two fit together</strong></h2>
<p>I see the relationship between the two packages as fairly natural.</p>
<p>spread-compat is the compatibility layer. It gives you one interface over several libraries and lets you choose the right adapter for the job. It is especially useful when your application needs to support more than one format or when you want flexibility without coupling your code to a single spreadsheet package. The project supports multiple backends and exposes a facade for reading, writing, and browser output, all while keeping the “one worksheet” scope intentionally narrow so CSV and XLSX can be treated in a similar way.​</p>
<p>baresheet is the focused engine that grew out of the fast-path use case. When you just want efficient handling of common CSV/XLSX scenarios, a dedicated library can be a better foundation than generic adapters embedded inside a broader compatibility package. That separation also makes the design cleaner: one package is about abstraction, the other is about doing the low-level work well.</p>
<h2><strong>A few things I find interesting</strong></h2>
<p>A few lessons became clearer while building both packages.</p>
<ul>
<li><p>“Simple” formats are rarely simple. CSV looks trivial right up until you care about correctness on weird files (hello BOM).</p>
</li>
<li><p>Lightweight code does not mean less engineering. In practice, it often means more attention to edge cases, benchmarks, and failure modes.</p>
</li>
<li><p>Older formats never really disappear. Legacy XLS support is the kind of requirement that sounds outdated until a client sends you one.</p>
</li>
<li><p>Small-scope tools can become better by being honest about scope. spread-compat explicitly limits itself to one worksheet, which is a good example of choosing constraints instead of pretending to cover everything.​</p>
</li>
</ul>
<p>If I had to describe the evolution in one line, I would say this: spread-compat started as a practical abstraction layer, and baresheet grew out of the realization that the fast, simple path deserved to become a real library of its own.</p>
<p>At the moment, baresheet is published as version 0.2 and already feels much better than what used to live as the “native” adapters inside spread-compat.</p>
<p>Have a look if you are interested :-)</p>
<p><a href="https://github.com/lekoala/baresheet">https://github.com/lekoala/baresheet</a></p>
<p><a href="https://github.com/lekoala/spread-compat">https://github.com/lekoala/spread-compat</a></p>
]]></content:encoded></item><item><title><![CDATA[Media Queries Level 5 are awesome]]></title><description><![CDATA[With modern css, you can almost get rid of sass. But there is one last pain point : media queries. Not being able to use variables or an easy, common syntax for typical breakpoints is really annoying. That is, until you meet Media Queries Level 5.
I’...]]></description><link>https://blog.lekoala.be/media-queries-level-5-are-awesome</link><guid isPermaLink="true">https://blog.lekoala.be/media-queries-level-5-are-awesome</guid><category><![CDATA[CSS]]></category><category><![CDATA[media queries]]></category><category><![CDATA[Bootstrap]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 10 Oct 2025 07:24:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/AjWh5_Jsnlg/upload/ec74118a95ca35afe115f6813d880f18.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With modern css, you can <a target="_blank" href="https://blog.lekoala.be/you-might-not-need-sass-anymore">almost get rid of sass</a>. But there is one last pain point : media queries. Not being able to use variables or an easy, common syntax for typical breakpoints is really annoying. That is, until you meet Media Queries Level 5.</p>
<p>I’ve discovered them by chance by reading the <a target="_blank" href="https://lightningcss.dev/transpilation.html#custom-media-queries">Lightning CSS documentation</a> (because, yes, modern css is great, but realistically you still need to transpile it for older browsers in most cases). This gives you the ability to do something like this:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@custom-media</span> --modern (color), (hover);

<span class="hljs-keyword">@media</span> (--modern) <span class="hljs-keyword">and</span> (width &gt; <span class="hljs-number">1024px</span>) {
  <span class="hljs-selector-class">.a</span> { <span class="hljs-attribute">color</span>: green; }
}
</code></pre>
<p>Compile it with the <code>--custom-media</code> flag, and you are good to go!</p>
<pre><code class="lang-bash">lightningcss --minify --custom-media --bundle src/index.css -o dist/index.css
</code></pre>
<h2 id="heading-taking-bootstrap-breakpoints-for-a-spin">Taking Bootstrap breakpoints for a spin</h2>
<p>So, how would this look like for a typical framework like Bootstrap. Let’s see. Combine this with the <a target="_blank" href="https://lightningcss.dev/transpilation.html#media-query-ranges">CSS Media Query Ranges</a> and it’s actually really great.</p>
<pre><code class="lang-css"><span class="hljs-comment">/*

Bootstrap media queries using level 5 media queries

Example:

```css
@media (--lg-only) {
    ...
}
```

<span class="hljs-doctag">Note:</span> requires --custom-media when using lightningcss

Documentation:
@link https://www.w3.org/TR/mediaqueries-5/
@link https://lightningcss.dev/transpilation.html#media-query-ranges
@link https://lightningcss.dev/transpilation.html#custom-media-queries

Breakpoint            Name    Dimensions
Extra small            None    &lt;576px
Small                sm        ≥576px
Medium                md        ≥768px
Large                lg        ≥992px
Extra large            xl        ≥1200px
Extra extra large    xxl        ≥1400px
*/</span>

<span class="hljs-comment">/* Small devices (landscape phones, 576px and up) */</span>
<span class="hljs-keyword">@custom-media</span> --sm-up (width &gt;= <span class="hljs-number">576px</span>);
<span class="hljs-comment">/* Medium devices (tablets, 768px and up) */</span>
<span class="hljs-keyword">@custom-media</span> --md-up (width &gt;= <span class="hljs-number">768px</span>);
<span class="hljs-comment">/* Large devices (desktops, 992px and up) */</span>
<span class="hljs-keyword">@custom-media</span> --lg-up (width &gt;= <span class="hljs-number">992px</span>);
<span class="hljs-comment">/* X-Large devices (large desktops, 1200px and up) */</span>
<span class="hljs-keyword">@custom-media</span> --xl-up (width &gt;= <span class="hljs-number">1200px</span>);
<span class="hljs-comment">/* XX-Large devices (larger desktops, 1400px and up) */</span>
<span class="hljs-keyword">@custom-media</span> --xxl-up (width &gt;= <span class="hljs-number">1400px</span>);

<span class="hljs-comment">/*
Why subtract .02px?
Browsers don’t currently support range context queries,
so we work around the limitations of min- and max- prefixes and viewports
with fractional widths
(which can occur under certain conditions on high-dpi devices, for instance)
by using values with higher precision.
*/</span>

<span class="hljs-comment">/* `sm` applies to x-small devices (portrait phones, less than 576px) */</span>
<span class="hljs-keyword">@custom-media</span> --sm-down (width &lt;= <span class="hljs-number">575.98px</span>);
<span class="hljs-comment">/* `md` applies to small devices (landscape phones, less than 768px) */</span>
<span class="hljs-keyword">@custom-media</span> --md-down (width &lt;= <span class="hljs-number">767.98px</span>);
<span class="hljs-comment">/* `lg` applies to medium devices (tablets, less than 992px) */</span>
<span class="hljs-keyword">@custom-media</span> --lg-down (width &lt;= <span class="hljs-number">991.98px</span>);
<span class="hljs-comment">/* `xl` applies to large devices (desktops, less than 1200px) */</span>
<span class="hljs-keyword">@custom-media</span> --xl-down (width &lt;= <span class="hljs-number">1199.98px</span>);
<span class="hljs-comment">/* `xxl` applies to x-large devices (large desktops, less than 1400px) */</span>
<span class="hljs-keyword">@custom-media</span> --xxl-down (width &lt;= <span class="hljs-number">1399.98px</span>);

<span class="hljs-comment">/* `xs-only` is equivalent to `sm-down` */</span>
<span class="hljs-keyword">@custom-media</span> --sm-<span class="hljs-keyword">only</span> (<span class="hljs-number">576px</span> &lt;= width &lt;= <span class="hljs-number">767.98px</span>);
<span class="hljs-keyword">@custom-media</span> --md-<span class="hljs-keyword">only</span> (<span class="hljs-number">768px</span> &lt;= width &lt;= <span class="hljs-number">991.98px</span>);
<span class="hljs-keyword">@custom-media</span> --lg-<span class="hljs-keyword">only</span> (<span class="hljs-number">992px</span> &lt;= width &lt;= <span class="hljs-number">1199.98px</span>);
<span class="hljs-keyword">@custom-media</span> --xl-<span class="hljs-keyword">only</span> (<span class="hljs-number">1200px</span> &lt;= width &lt;= <span class="hljs-number">1399.98px</span>);
<span class="hljs-comment">/* `xxl-only` is equivalent to `xxl-up` */</span>
</code></pre>
<p>And now, simply use it</p>
<pre><code class="lang-css"><span class="hljs-keyword">@media</span> (--lg-<span class="hljs-keyword">only</span>) {
    <span class="hljs-selector-class">.test</span> {
        <span class="hljs-attribute">color</span>: green;
    }
}
<span class="hljs-comment">/* Outputs:
@media (min-width: 992px) and (max-width: 1199.98px) {
  .test {
    color: green;
  }
}
*/</span>
</code></pre>
<p>This is actually even nicer than the equivalent Bootstrap syntax</p>
<pre><code class="lang-scss"><span class="hljs-keyword">@include</span> media-breakpoint-<span class="hljs-keyword">only</span>(lg) {
    <span class="hljs-selector-class">.test</span> {
        <span class="hljs-attribute">color</span>: green;
    }
}
</code></pre>
<p>The CSS equivalent is both shorter and self documenting. What kind of output is the mixin going to produce? You can’t know for sure. But the CSS variable ? Just look at it and see its value.</p>
<p>The draft specification is not going to be usable for quite some time, so using a tool like lightning css is still required, but that seems like a small price to pay in my opinion.</p>
]]></content:encoded></item><item><title><![CDATA[Modern tooltips]]></title><description><![CDATA[This is a quick article to share this demo I just made.
https://codepen.io/lekoalabe/pen/JoPNWpX]]></description><link>https://blog.lekoala.be/modern-tooltips</link><guid isPermaLink="true">https://blog.lekoala.be/modern-tooltips</guid><category><![CDATA[ES6]]></category><category><![CDATA[tooltip]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[HTML5]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 20 Dec 2024 19:29:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734722967981/230ccc5b-be16-47e5-a2e3-ff70a350ecb2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a quick article to share this demo I just made.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/JoPNWpX">https://codepen.io/lekoalabe/pen/JoPNWpX</a></div>
]]></content:encoded></item><item><title><![CDATA[Using Dialog elements today]]></title><description><![CDATA[Modals are typical UI elements for many web apps. There are countless packages dedicated to that specific features, each framework creating its own variation. Popular frameworks like Bootstrap also have custom code to deal with it.
But all this feels...]]></description><link>https://blog.lekoala.be/using-dialog-elements-today</link><guid isPermaLink="true">https://blog.lekoala.be/using-dialog-elements-today</guid><category><![CDATA[Dialog]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[HTML5]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Tue, 29 Oct 2024 11:24:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730200881887/720e858a-b32f-44b2-9894-a5a6e148b1c5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modals are typical UI elements for many web apps. There are countless packages dedicated to that specific features, each framework creating its own variation. Popular frameworks like <a target="_blank" href="https://getbootstrap.com/docs/5.3/components/modal/#how-it-works">Bootstrap also have custom code to deal with it</a>.</p>
<p>But all this feels really unnecessary when you could just use the <a target="_blank" href="https://web.dev/learn/html/dialog"><code>dialog</code> tag</a>, that also comes with features (like proper stacking, trapped focus, inert background…) that are hard (or impossible) to fully replicate using user land JavaScript.</p>
<h2 id="heading-but-its-not-perfect">But it’s not perfect</h2>
<p>While <code>dialog</code> is great, it’s not perfect either. For example, it’s impossible to use plain html to open the dialog. Most tutorials show you something along these lines:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"modal"</span>&gt;</span>Open Modal dialog<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dialog</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"dialog"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">dialog</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> modal = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'modal'</span>);
<span class="hljs-keyword">const</span> dialog= <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'dialog'</span>);
modal.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  dialog.showModal();
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>And I feel really sad reading this. I would have imagined something like:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"modal"</span> <span class="hljs-attr">data-dialog</span>=<span class="hljs-string">"dialog"</span>&gt;</span>Open Modal dialog<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">dialog</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"dialog"</span>&gt;</span>...<span class="hljs-tag">&lt;/<span class="hljs-name">dialog</span>&gt;</span>
</code></pre>
<p>Wouldn’t that be better? Sure, it’s easy enough to add this as a global helper, but it should really be part of the spec. Adding a global helper is also not always so easy if the button and dialog are injected in the page, so you cannot rely on DOMContentLoaded event.</p>
<p>Don’t worry, I have a solution for you. But first, let’s talk about another problem.</p>
<h2 id="heading-what-if-you-need-old-ios-support">What if you need old IOS support ?</h2>
<p>Dialogs are only properly supported since IOS 15.4. So, if you want to support older browsers, that’s not so easy. Sure, <a target="_blank" href="https://github.com/GoogleChrome/dialog-polyfill">there is a polyfill</a>, but then it’s extra setup, and maybe you end up loading a polyfill just for that.</p>
<p>Wouldn’t it be great if the polyfill was loaded if needed, without any bloat for modern browsers.</p>
<p>Again, I have a solution for you.</p>
<h2 id="heading-improving-the-dialog-api">Improving the dialog api</h2>
<p>I recently (almost) <a target="_blank" href="https://modern-now.vercel.app/demos/dialog.html">finalized this demo</a>. It uses under the hood the wonderful <a target="_blank" href="https://github.com/WebReflection/qsa-observer/tree/master/esm">qsa-observer</a> and a little bit of my own magic to do the work for you.</p>
<p>Basically, this will:</p>
<ul>
<li><p>Check for dialog support, dynamically importing it from a cdn (or a chosen location) if needed, then, registering the polyfill.</p>
</li>
<li><p>Look for any dialog element, even if it’s injected in the page (thanks to qsa-observer… so if you load a dialog with htmx, it works!).</p>
</li>
<li><p>Bind buttons to open/close the dialog automatically and expose features (modal, dismissible) using data attributes.</p>
</li>
<li><p>Allows animation by adding a <code>is-closing</code> class, so that means it’s easy to add open/close animations (I found out it’s easier to use animations for this use case instead of transitions that are a bit tricky due to display:none being used).</p>
</li>
<li><p>If you want to disable scrollbars, there is a little bit of magical css and css variables to avoid layout shifting.</p>
</li>
</ul>
<p>While building this, I realized that creating offcanvas menus is very similar to opening dialogs. And indeed, with a couple of css styles, it’s easily achieved.</p>
<p>Once you start using this approach (improving native html), you might wonder: why stop to dialogs ? And indeed, that’s exactly what I thought. <a target="_blank" href="https://modern-now.vercel.app/index.html">This is why I’m currently building this and see how far it gets</a>. Maybe I’ll talk more about it in the next article.</p>
<h2 id="heading-a-quick-demo">A quick demo</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/GgKOKOE">https://codepen.io/lekoalabe/pen/GgKOKOE</a></div>
]]></content:encoded></item><item><title><![CDATA[Using assert to fix PHPStan errors]]></title><description><![CDATA[I’ve been using more and more PHPStan recently and it’s been a real pleasure to improve my codebases.
Being strict is nice, but sometimes it can also be inconvenient. A lot of built-in methods in php can return false. Or sometimes, you have a “mixed”...]]></description><link>https://blog.lekoala.be/using-assert-to-fix-phpstan-errors</link><guid isPermaLink="true">https://blog.lekoala.be/using-assert-to-fix-phpstan-errors</guid><category><![CDATA[PHP]]></category><category><![CDATA[phpstan]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 20 Sep 2024 10:40:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726828790569/13cc29f4-8b1b-40ba-907c-82d26f01c50b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been using more and more <a target="_blank" href="https://phpstan.org/">PHPStan</a> recently and it’s been a real pleasure to improve my codebases.</p>
<p>Being strict is nice, but sometimes it can also be inconvenient. A lot of built-in methods in php can return false. Or sometimes, you have a “mixed” result even though you know for sure it’s type.</p>
<p>Let’s say you have something like this:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stringOrArray</span>(<span class="hljs-params"></span>):<span class="hljs-title">string</span>|<span class="hljs-title">array</span> </span>{
    $v = someMethodThatReturnMixed();
    <span class="hljs-keyword">return</span> $v;
}
</code></pre>
<p>Of course, you can do this:</p>
<pre><code class="lang-php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stringOrArray</span>(<span class="hljs-params"></span>):<span class="hljs-title">string</span>|<span class="hljs-title">array</span> </span>{
    $v = someMethodThatReturnMixed();
    <span class="hljs-keyword">if</span>(is_string($v) || is_array($v)) {
        <span class="hljs-keyword">return</span> $v;
    }
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Exception</span>(<span class="hljs-string">"Invalid type"</span>);
}
</code></pre>
<p>But this means adding an if. Throwing an Exception. Not a big deal by itself, but if you have to fix hundreds of lines like this, it’s not great.</p>
<p>A quick(er) fix, is this:</p>
<pre><code class="lang-php"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stringOrArray</span>(<span class="hljs-params"></span>):<span class="hljs-title">string</span>|<span class="hljs-title">array</span> </span>{
    $v = someMethodThatReturnMixed();
    assert(is_string($v)||is_array($v));
    <span class="hljs-keyword">return</span> $v;
}
</code></pre>
<p>Aaaaand… done! I really like using <a target="_blank" href="https://www.php.net/manual/en/function.assert.php">assert</a> : it adds type checks during development, and is ignored at no performance cost in production. And if somehow, as a developer, you made a mistake, it’s very likely you are going to catch the AssertionException that gets thrown.</p>
<p>That’s all for today, I hope you liked my little tip :)</p>
]]></content:encoded></item><item><title><![CDATA[Can you nest HTML forms?]]></title><description><![CDATA[According to this SO post, well... no. What if there was a way to do it anyway ?
Nesting form is not allowed
If you try to do this:
<form action="/main">
    <input type="text" name="main_field">
    <form action="/sub">
        <input type="text" na...]]></description><link>https://blog.lekoala.be/can-you-nest-html-forms</link><guid isPermaLink="true">https://blog.lekoala.be/can-you-nest-html-forms</guid><category><![CDATA[HTML5]]></category><category><![CDATA[HTML]]></category><category><![CDATA[forms]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 31 May 2024 09:46:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1717143605176/0697db8a-9a36-4910-a8f3-51e9d0213cf7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://stackoverflow.com/questions/379610/can-you-nest-html-forms">According to this SO post</a>, well... no. What if there was a way to do it anyway ?</p>
<h2 id="heading-nesting-form-is-not-allowed">Nesting form is not allowed</h2>
<p>If you try to do this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/main"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"main_field"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/sub"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"sub_field"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>You will notice that the nested form is discarded when parsed by the browser because <a target="_blank" href="https://www.w3.org/TR/html5/forms.html#the-form-element">it's not allowed in the spec</a>. Nested forms might seem dumb, but it can be useful sometimes, for example when nesting some kind of modal in a form with specific actions.</p>
<p>It turns out this was exactly my use case with <a target="_blank" href="https://github.com/lekoala/pop-modal">pop-modal</a> and I found a nice solution that I wanted to share with you.</p>
<h2 id="heading-a-fake-form-to-the-rescue">A fake form to the rescue</h2>
<p>Let's tweak our snippet a little bit</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"holder"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/main"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"main_field"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">nested-form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/sub"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"sub_field"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">nested-form</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>Here, I converted the <code>form</code> tag to a custom <code>nested-form</code> tag. This way, I can keep the form tag and declare any attribute I want. But obviously, this is not going to work, so we need to do a few things.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> holder = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#holder"</span>);
<span class="hljs-keyword">const</span> nestedForm = holder.querySelector(<span class="hljs-string">"nested-form"</span>);
<span class="hljs-keyword">const</span> form = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"form"</span>);
nestedForm.getAttributeNames().forEach(<span class="hljs-function">(<span class="hljs-params">attr</span>) =&gt;</span> {
    form.setAttribute(attr, nestedForm.getAttribute(attr));
});
form.append(...nestedForm.childNodes);
nestedForm.remove();
holder.append(form);
</code></pre>
<p>This will effectively "unnest" the forms, but it will also move the fields in the dom. In my case, since I'm working in a modal, that's good enough, but maybe you actually need these fields to stay in place.</p>
<h2 id="heading-a-fake-form-with-fake-fields">A fake form with fake fields</h2>
<p>Let's say you cannot move the form in the dom. Here is a slightly more complex solution. Thanks to the <code>form</code> attribute, we target a form with nested hidden inputs. Those hidden input get the value for the original inputs through a change listener. It's not a bulletproof solution, but this works well for simple forms.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> holder = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#holder2"</span>);
<span class="hljs-keyword">const</span> nestedForm = holder.querySelector(<span class="hljs-string">"nested-form"</span>);
<span class="hljs-keyword">const</span> form = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"form"</span>);
<span class="hljs-built_in">document</span>.body.append(form);
nestedForm.getAttributeNames().forEach(<span class="hljs-function">(<span class="hljs-params">attr</span>) =&gt;</span> {
  form.setAttribute(attr, nestedForm.getAttribute(attr));
});
form.id = form.id|| <span class="hljs-string">"form-"</span> + (+<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>());
nestedForm.querySelectorAll(<span class="hljs-string">'[type="submit"]'</span>).forEach(<span class="hljs-function">(<span class="hljs-params">btn</span>) =&gt;</span> {
  btn.setAttribute(<span class="hljs-string">"form"</span>, form.id);
});
nestedForm.querySelectorAll(<span class="hljs-string">"input,textarea,select"</span>).forEach(<span class="hljs-function">(<span class="hljs-params">input</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> hidden = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'input'</span>);
  hidden.type = <span class="hljs-string">"hidden"</span>;
  hidden.name = input.name;
  form.appendChild(hidden);
  <span class="hljs-comment">// Sync values</span>
  input.addEventListener(<span class="hljs-string">'change'</span>, <span class="hljs-function">(<span class="hljs-params">ev</span>) =&gt;</span> {
    hidden.value = input.value;
  })
});
</code></pre>
<p>If you want to give this a try, here is a codepen</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/qBGrWKY">https://codepen.io/lekoalabe/pen/qBGrWKY</a></div>
]]></content:encoded></item><item><title><![CDATA[You might not need SASS anymore]]></title><description><![CDATA[Ok. I did it. Back to (almost) plain old regular css for building my last project. You would think that SASS would be absolutely necessary to have a pleasant DX, but as it turns out, I don't think I need it anymore.
I don't know about you, but as far...]]></description><link>https://blog.lekoala.be/you-might-not-need-sass-anymore</link><guid isPermaLink="true">https://blog.lekoala.be/you-might-not-need-sass-anymore</guid><category><![CDATA[CSS]]></category><category><![CDATA[esbuild]]></category><category><![CDATA[Bun]]></category><category><![CDATA[Sass]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Mon, 29 Apr 2024 15:40:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714405137570/acf8e9b7-159e-42b7-8666-448e94a00dd2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ok. I did it. Back to (almost) plain old regular css for building my last project. You would think that SASS would be absolutely necessary to have a pleasant DX, but as it turns out, I don't think I need it anymore.</p>
<p>I don't know about you, but as far as I'm concerned, I mostly use SASS for 3 reasons:</p>
<ul>
<li><p>To be able to nest declarations</p>
</li>
<li><p>To split my css into multiples files</p>
</li>
<li><p>To use variables</p>
</li>
</ul>
<p>As it turns out, if you are using esbuild and targeting a browser <a target="_blank" href="https://caniuse.com/css-variables">that supports css variables</a> (that is, pretty much everything since 2018), you can basically ditch sass and just use <a target="_blank" href="https://esbuild.github.io/content-types/#css">esbuild to compile your css</a>.</p>
<p>The magic part of your package.json looks like this (and yes, using bun, even if I'm on windows and bundows had some issues at launch) :</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"my project"</span>,
  <span class="hljs-attr">"module"</span>: <span class="hljs-string">"./public/assets/js/index.js"</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"esbuild"</span>: <span class="hljs-string">"^0.20"</span>
  },
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"esbuild --bundle --minify --loader:.woff2=file --target=safari15.4,chrome88 ./src/css/index.css ./src/js/index.js --outdir=./public/assets"</span>,
    <span class="hljs-attr">"watch"</span>: <span class="hljs-string">"bun run build -- --watch"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"bun run build -- --servedir=./public"</span>
  }
}
</code></pre>
<h2 id="heading-nesting-declarations">Nesting declarations</h2>
<p><a target="_blank" href="https://caniuse.com/?search=nesting">Only supported by browsers from 2023</a>, <a target="_blank" href="https://www.w3.org/TR/css-nesting-1/">native css nesting</a> is not ready yet for public consumption at the time of writing this article. But by using esbuild, you can transpile easily your nicely nested css to plain old expanded css.</p>
<p><a target="_blank" href="https://thecascade.dev/article/native-css-nesting-vs-scss-nesting/">The syntax is almost the same as the one from SASS</a>.</p>
<h2 id="heading-splitting-files">Splitting files</h2>
<p>Again, using esbuild, you simply <code>@import</code> your files and they will be bundled automatically. If you are including woff2 files for local fonts, don't forget the <code>--loader:.woff2=file</code> part of the script.</p>
<h2 id="heading-css-variables">CSS Variables</h2>
<p>On this one, I even think SASS variables are less powerful that CSS variables. It's so cool to have fallback values, to be able to change variables based on media queries, inject them in Javascript, use them in math representation...</p>
<p>I'm still a bit sad about <a target="_blank" href="https://blog.lekoala.be/pure-css-color-palette-using-oklch">not being able to use properly oklch colors.</a> Yes, they are converted automatically by esbuild, but then you cannot use them as part of a formula, which is, I think, the main reason behind using oklch.</p>
]]></content:encoded></item><item><title><![CDATA[I don't like Tailwind]]></title><description><![CDATA[There. I said it. I know every cool kid likes it, but I don't. Never seen the point of it. Always seemed way too complicated to do simple things. I don't like to have, you know, run a whole build system when I could just write css classes that work f...]]></description><link>https://blog.lekoala.be/i-dont-like-tailwind</link><guid isPermaLink="true">https://blog.lekoala.be/i-dont-like-tailwind</guid><category><![CDATA[CSS]]></category><category><![CDATA[Tailwind CSS]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Sat, 02 Mar 2024 22:27:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709417891529/d243d097-27e7-4cf1-95ac-55b0ca22656b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There. I said it. I know every cool kid likes it, but I don't. Never seen the point of it. Always seemed way too complicated to do simple things. I don't like to have, you know, run a whole build system when I could just write css classes that work fine.</p>
<p>And it seems I'm not the only one, since I've recently read these two articles:</p>
<p><a target="_blank" href="https://heydonworks.com/article/what-is-utility-first-css/">https://heydonworks.com/article/what-is-utility-first-css/</a></p>
<p><a target="_blank" href="https://nuejs.org/blog/tailwind-misinformation-engine/">https://nuejs.org/blog/tailwind-misinformation-engine/</a></p>
<p>Which I highly recommend, obviously.</p>
<h2 id="heading-but-it-works">But it works!</h2>
<p>Yes, sure it works. But at what cost ? In order to make tailwind work, you need to have a build pipeline to analyze which utilities are used in order to include only them. Not possible for you ? Too bad !</p>
<p>By using only utilities (and I'm not saying utilities, as such, are bad... I do like having a little spacer utility here and there or a flex class to do flexy stuff), you end up with a big pile of classes in your html. Sure you can refactor that into a custom class... but isn't that basically writing old, traditional css then?</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- Before extracting a custom class --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75"</span>&gt;</span>
  Save changes
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

<span class="hljs-comment">&lt;!-- After extracting a custom class --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn-primary"</span>&gt;</span>
  Save changes
<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>And why on earth would I want to use <code>@apply</code> when I could just write css ? Where I could just, write things, refresh my page, see the changes ? (You can do that in tailwind, but not without tooling).</p>
<p>And in this specific example, it's very likely that you would actually have two classes. A global <code>btn</code> class, and a <code>btn-primary</code> variant. So even this example taken straight from Tailwind docs is not quite realistic. How do you split these properties in two in Tailwind? I don't know (it's probably possible, but since I don't use it...),</p>
<h2 id="heading-what-about-customization">What about customization ?</h2>
<p>So you have your pretty <code>tailwind.config.js</code> file where you store your configuration.</p>
<p>But you know, css can have configuration too, even without using scss or anything.</p>
<pre><code class="lang-javascript">    colors: {
      <span class="hljs-string">'blue'</span>: <span class="hljs-string">'#1fb6ff'</span>,
      <span class="hljs-string">'pink'</span>: <span class="hljs-string">'#ff49db'</span>,
      ...
    },
</code></pre>
<p>Could really just be:</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">--color-blue</span>: '<span class="hljs-selector-id">#1fb6ff</span>';
<span class="hljs-selector-tag">--color-pink</span>: '<span class="hljs-selector-id">#ff49db</span>';
</code></pre>
<p>That works just fine :-) And you can scope them if needed. Make them responsive. Have fallback values. So if you want a design system, as advertised as one of the strengths of Tailwind, you could very well do something like <a target="_blank" href="https://www.pollen.style/">https://www.pollen.style/</a>.</p>
<p>My dislike for Tailwind <a target="_blank" href="https://blog.lekoala.be/typescript-or-jsdocs">is very similar to my dislike for Typescript</a>. I don't like adding a tool unless it really brings a big added value. Most of the time, the cost of adding/learning/dealing with upgrades/changes of version of these tools are not worth the effort. <strong>Regular css and js is actually quite fine these days (I wouldn't have said that 5 years ago, but here we are).</strong></p>
]]></content:encoded></item><item><title><![CDATA[Typescript... or jsdocs ?]]></title><description><![CDATA[Each time I see an article (like this one) about typescript, they never quite seem to mention jsdocs. Therefore, it always feels like an unfair, one-sided comparison. Let's dive in some of the typical examples.
Type safety
This is probably the bigges...]]></description><link>https://blog.lekoala.be/typescript-or-jsdocs</link><guid isPermaLink="true">https://blog.lekoala.be/typescript-or-jsdocs</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[jsdoc]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Thu, 11 Jan 2024 12:11:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704975041434/100ea724-1e28-49dc-a38f-617bb686bd74.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Each time I see an article (<a target="_blank" href="https://www.blackslate.io/articles/transitioning-from-javascript-to-typescript">like this one</a>) about typescript, they never quite seem to mention <a target="_blank" href="https://jsdoc.app/">jsdocs</a>. Therefore, it always feels like an unfair, one-sided comparison. Let's dive in some of the typical examples.</p>
<h2 id="heading-type-safety">Type safety</h2>
<p>This is probably the biggest argument... And while it's nice and convenient to be able to declare types directly, you can <a target="_blank" href="https://dev.to/t7yang/type-safety-in-javascript-with-jsdoc-and-vscode-1a28">do something quite similar with jsdocs</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">add</span>(<span class="hljs-params">a: <span class="hljs-built_in">number</span>, b: <span class="hljs-built_in">number</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">return</span> a + b;
}
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Add some numbers
 * <span class="hljs-doctag">@param <span class="hljs-type">{number}</span> <span class="hljs-variable">a</span></span>
 * <span class="hljs-doctag">@param <span class="hljs-type">{number}</span> <span class="hljs-variable">b</span></span>
 * <span class="hljs-doctag">@returns <span class="hljs-type">{number}</span></span>
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">add</span>(<span class="hljs-params">a, b</span>)</span>{
  <span class="hljs-keyword">return</span> a + b;
}
</code></pre>
<p>You might argue this adds quite a bit of boilerplate, but in most cases (something other than adding a and b) you would probably end up adding a docblock anyway to document the function usage, what the parameters actually do (not just their type...)... And best of all, with jsdocs, you could generate documentation from all this.</p>
<h2 id="heading-detect-errors">Detect errors</h2>
<p>When jsdocs is used, you can detect errors statically in just about the same way. Just create your jsconfig.json.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"checkJs"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"maxNodeModuleJsDepth"</span>: <span class="hljs-number">1</span>
  },
  <span class="hljs-attr">"input"</span>: [<span class="hljs-string">"src"</span>]
}
</code></pre>
<p>No need for typescript here...</p>
<h2 id="heading-interfaces">Interfaces ?</h2>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> Person {
  name: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">const</span> person: Person = {
  name: <span class="hljs-string">"Bob"</span>,
  age: <span class="hljs-number">30</span>
};
</code></pre>
<p>Is this really better than</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * <span class="hljs-doctag">@typedef <span class="hljs-type">{Object}</span> <span class="hljs-variable">Person</span></span>
 * <span class="hljs-doctag">@property <span class="hljs-type">{String}</span> <span class="hljs-variable">name</span></span>
 * <span class="hljs-doctag">@property <span class="hljs-type">{Number}</span> <span class="hljs-variable">age</span></span>
 */</span>

<span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{Person}</span> </span>*/</span>
<span class="hljs-keyword">const</span> person= { <span class="hljs-attr">name</span>: <span class="hljs-string">'Bob'</span>, <span class="hljs-attr">age</span>: <span class="hljs-number">30</span> };
</code></pre>
<p>And again... what if you need to document some other things along the way ?</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This is not to say that Typescript is bad. I'm sure it can help teams producing quality code. For my part, I don't like leaving up to some bundler to convert my code to something I don't control. I enjoy running native js code on the browser with no conversion whatsoever.</p>
<p>In any case, please stop saying that there is no way to type data in js without using typescript :-)</p>
]]></content:encoded></item><item><title><![CDATA[Toasts notification with a pop]]></title><description><![CDATA[Let's take this article about toaster and this article about custom elements and merge them together to create a new article about toast notification as custom elements.
I recently published pop-notify a custom element that is framework agnostic, ful...]]></description><link>https://blog.lekoala.be/toasts-notification-with-a-pop</link><guid isPermaLink="true">https://blog.lekoala.be/toasts-notification-with-a-pop</guid><category><![CDATA[toast notifications]]></category><category><![CDATA[Bootstrap]]></category><category><![CDATA[ES6]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Custom Elements]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 24 Nov 2023 10:55:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700823295634/f755222f-67af-4e0a-8bac-11381e0568e8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's take <a target="_blank" href="https://hashnode.com/post/ckzwv62ts001zn2nv0bl19ius">this article about toaster</a> and <a target="_blank" href="https://hashnode.com/post/cl52d445d01r3snnv9j6e7miw">this article about custom elements</a> and merge them together to create a new article about toast notification as custom elements.</p>
<p>I recently published <code>pop-notify</code> a custom element that is framework agnostic, fully accessible and html friendly. You can use it with any framework. It works great with bootstrap's toasts. You can use its default styles if you want. Or you can go on and make your own thing.</p>
<p>Here is the playground</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/NWoXRaV">https://codepen.io/lekoalabe/pen/NWoXRaV</a></div>
<p> </p>
<p><a target="_blank" href="https://github.com/lekoala/pop-notify">And here is the repository</a></p>
<p>Did I mention it's html friendly ? If you use something like <a target="_blank" href="https://htmx.org/">htmx</a>, this might just be a great way to send toast notification along with the html over the wire. Any notification will reassign itself to the toast container and be nicely displayed in a popover container (<a target="_blank" href="https://caniuse.com/?search=popover">if your browser supports it</a>, otherwise it's going to be a good old fixed div with a z-index).</p>
<p>I hope you have fun with it :-)</p>
]]></content:encoded></item><item><title><![CDATA[Pure css color palette using OKLCH]]></title><description><![CDATA[With OKLCH support being now supported in all major browsers, it is time to give a chance to the new OKLCH color. If you don't know what this is, please read this wonderful article by evil martians.
It is now easier than ever to create a color palett...]]></description><link>https://blog.lekoala.be/pure-css-color-palette-using-oklch</link><guid isPermaLink="true">https://blog.lekoala.be/pure-css-color-palette-using-oklch</guid><category><![CDATA[color]]></category><category><![CDATA[CSS]]></category><category><![CDATA[UI]]></category><category><![CDATA[Design]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Wed, 18 Oct 2023 09:29:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697621330033/19296cc1-6462-46af-ac73-5662afcc56fd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With <a target="_blank" href="https://caniuse.com/?search=oklch">OKLCH support being now supported in all major browsers</a>, it is time to give a chance to the new OKLCH color. If you don't know what this is, please read this <a target="_blank" href="https://evilmartians.com/chronicles/oklch-in-css-why-quit-rgb-hsl">wonderful article by evil martians</a>.</p>
<p>It is now easier than ever to create a color palette system using just a handful of css variables. Simply set the lightness from 0 to 100%, then, set up your chroma (an average value of 0.2 works well, but you might want to lower this to 0.10 or 0.15 for softer tones, or crank it up to 0.3 or more for vivid colors), and your hue (starting with 0 being pink, then move it up to 360... being pink again. Increments between 25 and 50 work well).... and you are good to go!</p>
<p>Here is a simple example with APCA contrast value being written in it to assess accessibility.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/dywxYoy">https://codepen.io/lekoalabe/pen/dywxYoy</a></div>
]]></content:encoded></item><item><title><![CDATA[Welcome to Belgium: e-Box Enterprise]]></title><description><![CDATA[As a web developer, it's always painful when you have to work with a badly designed web application. As a lucky Belgian and company owner, I need to connect to the "e-Box Enterprise". Let me tell you how painful this is.
Useless emails
The e-Box Ente...]]></description><link>https://blog.lekoala.be/welcome-to-belgium-e-box-enterprise</link><guid isPermaLink="true">https://blog.lekoala.be/welcome-to-belgium-e-box-enterprise</guid><category><![CDATA[belgium]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[web application]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 16 Jun 2023 08:44:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1686904911826/90dd4f7d-199e-47e7-9170-336a298c0f2c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a web developer, it's always painful when you have to work with a badly designed web application. As a lucky Belgian and company owner, I need to connect to the "e-Box Enterprise". Let me tell you how painful this is.</p>
<h2 id="heading-useless-emails">Useless emails</h2>
<p>The e-Box Enterprise aggregates messages from all sources for your company. Each time there is a new message, you get a notification by email telling you there is a new message available.</p>
<p>But that notification doesn't contain any information. Is it an important message? Is it urgent? Is there a deadline?</p>
<p>You don't know! So, each time, I need to connect to check what this is about. And most of the time, it's useless stuff like VAT notification.</p>
<p><strong>I wish there was more information in the email: sender, type of message, and deadline if any.</strong></p>
<p><em>EDIT December 2023 : good thing ! This is now a thing, they finally included the subject of the document :-) And I get the email in my own language.</em></p>
<h2 id="heading-so-slow">So slow</h2>
<p>Since you don't know what the notification is about, you need to connect. So, first you need to identify using ItsMe. That's not too bad, just make sure you get your phone near you.</p>
<p>And then, you enter a wonderfully (ahem) designed angular application that makes TONS of ajax calls to load data from various sources. So, obviously, you get a wonderful loader with no idea of progress, because how could they know how long it's going to take ?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686902494732/8b7f11bd-4586-4af0-b606-f407870ec367.png" alt class="image--center mx-auto" /></p>
<p>And when I say it's slow, it's like, really slow. Some calls take up to 13 seconds. When loading the e-Box, you have time to take your coffee. Sometimes it does load fast (when things are cached, I guess), but sometimes, half of the fetch calls fail with 403, 500 or CORS errors.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686901956940/d28b5ef9-ce9e-4cbd-8795-ab668cafc151.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-and-sometimes-it-doesnt-work">And sometimes, it doesn't work</h2>
<p>Once your coffee is finished, you get back to the app, and what do you see? You would expect a list of messages, but sometimes, it doesn't work, saying you don't have access to the application. Which is false, because sometimes it works just fine, with the same credentials, same urls, same browser, same everything.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686902457486/3d97f27f-29a1-4cad-8353-6223ea1afcd1.png" alt class="image--center mx-auto" /></p>
<p>And when it does manage to load the list, sometimes it fails at replacing the values. Ah, the good old {{value1}}...</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686904450284/f8c51927-8207-4c78-ad28-1188f3a5414c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-and-when-it-works-its-not-really-helpful">And when it works, it's not really helpful</h2>
<p>When everything works, you can read the message. So here I have a friendly message of MyMinfin... as a pdf attachment. So you download the pdf, called with the very unique name "message from myminfin" (i got another one which was called "mainDocument.pdf"... so you know, naming things is hard I know).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686904111290/e762e07b-dd08-4b5d-9ed3-7dc12914a299.png" alt class="image--center mx-auto" /></p>
<p>Do they know you can display the pdf in an iframe ? Nope...</p>
<p>And what's inside the pdf? You guessed it, a new button ! Because, you know, it would be too hard to put the actual message from MyMinfin in the e-Box, it's much more fun to have to go somewhere else to read the message.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686904613549/0e172bd6-0296-4b69-8cb3-dac568085f77.png" alt class="image--center mx-auto" /></p>
<p>So you go to MyMinfin. Where sometimes you need to login. AGAIN. Then you need to pick if you login for yourself or for your company. The whole thing cannot work automatically, because you clicked in a bloody link in a pdf. Then you go to a list of messages. Then you download another PDF.</p>
<p>And then you realize it's not relevant and you just lost 15 minutes out of nowhere.</p>
<p>Welcome to productive Belgium :-)</p>
]]></content:encoded></item><item><title><![CDATA[Finding a lightweight, yet extensible, Wysiwyg editor]]></title><description><![CDATA[I'm in the process of building a collection of custom elements for my web applications (more on that next time). Today I wanted to talk about a specific kind of input elements: wysiwyg editor.
When it comes to select an (open source, obviously) edito...]]></description><link>https://blog.lekoala.be/finding-a-lightweight-yet-extensible-wysiwyg-editor</link><guid isPermaLink="true">https://blog.lekoala.be/finding-a-lightweight-yet-extensible-wysiwyg-editor</guid><category><![CDATA[wysiwyg]]></category><category><![CDATA[editor]]></category><category><![CDATA[UI]]></category><category><![CDATA[Bootstrap]]></category><category><![CDATA[js]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 12 May 2023 09:57:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683885393146/37228222-8f9a-4598-bcea-a94fb7741745.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm in the process of building <a target="_blank" href="https://formidable-elements.vercel.app/">a collection of custom elements</a> for my web applications (more on that next time). Today I wanted to talk about a specific kind of input elements: wysiwyg editor.</p>
<p>When it comes to select an (open source, obviously) editor, there are a few choices that comes to mind. The most obvious being: TinyMCE. Did you already try to bundle this as a custom element (yes, <a target="_blank" href="https://github.com/tinymce/tinymce-webcomponent">there is one</a>, but it's basically a glorified loader) ? It leads to a file of about 1mb. That's a lot if you are just looking of adding a couple of styles to your text.</p>
<p>I've also tried Jodit, but that's not much better: around 700kb. Still a bit much in my opinion.</p>
<h2 id="heading-lets-try-squire">Let's try Squire</h2>
<p><a target="_blank" href="https://github.com/fastmail/Squire">Squire</a> is a lightweight editor that doesn't use the deprecated <code>exec</code> command. By default, it doesn't contain an ui so I had to create one, but that was quite straightforward. You can see an example below.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/dygeemP">https://codepen.io/lekoalabe/pen/dygeemP</a></div>
<p> </p>
<p>The good news? Its weight is about 60kb, ui included. On top of that, you need to add DOMPurifier if your browser doesn't support the <a target="_blank" href="https://blog.lekoala.be/modern-way-to-sanitize-your-data-using-the-sanitizer-api">sanitizer api</a>.</p>
<p>I think that's a really nice result! The <a target="_blank" href="https://github.com/fastmail/Squire/issues/432">only issue</a> I had was using paragraphs instead of divs by default.</p>
<p>But by design, Squire is also limited. What if you need to extend it? Let's have a look at another contender.</p>
<h2 id="heading-tiptap-the-headless-editor">Tiptap, the headless editor</h2>
<p><a target="_blank" href="https://tiptap.dev/">Titap</a> is an extensible, headless editor. Just like Squire, it doesn't contain a ui. But not problem, I just used the same as the one I've built for Squire :-)</p>
<p>Tiptap is built on top of ProseMirror, making it much more powerful, but also a bit more heavyweight. For example, ProseMirror includes its own sanitizer thanks to its dom parser, so it's bound to be heavier than an alternative that can use the sanitizer api.</p>
<p>How does it look? You bet: just the same :-) Its weight? Just about 300kb, so 5 times more than Squire, but it includes a few more tricks by default (like, a proper code block, horizontal rules, and typography rules...).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/JjmvvZg">https://codepen.io/lekoalabe/pen/JjmvvZg</a></div>
<p> </p>
<p>Why would you want to use it instead of Squire? Because it's extensible! You can add many plugins so for more complex projects, that's definitely the way to go.</p>
<p>And that's it for today! Happy editing :-)</p>
]]></content:encoded></item><item><title><![CDATA[Modern way to sanitize your data using the Sanitizer API]]></title><description><![CDATA[If you don't know what the Sanitizer API is, I highly recommend reading this article. So, that's all well and good... but that's only supported in Chrome, so that's going to be a problem.
Using a polyfill ?
The obvious solution is to use a polyfill, ...]]></description><link>https://blog.lekoala.be/modern-way-to-sanitize-your-data-using-the-sanitizer-api</link><guid isPermaLink="true">https://blog.lekoala.be/modern-way-to-sanitize-your-data-using-the-sanitizer-api</guid><category><![CDATA[js]]></category><category><![CDATA[api]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[sanitizer]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Fri, 05 May 2023 10:12:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1683281442585/a617ec92-0ff4-47ba-a8fc-6b98fd45ea29.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you don't know what the Sanitizer API is, I highly recommend reading <a target="_blank" href="https://web.dev/sanitizer/">this article</a>. So, that's all well and good... but that's only supported in <a target="_blank" href="https://caniuse.com/mdn-api_sanitizer">Chrome</a>, so that's going to be a problem.</p>
<h2 id="heading-using-a-polyfill">Using a polyfill ?</h2>
<p>The obvious solution is <a target="_blank" href="https://github.com/mozilla/sanitizer-polyfill">to use a polyfill</a>, which uses DOMPurify under the hood. The issue with this is that unless you use feature detection to include the polyfill, you are going to include it in Chrome even if you don't need it. And if you use feature detection, you need to wait until the polyfill is loaded before using any code using the polyfill, which may be difficult to detect.</p>
<h2 id="heading-a-simpler-approach">A simpler approach</h2>
<p>In my case, all I needed is the <code>setHTML</code>. I don't really care about the rest of the spec.</p>
<p>So let's see what we got:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * <span class="hljs-doctag">@link </span>https://web.dev/sanitizer/
 * <span class="hljs-doctag">@link </span>https://sanitizer-api.dev/
 * <span class="hljs-doctag">@param <span class="hljs-type">{HTMLElement}</span> </span>el (NOT a template element)
 * <span class="hljs-doctag">@param <span class="hljs-type">{string}</span> <span class="hljs-variable">html</span></span>
 * <span class="hljs-doctag">@param <span class="hljs-type">{Boolean}</span> <span class="hljs-variable">force</span></span>
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setHTML</span>(<span class="hljs-params">el, html, force = false</span>) </span>{
  <span class="hljs-comment">//@ts-ignore</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.Sanitizer &amp;&amp; !force) {
    <span class="hljs-comment">//@ts-ignore</span>
    el.setHTML(html);
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>[<span class="hljs-string">"DOMPurify"</span>]) {
    el.innerHTML = <span class="hljs-built_in">window</span>[<span class="hljs-string">"DOMPurify"</span>].sanitize(html, {
      <span class="hljs-attr">WHOLE_DOCUMENT</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">FORCE_BODY</span>: <span class="hljs-literal">false</span>,
    });
  } <span class="hljs-keyword">else</span> {
    el.innerHTML = html;
  }
}

<span class="hljs-comment">/**
 * Set the global DOMPurify if missing and if Sanitizer api is not available
 * Call this with await before any call to setHTML
 * <span class="hljs-doctag">@param <span class="hljs-type">{Boolean}</span> <span class="hljs-variable">force</span></span>
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loadDOMPurify</span>(<span class="hljs-params">force = false</span>) </span>{
  <span class="hljs-comment">//@ts-ignore</span>
  <span class="hljs-keyword">if</span> ((!<span class="hljs-built_in">window</span>.Sanitizer || force) &amp;&amp; !<span class="hljs-built_in">window</span>[<span class="hljs-string">"DOMPurify"</span>]) {
    <span class="hljs-comment">//@ts-ignore</span>
    <span class="hljs-built_in">window</span>[<span class="hljs-string">"DOMPurify"</span>] = (<span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">"https://cdn.jsdelivr.net/npm/dompurify@3/+esm"</span>)).default;
  }
}
</code></pre>
<p>Import this little snippet, and you are good to go. Call <code>await loadDOMPurify</code> before any code using the <code>setHTML</code> helper. This will load DOMPurify using dynamic import from the cdn (or feel free to use your own local path for this).</p>
<p>Neat, no? :-) I've been using this in my WIP <a target="_blank" href="https://github.com/lekoala/formidable-elements">formidable elements library</a>.</p>
<p>EDIT: it seems the current draft has been deprecated, see here: <a target="_blank" href="https://developer.chrome.com/blog/sanitizer-api-deprecation?hl=fr">https://developer.chrome.com/blog/sanitizer-api-deprecation?hl=fr</a></p>
]]></content:encoded></item><item><title><![CDATA[Many ways to deal with events in javascript]]></title><description><![CDATA[Do you remember the time where you discovered document.ready ? I do, it seemed like magic: run the code once your html is loaded. Then, you initialise your jQuery plugins, and you are good to go...
But recently, I've built a SPA (to be more precise, ...]]></description><link>https://blog.lekoala.be/many-ways-to-deal-with-events-in-javascript</link><guid isPermaLink="true">https://blog.lekoala.be/many-ways-to-deal-with-events-in-javascript</guid><category><![CDATA[js]]></category><category><![CDATA[event listener ]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[event delegation]]></category><category><![CDATA[Custom Elements]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Thu, 20 Apr 2023 12:45:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681994710611/24b9f6e4-b277-4133-b89b-93bc34af3a0b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Do you remember the time where you discovered <code>document.ready</code> ? I do, it seemed like magic: run the code once your html is loaded. Then, you initialise your jQuery plugins, and you are good to go...</p>
<p>But recently, I've built a SPA (to be more precise, a PWA-SPA, which is a funky acronym if you ask me, or should we say a PSPWA ?). And I'm also dealing with ajax loaded interfaces for my <a target="_blank" href="https://blog.lekoala.be/admini-the-minimalistic-admin-panel-built-with-bootstrap-52">admini panel</a>.</p>
<p>And you know what? Suddenly, <code>document.ready</code> is not so magical anymore: your ajax loaded content does not trigger <code>DOMContentLoaded</code> . Your nodes get initialized twice. Event handlers are getting crazy. Stuff gets removed from the DOM. Not nice.</p>
<h2 id="heading-a-more-robust-initialization-phase">A more robust initialization phase</h2>
<p>What's becoming very clear, is that as soon as you do anything a bit more complex, you cannot rely on <code>DOMContentLoaded</code> anymore. Take <a target="_blank" href="https://unpoly.com/up.compiler">Unpoly</a> for example. They have this special <code>up.compile</code> function which parse each fragment loaded into the page. Nice, but then you need to tweak everything to work with this. And what if you want to switch to <a target="_blank" href="https://turbo.hotwired.dev/">Turbo</a> for example?</p>
<p>The first step is to make sure you don't initialize things twice. This can be easy and simple. Simply call a initialize function which takes care of doing everything you need and add the element to a WeakSet. It won't prevent garbage collection once it leaves the dom, and you can just fire your big "init" function which contains multiple initialize calls.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="7ec2732e2bca73363daeac2d86130b93"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/lekoala/7ec2732e2bca73363daeac2d86130b93" class="embed-card">https://gist.github.com/lekoala/7ec2732e2bca73363daeac2d86130b93</a></div><p> </p>
<h2 id="heading-but-you-could-make-your-life-easier">But you could make your life easier</h2>
<p>Most of the time, you want to listen to event, and add some initialization code. Therefore, calling <code>addEventListener</code> yourself might be a bit verbose, so you can go this way.</p>
<p>Again, the goal here is to prevent the same handler to fire twice, so in our case, we create a registry of events for each element and only add it once. And then, all you need is <code>listen('.my-elem', 'click', someHandler)</code></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="4a8127e35b6d2baeae42a8e2aea222de"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/lekoala/4a8127e35b6d2baeae42a8e2aea222de" class="embed-card">https://gist.github.com/lekoala/4a8127e35b6d2baeae42a8e2aea222de</a></div><p> </p>
<h2 id="heading-but-what-about-ajax-content">But what about ajax content?</h2>
<p>Yes, I hear you. This is all nice and well, but you still need to call your big old global init function each time some part of the dom changes. What if somehow this could be done automatically?</p>
<p>There are three ways to deal with this:</p>
<ul>
<li><p>Event delegation: since we listen at document level, new element works transparently</p>
</li>
<li><p>Mutation observer: bind/unbind behaviour dynamically as stuff gets added to the dom</p>
</li>
<li><p>Self initializing elements: with custom elements, it's now trivial to have elements that take care of themselves</p>
</li>
</ul>
<p>Let's look at these solutions.</p>
<h2 id="heading-event-delegation">Event delegation</h2>
<p>I didn't find a library I liked, so, I've tried a little something on my own. That also let me test and try things :-)</p>
<p>It works like <code>listen</code> above, but instead of adding a listener to the element, we add everything to the document. We define a global listener. Then, we leverage <code>closest</code> to match the actual target during the capture phase.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="2c27724c809e1c5338615e4cc2e1d743"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/lekoala/2c27724c809e1c5338615e4cc2e1d743" class="embed-card">https://gist.github.com/lekoala/2c27724c809e1c5338615e4cc2e1d743</a></div><p> </p>
<h2 id="heading-mutation-observer">Mutation observer</h2>
<p>This is the big one. Inspired by <a target="_blank" href="https://github.com/hafriedlander/jquery.entwine">jquery.entwine</a> the entwine function lets you define a set of definitions for a given selector. For example:</p>
<pre><code class="lang-javascript">    entwine(<span class="hljs-string">'.my-elem'</span>, {
      <span class="hljs-attr">click</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">e, el</span>) </span>{
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"click"</span>, e, <span class="hljs-built_in">this</span>, el, el === <span class="hljs-built_in">this</span>);
      },
      <span class="hljs-attr">mouseenter</span>: <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"enter"</span>, e);
      },
      <span class="hljs-attr">mouseleave</span>: <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"leave"</span>, e);
      },
    });
</code></pre>
<p>There is also support for <code>connected</code> and <code>disconnected</code> events thanks to the mutation observer. I added namespaced definitions support like in the original lib, otherwise multiple definitions matching the same selector would get merged together.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="5312791d0be841c64e217594815b3d42"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/lekoala/5312791d0be841c64e217594815b3d42" class="embed-card">https://gist.github.com/lekoala/5312791d0be841c64e217594815b3d42</a></div><p> </p>
<h2 id="heading-self-initializing-elements">Self initializing elements</h2>
<p>Last, but not least, <a target="_blank" href="https://blog.lekoala.be/self-initializing-elements-with-super-powers">a topic I've already discussed on this blog</a>: self-initializing elements. If you can afford to go the custom elements way, they will initialize themselves.</p>
<p>The only real downside with this is that is that you are forced to use dedicated html, so that may not work too well if you cannot change how your html is done.</p>
<p><a target="_blank" href="https://github.com/lekoala/modular-behaviour.js">Check the repo</a> for modular behaviour</p>
<h2 id="heading-also-worth-a-visit">Also worth a visit</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/thednp/event-listener">Event Listener</a> from thednp</p>
</li>
<li><p><a target="_blank" href="https://unpoly.com/up.compiler">Unpoly.compile</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Querying elements in a typesafe manner]]></title><description><![CDATA[If you are like me, you may not like typescript but you do like proper types. With jsdoc, it's easy enough to add proper typing to any bit of vanilla javascript code.
However, things are not so great when you use the querySelector and querySelectorAl...]]></description><link>https://blog.lekoala.be/querying-elements-in-a-typesafe-manner</link><guid isPermaLink="true">https://blog.lekoala.be/querying-elements-in-a-typesafe-manner</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[VSCode Tips]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Tue, 11 Apr 2023 09:25:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681204624188/b3a9dc84-f155-4bc6-af78-01d99df263da.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you are like me, you may not like typescript but you do like proper types. With jsdoc, it's easy enough to add proper typing to any bit of vanilla javascript code.</p>
<p>However, things are not so great when you use the <code>querySelector</code> and <code>querySelectorAll</code> api, because it returns a generic <code>Element</code>. How to fix this ?</p>
<h2 id="heading-the-issue">The issue</h2>
<p>You want typesafe js, so you enable this in your project in the <code>jsconfig.json</code>file</p>
<pre><code class="lang-javascript">{
  <span class="hljs-string">"compilerOptions"</span>: {
    <span class="hljs-string">"target"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-string">"module"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-string">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-string">"lib"</span>: [<span class="hljs-string">"es2017"</span>, <span class="hljs-string">"dom"</span>],
    <span class="hljs-string">"allowJs"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"checkJs"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"noEmit"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"strict"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-string">"noImplicitOverride"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-string">"noImplicitThis"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"alwaysStrict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"esModuleInterop"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-string">"include"</span>: [<span class="hljs-string">"src/*.js"</span>],
  <span class="hljs-string">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>, <span class="hljs-string">"./test/"</span>]
}
</code></pre>
<p>Let's say you have a form with an <code>input</code>. Maybe you do something like this:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> myInput = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'form input'</span>);
myInput.checked = <span class="hljs-literal">true</span>; <span class="hljs-comment">// you get "Property checked does not exist on Element"</span>
</code></pre>
<p>The obvious fix: add some inline jsdoc</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
* <span class="hljs-doctag">@type <span class="hljs-type">{HTMLInputElement}</span></span>
*/</span>
<span class="hljs-keyword">const</span> myInput = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">'form input'</span>);
myInput.checked = <span class="hljs-literal">true</span>; <span class="hljs-comment">// yay!</span>
</code></pre>
<p>This works, but it's not so great when you need to deal with collections. Sure, it works, but it's not exactly developer friendly.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'form input.checkbox'</span>).forEach(<span class="hljs-function">(<span class="hljs-params">input</span>) =&gt;</span> {
    input.checked = <span class="hljs-literal">true</span>; <span class="hljs-comment">// Huho... again ?</span>
});

<span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">'form input.checkbox'</span>).forEach(
<span class="hljs-comment">/**
* @param {HTMLInputElement} input
*/</span>
<span class="hljs-function">(<span class="hljs-params">input</span>) =&gt;</span> {
    input.checked = <span class="hljs-literal">true</span>; <span class="hljs-comment">// yay!</span>
});
</code></pre>
<h2 id="heading-a-better-solution">A better solution?</h2>
<p>Introducing <code>q</code> the only function you need to query your dom! If you were missing jQuery ease of use, this might just be your new best friend.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/**
 * Query elements in a typesafe manner
 *
 * You can declare your owns tags in the HTMLElementTagNameMap namespace
 *
 * ```js
 * // my-tag.d.ts
 * declare global {
 *   interface HTMLElementTagNameMap {
 *      "my-tag": MyTag;
 *   }
 * }
 *</span>
</code></pre>
<p> *</p>
<ul>
<li>@template {keyof HTMLElementTagNameMap} K</li>
<li>@param {K|String} tagName Name of the element, or global selector string (returns any element).</li>
<li>@param {String} selector Selector appended to the type. If it contains a space, type is ignored.</li>
<li>@param {Document|HTMLElement} ctx Context (document by default). If a context is specified, :scope is applied</li>
<li>@returns {Array}
*/
export default function q(tagName, selector = '', ctx = document) {
// Don't prepend the type of we are asking for children, eg: #my-element type
selector = selector.includes(' ') ? selector : <code>${tagName}${selector}</code>;
// Needed for direct children queries
// @link https://developer.mozilla.org/en-US/docs/Web/CSS/:scope#direct_children
// Needed to avoid inconsistent behaviour
// @link https://lists.w3.org/Archives/Public/public-webapi/2008Apr/0251.html
if (!(ctx instanceof Document)) {
 selector = <code>:scope ${selector}</code>;
}
return Array.from(ctx.querySelectorAll(selector));
}
```</li>
</ul>
<p>Let's revisit our previous examples</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> myInput = q(<span class="hljs-string">'input'</span>, <span class="hljs-string">'form input'</span>)[<span class="hljs-number">0</span>];
myInput.checked = <span class="hljs-literal">true</span>; <span class="hljs-comment">// yay!</span>

q(<span class="hljs-string">'input'</span>, <span class="hljs-string">'form input.checkbox'</span>).forEach(<span class="hljs-function">(<span class="hljs-params">input</span>) =&gt;</span> {
    input.checked = <span class="hljs-literal">true</span>; <span class="hljs-comment">// yay!</span>
});
</code></pre>
<p>Not sure yet if it's worth it to make another function that returns only one element using <code>querySelector</code>, but that would be easy enough to create on your own :-)</p>
<h2 id="heading-some-extra-goodies">Some extra goodies</h2>
<p>You might have noticed that the code does a little bit more than that. Let's see the extra goodies included.</p>
<p>You can easily query an element and add more specificity.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> inputs = q(<span class="hljs-string">'input'</span>, <span class="hljs-string">'.checkbox'</span>); <span class="hljs-comment">// will query input.checkbox</span>
</code></pre>
<p>When querying children nodes (as soon as you have a space in the selector), the tag name is ignored.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> inputs = q(<span class="hljs-string">'input'</span>, <span class="hljs-string">'form input'</span>); <span class="hljs-comment">// will query form input</span>
</code></pre>
<p>Fragments are properly scoped by adding <code>:scope</code> automatically. This is almost always the desired behavior because it allows using direct selectors without extra syntax, and it will make sure you don't get elements outside of the context.</p>
<p>This scope issue was so bad in even wondered if it makes sense to allow this third parameter.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> form = q(<span class="hljs-string">'form'</span>)[<span class="hljs-number">0</span>]; <span class="hljs-comment">// will query form</span>
<span class="hljs-keyword">const</span> formInputs = q(<span class="hljs-string">'input'</span>, <span class="hljs-string">'.container .checkbox'</span>, form); <span class="hljs-comment">// will query :scope .container input.checkbox</span>
<span class="hljs-keyword">const</span> directInputs = q(<span class="hljs-string">'input'</span>, <span class="hljs-string">'&gt; .checkbox'</span>, form); <span class="hljs-comment">// will query :scope &gt; input.checkbox</span>
</code></pre>
<p>You can still query multiple elements if you want.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> mixed = q(<span class="hljs-string">'label,input'</span>); <span class="hljs-comment">// still working fine since you can pass a regular string, but obviously, you don't get a proper type back</span>
</code></pre>
<p>You get an array as a result... meaning, you can map, filter... with properly typed element inside your callback function.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> allNames = formInputs.map(<span class="hljs-function">(<span class="hljs-params">el</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> el.getAttribute(<span class="hljs-string">'name'</span>);
});
</code></pre>
<p>You can extend the list of tags if you use custom elements.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">declare</span> <span class="hljs-built_in">global</span> {
   <span class="hljs-keyword">interface</span> HTMLElementTagNameMap {
      <span class="hljs-string">"my-tag"</span>: MyTag;
   }
}
</code></pre>
<p>And let the magic happen.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> myTag = q(<span class="hljs-string">'my-tag'</span>); <span class="hljs-comment">// who gets a properly typed element ?</span>
</code></pre>
<p>You can play with it here (check the console).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/XWxbQJe">https://codepen.io/lekoalabe/pen/XWxbQJe</a></div>
]]></content:encoded></item><item><title><![CDATA[Splitting your media queries into multiple files]]></title><description><![CDATA[Before the summer, I had the pleasure to read this article that talked about splitting your css into multiple files to optimize file size. At the time, I thought: well, this is nice, but that is going to be cumbersome to implement.
Recently, I made a...]]></description><link>https://blog.lekoala.be/splitting-your-media-queries-into-multiple-files</link><guid isPermaLink="true">https://blog.lekoala.be/splitting-your-media-queries-into-multiple-files</guid><category><![CDATA[Bootstrap]]></category><category><![CDATA[Bootstrap 5]]></category><category><![CDATA[CSS]]></category><category><![CDATA[Sass]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Mon, 20 Feb 2023 17:28:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1676913847051/bb2e1576-395d-43aa-9c89-ff73b8a95412.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before the summer, I had the pleasure to read <a target="_blank" href="https://alistapart.com/article/mobile-first-css-is-it-time-for-a-rethink/">this article</a> that talked about splitting your css into multiple files to optimize file size. At the time, I thought: well, this is nice, but that is going to be cumbersome to implement.</p>
<p>Recently, I made a small one-pager and I decided to give it a try. On this website, I'm using bootstrap 5 and I'm going to show you how I manage to get it working.</p>
<h2 id="heading-what-are-we-working-towards">What are we working towards?</h2>
<p>The end goal is to have something like this:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/css/default.min.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/css/mobile.min.css"</span> <span class="hljs-attr">media</span>=<span class="hljs-string">"screen and (max-width: 767.98px)"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/css/tablet.min.css"</span> <span class="hljs-attr">media</span>=<span class="hljs-string">"screen and (min-width: 768px) and (max-width: 991.98px)"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/css/desktop.min.css"</span> <span class="hljs-attr">media</span>=<span class="hljs-string">"screen and (min-width: 992px)"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/css/print.min.css"</span> <span class="hljs-attr">media</span>=<span class="hljs-string">"print"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
</code></pre>
<p>At the same time, in my source file, I don't want to split my logic between files. This means, that the following scss code:</p>
<pre><code class="lang-scss"><span class="hljs-comment">// This should go to default.min.css</span>
<span class="hljs-selector-class">.col-separator</span> {
  <span class="hljs-attribute">border-right</span>: <span class="hljs-number">1px</span> solid <span class="hljs-variable">$primary</span>;
}
<span class="hljs-comment">// This should go to mobile.min.css</span>
<span class="hljs-keyword">@include</span> media-breakpoint-down(md) {
  <span class="hljs-selector-class">.col-separator</span> {
    <span class="hljs-attribute">border-right</span>: <span class="hljs-number">0</span>;
  }
}
</code></pre>
<p>Should get split properly between multiple files. While scss is really great at combining files, it does not provide much out of the box to split files.</p>
<h2 id="heading-getting-your-structure-ready">Getting your structure ready</h2>
<p>First things first, let's get our files in a proper structure. In my workflow, where I'm using plain dart sass cli tooling, I need to find a way to have my files sent to the /public/css folder. I do it like this:</p>
<pre><code class="lang-json"> <span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"esbuild --bundle --minify --format=esm --sourcemap src/index.js --outfile=public/js/index.min.js"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"npm run build -- --servedir=./public"</span>,
    <span class="hljs-attr">"watch"</span>: <span class="hljs-string">"npm run build -- --watch"</span>,
    <span class="hljs-attr">"compile-css"</span>: <span class="hljs-string">"sass scss/dist:public/css --load-path=node_modules/"</span>,
    <span class="hljs-attr">"compile-min-css"</span>: <span class="hljs-string">"sass scss/min:public/css --style compressed --load-path=node_modules/"</span>,
    <span class="hljs-attr">"watch-css"</span>: <span class="hljs-string">"npm run compile-css -- --watch --poll"</span>,
    <span class="hljs-attr">"watch-min-css"</span>: <span class="hljs-string">"npm run compile-min-css -- --watch --poll"</span>,
    <span class="hljs-attr">"prefix-css"</span>: <span class="hljs-string">"postcss public/css/*.min.css --replace --use autoprefixer"</span>,
    <span class="hljs-attr">"build-all"</span>: <span class="hljs-string">"npm run build &amp;&amp; npm run compile-css &amp;&amp; npm run compile-min-css &amp;&amp; npm run prefix-css"</span>
  }
</code></pre>
<p>I create two folders: one <code>scss/dist</code> and one <code>scss/min</code>. Both of them get their own compile command with dedicated settings. If you only want minified files, it gets even simpler.</p>
<p>In these folders, I create the following files:</p>
<ul>
<li><p>default(.min).scss : this is the general stylesheet that get non specific styles</p>
</li>
<li><p>desktop(.min).scss : for desktop (&gt; 992px)</p>
</li>
<li><p>mobile(.min).scss: for mobile (&lt; 768px)</p>
</li>
<li><p>tablet(.min).scss: for tablet (between 768 and 992px). Not sure this one is actually worth it, feel free to remove it and put these styles in default instead.</p>
</li>
</ul>
<p>The content of these files is the same:</p>
<pre><code class="lang-scss"><span class="hljs-variable">$css-sheet</span>: <span class="hljs-string">"mobile"</span>; <span class="hljs-comment">// change according to current file</span>
<span class="hljs-keyword">@import</span> <span class="hljs-string">"../bootstrap/bootstrap-common"</span>;
<span class="hljs-keyword">@import</span> <span class="hljs-string">"../index"</span>;
</code></pre>
<p>The only difference is this mysterious <code>css-sheet</code> variable.</p>
<h2 id="heading-lets-get-the-print-out-of-the-way">Let's get the print out of the way</h2>
<p>But before diving into the media queries, let's go the print styles out of the way. <a target="_blank" href="https://christianoliff.com/blog/bootstrap-5-print-stylesheet/">Bootstrap 5 removed their print styles</a>, so if you want to restore them, you need to create a distinct file. <a target="_blank" href="https://gist.github.com/lekoala/33aa29a1a4bf1a65cc426c366d02d7e6">You can find its content here</a>, but the general idea is like this:</p>
<ul>
<li><p>Creating some nice default print styles</p>
</li>
<li><p>Include print utils through a reduced utilities api</p>
</li>
<li><p>Remove the print utils from the regular utilities api to prevent their inclusion in default.css</p>
</li>
</ul>
<p>Ok, simple enough. Let's have a look at the media queries now.</p>
<h2 id="heading-sending-media-queries-to-distinct-files">Sending media queries to distinct files</h2>
<p>The main issue with my first snippet is that there is no way to know what should belong to the "default" stylesheet. Let's solve this:</p>
<pre><code class="lang-scss">
<span class="hljs-keyword">@include</span> media-default {
  <span class="hljs-selector-class">.col-separator</span> {
    <span class="hljs-attribute">border-right</span>: <span class="hljs-number">1px</span> solid <span class="hljs-variable">$primary</span>;
  }
}
<span class="hljs-keyword">@include</span> media-breakpoint-down(md) {
  <span class="hljs-selector-class">.col-separator</span> {
    <span class="hljs-attribute">border-right</span>: <span class="hljs-number">0</span>;
  }
}
</code></pre>
<p>Notice the updated <code>media-default</code> mixin. This is actually kind of nice because it allows you to treat "base" scss code into its own block. The issue with this is that my approach doesn't work for code that is not organized to work with it, so base bootstrap code will have to be sent to default.scss.</p>
<p>Remember the little <code>css-sheet</code> variable? This variable will help us to track which file we are currently processing and include (or not) the content based on it. This require <a target="_blank" href="https://gist.github.com/lekoala/22179f272a76df682a5882cf791da499">overriding the default media queries mixins with the following code</a>. Again, feel free to read to code (warning! while it seems to be working all right, there might be edge cases that I've missed), but the basic idea is this:</p>
<ul>
<li><p>We know that some media queries are only relevant to a specific breakpoint and don't need to be included for some viewports (eg: include media-breakpoint-down(md) should only be included in mobile.css).</p>
</li>
<li><p>Knowing that, it's easy enough to process the media query mixins according to the current sheet being processed. Based on that, we ignore or include the content. We can even get rid of the top level media query if it matches the stylesheet criteria.</p>
</li>
<li><p>Our new mixin <code>media-default</code> allows us to flag content that should be included in the default.css and ignored on viewport specific sheets.</p>
</li>
</ul>
<p>And that's it! As mentioned, this approach doesn't help us with base bootstrap code, but any custom code will be split properly into its own file.</p>
<p>At the end of the day, for my project, it proved not so effective because most of the code still belongs to default.css and due to bootstrap size, css splitting by viewport does not bring many benefits. But I can imagine that a framework engineered for these viewports or for large websites, this approach could provide a real size reduction.</p>
]]></content:encoded></item><item><title><![CDATA[Beautifully animated toasts stacks]]></title><description><![CDATA[I've already talked about my "toaster" library in a previous article. Today, I've released a rather nice update that makes the Bootstrap toasts stack much nicer.
What is the issue?
As strange as it may sound, Bootstrap toast stacks are not well-desig...]]></description><link>https://blog.lekoala.be/beautifully-animated-toasts-stacks</link><guid isPermaLink="true">https://blog.lekoala.be/beautifully-animated-toasts-stacks</guid><category><![CDATA[Bootstrap]]></category><category><![CDATA[Bootstrap 5]]></category><category><![CDATA[js]]></category><category><![CDATA[toast]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Thu, 22 Dec 2022 12:28:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671711637521/BgUWhn7nj.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've already talked about <a target="_blank" href="https://blog.lekoala.be/bootstrap-5-toaster">my "toaster" library</a> in a previous article. Today, I've released a rather nice update that makes the Bootstrap toasts stack much nicer.</p>
<h2 id="heading-what-is-the-issue">What is the issue?</h2>
<p>As strange as it may sound, Bootstrap toast stacks are not well-designed for animated toasts (in my opinion, anyway...). This means that it looks great for static toasts, but as soon as you try to add or remove toasts in them, they move in a very "jumpy" way due to the <code>display: none</code> being applied at the end of the animation.</p>
<p>This can be seen in the codepen below</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/poKMprj">https://codepen.io/lekoalabe/pen/poKMprj</a></div>
<p> </p>
<h2 id="heading-what-is-the-solution">What is the solution?</h2>
<p>The solution is simply to animate the margin (top or bottom, depending of the position of the order of your stack) by the height of the toast. This require disabling the <code>display:none</code> being applied by default and in my case, it's not an issue, since I'm working with dynamic toasts that get removed from the dom anyway (<a target="_blank" href="https://github.com/twbs/bootstrap/issues/37265">after some delay...</a>).</p>
<hr />
<p>These changes have already been integrated in my <a target="_blank" href="https://github.com/lekoala/bs-companion">bs-companion library</a> and <a target="_blank" href="https://bs-companion.vercel.app/toaster.html">can be seen lives here</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Autocomplete and accessibility]]></title><description><![CDATA[Since I created my Bootstrap Tags library, I realized that it also contained most of what was needed to create an Autocomplete library. I gave it a try with some much-needed code refactoring and while creating the library, I stumbled upon an interest...]]></description><link>https://blog.lekoala.be/autocomplete-and-accessibility</link><guid isPermaLink="true">https://blog.lekoala.be/autocomplete-and-accessibility</guid><category><![CDATA[autocompletion]]></category><category><![CDATA[js]]></category><category><![CDATA[Bootstrap]]></category><category><![CDATA[Bootstrap 5]]></category><category><![CDATA[Accessibility]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Tue, 08 Nov 2022 11:04:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1667904487867/o2YRgM1ob.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Since I created my <a target="_blank" href="https://blog.lekoala.be/bootstrap-5-tags">Bootstrap Tags library</a>, I realized that it also contained most of what was needed to create an <a target="_blank" href="https://github.com/lekoala/bootstrap5-autocomplete">Autocomplete library</a>. I gave it a try with some much-needed code refactoring and while creating the library, I stumbled upon an interesting issue.</p>
<h2 id="heading-to-tab-or-not-to-tab">To tab or not to tab</h2>
<p>The issue is the following: while you type, you show suggestions. The focus is still in the input field, and yet, you should somehow show to the user what is the currently matched suggestion (I'm using <code>bg-primary</code> to show this). I cannot use hover styles, because you can hover a proposal, and yet, change suggestions with the arrow key. I cannot use the focus styles, because they don't exist as a pseudo class (and I really want to avoid any extra css for my library). Also, focus styles are ugly :-)</p>
<p>But the, I realized that when pressing tab, I was moving the focus to the suggestions. At first, <strong>it seemed like a great idea</strong>: this way, users can tab through proposals. And at the same time, <strong>it also seems like a really bad idea</strong>: because users need to tab through all proposals to move to the next input field.</p>
<h2 id="heading-tab-vs-arrow-keys">Tab vs arrow keys</h2>
<p>I wondered: how does behave a <a target="_blank" href="https://developer.mozilla.org/fr/docs/Web/HTML/Element/datalist">native datalist</a> ? Turns out: you need to use arrow keys to move through proposals, and tab move you to the next element. So I decided to do just that: I added a <code>tabindex=-1</code> and it works fine. You can still use arrow keys as before to move through suggestions.</p>
<p>But is it accessible? How would the user know which proposal is selected if he is blind? I guess I can use <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions">Live Regions</a> but I'm not sure how well that is supported (sometimes, the specs are wonderful, but real life isn't).</p>
<p>At least, with tabs, you know for sure that when focusing an element, it will get announced. I'm still on the fence regarding this, but I will definitely investigate and make sure it's working as intended for version 1.1.</p>
<h2 id="heading-full-demo">Full demo</h2>
<p>If you are curious, here is a demo of the new library</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codepen.io/lekoalabe/pen/MWXydNQ">https://codepen.io/lekoalabe/pen/MWXydNQ</a></div>
<p>You can also <a target="_blank" href="https://github.com/lekoala/bootstrap5-autocomplete">head to the repo</a></p>
<h2 id="heading-extra-tip-using-regions">Extra tip: using regions</h2>
<p>Since I didn't want to split my library into multiple files, but still keep it manageable, I discovered the region/endregion syntax in vs code that allows to collapse/expand whole section of code. It also helps to keep your code structured and grouped in a meaningful way.</p>
<pre><code class="lang-js"><span class="hljs-comment">// #region REGION_NAME_HERE</span>

... some code

<span class="hljs-comment">// #endregion</span>
</code></pre>
]]></content:encoded></item><item><title><![CDATA[The only snippet you will need to deal with push notifications in a service worker]]></title><description><![CDATA[I'm working on a PWA at the moment. What a great time it is to be able to develop near native looking mobile applications without having to deal with flutter, native code, etc.
One part in particular is great: push notifications. Now that Safari 16 h...]]></description><link>https://blog.lekoala.be/the-only-snippet-you-will-need-to-deal-with-push-notifications-in-a-service-worker</link><guid isPermaLink="true">https://blog.lekoala.be/the-only-snippet-you-will-need-to-deal-with-push-notifications-in-a-service-worker</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[PWA]]></category><category><![CDATA[push notifications]]></category><dc:creator><![CDATA[Thomas Portelange]]></dc:creator><pubDate>Wed, 26 Oct 2022 11:56:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1666785373464/bglPvziJN.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm working on a PWA at the moment. What a great time it is to be able to develop near native looking mobile applications without having to deal with flutter, native code, etc.</p>
<p>One part in particular is great: push notifications. Now that Safari 16 has added support for them, it's not realistic to actually use this in production.</p>
<h2 id="heading-show-me-the-snippet">Show me the snippet</h2>
<p>I know, i know... let's get to the point of this article: the snippet.</p>
<pre><code class="lang-js"><span class="hljs-comment">// @link https://flaviocopes.com/push-api/</span>
<span class="hljs-comment">// @link https://web.dev/push-notifications-handling-messages/</span>
self.addEventListener(<span class="hljs-string">'push'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
  <span class="hljs-keyword">if</span> (!event.data) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'This push event has no data.'</span>);
    <span class="hljs-keyword">return</span>;
  }
  <span class="hljs-keyword">if</span> (!self.registration) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Service worker does not control the page'</span>);
    <span class="hljs-keyword">return</span>;
  }
  <span class="hljs-keyword">if</span> (!self.registration || !self.registration.pushManager) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Push is not supported'</span>);
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-keyword">const</span> eventText = event.data.text();
  <span class="hljs-comment">// Specify default options</span>
  <span class="hljs-keyword">let</span> options = {};
  <span class="hljs-keyword">let</span> title = <span class="hljs-string">''</span>;

  <span class="hljs-comment">// Support both plain text notification and json</span>
  <span class="hljs-keyword">if</span> (eventText.substr(<span class="hljs-number">0</span>, <span class="hljs-number">1</span>) === <span class="hljs-string">'{'</span>) {
    <span class="hljs-keyword">const</span> eventData = <span class="hljs-built_in">JSON</span>.parse(eventText);
    title = eventData.title;

    <span class="hljs-comment">// Set specific options</span>
    <span class="hljs-comment">// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#parameters</span>
    <span class="hljs-keyword">if</span> (eventData.options) {
      options = <span class="hljs-built_in">Object</span>.assign(options, eventData.options);
    }

    <span class="hljs-comment">// Check expiration if specified</span>
    <span class="hljs-keyword">if</span> (eventData.expires &amp;&amp; <span class="hljs-built_in">Date</span>.now() &gt; eventData.expires) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Push notification has expired'</span>);
      <span class="hljs-keyword">return</span>;
    }
  } <span class="hljs-keyword">else</span> {
    title = eventText;
  }

  <span class="hljs-comment">// Warning: this can fail silently if notifications are disabled at system level</span>
  <span class="hljs-comment">// The promise itself resolve to undefined and is not helpful to see if it has been displayed properly</span>
  <span class="hljs-keyword">const</span> promiseChain = self.registration.showNotification(title, options);

  <span class="hljs-comment">// With this, the browser will keep the service worker running until the promise you passed in has settled.</span>
  event.waitUntil(promiseChain);
});
</code></pre>
<p>So what does this do ?</p>
<ul>
<li>In your service worker (using workbox is highly recommended), define the listener for push notifications</li>
<li>Do some checks to see if the push is actually valid</li>
<li>Support push messages in both plain text and json format. The json format allows us to set custom options for each push message (body, icons, actions...)</li>
<li>I added an expiration check. Indeed, if the device is offline, it will get the push notifications as soon as it gets online. But maybe your push notifications won't be relevant anymore. If it's the case, simply pass a "expires" property along with the timestamp at which the notification expires.</li>
<li>And then, show the notification :-)</li>
<li>(and yes, I added some comments and links to make sure to remember how it works)</li>
</ul>
<p>Happy coding :-)</p>
]]></content:encoded></item></channel></rss>