Tag: Easier

Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think

We’ve discussed a lot about the internals of using CSS in this ongoing series on web components, but there are a few special pseudo-elements and pseudo-classes that, like good friends, willingly smell your possibly halitotic breath before you go talk to that potential love interest. You know, they help you out when you need it most. And, like a good friend will hand you a breath mint, these pseudo-elements and pseudo-classes provide you with some solutions both from within the web component and from outside the web component — the website where the web component lives.

I’m specifically referring to the ::part and ::slotted pseudo-elements, and the :defined, :host, and :host-context pseudo-classes. They give us extra ways to interact with web components. Let’s examine them closer.

Article series

The ::part pseudo-element

::part, in short, allows you to pierce the shadow tree, which is just my Lord-of-the-Rings-y way to say it lets you style elements inside the shadow DOM from outside the shadow DOM. In theory, you should encapsulate all of your styles for the shadow DOM within the shadow DOM, i.e. within a <style> element in your <template> element.

So, given something like this from the very first part of this series, where you have an <h2> in your <template>, your styles for that <h2> should all be in the <style> element.

<template id="zprofiletemplate">   <style>     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     /* other styles */   </style>   <div class="profile-wrapper">     <div class="info">       <h2>         <slot name="zombie-name">Zombie Bob</slot>       </h2>       <!-- other zombie profile info -->     </div> </template>

But sometimes we might need to style an element in the shadow DOM based on information that exists on the page. For instance, let’s say we have a page for each zombie in the undying love system with matches. We could add a class to profiles based on how close of a match they are. We could then, for instance, highlight a match’s name if he/she/it is a good match. The closeness of a match would vary based on whose list of potential matches is being shown and we won’t know that information until we’re on that page, so we can’t bake the functionality into the web component. Since the <h2> is in the shadow DOM, though, we can’t access or style it from outside the shadow DOM meaning a selector of zombie-profile h2 on the matches page won’t work.

But, if we make a slight adjustment to the <template> markup by adding a part attribute to the <h2>:

<template id="zprofiletemplate">   <style>     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     /* other styles */   </style>   <div class="profile-wrapper">     <div class="info">       <h2 part="zname">         <slot name="zombie-name">Zombie Bob</slot>       </h2>       <!-- other zombie profile info -->     </div> </template>

Like a spray of Bianca in the mouth, we now have the superpowers to break through the shadow DOM barrier and style those elements from outside of the <template>:

/* External stylesheet */ .high-match::part(zname) {   color: blue; } .medium-match::part(zname) {   color: navy; } .low-match::part(zname) {   color: slategray; }

There are lots of things to consider when it comes to using CSS ::part. For example, styling an element inside of a part is a no-go:

/* frowny-face emoji */ .high-match::part(zname) span { ... }

But you can add a part attribute on that element and style it via its own part name.

What happens if we have a web component inside another web component, though? Will ::part still work? If the web component appears in the page’s markup, i.e. you’re slotting it in, ::part works just fine from the main page’s CSS.

<zombie-profile class="high-match">   <img slot="profile-image" src="https://assets.codepen.io/1804713/leroy.png" />   <span slot="zombie-name">Leroy</span>   <zombie-details slot="zdetails">     <!-- Leroy's details -->   </zombie-details> </zombie-profile>

But if the web component is in the template/shadow DOM, then ::part cannot pierce both shadow trees, just the first one. We need to bring the ::part into the light… so to speak. We can do that with an exportparts attribute.

To demonstrate this we’ll add a “watermark” behind the profiles using a web component. (Why? Believe it or not this was the least contrived example I could come up with.) Here are our templates: (1) the template for <zombie-watermark>, and (2) the same template for <zombie-profile> but with added a <zombie-watermark> element on the end.

<template id="zwatermarktemplate">   <style>     div {     text-transform: uppercase;       font-size: 2.1em;       color: rgb(0 0 0 / 0.1);       line-height: 0.75;       letter-spacing: -5px;     }     span {       color: rgb( 255 0 0 / 0.15);     }   </style>   <div part="watermark">     U n d y i n g  L o v e  U n d y i n g  L o v e  U n d y i n g  L o v e  <span part="copyright">©2 0 2 7 U n d y i n g  L o v e  U n L t d .</span>   <!-- Repeat this a bunch of times so we can cover the background of the profile -->   </div>  </template> <template id="zprofiletemplate">   <style>     ::part(watermark) {       color: rgb( 0 0 255 / 0.1);     }     /* More styles */   </style>   <!-- zombie-profile markup -->   <zombie-watermark exportparts="copyright"></zombie-watermark> </template> <style>   /* External styles */   ::part(copyright) {     color: rgb( 0 100 0 / 0.125);   } </style>

Since ::part(watermark) is only one shadow DOM above the <zombie-watermark>, it works fine from within the <zombie-profile>’s template styles. Also, since we’ve used exportparts="copyright" on the <zombie-watermark>, the copyright part has been pushed up into the <zombie-profile>‘s shadow DOM and ::part(copyright) now works even in external styles, but ::part(watermark) will not work outside the <zombie-profile>’s template.

We can also forward and rename parts with that attribute:

<zombie-watermark exportparts="copyright: cpyear"></zombie-watermark>
/* Within zombie-profile's shadow DOM */  /* happy-face emoji */ ::part(cpyear) { ... }  /* frowny-face emoji */ ::part(copyright) { ... }

Structural pseudo-classes (:nth-child, etc.) don’t work on parts either, but you can use pseudo-classes like :hover. Let’s animate the high match names a little and make them shake as they’re lookin’ for some lovin’. Okay, I heard that and agree it’s awkward. Let’s… uh… make them more, shall we say, noticeable, with a little movement.

.high::part(name):hover {   animation: highmatch 1s ease-in-out; }

The ::slotted pseudo-element

The ::slotted CSS pseudo-element actually came up when we covered interactive web components. The basic idea is that ::slotted represents any content in a slot in a web component, i.e. the element that has the slot attribute on it. But, where ::part pierces through the shadow DOM to make a web component’s elements accessible to outside styles, ::slotted remains encapsulated in the <style> element in the component’s <template> and accesses the element that’s technically outside the shadow DOM.

In our <zombie-profile> component, for example, each profile image is inserted into the element through the slot="profile-image".

<zombie-profile>   <img slot="profile-image" src="photo.jpg" />    <!-- rest of the content --> </zombie-profile>

That means we can access that image — as well as any image in any other slot — like this:

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

Similarly, we could select all slots with ::slotted(*) regardless of what element it is. Just beware that ::slotted has to select an element — text nodes are immune to ::slotted zombie styles. And children of the element in the slot are inaccessible.

The :defined pseudo-class

:defined matches all defined elements (I know, surprising, right?), both built-in and custom. If your custom element is shuffling along like a zombie avoiding his girlfriend’s dad’s questions about his “living” situation, you may not want the corpses of the content to show while you’re waiting for them to come back to life errr… load.

You can use the :defined pseudo-class to hide a web component before it’s available — or “defined” — like this:

:not(:defined) {   display: none; }

You can see how :defined acts as a sort of mint in the mouth of our component styles, preventing any broken content from showing (or bad breath from leaking) while the page is still loading. Once the element’s defined, it’ll automatically appear because it’s now, you know, defined and not not defined.

I added a setTimeout of five seconds to the web component in the following demo. That way, you can see that <zombie-profile> elements are not shown while they are undefined. The <h1> and the <div> that holds the <zombie-profile> components are still there. It’s just the <zombie-profile> web component that gets display: none since they are not yet defined.

The :host pseudo-class

Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.

It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host.

In previous examples throughout this series, I set the <zombie-profile> width from the main page’s CSS, like this:

zombie-profile {   width: calc(50% - 1em); }

With :host, however, I can set that width from inside the web component, like this:

:host {   width: calc(50% - 1em); }

In fact, there was a div with a class of .profile-wrapper in my examples that I can now remove because I can use the shadow root as my wrapper with :host. That’s a nice way to slim down the markup.

You can do descendant selectors from the :host, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted).

Showing the parts of the HTML that are relevant to the :host pseudo-element.

That said, :host isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.

:host(.high) {   border: 2px solid blue; }

This allows you to make changes should certain classes be added to the custom element.

You can also pass pseudo-classes in there, like :host(:last-child) and :host(:hover).

The :host-context pseudo-class

Now let’s talk about :host-context. It’s like our friend :host(), but on steroids. While :host gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.

:host-context, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context is unsupported in Firefox or Safari. So use it for progressive enhancement.

Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch class. The second div will hold all the medium and low love matches with a .worstmatch class.

<div class="profiles bestmatch">   <zombie-profile class="high">     <!-- etc. -->   </zombie-profile>   <!-- more profiles --> </div>  <div class="profiles worstmatch">   <zombie-profile class="medium">     <!-- etc. -->   </zombie-profile>   <zombie-profile class="low">     <!-- etc. -->   </zombie-profile>   <!-- more profiles --> </div>

Let’s say we want to apply different background colors to the .bestmatch and .worstmatch classes. We are unable to do this with just :host:

:host(.bestmatch) {   background-color: #eef; } :host(.worstmatch) {   background-color: #ddd; }

That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context pokes past the custom element to match the, er, match classes we want to style.

:host-context(.bestmatch) {   background-color: #eef; } :host-context(.worstmatch) {   background-color: #ddd; }

Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)

How would you use ::part, ::slotted, :defined, :host, and :host-context in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)


Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,

Context-Aware Web Components Are Easier Than You Think

Another aspect of web components that we haven’t talked about yet is that a JavaScript function is called whenever a web component is added or removed from a page. These lifecycle callbacks can be used for many things, including making an element aware of its context.

Article series

The four lifecycle callbacks of web components

There are four lifecycle callbacks that can be used with web components:

  • connectedCallback: This callback fires when the custom element is attached to the element.
  • disconnectedCallback: This callback fires when the element is removed from the document.
  • adoptedCallback: This callback fires when the element is added to a new document.
  • attributeChangedCallback: This callback fires when an attribute is changed, added or removed, as long as that attribute is being observed.

Let’s look at each of these in action.

Our post-apocalyptic person component

Two renderings of the web component side-by-side, the left is a human, and the right is a zombie.

We’ll start by creating a web component called <postapocalyptic-person>. Every person after the apocalypse is either a human or a zombie and we’ll know which one based on a class — either .human or .zombie — that’s applied to the parent element of the <postapocalyptic-person> component. We won’t do anything fancy with it (yet), but we’ll add a shadowRoot we can use to attach a corresponding image based on that classification.

customElements.define(   "postapocalyptic-person",   class extends HTMLElement {     constructor() {       super();       const shadowRoot = this.attachShadow({ mode: "open" });     } }

Our HTML looks like this:

<div class="humans">   <postapocalyptic-person></postapocalyptic-person> </div> <div class="zombies">   <postapocalyptic-person></postapocalyptic-person> </div>

Inserting people with connectedCallback

When a <postapocalyptic-person> is loaded on the page, the connectedCallback() function is called.

connectedCallback() {   let image = document.createElement("img");   if (this.parentNode.classList.contains("humans")) {     image.src = "https://assets.codepen.io/1804713/lady.png";     this.shadowRoot.appendChild(image);   } else if (this.parentNode.classList.contains("zombies")) {     image.src = "https://assets.codepen.io/1804713/ladyz.png";     this.shadowRoot.appendChild(image);   } }

This makes sure that an image of a human is output when the <postapocalyptic-person> is a human, and a zombie image when the component is a zombie.

Be careful working with connectedCallback. It runs more often than you might realize, firing any time the element is moved and could (baffling-ly) even run after the node is no longer connected — which can be an expensive performance cost. You can use this.isConnected to know whether the element is connected or not.

Counting people with connectedCallback() when they are added

Let’s get a little more complex by adding a couple of buttons to the mix. One will add a <postapocalyptic-person>, using a “coin flip” approach to decide whether it’s a human or a zombie. The other button will do the opposite, removing a <postapocalyptic-person> at random. We’ll keep track of how many humans and zombies are in view while we’re at it.

<div class="btns">   <button id="addbtn">Add Person</button>   <button id="rmvbtn">Remove Person</button>    <span class="counts">     Humans: <span id="human-count">0</span>      Zombies: <span id="zombie-count">0</span>   </span> </div>

Here’s what our buttons will do:

let zombienest = document.querySelector(".zombies"),   humancamp = document.querySelector(".humans");  document.getElementById("addbtn").addEventListener("click", function () {   // Flips a "coin" and adds either a zombie or a human   if (Math.random() > 0.5) {     zombienest.appendChild(document.createElement("postapocalyptic-person"));   } else {     humancamp.appendChild(document.createElement("postapocalyptic-person"));   } }); document.getElementById("rmvbtn").addEventListener("click", function () {   // Flips a "coin" and removes either a zombie or a human   // A console message is logged if no more are available to remove.   if (Math.random() > 0.5) {     if (zombienest.lastElementChild) {       zombienest.lastElementChild.remove();     } else {       console.log("No more zombies to remove");     }   } else {     if (humancamp.lastElementChild) {       humancamp.lastElementChild.remove();     } else {       console.log("No more humans to remove");     }   } });

Here’s the code in connectedCallback() that counts the humans and zombies as they are added:

connectedCallback() {   let image = document.createElement("img");   if (this.parentNode.classList.contains("humans")) {     image.src = "https://assets.codepen.io/1804713/lady.png";     this.shadowRoot.appendChild(image);     // Get the existing human count.     let humancount = document.getElementById("human-count");     // Increment it     humancount.innerHTML = parseInt(humancount.textContent) + 1;   } else if (this.parentNode.classList.contains("zombies")) {     image.src = "https://assets.codepen.io/1804713/ladyz.png";     this.shadowRoot.appendChild(image);     // Get the existing zombie count.     let zombiecount = document.getElementById("zombie-count");     // Increment it     zombiecount.innerHTML = parseInt(zombiecount.textContent) + 1;   } }

Updating counts with disconnectedCallback

Next, we can use disconnectedCallback() to decrement the number as a humans and zombies are removed. However, we are unable to check the class of the parent element because the parent element with the corresponding class is already gone by the time disconnectedCallback is called. We could set an attribute on the element, or add a property to the object, but since the image’s src attribute is already determined by its parent element, we can use that as a proxy for knowing whether the web component being removed is a human or zombie.

disconnectedCallback() {   let image = this.shadowRoot.querySelector('img');   // Test for the human image   if (image.src == "https://assets.codepen.io/1804713/lady.png") {     let humancount = document.getElementById("human-count");     humancount.innerHTML = parseInt(humancount.textContent) - 1; // Decrement count   // Test for the zombie image   } else if (image.src == "https://assets.codepen.io/1804713/ladyz.png") {     let zombiecount = document.getElementById("zombie-count");     zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1; // Decrement count   } }

Beware of clowns!

Now (and I’m speaking from experience here, of course) the only thing scarier than a horde of zombies bearing down on your position is a clown — all it takes is one! So, even though we’re already dealing with frightening post-apocalyptic zombies, let’s add the possibility of a clown entering the scene for even more horror. In fact, we’ll do it in such a way that there’s a possibility any human or zombie on the screen is secretly a clown in disguise!

I take back what I said earlier: a single zombie clown is scarier than even a group of “normal” clowns. Let’s say that if any sort of clown is found — be it human or zombie — we separate them from the human and zombie populations by sending them to a whole different document — an <iframe> jail, if you will. (I hear that “clowning” may be even more contagious than zombie contagion.)

And when we move a suspected clown from the current document to an <iframe>, it doesn’t destroy and recreate the original node; rather it adopts and connects said node, first calling adoptedCallback then connectedCallback.

We don’t need anything in the <iframe> document except a body with a .clowns class. As long as this document is in the iframe of the main document — not viewed separately — we don’t even need the <postapocalyptic-person> instantiation code. We’ll include one space for humans, another space for zombies, and yes, the clowns’s jail… errr… <iframe> of… fun.

<div class="btns">   <button id="addbtn">Add Person</button>   <button id="jailbtn">Jail Potential Clown</button> </div> <div class="humans">   <postapocalyptic-person></postapocalyptic-person> </div> <div class="zombies">   <postapocalyptic-person></postapocalyptic-person> </div> <iframe class="clowniframeoffun” src="adoptedCallback-iframe.html"> </iframe>

Our “Add Person” button works the same as it did in the last example: it flips a digital coin to randomly insert either a human or a zombie. When we hit the “Jail Potential Clown” button another coin is flipped and takes either a zombie or a human, handing them over to <iframe> jail.

document.getElementById("jailbtn").addEventListener("click", function () {   if (Math.random() > 0.5) {     let human = humancamp.querySelector('postapocalyptic-person');     if (human) {       clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(human));     } else {       console.log("No more potential clowns at the human camp");     }   } else {     let zombie = zombienest.querySelector('postapocalyptic-person');     if (zombie) {       clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(zombie));     } else {       console.log("No more potential clowns at the zombie nest");     }   } });

Revealing clowns with adoptedCallback

In the adoptedCallback we’ll determine whether the clown is of the zombie human variety based on their corresponding image and then change the image accordingly. connectedCallback will be called after that, but we don’t have anything it needs to do, and what it does won’t interfere with our changes. So we can leave it as is.

adoptedCallback() {   let image = this.shadowRoot.querySelector("img");   if (this.parentNode.dataset.type == "clowns") {     if (image.src.indexOf("lady.png") != -1) {        // Sometimes, the full URL path including the domain is saved in `image.src`.       // Using `indexOf` allows us to skip the unnecessary bits.        image.src = "ladyc.png";       this.shadowRoot.appendChild(image);     } else if (image.src.indexOf("ladyz.png") != -1) {       image.src = "ladyzc.png";       this.shadowRoot.appendChild(image);     }   } }

Detecting hidden clowns with attributeChangedCallback

Finally, we have the attributeChangedCallback. Unlike the the other three lifecycle callbacks, we need to observe the attributes of our web component in order for the the callback to fire. We can do this by adding an observedAttributes() function to the custom element’s class and have that function return an array of attribute names.

static get observedAttributes() {   return [“attribute-name”]; }

Then, if that attribute changes — including being added or removed — the attributeChangedCallback fires.

Now, the thing you have to worry about with clowns is that some of the humans you know and love (or the ones that you knew and loved before they turned into zombies) could secretly be clowns in disguise. I’ve set up a clown detector that looks at a group of humans and zombies and, when you click the “Reveal Clowns” button, the detector will (through completely scientific and totally trustworthy means that are not based on random numbers choosing an index) apply data-clown="true" to the component. And when this attribute is applied, attributeChangedCallback fires and updates the component’s image to uncover their clownish colors.

I should also note that the attributeChangedCallback takes three parameters:

  • the name of the attribute
  • the previous value of the attribute
  • the new value of the attribute

Further, the callback lets you make changes based on how much the attribute has changed, or based on the transition between two states.

Here’s our attributeChangedCallback code:

attributeChangedCallback(name, oldValue, newValue) {   let image = this.shadowRoot.querySelector("img");   // Ensures that `data-clown` was the attribute that changed,   // that its value is true, and that it had an image in its `shadowRoot`   if (name="data-clown" && this.dataset.clown && image) {     // Setting and updating the counts of humans, zombies,     // and clowns on the page     let clowncount = document.getElementById("clown-count"),     humancount = document.getElementById("human-count"),     zombiecount = document.getElementById("zombie-count");     if (image.src.indexOf("lady.png") != -1) {       image.src = "https://assets.codepen.io/1804713/ladyc.png";       this.shadowRoot.appendChild(image);       // Update counts       clowncount.innerHTML = parseInt(clowncount.textContent) + 1;       humancount.innerHTML = parseInt(humancount.textContent) - 1;     } else if (image.src.indexOf("ladyz.png") != -1) {       image.src = "https://assets.codepen.io/1804713/ladyzc.png";       this.shadowRoot.appendChild(image);       // Update counts       clowncount.innerHTML = parseInt(clowncount.textContent) + 1;       zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1;     }   } }

And there you have it! Not only have we found out that web component callbacks and creating context-aware custom elements are easier than you may have thought, but detecting post-apocalyptic clowns, though terrifying, is also easier that you thought. What kind of devious, post-apocalyptic clowns can you detect with these web component callback functions?


Context-Aware Web Components Are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,
[Top]

Build Great Apps: Designer and Developer Collaboration Just Got Easier

(This is a sponsored post.)

We’re going to go over details on how designer ↔️ developer collaboration in AWS Amplify Studio can make app building easier, but let’s cover one of those things right at the top here.

Say your designer (or you!) like to work in Figma, the predominant tool in UI design these days. The whole design system is there, from colors and fonts, to atomic components like buttons and forms, to larger layout components like Cards and Collections. The designs in Figma evolve, and when they do, it’s a developer’s job to port the changes over to the site. What if… that didn’t need to be manual work? With AWS Amplify you can now suck over those updated Figma designs directly without any work other than reviewing and accepting the changes. That’s some pretty magical stuff that people have only theorized would be possible one day with design tooling. Now it’s here.

We will get more into that in a minute, I just wanted to make sure you caught that upfront.

Setting the Stage

Designers often struggle to work with developers and vice versa, even though both are usually passionate about delivering great app experiences. If you’re a designer, if you’ve ever had to hand off your visual wonder to a developer, then you know the dread that sometimes follows. Will your developer know how to implement your design fully? Will pixels get removed? Will I be happy with the final iteration?

If you’re a developer, you may think of design implementation and iteration as a tedious and manual process. You may not feel equipped to think about design, even though you do care about delighting your customers and end-users and growing your business.

There are a lot of tools that are intended to help facilitate the handoff process. However, most don’t fully address the implementation of the design.

The AWS Amplify team recently launched AWS Amplify Studio, a new visual development environment that lets front-end developers build full-stack apps with minimal coding while integrating Amplify’s powerful backend configuration and management capabilities. Amplify Studio helps automate the design implementation process, helping designers and developers to work better together, and ensuring that designs are implemented into the final product. Let’s dive in and check out the new capabilities.

Let’s build an app

In the previous post, we outlined Amplify Studio’s functionality. Now let’s walk through how to build and deploy a web app quickly, on AWS. Amplify Studio contains backend creation and management capabilities, simplifying setup and management of app backend infrastructure, such as database tables, user authentication, and file storage, without requiring cloud expertise.

Use the visual development environment to define a data model, user authentication, and file storage without backend expertise. Amplify Studio helps you do the heavy lifting. Let’s focus on the UI and data binding.

This whole demo was done by Ali Spittel, a Senior Developer Advocate for AWS Amplify.

For demonstration’s sake, imagine it’s a home rental app.

First, let’s create a data model so that we can link data to my UI components. Then create a component with an image of the rental, a name, price, and location. 

Then, we can save and deploy.

Next, let’s go over to Figma. Amplify Studio helps developers and UX designers work better together. In addition to the significant time it takes for developers to implement designs and iterations, often, the UX designs aren’t implemented accurately, which frustrates designers and leads to a sub-optimal end-user experience. With Amplify Studio, developers can import custom UI components created by their designers from Figma, or they can use the UI component library provided by Amplify Studio, and customize them to fit their style guide. 

The Amplify team launched a Figma file called the AWS Amplify UI Kit. 

The styling correlates with the Amplify UI library, which includes primitives, components (which are combinations of the primitives), cards, and collections.

Duplicate the file as a starter then edit away!  

You can customize the components however you wish. If you’re working with a UX designer, both of you can work with the Figma project and import the UX designs into Amplify Studio.

Amplify Studio’s Figma-to-code plugin lets me import UX designs directly from Figma, which are automatically translated into clean React code that can be further customized.

To import the UX components, paste the link to the Figma file and paste it into Amplify Studio.

All of my components from my Figma file will be imported into Amplify Studio. You’re able to review the imported components in Figma and decide whether to accept or reject them, as an added quality check.  If you are importing a design iteration, this helps you to see what the design changes are before you implement them.

This process helps me save time from not having to write thousands of lines of code, since Amplify Studio autogenerates it for me. It creates human readable, credible React code. Later, if needed, you can extend this code for additional control over the components.

We can then link the UI components to data from the data model, and can add child properties like rental price, name, and location. 

We can create a collection of components, and customize the layout. Say we want to display them as a list or a grid or change the direction that it goes in, adjust spacing, and so on. We can choose the data that is displayed on my page. 

Now, let’s need to integrate this into the app. Studio gives me code snippets to use to get my components into my app. 

Run the command amplify pull, which runs some code generation from all the Figma components we pulled into Amplify Studio. Then we can use these React components like you would any other in the code base.

We’ll have an Amplify directory with information about the API, and also have a models directory with data that is created on the front end. We also have a full UI components directory, which has all of the UI components that we created in Figma and imported into Amplify Studio. 

Say we’ve installed the Amplify JavaScript libraries and React UI components. We’ll use a couple of lines of code to configure Amplify for this app. We can use the Amplify Provider component which will be at the top level. It will allow us to have the right styling for my UI components. 

In the component below, we import the CSS file, then import the rental collection. Then we can see my React code for the app. This is the code that was generated by Amplify. 

Here are the properties that we can change in the Amplify UI documentation. 

We can also add overrides to items in the generated components. 

A lot of companies have their own style guides, and that’s great! We can use Amplify Studio to details of these style guides to Figma and ultimately our entire app.

There’s a new Figma plugin you can install: AWS Amplify Theme Editor. If you want to add a custom theme to the UI, you can use the plugin for that. This happens via CSS custom properties, with JavaScript objects, or design tokens within Figma using the Amplify UI Theme Editor plugin. You can use it to change the color palette, component colors, and brand colors. 

One of the great things about Amplify Studio is it works with the tools that designers and developers use, but helps to centralize and streamline their workflow while facilitating tighter collaboration. 

Designers can be assured that their UX designs and iterations are being fully implemented. Developers can be assured that they are incorporating design implementations as a part of their CI/CD process. As developers, we can save time not needing to translate designers’ ideas and changes into code, and we can focus on building a better app experience and the end users’ needs while making sure we haven’t missed the design details. 

Once this app is ready to deploy, we can host the app with Amplify Hosting. It offers hosting for any React, Vue, or Next.js web app with built-in continuous integration and continuous deployment (CI/CD) workflows, testing, pull request previews, and custom domains. 

Amplify Studio’s frontend UI capabilities are in preview (its app backend capabilities are generally available). You can go to the sandbox to try it out.

Calls to action

CSS-Tricks

, , , , , , ,
[Top]

AWS Lambdas: Easy, Easier, Easiest

I’d say cloud functions are one of the most transformative technologies in the last bunch of years. They are (usually) cheap, scale well, secure in their inherit isolation, and often written in JavaScript—comfortable territory for front-end developers. Nearly every cloud provider offers them, but AWS Lambda was the OG and remains the leader.

But also: The DX around cloud functions is just as interesting to watch as the tech behind the functions themselves. There is all sorts of tech that has sprung up around them to make them easy to use and relatively transparent. Emrah Samdan wrote that it’s a win-win for both customers and companies.

Two of the most popular Jamstack hosting platforms, Netlify and Vercel, offer idiot-proof wrappers for AWS Lambda deployments, each more developer-friendly than the next.

Joey Anuff, “AWS Lambdas: Easy, Easier, Easiest”

AWS’ own Amplify is a front-runner for easiness as well, which is in stark contrast to trying to manage your functions right through the AWS console itself.

Joey found Vercel to be easiest by a narrow margin, with the caveat that he was already using Next.js which is from Vercel.

My favorite bit here is that in the research repo for this article, Joey listed in great detail (with action GIFs) the steps for each of the services cloud functions offerings.

Direct Link to ArticlePermalink


The post AWS Lambdas: Easy, Easier, Easiest appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Supercharging Built-In Elements With Web Components “is” Easier Than You Think

We’ve already discussed how creating web components is easier than you think, but there’s another aspect of the specification that we haven’t discussed yet and it’s a way to customize (nay, supercharge) a built-in element. It’s similar to creating fully custom or “autonomous” elements — like the <zombie-profile> element from the previous articles—but requires a few differences.

Customized built-in elements use an is attribute to tell the browser that this built-in element is no mild-mannered, glasses-wearing element from Kansas, but is, in fact, the faster than a speeding bullet, ready to save the world, element from planet Web Component. (No offense intended, Kansans. You’re super too.)

Supercharging a mild-mannered element not only gives us the benefits of the element’s formatting, syntax, and built-in features, but we also get an element that search engines and screen readers already know how to interpret. The screen reader has to guess what’s going on in a <my-func> element, but has some idea of what’s happening in a <nav is="my-func"> element. (If you have func, please, for the love of all that is good, don’t put it in an element. Think of the children.)

It’s important to note here that Safari (and a handful of more niche browsers) only support autonomous elements and not these customized built-in elements. We’ll discuss polyfills for that later.

Until we get the hang of this, let’s start by rewriting the <apocalyptic-warning> element we created back in our first article as a customized built-in element. (The code is also available in the CodePen demo.)

The changes are actually fairly simple. Instead of extending the generic HTMLElement, we’ll extend a specific element, in this case the <div> element which has the class HTMLDivElement. We’ll also add a third argument to the customElements.defines function: {extends: 'div'}.

customElements.define(   "apocalyptic-warning",   class ApocalypseWarning extends HTMLDivElement {     constructor() {       super();       let warning = document.getElementById("warningtemplate");       let mywarning = warning.content;        const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(         mywarning.cloneNode(true)       );     }   },   { extends: "div" } );

Lastly, we’ll update our HTML from <apocalyptic-warning> tags to <div> tags that include an is attribute set to “apocalyptic-warning” like this:

<div is="apocalyptic-warning">   <span slot="whats-coming">Undead</span> </div>

Reminder: If you’re looking at the below in Safari, you won’t see any beautiful web component goodness *shakes fist at Safari*

Only certain elements can have a shadow root attached to them. Some of this is because attaching a shadow root to, say, an <a> element or <form> element could have security implications. The list of available elements is mostly layout elements, such as <article>, <section>, <aside>, <main>, <header>, <div>, <nav>, and <footer>, plus text-related elements like <p>, <span>, <blockquote>, and <h1><h6>. Last but not least, we also get the body element and any valid autonomous custom element.

Adding a shadow root is not the only thing we can do to create a web component. At its base, a web component is a way to bake functionality into an element and we don’t need additional markup in the shadows to do that. Let’s create an image with a built-in light box feature to illustrate the point.

We’ll take a normal <img> element and add two attributes: first, the is attribute that signifies this <img> is a customized built-in element; and a data attribute that holds the path to the larger image that we’ll show in the light box. (Since I’m using an SVG, I just used the same URL, but you could easily have a smaller raster image embedded in the site and a larger version of it in the light box.)

<img is="light-box" src="https://assets.codepen.io/1804713/ninja2.svg" data-lbsrc="https://assets.codepen.io/1804713/ninja2.svg" alt="Silent but Undeadly Zombie Ninja" />

Since we can’t do a shadow DOM for this <img>, there’s no need for a <template> element, <slot> elements, or any of those other things. We also won’t have any encapsulated styles.

So, let’s skip straight to the JavaScript:

customElements.define(   "light-box",   class LightBox extends HTMLImageElement {     constructor() {       super();       // We’re creating a div element to use as the light box. We’ll eventually insert it just before the image in question.       let lb = document.createElement("div");       // Since we can’t use a shadow DOM, we can’t encapsulate our styles there. We could add these styles to the main CSS file, but they could bleed out if we do that, so I’m setting all styles for the light box div right here       lb.style.display = "none";       lb.style.position = "absolute";       lb.style.height = "100vh";       lb.style.width = "100vw";       lb.style.top = 0;       lb.style.left = 0;       lb.style.background =         "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";       lb.style.backgroundSize = "contain";        lb.addEventListener("click", function (evt) {         // We’ll close our light box by clicking on it         this.style.display = "none";       });       this.parentNode.insertBefore(lb, this); // This inserts the light box div right before the image       this.addEventListener("click", function (evt) {         // Opens the light box when the image is clicked.         lb.style.display = "block";       });     }   },   { extends: "img" } );

Now that we know how customized built-in elements work, we need to move toward ensuring they’ll work everywhere. Yes, Safari, this stink eye is for you.

WebComponents.org has a generalized polyfill that handles both customized built-in elements and autonomous elements, but because it can handle so much, it may be a lot more than you need, particularly if all you’re looking to do is support customized built-in elements in Safari.

Since Safari supports autonomous custom elements, we can swap out the <img> with an autonomous custom element such as <lightbox-polyfill>. “This will be like two lines of code!” the author naively said to himself. Thirty-seven hours of staring at a code editor, two mental breakdowns, and a serious reevaluation of his career path later, he realized that he’d need to start typing if he wanted to write those two lines of code. It also ended up being more like sixty lines of code (but you’re probably good enough to do it in like ten lines).

The original code for the light box can mostly stand as-is (although we’ll add a new autonomous custom element shortly), but it needs a few small adjustments. Outside the definition of the custom element, we need to set a Boolean.

let customBuiltInElementsSupported = false;

Then within the LightBox constructor, we set the Boolean to true. If customized built-in elements aren’t supported, the constructor won’t run and the Boolean won’t be set to true; thus we have a direct test for whether customized built-in elements are supported.

Before we use that test to replace our customized built-in element, we need to create an autonomous custom element to be used as a polyfill, namely <lightbox-polyfill>.

customElements.define(   "lightbox-polyfill", // We extend the general HTMLElement instead of a specific one   class LightBoxPoly extends HTMLElement {      constructor() {       super();        // This part is the same as the customized built-in element’s constructor       let lb = document.createElement("div");       lb.style.display = "none";       lb.style.position = "absolute";       lb.style.height = "100vh";       lb.style.width = "100vw";       lb.style.top = 0;       lb.style.left = 0;       lb.style.background =         "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";       lb.style.backgroundSize = "contain";        // Here’s where things start to diverge. We add a `shadowRoot` to the autonomous custom element because we can’t add child nodes directly to the custom element in the constructor. We could use an HTML template and slots for this, but since we only need two elements, it's easier to just create them in JavaScript.       const shadowRoot = this.attachShadow({ mode: "open" });        // We create an image element to display the image on the page       let lbpimg = document.createElement("img");        // Grab the `src` and `alt` attributes from the autonomous custom element and set them on the image       lbpimg.setAttribute("src", this.getAttribute("src"));       lbpimg.setAttribute("alt", this.getAttribute("alt"));        // Add the div and the image to the `shadowRoot`       shadowRoot.appendChild(lb);       shadowRoot.appendChild(lbpimg);        // Set the event listeners so that you show the div when the image is clicked, and hide the div when the div is clicked.       lb.addEventListener("click", function (evt) {         this.style.display = "none";       });       lbpimg.addEventListener("click", function (evt) {         lb.style.display = "block";       });     }   } );

Now that we have the autonomous element ready, we need some code to replace the customized <img> element when it’s unsupported in the browser.

if (!customBuiltInElementsSupported) {   // Select any image with the `is` attribute set to `light-box`   let lbimgs = document.querySelectorAll('img[is="light-box"]');   for (let i = 0; i < lbimgs.length; i++) { // Go through all light-box images     let replacement = document.createElement("lightbox-polyfill"); // Create an autonomous custom element      // Grab the image and div from the `shadowRoot` of the new lighbox-polyfill element and set the attributes to those originally on the customized image, and set the background on the div.     replacement.shadowRoot.querySelector("img").setAttribute("src", lbimgs[i].getAttribute("src"));     replacement.shadowRoot.querySelector("img").setAttribute("alt", lbimgs[i].getAttribute("alt"));     replacement.shadowRoot.querySelector("div").style.background =       "rgba(0,0,0, 0.7) url(" + lbimgs[i].dataset.lbsrc + ") no-repeat center";      // Stick the new lightbox-polyfill element into the DOM just before the image we’re replacing     lbimgs[i].parentNode.insertBefore(replacement, lbimgs[i]);     // Remove the customized built-in image     lbimgs[i].remove();   } }

So there you have it! We not only built autonomous custom elements, but customized built-in elements as well — including how to make them work in Safari. And we get all the benefits of structured, semantic HTML elements to boot including giving screen readers and search engines an idea of what these custom elements are.

Go forth and customize yon built-in elements with impunity!


The post Supercharging Built-In Elements With Web Components “is” Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,
[Top]

Using Web Components in WordPress is Easier Than You Think

Now that we’ve seen that web components and interactive web components are both easier than you think, let’s take a look at adding them to a content management system, namely WordPress.

There are three major ways we can add them. First, through manual input into the siteputting them directly into widgets or text blocks, basically anywhere we can place other HTML. Second, we can add them as the output of a theme in a theme file. And, finally, we can add them as the output of a custom block.

Loading the web component files

Now whichever way we end up adding web components, there’s a few things we have to ensure:

  1. our custom element’s template is available when we need it,
  2. any JavaScript we need is properly enqueued, and
  3. any un-encapsulated styles we need are enqueued.

We’ll be adding the <zombie-profile> web component from my previous article on interactive web components. Check out the code over at CodePen.

Let’s hit that first point. Once we have the template it’s easy enough to add that to the WordPress theme’s footer.php file, but rather than adding it directly in the theme, it’d be better to hook into wp_footer so that the component is loaded independent of the footer.php file and independent of the overall theme— assuming that the theme uses wp_footer, which most do. If the template doesn’t appear in your theme when you try it, double check that wp_footer is called in your theme’s footer.php template file.

<?php function diy_ezwebcomp_footer() { ?>   <!-- print/echo Zombie profile template code. -->   <!-- It's available at https://codepen.io/undeadinstitute/pen/KKNLGRg --> <?php }  add_action( 'wp_footer', 'diy_ezwebcomp_footer');

Next is to enqueue our component’s JavaScript. We can add the JavaScript via wp_footer as well, but enqueueing is the recommended way to link JavaScript to WordPress. So let’s put our JavaScript in a file called ezwebcomp.js (that name is totally arbitrary), stick that file in the theme’s JavaScript directory (if there is one), and enqueue it (in the functions.php file).

wp_enqueue_script( 'ezwebcomp_js', get_template_directory_uri() . '/js/ezwebcomp.js', '', '1.0', true ); 

We’ll want to make sure that last parameter is set to true , i.e. it loads the JavaScript before the closing body tag. If we load it in the head instead, it won’t find our HTML template and will get super cranky (throw a bunch of errors.)

If you can fully encapsulate your web component, then you can skip this next step. But if you (like me) are unable to do it, you’ll need to enqueue those un-encapsulated styles so that they’re available wherever the web component is used. (Similar to JavaScript, we could add this directly to the footer, but enqueuing the styles is the recommended way to do it). So we’ll enqueue our CSS file:

wp_enqueue_style( 'ezwebcomp_style', get_template_directory_uri() . '/ezwebcomp.css', '', '1.0', 'screen' ); 

That wasn’t too tough, right? And if you don’t plan to have any users other than Administrators use it, you should be all set for adding these wherever you want them. But that’s not always the case, so we’ll keep moving ahead!

Don’t filter out your web component

WordPress has a few different ways to both help users create valid HTML and prevent your Uncle Eddie from pasting that “hilarious” picture he got from Shady Al directly into the editor (complete with scripts to pwn every one of your visitors).

So when adding web-components directly into blocks or widgets, we’ll need to be careful about WordPress’s built-in code filtering . Disabling it all together would let Uncle Eddie (and, by extension, Shady Al) run wild, but we can modify it to let our awesome web component through the gate that (thankfully) keeps Uncle Eddie out.

First, we can use the wp_kses_allowed filter to add our web component to the list of elements not to filter out. It’s sort of like we’re whitelisting the component, and we do that by adding it to the the allowed tags array that’s passed to the filter function.

function add_diy_ezwebcomp_to_kses_allowed( $ the_allowed_tags ) {   $ the_allowed_tags['zombie-profile'] = array(); } add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

We’re adding an empty array to the <zombie-profile> component because WordPress filters out attributes in addition to elements—which brings us to another problem: the slot attribute (as well as part and any other web-component-ish attribute you might use) are not allowed by default. So, we have to explitcly allow them on every element on which you anticipate using them, and, by extension, any element your user might decide to add them to. (Wait, those element lists aren’t the same even though you went over it six times with each user… who knew?) Thus, below I have set slot to true on <span>, <img> and <ul>, the three elements I’m putting into slots in the <zombie-profile> component. (I also set part to true on span elements so that I could let that attribute through too.)

function add_diy_ezwebcomp_to_kses_allowed( $ the_allowed_tags ) {   $ the_allowed_tags['zombie-profile'] = array();   $ the_allowed_tags['span']['slot'] = true;   $ the_allowed_tags['span']['part'] = true;   $ the_allowed_tags['ul']['slot'] = true;   $ the_allowed_tags['img']['slot'] = true;   return $ the_allowed_tags; } add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

We could also enable the slot (and part) attribute in all allowed elements with something like this:

function add_diy_ezwebcomp_to_kses_allowed($ the_allowed_tags) {   $ the_allowed_tags['zombie-profile'] = array();   foreach ($ the_allowed_tags as &$ tag) {     $ tag['slot'] = true;     $ tag['part'] = true;   }   return $ the_allowed_tags; } add_filter('wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed'); 

Sadly, there is one more possible wrinkle with this. You may not run into this if all the elements you’re putting in your slots are inline/phrase elements, but if you have a block level element to put into your web component, you’ll probably get into a fistfight with the block parser in the Code Editor. You may be a better fist fighter than I am, but I always lost.

The code editor is an option that allows you to inspect and edit the markup for a block.

For reasons I can’t fully explain, the client-side parser assumes that the web component should only have inline elements within it, and if you put a <ul> or <div>, <h1> or some other block-level element in there, it’ll move the closing web component tag to just after the last inline/phrase element. Worse yet, according to a note in the WordPress Developer Handbook, it’s currently “not possible to replace the client-side parser.”

While this is frustrating and something you’ll have to train your web editors on, there is a workaround. If we put the web component in a Custom HTML block directly in the Block Editor, the client-side parser won’t leave us weeping on the sidewalk, rocking back and forth, and questioning our ability to code… Not that that’s ever happened to anyone… particularly not people who write articles…

Component up the theme

Outputting our fancy web component in our theme file is straightforward as long as it isn’t updated outside the HTML block. We add it the way we would add it in any other context, and, assuming we have the template, scripts and styles in place, things will just work.

But let’s say we want to output the contents of a WordPress post or custom post type in a web component. You know, write a post and that post is the content for the component. This allows us to use the WordPress editor to pump out an archive of <zombie-profile> elements. This is great because the WordPress editor already has most of the UI we need to enter the content for one of the <zombie-profile> components:

  • The post title can be the zombie’s name.
  • A regular paragraph block in the post content can be used for the zombie’s statement.
  • The featured image can be used for the zombie’s profile picture.

That’s most of it! But we’ll still need fields for the zombie’s age, infection date, and interests. We’ll create these with WordPress’s built in Custom Fields feature.

We’ll use the template part that handles printing each post, e.g. content.php, to output the web component. First, we’ll print out the opening <zombie-profile> tag followed by the post thumbnail (if it exists).

<zombie-profile>   <?php      // If the post featured image exists...     if (has_post_thumbnail()) {       $ src = wp_get_attachment_image_url(get_post_thumbnail_id()); ?>       <img src="<?php echo $ src; ?>" slot="profile-image">     <?php     }   ?>

Next we’ll print the title for the name

<?php   // If the post title field exits...   if (get_the_title()) { ?>   <span slot="zombie-name"><?php echo get_the_title(); ?></span>   <?php   } ?>

In my code, I have tested whether these fields exist before printing them for two reasons:

  1. It’s just good programming practice (in most cases) to hide the labels and elements around empty fields.
  2. If we end up outputting an empty <span> for the name (e.g. <span slot="zombie-name"></span>), then the field will show as empty in the final profile rather than use our web component’s built-in default text, image, etc. (If you want, for instance, the text fields to be empty if they have no content, you can either put in a space in the custom field or skip the if statement in the code).

Next, we will grab the custom fields and place them into the slots they belong to. Again, this goes into the theme template that outputs the post content.

<?php   // Zombie age   $ temp = get_post_meta(the_ID(), 'Age', true);   if ($ temp) { ?>     <span slot="z-age"><?php echo $ temp; ?></span>     <?php   }   // Zombie infection date   $ temp = get_post_meta(the_ID(), 'Infection Date', true);   if ($ temp) { ?>     <span slot="idate"><?php echo $ temp; ?></span>     <?php   }   // Zombie interests   $ temp = get_post_meta(the_ID(), 'Interests', true);   if ($ temp) { ?>     <ul slot="z-interests"><?php echo $ temp; ?></ul>     <?php   } ?>

One of the downsides of using the WordPress custom fields is that you can’t do any special formatting, A non-technical web editor who’s filling this out would need to write out the HTML for the list items (<li>) for each and every interest in the list. (You can probably get around this interface limitation by using a more robust custom field plugin, like Advanced Custom Fields, Pods, or similar.)

Lastly. we add the zombie’s statement and the closing <zombie-profile> tag.

<?php   $ temp = get_the_content();   if ($ temp) { ?>     <span slot="statement"><?php echo $ temp; ?></span>   <?php   } ?> </zombie-profile>

Because we’re using the body of the post for our statement, we’ll get a little extra code in the bargain, like paragraph tags around the content. Putting the profile statement in a custom field will mitigate this, but depending on your purposes, it may also be intended/desired behavior.

You can then add as many posts/zombie profiles as you need simply by publishing each one as a post!

Block party: web components in a custom block

Creating a custom block is a great way to add a web component. Your users will be able to fill out the required fields and get that web component magic without needing any code or technical knowledge. Plus, blocks are completely independent of themes, so really, we could use this block on one site and then install it on other WordPress sites—sort of like how we’d expect a web component to work!

There are the two main parts of a custom block: PHP and JavaScript. We’ll also add a little CSS to improve the editing experience.

First, the PHP:

function ez_webcomp_register_block() {   // Enqueues the JavaScript needed to build the custom block   wp_register_script(     'ez-webcomp',     plugins_url('block.js', __FILE__),     array('wp-blocks', 'wp-element', 'wp-editor'),     filemtime(plugin_dir_path(__FILE__) . 'block.js')   );    // Enqueues the component's CSS file   wp_register_style(     'ez-webcomp',     plugins_url('ezwebcomp-style.css', __FILE__),     array(),     filemtime(plugin_dir_path(__FILE__) . 'ezwebcomp-style.css')   );    // Registers the custom block within the ez-webcomp namespace   register_block_type('ez-webcomp/zombie-profile', array(     // We already have the external styles; these are only for when we are in the WordPress editor     'editor_style' =&gt; 'ez-webcomp',     'editor_script' =&gt; 'ez-webcomp',   )); } add_action('init', 'ez_webcomp_register_block'); 

The CSS isn’t necessary, it does help prevent the zombie’s profile image from overlapping the content in the WordPress editor.

/* Sets the width and height of the image.  * Your mileage will likely vary, so adjust as needed.  * "pic" is a class we'll add to the editor in block.js */ #editor .pic img {   width: 300px;   height: 300px; } /* This CSS ensures that the correct space is allocated for the image,  * while also preventing the button from resizing before an image is selected. */ #editor .pic button.components-button {    overflow: visible;   height: auto; } 

The JavaScript we need is a bit more involved. I’ve endeavored to simplify it as much as possible and make it as accessible as possible to everyone, so I’ve written it in ES5 to remove the need to compile anything.

Show code
(function (blocks, editor, element, components) {   // The function that creates elements   var el = element.createElement;   // Handles text input for block fields    var RichText = editor.RichText;   // Handles uploading images/media   var MediaUpload = editor.MediaUpload;        // Harkens back to register_block_type in the PHP   blocks.registerBlockType('ez-webcomp/zombie-profile', {     title: 'Zombie Profile', //User friendly name shown in the block selector     icon: 'id-alt', //the icon to usein the block selector     category: 'layout',     // The attributes are all the different fields we'll use.     // We're defining what they are and how the block editor grabs data from them.     attributes: {       name: {         // The content type         type: 'string',         // Where the info is available to grab         source: 'text',         // Selectors are how the block editor selects and grabs the content.         // These should be unique within an instance of a block.         // If you only have one img or one <ul> etc, you can use element selectors.         selector: '.zname',       },       mediaID: {         type: 'number',       },       mediaURL: {         type: 'string',         source: 'attribute',         selector: 'img',         attribute: 'src',       },       age: {         type: 'string',         source: 'text',         selector: '.age',       },       infectdate: {         type: 'date',         source: 'text',         selector: '.infection-date'       },       interests: {         type: 'array',         source: 'children',         selector: 'ul',       },       statement: {         type: 'array',         source: 'children',         selector: '.statement',       },   },   // The edit function handles how things are displayed in the block editor.   edit: function (props) {     var attributes = props.attributes;     var onSelectImage = function (media) {       return props.setAttributes({         mediaURL: media.url,         mediaID: media.id,       });     };     // The return statement is what will be shown in the editor.     // el() creates an element and sets the different attributes of it.     return el(       // Using a div here instead of the zombie-profile web component for simplicity.       'div', {         className: props.className       },       // The zombie's name       el(RichText, {         tagName: 'h2',         inline: true,         className: 'zname',         placeholder: 'Zombie Name…',         value: attributes.name,         onChange: function (value) {           props.setAttributes({             name: value           });         },       }),       el(         // Zombie profile picture         'div', {           className: 'pic'         },         el(MediaUpload, {           onSelect: onSelectImage,           allowedTypes: 'image',           value: attributes.mediaID,           render: function (obj) {             return el(               components.Button, {                 className: attributes.mediaID ?                   'image-button' : 'button button-large',                 onClick: obj.open,               },               !attributes.mediaID ?               'Upload Image' :               el('img', {                 src: attributes.mediaURL               })             );           },         })       ),       // We'll include a heading for the zombie's age in the block editor       el('h3', {}, 'Age'),       // The age field       el(RichText, {         tagName: 'div',         className: 'age',         placeholder: 'Zombie's Age…',         value: attributes.age,         onChange: function (value) {           props.setAttributes({             age: value           });         },       }),       // Infection date heading       el('h3', {}, 'Infection Date'),       // Infection date field       el(RichText, {         tagName: 'div',         className: 'infection-date',         placeholder: 'Zombie's Infection Date…',         value: attributes.infectdate,         onChange: function (value) {           props.setAttributes({             infectdate: value           });         },       }),       // Interests heading       el('h3', {}, 'Interests'),       // Interests field       el(RichText, {         tagName: 'ul',         // Creates a new <li> every time `Enter` is pressed         multiline: 'li',         placeholder: 'Write a list of interests…',         value: attributes.interests,         onChange: function (value) {           props.setAttributes({             interests: value           });         },         className: 'interests',       }),       // Zombie statement heading       el('h3', {}, 'Statement'),       // Zombie statement field       el(RichText, {         tagName: 'div',         className: "statement",         placeholder: 'Write statement…',         value: attributes.statement,         onChange: function (value) {           props.setAttributes({             statement: value           });         },       })     );   },    // Stores content in the database and what is shown on the front end.   // This is where we have to make sure the web component is used.   save: function (props) {     var attributes = props.attributes;     return el(       // The <zombie-profile web component       'zombie-profile',       // This is empty because the web component does not need any HTML attributes       {},       // Ensure a URL exists before it prints       attributes.mediaURL &&       // Print the image       el('img', {         src: attributes.mediaURL,         slot: 'profile-image'       }),       attributes.name &&       // Print the name       el(RichText.Content, {         tagName: 'span',         slot: 'zombie-name',         className: 'zname',         value: attributes.name,       }),       attributes.age &&       // Print the zombie's age       el(RichText.Content, {         tagName: 'span',         slot: 'z-age',         className: 'age',         value: attributes.age,     }),       attributes.infectdate &&       // Print the infection date       el(RichText.Content, {         tagName: 'span',         slot: 'idate',         className: 'infection-date',         value: attributes.infectdate,     }),       // Need to verify something is in the first element since the interests's type is array       attributes.interests[0] &&       // Pint the interests       el(RichText.Content, {         tagName: 'ul',         slot: 'z-interests',         value: attributes.interests,       }),       attributes.statement[0] &&       // Print the statement       el(RichText.Content, {         tagName: 'span',         slot: 'statement',         className: 'statement',         value: attributes.statement,     })     );     },   }); })(   //import the dependencies   window.wp.blocks,   window.wp.blockEditor,   window.wp.element,   window.wp.components );

Plugging in to web components

Now, wouldn’t it be great if some kind-hearted, article-writing, and totally-awesome person created a template that you could just plug your web component into and use on your site? Well that guy wasn’t available (he was off helping charity or something) so I did it. It’s up on github:

Do It Yourself – Easy Web Components for WordPress

The plugin is a coding template that registers your custom web component, enqueues the scripts and styles the component needs, provides examples of the custom block fields you might need, and even makes sure things are styled nicely in the editor. Put this in a new folder in /wp-content/plugins like you would manually install any other WordPress plugin, make sure to update it with your particular web component, then activate it in WordPress on the “Installed Plugins” screen.

Not that bad, right?

Even though it looks like a lot of code, we’re really doing a few pretty standard WordPress things to register and render a custom web component. And, since we packaged it up as a plugin, we can drop this into any WordPress site and start publishing zombie profiles to our heart’s content.

I’d say that the balancing act is trying to make the component work as nicely in the WordPress block editor as it does on the front end. We would have been able to knock this out with a lot less code without that consideration.

Still, we managed to get the exact same component we made in my previous articles into a CMS, which allows us to plop as many zombie profiles on the site. We combined our knowledge of web components with WordPress blocks to develop a reusable block for our reusable web component.

What sort of components will you build for your WordPress site? I imagine there are lots of possibilities here and I’m interested to see what you wind up making.

Article series

  1. Web Components Are Easier Than You Think
  2. Interactive Web Components Are Easier Than You Think
  3. Using Web Components in WordPress is Easier Than You Think

The post Using Web Components in WordPress is Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Making Tables With Sticky Header and Footers Got a Bit Easier

It wasn’t long ago when I looked at sticky headers and footers in HTML <table>s in the blog post A table with both a sticky header and a sticky first column. In it, I never used position: sticky on any <thead>, <tfoot>, or <tr> element, because even though Safari and Firefox could do that, Chrome could not. But it could do table cells like <th> and <td>, which was a decent-enough workaround.

Well that’s changed.

Sounds like a big effort went into totally revamping tables in the rendering engine in Chromium, bringing tables up to speed. It’s not just the stickiness that was fixed, but all sorts of things. I’ll just focus on the sticky thing since that’s what I looked at.

The headline to me is that <thead> and <tfoot> are sticky-able. That seems like it will be the most common use case here.

table thead, table tfoot {   position: sticky; } table thead {   inset-block-start: 0; /* "top" */ } table tfoot {   inset-block-end: 0; /* "bottom" */ }

That works in all three major browsers. You might want to get clever and only sticky them at certain minimum viewport heights or something, but the point is it works.

I heard several questions about table columns as well. My original article had a sticky first column (that was kind of the point). While there is a table <col> tag, it’s… weird. It doesn’t actually wrap columns, it’s more like a pointer thing to be able to style down the column if you need to. I hardly ever see it used, but it’s there. Anyway, you totally can’t position: sticky; a <col>, but you can make sticky columns. You need to select all the cells in that column and stick them to the left or right. Here’s that using logical properties…

table tr th:first-child {   position: sticky;   inset-inline-start: 0; /* "left" */ }

Here’s a sorta obnoxious table where the <thead>, <tfoot>, and the first and last columns are all sticky.

I’m sure you could do something tasteful with this. Like maybe:


The post Making Tables With Sticky Header and Footers Got a Bit Easier 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]

Web Components Are Easier Than You Think

When I’d go to a conference (when we were able to do such things) and see someone do a presentation on web components, I always thought it was pretty nifty (yes, apparently, I’m from 1950), but it always seemed complicated and excessive. A thousand lines of JavaScript to save four lines of HTML. The speaker would inevitably either gloss over the oodles of JavaScript to get it working or they’d go into excruciating detail and my eyes would glaze over as I thought about whether my per diem covered snacks.

But in a recent reference project to make learning HTML easier (by adding zombies and silly jokes, of course), the completist in me decided I had to cover every HTML element in the spec. Beyond those conference presentations, this was my first introduction to the <slot> and <template> elements. But as I tried to write something accurate and engaging, I was forced to delve a bit deeper.

And I’ve learned something in the process: web components are a lot easier than I remember.

Either web components have come a long way since the last time I caught myself daydreaming about snacks at a conference, or I let my initial fear of them get in the way of truly knowing them — probably both.

I’m here to tell you that you—yes, you—can create a web component. Let’s leave our distractions, fears, and even our snacks at the door for a moment and do this together.

Let’s start with the <template>

A <template> is an HTML element that allows us to create, well, a template—the HTML structure for the web component. A template doesn’t have to be a huge chunk of code. It can be as simple as:

<template>   <p>The Zombies are coming!</p> </template>

The <template> element is important because it holds things together. It’s like the foundation of building; it’s the base from which everything else is built. Let’s use this small bit of HTML as the template for an <apocalyptic-warning> web component—you know, as a warning when the zombie apocalypse is upon us.

Then there’s the <slot>

<slot> is merely another HTML element just like <template>. But in this case, <slot> customizes what the <template> renders on the page.

<template>   <p>The <slot>Zombies</slot> are coming!</p> </template>

Here, we’ve slotted (is that even a word?) the word “Zombies” in the templated markup. If we don’t do anything with the slot, it defaults to the content between the tags. That would be “Zombies” in this example.

Using <slot> is a lot like having a placeholder. We can use the placeholder as is, or define something else to go in there instead. We do that with the name attribute.

<template>   <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>

The name attribute tells the web component which content goes where in the template. Right now, we’ve got a slot called whats-coming. We’re assuming zombies are coming first in the apocalypse, but the <slot> gives us some flexibility to slot something else in, like if it ends up being a robot, werewolf, or even a web component apocalypse.

Using the component

We’re technically done “writing” the component and can drop it in anywhere we want to use it.

<apocalyptic-warning>   <span slot="whats-coming">Halitosis Laden Undead Minions</span> </apocalyptic-warning>  <template>   <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>

See what we did there? We put the <apocalyptic-warning> component on the page just like any other <div> or whatever. But we also dropped a <span> in there that references the name attribute of our <slot>. And what’s between that <span> is what we want to swap in for “Zombies” when the component renders.

Here’s a little gotcha worth calling out: custom element names must have a hyphen in them. It’s just one of those things you’ve gotta know going into things. The spec (which is still in flux) prescribes that to prevent conflicts in the event that HTML releases a new element with the same name.

Still with me so far? Not too scary, right? Well, minus the zombies. We still have a little work to do to make the <slot> swap possible, and that’s where we start to get into JavaScript.

Registering the component

As I said, you do need some JavaScript to make this all work, but it’s not the super complex, thousand-lined, in-depth code I always thought. Hopefully I can convince you as well.

You need a constructor function that registers the custom element. Otherwise, our component is like the undead: it’s there but not fully alive.

Here’s the constructor we’ll use:

// Defines the custom element with our appropriate name, <apocalyptic-warning> customElements.define("apocalyptic-warning",    // Ensures that we have all the default properties and methods of a built in HTML element   class extends HTMLElement {      // Called anytime a new custom element is created     constructor() {        // Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element       super();        // Grabs the <template> and stores it in `warning`       let warning = document.getElementById("warningtemplate");        // Stores the contents of the template in `mywarning`       let mywarning = warning.content;        const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));     }   });

I left detailed comments in there that explain things line by line. Except the last line:

const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));

We’re doing a lot in here. First, we’re taking our custom element (this) and creating a clandestine operative—I mean, shadow DOM. mode: open simply means that JavaScript from outside the :root can access and manipulate the elements within the shadow DOM, sort of like setting up back door access to the component.

From there, the shadow DOM has been created and we append a node to it. That node will be a deep copy of the template, including all elements and text of the template. With the template attached to the shadow DOM of the custom element, the <slot> and slot attribute take over for matching up content with where it should go.

Check this out. Now we can plop two instances of the same component, rendering different content simply by changing one element.

Styling the component

You may have noticed styling in that demo. As you might expect, we absolutely have the ability to style our component with CSS. In fact, we can include a <style> element right in the <template>.

<template id="warningtemplate">   <style>     p {       background-color: pink;       padding: 0.5em;       border: 1px solid red;     }   </style>      <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>

This way, the styles are scoped directly to the component and nothing leaks out to other elements on the same page, thanks to the shadow DOM.

Now in my head, I assumed that a custom element was taking a copy of the template, inserting the content you’ve added, and then injecting that into the page using the shadow DOM. While that’s what it looks like on the front end, that’s not how it actually works in the DOM. The content in a custom element stays where it is and the shadow DOM is sort of laid on top like an overlay.

Screenshot of the HTML source of the zombie-warning component. The custom element is expanded in the shadow dam, including the style block, the custom element, and the template.

And since the content is technically outside the template, any descendant selectors or classes we use in the template’s <style> element will have no affect on the slotted content. This doesn’t allow full encapsulation the way I had hoped or expected. But since a custom element is an element, we can use it as an element selector in any ol’ CSS file, including the main stylesheet used on a page. And although the inserted material isn’t technically in the template, it is in the custom element and descendant selectors from the CSS will work.

apocalyptic-warning span {   color: blue; }

But beware! Styles in the main CSS file cannot access elements in the <template> or shadow DOM.

Let’s put all of this together

Let’s look at an example, say a profile for a zombie dating service, like one you might need after the apocalypse. In order to style both the default content and any inserted content, we need both a <style> element in the <template> and styling in a CSS file.

The JavaScript code is exactly the same except now we’re working with a different component name, <zombie-profile>.

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));     }   } );

Here’s the HTML template, including the encapsulated CSS:

<template id="zprofiletemplate">   <style>     img {       width: 100%;       max-width: 300px;       height: auto;       margin: 0 1em 0 0;     }     h2 {       font-size: 3em;       margin: 0 0 0.25em 0;       line-height: 0.8;     }     h3 {       margin: 0.5em 0 0 0;       font-weight: normal;     }     .age, .infection-date {       display: block;     }     span {       line-height: 1.4;     }     .label {       color: #555;     }     li, ul {       display: inline;       padding: 0;     }     li::after {       content: ', ';     }     li:last-child::after {       content: '';     }     li:last-child::before {       content: ' and ';     }   </style>    <div class="profilepic">     <slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>   </div>    <div class="info">     <h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>      <span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>     <span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>      <div class="interests">       <span class="label">Interests: </span>       <slot name="z-interests">         <ul>           <li>Long Walks on Beach</li>           <li>brains</li>           <li>defeating humanity</li>         </ul>       </slot>     </div>      <span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>    </div> </template>

Here’s the CSS for our <zombie-profile> element and its descendants from our main CSS file. Notice the duplication in there to ensure both the replaced elements and elements from the template are styled the same.

zombie-profile {   width: calc(50% - 1em);   border: 1px solid red;   padding: 1em;   margin-bottom: 2em;   display: grid;   grid-template-columns: 2fr 4fr;   column-gap: 20px; } zombie-profile img {   width: 100%;   max-width: 300px;   height: auto;   margin: 0 1em 0 0; } zombie-profile li, zombie-profile ul {   display: inline;   padding: 0; } zombie-profile li::after {   content: ', '; } zombie-profile li:last-child::after {   content: ''; } zombie-profile li:last-child::before {   content: ' and '; }

All together now!

While there are still a few gotchas and other nuances, I hope you feel more empowered to work with the web components now than you were a few minutes ago. Dip your toes in like we have here. Maybe sprinkle a custom component into your work here and there to get a feel for it and where it makes sense.

That’s really it. Now what are you more scared of, web components or the zombie apocalypse? I might have said web components in the not-so-distant past, but now I’m proud to say that zombies are the only thing that worry me (well, that and whether my per diem will cover snacks…)


The post 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]

A Trick That Makes Drawing SVG Lines Way Easier

When drawing lines with SVG, you often have a <path> element with a stroke. You set a stroke-dasharray that is as long as the path itself, as well as a stroke-offset that extends so far that you that it’s initially hidden. Then you animate the stroke-offset back to 0 so you can watch it “draw” the shape.

Figuring out the length of the path is the trick, which fortunately you can do in JavaScript by selecting the path and doing pathEl.getTotalLength(). It’ll probably be some weird decimal. A smidge unfortunate we can’t get that in CSS, but c’est la vie.

Here’s the trick!

You don’t have to measure the length of the path, because you can set it.

So you do like:

<path d="M66.039,133.545 ... " pathLength="1" />

That doesn’t do anything by itself (as far as I know). It’s not like that only draws part of the path — it still draws the whole thing like as if you did nothing, only now the “math” of the path length is based on a value of 1.

Now we can set the stroke-dasharray to 1, and animate the offset in CSS!

.path {   stroke-dasharray: 1;   stroke-dashoffset: 1;   animation: dash 5s linear alternate infinite; }  @keyframes dash {   from {     stroke-dashoffset: 1;   }   to {     stroke-dashoffset: 0;   } }

Which works:

See the Pen
Basic Example of SVG Line Drawing, Backward and Forward
by Chris Coyier (@chriscoyier)
on CodePen.

High five to Adam Haskell who emailed me about this a few months back.


Hey, speaking of SVG line drawing: Lemonade made a landing page for their 2019 charity that uses scroll-triggered SVG line drawing up and down the entire page. They did a behind-the-scenes look at it, which I always appreciate.

animated GIF of line drawing on Lemonade page - as page scrolls down a teddy bear shape is drawn

The post A Trick That Makes Drawing SVG Lines Way Easier appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]