diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 90fa6217..21501c31 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -49,6 +49,7 @@ - Utility Components - [Animation](/components/animation.md) - [Format Bytes](/components/format-bytes.md) + - [Include](/components/include.md) - [Responsive Embed](/components/responsive-embed.md) - Design Tokens diff --git a/docs/assets/examples/include.html b/docs/assets/examples/include.html new file mode 100644 index 00000000..9f22bb05 --- /dev/null +++ b/docs/assets/examples/include.html @@ -0,0 +1,5 @@ +

+ The content in this example was included from a separate file. 🤯 +

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Fringilla urna porttitor rhoncus dolor purus non enim. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Gravida in fermentum et sollicitudin.

+

Cursus sit amet dictum sit amet justo donec enim. Sed id semper risus in hendrerit gravida. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit. Nec ullamcorper sit amet risus nullam. Et egestas quis ipsum suspendisse ultrices gravida dictum. Lorem donec massa sapien faucibus et molestie. A cras semper auctor neque vitae.

diff --git a/docs/components/include.md b/docs/components/include.md new file mode 100644 index 00000000..45009f24 --- /dev/null +++ b/docs/components/include.md @@ -0,0 +1,39 @@ +# Include + +[component-header:sl-include] + +Includes give you the power to embed external HTML files into the page. + +Included files are asynchronously requested using `window.fetch()`. Requests are cached, so the same file can be included multiple times, but only one request will be made. + +The included content will be inserted into the `` element's default slot so it can be easily accessed and styled through the light DOM. + +```html preview + +``` + +## Examples + +### Listening for Events + +When an include file loads successfully, the `sl-load` event will be emitted. You can listen for this event to add custom loading logic to your includes. + +If the request fails, the `sl-error` event will be emitted. In this case, `event.detail.status` will contain the resulting HTTP status code of the request, e.g. 404 (not found). + +```html + + + +``` + +[component-metadata:sl-include] diff --git a/docs/getting-started/changelog.md b/docs/getting-started/changelog.md index eb64e93e..45794009 100644 --- a/docs/getting-started/changelog.md +++ b/docs/getting-started/changelog.md @@ -10,6 +10,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Added `label` slot to `sl-input`, `sl-select`, and `sl-textarea` [#248](https://github.com/shoelace-style/shoelace/issues/248) - Added `label` slot to `sl-dialog` and `sl-drawer` +- Added experimental `sl-include` component - Fixed a bug where initial transitions didn't show in `sl-dialog` and `sl-drawer` [#247](https://github.com/shoelace-style/shoelace/issues/247) - Fixed a bug where indeterminate checkboxes would maintain the indeterminate state when toggled - Fixed a bug where concurrent active modals (i.e. dialog, drawer) would try to steal focus from each other diff --git a/src/components.d.ts b/src/components.d.ts index f07630e9..ecf24839 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -529,6 +529,16 @@ export namespace Components { */ "position": number; } + interface SlInclude { + /** + * The fetch mode to use. + */ + "mode": 'cors' | 'no-cors' | 'same-origin'; + /** + * The location of the HTML file to include. + */ + "src": string; + } interface SlInput { /** * The input's autocaptialize attribute. @@ -1286,6 +1296,12 @@ declare global { prototype: HTMLSlImageComparerElement; new (): HTMLSlImageComparerElement; }; + interface HTMLSlIncludeElement extends Components.SlInclude, HTMLStencilElement { + } + var HTMLSlIncludeElement: { + prototype: HTMLSlIncludeElement; + new (): HTMLSlIncludeElement; + }; interface HTMLSlInputElement extends Components.SlInput, HTMLStencilElement { } var HTMLSlInputElement: { @@ -1432,6 +1448,7 @@ declare global { "sl-icon-button": HTMLSlIconButtonElement; "sl-icon-library": HTMLSlIconLibraryElement; "sl-image-comparer": HTMLSlImageComparerElement; + "sl-include": HTMLSlIncludeElement; "sl-input": HTMLSlInputElement; "sl-menu": HTMLSlMenuElement; "sl-menu-divider": HTMLSlMenuDividerElement; @@ -2021,6 +2038,24 @@ declare namespace LocalJSX { */ "position"?: number; } + interface SlInclude { + /** + * The fetch mode to use. + */ + "mode"?: 'cors' | 'no-cors' | 'same-origin'; + /** + * Emitted when the included file fails to load due to an error. + */ + "onSl-error"?: (event: CustomEvent<{ status: number }>) => void; + /** + * Emitted when the included file is loaded. + */ + "onSl-load"?: (event: CustomEvent) => void; + /** + * The location of the HTML file to include. + */ + "src"?: string; + } interface SlInput { /** * The input's autocaptialize attribute. @@ -2670,6 +2705,7 @@ declare namespace LocalJSX { "sl-icon-button": SlIconButton; "sl-icon-library": SlIconLibrary; "sl-image-comparer": SlImageComparer; + "sl-include": SlInclude; "sl-input": SlInput; "sl-menu": SlMenu; "sl-menu-divider": SlMenuDivider; @@ -2716,6 +2752,7 @@ declare module "@stencil/core" { "sl-icon-button": LocalJSX.SlIconButton & JSXBase.HTMLAttributes; "sl-icon-library": LocalJSX.SlIconLibrary & JSXBase.HTMLAttributes; "sl-image-comparer": LocalJSX.SlImageComparer & JSXBase.HTMLAttributes; + "sl-include": LocalJSX.SlInclude & JSXBase.HTMLAttributes; "sl-input": LocalJSX.SlInput & JSXBase.HTMLAttributes; "sl-menu": LocalJSX.SlMenu & JSXBase.HTMLAttributes; "sl-menu-divider": LocalJSX.SlMenuDivider & JSXBase.HTMLAttributes; diff --git a/src/components/include/include.scss b/src/components/include/include.scss new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/src/components/include/include.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/components/include/include.tsx b/src/components/include/include.tsx new file mode 100644 index 00000000..455ecaea --- /dev/null +++ b/src/components/include/include.tsx @@ -0,0 +1,84 @@ +import { Component, Element, Event, EventEmitter, Prop, State, Watch, h } from '@stencil/core'; + +export interface IncludeFile { + ok: boolean; + status: number; + html: string; +} + +const includeFiles = new Map>(); + +/** + * @since 2.0 + * @status experimental + */ + +@Component({ + tag: 'sl-include', + styleUrl: 'include.scss', + shadow: true +}) +export class Include { + @Element() host: HTMLSlIncludeElement; + + @State() html = ''; + + /** The location of the HTML file to include. */ + @Prop() src: string; + + /** The fetch mode to use. */ + @Prop() mode: 'cors' | 'no-cors' | 'same-origin' = 'cors'; + + /** Emitted when the included file is loaded. */ + @Event({ eventName: 'sl-load' }) slLoad: EventEmitter; + + /** Emitted when the included file fails to load due to an error. */ + @Event({ eventName: 'sl-error' }) slError: EventEmitter<{ status: number }>; + + @Watch('src') + handleSrcChange() { + this.loadSource(); + } + + componentWillLoad() { + this.loadSource(); + } + + async requestFile(src: string) { + if (includeFiles.has(src)) { + return includeFiles.get(src); + } else { + const request = fetch(src, { mode: this.mode }).then(async response => { + return { + ok: response.ok, + status: response.status, + html: await response.text() + }; + }); + includeFiles.set(src, request); + return request; + } + } + + async loadSource() { + const src = this.src; + const file = await this.requestFile(src); + + // If the src changed since the request started do nothing, otherwise we risk overwriting a subsequent response + if (src !== this.src) { + return; + } + + if (!file.ok) { + this.slError.emit({ status: file.status }); + return; + } + + this.host.innerHTML = file.html; + this.slLoad.emit(); + } + + render() { + return ; + } +}