Tag: Interactive

Building Interactive Figma Widgets

Figma has always encouraged collaboration between developers and designers. It strives on an endless treasury of community-made plugins. Need 3D elements? There’s a plugin for that. Need abstract SVGs? There’s a plugin for that, too.

That said, the design part of Figma has always been relatively static — always working with unmovable rectangles connected to each other through predefined user interactions. But what if I told you that your designs could suddenly come to life — that they could be animated, interactive, and even stateful? Then, what would separate concept from implementation?

Figma announced in June that it’s bringing JavaScript-powered widgets to the table. Now, designers have can browse and implement logic-driven components straight in Figma!

Say hello to the Widgets API! You want to know what it is and how to use it? That’s exactly what we’re going to do together in this post.

Figma widgets open up tons of possibilities

Imagine that you’re working around the clock with your partner to design a large restaurant application. You’re both already collaborating on the same Figma board; both of you are sharing the exact same document with changes happening on the fly.

Surely, you already know that collaboration involves more that just the design process:

  • project management,
  • hosting polls to gather votes,
  • importing and visualizing mock data,
  • and perhaps even playing a multiplayer game to cool-off after many hours of work.

We just require one person to manage everything and send-out links to other members of the group. But oh, that’s not very efficient, is it?

Well, that’s where widgets come into play. We can conceivably do all of that — yes, everything —without ever leaving Figma.

Here are just a few of the ways you might want to use widgets in Figma:

The list goes on and on. As you can tell, there’s already a plethora of widgets that you can freely use in your documents. In fact, you can add Widgets straight to your board from the Widgets menu (Shift+I).

But we’re not here to learn how to use widgets, because that’s easy. Let us do what we do best: we’re gonna create our own Figma widget! This one will be inspired by Chris Coyier’s design quotes website. We’ll take the API, feed it into the widget, then display random design quotes directly in Figma.

Here’s what we need

I don’t like to be the bearer of bad news, but in order to develop widgets, you must be on Windows or Mac. Linux users, I’m sorry, but you’re out of luck. (You could still use a VM if you want to follow along.)

We’re gonna download the Figma Desktop application. The simplest way to get started is by generating a widget template, straight from the app.

Let’s create a new board by opening the widgets menu (ShiftI), switching to the Development tab, and creating a new item.

Following that, Figma will prompt you to name the new widget and decide whether it’s more tailored towards design boards or FigJam boards too. The former option is sufficient for the purposes of this article.

And the customization doesn’t end here; Figma will also give you the option to start with a pre-made counter widget or an iFrame-enabled alternative that also gives you access to the Canvas and Fetch APIs (as well as all other browser APIs). We’ll go with the simple “Empty” option, but we’ll eventually modify it ourselves to make use of the Fetch API.

You’ll then be prompted to save your new widget project to a special directory in your system. Once that’s done, launch your terminal and direct it to that folder. Don’t run any commands yet — we’ll do that later and purposefully get an error with the goal of learning more about the Widgets API.

Designing the widget

We’re pulling the design straight from Chris Coyier’s design quotes website. So, let’s go there and dive into by firing up DevTools.

The two key shortcuts that I’m using here are Ctrl+Shift+C (or Cmd+Shift+C) to toggle the “Pick element” tool, and Shift+Click to change the color format to HEX code. We’re doing this to learn about the colors, fonts, font weights and font sizes used in Chris’s website. All this information is critical to build a closely-resembling widget in Figma, which will be our next step! You can grab the designed component and use it in your own canvas.

I won’t go into much detail here as this article’s main topic is building widgets by writing code. But I can’t stress enough how important it is to take good care of your widgets’ style… CSS-Tricks already has a plethora of design-oriented Figma tutorials; you won’t regret adding them to your reading list.

Creating the layout for our widget

With design out of the way, it’s time to take our programming fingers out and start building the gears of our widget.

It’s very interesting how Figma translates its design building blocks to React-like components. Frame elements with the auto-layout feature, for example, are represented as the <AutoLayout /> component in code. In addition to that, we’ll be using two more components: <Text /> and <SVG />.

Take a look at my Figma board… I’m precisely asking you to focus on the object tree. It’s what we need to be able to translate our widget design to JSX code.

As you can see, our design quotes widget demands three components to be imported. That’s a decent number of components considering that the full API only contains eight layer-based nodes. But as you’ll soon see, these modules are more than sufficient to craft all kinds of layouts.

// code.tsx const { widget } = figma; const { AutoLayout, Text, SVG } = widget;

And with this, we have all we need to go ahead and build the skeleton of our widget like we would in React:

function QuotesWidget() {   const quote = `...`;   const author = `...`;    return (     <AutoLayout>       <SVG />       <AutoLayout>         <Text>{quote}</Text>         <Text>— Eluda</Text>       </AutoLayout>       <SVG />     </AutoLayout>   ); }  widget.register(QuotesWidget);

This code is very confusing, to say the least. Right now, we can’t tell the design layers apart. Thankfully, we’re able to easily solve this issue through the use of the name property.

<AutoLayout name={"Quote"}>   <SVG name={"LeftQuotationMark"} />   <AutoLayout name={"QuoteContent"}>     <Text name={"QuoteText"}>{quote}</Text>     <Text name={"QuoteAuthor"}>— Eluda</Text>   </AutoLayout>   <SVG name={"RightQuotationMark"} /> </AutoLayout>;

And, of course, we still can’t see our quotation mark SVGs, so let’s work on fixing that. The <SVG/> component accept a srcproperty that takes the source code for an SVG element. There isn’t much to say on this one, so let’s keep it simple and jump straight back to code:

const leftQuotationSvgSrc = `<svg width="117" height="103" viewBox="0 0 117 103" fill="none" xmlns="<http://www.w3.org/2000/svg>">   // shortened for brevity </svg>`; const rightQuotationSvgSrc = `<svg width="118" height="103" viewBox="0 0 118 103" fill="none" xmlns="<http://www.w3.org/2000/svg>"> // shortened for brevity </svg>`;  function QuotesWidget() {   return (     <SVG name={"LeftQuotationMark"} src={leftQuotationSvgSrc} />     <SVG name={"RightQuotationMark"} src={rightQuotationSvgSrc} />   ); }

I think we can all agree that everything is much clearer now! When we name things, their purpose suddenly becomes much more obvious to the readers of our code.

Previewing our widget in real-time

Figma offers a great developer experience when building widgets, including (but not limited to ) hot-reloading. With this feature, we’re able to code and preview changes to our widget in real-time.

Get started by opening the widgets menu (Shift+I), switching to the development tab and clicking or dragging your new widget to the board. Unable to locate your widget? Don’t worry, just click on the three-dot menu and import your widget’s manifest.json file. Yes, that’s all it takes bring it back to existence!

Wait, did you get an error message at the bottom of your screen?

If so, let’s investigate. Click on “Open console” and read what it has to say. If the Open console button is gone, there’s an alternative way to open the debugging console. Click on the Figma logo, jump to the widgets category and reveal the development menu.

That error is likely due to the fact that we haven’t compiled our TypeScript to JavaScript yet. We can do that in the command line by running npm install and npm run watch. (or yarn and yarn watch ). No errors this time!

One more obstacle you might hit is that the widget fails to re-render any time the code is changed. We can easily force our widget to update using the following context menu command: Widgets → Re-render widget.

Styling the widget

As it currently stands, the looks of our widgets are still pretty far from our final goal.

So how do we style Figma components from code? Maybe with CSS like we would do in a React project? Negative. With Figma widgets, all the styling happens through a set of well-documented props. Lucky for us, these items are named almost identically to their counterparts in Figma.

We’ll get started by configuring our two <AutoLayout /> components. As you can see in the infographic above, prop names are pretty descriptive of their purpose. This makes it easy for us to jump straight into code and start making some changes. I won’t be showing the whole code again, so please rely on the component names to guide you where the snippets belongs.

<AutoLayout   name={"Quote"}   direction={"horizontal"}   verticalAlignItems={"start"}   horizontalAlignItems={"center"}   spacing={54}   padding={{     horizontal: 61,     vertical: 47,   }} >   <AutoLayout     name={"QuoteContent"}     direction={"vertical"}     verticalAlignItems={"end"}     horizontalAlignItems={"start"}     spacing={10}     padding={{       horizontal: 0,       vertical: 0,     }}   ></AutoLayout> </AutoLayout>;

We just made a lot of progress! Let’s save and jump back to Figma to see how our widget looks like. Remember how Figma reloads widgets automatically upon new changes?

But it’s not quite there yet. We must also add a background color to the root component:

<AutoLayout name={"Quote"} fill={"#ffffff"}>

Again, take a look at your Figma board and notice how changes can be reflected almost immediately back into the widget.

Let’s move along this guide and style the <Text> components.

After taking a look at the Widgets API documentation, it’s again clear that property names are almost identical to their counterparts in the Figma app, as can be seen in the infographic above. We’ll also be using values from the last section where we inspected Chris’ website.

<Text name={'QuoteText'}   fontFamily={'Lora'}   fontSize={36}   width={700}   fill={'#545454'}   fontWeight={'normal'} >{quote}</Text>  <Text name={'QuoteAuthor'}   fontFamily={'Raleway'}   fontSize={26}   width={700}   fill={'#16B6DF'}   fontWeight={'bold'}   textCase={'upper'} >— Eluda</Text>

Adding state to the widget

Oour widget currently displays the same quote, but we want to pull from the entire pool of quotes at random. We must add state to our widget, which all React developers know is a variable whose change triggers the re-rendering of our component.

With Figma, state is created with the useSyncedState hook; it’s pretty much React’s useState, but it requires programmers to specify a unique key. This requirement stems from the fact that Figma must sync our widget’s state across all clients that may be viewing the same design board, but through different computers.

const { useSyncedState } = widget;  function QuotesWidget() {   const [quote, setQuote] = useSyncedState("quote-text", "");   const [author, setAuthor] = useSyncedState("quote-author", ""); }

That’s all the change that we need for now. In the next section, we’ll figure out how to fetch data from the Internet. Spoiler Alert: it’s not as simple as it seems.

Fetching data from the network

Recall when Figma gave us the choice to start with an iFrame-enabled widget. Although we didn’t go with that option, we must still implement some of its features. Let me explain why we can’t simply call fetch() within our widget code.

When you use a widget, you are running JavaScript code on your own computer that’s written by someone else. While all widgets are thoroughly reviewed by the Figma staff, it’s still a huge security hole as we all know how much damage can be created by even one line of JavaScript.

As a result, Figma cannot simply eval() any widget code written by anonymous programmers. Long story short, the team decided that the best solution was running third-party code in a closely-guarded sandbox environment. And as you might have guessed, browser APIs are unavailable in such an environment.

But don’t fret, Figma’s solution to this second problem is <iframe>s. Any HTML code that we write in a file, preferably called ui.html, will have access to all browser APIs. You might be wondering how we can trigger this code from the widget, but we’ll look into that later. Right now, let’s jump back into code:

// manifest.json {   "ui": "ui.html" }
<!-- ui.html --> <script> window.onmessage = async (event) => {   if (event.data.pluginMessage.type === 'networkRequest') {     // TODO: fetch data from the server      window.parent.postMessage({       pluginMessage: {         // TODO: return fetched data       }     }, '*')   } } </script>

That’s the general template for widget-to-iframe communication. Let’s use it to fetch data from the server:

<!-- ui.html --> <script> window.onmessage = async (event) => {   if (event.data.pluginMessage.type === 'networkRequest') {     // Get random number from 0 to 100     const randomPage = Math.round(Math.random() * 100)      // Get a random quote from the Design Quotes API     const res = await fetch(`https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand&per_page=1&page=$ {randomPage}&_fields=title,yoast_head_json`)     const data = await res.json()      // Extract author name and quote content from response     const authorName = data[0].title.rendered     const quoteContent = data[0].yoast_head_json.og_description      window.parent.postMessage({       pluginMessage: {         authorName,         quoteContent       }     }, '*')   } } </script>

We’re leaving out error-handling to keep this simple and to-the-point. Let’s jump back into the widget code and see how we access functions defined in the <iframe>:

function fetchData() {   return new Promise<void>(resolve => {     figma.showUI(__html__, {visible: false})     figma.ui.postMessage({type: 'networkRequest'})      figma.ui.onmessage = async ({authorName, quoteContent}) => {       setAuthor(authorName)       setQuote(quoteContent)        resolve()     }   }) }

As you can see, we’re first telling Figma to expose access to our hidden <iframe> and to trigger an event with the name "networkRequest". We’re handling this event in the ui.html file by checking event.data.pluginMessage.type === 'networkRequest', and then posting data back to the widget.

But nothing is happening yet… We still haven’t called the fetchData() function. If we call it directly in the component function, the following error occurs in the console:

Cannot use showUI during widget rendering.

Figma is telling us not to call showUI directly in the function body… So, where should we put it? The answer to that is one new hook and one new function: useEffect and waitForTask. You might already have familiarity with useEffect if you’re a React developer, but we’re gonna use it here to fetch data from the server when the widget component mounts.

const { useEffect, waitForTask } = widget;  function QuotesWidget() {   useEffect(() => {     waitForTask(fetchData());   }); }

But this will result in yet another “error” where our widget will keep re-rendering with a new quote, forever. This happens because useEffect, by definition, triggers again whenever the widget’s state changes, nay when we call fetchData. And while there’s a technique to only call useEffect once in React, it does not work on Figma’s implementation. From Figma’s docs:

Because of How Widgets Run, useEffect should handle being called multiple times with the same state.

Thankfully, there’s a simple workaround that we can take advantage of and call useEffect only once when the component first mounts, and it’s by checking whether or not the state’s values are still empty:

function QuotesWidget() {   useEffect(() => {     if (!author.length & !quote.length) {       waitForTask(fetchData());     }   }); }

You might run into a scary “memory access out of bounds” error. It’s quite common to see in plugin and widget development. Just restart Figma and it won’t be there anymore.

You might have noticed that sometimes, the quote text contains weird characters.

These are Unicode characters and we must properly format them in code:

<!-- ui.html --> <script> window.onmessage = async (event) => {   // ...   const quoteContent = decodeEntities(data[0].yoast_head_json.og_description); };  // <https://stackoverflow.com/a/9609450> var decodeEntities = (function () {   // this prevents any overhead from creating the object each time   var element = document.createElement("div");    function decodeHTMLEntities(str) {     if (str && typeof str === "string") {       // strip script/html tags       str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gim, "");       str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gim, "");       element.innerHTML = str;       str = element.textContent;       element.textContent = "";     }      return str;   }    return decodeHTMLEntities; })(); </script>

And voilà, our widget fetched a brand new design quote every single time it’s added to the design board.

Adding a property menu to our widget

While our widget fetches a fresh quote upon instantiation, it would be much more practical if we could do this process again but without deleting it. This section will be short as the solution is quite remarkable. With property menus, we can add interactivity to our widget with a single call to the usePropertyMenu hook.

Credit: Figma Docs.
const { usePropertyMenu } = widget;  function QuotesWidget() {   usePropertyMenu(     [       {         itemType: "action",         propertyName: "generate", 	tooltip: "Generate",         icon: `<svg width="22" height="15" viewBox="0 0 22 15" fill="none" xmlns="<http://www.w3.org/2000/svg>">           <!-- Shortened for brevity -->         </svg>`,       },     ],     () => fetchData()   ); }

With one simple hook we’re able to create a button that appears near our widget when it’s selected. That was the last piece that we needed to add in order to complete this project.

Publishing our widget to the public

There’s not much use in building a widget if, well, no one uses it. And while Figma grants organizations with the option to launch private widgets for internal use, it’s much more common to release these little programs to the world.

Figma has a delicate widget review process that may take up 5 to 10 business days. And while the design quotes widget we built together is already in the widget library, I will still demonstrate how it got there. Please don’t attempt to re-publish this widget again as that will only result in removal. But if you gave it some significant alterations, go ahead and share your own widget with the community!

Get started by clicking the widgets menu (Shift+I) and switching to the Development tab to view our widget. Click on the three-dots menu and press Publish.

Figma will prompt you to enter some details about your widget, such as a title, description, and some tags. We’ll also need a 128×128 icon image and a 1920×960 banner image.

After importing all these assets, we still need a screenshot of our widget. Close the publishing modal (don’t worry, you won’t lose your data) and right-click on the widget to reveal an interesting context menu. Find the Copy/Paste ascategory and select Copy as PNG.

With that done, let’s go back to the publishing modal and paste the widget’s screenshot:

Scroll down and finally publish your modal. Celebrate! 🎉

Figma will reach out to you in a couple of days about the status of your modal’s review. In the case of a rejection, you’ll be given the opportunity to make changes and submit again.

Conclusion

We just built a Figma widget from scratch! There are many things not covered here, such as click eventsinput forms, and much more. You can dig into the full source code for the widget in this GitHub repo.

To those who aspire to take their Figma skills to greater levels, I suggest exploring the Widgets community and using what catches your eye as inspiration. Keep building more widgets, keep sharpening your React skills, and before you even realize it, you’ll be teaching me how to do all this.

Further resources

I had to refer to lots of documentation while I was making this widget. I thought I’d share what I found to help the most.

Build more widgets:

Learn widgets in greater depth:

Widgets vs. plugins


Building Interactive Figma Widgets originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , ,

An Interactive Starry Backdrop for Content

I was fortunate last year to get approached by Shawn Wang (swyx) about doing some work for Temporal. The idea was to cast my creative eye over what was on the site and come up with some ideas that would give the site a little “something” extra. This was quite a neat challenge as I consider myself more of a developer than a designer. But I love learning and leveling up the design side of my game.

One of the ideas I came up with was this interactive starry backdrop. You can see it working in this shared demo:

The neat thing about this design is that it’s built as a drop-in React component. And it’s super configurable in the sense that once you’ve put together the foundations for it, you can make it completely your own. Don’t want stars? Put something else in place. Don’t want randomly positioned particles? Place them in a constructed way. You have total control of what to bend it to your will.

So, let’s look at how we can create this drop-in component for your site! Today’s weapons of choice? React, GreenSock and HTML <canvas>. The React part is totally optional, of course, but, having this interactive backdrop as a drop-in component makes it something you can employ on other projects.

Let’s start by scaffolding a basic app

import React from 'https://cdn.skypack.dev/react' import ReactDOM from 'https://cdn.skypack.dev/react-dom' import gsap from 'https://cdn.skypack.dev/gsap'  const ROOT_NODE = document.querySelector('#app')  const Starscape = () => <h1>Cool Thingzzz!</h1>  const App = () => <Starscape/>  ReactDOM.render(<App/>, ROOT_NODE)

First thing we need to do is render a <canvas> element and grab a reference to it that we can use within React’s useEffect. For those not using React, store a reference to the <canvas> in a variable instead.

const Starscape = () => {   const canvasRef = React.useRef(null)   return <canvas ref={canvasRef} /> }

Our <canvas> is going to need some styles, too. For starters, we can make it so the canvas takes up the full viewport size and sits behind the content:

canvas {   position: fixed;   inset: 0;   background: #262626;   z-index: -1;   height: 100vh;   width: 100vw; }

Cool! But not much to see yet.

We need stars in our sky

We’re going to “cheat” a little here. We aren’t going to draw the “classic” pointy star shape. We’re going to use circles of differing opacities and sizes.

Draw a circle on a <canvas> is a case of grabbing a context from the <canvas> and using the arc function. Let’s render a circle, err star, in the middle. We can do this within a React useEffect:

const Starscape = () => {   const canvasRef = React.useRef(null)   const contextRef = React.useRef(null)   React.useEffect(() => {     canvasRef.current.width = window.innerWidth     canvasRef.current.height = window.innerHeight     contextRef.current = canvasRef.current.getContext('2d')     contextRef.current.fillStyle = 'yellow'     contextRef.current.beginPath()     contextRef.current.arc(       window.innerWidth / 2, // X       window.innerHeight / 2, // Y       100, // Radius       0, // Start Angle (Radians)       Math.PI * 2 // End Angle (Radians)     )     contextRef.current.fill()   }, [])   return <canvas ref={canvasRef} /> }

So what we have is a big yellow circle:

This is a good start! The rest of our code will take place within this useEffect function. That’s why the React part is kinda optional. You can extract this code out and use it in whichever form you like.

We need to think about how we’re going to generate a bunch of “stars” and render them. Let’s create a LOAD function. This function is going to handle generating our stars as well as the general <canvas> setup. We can also move the sizing logic of the <canvas> sizing logic into this function:

const LOAD = () => {   const VMIN = Math.min(window.innerHeight, window.innerWidth)   const STAR_COUNT = Math.floor(VMIN * densityRatio)   canvasRef.current.width = window.innerWidth   canvasRef.current.height = window.innerHeight   starsRef.current = new Array(STAR_COUNT).fill().map(() => ({     x: gsap.utils.random(0, window.innerWidth, 1),     y: gsap.utils.random(0, window.innerHeight, 1),     size: gsap.utils.random(1, sizeLimit, 1),     scale: 1,     alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),   })) }

Our stars are now an array of objects. And each star has properties that define their characteristics, including:

  • x: The star’s position on the x-axis
  • y: The star’s position on the y-axis
  • size: The star’s size, in pixels
  • scale: The star’s scale, which will come into play when we interact with the component
  • alpha: The star’s alpha value, or opacity, which will also come into play during interactions

We can use GreenSock’s random() method to generate some of these values. You may also be wondering where sizeLimit, defaultAlpha, and densityRatio came from. These are now props we can pass to the Starscape component. We’ve provided some default values for them:

const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {

A randomly generated star Object might look like this:

{   "x": 1252,   "y": 29,   "size": 4,   "scale": 1,   "alpha": 0.5 } 

But, we need to see these stars and we do that by rendering them. Let’s create a RENDER function. This function will loop over our stars and render each of them onto the <canvas> using the arc function:

const RENDER = () => {   contextRef.current.clearRect(     0,     0,     canvasRef.current.width,     canvasRef.current.height   )   starsRef.current.forEach(star => {     contextRef.current.fillStyle = `hsla(0, 100%, 100%, $  {star.alpha})`     contextRef.current.beginPath()     contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)     contextRef.current.fill()   }) }

Now, we don’t need that clearRect function for our current implementation as we are only rendering once onto a blank <canvas>. But clearing the <canvas> before rendering anything isn’t a bad habit to get into, And it’s one we’ll need as we make our canvas interactive.

Consider this demo that shows the effect of not clearing between frames.

Our Starscape component is starting to take shape.

See the code
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {   const canvasRef = React.useRef(null)   const contextRef = React.useRef(null)   const starsRef = React.useRef(null)   React.useEffect(() => {     contextRef.current = canvasRef.current.getContext('2d')     const LOAD = () => {       const VMIN = Math.min(window.innerHeight, window.innerWidth)       const STAR_COUNT = Math.floor(VMIN * densityRatio)       canvasRef.current.width = window.innerWidth       canvasRef.current.height = window.innerHeight       starsRef.current = new Array(STAR_COUNT).fill().map(() => ({         x: gsap.utils.random(0, window.innerWidth, 1),         y: gsap.utils.random(0, window.innerHeight, 1),         size: gsap.utils.random(1, sizeLimit, 1),         scale: 1,         alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),       }))     }     const RENDER = () => {       contextRef.current.clearRect(         0,         0,         canvasRef.current.width,         canvasRef.current.height       )       starsRef.current.forEach(star => {         contextRef.current.fillStyle = `hsla(0, 100%, 100%, $  {star.alpha})`         contextRef.current.beginPath()         contextRef.current.arc(star.x, star.y, star.size / 2, 0, Math.PI * 2)         contextRef.current.fill()       })     }     LOAD()     RENDER()   }, [])   return <canvas ref={canvasRef} /> }

Have a play around with the props in this demo to see how they affect the the way stars are rendered.

Before we go further, you may have noticed a quirk in the demo where resizing the viewport distorts the <canvas>. As a quick win, we can rerun our LOAD and RENDER functions on resize. In most cases, we’ll want to debounce this, too. We can add the following code into our useEffect call. Note how we also remove the event listener in the teardown.

// Naming things is hard... const RUN = () => {   LOAD()   RENDER() }  RUN()  // Set up event handling window.addEventListener('resize', RUN) return () => {   window.removeEventListener('resize', RUN) }

Cool. Now when we resize the viewport, we get a new generated starry.

Interacting with the starry backdrop

Now for the fun part! Let’s make this thing interactive.

The idea is that as we move our pointer around the screen, we detect the proximity of the stars to the mouse cursor. Depending on that proximity, the stars both brighten and scale up.

We’re going to need to add another event listener to pull this off. Let’s call this UPDATE. This will work out the distance between the pointer and each star, then tween each star’s scale and alpha values. To make sure those tweeted values are correct, we can use GreenSock’s mapRange() utility. In fact, inside our LOAD function, we can create references to some mapping functions as well as a size unit then share these between the functions if we need to.

Here’s our new LOAD function. Note the new props for scaleLimit and proximityRatio. They are used to limit the range of how big or small a star can get, plus the proximity at which to base that on.

const Starscape = ({   densityRatio = 0.5,   sizeLimit = 5,   defaultAlpha = 0.5,   scaleLimit = 2,   proximityRatio = 0.1 }) => {   const canvasRef = React.useRef(null)   const contextRef = React.useRef(null)   const starsRef = React.useRef(null)   const vminRef = React.useRef(null)   const scaleMapperRef = React.useRef(null)   const alphaMapperRef = React.useRef(null)      React.useEffect(() => {     contextRef.current = canvasRef.current.getContext('2d')     const LOAD = () => {       vminRef.current = Math.min(window.innerHeight, window.innerWidth)       const STAR_COUNT = Math.floor(vminRef.current * densityRatio)       scaleMapperRef.current = gsap.utils.mapRange(         0,         vminRef.current * proximityRatio,         scaleLimit,         1       );       alphaMapperRef.current = gsap.utils.mapRange(         0,         vminRef.current * proximityRatio,         1,         defaultAlpha       );     canvasRef.current.width = window.innerWidth     canvasRef.current.height = window.innerHeight     starsRef.current = new Array(STAR_COUNT).fill().map(() => ({       x: gsap.utils.random(0, window.innerWidth, 1),       y: gsap.utils.random(0, window.innerHeight, 1),       size: gsap.utils.random(1, sizeLimit, 1),       scale: 1,       alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),     }))   } }

And here’s our UPDATE function. It calculates the distance and generates an appropriate scale and alpha for a star:

const UPDATE = ({ x, y }) => {   starsRef.current.forEach(STAR => {     const DISTANCE = Math.sqrt(Math.pow(STAR.x - x, 2) + Math.pow(STAR.y - y, 2));     gsap.to(STAR, {       scale: scaleMapperRef.current(         Math.min(DISTANCE, vminRef.current * proximityRatio)       ),       alpha: alphaMapperRef.current(         Math.min(DISTANCE, vminRef.current * proximityRatio)       )     });   }) }; 

But wait… it doesn’t do anything?

Well, it does. But, we haven’t set our component up to show updates. We need to render new frames as we interact. We can reach for requestAnimationFrame often. But, because we’re using GreenSock, we can make use of gsap.ticker. This is often referred to as “the heartbeat of the GSAP engine” and it’s is a good substitute for requestAnimationFrame.

To use it, we add the RENDER function to the ticker and make sure we remove it in the teardown. One of the neat things about using the ticker is that we can dictate the number of frames per second (fps). I like to go with a “cinematic” 24fps:

// Remove RUN LOAD() gsap.ticker.add(RENDER) gsap.ticker.fps(24)  window.addEventListener('resize', LOAD) document.addEventListener('pointermove', UPDATE) return () => {   window.removeEventListener('resize', LOAD)   document.removeEventListener('pointermove', UPDATE)   gsap.ticker.remove(RENDER) }

Note how we’re now also running LOAD on resize. We also need to make sure our scale is being picked up in that RENDER function when using arc:

const RENDER = () => {   contextRef.current.clearRect(     0,     0,     canvasRef.current.width,     canvasRef.current.height   )   starsRef.current.forEach(star => {     contextRef.current.fillStyle = `hsla(0, 100%, 100%, $  {star.alpha})`     contextRef.current.beginPath()     contextRef.current.arc(       star.x,       star.y,       (star.size / 2) * star.scale,       0,       Math.PI * 2     )     contextRef.current.fill()   }) }

It works! 🙌

It’s a very subtle effect. But, that’s intentional because, while it’s is super neat, we don’t want this sort of thing to distract from the actual content. I’d recommend playing with the props for the component to see different effects. It makes sense to set all the stars to low alpha by default too.

The following demo allows you to play with the different props. I’ve gone for some pretty standout defaults here for the sake of demonstration! But remember, this article is more about showing you the techniques so you can go off and make your own cool backdrops — while being mindful of how it interacts with content.

Refinements

There is one issue with our interactive starry backdrop. If the mouse cursor leaves the <canvas>, the stars stay bright and upscaled but we want them to return to their original state. To fix this, we can add an extra handler for pointerleave. When the pointer leaves, this tweens all of the stars down to scale 1 and the original alpha value set by defaultAlpha.

const EXIT = () => {   gsap.to(starsRef.current, {     scale: 1,     alpha: defaultAlpha,   }) }  // Set up event handling window.addEventListener('resize', LOAD) document.addEventListener('pointermove', UPDATE) document.addEventListener('pointerleave', EXIT) return () => {   window.removeEventListener('resize', LOAD)   document.removeEventListener('pointermove', UPDATE)   document.removeEventListener('pointerleave', EXIT)   gsap.ticker.remove(RENDER) }

Neat! Now our stars scale back down and return to their previous alpha when the mouse cursor leaves the scene.

Bonus: Adding an Easter egg

Before we wrap up, let’s add a little Easter egg surprise to our interactive starry backdrop. Ever heard of the Konami Code? It’s a famous cheat code and a cool way to add an Easter egg to our component.

We can practically do anything with the backdrop once the code runs. Like, we could make all the stars pulse in a random way for example. Or they could come to life with additional colors? It’s an opportunity to get creative with things!

We’re going listen for keyboard events and detect whether the code gets entered. Let’s start by creating a variable for the code:

const KONAMI_CODE =   'arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya';

Then we create a second effect within our the starry backdrop. This is a good way to maintain a separation of concerns in that one effect handles all the rendering, and the other handles the Easter egg. Specifically, we’re listening for keyup events and check whether our input matches the code.

const codeRef = React.useRef([]) React.useEffect(() => {   const handleCode = e => {     codeRef.current = [...codeRef.current, e.code]       .slice(         codeRef.current.length > 9 ? codeRef.current.length - 9 : 0       )     if (codeRef.current.join(',').toLowerCase() === KONAMI_CODE) {       // Party in here!!!     }   }   window.addEventListener('keyup', handleCode)   return () => {     window.removeEventListener('keyup', handleCode)   } }, [])

We store the user input in an Array that we store inside a ref. Once we hit the party code, we can clear the Array and do whatever we want. For example, we may create a gsap.timeline that does something to our stars for a given amount of time. If this is the case, we don’t want to allow Konami code to input while the timeline is active. Instead, we can store the timeline in a ref and make another check before running the party code.

const partyRef = React.useRef(null) const isPartying = () =>   partyRef.current &&   partyRef.current.progress() !== 0 &&   partyRef.current.progress() !== 1;

For this example, I’ve created a little timeline that colors each star and moves it to a new position. This requires updating our LOAD and RENDER functions.

First, we need each star to now have its own hue, saturation and lightness:

// Generating stars! ⭐️ starsRef.current = new Array(STAR_COUNT).fill().map(() => ({   hue: 0,   saturation: 0,   lightness: 100,   x: gsap.utils.random(0, window.innerWidth, 1),   y: gsap.utils.random(0, window.innerHeight, 1),   size: gsap.utils.random(1, sizeLimit, 1),   scale: 1,   alpha: defaultAlpha }));

Second, we need to take those new values into consideration when rendering takes place:

starsRef.current.forEach((star) => {   contextRef.current.fillStyle = `hsla(     $  {star.hue},     $  {star.saturation}%,     $  {star.lightness}%,     $  {star.alpha}   )`;   contextRef.current.beginPath();   contextRef.current.arc(     star.x,     star.y,     (star.size / 2) * star.scale,     0,     Math.PI * 2   );   contextRef.current.fill(); });

And here’s the fun bit of code that moves all the stars around:

partyRef.current = gsap.timeline().to(starsRef.current, {   scale: 1,   alpha: defaultAlpha });  const STAGGER = 0.01;  for (let s = 0; s < starsRef.current.length; s++) {   partyRef.current     .to(     starsRef.current[s],     {       onStart: () => {         gsap.set(starsRef.current[s], {           hue: gsap.utils.random(0, 360),           saturation: 80,           lightness: 60,           alpha: 1,         })       },       onComplete: () => {         gsap.set(starsRef.current[s], {           saturation: 0,           lightness: 100,           alpha: defaultAlpha,         })       },       x: gsap.utils.random(0, window.innerWidth),       y: gsap.utils.random(0, window.innerHeight),       duration: 0.3     },     s * STAGGER   ); }

From there, we generate a new timeline and tween the values of each star. These new values get picked up by RENDER. We’re adding a stagger by positioning each tween in the timeline using GSAP’s position parameter.

That’s it!

That’s one way to make an interactive starry backdrop for your site. We combined GSAP and an HTML <canvas>, and even sprinkled in some React that makes it more configurable and reusable. We even dropped an Easter egg in there!

Where can you take this component from here? How might you use it on a site? The combination of GreenSock and <canvas> is a lot of fun and I’m looking forward to seeing what you make! Here are a couple more ideas to get your creative juices flowing…


An Interactive Starry Backdrop for Content originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , ,
[Top]

Bartosz Ciechanowski’s Interactive Blog Posts

I saw Bartosz Ciechanowski’s “Curves and Surfaces” going around the other day and was like, oh hey, this is the same fella that did that other amazingly interactive blog post on the Internal Combustion Engine the other day. I feel like I pretty much get how engines work now because of that blog post. Then I thought I should see what other blog posts Bartosz has and, lo and behold, there are a dozen or so — and they are all super good. Like one on gears, color spaces, Earth and Sun, and mesh transforms.

If I was a person who hired people to design interactive science museums, I’d totally try to hire Bartosz to design one. I’m glad, though, that the web is the output of choice so far as the reach of the web is untouchable.

I wonder what the significance of the Patreon membership level numbers are? 3, 7, 19, 37, 71. Just random prime numbers? I threw in a few bucks. I’d increase my pledge if some of the bucks could go toward an improved accessibility situation. I think the sliders are largely unfocusable <div> situations so I imagine something better could be done there.

CSS-Tricks

, , , ,
[Top]

Interactive Rebase: Clean up your Commit History

This article is part of our “Advanced Git” series. Be sure to follow Tower on Twitter or sign up for their newsletter to hear about the next articles.

Interactive Rebase is the Swiss Army knife of Git commands: lots of use cases and lots of possibilities! It’s really a great addition to any developer’s tool chain, because it lets you revise your local commit history—before you share your work with the rest of the team.

Let’s see what you can do with an interactive rebase and then look at some practical examples.

Advanced Git series:

  1. Part 1: Creating the Perfect Commit in Git
  2. Part 2: Branching Strategies in Git
  3. Part 3: Better Collaboration With Pull Requests
  4. Part 4: Merge Conflicts
  5. Part 5: Rebase vs. Merge
  6. Part 6: Interactive Rebase (You are here!)
  7. Part 7: Cherry-Picking Commits in Git (Coming soon!)
  8. Part 8: Using the Reflog to Restore Lost Commits

Rewriting your commit history

In short, interactive rebase allows you to manipulate your commit history. It’s meant for optimizing and cleaning up. You can…

  • change commit messages
  • combine multiple commits
  • split and edit existing commits
  • reorder commits
  • delete commits

Keep in mind that an interactive rebase rewrites your commit history: all of the involved commits get a new hash ID. Also, a quick reminder: commit IDs are there to identify commits—they are SHA-1 checksums. So, by changing that hash, you technically create completely new commits. This means that you shouldn’t use an interactive rebase on stuff that you’ve already pushed to a shared remote repository. Your colleagues might have based their work on these commits—and when you use interactive rebase to rewrite commit history, you are changing these base commits.

All of this means that an interactive rebase is meant to help you clean up and optimize your own local commit history before you merge (and possibly push) it back into a shared team branch.

Interactive rebase workflow

Before we take interactive rebase for a test drive, let’s look at the general workflow. This is always the same, no matter what exactly we’re doing—deleting a commit, changing a commit message, combining commits… the steps are identical.

The first step is to determine the range of commits you want to manipulate. How far back in time do you want to go? Once you have the answer, you can start your interactive rebase session. Here, you have the chance to edit your commit history. For example, you can manipulate the selected commits by reordering, deleting, combining them, and so on.

In your first step, you are always going to look at the current state of the commit history. You can use the git log command to examine a project’s history and show the commit log.

Here’s the little example repository we’re going to use throughout this article:

Showing a Git Tower app screen with the main master branch of a repo selected on the left panel, and a list of existing commits on the right panel.
Note that I’m using the Tower Git desktop GUI in some of my screenshots for easier visualization.

After you’ve examined the list, it’s time to start the work. Let’s do this step-by-step. In the examples of this article, we will do the following things:

  • First, we change an old commit’s message.
  • Secondly, we combine two old commits.
  • After that, we split one commit.
  • Finally, we delete a commit.

Change a commit message

In many cases, you’ll want to change the most recent commit. Keep in mind that there’s a shortcut for this scenario which doesn’t involve interactive rebase:

$  git commit --amend

This command can modify both the content and the message of the most recent commit, and it opens your default text editor. Here you can make your changes, save them, and quit the editor. This will not only update the commit message, but will effectively change the commit itself and write a new one.

Again, please be careful and don’t amend your last commit if you’ve already pushed it to the remote repository!

For any other commit (anything older than the most recent one), you have to perform an interactive rebase. To run git rebase interactively, add the -i option. 

The first step is to determine the base commit: the parent commit of the one you want to change. You can achieve this by using the commit’s hash ID or by doing a little bit of counting. To change the last three commit messages (or at least one of them), you can define the parent commit like this:

$  git rebase -i HEAD~3

An editor window opens and you can see all three commits you selected (and by “selected” I mean a range of commits: from HEAD all the way down to HEAD~3). Please notice the reverse order: unlike git log, this editor shows the oldest commit (HEAD~3) at the top and the newest at the bottom.

Animated screenshot showing an open terminal that edits the commit and adds a reword command on another line.

In this window you don’t actually change the commit message. You only tell Git what kind of manipulation you want to perform. Git offers a series of keywords for this—in our case, we change the word pick to reword which allows us to change the commit messages. After saving and closing the editor, Git will show the actual commit message and you can change it. Save and exit again, that’s it!

Combining two commits

In this next example, we’ll combine the two commits—“7b2317cf Change the page structure” and “6bcf266 Optimize markup”—so that they become one single commit. Again, as a first step you need to determine the base commit. And again, we have to go back to at least the parent commit:

$  git rebase -i HEAD~3

The editor window opens again, but instead of reword, we’ll enter squash. To be exact, we replace pick with squash in line 2 to combine it with line 1. This is an important bit to keep in mind: the squash keyword combines the line you mark up with the line above it!

Showing an open terminal with a squash on line 2 and giants words in red pointing to that line saying that squash combines the marked up line with the line above it.

After saving the changes and closing the window, a new editor window pops up. Why’s that? By combining two commits we are creating… well… a new commit! And this new commit wants a commit message. Enter the message, save and close the window… and you’ve successfully combined the two commits. Powerful stuff!

Finally a little “pro tip” for those of you working with the “Tower” Git desktop GUI: to perform a squash, you can simply drag and drop commits onto each other, right in the commits view. And if you want to change a commit message, simply right click the commit in question and select “Edit commit message” from the contextual menu.

Animated screenshot showing the process of dragging one commit message on top of another, squashing the message, then editing the commit message.

Deleting a commit

We’re bringing in the big guns for our final example: we are going to delete a revision from our commit history! To do this, we’re using the drop keyword to mark up the commit we want to get rid of:

drop 0023cdd Add simple robots.txt pick 2b504be Change headlines for about and imprint pick 6bcf266 Optimizes markup structure in index page

This is probably a good moment to answer a question you might have had for some time now: what can you do if you’re in the middle of a rebase operation and think, “Oh, no, this wasn’t such a good idea after all”? No problem—you can always abort! Just enter the following command to turn back to the state your repository was in before you initiated the rebase:

$  git rebase --abort

Changing the past

These were just a few examples of what an interactive rebase can do. There are plenty of other possibilities to control and revise your local commit history.

If you want to dive deeper into advanced Git tools, feel free to check out my (free!) “Advanced Git Kit”: it’s a collection of short videos about topics like branching strategies, Interactive Rebase, Reflog, Submodules and much more.

Happy rebasing and hacking—and see you soon for the next part in our series on “Advanced Git”!

Advanced Git series:

  1. Part 1: Creating the Perfect Commit in Git
  2. Part 2: Branching Strategies in Git
  3. Part 3: Better Collaboration With Pull Requests
  4. Part 4: Merge Conflicts
  5. Part 5: Rebase vs. Merge
  6. Part 6: Interactive Rebase (You are here!)
  7. Part 7: Cherry-Picking Commits in Git (Coming soon!)
  8. Part 8: Using the Reflog to Restore Lost Commits

The post Interactive Rebase: Clean up your Commit History appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Interactive Web Components Are Easier Than You Think

In my last article, we saw that web components aren’t as scary as they seem. We looked at a super simple setup and made a zombie dating service profile, complete with a custom <zombie-profile> element. We reused the element for each profile and populated each one with unique info using the <slot> element.

Here’s how it all came together.

That was cool and a lot of fun (well, I had fun anyway…), but what if we take this idea one step further by making it interactive. Our zombie profiles are great, but for this to be a useful, post-apocalyptic dating experience you’d want to, you know, “Like” a zombie or even message them. That’s what we’re going to do in this article. We’ll leave swiping for another article. (Would swiping left be the appropriate thing for zombies?)

This article assumes a base level of knowledge about web components. If you’re new to the concept, that’s totally fine — the previous article should give you everything you need. Go ahead. Read it. I’ll wait. *Twiddles thumbs* Ready? Okay.

First, an update to the original version

Let’s pause for one second (okay, maybe longer) and look at the ::slotted() pseudo element. It was brought to my attention after the last article went out (thanks, Rose!) and it solves some (though not all) of the encapsulation issues I encountered. If you recall, we had some CSS styles outside of the component’s <template> and some inside a <style> element within the <template>. The styles inside the <template> were encapsulated but the ones outside were not.

But that’s where ::slotted comes into play. We declare an element in the selector like so:

::slotted(img) {   width: 100%;   max-width: 300px;   height: auto;   margin: 0 1em 0 0; }

Now, any <img> element placed in any slot will be selected. This helps a lot!

But this doesn’t solve all of our encapsulation woes. While we can select anything directly in a slot, we cannot select any descendant of the element in the slot. So, if we have a slot with children — like the interests section of the zombie profiles — we’re unable to select them from the <style> element. Also, while ::slotted has great browser support, some things (like selecting a pseudo element, e.g., ::slotted(span)::after) will work in some browsers (hello, Chrome), but won’t work in others (hello, Safari).

So, while it’s not perfect, ::slotted does indeed provide more encapsulation than what we had before. Here’s the dating service updated to reflect that:

Back to interactive web components!

First thing I’d like to do is add a little animation to spice things up. Let’s have our zombie profile pics fade in and translate up on load.

When I first attempted this, I used img and ::slotted(img) selectors to directly animate the image. But all I got was Safari support. Chrome and Firefox would not run the animation on the slotted image, but the default image animated just fine. To get it working, I wrapped the slot in a div with a .pic class and applied the animation to the div instead.

.pic {   animation: picfadein 1s 1s ease-in forwards;   transform: translateY(20px);   opacity: 0; }  @keyframes picfadein {   from { opacity: 0; transform: translateY(20px); }   to { opacity: 1; transform: translateY(0); } }

“Liking” zombies

Wouldn’t it be something to “Like” that cute zombie? I mean from the user’s perspective, of course. That seems like something an online dating service ought to have at the very least.

We’ll add a checkbox “button” that initiates a heart animation on click. Let’s add this HTML at the top of the .info div:

<input type="checkbox" id="trigger"><label class="likebtn" for="trigger">Like</label>

Here’s a heart SVG I pulled together. We know that Zombies love things to be terrible, so their heart will be an eye searing shade of chartreuse:

<svg viewBox="0 0 160 135" class="heart" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M61 12V0H25v12H12v13H0v36h12v13h13v12h12v12h12v12h12v13h13v12h12v-12h13v-13h11V98h13V86h-1 13V74h12V61h12V25h-12V12h-12V0H98v12H85v13H74V12H61z" fill="#7aff00"/></svg>

Here’s the important bits of the CSS that are added to the template’s <style> element:

#trigger:checked + .likebtn {   /* Checked state of the .likebtn. Flips the foreground/background color of the unchecked state. */   background-color: #960B0B;   color: #fff; }  #trigger {   /* With the label attached to the input with the for attribute, clicking the label checks/unchecks the box, so we can remove the checkbox. */   display: none; }  .heart {   /* Start the heart off so small it's nigh invisible */   transform: scale(0.0001); }  @keyframes heartanim {   /* Heart animation */   0% { transform: scale(0.0001); }   50% { transform: scale(1); }   85%, 100% { transform: scale(0.4); } }  #trigger:checked ~ .heart {   /* Checking the checkbox initiates the animation */   animation: 1s heartanim ease-in-out forwards; }

Pretty much standard HTML and CSS there. Nothing fancy or firmly web-component-ish. But, hey, it works! And since it’s technically a checkbox, it’s just as easy to “unlike” a zombie as it is to “Like” one.

Messaging zombies

If you’re a post-apocalyptic single who’s ready to mingle, and see a zombie whose personality and interests match yours, you might want to message them. (And, remember, zombies aren’t concerned about looks — they’re only interested in your braaains.)

Let’s reveal a message button after a zombie is “Liked.” The fact that the Like button is a checkbox comes in handy once again, because we can use its checked state to conditionally reveal the message option with CSS. Here’s the HTML added just below the heart SVG. It can pretty much go anywhere as long as it’s a sibling of and comes after the #trigger element.

<button type="button" class="messagebtn">Message</button>

Once the #trigger checkbox is checked, we can bring the messaging button into view:

#trigger:checked ~ .messagebtn {   display: block; }

We’ve done a good job avoiding complexity so far, but we’re going to need to reach for a little JavaScript in here. If we click the message button, we’d expect to be able to message that zombie, right? While we could add that HTML to our <template>, for demonstration purposes, lets use some JavaScript to build it on the fly.

My first (naive) assumption was that we could just add a <script> element to the template, create an encapsulated script, and be on our merry way. Yeah, that doesn’t work. Any variables instantiated in the template get instantiated multiple times and well, JavaScript’s cranky about variables that are indistinguishable from each other. *Shakes fist at cranky JavaScript*

You probably would have done something smarter and said, “Hey, we’re already making a JavaScript constructor for this element, so why wouldn’t you put the JavaScript in there?” Well, I was right about you being smarter than me.

Let’s do just that and add JavaScript to the constructor. We’ll add a listener that, once clicked, creates and displays a form to send a message. Here’s what the constructor looks like now, smarty pants:

customElements.define('zombie-profile', class extends HTMLElement {   constructor() {     super();     let profile = document.getElementById('zprofiletemplate');     let myprofile = profile.content;     const shadowRoot = this.attachShadow({       mode: 'open'     }).appendChild(myprofile.cloneNode(true));      // The "new" code     // Grabbing the message button and the div wrapping the profile for later use     let msgbtn = this.shadowRoot.querySelector('.messagebtn'),         profileEl = this.shadowRoot.querySelector('.profile-wrapper');          // Adding the event listener     msgbtn.addEventListener('click', function (e) {        // Creating all the elements we'll need to build our form       let formEl = document.createElement('form'),           subjectEl = document.createElement('input'),           subjectlabel = document.createElement('label'),           contentEl = document.createElement('textarea'),           contentlabel = document.createElement('label'),           submitEl = document.createElement('input'),           closebtn = document.createElement('button');                // Setting up the form element. The action just goes to a page I built that spits what you submitted back at you       formEl.setAttribute('method', 'post');       formEl.setAttribute('action', 'https://johnrhea.com/undead-form-practice.php');       formEl.classList.add('hello');        // Setting up a close button so we can close the message if we get shy       closebtn.innerHTML = "x";       closebtn.addEventListener('click', function () {         formEl.remove();       });        // Setting up form fields and labels       subjectEl.setAttribute('type', 'text');       subjectEl.setAttribute('name', 'subj');       subjectlabel.setAttribute('for', 'subj');       subjectlabel.innerHTML = "Subject:";       contentEl.setAttribute('name', 'cntnt');       contentlabel.setAttribute('for', 'cntnt');       contentlabel.innerHTML = "Message:";       submitEl.setAttribute('type', 'submit');       submitEl.setAttribute('value', 'Send Message');        // Putting all the elments in the Form       formEl.appendChild(closebtn);       formEl.appendChild(subjectlabel);       formEl.appendChild(subjectEl);       formEl.appendChild(contentlabel);       formEl.appendChild(contentEl);       formEl.appendChild(submitEl);        // Putting the form on the page       profileEl.appendChild(formEl);     });   } });

So far, so good!

Before we call it a day, there’s one last thing we need to address. There’s nothing worse than that first awkward introduction, so lets grease those post-apocalyptic dating wheels by adding the zombie’s name to the default message text. That’s a nice little convenience for the user.

Since we know that the first span in the <zombie-profile> element is always the zombie’s name, we can grab it and stick its content in a variable. (If your implementation is different and the elements’s order jumps around, you may want to use a class to ensure you always get the right one.)

let zname = this.getElementsByTagName("span")[0].innerHTML;

And then add this inside the event listener:

contentEl.innerHTML = "Hi " + zname + ",\nI like your braaains...";

That wasn’t so bad, was it? Now we know that interactive web components are just as un-scary as the zombie dating scene… well you know what I mean. Once you get over the initial hurdle of understanding the structure of a web component, it starts to make a lot more sense. Now that you’re armed with interactive web component skills, let’s see what you can come up with! What other sorts of components or interactions would make our zombie dating service even better? Make it and share it in the comments.


The post Interactive Web Components Are Easier Than You Think appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

An Interactive Guide to CSS Transitions

A wonderful post by Josh that both introduces CSS transitions and covers the nuances for using them effectively. I like the advice about transitioning the position of an element, leaving the original space it occupied alone so it doesn’t result in what he calls “doom flicker.” Six hundred and fifty years ago I created CSS Jitter Man to attempt to explain that idea.

The interactive stuff is really neat and helps explain the concepts. I’m a little jealous that Josh writes in MDX — meaning he’s got Markdown and JSX at his disposal. That means these demos can be little one-off React components. Here’s a thread that Josh did showing off how valuable that can be.

Direct Link to ArticlePermalink


The post An Interactive Guide to CSS Transitions appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Embedding an Interactive Analytics Component with Cumul.io and Any Web Framework

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

  1. Events work on a chart level and will include information within the limits of the information on the chart
  2. To add an event, go to the chart settings on the chart you want to add them to
  3. Define name and label of event. And you’re done!
  4. (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’.

Useful info for following steps

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:

  1. Integrate the dashboard with your app
  2. Listen to incoming events

Integrate the dashboard with your app

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:

export const openPageCumulioFavorites = async () => {   ui.openPage('Cumul.io playlist visualized', 'cumulio-playlist-viz');   /**************** INTEGRATE DASHBOARD ****************/   loadDashboard(dashboards.playlist); };

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:

const listenToEvents = () => {   Cumulio.onCustomEvent((event) => {     if (event.data.event === 'add_to_playlist'){       //DO SOMETHING     }     else if (event.data.event === 'song_info'){       //DO SOMETHING     }   }); };

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"}

And these columns for the table:

"columns":[  {"id":"Weapon Of Choice (feat. Bootsy Collins) - Remastered Version","value":"Weapon Of Choice (feat. Bootsy Collins) - Remastered Version","label":"Name"},  {"id":"Fatboy Slim","value":"Fatboy Slim","label":"Artist"},    // ...  {"id":"3qs3aHNUcqFGv7jMYJJCYa","value":"3qs3aHNUcqFGv7jMYJJCYa","label":"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:

const loadDashboard = (id, container, key, token) => {   dashboardOptions.dashboardId = id;   dashboardOptions.container = container || '#dashboard-container';      if (key && token) {     dashboardOptions.key = key;     dashboardOptions.token = token;   }    Cumulio.addDashboard(dashboardOptions); };

We make some modifications to the loadDashboard() function so as to use the new token:

const loadDashboard = (id, container, key, token) =u003e {n  dashboardOptions.dashboardId = id;n  dashboardOptions.container = container || '#dashboard-container';  nn  if (key u0026u0026 token) {n    dashboardOptions.key = key;n    dashboardOptions.token = token;n  }nn  Cumulio.addDashboard(dashboardOptions);n};

Then call the ui.displaySongInfo(). The final result looks as follows:

const listenToEvents = () => {   Cumulio.onCustomEvent(async (event) => {     const song = getSong(event);     if (event.data.event === 'add_to_playlist'){       await ui.addToPlaylistSelector(song.name, song.id);     }     else if (event.data.event === 'song_info'){       const token = await getDashboardAuthorizationToken({ songId: [song.id] });       loadDashboard(dashboards.songInfo, '#song-info-dashboard', token.id, token.token);       await ui.displaySongInfo(song);     }   }); };

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 🙂

Steps to run this project

Before you start:

  1. Clone the cumulio-spotify-datatalks repository with npm install
  2. Create a .env file in the root directory and add the following from your Cumul.io and Spotify Developer accounts:
  3. From Cumul.io: CUMULIO_API_KEY=xxx CUMULIO_API_TOKEN=xxx
  4. From Spotify: SPOTIFY_CLIENT_ID=xxx SPOTIFY_CLIENT_SECRET=xxx ACCESS_TOKEN=xxx REFRESH_TOKEN=xxxnpm run start
  5. On your browser, go to http://localhost:3000/ and log into your Spotify account 🥳


The post Embedding an Interactive Analytics Component with Cumul.io and Any Web Framework appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Proper Tabbing to Interactive Elements in Firefox on macOS

I just had to debug an issue with focusable elements in Firefox. Someone reported to me that when tabbing to a certain element within a CodePen embed, it shot the scroll position to the top of the page (WTF?!). So, I went to go debug the problem by tabbing through an example page in Firefox, and this is what I saw:

I didn’t even know what to make of that. It was like some elements you could tab to but not others? You can tab to <button>s but not <a>s? Uhhhhh, that doesn’t seem right that you can’t tab to links in Firefox?

After searching and asking around, it turns out it’s this preference at the OS level on macOS.

System Preferences > Keyboard > Shortcuts > User keyboard navigation to move focus between controls

If you have to turn that on, you also have to restart Firefox. Once you have, then you can tab to things you’d expect to be able to tab to, like links.

About that bug with the scrolling to the top of the page. See that “Skip Results Iframe” link that shows up when tabbing through the CodePen Embed? It only shows up when :focus-ed (as the point of it is to skip over the <iframe> rather than being forced to tab through it). I “hid” it by doing a position: absolute; top: -9999px; left: -9999px thing (old muscle memory), then removing those values when in focus. For some reason, when tabbed to, Firefox would see those values and instantly jump the page up, even though the focus style moved it back into a normal place. Must have been some kind of race condition thing.

I also found it very silly that Firefox would do that to the parent page when that link was inside an iframe. I fixed it up using a more vetted accessible hiding technique.


The post Proper Tabbing to Interactive Elements in Firefox on macOS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Let Mavo Shine in Building Interactive Web Applications

As you could guess from the title, this tutorial is dedicated to Mavo: a new, approachable way to create complex, reactive, persistent web applications just by writing HTML and CSS, without a single line of JavaScript and no server backend.

The app we will build together is a flashcard study app for foreign language learners. It is a fully-fledged CRUD application that lets you:

  • Create, delete, update flashcards and re-arrange them via drag and drop.
  • Import and export flashcards.
  • Evaluate how well you’ve learned the word on a flashcard.

Here is what our finished application looks like:

In the tutorial, I will guide you through the entire process of building the app.

At the end of some steps, I provide suggestions for you to experiment with Mavo—to learn a bit more—and to make some enhancements to the application we are building.

Are you ready? Let’s get started! 😀

Static template

See the Pen
01. Static template
by Dmitry Sharabin (@dsharabin)
on CodePen.

In order to illustrate how Mavo enhances standard HTML, we will first create a purely static HTML page and then use Mavo to turn this static HTML into a fully-functional web application.

Assume we have the following HTML code inside <body>:

<header>   <h1>Flashcards</h1> </header>  <main> <article>   <p>Word or phrase</div>   <p>Translation</div> </article> </main>

In that code, the <article> element represents a single flashcard.

Let’s add some styling in order to make our HTML look more like an actual flashcards app. You can take a look at the source code and play with it here.

Getting started with Mavo

Right now, all we have is a static template. It’s time to add functionality, so it can actually work like a flashcard application. Here comes Mavo!

In order to use Mavo, we first need to include its JavaScript and CSS files in our page’s <head> section:

<head>   ...   <script src="https://get.mavo.io/mavo.min.js"></script>   <link rel="stylesheet" href="https://get.mavo.io/mavo.css">   ... </head>

Defining a Mavo app

To enable Mavo functionality on an HTML structure, we must use the mv-app attribute on the element that contains our Mavo app (which may even be the <body> or <html> element, that’s fine!). Its value is an ID for our app that should be unique in the page.

Considering that the <main> element is representing our Mavo app, let’s add the mv-app attribute to it and give our app the ID “flashcards”:

<main mv-app="flashcards">   ... </main>

The property attribute

See the Pen
02. The property attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

It’s time to tell Mavo what elements of our app are important, i.e., which elements we want to be editable and be saved.

Now we have two such elements, and they are the <p> elements. Let’s add the property attribute to those elements to tell Mavo they contain data. Elements with the property attribute are called properties.

Keep in mind that the value of the property attribute should describe the element, similarly to an id or class attribute:

...    <p property="source">Word or phrase</div>   <p property="translation">Translation</div>  ...

Notice any changes in our app? The Mavo toolbar with an Edit button appeared at the top of the page. The Edit button lets the user switch between read and edit modes. Now our app is in read mode. That means we can’t edit the data on the page.

Now lets us switch to edit mode by clicking the Edit button. What has changed? The text of the Edit button becomes Editing to indicate that we are in edit mode. If you hover over the paragraphs, Mavo communicates that you can click to edit them by highlighting them in yellow. Go ahead! Click the text and edit it. Wow! We can change the content of the page right in place!

💻 Let’s get our hands dirty!

Assume that in addition to the word and its translation, the flashcard should have an example of the word’s usage in a sentence. Enhance the app by adding the appropriate elements to the flashcard.

The mv-multiple attribute

See the Pen
03. The mv-multiple attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

At this point, we have only one flashcard in our app. That’s not very useful! For a working flashcard application, we need the ability to add, delete, and rearrange flashcards. How can we do that? We could create more flashcards by adding more <article> elements to our code, but then how does an end user create and delete flashcards?

Fortunately, Mavo has something to offer that makes this a breeze: the mv-multiple attribute, which tells Mavo that certain elements can be multiplied. It converts the element it’s used on to an editable collection of items and generates (customizable) UI for adding, deleting, and rearranging items.

Let’s use the mv-multiple attribute in our app to convert our lonely flashcard into a collection of flashcards:

<article property="flashcard" mv-multiple>   ...     </article>

Now switch the app to edit mode. Note that below the flashcard, there is now an Add flashcard button. Let’s give it a try: create a few flashcards with the help of that button. Now we can dynamically add new elements right in the app, even though there are no corresponding elements in the HTML. But that is not all!

Try hovering over a flashcard and notice the three buttons that appear near its top right corner for adding, deleting and rearranging elements via a drag and drop handle. And by hovering over any item bar button, we can understand which flashcard they correspond: Mavo highlights it. Isn’t that amazing?

The mv-storage attribute

See the Pen
04. The mv-storage attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

Now that we have the basic UI in place, let’s try the following:

  • Switch to edit mode (if you haven’t already done so).
  • Edit the first flashcard’s source word and translation. Add a couple more flashcards too.
  • Switch the app back to read mode.
  • And finally… refresh the page.

What?! Where did our data go? Wasn’t Mavo supposed to save it? What happened?

Actually, we never told Mavo if or where to store our data!

To do so, we need to use the mv-storage attribute. What options do we have? Well, Mavo opens great possibilities for us, and Mavo plugins open up even more!

In our application, we are going to store the data in the browser’s localStorage, which is one of the simplest options available, so it’s good for our first Mavo app. We just need to add the attribute mv-storage with the value local to the element with the mv-app attribute (also called the Mavo root).

<main mv-app="flashcards" mv-storage="local">   ... </main>

Have a look at the Mavo toolbar. Notice something? Another button appeared—the Save button.

Try to edit the app data one more time. Note that the Save button is now highlighted. Hover over the Save button, and Mavo will highlight the properties with the unsaved data. Isn’t that cool?

Click the Save button and refresh the page (there is no need to switch to read mode before refreshing the page). Is your data still there? Great! We are one step closer to our goal—a fully-fledged flashcard application.

The mv-autosave attribute

Now we have to click the Save button every time we need our data to be saved? That may be safer, to prevent destroying valuable data, but it can often be inconvenient. Can we just save the data automatically? Sure! To save the data automatically every time it is changed, we can use the mv-autosave attribute on our Mavo root. Its value is the number of seconds to throttle saving by. Let’s add mv-autosave="3" to the root element of our app:

<main mv-app="flashcard" mv-storage="local" mv-autosave="3">   ... </main>

Change the data one more time and have a look at the Save button. See? In the beginning, it was highlighted but after 3 seconds–it is not. All our data is now saved automatically!

So, now the main part of our app would look like that:

<main mv-app="flashcards" mv-storage="local" mv-autosave="3">   <article property="flashcard" mv-multiple>     <p property="source">Word or phrase</div>     <p property="translation">Translation</div>   </article> </main>
💻 Let’s get our hands dirty!

We are almost done with the alpha version of our app. Now it’s your turn to make the app even better. No worries, you have all the knowledge you need.

Enhance the app so as flashcards could be organized by end users in different groups related to various topics, e.g., the users could gather all the flashcards corresponding to clothing in one group, all the flashcards associated with kitchen utensils in the other one, etc.

💡 Hints!

There are many ways to achieve that goal, and it’s up to you to decide what to follow. However, I’d like you to think about some questions before proceeding:

  1. What HTML element would you use as a grouping element? It would be convenient for the users if they could see the name of the group of flashcards (topic name) and could collapse the group up to the title.
  2. What Mavo attribute(s) are you going to add to that element, if any? Will the element be a property or a collection?
  3. Will end users be able to add new topics, delete and rearrange them, change the title of a topic and move flashcards between different topics?

What if you decide not to organize flashcards in groups, but instead just label them with tags that correspond to various topics? Well, that is perfectly fine. The solution with tags is also appropriate. For the sake of practice, try to accomplish that approach too.

The mv-bar attribute

See the Pen
05. The mv-bar attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

As our app stores the data locally, by default, the users of the app won’t be able to share their cards with other users. Wouldn’t it be great if we would let them export their flashcards and import somebody else’s flashcards? Thankfully, these features are already implemented in Mavo, and we can very easily add them to our app!

The mv-bar attribute controls which buttons are going to appear in the toolbar, if any. It’s typically specified on the Mavo root (an element with the mv-app attribute). Buttons are represented by their ids (which are very logical): edit, import, export, etc.

As we only want to add a few buttons to the default set, we can use the so-called relative syntax, which allows us to add and remove buttons from the default set without having to explicitly list everything out. All we need is to start the mv-bar attribute’s value with the with keyword.

By doing that, we would get the following:

<main mv-app="flashcards"       mv-storage="local"       mv-autosave="3"       mv-bar="with import export">       ... </main>
💻 Let’s get our hands dirty!

Give those features a try: add some flashcards, try to export them in a file. Then delete existing flashcards and import the flashcards from the previously exported file.

Expressions and MavoScript

See the Pen
06. Expressions and MavoScript
by Dmitry Sharabin (@dsharabin)
on CodePen.

Let’s now add some statistics to our app, such as the number of flashcards! Sounds interesting? I hoped so. 😀

To do that, we need to learn something new about Mavo.

We can dynamically refer to the value of any property we have defined, anywhere in our Mavo app (including in HTML attributes), by putting its name inside brackets, like this: [propertyName]. This is an example of a simple expression, which allows us to dynamically calculate things, reactively as they change.

Now let’s experiment and add a [source] expression inside the flashcard property, e.g., between two properties: the source and the translation.

...   <p property="source">Word or phrase</div>   [source]   <p property="translation">Translation</div> ...

What has changed in our app? The value of the flashcard source property is now shown on the page twice.

Switch to edit mode and try to change the value of the source property. Can you see that? The page content updates while you are changing the property value! That’s why I said earlier that Mavo lets us develop reactive web applications.

That’s indeed cool, but unfortunately, in our case, it’s not really helpful: we can’t use this expression to count the number of flashcards—we would always have only one value.

What if we put the [source] expression outside the flashcard property? We will have something like that:

...   [source]   <article property="flashcard" mv-multiple>     ...   </article> ...

How does this differ from the previous case? To see the difference add some flashcards if you haven’t done so yet. Now instead of one value we have a list of comma separated values: the source property of all flashcards. That’s exactly we were looking for: the number of items in the list corresponds the number of flashcards in the app.

Makes sense? Well, yes, but it wouldn’t it be more logical if we would count the number of flashcards, not the number of values of its source property? After all, a flashcard added exists even before we fill in its source or translation. I suggest you do the following: let’s substitute the [source] expression with [flashcard]:

...   [flashcard]   <article property="flashcard" mv-multiple>     ...   </article> ...

Noticed the difference? We still have a list, but its values are not simple values but objects, i.e., complex values containing all data that pertains to each flashcard. The good news is that the number of these objects is equal to the number of flashcards, since there is one for each flashcard, even when it’s completely empty. So, right now we have an object for each flashcard, but how do we count them and display the total count?

Now let’s get familiar with the MavoScript functions and find the one that would let us count the number of flashcards. Remember, we have a list of flashcards, so we need to find a function that would let us count the number of items in a list. And here it is—the count() function does exactly that!

But how can we use functions in expressions? Are there any rules we need to be aware of? Well, yes, there is a couple:

  1. Expressions are denoted by brackets.
  2. Do not nest brackets.

Let’s try using the count() function to count the number of flashcards:

... [count(flashcard)] <article property="flashcard" mv-multiple>   ... </article> ...

And that’s exactly what we were aiming for—now we have some statistics in our app! Isn’t that cool?

💻 Let’s get our hands dirty!

I hope you’ve already warmed up and ready to continue experimenting with Mavo.

Improve the application so that the statistics are displayed not only for the total number of flashcards in the app but also for the number of flashcards in each topic separately if there are any topics.

💡Hint!
Want to filter a list based on some criteria? The where operator will help.

The self-evaluation feature

We already have an application that lets us create, edit and store multiple flashcards. But how do we keep track of which ones we have already learned and which ones we need to practice more? Any respectable flashcards application needs a self-evaluation feature. Let’s investigate how we can add that!

Suppose in our app we have two buttons for the self-evaluation: the Bad and the Good. What exactly do we want to happen every time an end user clicks the buttons? Well, the idea is rather simple:

  • Clicking the Bad button would indicate the user hasn’t learned the word yet and we want our app to move the corresponding flashcard to the beginning of the list so they could see it as soon as possible after launching the app.
  • Clicking the Good button would indicate the user has learned the word and the corresponding flashcard needs to move to the end of the list to let them work with other flashcards which they haven’t learned yet.

“Are you sure we can do that without JavaScript?” you may ask. Yep! Mavo is extremely powerful and is able to equip us with all the tools we need!

Now when we know what we are going to implement, let’s set the UI in place first and then move on to the next step. Our markup would look something like this:

... <article property="flashcard" mv-multiple>   ...   <section>     <h2>Evaluate Yourself</h2>     <button>Bad</button>     <button>Good</button>   </section> </article> ...

The mv-action attribute

See the Pen
07. The mv-action attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

Mavo actions allow us to create our very own controls that modify data in custom ways when the user interacts with them. Sounds promising right?

To define a custom action we need to use the mv-action attribute on an appropriate element inside our Mavo app. The action is performed every time the element is clicked. That’s exactly what we were looking for.

The value of the mv-action attribute is an expression. We can use any of the expression functions and syntax that MavoScript provides to us, as well as a few more to facilitate data manipulation, such as add(), set(), move(), or delete(). It is important to note that unlike normal expressions which are evaluated reactively, these expressions are only evaluated each time the action is triggered.

So, we need to move flashcards inside the collection, and Mavo has an appropriate function that lets us do that—the move() function. Its first argument refers to the item we are moving, and the second is its position in the collection. Bear in mind that elements of the collection are numbered starting from 0.

Let’s implement the first point of the outline we discussed earlier: while self-evaluating, an end user clicks the Bad button and the corresponding flashcard moves to the beginning of the collection, i.e., it becomes the first one. So in the code, we have:

... <article property="flashcard" mv-multiple>   ...   <button mv-action="move(flashcard, 0)">Bad</button>   ... </article> ...

Pay attention that in the mv-action attribute we refer to the flashcard property inside the property itself, since we want to deal with the current flashcard.

If we try to implement the second point of the outline, we will face a problem. Can you suggest what problem exactly will it be?

Let’s remember that if an end user clicks the Good button the corresponding flashcard moves to the end of the collection, i.e., it becomes the last one. To make a flashcard last in the collection we need to know the number of items in it.

Thankfully, a bit earlier we’ve already solved that task and implemented the corresponding feature. But could we use that solution to solve our current problem? Unfortunately, we can’t: as we already know, we can refer to the collection of flashcards by its name (and evaluate its size) only outside the flashcard property. But in our case, we need to do that inside it: the Good button for which we need to write an expression is inside the flashcard property.

What should we do then? I’m glad you ask. Mavo has the solution.

Using the meta element to hold intermediate values

See the Pen
08. The <meta> element
by Dmitry Sharabin (@dsharabin)
on CodePen.

So, on the one hand, we know that the [count(flashcards)] expression gives us the number of flashcards if it is evaluated outside the flashcard property. On the other hand, we need to use that value inside the flashcard property.

To solve that dilemma, we need to evaluate the number of flashcards outside the flashcard property and somehow hold the result to be able to use it elsewhere in the app, precisely inside the flashcard property. For cases like that, in Mavo, there are so-called computed properties.

To hold an intermediate result so we can refer to it, we need an HTML element in our code. It is recommended to use the <meta> element for that purpose, like so: <meta property="propertyName" content="[expression]">. The advantage of using this element is that it is hidden outside edit mode, both semantically and visually.

Now let’s add the flashcardCount computed property in our app. Remember, we must place it outside the flashcard property, but then we can refer to it from anywhere:

... <meta property="flashcardCount" content="[count(flashcard)]"> <article property="flashcard" mv-multiple>     ... </article> ...

Only one step left to finish the implementation of the self-evaluation feature: if an end user clicks the Good button the corresponding flashcard moves to the end of the collection, i.e., it becomes the last one. Let’s add the relevant action in the code of our app:

... <meta property="flashcardCount" content="[count(flashcard)]"> <article property="flashcard" mv-multiple>   ...   <button mv-action="move(flashcard, flashcardCount)">Good</button> </article> ...

We are done! Congratulations! 😀

💻 Let’s get our hands dirty!

There is another way to solve that task: with the help of the $ all special property. The $ all property represents a collection itself if it is placed inside the collection. So there is no need to use any computed property in this case. Try to implement that solution on your own.

There is only one more tiny thing left that we need to fix. Remember the part where we added some stats to our app? Remember the expression we built to evaluate the number of flashcards in the app: [count(flashcard)]? Instead, we can (and should) now use the computed property we defined. Make the appropriate changes in the app.

Takeaways

So what have we learned so far? Let’s recap. In order to turn any static HTML page into a Mavo app we need to:

  1. Include the Mavo JavaScript and CSS files in the page’s <head> section.
  2. Add the mv-app attribute to the Mavo root element.
  3. Tell Mavo what elements of our app are important by adding the property attribute to them.
  4. Place the mv-multiple attribute on the element that will be multiplied and converted into a collection.
  5. Tell Mavo where to store our data by adding mv-storage attribute to the Mavo root.
  6. Decide whether Mavo should save our data automatically or not. If yes, add the mv-autosave attribute to the Mavo root.

    We also know that:

  7. The Mavo toolbar is fully-customizable. The mv-bar attribute controls which buttons are going to appear there.
  8. Expressions let us present the current value of properties in other elements and perform computations. An expression value (and type) vary depending on the place the expression takes in the code. Mavo’s expression syntax is called MavoScript.
  9. Custom actions allow creating controls that modify data in custom ways. To define a custom action set the mv-action attribute on an appropriate element inside a Mavo app
  10. Properties whose values are expressions are called computed properties. To hold an intermediate result to be able to refer to it elsewhere in the app, it is recommended to use the <meta> element.

Instead of an epilogue

So we built our app. Is it already perfect? Of course not, nothing is! There are so many things that can be improved, and so many features that can be added (with the help of Mavo, we can even make our app multilingual!). Go ahead, enhance it more, don’t hesitate to try something new!

What we’ve learned so far about Mavo is just the tip of the iceberg, and there is so much more. I encourage you to give it a closer look, by reading the documentation, by examining examples (on the Mavo site, or on CodePen: made by Lea Verou and a few made by myself), and by creating new stuff! Good luck! 😉

Acknowledgments

I want to thank two great people. First of all, my huge thanks go to Lea Verou, who not only inspired me to write this tutorial (and helped me make it happen) but also inspires me all the time by the way she makes the world of web development a better place. I’ve never met such a gifted human being, and I am happy having an opportunity to make some stuff with her!

I also thank James Moore. The examples he uses in his course “Functional Programming for Beginners with JavaScript” on Udemy pushed me to make my very own version of a flashcard study app. He is a wonderful teacher!

The post Let Mavo Shine in Building Interactive Web Applications appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Creating Interactive Maps in WordPress with MapSVG

The MapSVG plugin for WordPress allows you to create feature-rich, interactive maps quickly and through a smart admin UI. Interactive maps are a common request for projects when you need to visualize impact over particular locations. If you are already on WordPress, this comprehensive plugin could be your solution for production-ready maps.

Think about the last time you implemented or scoped an interactive map. You may wind up estimating a significant amount of time for mapping your data from its original source into JSON to be hooked to a visualization framework, like D3. Then comes configuring how users interact with the map itself. These are already significant development lifts.

Next comes the question of how you will manage your map data. Did it live in an external file? Require regular API calls to fetch data? It would be a lot easier to manage this data directly within your site’s WordPress admin as you would all other site content. No need to open up a code editor and maintain the map separately!

These and many other common map functionalities come baked into the MapSVG plugin for you to create multiple unique maps to fit your visualization needs. We’re going to dig into the plugin a little bit and use it to build an interactive SVG-based map in WordPress

Options for creating new maps

Once installed, MapSVG gives you five options for creating a new map based on vector, image, or Google Maps features. For the sake of this article, I will be focusing on the SVG features.

A screenshot of the MapSVG settings with options for the type of map to create. Options include Google Map, image map, upload SVG, download SVG with Google map, and Google API.
Choose between SVG, flat images and Google Maps as the base for each new map.

Within that New SVG map option are nearly 200 geo-calibrated and un-calibrated vector maps of countries and regions. The free version of this plugin, MapSVG Lite, lets site administrators create interactive maps with tooltips, customizable detail views, and markers placed by latitude/longitude coordinates. Upgrading to the paid version ($ 46) provides support for: map directory, integrated search, location filters, a detailed view of a map region, integrations with WordPress posts and Advanced Custom Fields, custom tooltips, and choropleth maps, among other premium features.

Creating a new map

Right, so we’ve chosen to create a new SVG-based map. Select one of the options from the dropdown to create a vector map. Whenever possible, choose geo-calibrated maps over ones that are un-calibrated. Geo-calibrated maps are newer and have region titles. Plus, they allow you to add map markers by geo-coordinates (i.e. latitude and longitude), or by entering an address that automatically converts to coordinates.

If using your own custom SVG file, select the Upload SVG option. Once uploaded, your custom file will be available in the New SVG Map dropdown under user-uploads/your-file-name.svg. There are a few additional steps I recommend to optimize your file prior to uploading it to the plugin’s map:

  1. Regions that contain multiple vector paths (e.g. Hawaii), need to be grouped as a compound path in Illustrator. In Illustrator, this can be done by selecting the relevant paths, then going to Object > Compound Path > Make (or CMD + 8).
  2. A screenshot of a black United States country map in Adobe Illustrator with the map on the left side and the layers panel on the right showing all of the image paths.
    Optimized custom SVG map of U.S. + British Columbia

  3. Ensure your layers have clear, unique names. The layer name will be used as the region {{id}} for templating and organizational purposes.
  4. Next, open your SVG file in a text editor and give each path a title attribute. This will be used as the region {Creating Interactive Maps in WordPress with MapSVG} in the templates.

The id and title fields are default attributes pulled from the file to associate them with the individual paths. As of the latest release (currently 5.3.7) of the plugin, you can edit the SVG file directly in the WordPress admin to set the id and title values of each path, along with adjusting the path definition and drawing new paths. I personally prefer the control of editing in Illustrator and a code editor, but this is a nice option to have available to you.

Let’s proceed by creating a demo using the geo-calibrated maps provided by the plugin and using entries from https://confs.tech to visualize (some of) this year’s tech conference data from around the world.

Styling the map

Now that we’ve got our SVG file all set up and selected for use, the plugin interface opens to the settings panel. This is where we set the map name, define sizing dimensions, and enable tooltips, among other primary settings.

Alongside the settings tab, you will see tabs for controlling and setting specific map features. Switching over to the next tab, Colors, is where we set the map theme.

A screenshot of the MapSVG screen for color settings. A preview of the map is displayed on the left side and option fields on the right for background, borders, regions, and hover effects.

As you can see above, we can control the fill and stroke values of the map and the various active states of each path. A benefit of using a vector based map! For that reason, I prefer to leave the fill and stroke values undefined in Illustrator if I’m creating a custom map file. Stroke width, however, cannot be efficiently adjusted within the UI (because it would requires editing the SVG and changing the value for every single path), so it is best to set it in Illustrator and re-upload the file.

Further down are color options for the containers (e.g. directory, filters, sidebars, etc.) as well as the minimum and max colors values for a choropleth map (more on this later!). Container styles can also be set globally either in your theme’s stylesheet or in the setting’s CSS tab.

Setting up map data

By default, the Regions tab will be pre-populated by regions based on the paths of the selected SVG, displaying their id and title values. You can edit and create your own region’s fields as needed. The same applies for the Database tab — where you enter your map data and associate it with the regions of the map.

For both region and database entries, you have the option of manually creating the fields and content in the UI or upload the data from a CSV file.

A screenshot of the MapSVG regions settings. It includes fields for label and name as well as custom fields for status, link and conference count.
This conference_count custom field will be used for the choropleth feature to create a thematic map of countries that have technical conferences this year, from least to most.
A screenshot of the MapSVG regions data showing the use of custom region fields.
Populating the Conference Count region’s custom field.
A screenshot of the populated database entries and the overlay displaying the data in the detailed view template.
The populated database entries and the overlay displaying the data in the detailed view template.

Notice the final dataset in the screenshot above, specifically the custom Regions fields for conference_details, conference details, and a field to upload images that display the flag of each country in the database. We can take this a step further by separating the content from the map itself and pulling it in as post data of any WordPress post type. That’s where Advanced Custom Fields can help to create fields for that data in the post editor and creating a relationship between the submitted data to populate the values in the map. Not required, of course, but it’s nice to have this option for the sake of separation of concerns.

Displaying map data

When showing information about a region or database object on a map, we have to set up a template in the plugin. There are five types of templates: popovers, tooltips, detail view, directory, and labels.

Templates accept plain text, HTML, and Handlebar markup to display region and database variable values. If pulling in post data via the Post database field, you will have access to the standard WordPress fields, post.id, post.post_title, post.post_content, post.url, along with any custom fields created with Advanced Custom Fields with this syntax: post.acf.my_field_name. Whenever you need to render a rich text field such as with
post.post_content, you have to use Handlebars’ {{{triple-stash}}} syntax to output the rendered HTML.

A screenshot of a Details View template showing the conditional output and default helper comments
Details View template showing Handlebars conditional content and default helper comments.

The use of Handlebar syntax also means we can build conditional logic into the templates to create dynamic template views based on region and database values. In MapSVG 5.0 and up, the template options come pre-populated with HTML comments and starter markup for the default region and database fields. Super helpful!

Adding a map to a page

MapSVG includes a shortcode used to embed a map on any page or post. Drop a Gutenberg shortcode block on the page and call MapSVG with the ID of the map to embed: [mapsvg id="418"].

A screenshot of the shortcode added to a shortcode block in the Gutenberg editor. It contains map <abbr>SVG</abbr> equals 418 inside of square brackets.” /><figcaption>Embedding a map using the shortcode block in the Gutenberg editor.</figcaption></figure>
<p>For those working in the classic WordPress editor or with a plugin version that predates 5.0, a map icon will be added to the TinyMCE toolbar, which will inject the shortcode to your post content that takes the map <abbr>ID</abbr>. The map <abbr>ID</abbr> can be found in the MapSVG editor dashboard or in the top breadcrumbs of the active map. A handy <strong>Copy to clipboard</strong> button is included next to both instances shortcode to grab it for use in the post editor.</p>
<h3>Demo</h3>
<p>With data entry complete and a few toggles of additional MapSVG settings, we have a fully functional interactive <em>and responsive</em> map! And notice just how little code we had to touch along the way.</p>
<figure><iframe width=

View Full Demo

Going beyond maps

The great thing about the SVG feature of the plugin is that, at it’s core, SVG can be whatever we want. You can create an interactive “map” from the vector paths of just about anything: a building floor plan, an infographic, or maybe an interactive timeline of your career? It’s more than just maps!

Not on WordPress? No problem. MapSVG also comes as a jQuery plugin that’s equally worth trying for interactive maps outside of WordPress.

Resources

The post Creating Interactive Maps in WordPress with MapSVG appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]