From da9bb278cdc10deccf83becb5c9542739c8d1b38 Mon Sep 17 00:00:00 2001 From: Lewis Dale Date: Tue, 31 Dec 2024 13:21:12 +0000 Subject: [PATCH] New post about "the good bits" of 2024 --- src/blog/posts/2024/12/2024-year-review.md | 20 +++++++ ...ed-from-building-my-first-webcomponents.md | 59 +++++++++++++++++++ src/css/globals.css | 9 ++- 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/blog/posts/2024/12/2024-year-review.md create mode 100644 src/blog/posts/2024/12/some-things-i-learned-from-building-my-first-webcomponents.md diff --git a/src/blog/posts/2024/12/2024-year-review.md b/src/blog/posts/2024/12/2024-year-review.md new file mode 100644 index 0000000..d4558f3 --- /dev/null +++ b/src/blog/posts/2024/12/2024-year-review.md @@ -0,0 +1,20 @@ +---json +{ + "title": "2024: Only the good bits", + "date": "2024-12-30T11:44:53.620Z", + "tags": [], + "excerpt": "In an effort to resist the urge to be a miserable bastard, here's a list of all of the things I'm grateful for this year in lieu of a regular \"Year review\"" +} +--- + +I wanted to write a year in review, like I have done [at least once before](/post/2022-a-retrospective/). But to be honest, I didn't want to focus on the negative things from the year, at all. Instead, here's a non-exhaustive and un-ordered list of all the things I'm grateful for from this year: + +* My daughter is _thriving_. She's the funniest, warmest child I know[^1], and I'm so glad I get to see her grow up +* I got [my first ever promotion](/post/my-first-ever-promotion) at work, and got to form a team around a niche that I'm genuinely passionate about +* I [joined a cycling club](/post/trying-out-a-cycling-club/) earlier this year, who all turned out to be a great bunch of people +* I [actually launched a personal project](/post/filter-rss-podcast-feeds/)[^2] which has been running happily on my server for a while now +* I got a new [Ribble Endurance bike](/post/new-bike-s-day/) which has been serving me _very_ well since +* I managed to complete [#WeBlogPoMo2024](/post/weblogpomo2024-retrospective/), and spent some time learning Go, a language I haven't touched since and probably never will + +[^1]: I may be a little biased +[^2]: I haven't really touched it since, which is why it doesn't have a usable UI diff --git a/src/blog/posts/2024/12/some-things-i-learned-from-building-my-first-webcomponents.md b/src/blog/posts/2024/12/some-things-i-learned-from-building-my-first-webcomponents.md new file mode 100644 index 0000000..40a51a4 --- /dev/null +++ b/src/blog/posts/2024/12/some-things-i-learned-from-building-my-first-webcomponents.md @@ -0,0 +1,59 @@ +---json +{ + "title": "Some things I learned from building my first WebComponents", + "date": "2024-12-13T11:20:04.269Z", + "tags": [ + "webcomponents", + "javascript", + "html", + "draft" + ], + "excerpt": "My team at work have recently released the first versions of some new WebComponents for internal use. As I'd only had fairly limited experience with WebComponents before starting this, I learned a few things that I thought I'd share." +} +--- + +My team at work have recently released the first versions of some new WebComponents for internal use. As I'd only had fairly limited experience with WebComponents before starting this, I learned a few things that I thought I'd share. + +Our first component was an implementation of the ComboBox pattern - basically a custom Select element, with some brand-specific styling and functionality. We were doing this in an effort to remove the many custom versions scattered around our product, especially those that hadn't been built with accessibility in mind. We decided to use [Lit](https://lit.dev) to build the components, as it's fairly lightweight but handles a lot of the boilerplate while still exposing the native APIs. + +## It's not React, so don't try to emulate it + +Pretty much all of the frontend code currently uses React, which means that it's not unusual to see components with these sorts of APIs: + +```javascript +} + someFlag={true} + complexPropertyThatIsUsedToRender={[ + { foo: 'foo', bar: 'bar' }, + ... + ]} +/> +``` + +So initially, we tried to emulate this behaviour for our ComboBox component, which you _can_ do with Lit by using the `json` type for taking complex property types. I have no idea if it's possible to pass a DOM element as a property, and frankly I don't want to find out. But this is an anti-pattern, because we weren't building a React component, we're defining custom HTML elements - and they should be consumed just as you would an HTML element. + +So instead of doing that, we used [slots](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) for user-defined content. In our case, that included the `option` elements, which we applied to a named slot: + +```html + + The label text + + + + ... + +``` + +Then, as I mentioned in my [previous post about this pattern](/post/til-using-lit-decorators-for-better-webcomponent-apis/), we use the `queryAssignedElements` Lit decorator to retrieve and "enhance" the options for our use-case. This meant our component started to feel a bit more like a native element, which is what we ultimately wanted to go for. + +## ElementInternals lets the component act like a regular form input + +We found some cases where the label for our component needed to be external to it, for structural or formatting reason. Of course, this doesn't work for WebComponents because IDs are scoped the shadow root, so the browser isn't aware that the id on the label `for` attribute refers to the element in the WebComponent. + +So, after putting out [a question on Mastodon](https://social.lol/@lewis/113599779828387002) about this, thankfully [Aleš Roubíček had the answer we needed](https://indieweb.social/@alesroubicek/113599829106390768). The [ElementInternals api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) lets you call `this.attachInternals()`, and allows you to associate the component with a form, as well as other elements (including labels!). I had to make sure `delegatesFocus` was true when calling `attachShadow`, but once I did that my component was correctly associated with the form and labels. + +I just had to make sure that my component was calling `this.internals.setFormValue()` whenever the value was changed, and then it would also send the value and name as part of the `FormData` when submitting. It can even take part in native form validation via the `ElementInternals.validity` object. The only thing that it can't do is match the `:user-invalid` selector, because that hasn't yet been implemented for any browser. + +## Shadow Parts make stylable WebComponents much easier diff --git a/src/css/globals.css b/src/css/globals.css index 552a203..d611ce5 100644 --- a/src/css/globals.css +++ b/src/css/globals.css @@ -50,12 +50,11 @@ body { flex-basis: 0; flex-grow: 999; min-inline-size: 75%; - max-inline-size: var(--screen-lg); + max-inline-size: 100%; padding: var(--space-size-s) var(--space-size-m); } } - h1, h2, h3, @@ -170,4 +169,10 @@ main { margin-right: auto; padding: 0 var(--space-size-m); max-width: 70ch; +} + +@media (max-width: 1280px) { + main { + margin: 0 auto; + } } \ No newline at end of file