We’ve discussed a lot about the internals of using CSS in this ongoing series on web components, but there are a few special pseudo-elements and pseudo-classes that, like good friends, willingly smell your possibly halitotic breath before you go talk to that potential love interest. You know, they help you out when you need it most. And, like a good friend will hand you a breath mint, these pseudo-elements and pseudo-classes provide you with some solutions both from within the web component and from outside the web component — the website where the web component lives.
I’m specifically referring to the ::part and ::slotted pseudo-elements, and the :defined, :host, and :host-context pseudo-classes. They give us extra ways to interact with web components. Let’s examine them closer.
::part, in short, allows you to pierce the shadow tree, which is just my Lord-of-the-Rings-y way to say it lets you style elements inside the shadow DOM from outside the shadow DOM. In theory, you should encapsulate all of your styles for the shadow DOM within the shadow DOM, i.e. within a <style> element in your <template> element.
So, given something like this from the very first part of this series, where you have an <h2> in your <template>, your styles for that <h2> should all be in the <style> element.
But sometimes we might need to style an element in the shadow DOM based on information that exists on the page. For instance, let’s say we have a page for each zombie in the undying love system with matches. We could add a class to profiles based on how close of a match they are. We could then, for instance, highlight a match’s name if he/she/it is a good match. The closeness of a match would vary based on whose list of potential matches is being shown and we won’t know that information until we’re on that page, so we can’t bake the functionality into the web component. Since the <h2> is in the shadow DOM, though, we can’t access or style it from outside the shadow DOM meaning a selector of zombie-profile h2 on the matches page won’t work.
But, if we make a slight adjustment to the <template> markup by adding a part attribute to the <h2>:
Like a spray of Bianca in the mouth, we now have the superpowers to break through the shadow DOM barrier and style those elements from outside of the <template>:
But you can add a part attribute on that element and style it via its own part name.
What happens if we have a web component inside another web component, though? Will ::part still work? If the web component appears in the page’s markup, i.e. you’re slotting it in, ::part works just fine from the main page’s CSS.
But if the web component is in the template/shadow DOM, then ::part cannot pierce both shadow trees, just the first one. We need to bring the ::part into the light… so to speak. We can do that with an exportparts attribute.
To demonstrate this we’ll add a “watermark” behind the profiles using a web component. (Why? Believe it or not this was the least contrived example I could come up with.) Here are our templates: (1) the template for <zombie-watermark>, and (2) the same template for <zombie-profile> but with added a <zombie-watermark> element on the end.
Since ::part(watermark) is only one shadow DOM above the <zombie-watermark>, it works fine from within the <zombie-profile>’s template styles. Also, since we’ve used exportparts="copyright" on the <zombie-watermark>, the copyright part has been pushed up into the <zombie-profile>‘s shadow DOM and ::part(copyright) now works even in external styles, but ::part(watermark) will not work outside the <zombie-profile>’s template.
We can also forward and rename parts with that attribute:
Structural pseudo-classes (:nth-child, etc.) don’t work on parts either, but you can use pseudo-classes like :hover. Let’s animate the high match names a little and make them shake as they’re lookin’ for some lovin’. Okay, I heard that and agree it’s awkward. Let’s… uh… make them more, shall we say, noticeable, with a little movement.
The ::slotted CSS pseudo-element actually came up when we covered interactive web components. The basic idea is that ::slotted represents any content in a slot in a web component, i.e. the element that has the slot attribute on it. But, where ::part pierces through the shadow DOM to make a web component’s elements accessible to outside styles, ::slotted remains encapsulated in the <style> element in the component’s <template> and accesses the element that’s technically outside the shadow DOM.
In our <zombie-profile> component, for example, each profile image is inserted into the element through the slot="profile-image".
<zombie-profile> <img slot="profile-image" src="photo.jpg" /> <!-- rest of the content --> </zombie-profile>
That means we can access that image — as well as any image in any other slot — like this:
Similarly, we could select all slots with ::slotted(*) regardless of what element it is. Just beware that ::slotted has to select an element — text nodes are immune to ::slotted zombie styles. And children of the element in the slot are inaccessible.
The :defined pseudo-class
:defined matches all defined elements (I know, surprising, right?), both built-in and custom. If your custom element is shuffling along like a zombie avoiding his girlfriend’s dad’s questions about his “living” situation, you may not want the corpses of the content to show while you’re waiting for them to come back to life errr… load.
You can use the :defined pseudo-class to hide a web component before it’s available — or “defined” — like this:
:not(:defined) { display: none; }
You can see how :defined acts as a sort of mint in the mouth of our component styles, preventing any broken content from showing (or bad breath from leaking) while the page is still loading. Once the element’s defined, it’ll automatically appear because it’s now, you know, defined and not not defined.
I added a setTimeout of five seconds to the web component in the following demo. That way, you can see that <zombie-profile> elements are not shown while they are undefined. The <h1> and the <div> that holds the <zombie-profile> components are still there. It’s just the <zombie-profile> web component that gets display: none since they are not yet defined.
The :host pseudo-class
Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.
It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host.
In previous examples throughout this series, I set the <zombie-profile> width from the main page’s CSS, like this:
zombie-profile { width: calc(50% - 1em); }
With :host, however, I can set that width from inside the web component, like this:
:host { width: calc(50% - 1em); }
In fact, there was a div with a class of .profile-wrapper in my examples that I can now remove because I can use the shadow root as my wrapper with :host. That’s a nice way to slim down the markup.
You can do descendant selectors from the :host, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted).
That said, :host isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.
:host(.high) { border: 2px solid blue; }
This allows you to make changes should certain classes be added to the custom element.
You can also pass pseudo-classes in there, like :host(:last-child) and :host(:hover).
The :host-context pseudo-class
Now let’s talk about :host-context. It’s like our friend :host(), but on steroids. While :host gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.
:host-context, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context is unsupported in Firefox or Safari. So use it for progressive enhancement.
Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch class. The second div will hold all the medium and low love matches with a .worstmatch class.
<div class="profiles bestmatch"> <zombie-profile class="high"> <!-- etc. --> </zombie-profile> <!-- more profiles --> </div> <div class="profiles worstmatch"> <zombie-profile class="medium"> <!-- etc. --> </zombie-profile> <zombie-profile class="low"> <!-- etc. --> </zombie-profile> <!-- more profiles --> </div>
Let’s say we want to apply different background colors to the .bestmatch and .worstmatch classes. We are unable to do this with just :host:
That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context pokes past the custom element to match the, er, match classes we want to style.
Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)
How would you use ::part, ::slotted, :defined, :host, and :host-context in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)
Say you’ve got a <Card /> component. It’s highly likely it shouldn’t be butted right up against any other components with no spacing around it. That’s true for… pretty much every component. So, how do you handle component spacing in a design system?
Do you apply spacing using margin directly on the <Card />? Perhaps margin-block-end: 1rem; margin-inline-end: 1rem; so it pushes away from the two sides where more content natural flows? That’s a little presumptuous. Perhaps the cards are children inside a <Grid /> component and the grid applies a gap: 1rem. That’s awkward, as now the <Card /> component spacing is going to conflict with the <Grid /> component spacing, which is very likely not what you want, not to mention the amount of space is hard coded.
Adding space to the inline start and block end of a card component.
You could bake spacing into every component and try to be as clever as you can about it. (But that’s pretty limiting.)
You could pass in component spacing, like <Card space="xxl" />. (That can be a good approach, likely needs more than one prop, maybe even one for each direction, which is quite verbose.)
You could use no component spacing and create something like a <Spacer /> or <Layout /> component specifically for spacing between components. (It breaks up the job of components nicely, but can also be verbose and add unnecessary DOM weight.)
This conversation has a wide spectrum of viewpoints, some as extreme as Max Stoiber saying just never use margin ever at all. That’s a little dogmatic for me, but I like that it’s trying to rethink things. I do like the idea of taking the job of spacing and layout away from components themselves — like, for example, those content components should completely not care where they are used and let layout happen a level up from them.
Adam Argyle predicted a few years back that the use of margin in CSS would decline as the use of gap rises. He’s probably going to end up right about this, especially now that flexbox has gap and that developers have an appetite these dats to use CSS Flexbox and Grid on nearly everything at both a macro and micro level.
Your mission — should you decide to accept it — is to build a Button component in four frameworks, but, only use one button.css file!
This idea is very important to me. I’ve been working on a component library called AgnosticUI where the purpose is building UI components that aren’t tied to any one particular JavaScript framework. AgnosticUI works in React, Vue 3, Angular, and Svelte. So that’s exactly what we’ll do today in this article: build a button component that works across all these frameworks.
The source code for this article is available on GitHub on the the-little-button-that-could-series branch.
We’re going to set up a tiny Yarn workspaces-based monorepo. Why? Chris actually has a nice outline of the benefits in another post. But here’s my own biased list of benefits that I feel are relevant for our little buttons endeavor:
Coupling
We’re trying to build a single button component that uses just one button.css file across multiple frameworks. So, by nature, there’s some purposeful coupling going on between the various framework implementations and the single-source-of-truth CSS file. A monorepo setup provides a convenient structure that facilitates copying our single button.css component into various framework-based projects.
Workflow
Let’s say the button needs a tweak — like the “focus-ring” implementation, or we screwed up the use of aria in the component templates. Ideally, we’d like to correct things in one place rather than making individual fixes in separate repositories.
Testing
We want the convenience of firing up all four button implementations at the same time for testing. As this sort of project grows, it’s safe to assume there will be more proper testing. In AgnosticUI, for example, I’m currently using Storybook and often kick off all the framework Storybooks, or run snapshot testing across the entire monorepo.
I believe the monorepo is particularly useful when all packages are coded in the same programming language, tightly coupled, and relying on the same tooling.
Setting up
Time to dive into code — start by creating a top-level directory on the command-line to house the project and then cd into it. (Can’t think of a name? mkdir buttons && cd buttons will work fine.)
First off, let’s initialize the project:
$ yarn init yarn init v1.22.15 question name (articles): littlebutton question version (1.0.0): question description: my little button project question entry point (index.js): question repository url: question author (Rob Levin): question license (MIT): question private: success Saved package.json
That gives us a package.json file with something like this:
{ "name": "littlebutton", "version": "1.0.0", "description": "my little button project", "main": "index.js", "author": "Rob Levin", "license": "MIT" }
Creating the baseline workspace
We can set the first one up with this command:
mkdir -p ./littlebutton-css
Next, we need to add the two following lines to the monorepo’s top-level package.json file so that we keep the monorepo itself private. It also declares our workspaces:
Now descend into the littlebutton-css directory. We’ll again want to generate a package.json with yarn init. Since we’ve named our directory littlebutton-css (the same as how we specified it in our workspaces in package.json) we can simply hit the Return key and accept all the prompts:
$ cd ./littlebutton-css && yarn init yarn init v1.22.15 question name (littlebutton-css): question version (1.0.0): question description: question entry point (index.js): question repository url: question author (Rob Levin): question license (MIT): question private: success Saved package.json
At this point, the directory structure should look like this:
We’ve only created the CSS package workspace at this point as we’ll be generating our framework implementations with tools like vite which, in turn, generate a package.json and project directory for you. We will have to remember that the name we choose for these generated projects must match the name we’ve specified in the package.json for our earlier workspaces to work.
Baseline HTML & CSS
Let’s stay in the ./littlebutton-css workspace and create our simple button component using vanilla HTML and CSS files.
And, just so we have something visual to test, we can add a little color in ./css/button.css:
.btn { color: hotpink; }
Now open up that index.html page in the browser. If you see an ugly generic button with hotpink text… success!
Framework-specific workspaces
So what we just accomplished is the baseline for our button component. What we want to do now is abstract it a bit so it’s extensible for other frameworks and such. For example, what if we want to use the button in a React project? We’re going to need workspaces in our monorepo for each one. We’ll start with React, then follow suit for Vue 3, Angular, and Svelte.
React
We’re going to generate our React project using vite, a very lightweight and blazingly fast builder. Be forewarned that if you attempt to do this with create-react-app, there’s a very good chance you will run into conflicts later with react-scripts and conflicting webpack or Babel configurations from other frameworks, like Angular.
To get our React workspace going, let’s go back into the terminal and cd back up to the top-level directory. From there, we’ll use vite to initialize a new project — let’s call it littlebutton-react — and, of course, we’ll select react as the framework and variant at the prompts:
$ yarn create vite yarn create v1.22.15 [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies... [4/4] 🔨 Building fresh packages... success Installed "create-vite@2.6.6" with binaries: - create-vite - cva ✔ Project name: … littlebutton-react ✔ Select a framework: › react ✔ Select a variant: › react Scaffolding project in /Users/roblevin/workspace/opensource/guest-posts/articles/littlebutton-react... Done. Now run: cd littlebutton-react yarn yarn dev ✨ Done in 17.90s.
We initialize the React app with these commands next:
cd littlebutton-react yarn yarn dev
With React installed and verified, let’s replace the contents of src/App.jsx to house our button with the following code:
Now we’re going to write a small Node script that copies our littlebutton-css/css/button.css right into our React application for us. This step is probably the most interesting one to me because it’s both magical and ugly at the same time. It’s magical because it means our React button component is truly deriving its styles from the same CSS written in the baseline project. It’s ugly because, well, we are reaching up out of one workspace and grabbing a file from another. ¯_(ツ)_/¯
Add the following little Node script to littlebutton-react/copystyles.js:
Let’s place a node command to run that in a package.json script that happens before the dev script in littlebutton-react/package.json. We’ll add a syncStyles and update the dev to call syncStyles before vite:
Now, anytime we fire up our React application with yarn dev, we’ll first be copying the CSS file over. In essence, we’re “forcing” ourselves to not diverge from the CSS package’s button.css in our React button.
But we want to also leverage CSS Modules to prevent name collisions and global CSS leakage, so we have one more step to do to get that wired up (from the same littlebutton-react directory):
touch src/button.module.css
Next, add the following to the new src/button.module.css file:
.btn { composes: btn from './button.css'; }
I find composes (also known as composition) to be one of the coolest features of CSS Modules. In a nutshell, we’re copying our HTML/CSS version of button.css over wholesale then composing from our one .btn style rule.
With that, we can go back to our src/App.jsx and import the CSS Modules styles into our React component with this:
Whew! Let’s pause and try to run our React app again:
yarn dev
If all went well, you should see that same generic button, but with hotpink text. Before we move on to the next framework, let’s move back up to our top-level monorepo directory and update its package.json:
Run the yarn command from the top-level directory to get the monorepo-hoisted dependencies installed.
The only change we’ve made to this package.json is a new scripts section with a single script to start the React app. By adding start:react we can now run yarn start:react from our top-level directory and it will fire up the project we just built in ./littlebutton-react without the need for cd‘ing — super convenient!
We’ll tackle Vue and Svelte next. It turns out that we can take a pretty similar approach for these as they both use single file components (SFC). Basically, we get to mix HTML, CSS, and JavaScript all into one single file. Whether you like the SFC approach or not, it’s certainly adequate enough for building out presentational or primitive UI components.
Vue
Following the steps from vite’s scaffolding docs we’ll run the following command from the monorepo’s top-level directory to initialize a Vue app:
yarn create vite littlebutton-vue --template vue
This generates scaffolding with some provided instructions to run the starter Vue app:
cd littlebutton-vue yarn yarn dev
This should fire up a starter page in the browser with some heading like “Hello Vue 3 + Vite.” From here, we can update src/App.vue to:
:class="classes" is using Vue’s binding to call the computed classes method.
The classes method, in turn, is utilizing CSS Modules in Vue with the this.$ style.btn syntax which will use styles contained in a <style module> tag.
For now, we’re hardcoding color: slateblue simply to test that things are working properly within the component. Try firing up the app again with yarn dev. If you see the button with our declared test color, then it’s working!
Now we’re going to write a Node script that copies our littlebutton-css/css/button.css into our Button.vue file similar to the one we did for the React implementation. As mentioned, this component is a SFC so we’re going to have to do this a little differently using a simple regular expression.
Add the following little Node.js script to littlebutton-vue/copystyles.js:
const fs = require("fs"); let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8"); const vue = fs.readFileSync("./src/components/Button.vue", "utf8"); // Take everything between the starting and closing style tag and replace const styleRegex = /<style module>([sS]*?)</style>/; let withSynchronizedStyles = vue.replace(styleRegex, `<style module>n$ {css}n</style>`); fs.writeFileSync("./src/components/Button.vue", withSynchronizedStyles, "utf8");
There’s a bit more complexity in this script, but using replace to copy text between opening and closing style tags via regex isn’t too bad.
Now let’s add the following two scripts to the scripts clause in the littlebutton-vue/package.json file:
Now run yarn syncStyles and look at ./src/components/Button.vue again. You should see that our style module gets replaced with this:
<style module> .btn { color: hotpink; } </style>
Run the Vue app again with yarn dev and verify you get the expected results — yes, a button with hotpink text. If so, we’re good to move on to the next framework workspace!
Svelte
Per the Svelte docs, we should kick off our littlebutton-svelte workspace with the following, starting from the monorepo’s top-level directory:
npx degit sveltejs/template littlebutton-svelte cd littlebutton-svelte yarn && yarn dev
Confirm you can hit the “Hello World” start page at http://localhost:5000. Then, update littlebutton-svelte/src/App.svelte:
<script> import Button from './Button.svelte'; </script> <main> <Button>Go</Button> </main>
Also, in littlebutton-svelte/src/main.js, we want to remove the name prop so it looks like this:
import App from './App.svelte'; const app = new App({ target: document.body }); export default app;
And finally, add littlebutton-svelte/src/Button.svelte with the following:
One last thing: Svelte appears to name our app: "name": "svelte-app" in the package.json. Change that to "name": "littlebutton-svelte" so it’s consistent with the workspaces name in our top-level package.json file.
Once again, we can copy our baseline littlebutton-css/css/button.css into our Button.svelte. As mentioned, this component is a SFC, so we’re going to have to do this using a regular expression. Add the following Node script to littlebutton-svelte/copystyles.js:
Now run yarn syncStyles && yarn dev. If all is good, we once again should see a button with hotpink text.
If this is starting to feel repetitive, all I have to say is welcome to my world. What I’m showing you here is essentially the same process I’ve been using to build my AgnosticUI project!
Angular
You probably know the drill by now. From the monorepo’s top-level directory, install Angular and create an Angular app. If we were creating a full-blown UI library we’d likely use ng generate library or even nx. But to keep things as straightforward as possible we’ll set up a boilerplate Angular app as follows:
npm install -g @angular/cli ### unless you already have installed ng new littlebutton-angular ### choose no for routing and CSS ? Would you like to add Angular routing? (y/N) N ❯ CSS SCSS [ https://sass-lang.com/documentation/syntax#scss ] Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ] Less [ http://lesscss.org ] cd littlebutton-angular && ng serve --open
With the Angular setup confirmed, let’s update some files. cd littlebutton-angular, delete the src/app/app.component.spec.ts file, and add a button component in src/components/button.component.ts, like this:
import { Component } from '@angular/core'; @Component({ selector: 'little-button', templateUrl: './button.component.html', styleUrls: ['./button.component.css'], }) export class ButtonComponent {}
Add the following to src/components/button.component.html:
<button class="btn">Go</button>
And put this in the src/components/button.component.css file for testing:
.btn { color: fuchsia; }
In src/app/app.module.ts:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { ButtonComponent } from '../components/button.component'; @NgModule({ declarations: [AppComponent, ButtonComponent], imports: [BrowserModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
Next, replace src/app/app.component.ts with:
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent {}
Then, replace src/app/app.component.html with:
<main> <little-button>Go</little-button> </main>
With that, let’s run yarn start and verify our button with fuchsia text renders as expected.
Again, we want to copy over the CSS from our baseline workspace. We can do that by adding this to littlebutton-angular/copystyles.js:
[…] the behavior of shadow DOM by preprocessing (and renaming) the CSS code to effectively scope the CSS to the component’s view.
This basically means we can literally copy over button.css and use it as-is.
Finally, update the package.json file by adding these two lines in the scripts section:
"start": "yarn syncStyles && ng serve", "syncStyles": "node copystyles.js",
With that, we can now run yarn start once more and verify our button text color (which was fuchsia) is now hotpink.
What have we just done?
Let’s take a break from coding and think about the bigger picture and what we’ve just done. Basically, we’ve set up a system where any changes to our CSS package’s button.css will get copied over into all the framework implementations as a result of our copystyles.js Node scripts. Further, we’ve incorporated idiomatic conventions for each of the frameworks:
SFC for Vue and Svelte
CSS Modules for React (and Vue within the SFC <style module> setup)
ViewEncapsulation for Angular
Of course I state the obvious that these aren’t the only ways to do CSS in each of the above frameworks (e.g. CSS-in-JS is a popular choice), but they are certainly accepted practices and are working quite well for our greater goal — to have a single CSS source of truth to drive all framework implementations.
If, for example, our button was in use and our design team decided we wanted to change from 4px to 3pxborder-radius, we could update the one file, and any separate implementations would stay synced.
This is compelling if you have a polyglot team of developers that enjoy working in multiple frameworks, or, say an offshore team (that’s 3× productive in Angular) that’s being tasked to build a back-office application, but your flagship product is built in React. Or, you’re building an interim admin console and you’d love to experiment with using Vue or Svelte. You get the picture.
Finishing touches
OK, so we have the monorepo architecture in a really good spot. But there’s a few things we can do to make it even more useful as far as the developer experience goes.
Better start scripts
Let’s move back up to our top-level monorepo directory and update its package.jsonscripts section with the following so we can kick any framework implementation without cd‘ing:
We can also provide a better set of baseline styles for the button so it starts from a nice, neutral place. Here’s what I did in the littlebutton-css/css/button.css file.
Let’s test this out! Fire up each of the four framework implementations with the new and improved start scripts and confirm the styling changes are in effect.
One CSS file update proliferated to four frameworks — pretty cool, eh!?
Set a primary mode
We’re going to add a mode prop to each of our button’s and implement primary mode next. A primary button could be any color but we’ll go with a shade of green for the background and white text. Again, in the baseline stylesheet:
There we go! Some developer conveniences and better baseline styles!
Updating each component to take a mode property
Now that we’ve added our new primary mode represented by the .btn-primary class, we want to sync the styles for all four framework implementations. So, let’s add some more package.json scripts to our top level scripts:
Be sure to respect JSON’s comma rules! Depending on where you place these lines within your scripts: {...}, you’ll want to make sure there are no missing or trailing commas.
Go ahead and run the following to fully synchronize the styles:
Running this doesn’t change anything because we haven’t applied the primary class yet, but you should at least see the CSS has been copied over if you go look at the framework’s button component CSS.
React
If you haven’t already, double-check that the updated CSS got copied over into littlebutton-react/src/button.css. If not, you can run yarn syncStyles. Note that if you forget to run yarn syncStyles our dev script will do this for us when we next start the application anyway:
"dev": "yarn syncStyles && vite",
For our React implementation, we additionally need to add a composed CSS Modules class in littlebutton-react/src/button.module.css that is composed from the new .btn-primary:
.btnPrimary { composes: btn-primary from './button.css'; }
Now we can update the markup in littlebutton-vue/src/App.vue to use the new mode prop:
<Button mode="primary">Go</Button>
Now you can yarn start:vue from the top-level directory and check for the same green button.
Svelte
Let’s cd into littlebutton-svelte and verify that the styles in littlebutton-svelte/src/Button.svelte have the new .btn-primary class copied over, and yarn syncStyles if you need to. Again, the dev script will do that for us anyway on the next startup if you happen to forget.
Next, update the Svelte template to pass the mode of primary. In src/App.svelte:
<script> import Button from './Button.svelte'; </script> <main> <Button mode="primary">Go</Button> </main>
We also need to update the top of our src/Button.svelte component itself to accept the mode prop and apply the CSS Modules class:
Now we need to set up a binding to a classes getter to compute the correct classes based on if the mode was passed in to the component or not. Add this to littlebutton-angular/src/components/button.component.html (and note the binding is happening with the square brackets):
<button [class]="classes">Go</button>
Next, we actually need to create the classes binding in our component at littlebutton-angular/src/components/button.component.ts:
We use the Input directive to take in the mode prop, then we create a classes accessor which adds the mode class if it’s been passed in.
Fire it up and look for the green button!
Code complete
If you’ve made it this far, congratulations — you’ve reached code complete! If something went awry, I’d encourage you to cross-reference the source code over at GitHub on the the-little-button-that-could-series branch. As bundlers and packages have a tendency to change abruptly, you might want to pin your package versions to the ones in this branch if you happen to experience any dependency issues.
Take a moment to go back and compare the four framework-based button component implementations we just built. They’re still small enough to quickly notice some interesting differences in how props get passed in, how we bind to props, and how CSS name collisions are prevented among other subtle differences. As I continue to add components to AgnosticUI (which supports these exact same four frameworks), I’m continually pondering which offers the best developer experience. What do you think?
Homework
If you’re the type that likes to figure things out on your own or enjoys digging in deeper, here are ideas.
Button states
The current button styles do not account for various states, like :hover. I believe that’s a good first exercise.
/* You should really implement the following states but I will leave it as an exercise for you to decide how to and what values to use. */ .btn:focus { /* If you elect to remove the outline, replace it with another proper affordance and research how to use transparent outlines to support windows high contrast */ } .btn:hover { } .btn:visited { } .btn:active { } .btn:disabled { }
Variants
Most button libraries support many button variations for things like sizes, shapes, and colors. Try creating more than the primary mode we already have. Maybe a secondary variation? A warning or success? Maybe filled and outline? Again, you can look at AgnosticUI’s buttons page for ideas.
CSS custom properties
If you haven’t started using CSS custom properties yet, I’d strongly recommend it. You can start by having a look at AgnosticUI’s common styles. I heavily lean on custom properties in there. Here are some great articles that cover what custom properties are and how you might leverage them:
No… not typings, but the <button> element’s type attribute. We didn’t cover that in our component but there’s an opportunity to extend the component to other use cases with valid types, like button, submit, and reset. This is pretty easy to do and will greatly improve the button’s API.
More ideas
Gosh, you could do so much — add linting, convert it to Typescript, audit the accessibility, etc.
The current Svelte implementation is suffering from some pretty loose assumptions as we have no defense if the valid primary mode isn’t passed — that would produce a garbage CSS class:
mode ? `btn-$ {mode}` : "",
You could say, “Well, .btn-garbage as a class isn’t exactly harmful.” But it’s probably a good idea to style defensively when and where possible.
Potential pitfalls
There are some things you should be aware of before taking this approach further:
Positional CSS based on the structure of the markup will not work well for the CSS Modules based techniques used here.
Angular makes positional techniques even harder as it generates :host element representing each component view. This means you have these extra elements in between your template or markup structure. You’ll need to work around that.
Copying styles across workspace packages is a bit of an anti-pattern to some folks. I justify it because I believe the benefits outweigh the costs; also, when I think about how monorepos use symlinks and (not-so-failproof) hoisting, I don’t feel so bad about this approach.
You’ll have to subscribe to the decoupled techniques used here, so no CSS-in-JS.
I believe that all approaches to software development have their pros and cons and you ultimately have to decide if sharing a single CSS file across frameworks works for you or your specific project. There are certainly other ways you could do this (e.g. using littlebuttons-css as an npm package dependency) if needed.
Conclusion
Hopefully I’ve whet your appetite and you’re now really intrigued to create UI component libraries and/or design systems that are not tied to a particular framework. Maybe you have a better idea on how to achieve this — I’d love to hear your thoughts in the comments!
I’m sure you’ve seen the venerable TodoMVC project and how many framework implementations have been created for it. Similarly, wouldn’t it be nice to have a UI component library of primitives available for many frameworks? Open UI is making great strides to properly standardize native UI component defaults, but I believe we’ll always need to insert ourselves to some extent. Certainly, taking a good year to build a custom design system is quickly falling out of favor and companies are seriously questioning their ROI. Some sort of scaffolding is required to make the endeavor practical.
The vision of AgnosticUI is to have a relatively agnostic way to build design systems quickly that are not tied down to a particular frontend framework. If you’re compelled to get involved, the project is still very early and approachable and I’d love some help! Plus, you’re already pretty familiar with the how the project works now that you’ve gone through this tutorial!
I enjoy little frameworks like Tonic. It’s essentially syntactic sugar over <web-components /> to make them feel easier to use. Define a Class, template literal an HTML template, probably some other fancy helpers, and you’ve got a component that doesn’t feel terribly different to something like a React component, except you need no build process or other exotic tooling.
Here’s a Hello World + Counter example:
They have a whole bunch of examples (in a separate repo). You can snag and use them, and they are pretty nice! So that makes Tonic a bit like a design system as well as a web component framework.
To be fair, it’s not that different from Lit, which Google is behind and pushing pretty actively.
I’d say that petite-vue example wins for just how super easy that is to pull of in just declarative HTML. But of course, there are a bunch of other considerations from specific features, syntax, philosophy, and size. Just looking at size, if I pop open the Network tab in DevTools and see the over-the-wire JavaScript for each demo…
Tonic = 5.1 KB
Lit = 12.6 KB
petite-vue = 8.1 KB
They are all basically the same: tiny.
I’ve never actually built anything real in any of them, so I’m not the best to judge one from the other. But they all seem pretty neat to me, particularly because they require no build step.
This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!
I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):
There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.
This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!
I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):
There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.
React component tests should be interesting, straightforward, and easy for a human to build and maintain.
Yet, the current state of the testing library ecosystem is not sufficient to motivate developers to write consistent JavaScript tests for React components. Testing React components—and the DOM in general—often require some kind of higher-level wrapper around popular testing frameworks like Jest or Mocha.
Here’s the problem
Writing component tests with the tools available today is boring, and even when you get to writing them, it takes lots of hassle. Expressing test logic following a jQuery-like style (chaining) is confusing. It doesn’t jive with how React components are usually built.
The Enzyme code below is readable, but a bit too bulky because it uses too many words to express something that is ultimately simple markup.
What if you need to test heavier components? While the syntax is still bearable, it doesn’t help your brain grasp the structure and logic. Reading and writing several tests like this is bound to wear you out—it certainly wears me out. That’s because React components follow certain principles to generate HTML code at the end. Tests that express the same principles, on the other hand, are not straightforward. Simply using JavaScript chaining won’t help in the long run.
There are two main issues with testing in React:
How to even approach writing tests specifically for components
How to avoid all the unnecessary noise
Let’s further expand those before jumping into the real examples.
Approaching React component tests
A simple React component may look like this:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
This is a function that accepts a props object and returns a DOM node using the JSX syntax.
Since a component can be represented by a function, it is all about testing functions. We need to account for arguments and how they influence the returned result. Applying that logic to React components, the focus in the tests should be on setting up props and testing for the DOM rendered in the UI. Since user actions like mouseover, click, typing, etc. may also lead to UI changes, you will need to find a way to programmatically trigger those too.
Hiding the unnecessary noise in tests
Tests require a certain level of readability achieved by both slimming the wording down and following a certain pattern to describe each scenario.
Component tests flow through three phases:
Preparation (setup): The component props are prepared.
Render (action): The component needs to render its DOM to the UI before either triggering any actions on it or testing for certain texts and attributes. That’s when actions can be programmatically triggered.
Validation (verify): The expectations are set, verifying certain side effects over the component markup.
Here is an example:
it("should click a large button", () => { // 1️⃣ Preparation // Prepare component props props.size = "large"; // 2️⃣ Render // Render the Button's DOM and click on it const component = mount(<Button {...props}>Send</Button>); simulate(component, { type: "click" }); // 3️⃣ Validation // Verify a .clicked class is added expect(component, "to have class", "clicked"); });
For simpler tests, the phases can merge:
it("should render with a custom text", () => { // Mixing up all three phases into a single expect() call expect( // 1️⃣ Preparation <Button>Send</Button>, // 2️⃣ Render "when mounted", // 3️⃣ Validation "to have text", "Send" ); });
Writing component tests today
Those two examples above look logical but are anything but trivial. Most of the testing tools do not provide such a level of abstraction, so we have to handle it ourselves. Perhaps the code below looks more familiar.
It does look much better. Less code, obvious flow, and more DOM instead of JavaScript. This is not a fiction test, but something you can achieve with UnexpectedJS today.
The following section is a deep dive into testing React components without getting too deep into UnexpectedJS. Its documentation more than does the job. Instead, we’ll focus on usage, examples, and possibilities.
Writing React Tests with UnexpectedJS
UnexpectedJS is an extensible assertion toolkit compatible with all test frameworks. It can be extended with plugins, and some of those plugins are used in the test project below. Probably the best thing about this library is the handy syntax it provides to describe component test cases in React.
The example: A Profile Card component
The subject of the tests is a Profile card component.
And here is the full component code of ProfileCard.js:
We will work with the component’s desktop version. You can read more about device-driven code split in React but note that testing mobile components is still pretty straightforward.
Setting up the example project
Not all tests are covered in this article, but we will certainly look at the most interesting ones. If you want to follow along, view this component in the browser, or check all its tests, go ahead and clone the GitHub repo.
## 1. Clone the project: git clone git@github.com:moubi/profile-card.git ## 2. Navigate to the project folder: cd profile-card ## 3. Install the dependencies: yarn ## 4. Start and view the component in the browser: yarn start ## 5. Run the tests: yarn test
Here’s how the <ProfileCard /> component and UnexpectedJS tests are structured once the project has spun up:
Let’s take a look at some of the component tests. These are located in src/components/ProfileCard/ProfileCard.test.js. Note how each test is organized by the three phases we covered earlier.
Setting up required component props for each test.
Before each test, a props object with the required <ProfileCard /> props is composed, where props.data contains the minimum info for the component to render.
Render with a default set of props.
This test checks the whole DOM produced by the component when passing name, posts, and creationDate fields.
Now we check if the profile renders with the “online” status icon.
And the test case for that:
it("should display online icon", () => { // Set the isOnline prop props.data.isOnline = true; // The minimum to test for is the presence of the .online class expect( <ProfileCard {...props} />, "when mounted", "queried for first", ".status", "to have class", "online" ); });
Render with bio text.
<ProfileCard /> accepts any arbitrary string for its bio.
So, let’s write a test case for that:
it("should display online icon", () => { // Set the isOnline prop props.data.isOnline = true; // The minimum to test for is the presence of the .online class expect( <ProfileCard {...props} />, "when mounted", "queried for first", ".status", "to have class", "online" ); });
Render “Technologies” view with an empty list.
Clicking on the “View Skills” link should switch to a list of technologies for this user. If no data is passed, then the list should be empty.
Here’s that test case:
it("should display the technologies view", () => { // Mount <ProfileCard /> and obtain a ref const component = mount(<ProfileCard {...props} />); // Simulate a click on the button element ("View Skills" link) simulate(component, { type: "click", target: "button", }); // Check if the .details element contains the technologies view expect( component, "queried for first", ".details", "to exhaustively satisfy", <div className="details technologies"> <h3>Technologies</h3> <div> <button>View Bio</button> </div> </div> ); });
Render a list of technologies.
If a list of technologies is passed, it will display in the UI when clicking on the “View Skills” link.
Yep, another test case:
it("should display list of technologies", () => { // Set the list of technologies props.data.technologies = ["JavaScript", "React", "NodeJs"]; // Mount ProfileCard and obtain a ref const component = mount(<ProfileCard {...props} />); // Simulate a click on the button element ("View Skills" link) simulate(component, { type: "click", target: "button", }); // Check if the list of technologies is present and matches prop values expect( component, "queried for first", ".technologies ul", "to exhaustively satisfy", <ul> <li>JavaScript</li> <li>React</li> <li>NodeJs</li> </ul> ); });
Render a user location.
That information should render in the DOM only if it was provided as a prop.
The test case:
it("should display location", () => { // Set the location props.data.location = "Copenhagen, Denmark"; // Mount <ProfileCard /> and obtain a ref const component = mount(<ProfileCard {...props} />); // Simulate a click on the button element ("View Skills" link) // Location render only as part of the Technologies view simulate(component, { type: "click", target: "button", }); // Check if the location string matches the prop value expect( component, "queried for first", ".location", "to have text", "Location: Copenhagen, Denmark" ); });
Calling a callback when switching views.
This test does not compare DOM nodes but does check if a function prop passed to <ProfileCard /> is executed with the correct argument when switching between the Bio and Technologies views.
it("should call onViewChange prop", () => { // Create a function stub (dummy) props.data.onViewChange = sinon.stub(); // Mount ProfileCard and obtain a ref const component = mount(<ProfileCard {...props} />); // Simulate a click on the button element ("View Skills" link) simulate(component, { type: "click", target: "button", }); // Check if the stub function prop is called with false value for isBioVisible // isBioVisible is part of the component's local state expect( props.data.onViewChange, "to have a call exhaustively satisfying", [false] ); });
Running all the tests
Now, all of the tests for <ProfileCard /> can be executed with a simple command:
yarn test
Notice that tests are grouped. There are two independent tests and two groups of tests for each of the <ProfileCard /> views—bio and technologies. Grouping makes test suites easier to follow and is a nice way to organize logically-related UI units.
Some final words
Again, this is meant to be a fairly simple example of how to approach React component tests. The essence is to look at components as simple functions that accept props and return a DOM. From that point on, choosing a testing library should be based on the usefulness of the tools it provides for handling component renders and DOM comparisons. UnexpectedJS happens to be very good at that in my experience.
What should be your next steps? Look at the GitHub project and give it a try if you haven’t already! Check all the tests in ProfileCard.test.js and perhaps try to write a few of your own. You can also look at src/test-utils/unexpected-react.js which is a simple helper function exporting features from the third-party testing libraries.
And lastly, here are a few additional resources I’d suggest checking out to dig even deeper into React component testing:
UnexpectedJS – The official page and docs for UnexpectedJS. See the Plugins section as well.
UnexpectedJS Gitter room – Perfect for when you need help or have a specific question for the maintainers.
Testing Overview – You can test React components similar to testing other JavaScript code.
React Testing Library – The recommended tooling for writing component tests in React.
In this article, we explain how to build an integrated and interactive data visualization layer into an application with Cumul.io. To do so, we’ve built a demo application that visualizes Spotify Playlist analytics! We use Cumul.io as our interactive dashboard as it makes integration super easy and provides functionality that allow interaction between the dashboard and applications (i.e. custom events). The app is a simple JavaScript web app with a Node.js server, although you can, if you want, achieve the same with Angular, React and React Native while using Cumul.io dashboards too.
Here, we build dashboards that display data from the The Kaggle Spotify Dataset 1921–2020, 160k+ Tracks and also data via the Spotify Web API when a user logs in. We’ve built dashboards as an insight into playlist and song characteristics. We’ve added some Cumul.io custom events that will allow any end user visiting these dashboards to select songs from a chart and add them to one of their own Spotify playlists. They can also select a song to display more info on them, and play them from within the application. The code for the full application is also publicly available in an open repository.
Here’s a sneak peak into what the end result for the full version looks like:
What are Cumul.io custom events and their capabilities?
Simply put, Cumul.io custom events are a way to trigger events from a dashboard, to be used in the application that the dashboard is integrated in. You can add custom events into selected charts in a dashboard, and have the application listen for these events.
Why? The cool thing about this tool is in how it allows you to reuse data from an analytics dashboard, a BI tool, within the application it’s built into. It gives you the freedom to define actions based on data, that can be triggered straight from within an integrated dashboard, while keeping the dashboard, analytics layer a completely separate entity to the application, that can be managed separately to it.
What they contain:Cumul.io custom events are attached to charts rather than dashboards as a whole. So the information an event has is limited to the information a chart has.
An event is simply put a JSON object. This object will contain fields such as the ID of the dashboard that triggered it, the name of the event and a number of other fields depending on the type of chart that the event was triggered from. For example, if the event was triggered from a scatter plot, you will receive the x-axis and y-axis values of the point it was triggered from. On the other hand, if it were triggered from a table, you would receive column values for example. See examples of what these events will look like from different charts:
// 'Add to Playlist' custom event from a row in a table { "type":"customEvent", "dashboard":"xxxx", "name":"xxxx", "object":"xxxx", "data":{ "language":"en", "columns":[ {"id":"Ensueno","value":"Ensueno","label":"Name"}, {"id":"Vibrasphere","value":"Vibrasphere","label":"Artist"}, {"value":0.406,"formattedValue":"0.41","label":"Danceability"}, {"value":0.495,"formattedValue":"0.49","label":"Energy"}, {"value":180.05,"formattedValue":"180.05","label":"Tempo (bpm)"}, {"value":0.568,"formattedValue":"0.5680","label":"Accousticness"}, {"id":"2007-01-01T00:00:00.000Z","value":"2007","label":"Release Date (Yr)"}, ], "event":"add_to_playlist" } }
//'Song Info' custom event from a point in a scatter plot { "type":"customEvent", "dashboard":"xxxx", "name":"xxxx", "object":"xxxx", "data":{ "language":"en", "x-axis":{"id":0.601,"value":"0.601","label":"Danceability"}, "y-axis":{"id":0.532,"value":"0.532","label":"Energy"}, "name":{"id":"xxxx","value":"xxx","label":"Name"}, "event":"song_info" } }
The possibilities with this functionality are virtually limitless. Granted, depending on what you want to do, you may have to write a couple more lines of code, but it is unarguably quite a powerful tool!
The dashboard
We won’t actually go through the dashboard creation process here and we’ll focus on the interactivity bit once it’s integrated into the application. The dashboards integrated in this walk through have already been created and have custom events enabled. You can, of course create your own ones and integrate those instead of the one we’ve pre-built (you can create an account with a free trial). But before, some background info on Cumul.io dashboards;
Cumul.io offers you a way to create dashboards from within the platform, or via its API. In either case, dashboards will be available within the platform, decoupled from the application you want to integrate it into, so can be maintained completely separately.
On your landing page you’ll see your dashboards and can create a new one:
You can open one and drag and drop any chart you want:
You can connect data which you can then drag and drop into those charts:
And, that data can be one of a number of things. Like a pre-existing database which you can connect to Cumul.io, a dataset from a data warehouse you use, a custom built plugin etc.
Enabling custom events
We have already enabled these custom events to the scatter plot and table in the dashboard used in this demo, which we will be integrating in the next section. If you want to go through this step, feel free to create your own dashboards too!
First thing you need to do will be to add custom events to a chart. To do this, first select a chart in your dashboard you’d like to add an event to. In the chart settings, select Interactivity and turn Custom Events on:
To add an event, click edit and define its Event Name and Label. Event Name is what your application will receive and Label is the one that will show up on your dashboard. In our case, we’ve added 2 events; ‘Add to Playlist’ and ‘Song Info’:
This is all the setup you need for your dashboard to trigger an event on a chart level. Before you leave the editor, you will need your dashboard ID to integrate the dashboard later. You can find this in the Settings tab of your dashboard. The rest of the work remains on application level. This will be where we define what we actually want to do once we receive any of these events.
Takeaway points
Events work on a chart level and will include information within the limits of the information on the chart
To add an event, go to the chart settings on the chart you want to add them to
Define name and label of event. And you’re done!
(Don’t forget to take note of the dashboard ID for integration)
Using custom events in your own platform
Now that you’ve added some events to the dashboard, the next step is to use them. The key point here is that, once you click an event in your dashboard, your application that integrates the dashboard receives an event. The Integration API provides a function to listen to these events, and then it’s up to you to define what you do with them. For more information on the API and code examples for your SDK, you can also check out the relevant developer docs.
For this section, we’re also providing an open GitHub repository (separate to the repository for the main application) that you can use as a starting project to add custom events to.
The cumulio-spotify-datatalks repository is structured so that you can checkout on the commit called skeleton to start from the beginning. All the following commits will represent a step we go through here. It’s a boiled down version of the full application, focusing on the main parts of the app that demonstrates Custom Events. I’ll be skipping some steps such as the Spotify API calls which are in src/spotify.js, so as to limit this tutorial to the theme of ‘adding and using custom events’.
Let’s have a look at what happens in our case. We had created two events; add_to_playlist and song_info. We want visitors of our dashboard to be able to add a song to their own playlist of choice in their own Spotify account. In order to do so, we take the following steps:
First, we need to add a dashboard to our application. Here we use the Cumul.io Spotify Playlist dashboard as the main dashboard and the Song Info dashboard as the drill through dashboard (meaning we create a new dashboard within the main one that pops up when we trigger an event). If you have checked out on the commit called skeleton and npm run start, the application should currently just open up an empty ‘Cumul.io Favorites’ tab, with a Login button at the top right. For instructions on how to locally run the project, go to the bottom of the article:
To integrate a dashboard, we will need to use the Cumulio.addDashboard() function. This function expects an object with dashboard options. Here’s what we do to add the dashboard:
In src/app.js, we create an object that stores the dashboard IDs for the main dashboard and the drill through dashboard that displays song info alongside a dashboardOptions object:
// create dashboards object with the dashboard ids and dashboardOptions object // !!!change these IDs if you want to use your own dashboards!!! const dashboards = { playlist: 'f3555bce-a874-4924-8d08-136169855807', songInfo: 'e92c869c-2a94-406f-b18f-d691fd627d34', }; const dashboardOptions = { dashboardId: dashboards.playlist, container: '#dashboard-container', loader: { background: '#111b31', spinnerColor: '#f44069', spinnerBackground: '#0d1425', fontColor: '#ffffff' } };
We create a loadDashboard() function that calls Cumulio.addDashboard(). This function optionally receives a container and modifies the dashboardOptions object before adding dashboard to the application.
// create a loadDashboard() function that expects a dashboard ID and container const loadDashboard = (id, container) => { dashboardOptions.dashboardId = id; dashboardOptions.container = container || '#dashboard-container'; Cumulio.addDashboard(dashboardOptions); };
Finally, we use this function to add our playlist dashboard when we load the Cumul.io Favorites tab:
At this point, we’ve integrated the playlist dashboard and when we click on a point in the Energy/Danceability by Song scatter plot, we get two options with the custom events we added earlier. However, we’re not doing anything with them yet.
Listen to incoming events
Now that we’ve integrated the dashboard, we can tell our app to do stuff when it receives an event. The two charts that have ‘Add to Playlist’ and ‘Song Info’ events here are:
First, we need to set up our code to listen to incoming events. To do so, we need to use the Cumulio.onCustomEvent() function. Here, we chose to wrap this function in a listenToEvents() function that can be called when we load the Cumul.io Favorites tab. We then use if statements to check what event we’ve received:
This is the point after which things are up to your needs and creativity. For example, you could simply print a line out to your console, or design your own behaviour around the data you receive from the event. Or, you could also use some of the helper functions we’ve created that will display a playlist selector to add a song to a playlist, and integrate the Song Info dashboard. This is how we did it;
Add song to playlist
Here, we will make use of the addToPlaylistSelector() function in src/ui.js. This function expects a Song Name and ID, and will display a window with all the available playlists of the logged in user. It will then post a Spotify API request to add the song to the selected playlist. As the Spotify Web API requires the ID of a song to be able to add it, we’ve created a derived Name & ID field to be used in the scatter plot.
An example event we receive on add_to_playlist will include the following for the scatter plot:
"name":{"id":"So Far To Go&id=3R8CATui5dGU42Ddbc2ixE","value":"So Far To Go&id=3R8CATui5dGU42Ddbc2ixE","label":"Name & ID"}
We extract the Name and ID of the song from the event via the getSong() function, then call the ui.addToPlaylistSelector() function:
/*********** LISTEN TO CUSTOM EVENTS AND ADD EXTRAS ************/ const getSong = (event) => { let songName; let songArtist; let songId; if (event.data.columns === undefined) { songName = event.data.name.id.split('&id=')[0]; songId = event.data.name.id.split('&id=')[1]; } else { songName = event.data.columns[0].value; songArtist = event.data.columns[1].value; songId = event.data.columns[event.data.columns.length - 1].value; } return {id: songId, name: songName, artist: songArtist}; }; const listenToEvents = () => { Cumulio.onCustomEvent(async (event) => { const song = getSong(event); console.log(JSON.stringify(event)); if (event.data.event === 'add_to_playlist'){ await ui.addToPlaylistSelector(song.name, song.id); } else if (event.data.event === 'song_info'){ //DO SOMETHING } }); };
Now, the ‘Add to Playlist’ event will display a window with the available playlists that a logged in user can add the song to:
Display more song info
The final thing we want to do is to make the ‘Song Info’ event display another dashboard when clicked. It will display further information on the selected song, and include an option to play the song. It’s also the step where we get into more some more complicated use cases of the API which may need some background knowledge. Specifically, we make use of Parameterizable Filters. The idea is to create a parameter on your dashboard, for which the value can be defined while creating an authorization token. We include the parameter as metadata while creating an authorization token.
For this step, we have created a songId parameter that is used in a filter on the Song Info dashboard:
Then, we create a getDashboardAuthorizationToken() function. This expects metadata which it then posts to the /authorization endpoint of our server in server/server.js:
const getDashboardAuthorizationToken = async (metadata) => { try { const body = {}; if (metadata && typeof metadata === 'object') { Object.keys(metadata).forEach(key => { body[key] = metadata[key]; }); } /* Make the call to the backend API, using the platform user access credentials in the header to retrieve a dashboard authorization token for this user */ const response = await fetch('/authorization', { method: 'post', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } }); // Fetch the JSON result with the Cumul.io Authorization key & token const responseData = await response.json(); return responseData; } catch (e) { return { error: 'Could not retrieve dashboard authorization token.' }; } };
Finally, we use the load the songInfo dashboard when the song_info event is triggered. In order to do this, we create a new authorization token using the song ID:
And voilá! We are done! In this demo we used a lot of helper functions I haven’t gone through in detail, but you are free clone the demo repository and play around with them. You can even disregard them and build your own functionality around the custom events.
Conclusion
For any one intending to have a layer of data visualisation and analytics integrated into their application, Cumul.io provides a pretty easy way of achieving it as I’ve tried to demonstrate throughout this demo. The dashboards remain decoupled entities to the application that can then go on to be managed separately. This becomes quite an advantage if say you’re looking at integrated analytics within a business setting and you’d rather not have developers going back and fiddling with dashboards all the time.
Events you can trigger from dashboards and listen to in their host applications on the other hand allows you to define implementations based off of the information in those decoupled dashboards. This can be anything from playing a song in our case to triggering a specific email to be sent. The world is your oyster in this sense, you decide what to do with the data you have from your analytics layer. In other words, you get to reuse the data from your dashboards, it doesn’t have to just stay there in its dashboard and analytics world 🙂
I’m thoroughly convinced that SVG unlocks a whole entire world of building interfaces on the web. It might seem daunting to learn SVG at first, but you have a spec that was designed to create shapes and yet, still has elements, like text, links, and aria labels available to you. You can accomplish some of the same effects in CSS, but it’s a little more particular to get positioning just right, especially across viewports and for responsive development.
What’s special about SVG is that all the positioning is based on a coordinate system, a little like the game Battleship. That means deciding where everything goes and how it’s drawn, as well as how it’s relative to each other, can be really straightforward to reason about. CSS positioning is for layout, which is great because you have things that correspond to one another in terms of the flow of the document. This otherwise positive trait is harder to work with if you’re making a component that’s very particular, with overlapping and precisely placed elements.
Truly, once you learn SVG, you can draw anything, and have it scale on any device. Even this very site uses SVG for custom UI elements, such as my avatar, above (meta!).
That little half circle below the author image is just SVG markup.
We won’t cover everything about SVGs in this post (you can learn some of those fundamentals here, here, here and here), but in order to illustrate the possibilities that SVG opens up for UI component development, let’s talk through one particular use case and break down how we would think about building something custom.
The timeline task list component
Recently, I was working on a project with my team at Netlify. We wanted to show the viewer which video in a series of videos in a course they were currently watching. In other words, we wanted to make some sort of thing that’s like a todo list, but shows overall progress as items are completed. (We made a free space-themed learning platform and it’s hella cool. Yes, I said hella.)
Here’s how that looks:
So how would we go about this? I’ll show an example in both Vue and React so that you can see how it might work in both frameworks.
The Vue version
We decided to make the platform in Next.js for dogfooding purposes (i.e. trying out our own Next on Netlify build plugin), but I’m more fluent in Vue so I wrote the initial prototype in Vue and ported it over to React.
Here is the full CodePen demo:
Let’s walk through this code a bit. First off, this is a single file component (SFC), so the template HTML, reactive script, and scoped styles are all encapsulated in this one file.
We’ll store some dummy tasks in data, including whether each task is completed or not. We’ll also make a method we can call on a click directive so that we can toggle whether the state is done or not.
Now, what we want to do is create an SVG that has a flexible viewBox depending on the amount of elements. We also want to tell screen readers that this a presentational element and that we will provide a title with a unique id of timeline. (Get more information on creating accessible SVGs.)
The stroke is set to currentColor to allow for some flexibility — if we want to reuse the component in multiple places, it will inherit whatever color is used on the encapsulating div.
Next, inside the SVG, we want to create a vertical line that’s the length of the task list. Lines are fairly straightforward. We have x1 and x2 values (where the line is plotted on the x-axis), and similarly, y1 and y2.
The x-axis stays consistently at 10 because we’re drawing a line downward rather than left-to-right. We’ll store two numbers in data: the amount we want our spacing to be, which will be num1, and the amount we want our margin to be, which will be num2.
data() { return { num1: 32, num2: 15, // ... } }
The y-axis starts with num2, which is subtracted from the end, as well as the margin. The tasks.length is multiplied by the spacing, which is num1.
Now, we’ll need the circles that lie on the line. Each circle is an indicator for whether a task has been completed or not. We’ll need one circle for each task, so we’ll use v-for with a unique key, which is the index (and is safe to use here as they will never reorder). We’ll connect the click directive with our method and pass in the index as a param as well.
CIrcles in SVG are made up of three attributes. The middle of the circle is plotted at cx and cy, and then we draw a radius with r. Like the line, cx starts at 10. The radius is 4 because that’s what’s readable at this scale. cy will be spaced like the line: index times the spacing (num1), plus the margin (num2).
Finally, we’ll put use a ternary to set the fill. If the task is done, it will be filled with currentColor. If not, it will be filled with white (or whatever the background is). This could be filled with a prop that gets passed in the background, for instance, where you have light and dark circles.
Finally, we are using CSS grid to align a div with the names of tasks. This is laid out much in the same way, where we’re looping through the tasks, and are also tied to that same click event to toggle the done state.
Here is where we ended up with the React version. We’re working towards open sourcing this so that you can see the full code and its history. Here are a few modifications:
We’re using CSS modules rather than the SCFs in Vue
We’re importing the Next.js link, so that rather than toggling a “done” state, we’re taking a user to a dynamic page in Next.js
The tasks we’re using are actually stages of the course —or “Mission” as we call them — which are passed in here rather than held by the component.
This component is flexible enough to accommodate lists small and large, multiple browsers, and responsive sizing. It also allows the user to have better understanding of where they are in their progress in the course.
But this is just one component. You can make any number of UI elements: knobs, controls, progress indicators, loaders… the sky’s the limit. You can style them with CSS, or inline styles, you can have them update based on props, on context, on reactive data, the sky’s the limit! I hope this opens some doors on how you yourself can develop more engaging UI elements for the web.
This is a neat page that compares a ton of different libraries with web components. One of the things I learned after posting “A Bit on Web Components Libraries” is that the web platform APIs were designed for libraries to be built around them. Interesting, right?
This page makes a counter component. By extending HTMLElement natively, they do it in 1,293 bytes, then each library adds things on top of that. The big libraries, like Vue and React, are clearly much bigger (but bring a ton of other functionality to the table). One of the biggest is CanJS (230,634 bytes), which isn’t aiming to be small, but, from their about page: “It targets experienced developers building complex applications with long futures ahead of them.” If the goal is small, Svelte is true to its mission of nearly compiling itself away ending at just 3,592 bytes, a third of the size of the super tiny lit-html and half the size of uhtml — both of which are just tiny abstractions that offer nicer templating and re-rendering.