mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
First stab at with-* → ssr-slots
- Infrastructure in base class - Sample implementation in Card - Docs
This commit is contained in:
@@ -6,7 +6,7 @@ icon: card
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
<wa-card with-image with-footer class="card-overview">
|
||||
<wa-card class="card-overview">
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
@@ -61,10 +61,10 @@ Basic cards aren't very exciting, but they can display any content you want them
|
||||
### Card with Header
|
||||
|
||||
Headers can be used to display titles and more.
|
||||
If using SSR, you need to also use the `with-header` attribute to add a header to the card (if not, it is added automatically).
|
||||
If using SSR, you need to also use the `ssr-slots` attribute to add a header to the card (if not, it is added automatically).
|
||||
|
||||
```html {.example}
|
||||
<wa-card with-header class="card-header">
|
||||
<wa-card ssr-slots="header" class="card-header">
|
||||
<div slot="header">
|
||||
Header Title
|
||||
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
|
||||
@@ -97,10 +97,10 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
|
||||
### Card with Footer
|
||||
|
||||
Footers can be used to display actions, summaries, or other relevant content.
|
||||
If using SSR, you need to also use the `with-footer` attribute to add a footer to the card (if not, it is added automatically).
|
||||
If using SSR, you need to also use the `ssr-slots` attribute to add a footer to the card (if not, it is added automatically).
|
||||
|
||||
```html {.example}
|
||||
<wa-card with-footer class="card-footer">
|
||||
<wa-card ssr-slots="footer" class="card-footer">
|
||||
This card has a footer. You can put all sorts of things in it!
|
||||
|
||||
<div slot="footer">
|
||||
@@ -125,10 +125,10 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
|
||||
### Images
|
||||
|
||||
Card images are displayed atop the card and will stretch to fit.
|
||||
If using SSR, you need to also use the `with-image` attribute to add an image to the card (if not, it is added automatically).
|
||||
If using SSR, you need to also use the `ssr-slots` attribute to add an image to the card (if not, it is added automatically).
|
||||
|
||||
```html {.example}
|
||||
<wa-card with-image class="card-image">
|
||||
<wa-card ssr-slots="image" class="card-image">
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
|
||||
@@ -150,7 +150,7 @@ Use the `size` attribute to change a card's size.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<wa-card with-footer size="small">
|
||||
<wa-card ssr-slots=footer size="small">
|
||||
This is a small card.
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
@@ -159,7 +159,7 @@ Use the `size` attribute to change a card's size.
|
||||
</footer>
|
||||
</wa-card>
|
||||
|
||||
<wa-card with-footer size="medium">
|
||||
<wa-card ssr-slots=footer size="medium">
|
||||
This is a medium card (default).
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
@@ -168,7 +168,7 @@ Use the `size` attribute to change a card's size.
|
||||
</footer>
|
||||
</wa-card>
|
||||
|
||||
<wa-card with-footer size="large">
|
||||
<wa-card ssr-slots=footer size="large">
|
||||
This is a large card.
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
|
||||
@@ -65,6 +65,35 @@ All Web Awesome components that get rendered for SSR will receive the `did-ssr`
|
||||
|
||||
This can help if you need some styling prior to the element connecting.
|
||||
|
||||
### The `ssr-slots` Attribute
|
||||
|
||||
When using certain slots in a component rendered through SSR, you may need to use the `ssr-slots` attribute
|
||||
to declare which slots have content.
|
||||
|
||||
E.g. when using a `<wa-card>` component without SSR, you can do this:
|
||||
|
||||
```html
|
||||
<wa-card>
|
||||
<img src="cat.jpg" slot="image">
|
||||
<header slot=header>Card Header</header>
|
||||
Card body
|
||||
<footer slot=footer>Card Footer</footer>
|
||||
</wa-card>
|
||||
```
|
||||
|
||||
However, when server-side rendering the very same card, you need to duplicate the information about what is in the slots in the `ssr-slots` attribute:
|
||||
|
||||
```html
|
||||
<wa-card ssr-slots="image header footer">
|
||||
<img src="cat.jpg" slot="image">
|
||||
<header slot=header>Card Header</header>
|
||||
Card body
|
||||
<footer slot=footer>Card Footer</footer>
|
||||
</wa-card>
|
||||
```
|
||||
|
||||
We are hoping to eventually be able to remove this requirement.
|
||||
|
||||
### Timing Issues
|
||||
|
||||
Before setting any properties on your frontend, it is important to first wait for the element to be defined and then wait for its first update to complete.
|
||||
@@ -116,4 +145,4 @@ Here are some known issues and things we're still working on.
|
||||
- `@shoelace-style/localize` (our localization library) has no way to set a language currently so it always falls back to `en`.
|
||||
- `<wa-icon>` has no fallback if there's no JS besides a blank `<svg>`. There's perhaps some backend mechanisms we can use to fetch. But requires altering APIs. Should also have a way to set height / widths, but we don't want to increase pain for SSR users.
|
||||
- `<wa-qr-code>` QR Code will not error on the backend and will render a blank canvas at the appropriate size, but will not render the canvas until the client component connects.
|
||||
- `setBasePath` and `kit codes` may need reconfiguring to work with SSR.
|
||||
- `setBasePath` and `kit codes` may need reconfiguring to work with SSR.
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
.image,
|
||||
:host(:not([with-image])) .header {
|
||||
:host(:not([ssr-slots~='image'])) .header {
|
||||
border-start-start-radius: var(--inner-border-radius);
|
||||
border-start-end-radius: var(--inner-border-radius);
|
||||
}
|
||||
@@ -56,8 +56,8 @@
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
:host(:not([with-header])) .header,
|
||||
:host(:not([with-footer])) .footer,
|
||||
:host(:not([with-image])) .image {
|
||||
:host(:not([ssr-slots~='header'])) .header,
|
||||
:host(:not([ssr-slots~='footer'])) .footer,
|
||||
:host(:not([ssr-slots~='image'])) .image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import styles from './card.css';
|
||||
@@ -31,21 +32,29 @@ export default class WaCard extends WebAwesomeElement {
|
||||
/** The component's size. Will be inherited by any descendants with a `size` attribute. */
|
||||
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
||||
|
||||
/** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
|
||||
@property({ attribute: 'with-header', type: Boolean }) withHeader = false;
|
||||
|
||||
/** Renders the card with an image. Only needed for SSR, otherwise is automatically added. */
|
||||
@property({ attribute: 'with-image', type: Boolean }) withImage = false;
|
||||
|
||||
/** Renders the card with a footer. Only needed for SSR, otherwise is automatically added. */
|
||||
@property({ attribute: 'with-footer', type: Boolean }) withFooter = false;
|
||||
static SSR_SLOTS = ['image', 'header', 'footer'];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<slot name="image" part="image" class="image"></slot>
|
||||
<slot name="header" part="header" class="header"></slot>
|
||||
<slot
|
||||
name="image"
|
||||
part="image"
|
||||
class="${classMap({ image: true, 'has-slotted': this.hasSlotted.has('image') })}"
|
||||
@slotchange=${this.slotUpdate}
|
||||
></slot>
|
||||
<slot
|
||||
name="header"
|
||||
part="header"
|
||||
class="${classMap({ header: true, 'has-slotted': this.hasSlotted.has('header') })}"
|
||||
@slotchange=${this.slotUpdate}
|
||||
></slot>
|
||||
<slot part="body" class="body"></slot>
|
||||
<slot name="footer" part="footer" class="footer"></slot>
|
||||
<slot
|
||||
name="footer"
|
||||
part="footer"
|
||||
class="${classMap({ footer: true, 'has-slotted': this.hasSlotted.has('footer') })}"
|
||||
@slotchange=${this.slotUpdate}
|
||||
></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
18
src/internal/converters.ts
Normal file
18
src/internal/converters.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ComplexAttributeConverter } from 'lit';
|
||||
|
||||
export const setConverter: ComplexAttributeConverter<Set<string> | null, Set<string> | null> = {
|
||||
fromAttribute(value): Set<string> | null {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Set(value.split(/\s+/));
|
||||
},
|
||||
toAttribute: value => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [...(value as Set<string>)].join(' ');
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import type { CSSResult, CSSResultGroup, PropertyDeclaration, PropertyValues } f
|
||||
import { LitElement, defaultConverter, isServer, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import componentStyles from '../styles/shadow/component.css';
|
||||
import { setConverter } from './converters.js';
|
||||
|
||||
// Augment Lit's module
|
||||
declare module 'lit' {
|
||||
@@ -40,6 +41,20 @@ export default class WebAwesomeElement extends LitElement {
|
||||
@property() dir: string;
|
||||
@property() lang: string;
|
||||
|
||||
@property({
|
||||
reflect: true,
|
||||
attribute: 'ssr-slots',
|
||||
converter: setConverter,
|
||||
})
|
||||
ssrSlots: Set<string> | null = null;
|
||||
|
||||
/**
|
||||
* All slots whose slotted status we need to know on the root for SSR.
|
||||
* Subclasses are expected to override this with their own list of slots.
|
||||
*/
|
||||
static SSR_SLOTS: string[] = [];
|
||||
protected hasSlotted = new Set();
|
||||
|
||||
/**
|
||||
* One or more styles for the element’s own shadow DOM.
|
||||
* Shared component styles will automatically be added.
|
||||
@@ -72,6 +87,13 @@ export default class WebAwesomeElement extends LitElement {
|
||||
|
||||
internals: ElementInternals;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
let Self = this.constructor as typeof WebAwesomeElement;
|
||||
this.ssrSlots = new Set(Self.SSR_SLOTS.filter(name => this.slotUpdate(name)));
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
||||
if (!this.#hasRecordedInitialProperties) {
|
||||
(this.constructor as typeof WebAwesomeElement).elementProperties.forEach(
|
||||
@@ -169,6 +191,44 @@ export default class WebAwesomeElement extends LitElement {
|
||||
return this.hasStatesSupport() ? this.internals.states.has(state) : false;
|
||||
}
|
||||
|
||||
protected async slotUpdate(target: Event | HTMLSlotElement | string) {
|
||||
await this.updateComplete;
|
||||
|
||||
let slot = target;
|
||||
|
||||
if (target instanceof Event) {
|
||||
slot = target.target as HTMLSlotElement;
|
||||
} else if (typeof target === 'string') {
|
||||
slot = this.shadowRoot!.querySelector(`slot[name="${target}"]`) as HTMLSlotElement;
|
||||
} else {
|
||||
slot = slot as HTMLSlotElement;
|
||||
}
|
||||
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slotName = slot.name;
|
||||
const hasSlotted = slot.assignedNodes().length > 0;
|
||||
|
||||
let previousHasSlotted = this.hasSlotted.has(slotName);
|
||||
|
||||
if (previousHasSlotted === hasSlotted) {
|
||||
let ssrSlots = new Set(this.ssrSlots);
|
||||
|
||||
if (hasSlotted) {
|
||||
ssrSlots.add(slotName);
|
||||
} else {
|
||||
ssrSlots.delete(slotName);
|
||||
}
|
||||
|
||||
this.ssrSlots = ssrSlots;
|
||||
}
|
||||
|
||||
slot.classList.toggle('has-slotted', hasSlotted);
|
||||
return hasSlotted;
|
||||
}
|
||||
|
||||
getComputed(prop: PropertyKey) {
|
||||
let value = this[prop as keyof this];
|
||||
if (value !== 'inherit') {
|
||||
|
||||
Reference in New Issue
Block a user