diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..654981cb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{java,xml}] +indent_size = 4 + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json index 11926505..f20f7bdc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,9 +2,9 @@ "extends": [ "vaadin/typescript-requiring-type-checking", "vaadin/imports-typescript", + "vaadin/lit", "vaadin/prettier", - "vaadin/testing", - "plugin:oxlint/recommended" + "vaadin/testing" ], "parserOptions": { "project": "./tsconfig.json" diff --git a/README.md b/README.md index 59d0d1db..d90ab61f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ # Vaadin Router -[API documentation](https://vaadin.github.io/router/) +[Live demo](https://vaadin.github.io/router/index.html) +| [API documentation](https://vaadin.github.io/router/API/index.html) Vaadin Router is a small and powerful client-side router JS library. It uses the widely adopted express.js syntax for routes (`/users/:id`) to map URLs to Web Component views. All features one might expect from a modern router are supported: async route resolution, animated transitions, navigation guards, redirects, and more. It is framework-agnostic and works equally well with all Web Components regardless of how they are created (Polymer / SkateJS / Stencil / Angular / Vue / etc). @@ -52,24 +53,19 @@ Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https ## Running demos and tests in the browser -1. Fork the `vaadin-router` repository and clone it locally. +1. Fork the `vaadin/router` GitHub repository and clone it locally. 1. Make sure you have [npm](https://www.npmjs.com/) installed. -1. When in the `vaadin-router` directory, run `npm install` and then `npm run install:dependencies` to install dependencies. +1. In the router directory, run `npm install` to install dependencies. -1. Run `npm start`, and open [http://127.0.0.1:8000/components/vaadin-router](http://127.0.0.1:8000/components/vaadin-router) in your browser to see the component API documentation. - -1. You can also open demo or in-browser tests by adding **demo** or **test** to the URL, for example: - - - [http://127.0.0.1:8000/components/vaadin-router/demo](http://127.0.0.1:8000/components/vaadin-router/demo) - - Public API tests: [http://127.0.0.1:8000/components/vaadin-router/test](http://127.0.0.1:8000/components/vaadin-router/test) - - Unit tests: [http://127.0.0.1:8000/components/vaadin-router/test/index.html](http://127.0.0.1:8000/components/vaadin-router/test/index.html) +1. Run `npm run build` to build the library. +1. Run `npm start`, and open [http://localhost:4173](http://127.0.0.1:8000/components/vaadin-router) in your browser to see the component live demos and API documentation. ## Running tests from the command line -1. When in the `vaadin-router` directory, run `npm test` +1. In the router directory, run `npm test` ## Following the coding style diff --git a/demo/.eslintrc.json b/demo/.eslintrc.json index a616cff3..024aab45 100644 --- a/demo/.eslintrc.json +++ b/demo/.eslintrc.json @@ -1,15 +1,10 @@ { - "rules": { - }, - "globals": { - "ElementDemo": false, - "DemoReadyEventEmitter": false, - "Router": false + "extends": ["../.eslintrc.json"], + "root": true, + "parserOptions": { + "project": "./tsconfig.json" }, - "overrides": [{ - "files": ["vaadin-router-code-splitting-demos.html"], - "parserOptions": { - "ecmaVersion": 2017 - } - }] + "rules": { + "@typescript-eslint/unbound-method": "off" + } } diff --git a/demo/@debug/index.html b/demo/@debug/index.html new file mode 100644 index 00000000..e9bf1a5a --- /dev/null +++ b/demo/@debug/index.html @@ -0,0 +1,12 @@ + + + + Debug + + + + + + + + diff --git a/demo/@debug/index.ts b/demo/@debug/index.ts new file mode 100644 index 00000000..ad2f48a7 --- /dev/null +++ b/demo/@debug/index.ts @@ -0,0 +1,22 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +// An URL of the iframe to debug +// eslint-disable-next-line import/order +import url1 from '../url-generation/d4/iframe.html?url'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-debug': DemoDebug; + } +} + +@customElement('vaadin-demo-debug') +export default class DemoDebug extends LitElement { + override render(): TemplateResult { + return html``; + } +} diff --git a/demo/@helpers/common.css b/demo/@helpers/common.css new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/demo/@helpers/common.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/demo/@helpers/common.ts b/demo/@helpers/common.ts new file mode 100644 index 00000000..c58ac5b0 --- /dev/null +++ b/demo/@helpers/common.ts @@ -0,0 +1,3 @@ +import '@vaadin/vaadin-lumo-styles/badge-global.js'; +import '@vaadin/vaadin-lumo-styles/color-global.js'; +import '@vaadin/vaadin-lumo-styles/typography-global.js'; diff --git a/demo/@helpers/iframe.script.ts b/demo/@helpers/iframe.script.ts new file mode 100644 index 00000000..913038f2 --- /dev/null +++ b/demo/@helpers/iframe.script.ts @@ -0,0 +1,33 @@ +import './common.css'; +import './common.js'; + +import { Router } from '@vaadin/router'; + +history.replaceState(null, '', '/'); + +type MessageData = Readonly<{ url: string }>; + +type ParentData = { + source: MessageEventSource | null; + origin: string; +}; + +let parentData: ParentData | undefined; + +function updateParentUrl() { + if (parentData?.source) { + parentData.source.postMessage({ url: location.href }, { targetOrigin: location.origin }); + } +} + +addEventListener('message', ({ data, origin, source }: MessageEvent) => { + if (data != null) { + Router.go(new URL(data.url, location.origin).href); + } else { + parentData = { source, origin }; + } + + updateParentUrl(); +}); + +addEventListener('vaadin-router-location-changed', updateParentUrl); diff --git a/demo/@helpers/nested-styles.css b/demo/@helpers/nested-styles.css new file mode 100644 index 00000000..0f97ee91 --- /dev/null +++ b/demo/@helpers/nested-styles.css @@ -0,0 +1,15 @@ +:host { + display: flex; + flex-direction: column; + background: #ddd; + padding: 5px; + min-height: 100vh; +} + +main { + margin-top: 10px; + padding: 5px; + background: #fff; + flex: 1 0 auto; + min-height: 80px; +} diff --git a/demo/@helpers/page.css b/demo/@helpers/page.css new file mode 100644 index 00000000..52d810c2 --- /dev/null +++ b/demo/@helpers/page.css @@ -0,0 +1,4 @@ +code { + background: var(--code-background); + padding: 0.2em 0.4em; +} diff --git a/demo/@helpers/shared-styles.css b/demo/@helpers/shared-styles.css new file mode 100644 index 00000000..4bb73bf2 --- /dev/null +++ b/demo/@helpers/shared-styles.css @@ -0,0 +1,6 @@ +.note { + background-color: #24c0ea; + padding: 1em; + color: white; + border-radius: 4px; +} diff --git a/demo/@helpers/theme-controller.ts b/demo/@helpers/theme-controller.ts new file mode 100644 index 00000000..40b5c7d6 --- /dev/null +++ b/demo/@helpers/theme-controller.ts @@ -0,0 +1,23 @@ +import type { ReactiveController, ReactiveControllerHost } from 'lit'; + +export default class ThemeController implements ReactiveController { + readonly #host: ReactiveControllerHost; + #controller?: AbortController; + + constructor(host: ReactiveControllerHost) { + (this.#host = host).addController(this); + } + + get value(): string { + return document.documentElement.getAttribute('theme') ?? 'light'; + } + + hostConnected(): void { + this.#controller = new AbortController(); + addEventListener('theme-changed', () => this.#host.requestUpdate(), { signal: this.#controller.signal }); + } + + hostDisconnected(): void { + this.#controller?.abort(); + } +} diff --git a/demo/@helpers/vaadin-demo-code-snippet-file.css b/demo/@helpers/vaadin-demo-code-snippet-file.css new file mode 100644 index 00000000..411e0a5b --- /dev/null +++ b/demo/@helpers/vaadin-demo-code-snippet-file.css @@ -0,0 +1,53 @@ +header { + margin: 1.5em 0 0.5em; + font-size: 0.75rem; + display: flex; + justify-content: space-between; + align-items: baseline; + background: var(--code-file-background); +} + +section { + font-size: 0.75rem; + background: var(--code-background); + padding: .5em; +} + +section > pre { + white-space: pre-wrap; +} + +section > pre > code { + line-break: anywhere; +} + +.title { + font-family: monospace; +} + +.buttons { + clear: right; + float: right; + display: flex; + margin: -0.25em 0; +} + +.buttons > vaadin-button { + margin: 0; + padding: 0; +} + +/* ======= Highlight.js styles ======= */ + +:host([theme~='dark']) { + @nested-import 'highlight.js/styles/atom-one-dark.css'; +} + +:host([theme~='light']) { + @nested-import 'highlight.js/styles/vs.css'; +} + +pre { + margin: 0; + padding: 0.5em 1em; +} diff --git a/demo/@helpers/vaadin-demo-code-snippet-file.ts b/demo/@helpers/vaadin-demo-code-snippet-file.ts new file mode 100644 index 00000000..72a299fd --- /dev/null +++ b/demo/@helpers/vaadin-demo-code-snippet-file.ts @@ -0,0 +1,73 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import '@vaadin/button/src/vaadin-button'; +import '@vaadin/icon/src/vaadin-icon'; +import { type TemplateResult, LitElement, html, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { map } from 'lit/directives/map.js'; +import ThemeController from './theme-controller.js'; +import css from './vaadin-demo-code-snippet-file.css?ctr'; + +export type CodeSnippet = Readonly<{ + id?: string; + code: [original: string, full: TemplateResult, ...rest: TemplateResult[]]; + title?: string; +}>; + +@customElement('vaadin-demo-code-snippet-file') +export default class DemoCodeSnippetFile extends LitElement { + static override styles = [css]; + + @property({ attribute: false }) accessor file: CodeSnippet | undefined; + + @state() accessor #expanded = false; + + readonly #theme = new ThemeController(this); + + override updated(): void { + this.setAttribute('theme', this.#theme.value); + } + + override render(): TemplateResult | typeof nothing { + if (!this.file) { + return nothing; + } + + const [_, full, ...snippets] = this.file.code; + + return html` +
+
${this.file.title}
+
+
+
+ + + + + + +
+ ${this.#expanded + ? html`
${full}
` + : map(snippets, (snippet) => html`
${snippet}
`)} +
+ `; + } + + #toggleExpanded(): void { + this.#expanded = !this.#expanded; + } + + async #copyToClipboard(): Promise { + const [original] = this.file?.code ?? []; + if (original) { + await navigator.clipboard.writeText(original); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-code-snippet-file': DemoCodeSnippetFile; + } +} diff --git a/demo/@helpers/vaadin-demo-code-snippet.css b/demo/@helpers/vaadin-demo-code-snippet.css new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/demo/@helpers/vaadin-demo-code-snippet.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/demo/@helpers/vaadin-demo-code-snippet.ts b/demo/@helpers/vaadin-demo-code-snippet.ts new file mode 100644 index 00000000..bfcd19eb --- /dev/null +++ b/demo/@helpers/vaadin-demo-code-snippet.ts @@ -0,0 +1,41 @@ +/* eslint-disable import/no-duplicates */ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import type { CodeSnippet } from './vaadin-demo-code-snippet-file.js'; +import './vaadin-demo-code-snippet-file.js'; +import css from './vaadin-demo-code-snippet.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-code-snippet': DemoCodeSnippet; + } + + interface WindowEventMap { + 'theme-changed': Event; + } +} + +export type { CodeSnippet }; + +function renderFile(file: CodeSnippet): TemplateResult { + return html``; +} + +@customElement('vaadin-demo-code-snippet') +export default class DemoCodeSnippet extends LitElement { + static override styles = [css]; + + @property({ attribute: false }) accessor files: readonly CodeSnippet[] = []; + + override render(): TemplateResult { + switch (this.files.length) { + case 0: + return html``; + case 1: + return renderFile(this.files[0]); + default: + return html` ${repeat(this.files, ({ id }) => id, renderFile)} `; + } + } +} diff --git a/demo/@helpers/vaadin-demo-layout.css b/demo/@helpers/vaadin-demo-layout.css new file mode 100644 index 00000000..2f696c06 --- /dev/null +++ b/demo/@helpers/vaadin-demo-layout.css @@ -0,0 +1,25 @@ +:host { + --code-file-background: none; +} + +:host([theme~='dark']) { + --code-background: var(--lumo-shade); +} + +:host([theme~='light']) { + --code-background: var(--lumo-shade-5pct); +} + +main { + max-width: 40rem; + margin: 0 auto; + padding: .5rem; +} + +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 1rem; +} diff --git a/demo/@helpers/vaadin-demo-layout.ts b/demo/@helpers/vaadin-demo-layout.ts new file mode 100644 index 00000000..3d5b544c --- /dev/null +++ b/demo/@helpers/vaadin-demo-layout.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import '@vaadin/app-layout'; +import '@vaadin/app-layout/vaadin-drawer-toggle.js'; +import '@vaadin/icon'; +import '@vaadin/icons'; +import '@vaadin/scroller'; +import '@vaadin/side-nav'; +import '@vaadin/side-nav/vaadin-side-nav-item.js'; +import { SignalWatcher } from '@lit-labs/preact-signals'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import './vaadin-presentation.js'; +import css from './vaadin-demo-layout.css?ctr'; + +const colorScheme = window.localStorage.getItem('color-scheme'); +if (colorScheme) { + document.documentElement.setAttribute('theme', colorScheme); +} + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-layout': DemoLayout; + } +} + +@customElement('vaadin-demo-layout') +export default class DemoLayout extends SignalWatcher(LitElement) { + static override styles = [css]; + + @property({ attribute: 'app-title', type: String }) accessor appTitle = ''; + @property({ reflect: true, type: String }) accessor theme = document.documentElement.getAttribute('theme') ?? 'light'; + + override render(): TemplateResult { + return html` + + +
+ +
+ + + Getting Started + Code Splitting + Animated Transitions + Lifecycle Callback + Navigation Trigger + Redirect + Route Actions + Route Parameters + URL Generations + API + + +
`; + } + + #onToggleMode() { + this.theme = this.theme === 'dark' ? 'light' : 'dark'; + window.localStorage.setItem('color-scheme', this.theme); + document.documentElement.setAttribute('theme', this.theme); + dispatchEvent(new Event('theme-changed')); + } +} diff --git a/demo/@helpers/vaadin-presentation-addressbar.css b/demo/@helpers/vaadin-presentation-addressbar.css new file mode 100644 index 00000000..235e7bb6 --- /dev/null +++ b/demo/@helpers/vaadin-presentation-addressbar.css @@ -0,0 +1,9 @@ +:host { + display: flex; + gap: 1rem; + flex: 0 1 0; +} + +:host > :last-child { + flex: 1 0 auto; +} diff --git a/demo/@helpers/vaadin-presentation-addressbar.ts b/demo/@helpers/vaadin-presentation-addressbar.ts new file mode 100644 index 00000000..594b5184 --- /dev/null +++ b/demo/@helpers/vaadin-presentation-addressbar.ts @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import '@vaadin/button'; +import '@vaadin/icon'; +import '@vaadin/icons'; +import '@vaadin/text-field'; +import '@vaadin/tooltip'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import css from './vaadin-presentation-addressbar.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-presentation-addressbar': PresentationAddressbar; + } +} + +function onBack() { + history.back(); +} + +function onForward() { + history.forward(); +} + +@customElement('vaadin-presentation-addressbar') +export class PresentationAddressbar extends LitElement { + static override styles = css; + + @property({ attribute: true, type: String }) accessor url: string | undefined; + + override render(): TemplateResult { + return html` + + + + + + + + + + `; + } + + #onChange(event: Event) { + this.url = (event.target as HTMLInputElement).value; + this.dispatchEvent(new CustomEvent('url-changed', { detail: this.url })); + } +} diff --git a/demo/@helpers/vaadin-presentation.css b/demo/@helpers/vaadin-presentation.css new file mode 100644 index 00000000..a2848cfd --- /dev/null +++ b/demo/@helpers/vaadin-presentation.css @@ -0,0 +1,15 @@ +:host { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +vaadin-presentation-addressbar, +iframe { + width: 100%; + height: 15rem; +} + +iframe { + margin: 0; +} diff --git a/demo/@helpers/vaadin-presentation.ts b/demo/@helpers/vaadin-presentation.ts new file mode 100644 index 00000000..8e8c0603 --- /dev/null +++ b/demo/@helpers/vaadin-presentation.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { html, LitElement, type PropertyValues, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import css from './vaadin-presentation.css?ctr'; +import './vaadin-presentation-addressbar.js'; +import './vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-presentation': Presentation; + } +} + +type MessageData = Readonly<{ + url: string; +}>; + +@customElement('vaadin-presentation') +export default class Presentation extends LitElement { + static override styles = css; + + @property() accessor src: string | undefined; + @property({ attribute: true, type: String }) accessor url: string | undefined; + #controller?: AbortController; + #window?: Window; + + override connectedCallback(): void { + super.connectedCallback(); + this.#controller = new AbortController(); + } + + override disconnectedCallback(): void { + super.disconnectedCallback(); + this.#controller?.abort(); + } + + override firstUpdated(): void { + if (this.#controller) { + addEventListener( + 'message', + ({ data, origin, source }: MessageEvent) => { + if (origin === location.origin && source === this.#window) { + this.url = new URL(data.url).pathname; + } + }, + { signal: this.#controller.signal }, + ); + } + } + + changedProperties(map: PropertyValues): void { + if (map.has('url')) { + this.#window?.postMessage({ url: this.url }, '*'); + } + } + + override render(): TemplateResult { + return html` + + `; + } + + #onUrlChanged(event: CustomEvent) { + this.url = new URL(event.detail, location.origin).pathname; + this.#window?.postMessage({ url: this.url }, '*'); + } +} diff --git a/demo/@helpers/x-breadcrumbs.ts b/demo/@helpers/x-breadcrumbs.ts new file mode 100644 index 00000000..f5180bf0 --- /dev/null +++ b/demo/@helpers/x-breadcrumbs.ts @@ -0,0 +1,41 @@ +import { html, LitElement, nothing, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { map } from 'lit/directives/map.js'; +import css from './common.css?ctr'; + +export type Breadcrumb = Readonly<{ + title: string; + href: string; +}>; + +@customElement('x-breadcrumbs') +export class Breadcrumbs extends LitElement { + static override styles = css; + + @property({ type: Array }) accessor items: readonly Breadcrumb[] = []; + + // eslint-disable-next-line @typescript-eslint/class-methods-use-this + #isNotLastIndexOf(items: readonly Breadcrumb[], i: number): boolean { + return i < items.length - 1; + } + + override render(): TemplateResult { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'x-breadcrumbs': Breadcrumbs; + } +} diff --git a/demo/@helpers/x-home-view.ts b/demo/@helpers/x-home-view.ts new file mode 100644 index 00000000..4546de8e --- /dev/null +++ b/demo/@helpers/x-home-view.ts @@ -0,0 +1,19 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import css from './common.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'x-home-view': HomeView; + } +} + +@customElement('x-home-view') +export default class HomeView extends LitElement { + static override styles = css; + + override render(): TemplateResult { + return html`

Home

+ `; + } +} diff --git a/demo/@helpers/x-image-view.css b/demo/@helpers/x-image-view.css new file mode 100644 index 00000000..e579dacd --- /dev/null +++ b/demo/@helpers/x-image-view.css @@ -0,0 +1,5 @@ +.img-view { + width: var(--x-image-view-width, 100%); + height: var(--x-image-view-height, 100%); + background-color: var(--x-image-view-background-color, hotpink); +} diff --git a/demo/@helpers/x-image-view.ts b/demo/@helpers/x-image-view.ts new file mode 100644 index 00000000..9bc9f131 --- /dev/null +++ b/demo/@helpers/x-image-view.ts @@ -0,0 +1,33 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import type { RouterLocation } from '../../src/index.js'; +import commonCss from './common.css?ctr'; +import css from './x-image-view.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'x-image-view': ImageView; + } +} + +@customElement('x-image-view') +export default class ImageView extends LitElement { + static override styles = [commonCss, css]; + + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + const size = this.location?.params.size as number | undefined; + const color = this.location?.params.color as string | undefined; + + return html`
`; + } +} diff --git a/demo/@helpers/x-knowledge-base.ts b/demo/@helpers/x-knowledge-base.ts new file mode 100644 index 00000000..037e6925 --- /dev/null +++ b/demo/@helpers/x-knowledge-base.ts @@ -0,0 +1,18 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +@customElement('x-knowledge-base') +export class KnowledgeBase extends LitElement implements WebComponentInterface { + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html` Knowledge base path: '${this.location?.params.path ?? 'unknown'}' `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'x-knowledge-base': KnowledgeBase; + } +} diff --git a/demo/@helpers/x-login-view.ts b/demo/@helpers/x-login-view.ts new file mode 100644 index 00000000..4076def4 --- /dev/null +++ b/demo/@helpers/x-login-view.ts @@ -0,0 +1,33 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import css from './common.css?ctr'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +@customElement('x-login-view') +export default class LoginView extends LitElement implements WebComponentInterface { + static override styles = [css]; + + location?: RouterLocation; + + override render(): TemplateResult { + return html` +

Login Form

+ + `; + } + + #login() { + window.authorized = true; + dispatchEvent( + new CustomEvent('vaadin-router-go', { + detail: { pathname: this.location?.params.to ?? '/' }, + }), + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'x-login-view': LoginView; + } +} diff --git a/demo/@helpers/x-not-found-view.ts b/demo/@helpers/x-not-found-view.ts new file mode 100644 index 00000000..94e54495 --- /dev/null +++ b/demo/@helpers/x-not-found-view.ts @@ -0,0 +1,21 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import css from './common.css?ctr'; + +@customElement('x-not-found-view') +export class NotFoundView extends LitElement { + static override styles = css; + + override render(): TemplateResult { + return html` +

404

+

View not found

+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'x-not-found-view': NotFoundView; + } +} diff --git a/demo/@helpers/x-profile-view.ts b/demo/@helpers/x-profile-view.ts new file mode 100644 index 00000000..72495c9c --- /dev/null +++ b/demo/@helpers/x-profile-view.ts @@ -0,0 +1,21 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import css from './common.css?ctr'; +import type { RouterLocation } from '@vaadin/router'; + +declare global { + interface HTMLElementTagNameMap { + 'x-profile-view': ProfileView; + } +} + +@customElement('x-profile-view') +export default class ProfileView extends LitElement { + static override styles = [css]; + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html`User ID: ${this.location?.params.id ?? 'unknown'}
+ /user or /users: ${this.location?.params[0] ?? 'unknown'}`; + } +} diff --git a/demo/@helpers/x-user-list.ts b/demo/@helpers/x-user-list.ts new file mode 100644 index 00000000..0de63477 --- /dev/null +++ b/demo/@helpers/x-user-list.ts @@ -0,0 +1,22 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import css from './common.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'x-user-list': UserList; + } +} + +@customElement('x-user-list') +export default class UserList extends LitElement { + static override styles = css; + override render(): TemplateResult { + return html`

Users

+ `; + } +} diff --git a/demo/@helpers/x-user-not-found-view.ts b/demo/@helpers/x-user-not-found-view.ts new file mode 100644 index 00000000..0160b9d0 --- /dev/null +++ b/demo/@helpers/x-user-not-found-view.ts @@ -0,0 +1,24 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import css from './common.css?ctr'; +import type { RouterLocation } from '@vaadin/router'; + +@customElement('x-user-not-found-view') +export class UserNotFoundView extends LitElement { + @property({ type: Object }) accessor location: RouterLocation | undefined; + + static override styles = [css]; + + override render(): TemplateResult { + return html` +

The princess is in another castle

+

You've come to ${this.location?.pathname}, but alas, there is nothing there.

+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'x-user-not-found-view': UserNotFoundView; + } +} diff --git a/demo/@helpers/x-user-numeric-view.ts b/demo/@helpers/x-user-numeric-view.ts new file mode 100644 index 00000000..7ae5f13e --- /dev/null +++ b/demo/@helpers/x-user-numeric-view.ts @@ -0,0 +1,21 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import css from './common.css?ctr'; +import type { RouterLocation } from '@vaadin/router'; + +declare global { + interface HTMLElementTagNameMap { + 'x-user-numeric-view': UserNumericView; + } +} + +@customElement('x-user-numeric-view') +export default class UserNumericView extends LitElement { + static override styles = [css]; + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html`

User Profile

+

ID: ${this.location?.params[0] ?? 'unknown'}

`; + } +} diff --git a/demo/@helpers/x-user-profile.ts b/demo/@helpers/x-user-profile.ts new file mode 100644 index 00000000..f893264c --- /dev/null +++ b/demo/@helpers/x-user-profile.ts @@ -0,0 +1,22 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import css from './common.css?ctr'; +import type { RouterLocation } from '@vaadin/router'; + +declare global { + interface HTMLElementTagNameMap { + 'x-user-profile': UserProfile; + } +} + +@customElement('x-user-profile') +export default class UserProfile extends LitElement { + static override styles = css; + + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html`

User Profile

+

Name: ${this.location?.params.user ?? 'unknown'}

`; + } +} diff --git a/demo/animated-transitions/d1/iframe.html b/demo/animated-transitions/d1/iframe.html new file mode 100644 index 00000000..96512f58 --- /dev/null +++ b/demo/animated-transitions/d1/iframe.html @@ -0,0 +1,16 @@ + + + + Animated Transitions + + + + + + Home + Image + Users +
+ + + diff --git a/demo/animated-transitions/d1/script.ts b/demo/animated-transitions/d1/script.ts new file mode 100644 index 00000000..54749f0e --- /dev/null +++ b/demo/animated-transitions/d1/script.ts @@ -0,0 +1,22 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-image-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { + path: '/', + animate: true, + children: [ + { path: '', component: 'x-home-view' }, + { path: '/image-:size(\\d+)px', component: 'x-image-view' }, + { path: '/users', component: 'x-user-list' }, + { path: '/users/:user', component: 'x-user-profile' }, + ], + }, +]); +// end::snippet[] diff --git a/demo/animated-transitions/d1/styles.css b/demo/animated-transitions/d1/styles.css new file mode 100644 index 00000000..b4c549fe --- /dev/null +++ b/demo/animated-transitions/d1/styles.css @@ -0,0 +1,27 @@ +#outlet > .leaving { + animation: 1s fadeOut ease-in-out; +} + +#outlet > .entering { + animation: 1s fadeIn linear; +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} diff --git a/demo/animated-transitions/d2/iframe.html b/demo/animated-transitions/d2/iframe.html new file mode 100644 index 00000000..663ca43d --- /dev/null +++ b/demo/animated-transitions/d2/iframe.html @@ -0,0 +1,13 @@ + + + + Animated Transitions + + + + + +
+ + + diff --git a/demo/animated-transitions/d2/script.ts b/demo/animated-transitions/d2/script.ts new file mode 100644 index 00000000..3150e548 --- /dev/null +++ b/demo/animated-transitions/d2/script.ts @@ -0,0 +1,33 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; +import './x-wrapper.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { + path: '/', + component: 'x-wrapper', + children: [ + { + path: '/users', + animate: { + enter: 'users-entering', + leave: 'users-leaving', + }, + children: [ + { path: '', component: 'x-user-list' }, + { + path: '/:user', + component: 'x-user-profile', + animate: true, + }, + ], + }, + { path: '(.*)', redirect: '/users' }, + ], + }, +]); +// end::snippet[] diff --git a/demo/animated-transitions/d2/styles.css b/demo/animated-transitions/d2/styles.css new file mode 100644 index 00000000..e50a18a3 --- /dev/null +++ b/demo/animated-transitions/d2/styles.css @@ -0,0 +1,57 @@ +.leaving { + animation: 1s slideOutDown ease-in-out; +} + +.entering { + animation: 1s slideInDown linear; +} + +.users-entering { + animation: 0.5s fadeIn ease-in; +} + +.users-leaving { + animation: 0.5s fadeOut linear; +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideInDown { + from { + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideOutDown { + from { + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + transform: translate3d(0, 100%, 0); + } +} diff --git a/demo/animated-transitions/d2/x-wrapper.ts b/demo/animated-transitions/d2/x-wrapper.ts new file mode 100644 index 00000000..a861499a --- /dev/null +++ b/demo/animated-transitions/d2/x-wrapper.ts @@ -0,0 +1,28 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import sharedCss from '@helpers/shared-styles.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'x-wrapper': Wrapper; + } +} + +// tag::snippet[] +@customElement('x-wrapper') +export default class Wrapper extends LitElement { + static override styles = sharedCss; + + override render(): TemplateResult { + return html` +
+ +
`; + } +} +// end::snippet[] diff --git a/demo/animated-transitions/index.html b/demo/animated-transitions/index.html new file mode 100644 index 00000000..c24e1890 --- /dev/null +++ b/demo/animated-transitions/index.html @@ -0,0 +1,13 @@ + + + + Animated Transitions + + + + + + + + + diff --git a/demo/animated-transitions/index.ts b/demo/animated-transitions/index.ts new file mode 100644 index 00000000..bd3a5279 --- /dev/null +++ b/demo/animated-transitions/index.ts @@ -0,0 +1,113 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; +import cssCode1 from './d1/styles.css?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; +import cssCode2 from './d2/styles.css?snippet'; +import wrapperCode from './d2/x-wrapper.ts?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-animated-transitions': DemoAnimatedTransitions; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.ts', + }, + { + id: 'css', + code: cssCode1, + title: 'styles.css', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script.ts', + }, + { + id: 'css', + code: cssCode2, + title: 'styles.css', + }, + { + id: 'wrapper', + code: wrapperCode, + title: 'x-wrapper.ts', + }, +]; + +@customElement('vaadin-demo-animated-transitions') +export default class DemoAnimatedTransitions extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

+ Vaadin Router allows you to animate transitions between routes. In order to add an animation, do the next steps: +

+
    +
  1. update the router config: add the animate property set to true
  2. +
  3. add @keyframes animations, either in the view Web Component styles or in outside CSS
  4. +
  5. apply CSS for .leaving and .entering classes to use the animations
  6. +
+

+ The demo below illustrates how to add the transition between all the routes in the same group. You might also + add the transition for the specific routes only, by setting the animate + property on the corresponding route config objects. +

+ + + +

To run the animated transition, Vaadin Router performs the actions in the following order:

+
    +
  1. render the new view component to the outlet content
  2. +
  3. set the entering CSS class on the new view component
  4. +
  5. set the leaving CSS class on the old view component, if any
  6. +
  7. check if some @keyframes animation applies, and wait for it to complete
  8. +
  9. remove the old view component from the outlet content
  10. +
  11. continue the remaining navigation steps as usual
  12. +
+

Customize CSS Classes

+

+ In the basic use case, using single type of the animated transition could be enough to make the web app looking + great, but often we need to configure it depending on the route. Vaadin Router supports this feature by setting + object value to animate property, with the enter and leave string keys. + Their values are used for setting CSS classes to be set on the views. +

+

+ Note that you can first configure animated transition for the group of routes, and then override it for the + single route. In particular, you can switch back to using default CSS classes, as shown in the demo below. +

+ + + `; + } +} diff --git a/demo/code-splitting/d1/iframe.html b/demo/code-splitting/d1/iframe.html new file mode 100644 index 00000000..b988d8e3 --- /dev/null +++ b/demo/code-splitting/d1/iframe.html @@ -0,0 +1,14 @@ + + + + Code Splitting + + + + + Home + User Profile +
+ + + diff --git a/demo/code-splitting/d1/script.ts b/demo/code-splitting/d1/script.ts new file mode 100644 index 00000000..95e782b7 --- /dev/null +++ b/demo/code-splitting/d1/script.ts @@ -0,0 +1,17 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { + path: '/user/:id', + async action() { + await import(`./user.bundle.js`); + }, + component: 'x-user-js-bundle-view', // <-- defined in the bundle + }, +]); +// end::snippet[] diff --git a/demo/code-splitting/d1/user.bundle.ts b/demo/code-splitting/d1/user.bundle.ts new file mode 100644 index 00000000..5c2dddc8 --- /dev/null +++ b/demo/code-splitting/d1/user.bundle.ts @@ -0,0 +1,22 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouterLocation } from '../../../src/index.js'; +import css from '@helpers/common.css?ctr'; + +declare global { + interface HTMLElementTagNameMap { + 'x-user-js-bundle-view': UserJsBundleView; + } +} + +// tag::snippet[] +@customElement('x-user-js-bundle-view') +export default class UserJsBundleView extends LitElement { + static override styles = css; + @property({ attribute: false }) accessor location: RouterLocation | undefined; + override render(): TemplateResult { + return html`

User JS Bundle

+

User id: ${this.location?.params.id}. This view was loaded using JS bundle.

`; + } +} +// end::snippet[] diff --git a/demo/code-splitting/d2/iframe.html b/demo/code-splitting/d2/iframe.html new file mode 100644 index 00000000..3ea59ba0 --- /dev/null +++ b/demo/code-splitting/d2/iframe.html @@ -0,0 +1,14 @@ + + + + Code Splitting + + + + + Home + Users +
+ + + diff --git a/demo/code-splitting/d2/script.ts b/demo/code-splitting/d2/script.ts new file mode 100644 index 00000000..417dd390 --- /dev/null +++ b/demo/code-splitting/d2/script.ts @@ -0,0 +1,19 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-image-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { + path: '/users', + async children() { + return await import('./user-routes.js').then((mod) => mod.default); + }, + }, +]); +// end::snippet[] diff --git a/demo/code-splitting/d2/user-routes.ts b/demo/code-splitting/d2/user-routes.ts new file mode 100644 index 00000000..7d85c4a5 --- /dev/null +++ b/demo/code-splitting/d2/user-routes.ts @@ -0,0 +1,12 @@ +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import type { Route } from '@vaadin/router'; + +// tag::snippet[] +const usersRoutes: readonly Route[] = [ + { path: '/', component: 'x-user-list' }, + { path: '/:user', component: 'x-user-profile' }, +]; +// end::snippet[] + +export default usersRoutes; diff --git a/demo/code-splitting/index.html b/demo/code-splitting/index.html new file mode 100644 index 00000000..57bd5d79 --- /dev/null +++ b/demo/code-splitting/index.html @@ -0,0 +1,12 @@ + + + + Code Splitting + + + + + + + + diff --git a/demo/code-splitting/index.ts b/demo/code-splitting/index.ts new file mode 100644 index 00000000..67200866 --- /dev/null +++ b/demo/code-splitting/index.ts @@ -0,0 +1,111 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; +import bundleCode from './d1/user.bundle.js?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; +import userRoutesCode from './d2/user-routes.js?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-code-splitting': DemoCodeSplitting; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'iframe1.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script1.ts', + }, + { + id: 'bundle', + code: bundleCode, + title: 'user-bundle.ts', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'iframe2.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script2.ts', + }, + { + id: 'routes', + code: userRoutesCode, + title: 'user-routes.ts', + }, +]; + +@customElement('vaadin-demo-code-splitting') +export default class DemoCodeSplitting extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Using Dynamic Imports

+

+ Vaadin Router allows you to implement your own loading mechanism for bundles using custom + Route Actions. In that case, you can use + dynamic imports. +

+

+ Note: If the dynamically loaded route has lifecycle callbacks, the action should return a promise that resolves + only when the route component is loaded (like in the example below). Otherwise the lifecycle callbacks on the + dynamically loaded route's web component are not called. +

+ + + +

+ If dynamic imports are used both for parent and child routes, then the example above may possibly slow down + rendering because router would not start importing a child component until its parent is imported. +

+ +

Splitting and Lazy-Loading the Routes Configuration

+

+ Vaadin Router supports splitting the routes configuration object into parts and lazily loading them on-demand, + enabling developers to create non-monolithic app structures. This might be useful for implementing a distributed + sub routes configuration within a big project, so that multiple teams working on different parts of the app no + longer have to merge their changes into the same file. +

+

+ The children property on the route config object can be set to a function, which returns an array + of the route objects. It may return a Promise, which allows to dynamically import the configuration file, and return the children array exported from it. +

+

+ See the API documentation for detailed description of the + children callback function. +

+ + + `; + } +} diff --git a/demo/demo-elements/bundle-script.js b/demo/demo-elements/bundle-script.js deleted file mode 100644 index a03648e1..00000000 --- a/demo/demo-elements/bundle-script.js +++ /dev/null @@ -1 +0,0 @@ -window.bundleScriptTestVariable = 'Hello from bundle script!'; diff --git a/demo/demo-elements/user.bundle.html b/demo/demo-elements/user.bundle.html deleted file mode 100644 index e815c900..00000000 --- a/demo/demo-elements/user.bundle.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - diff --git a/demo/demo-elements/user.bundle.js b/demo/demo-elements/user.bundle.js deleted file mode 100644 index f7c20bc0..00000000 --- a/demo/demo-elements/user.bundle.js +++ /dev/null @@ -1,17 +0,0 @@ -(() => { - class XUserJsBundleView extends Polymer.Element { - static get is() { - return 'x-user-js-bundle-view'; - } - static get template() { - const tpl = document.createElement('template'); - tpl.innerHTML = ` -

User Profile

-

User id: [[location.params.id]]. This view was loaded using JS bundle.

- `; - return tpl; - } - } - - customElements.define(XUserJsBundleView.is, XUserJsBundleView); -})(); diff --git a/demo/demo-elements/users-routes.js b/demo/demo-elements/users-routes.js deleted file mode 100644 index 1b43034e..00000000 --- a/demo/demo-elements/users-routes.js +++ /dev/null @@ -1,14 +0,0 @@ -(() => { - window.Vaadin = window.Vaadin || {}; - Vaadin.Demo = Vaadin.Demo || {}; - Vaadin.Demo.moduleStorage = Vaadin.Demo.moduleStorage || []; - - const usersRoutes = [ - {path: '/', component: 'x-user-home'}, - {path: '/:user', component: 'x-user-profile'}, - ]; - - Vaadin.Demo.moduleStorage.push({ - default: usersRoutes - }); -})(); diff --git a/demo/demo-elements/x-breadcrumbs.html b/demo/demo-elements/x-breadcrumbs.html deleted file mode 100644 index 8b214910..00000000 --- a/demo/demo-elements/x-breadcrumbs.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - diff --git a/demo/demo-elements/x-home-view.html b/demo/demo-elements/x-home-view.html deleted file mode 100644 index b6ea3644..00000000 --- a/demo/demo-elements/x-home-view.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-image-view.html b/demo/demo-elements/x-image-view.html deleted file mode 100644 index 85d042ca..00000000 --- a/demo/demo-elements/x-image-view.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-knowledge-base.html b/demo/demo-elements/x-knowledge-base.html deleted file mode 100644 index cfdd0efa..00000000 --- a/demo/demo-elements/x-knowledge-base.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-login-view.html b/demo/demo-elements/x-login-view.html deleted file mode 100644 index e283819d..00000000 --- a/demo/demo-elements/x-login-view.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-not-found-view.html b/demo/demo-elements/x-not-found-view.html deleted file mode 100644 index 9a2edb13..00000000 --- a/demo/demo-elements/x-not-found-view.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-profile-view.html b/demo/demo-elements/x-profile-view.html deleted file mode 100644 index 25485307..00000000 --- a/demo/demo-elements/x-profile-view.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-user-list.html b/demo/demo-elements/x-user-list.html deleted file mode 100644 index cdb86852..00000000 --- a/demo/demo-elements/x-user-list.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-user-not-found-view.html b/demo/demo-elements/x-user-not-found-view.html deleted file mode 100644 index b87ed8b6..00000000 --- a/demo/demo-elements/x-user-not-found-view.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-user-numeric-view.html b/demo/demo-elements/x-user-numeric-view.html deleted file mode 100644 index f4d02e4a..00000000 --- a/demo/demo-elements/x-user-numeric-view.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/demo/demo-elements/x-user-profile.html b/demo/demo-elements/x-user-profile.html deleted file mode 100644 index 9b2cb197..00000000 --- a/demo/demo-elements/x-user-profile.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/demo/demo-shell.html b/demo/demo-shell.html deleted file mode 100644 index 2470a30c..00000000 --- a/demo/demo-shell.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/demo/demos.json b/demo/demos.json deleted file mode 100644 index f357fd5e..00000000 --- a/demo/demos.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "name": "Vaadin Router", - "pages": [ - { - "name": "Getting Started", - "url": "vaadin-router-getting-started-demos", - "src": "vaadin-router-getting-started-demos.html", - "meta": { - "title": "Vaadin Router Getting Started Examples", - "description": "", - "image": "" - } - }, - { - "name": "Route Parameters", - "url": "vaadin-router-route-parameters-demos", - "src": "vaadin-router-route-parameters-demos.html", - "meta": { - "title": "Vaadin Router Route Parameters Examples", - "description": "", - "image": "" - } - }, - { - "name": "URL Generation", - "url": "vaadin-router-url-generation-demos", - "src": "vaadin-router-url-generation-demos.html", - "meta": { - "title": "Vaadin Router URL Generation Examples", - "description": "", - "image": "" - } - }, - { - "name": "Redirects", - "url": "vaadin-router-redirect-demos", - "src": "vaadin-router-redirect-demos.html", - "meta": { - "title": "Vaadin Router Redirect Examples", - "description": "", - "image": "" - } - }, - { - "name": "Lifecycle Callbacks", - "url": "vaadin-router-lifecycle-callbacks-demos", - "src": "vaadin-router-lifecycle-callbacks-demos.html", - "meta": { - "title": "Vaadin Router Lifecycle Callbacks Examples", - "description": "", - "image": "" - } - }, - { - "name": "Code Splitting", - "url": "vaadin-router-code-splitting-demos", - "src": "vaadin-router-code-splitting-demos.html", - "meta": { - "title": "Vaadin Router Code Splitting Examples", - "description": "", - "image": "" - } - }, - { - "name": "Animations", - "url": "vaadin-router-animated-transitions-demos", - "src": "vaadin-router-animated-transitions-demos.html", - "meta": { - "title": "Vaadin Router Transitions Examples", - "description": "", - "image": "" - } - }, - { - "name": "Route Actions", - "url": "vaadin-router-route-actions-demos", - "src": "vaadin-router-route-actions-demos.html", - "meta": { - "title": "Vaadin Router Route Actions Examples", - "description": "", - "image": "" - } - }, - { - "name": "Navigation Triggers", - "url": "vaadin-router-navigation-trigger-demos", - "src": "vaadin-router-navigation-trigger-demos.html", - "meta": { - "title": "Vaadin Router Navigation Trigger Examples", - "description": "", - "image": "" - } - } - ] -} diff --git a/demo/element-demo.html b/demo/element-demo.html deleted file mode 100644 index 6c97f3a2..00000000 --- a/demo/element-demo.html +++ /dev/null @@ -1,17 +0,0 @@ - - diff --git a/demo/getting-started/d1/iframe.html b/demo/getting-started/d1/iframe.html new file mode 100644 index 00000000..6c4e9da5 --- /dev/null +++ b/demo/getting-started/d1/iframe.html @@ -0,0 +1,14 @@ + + + + Getting Started + + + + + Home + User Profile +
+ + + diff --git a/demo/getting-started/d1/script.ts b/demo/getting-started/d1/script.ts new file mode 100644 index 00000000..755c295e --- /dev/null +++ b/demo/getting-started/d1/script.ts @@ -0,0 +1,16 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-not-found-view.js'; +import '@helpers/x-not-found-view.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users', component: 'x-user-list' }, + { path: '/users/(.*)', component: 'x-user-not-found-view' }, + { path: '(.*)', component: 'x-not-found-view' }, +]); +// end::snippet[] diff --git a/demo/getting-started/index.html b/demo/getting-started/index.html new file mode 100644 index 00000000..7e06dd80 --- /dev/null +++ b/demo/getting-started/index.html @@ -0,0 +1,12 @@ + + + + Getting Started + + + + + + + + diff --git a/demo/getting-started/index.ts b/demo/getting-started/index.ts new file mode 100644 index 00000000..48554ab3 --- /dev/null +++ b/demo/getting-started/index.ts @@ -0,0 +1,115 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; +import htmlSnippet1 from './snippets/s1.html?snippet'; +import tsSnippet1 from './snippets/s2.ts?snippet'; +import tsSnippet2 from './snippets/s4.ts?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-getting-started': DemoGettingStarted; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.ts', + }, +]; + +@customElement('vaadin-demo-getting-started') +export default class DemoGettingStarted extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

The Router class

+

+ The Router class is the only thing you need to get started with Vaadin Router. Depending on your + project setup, there are several ways to access it. +

+

+ In modern browsers that support ES modules the Router class can be imported + directly into a script tag on a page: +

+ +

+ In Vite / Webpack / Rollup projects the Router class can be imported from the + @vaadin/router npm package: +

+ + +

Getting Started

+

+ Vaadin Router automatically listens to navigation events and asynchronously renders a matching Web Component + into the given DOM node (a.k.a. the router outlet). By default, navigation events are triggered by + popstate events on the window, and by click events on + <a> elements on the page. +

+

+ The routes config maps URL paths to Web Components. Vaadin Router goes through the routes until it finds the + first match, creates an instance of the route component, and inserts it into the router outlet (replacing any + pre-existing outlet content). For details on the route path syntax see the + Route Parameters demos. +

+ + + +

+ Route components can be any Web Components regardless of how they are built: vanilla JavaScript, Lit, Stencil, + SkateJS, Angular, Vue, etc. +

+

+ Vaadin Router follows the lifecycle callbacks convention described in + WebComponentInterface: if a route component defines any + of these callback methods, Vaadin Router will call them at the appropriate points in the navigation lifecycle. + See the Lifecycle Callbacks section for more details. +

+

+ In addition to that Vaadin Router also sets a + location property on every route Web Component so + that you could access the current location details via an instance property (e.g. + this.location.pathname). +

+ +

Using this.location

+

+ For LitElement and TypeScript a declaration in the component is required. Declare the + location property in the class and initialize it from the router Vaadin Router + instance: +

+ +

This property is automatically updated on navigation.

+ +

Fallback Routes (404)

+

+ If Vaadin Router does not find a matching route, the promise returned from the render() method gets + rejected, and any content in the router outlet is removed. In order to show a user-friendly 'page not found' + view, a fallback route with a wildcard '(.*)' path can be added to the end of the + routes list. +

+

+ There can be different fallbacks for different route prefixes, but since the route resolution is based on the + first match, the fallback route should always be after other alternatives. +

+

+ The path that leads to the fallback route is available to the route component via the + location.pathname property. +

`; + } +} diff --git a/demo/getting-started/snippets/s1.html b/demo/getting-started/snippets/s1.html new file mode 100644 index 00000000..f101bf7b --- /dev/null +++ b/demo/getting-started/snippets/s1.html @@ -0,0 +1,13 @@ + + + + Snippets + + + + + + + diff --git a/demo/getting-started/snippets/s2.ts b/demo/getting-started/snippets/s2.ts new file mode 100644 index 00000000..9b1b2358 --- /dev/null +++ b/demo/getting-started/snippets/s2.ts @@ -0,0 +1,5 @@ +// tag::snippet[] +import { Router } from '@vaadin/router'; +// end::snippet[] + +export const router = new Router(); diff --git a/demo/getting-started/snippets/s4.ts b/demo/getting-started/snippets/s4.ts new file mode 100644 index 00000000..99e666f4 --- /dev/null +++ b/demo/getting-started/snippets/s4.ts @@ -0,0 +1,22 @@ +/* eslint-disable import/order, import/no-duplicates */ +// tag::snippet[] +import type { RouterLocation } from '@vaadin/router'; +import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { router } from './s2.js'; + +@customElement('my-view') +class MyViewElement extends LitElement { + @property({ type: Object }) accessor location: RouterLocation = router.location; + + override render() { + return html`Current location URL: ${this.location.getUrl()}`; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'my-view': MyViewElement; + } +} diff --git a/demo/iframe.html b/demo/iframe.html deleted file mode 100644 index 24cc5d38..00000000 --- a/demo/iframe.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - Vaadin Router Demo - - - - - - - - - - - - - - - Waiting for a 'set-template' message... - - - diff --git a/demo/index.html b/demo/index.html index 49101782..ff6a268e 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,18 +1,10 @@ - - - - - Vaadin Router Demos - - - - - - - - - - - + + + + diff --git a/demo/index.ts b/demo/index.ts new file mode 100644 index 00000000..ba95da66 --- /dev/null +++ b/demo/index.ts @@ -0,0 +1,6 @@ +import '@vaadin/vaadin-lumo-styles/all-imports.js'; +import '@helpers/vaadin-demo-layout.js'; + +if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.setAttribute('theme', 'dark'); +} diff --git a/demo/lifecycle-callback/d1/iframe.html b/demo/lifecycle-callback/d1/iframe.html new file mode 100644 index 00000000..3913891c --- /dev/null +++ b/demo/lifecycle-callback/d1/iframe.html @@ -0,0 +1,14 @@ + + + + Lifecycle Callback + + + + + Home + Are you ready? +
+ + + diff --git a/demo/lifecycle-callback/d1/script.ts b/demo/lifecycle-callback/d1/script.ts new file mode 100644 index 00000000..ba287643 --- /dev/null +++ b/demo/lifecycle-callback/d1/script.ts @@ -0,0 +1,12 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import { Router } from '@vaadin/router'; +import './x-countdown.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/go', component: 'x-countdown' }, +]); +// end::snippet[] diff --git a/demo/lifecycle-callback/d1/x-countdown.ts b/demo/lifecycle-callback/d1/x-countdown.ts new file mode 100644 index 00000000..07cbdb70 --- /dev/null +++ b/demo/lifecycle-callback/d1/x-countdown.ts @@ -0,0 +1,52 @@ +import { html, LitElement, render, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import type { RouterLocation, WebComponentInterface } from '../../../src/types.t.js'; + +// tag::snippet[] +@customElement('x-countdown') +export default class Countdown extends LitElement implements WebComponentInterface { + readonly #home = document.body.querySelector('x-home-view'); + #count = 0; + #timer?: ReturnType; + + override render(): TemplateResult { + return html`

Go-go-go!

`; + } + + async onBeforeEnter(_: RouterLocation): Promise { + this.#count = 3; + this.#tick(); + return await new Promise((resolve) => { + this.#timer = setInterval(() => { + if (this.#count < 0) { + this.#clear(); + resolve(); + } else { + this.#tick(); + } + }, 500); + }); + } + + #tick(): void { + if (this.#home) { + render(html`

${this.#count}

`, this.#home); + } + + this.#count -= 1; + } + + #clear(): void { + if (this.#home) { + render(html``, this.#home); + } + clearInterval(this.#timer); + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-countdown': Countdown; + } +} diff --git a/demo/lifecycle-callback/d2/iframe.html b/demo/lifecycle-callback/d2/iframe.html new file mode 100644 index 00000000..238591d4 --- /dev/null +++ b/demo/lifecycle-callback/d2/iframe.html @@ -0,0 +1,14 @@ + + + + Lifecycle Callback + + + + + Home + Meet a friend +
+ + + diff --git a/demo/lifecycle-callback/d2/script.ts b/demo/lifecycle-callback/d2/script.ts new file mode 100644 index 00000000..c17a8f08 --- /dev/null +++ b/demo/lifecycle-callback/d2/script.ts @@ -0,0 +1,12 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import { Router } from '@vaadin/router'; +import './x-friend.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/friend', component: 'x-friend' }, +]); +// end::snippet[] diff --git a/demo/lifecycle-callback/d2/x-friend.ts b/demo/lifecycle-callback/d2/x-friend.ts new file mode 100644 index 00000000..841cd88a --- /dev/null +++ b/demo/lifecycle-callback/d2/x-friend.ts @@ -0,0 +1,28 @@ +import { css, html, LitElement, render, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import type { RouterLocation, WebComponentInterface } from '../../../src/types.t.js'; + +// tag::snippet[] +@customElement('x-friend') +export default class Friend extends LitElement implements WebComponentInterface { + static override styles = css` + ::slotted(h2) { + color: red !important; + } + `; + + override render(): TemplateResult { + return html`
`; + } + + onAfterEnter(_: RouterLocation): void { + render(html`

I am here!

`, this); + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-friend': Friend; + } +} diff --git a/demo/lifecycle-callback/d3/iframe.html b/demo/lifecycle-callback/d3/iframe.html new file mode 100644 index 00000000..0a539f79 --- /dev/null +++ b/demo/lifecycle-callback/d3/iframe.html @@ -0,0 +1,12 @@ + + + + Lifecycle Callback + + + + +
+ + + diff --git a/demo/lifecycle-callback/d3/script.ts b/demo/lifecycle-callback/d3/script.ts new file mode 100644 index 00000000..f862f7bb --- /dev/null +++ b/demo/lifecycle-callback/d3/script.ts @@ -0,0 +1,13 @@ +import '@helpers/iframe.script.js'; +import { Router } from '@vaadin/router'; +import './x-user-deleted.js'; +import './x-user-manage.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', redirect: '/user/guest/manage' }, + { path: '/user/:user/manage', component: 'x-user-manage' }, + { path: '/user/delete', component: 'x-user-deleted' }, +]); +// end::snippet[] diff --git a/demo/lifecycle-callback/d3/x-user-deleted.ts b/demo/lifecycle-callback/d3/x-user-deleted.ts new file mode 100644 index 00000000..4bac4580 --- /dev/null +++ b/demo/lifecycle-callback/d3/x-user-deleted.ts @@ -0,0 +1,18 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import type { WebComponentInterface } from '../../../src/types.t.js'; + +// tag::snippet[] +@customElement('x-user-deleted') +export default class UserDeleted extends LitElement implements WebComponentInterface { + override render(): TemplateResult { + return html`
User has been deleted.
`; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-user-deleted': UserDeleted; + } +} diff --git a/demo/lifecycle-callback/d3/x-user-manage.ts b/demo/lifecycle-callback/d3/x-user-manage.ts new file mode 100644 index 00000000..71b6945d --- /dev/null +++ b/demo/lifecycle-callback/d3/x-user-manage.ts @@ -0,0 +1,37 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouterLocation, PreventAndRedirectCommands, WebComponentInterface, PreventResult } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-user-manage') +export default class UserManage extends LitElement implements WebComponentInterface { + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html` +
+

Manage user

+

User name: ${this.location?.params.user}

+ Delete user +
+ `; + } + + onBeforeLeave(location: RouterLocation, commands: PreventAndRedirectCommands): PreventResult | undefined { + if (location.pathname.indexOf('user/delete') > 0) { + // eslint-disable-next-line no-alert + if (!window.confirm('Are you sure you want to delete this user?')) { + return commands.prevent(); + } + } + + return undefined; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-user-manage': UserManage; + } +} diff --git a/demo/lifecycle-callback/d4/iframe.html b/demo/lifecycle-callback/d4/iframe.html new file mode 100644 index 00000000..0a539f79 --- /dev/null +++ b/demo/lifecycle-callback/d4/iframe.html @@ -0,0 +1,12 @@ + + + + Lifecycle Callback + + + + +
+ + + diff --git a/demo/lifecycle-callback/d4/script.ts b/demo/lifecycle-callback/d4/script.ts new file mode 100644 index 00000000..41d8f976 --- /dev/null +++ b/demo/lifecycle-callback/d4/script.ts @@ -0,0 +1,12 @@ +import '@helpers/iframe.script.js'; +import { Router } from '@vaadin/router'; +import './x-main-page.js'; +import './x-autosave-view.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-main-page' }, + { path: '/edit', component: 'x-autosave-view' }, +]); +// end::snippet[] diff --git a/demo/lifecycle-callback/d4/x-autosave-view.ts b/demo/lifecycle-callback/d4/x-autosave-view.ts new file mode 100644 index 00000000..13a64cd5 --- /dev/null +++ b/demo/lifecycle-callback/d4/x-autosave-view.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import type { WebComponentInterface } from '../../../src/types.t.js'; + +let savedText = 'This text is automatically saved when router navigates away.'; + +// tag::snippet[] +@customElement('x-autosave-view') +export class AutosaveView extends LitElement implements WebComponentInterface { + @state() accessor #text = savedText; + + override render(): TemplateResult { + return html` +
+ +
+ Stop editing + `; + } + + onInput(event: Event): void { + const target = event.target as HTMLTextAreaElement; + this.#text = target.value; + } + + onAfterEnter(): void { + this.#text = savedText; + } + + onAfterLeave(): void { + savedText = this.#text; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-autosave-view': AutosaveView; + } +} diff --git a/demo/lifecycle-callback/d4/x-main-page.ts b/demo/lifecycle-callback/d4/x-main-page.ts new file mode 100644 index 00000000..f2d32ee0 --- /dev/null +++ b/demo/lifecycle-callback/d4/x-main-page.ts @@ -0,0 +1,17 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +// tag::snippet[] +@customElement('x-main-page') +export class MainPage extends LitElement { + override render(): TemplateResult { + return html`Edit the text`; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-main-page': MainPage; + } +} diff --git a/demo/lifecycle-callback/d5/iframe.html b/demo/lifecycle-callback/d5/iframe.html new file mode 100644 index 00000000..b7b9c00e --- /dev/null +++ b/demo/lifecycle-callback/d5/iframe.html @@ -0,0 +1,17 @@ + + + + Lifecycle Callback + + + + + Home + All Users + Kim + About + +
+ + + diff --git a/demo/lifecycle-callback/d5/script.ts b/demo/lifecycle-callback/d5/script.ts new file mode 100644 index 00000000..b6f3dd6a --- /dev/null +++ b/demo/lifecycle-callback/d5/script.ts @@ -0,0 +1,21 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import '@helpers/x-not-found-view.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +window.addEventListener('vaadin-router-location-changed', (event) => { + const breadcrumbs = document.querySelector('#breadcrumbs')!; + breadcrumbs.textContent = `You are at '${event.detail.location.pathname}'`; +}); + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users', component: 'x-user-list' }, + { path: '/users/:user', component: 'x-user-profile' }, + { path: '(.*)', component: 'x-not-found-view' }, +]); +// end::snippet[] diff --git a/demo/lifecycle-callback/d6/iframe.html b/demo/lifecycle-callback/d6/iframe.html new file mode 100644 index 00000000..6f33606b --- /dev/null +++ b/demo/lifecycle-callback/d6/iframe.html @@ -0,0 +1,16 @@ + + + + Lifecycle Callback + + + + + All Users + Kim + About + +
+ + + diff --git a/demo/lifecycle-callback/d6/script.ts b/demo/lifecycle-callback/d6/script.ts new file mode 100644 index 00000000..5f697bda --- /dev/null +++ b/demo/lifecycle-callback/d6/script.ts @@ -0,0 +1,51 @@ +/* eslint-disable import/no-duplicates */ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import '@helpers/x-breadcrumbs.js'; +import '@helpers/x-not-found-view.js'; +import type { Breadcrumb } from '@helpers/x-breadcrumbs.js'; +import { Router, type VaadinRouterLocationChangedEvent } from '@vaadin/router'; + +// tag::snippet[] +type RouteExtension = Readonly<{ + xBreadcrumb?: Breadcrumb; +}>; + +window.addEventListener('vaadin-router-location-changed', (event: VaadinRouterLocationChangedEvent) => { + const { + router, + location: { params }, + } = event.detail; + + const breadcrumbs = document.querySelector('x-breadcrumbs')!; + breadcrumbs.items = router.location.routes + .map((route) => route.xBreadcrumb) + .filter((xBreadcrumb) => xBreadcrumb != null) + .map(({ href, title }) => ({ + title: title.replace(/:user/u, params.user as string), + href: href.replace(/:user/u, params.user as string), + })); +}); + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { + path: '/', + xBreadcrumb: { title: 'home', href: '/' }, + children: [ + { path: '/', component: 'x-home-view' }, + { + path: '/users', + xBreadcrumb: { title: 'users', href: '/users' }, + children: [ + { path: '/', component: 'x-user-list' }, + { path: '/:user', xBreadcrumb: { title: ':user', href: '/users/:user' }, component: 'x-user-profile' }, + ], + }, + ], + }, + { path: '(.*)', component: 'x-not-found-view' }, +]); +// end::snippet[] diff --git a/demo/lifecycle-callback/index.html b/demo/lifecycle-callback/index.html new file mode 100644 index 00000000..bb72b36e --- /dev/null +++ b/demo/lifecycle-callback/index.html @@ -0,0 +1,12 @@ + + + + Lifecycle Callback + + + + + + + + diff --git a/demo/lifecycle-callback/index.ts b/demo/lifecycle-callback/index.ts new file mode 100644 index 00000000..32795fee --- /dev/null +++ b/demo/lifecycle-callback/index.ts @@ -0,0 +1,384 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; +import xCountdownCode from './d1/x-countdown.js?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; +import xFriend from './d2/x-friend.js?snippet'; + +import htmlCode3 from './d3/iframe.html?snippet'; +import tsCode3 from './d3/script.js?snippet'; +import xUserDeleted from './d3/x-user-deleted.js?snippet'; +import xUserManage from './d3/x-user-manage.js?snippet'; + +import htmlCode4 from './d4/iframe.html?snippet'; +import tsCode4 from './d4/script.js?snippet'; +import xAutosaveView from './d4/x-autosave-view.js?snippet'; +import xMainPage from './d4/x-main-page.js?snippet'; + +import htmlCode5 from './d5/iframe.html?snippet'; +import tsCode5 from './d5/script.js?snippet'; + +import htmlCode6 from './d6/iframe.html?snippet'; +import tsCode6 from './d6/script.js?snippet'; + +import onAfterEnterCode from './snippets/my-view-with-after-enter.ts?snippet'; +import onAfterLeaveCode from './snippets/my-view-with-after-leave.ts?snippet'; +import onBeforeEnterCode from './snippets/my-view-with-before-enter.ts?snippet'; +import onBeforeLeaveCode from './snippets/my-view-with-before-leave.ts?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-lifecycle-callback': DemoLifecycleCallback; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'iframe1.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script1.ts', + }, + { + id: 'x-countdown', + code: xCountdownCode, + title: 'x-countdown.ts', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'iframe2.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script2.ts', + }, + { + id: 'x-friend', + code: xFriend, + title: 'x-friend.ts', + }, +]; + +const files3: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode3, + title: 'iframe3.html', + }, + { + id: 'ts', + code: tsCode3, + title: 'script3.ts', + }, + { + id: 'x-user-deleted', + code: xUserDeleted, + title: 'x-user-deleted.ts', + }, + { + id: 'x-user-manage', + code: xUserManage, + title: 'x-user-manage.ts', + }, +]; + +const files4: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode4, + title: 'iframe4.html', + }, + { + id: 'ts', + code: tsCode4, + title: 'script4.ts', + }, + { + id: 'x-autosave-view', + code: xAutosaveView, + title: 'x-autosave-view.ts', + }, + { + id: 'x-main-page', + code: xMainPage, + title: 'x-main-page.ts', + }, +]; + +const files5: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode5, + title: 'iframe5.html', + }, + { + id: 'ts', + code: tsCode5, + title: 'script5.ts', + }, +]; + +const files6: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode6, + title: 'iframe6.html', + }, + { + id: 'ts', + code: tsCode6, + title: 'script6.ts', + }, +]; + +@customElement('vaadin-demo-lifecycle-callback') +export default class DemoLifecycleCallback extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Lifecycle Callbacks

+

+ Vaadin Router manages the lifecycle of all route Web Components. + Lifecycle callbacks allow you to add custom logic to any point of this lifecycle: +

+ +

+ Vaadin Router lifecycle callbacks can be defined as methods on the route Web Component + class definition + in a similar way as the native custom element callbacks (like disconnectedCallback()). +

+ +

onBeforeEnter(location, commands, router)

+

+ The component's route has matched the current path, an instance of the component has been created and is about + to be inserted into the DOM. Use this callback to create a route guard (e.g. redirect to the login page if the + user is not logged in). +

+

+ At this point there is yet + no guarantee that the navigation into this view will actually happen because another route's + callback may interfere. +

+

+ This callback may return a redirect (return commands.redirect('/new/path')) or a + prevent (return commands.prevent()) router command. If it returns a promise, the router + waits until the promise is resolved before proceeding with the navigation. +

+

+ See the + API documentation for more details. +

+

+ Note: Navigating to the same route also triggers this callback, e.g., click on the same link + multiple times will trigger the onBeforeEnter + callback on each click. +

+ + + + +

onAfterEnter(location, commands, router)

+

+ The component's route has matched the current path and an instance of the component has been rendered into the + DOM. At this point it is certain that navigation won't be prevented or redirected. Use this callback to process + route params and initialize the view so that it's ready for user interactions. +

+

+ NOTE: When navigating between views the onAfterEnter callback on the new view's component is called + before the onAfterLeave callback on the previous view's component (which is being + removed). At some point both the new and the old view components are present in the DOM to allow + animating the transition (you can listen to the + animationend event to detect when it is over). +

+

+ Any value returned from this callback is ignored. See the + API documentation for more details. +

+ + + + +

onBeforeLeave(location, commands, router)

+

+ The component's route does not match the current path anymore and the component is about to be removed from the + DOM. Use this callback to prevent the navigation if necessary like in the demo below. Note that the user is + still able to copy and open that URL manually in the browser. +

+

+ Even if this callback does not prevent the navigation, at this point there is yet + no guarantee that the navigation away from this view will actually happen because another + route's callback may also interfere. +

+

+ This callback may return a prevent (return commands.prevent()) router command. If it + returns a promise, the router waits until the promise is resolved before proceeding with the navigation. +

+

+ See the + API documentation for more details. +

+

+ Note: Navigating to the same route also triggers this callback, e.g., click on the same link + multiple times will trigger the onBeforeLeave + callback on each click. +

+ + + + +

onAfterLeave(location, commands, router)

+

+ The component's route does not match the current path anymore and the component's removal from the DOM has been + started (it will be removed after a transition animation, if any). At this point it is certain that navigation + won't be prevented. Use this callback to clean-up and perform any custom actions that leaving a view should + trigger. For example, the demo below auto-saves any unsaved changes when the user navigates away from the view. +

+

+ NOTE: When navigating between views the onAfterEnter callback on the new view's component is called + before the onAfterLeave callback on the previous view's component (which is being + removed). At some point both the new and the old view components are present in the DOM to allow + animating the transition (you can listen to the + animationend event to detect when it is over). +

+

+ Any value returned from this callback is ignored. See the + API documentation for more details. +

+ + + + +

Listen to Global Navigation Events

+

+ In order to react to route changes in other parts of the app (outside of route components), you can add an event + listener for the vaadin-router-location-changed events on the window. Vaadin Router + dispatches such events every time after navigating to a new path. +

+ + + +

+ In case if navigation fails for any reason (e.g. if no route matched the given pathname), instead of the + vaadin-router-location-changed event Vaadin Router dispatches vaadin-router-error and + attaches the error object to the event as event.detail.error. +

+ +

Getting the Current Location

+

+ When handling Vaadin Router events, you can access the router instance via + event.detail.router, and the current location via event.detail.location (which is a + shorthand for event.detail.router.location). The + location + object has all details about the current router state. For example, + location.routes is a read-only list of routes that correspond to the last completed navigation, + which may be useful for example when creating a breadcrumbs component to visualize the current in-app location. +

+ + + +

+ The router configuration allows you to add any custom properties to route objects. The example above uses that + to set a custom xBreadcrumb property on the routes that we want to show up in breadcrumbs. That + property is used later when processing the vaadin-router-location-changed events. +

+ +

TypeScript Interfaces

+

+ For using with components defined as TypeScript classes, the following interfaces are available for + implementing: +

+
    +
  • +

    + BeforeEnterObserver +

    + +
  • +
  • +

    + AfterEnterObserver +

    + +
  • +
  • +

    + BeforeLeaveObserver +

    + +
  • +
  • +

    + AfterLeaveObserver +

    + +
  • +
`; + } +} diff --git a/demo/lifecycle-callback/snippets/my-view-with-after-enter.ts b/demo/lifecycle-callback/snippets/my-view-with-after-enter.ts new file mode 100644 index 00000000..8caa8107 --- /dev/null +++ b/demo/lifecycle-callback/snippets/my-view-with-after-enter.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +// tag::snippet[] +import type { EmptyCommands, Router, RouterLocation, WebComponentInterface } from '@vaadin/router'; + +@customElement('my-view-with-after-enter') +class MyViewWithAfterEnter extends LitElement implements WebComponentInterface { + onAfterEnter(location: RouterLocation, commands: EmptyCommands, router: Router) { + // ... + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'my-view-with-after-enter': MyViewWithAfterEnter; + } +} diff --git a/demo/lifecycle-callback/snippets/my-view-with-after-leave.ts b/demo/lifecycle-callback/snippets/my-view-with-after-leave.ts new file mode 100644 index 00000000..ed435577 --- /dev/null +++ b/demo/lifecycle-callback/snippets/my-view-with-after-leave.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +// tag::snippet[] +import type { EmptyCommands, Router, RouterLocation, WebComponentInterface } from '@vaadin/router'; + +@customElement('my-view-with-after-leave') +class MyViewWithAfterLeave extends LitElement implements WebComponentInterface { + onAfterLeave(location: RouterLocation, commands: EmptyCommands, router: Router) { + // ... + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'my-view-with-after-leave': MyViewWithAfterLeave; + } +} diff --git a/demo/lifecycle-callback/snippets/my-view-with-before-enter.ts b/demo/lifecycle-callback/snippets/my-view-with-before-enter.ts new file mode 100644 index 00000000..d2e0046a --- /dev/null +++ b/demo/lifecycle-callback/snippets/my-view-with-before-enter.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +// tag::snippet[] +import type { PreventAndRedirectCommands, Router, RouterLocation, WebComponentInterface } from '@vaadin/router'; + +@customElement('my-view-with-before-enter') +export default class MyViewWithBeforeEnter extends LitElement implements WebComponentInterface { + onBeforeEnter(location: RouterLocation, commands: PreventAndRedirectCommands, router: Router): void { + // ... + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'my-view-with-before-enter': MyViewWithBeforeEnter; + } +} diff --git a/demo/lifecycle-callback/snippets/my-view-with-before-leave.ts b/demo/lifecycle-callback/snippets/my-view-with-before-leave.ts new file mode 100644 index 00000000..823b2ca7 --- /dev/null +++ b/demo/lifecycle-callback/snippets/my-view-with-before-leave.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; +// tag::snippet[] +import type { PreventCommands, Router, RouterLocation, WebComponentInterface } from '@vaadin/router'; + +@customElement('my-view-with-before-leave') +class MyViewWithBeforeLeave extends LitElement implements WebComponentInterface { + onBeforeLeave(location: RouterLocation, commands: PreventCommands, router: Router) { + // ... + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'my-view-with-before-leave': MyViewWithBeforeLeave; + } +} diff --git a/demo/navigation-trigger/d1/iframe.html b/demo/navigation-trigger/d1/iframe.html new file mode 100644 index 00000000..29907ab0 --- /dev/null +++ b/demo/navigation-trigger/d1/iframe.html @@ -0,0 +1,12 @@ + + + + Navigation Trigger + + + + +
+ + + diff --git a/demo/navigation-trigger/d1/script.ts b/demo/navigation-trigger/d1/script.ts new file mode 100644 index 00000000..2333338a --- /dev/null +++ b/demo/navigation-trigger/d1/script.ts @@ -0,0 +1,17 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users', component: 'x-user-list' }, +]); + +setInterval(() => { + window.history.pushState(null, document.title, window.location.pathname === '/' ? '/users' : '/'); + window.dispatchEvent(new PopStateEvent('popstate')); +}, 3000); +// end::snippet[] diff --git a/demo/navigation-trigger/d2/iframe.html b/demo/navigation-trigger/d2/iframe.html new file mode 100644 index 00000000..5ea73868 --- /dev/null +++ b/demo/navigation-trigger/d2/iframe.html @@ -0,0 +1,14 @@ + + + + Navigation Trigger + + + + + Home + Users +
+ + + diff --git a/demo/navigation-trigger/d2/script.ts b/demo/navigation-trigger/d2/script.ts new file mode 100644 index 00000000..c67e1e14 --- /dev/null +++ b/demo/navigation-trigger/d2/script.ts @@ -0,0 +1,12 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users', component: 'x-user-list' }, +]); +// end::snippet[] diff --git a/demo/navigation-trigger/d3/iframe.html b/demo/navigation-trigger/d3/iframe.html new file mode 100644 index 00000000..861fe256 --- /dev/null +++ b/demo/navigation-trigger/d3/iframe.html @@ -0,0 +1,16 @@ + + + + Navigation Trigger + + + + +
    +
  • Home
  • +
  • Users
  • +
+
+ + + diff --git a/demo/navigation-trigger/d3/script.ts b/demo/navigation-trigger/d3/script.ts new file mode 100644 index 00000000..79bb391e --- /dev/null +++ b/demo/navigation-trigger/d3/script.ts @@ -0,0 +1,22 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import { DEFAULT_TRIGGERS, Router } from '@vaadin/router'; + +// tag::snippet[] +const { POPSTATE } = DEFAULT_TRIGGERS; +Router.setTriggers(POPSTATE); + +document.querySelector('ul')?.addEventListener('click', (event) => { + if (event.target instanceof HTMLLIElement && event.target.dataset.href) { + window.history.pushState({}, '', event.target.dataset.href); + window.dispatchEvent(new PopStateEvent('popstate')); + } +}); + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users', component: 'x-user-list' }, +]); +// end::snippet[] diff --git a/demo/navigation-trigger/d4/iframe.html b/demo/navigation-trigger/d4/iframe.html new file mode 100644 index 00000000..5ea73868 --- /dev/null +++ b/demo/navigation-trigger/d4/iframe.html @@ -0,0 +1,14 @@ + + + + Navigation Trigger + + + + + Home + Users +
+ + + diff --git a/demo/navigation-trigger/d4/script.ts b/demo/navigation-trigger/d4/script.ts new file mode 100644 index 00000000..b3f75914 --- /dev/null +++ b/demo/navigation-trigger/d4/script.ts @@ -0,0 +1,23 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { + path: '/users', + children: [ + { path: '', component: 'x-user-list' }, + { path: '/:user', component: 'x-user-profile' }, + ], + }, +]); +router.unsubscribe(); + +// router will re-render only when the `render()` method is called explicitly: +await router.render('/users'); +// end::snippet[] diff --git a/demo/navigation-trigger/index.html b/demo/navigation-trigger/index.html new file mode 100644 index 00000000..bb8b635a --- /dev/null +++ b/demo/navigation-trigger/index.html @@ -0,0 +1,12 @@ + + + + Navigation Trigger + + + + + + + + diff --git a/demo/navigation-trigger/index.ts b/demo/navigation-trigger/index.ts new file mode 100644 index 00000000..48d4525c --- /dev/null +++ b/demo/navigation-trigger/index.ts @@ -0,0 +1,170 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; + +import htmlCode3 from './d3/iframe.html?snippet'; +import tsCode3 from './d3/script.js?snippet'; + +import htmlCode4 from './d4/iframe.html?snippet'; +import tsCode4 from './d4/script.js?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-navigation-trigger': DemoNavigationTrigger; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.js', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script.js', + }, +]; + +const files3: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode3, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode3, + title: 'script.js', + }, +]; + +const files4: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode4, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode4, + title: 'script.js', + }, +]; + +@customElement('vaadin-demo-navigation-trigger') +export default class DemoNavigationTrigger extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Navigation Triggers

+

This feature is for advanced use cases. Please make sure to read the documentation carefully.

+

+ There are several events that can trigger in-app navigation with Vaadin Router: popstate events, clicks on the + <a> elements, imperative navigation triggered by JavaScript. In order to make a flexible + solution that can be tweaked to particular app needs and remain efficient, Vaadin Router has a concept of + pluggable Navigation Triggers that listen to specific browser events and convert them into the Vaadin + Router navigation events. +

+

+ The @vaadin/router package comes with two Navigation Triggers bundled by default: + POPSTATE and CLICK. +

+

+ Developers can define and use additional Navigation Triggers that are specific to their application. A + Navigation Trigger object must have two methods: activate() to start listening on some UI events, + and inactivate() to stop. +

+ +

NavigationTrigger.POPSTATE

+

+ The default POPSTATE navigation trigger for Vaadin Router listens to popstate events + on the current window and for each of them dispatches a navigation event for Vaadin Router using + the current window.location.pathname as the navigation target. This allows using the browser + Forward and Back buttons for in-app navigation. +

+

+ In the demo below the popstate events are dispatched at 3 seconds intervals. Before dispatching an + event the global location.pathname is toggled between '/' and '/users'. +

+

+ Please note that when using the window.history.pushState() or + window.history.replaceState() methods, you need to dispatch the popstate event + manually—these methods do not do that by themselves (see + MDN + for details). +

+ + + + +

NavigationTrigger.CLICK

+

+ The CLICK navigation trigger intercepts clicks on <a> elements on the the page + and converts them into navigation events for Vaadin Router if they refer to a location within the app. That + allows using regular <a> link elements for in-app navigation. You can use + router-ignore + attribute to have the router ignore the link. +

+ + + + +

Custom Navigation Triggers

+

+ The set of default navigation triggers can be changed using the Router.setTriggers() static + method. It accepts zero, one or more NavigationTriggers. +

+

+ This demo shows how to disable the CLICK navigation trigger. It may be useful when the app has an + own custom element for in-app links instead of using the regular <a> elements for that + purpose. The demo also shows a very basic version of a custom in-app link element based on a list element that + fires popstate events when clicked. +

+

+ Note: if the default Navigation Triggers are not used by the app, they can be excluded from the app bundle to + avoid sending unnecessary code to the users. See src/router-config.js for details. +

+ + + + +

Unsubscribe from Navigation Events

+

+ Each Vaadin Router instance is automatically subscribed to navigation events upon creation. Sometimes it might + be necessary to cancel this subscription so that the router would re-render only in response to the explicit + render() method calls. Use the unsubscribe() method to cancel this automatic + subscription and the subscribe() method to re-subscribe. +

+ + + `; + } +} diff --git a/demo/polymer.iframe.json b/demo/polymer.iframe.json deleted file mode 100644 index 16ba711c..00000000 --- a/demo/polymer.iframe.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "entrypoint": "bower_components/vaadin-router/demo/iframe.html", - "shell": "bower_components/vaadin-router/demo/demo-shell.html", - "sources": [ - "bower_components/vaadin-router/demo/*.html" - ], - "extraDependencies": [ - "bower_components/webcomponentsjs/*.js", - "bower_components/webcomponentsjs/*.map" - ], - "builds": [ - { - "preset": "es5-bundled", - "addServiceWorker": false - } - ] -} \ No newline at end of file diff --git a/demo/polymer.json b/demo/polymer.json deleted file mode 100644 index 35172dc5..00000000 --- a/demo/polymer.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "entrypoint": "bower_components/vaadin-router/demo/index.html", - "shell": "bower_components/vaadin-router/demo/demo-shell.html", - "sources": [ - "bower_components/vaadin-router/demo/*.html", - "!bower_components/vaadin-router/demo/iframe.html" - ], - "extraDependencies": [ - "bower_components/webcomponentsjs/*.js", - "bower_components/webcomponentsjs/*.map", - "bower_components/vaadin-router/demo/demos.json", - "bower_components/vaadin-router/demo/demo-elements/user.bundle.js", - "bower_components/vaadin-router/demo/demo-elements/user.bundle.html", - "bower_components/vaadin-router/demo/demo-elements/users-routes.js", - "bower_components/vaadin-router/demo/demo-elements/bundle-script.js" - ], - "builds": [ - { - "preset": "es5-bundled", - "html": { - "minify": { - "exclude": ["bower_components/vaadin-router/demo/*-demos.html"] - } - }, - "css": { - "minify": { - "exclude": ["bower_components/vaadin-router/demo/*-demos.html"] - } - }, - "addServiceWorker": false - } - ] -} diff --git a/demo/redirect/d1/iframe.html b/demo/redirect/d1/iframe.html new file mode 100644 index 00000000..51ae95e4 --- /dev/null +++ b/demo/redirect/d1/iframe.html @@ -0,0 +1,15 @@ + + + + Redirect + + + + + Home + User profile + Knowledge Base +
+ + + diff --git a/demo/redirect/d1/script.ts b/demo/redirect/d1/script.ts new file mode 100644 index 00000000..39161c6a --- /dev/null +++ b/demo/redirect/d1/script.ts @@ -0,0 +1,16 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-profile.js'; +import '@helpers/x-knowledge-base.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/u/:user', redirect: '/user/:user' }, + { path: '/user/:user', component: 'x-user-profile' }, + { path: '/data/:segments+/:path+', redirect: '/kb/:path+' }, + { path: '/kb/:path+', component: 'x-knowledge-base' }, +]); +// end::snippet[] diff --git a/demo/redirect/d2/iframe.html b/demo/redirect/d2/iframe.html new file mode 100644 index 00000000..0acf2ab4 --- /dev/null +++ b/demo/redirect/d2/iframe.html @@ -0,0 +1,16 @@ + + + + Redirect + + + + + Home + Admin + Login + Logout +
+ + + diff --git a/demo/redirect/d2/script.ts b/demo/redirect/d2/script.ts new file mode 100644 index 00000000..3e0f0314 --- /dev/null +++ b/demo/redirect/d2/script.ts @@ -0,0 +1,23 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-login-view.js'; +import { Router } from '@vaadin/router'; +import './x-admin-view.js'; + +// tag::snippet[] +window.authorized = false; + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/admin', component: 'x-admin-view' }, + { path: '/login/:to?', component: 'x-login-view' }, + { + path: '/logout', + action(_, commands) { + window.authorized = false; + return commands.redirect('/'); + }, + }, +]); +// end::snippet[] diff --git a/demo/redirect/d2/x-admin-view.ts b/demo/redirect/d2/x-admin-view.ts new file mode 100644 index 00000000..aa817811 --- /dev/null +++ b/demo/redirect/d2/x-admin-view.ts @@ -0,0 +1,30 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import type { Commands, RedirectResult, RouterLocation, WebComponentInterface } from '@vaadin/router'; + +declare global { + interface HTMLElementTagNameMap { + 'x-admin-view': AdminView; + } + + interface Window { + authorized: boolean; + } +} + +// tag::snippet[] +@customElement('x-admin-view') +export default class AdminView extends LitElement implements WebComponentInterface { + onBeforeEnter(location: RouterLocation, commands: Commands): RedirectResult | undefined { + if (!window.authorized) { + return commands.redirect(`/login/${encodeURIComponent(location.pathname)}`); + } + + return undefined; + } + + override render(): TemplateResult { + return html`Secret admin stuff`; + } +} +// end::snippet[] diff --git a/demo/redirect/d3/iframe.html b/demo/redirect/d3/iframe.html new file mode 100644 index 00000000..a64ffbbb --- /dev/null +++ b/demo/redirect/d3/iframe.html @@ -0,0 +1,13 @@ + + + + Redirect + + + + + +
+ + + diff --git a/demo/redirect/d3/script.ts b/demo/redirect/d3/script.ts new file mode 100644 index 00000000..d0f662da --- /dev/null +++ b/demo/redirect/d3/script.ts @@ -0,0 +1,16 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +document.querySelector('#trigger')?.addEventListener('click', () => { + Router.go('/user/you-know-who'); +}); + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/user/:user', component: 'x-user-profile' }, +]); +// end::snippet[] diff --git a/demo/redirect/index.html b/demo/redirect/index.html new file mode 100644 index 00000000..a90fb947 --- /dev/null +++ b/demo/redirect/index.html @@ -0,0 +1,12 @@ + + + + Redirect + + + + + + + + diff --git a/demo/redirect/index.ts b/demo/redirect/index.ts new file mode 100644 index 00000000..5e059fcf --- /dev/null +++ b/demo/redirect/index.ts @@ -0,0 +1,151 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; + +import htmlCode3 from './d3/iframe.html?snippet'; +import tsCode3 from './d3/script.js?snippet'; + +import tsSnippet1 from './snippets/s1.ts?snippet'; +import tsSnippet2 from './snippets/s2.ts?snippet'; +import tsSnippet3 from './snippets/s3.ts?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-redirect': DemoRedirect; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.js', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script.js', + }, +]; + +const files3: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode3, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode3, + title: 'script.js', + }, +]; + +@customElement('vaadin-demo-redirect') +export default class DemoRedirect extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Unconditional Redirects

+

+ Vaadin Router supports the redirect property on the route + objects, allowing to unconditionally redirect users from one path to + another. The valid values are a path string or a pattern in the same + format as used for the path property. +

+

+ The original path is not stored as the window.history entry + and cannot be reached by pressing the "Back" browser button. Unconditional + redirects work for routes both with and without parameters. +

+

+ The original path is available to route Web Components as the + + location.redirectFrom string property, and to custom + route actions – + as context.redirectFrom. +

+

+ Note: If a route has both the redirect and action + properties, action is executed first and if it does not + return a result Vaadin Router proceeds to check the redirect + property. Other route properties (if any) would be ignored. In that case + Vaadin Router would also log a warning to the browser console. +

+ + + + +

Dynamic Redirects

+

+ Vaadin Router allows redirecting to another path dynamically based on + a condition evaluated at the run time. In order to do that, + return commands.redirect('/new/path') from the + onBeforeEnter() lifecycle + callback of the route Web Component. +

+

+ It is also possible to redirect from a custom route action. The demo below + has an example of that in the /logout route action. See the + Route Actions section for + more details. +

+ + + + +

Navigation from JavaScript

+

+ If you want to send users to another path in response to a user + action (outside of a lifecycle callback), you can do that by using the + static + Router.go('/to/path') method on the Vaadin.Router class. +

+

+ You can optionally pass search query string and hash to the method, either + as in-app URL string: +

+ + ... or using an object with named parameters: +

+ + + + +

+ NOTE: the same effect can be achieved by dispatching a + vaadin-router-go custom event on the window. The + target path should be provided as event.detail.pathname, + the search and hash strings can be optionally provided + with event.detail.search and event.detail.hash + properties respectively. +

+ `; + } +} diff --git a/demo/redirect/snippets/s1.ts b/demo/redirect/snippets/s1.ts new file mode 100644 index 00000000..4b676583 --- /dev/null +++ b/demo/redirect/snippets/s1.ts @@ -0,0 +1,5 @@ +import { Router } from '@vaadin/router'; + +// tag::snippet[] +Router.go('/to/path?paramName=value#sectionName'); +// end::snippet[] diff --git a/demo/redirect/snippets/s2.ts b/demo/redirect/snippets/s2.ts new file mode 100644 index 00000000..1162289a --- /dev/null +++ b/demo/redirect/snippets/s2.ts @@ -0,0 +1,11 @@ +import { Router } from '@vaadin/router'; + +// tag::snippet[] +Router.go({ + pathname: '/to/path', + // optional + search: '?paramName=value', + // optional + hash: '#sectionName', +}); +// end::snippet[] diff --git a/demo/redirect/snippets/s3.ts b/demo/redirect/snippets/s3.ts new file mode 100644 index 00000000..ee7aa156 --- /dev/null +++ b/demo/redirect/snippets/s3.ts @@ -0,0 +1,15 @@ +// tag::snippet[] +window.dispatchEvent( + new CustomEvent('vaadin-router-go', { + detail: { + pathname: '/to/path', + // optional search query string + search: '?paramName=value', + // optional hash string + hash: '#sectionName', + }, + }), +); +// end::snippet[] + +export {}; diff --git a/demo/route-actions/d1/iframe.html b/demo/route-actions/d1/iframe.html new file mode 100644 index 00000000..f46ffb38 --- /dev/null +++ b/demo/route-actions/d1/iframe.html @@ -0,0 +1,18 @@ + + + + Route Actions + + + + + +
+
+ + + diff --git a/demo/route-actions/d1/script.ts b/demo/route-actions/d1/script.ts new file mode 100644 index 00000000..9785d2aa --- /dev/null +++ b/demo/route-actions/d1/script.ts @@ -0,0 +1,30 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router, type RouteContext } from '@vaadin/router'; + +// tag::snippet[] +const urlToNumberOfVisits: Record = {}; + +async function recordUrlVisit(context: RouteContext) { + const visitedPath = context.pathname; // get current path + urlToNumberOfVisits[visitedPath] = (urlToNumberOfVisits[visitedPath] ?? 0) + 1; + document.getElementById('stats')!.textContent = + `Statistics on URL visits: ${JSON.stringify(urlToNumberOfVisits, null, 2)}}`; + return await context.next(); // pass to the next route in the list +} + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { + path: '/users', + action: recordUrlVisit, // will be triggered for all children + children: [ + { path: '/', component: 'x-user-list' }, + { path: '/:user', component: 'x-user-profile' }, + ], + }, +]); +// end::snippet[] diff --git a/demo/route-actions/d2/iframe.html b/demo/route-actions/d2/iframe.html new file mode 100644 index 00000000..c80f07fd --- /dev/null +++ b/demo/route-actions/d2/iframe.html @@ -0,0 +1,17 @@ + + + + Route Actions + + + + + +
+ + + diff --git a/demo/route-actions/d2/script.ts b/demo/route-actions/d2/script.ts new file mode 100644 index 00000000..7f97adc8 --- /dev/null +++ b/demo/route-actions/d2/script.ts @@ -0,0 +1,27 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +async function pollBackendForChanges() { + return await new Promise((resolve) => { + // this can be an async backend call + setTimeout(() => resolve(), 1000); + }); +} + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { + path: '/users', + action: pollBackendForChanges, // will be triggered for all children + children: [ + { path: '/', component: 'x-user-list' }, + { path: '/:user', component: 'x-user-profile' }, + ], + }, +]); +// end::snippet[] diff --git a/demo/route-actions/d3/iframe.html b/demo/route-actions/d3/iframe.html new file mode 100644 index 00000000..ffccec9a --- /dev/null +++ b/demo/route-actions/d3/iframe.html @@ -0,0 +1,17 @@ + + + + Route Actions + + + + + +
+ + + diff --git a/demo/route-actions/d3/script.ts b/demo/route-actions/d3/script.ts new file mode 100644 index 00000000..66c0741c --- /dev/null +++ b/demo/route-actions/d3/script.ts @@ -0,0 +1,19 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-profile.js'; +import { Router, type Commands, type RouteContext } from '@vaadin/router'; + +// tag::snippet[] +function redirect(context: RouteContext, commands: Commands) { + (context.params.user as string) += ' (redirected)'; + return commands.redirect(`/users/:user`); +} + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + // current route parameters are automatically transferred to redirect target + { path: '/employees/:user', action: redirect }, + { path: '/users/:user', component: 'x-user-profile' }, +]); +// end::snippet[] diff --git a/demo/route-actions/d4/iframe.html b/demo/route-actions/d4/iframe.html new file mode 100644 index 00000000..c4c04940 --- /dev/null +++ b/demo/route-actions/d4/iframe.html @@ -0,0 +1,17 @@ + + + + Route Actions + + + + + +
+ + + diff --git a/demo/route-actions/d4/script.ts b/demo/route-actions/d4/script.ts new file mode 100644 index 00000000..a0ffe441 --- /dev/null +++ b/demo/route-actions/d4/script.ts @@ -0,0 +1,22 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-profile.js'; +import { Router, type Commands, type RouteContext } from '@vaadin/router'; + +// tag::snippet[] +function render(context: RouteContext, commands: Commands) { + if (context.params.user === 'admin') { + return commands.component('x-user-profile'); + } + const stubElement = commands.component('h3'); + stubElement.innerHTML = 'Access denied'; + return stubElement; +} + +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + // current route parameters are automatically transferred to rendered element + { path: '/users/:user', action: render }, +]); +// end::snippet[] diff --git a/demo/route-actions/d5/iframe.html b/demo/route-actions/d5/iframe.html new file mode 100644 index 00000000..cd519484 --- /dev/null +++ b/demo/route-actions/d5/iframe.html @@ -0,0 +1,16 @@ + + + + Route Actions + + + + + +
+ + + diff --git a/demo/route-actions/d5/script.ts b/demo/route-actions/d5/script.ts new file mode 100644 index 00000000..a94377c6 --- /dev/null +++ b/demo/route-actions/d5/script.ts @@ -0,0 +1,28 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-login-view.js'; +import { Router, type Commands, type RouteContext } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { + path: '/', + async action(context: RouteContext, commands: Commands) { + // Extract the `?view=` parameter value and decide upon it + const view = new URLSearchParams(context.search).get('view'); + if (view === 'login') { + return commands.component('x-login-view'); + } else if (view) { + // Redirect home for unkown values + return commands.redirect('/'); + } + + // Skip to next route if parameter is absent + return await context.next(); + }, + }, + // Same path, only matches if the action above skips + { path: '/', component: 'x-home-view' }, +]); +// end::snippet[] diff --git a/demo/route-actions/index.html b/demo/route-actions/index.html new file mode 100644 index 00000000..77d85225 --- /dev/null +++ b/demo/route-actions/index.html @@ -0,0 +1,12 @@ + + + + Route Actions + + + + + + + + diff --git a/demo/route-actions/index.ts b/demo/route-actions/index.ts new file mode 100644 index 00000000..598d1481 --- /dev/null +++ b/demo/route-actions/index.ts @@ -0,0 +1,226 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; + +import htmlCode3 from './d3/iframe.html?snippet'; +import tsCode3 from './d3/script.js?snippet'; + +import htmlCod4 from './d4/iframe.html?snippet'; +import tsCode4 from './d4/script.js?snippet'; + +import htmlCode5 from './d5/iframe.html?snippet'; +import tsCode5 from './d5/script.js?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-route-actions': DemoRouteActions; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.ts', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script.ts', + }, +]; + +const files3: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode3, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode3, + title: 'script.ts', + }, +]; + +const files4: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCod4, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode4, + title: 'script.ts', + }, +]; + +const files5: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode5, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode5, + title: 'script.ts', + }, +]; + +@customElement('vaadin-demo-route-actions') +export default class DemoRouteActions extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Custom Route Actions

+

This feature is for advanced use cases. Please make sure to read the documentation carefully.

+

+ Route resolution is an async operation started by a navigation event, or by an explicit + render() method call. In that process Vaadin Router goes through the routes config and tries to + resolve each matching route from the root onwards. The default route resolution rule is to create and + return an instance of the route's component (see the API docs for the + setRoutes() method for details on other route properties and how they affect the route resolution). +

+

+ Vaadin Router provides a flexible API to customize the default route resolution rule. Each route may have an + action functional property that defines how exactly that route is resolved. An + action function can return a result either directly, or within a Promise resolving to + the result. If the action result is an HTMLElement instance, a + commands.component(name) result, a commands.redirect(path) result, or a + context.next() result, the resolution pass, and the returned value is what gets rendered. + Otherwise, the resolution process continues to check the other properties of the route and apply the default + resolution rules, and then further to check other matching routes. +

+

+ The action(context, commands) function receives a context + parameter with the following properties: +

+
    +
  • context.pathname [string] the pathname being resolved
  • +
  • context.search [string] the search query string
  • +
  • context.hash [string] the hash string
  • +
  • context.params [object] the route parameters
  • +
  • context.route [object] the route that is currently being rendered
  • +
  • + context.next() [function] function for asynchronously getting the next route contents from the + resolution chain (if any) +
  • +
+

The commands is a helper object with methods to create return values for the action:

+
    +
  • + return commands.redirect('/new/path') create and return a redirect command for the specified + path. This command should be returned from the action to perform an actual redirect. +
  • +
  • + return commands.prevent() create and return a prevent command. This command should be returned + from the action + to instruct router to stop the current navigation and remain at the previous location. +
  • +
  • + return commands.component('tag-name') create and return a new HTMLElement that will + be rendered into the router outlet. Using the component command ensures that the created + component is initialized as a Vaadin Router view (i.e. the location property is set according to + the current router context. +
    + If an action returns this element, the behavior is the same as for + component route property: the action result will be rendered, if the action is in a child route, + the result will be rendered as light dom child of the component from a parent route. +
  • +
+

+ This demo shows how to use the custom action property to collect visit statistics for each route. +

+ + + + +

Async Route Resolution

+

+ Since route resolution is async, the action() callback may be async as well and return a promise. + One use case for that is to create a custom route action that makes a remote API call to fetch the data + necessary to render the route component, before navigating to a route. +

+

+ Note: If a route has both the component and action properties, action is + executed first and if it does not return a result Vaadin.Router proceeds to check the + component property. +

+

This demo shows a way to perform async tasks before navigating to any route under /users.

+ + + + +

Redirecting from an Action

+

+ action() can return a command created using the commands parameter methods to affect + the route resolution result. The first demo had demonstrated the context.next() usage, this demo + demonstrates using commands.redirect(path) to redirect to any other defined route by using its + path. All the parameters in current context will be passed to the redirect target. +

+

+ Note: If you need only to redirect to another route, defining an action might be an overkill. More convenient + way is described in Redirects section. +

+ + + + +

Returning Custom Element as an Action Result

+

+ Another command available to a custom action() is commands.component('tag-name'). It + is useful to create a custom element with current context. All the parameters in current context will be passed + to the rendered element. +

+

+ Note: If the only thing your action does is custom element creation, it can be replaced with + component property of the route. See + Getting Started + section for examples. +

+ + + + +

Routing With Search Query and Hash

+

+ Route action function can access context.search and context.hash URL parts, even + though they are not involved in matching route paths. +

+

+ For example, an action can change the route behavior depending on a search parameter, and optionally render, + skip to next route or redirect. +

+ + + `; + } +} diff --git a/demo/route-parameters/d1/iframe.html b/demo/route-parameters/d1/iframe.html new file mode 100644 index 00000000..dbcc8c56 --- /dev/null +++ b/demo/route-parameters/d1/iframe.html @@ -0,0 +1,18 @@ + + + + Route Parameters + + + + + Home + Admin + Guest + Image 24px blue + Image 32px pink + Cats +
+ + + diff --git a/demo/route-parameters/d1/script.ts b/demo/route-parameters/d1/script.ts new file mode 100644 index 00000000..e8de6f2d --- /dev/null +++ b/demo/route-parameters/d1/script.ts @@ -0,0 +1,19 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-profile.js'; +import '@helpers/x-image-view.js'; +import '@helpers/x-knowledge-base.js'; +import '@helpers/x-profile-view.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/profile/:user', component: 'x-user-profile' }, + { path: '/image/:size/:color?', component: 'x-image-view' }, + { path: '/image-:size(\\d+)px', component: 'x-image-view' }, + { path: '/kb/:path*', component: 'x-knowledge-base' }, + { path: '/(user[s]?)/:id', component: 'x-profile-view' }, +]); +// end::snippet[] diff --git a/demo/route-parameters/d2/iframe.html b/demo/route-parameters/d2/iframe.html new file mode 100644 index 00000000..849094be --- /dev/null +++ b/demo/route-parameters/d2/iframe.html @@ -0,0 +1,15 @@ + + + + Route Parameters + + + + + Home + Project 1 + Project 2 +
+ + + diff --git a/demo/route-parameters/d2/script.ts b/demo/route-parameters/d2/script.ts new file mode 100644 index 00000000..f5989f74 --- /dev/null +++ b/demo/route-parameters/d2/script.ts @@ -0,0 +1,12 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import { Router } from '@vaadin/router'; +import './x-project-view.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/(project[s]?)/:id', component: 'x-project-view' }, +]); +// end::snippet[] diff --git a/demo/route-parameters/d2/x-project-view.ts b/demo/route-parameters/d2/x-project-view.ts new file mode 100644 index 00000000..3ffd3df8 --- /dev/null +++ b/demo/route-parameters/d2/x-project-view.ts @@ -0,0 +1,22 @@ +import { LitElement, html, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-project-view') +export default class ProjectView extends LitElement implements WebComponentInterface { + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html`

Project

+

ID: ${this.location?.params.id ?? 'unknown'}

+ /project or /projects: ${this.location?.params[0] ?? 'unknown'}`; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-project-view': ProjectView; + } +} diff --git a/demo/route-parameters/d3/iframe.html b/demo/route-parameters/d3/iframe.html new file mode 100644 index 00000000..af90762b --- /dev/null +++ b/demo/route-parameters/d3/iframe.html @@ -0,0 +1,15 @@ + + + + Route Parameters + + + + + Home + All Users + Kim +
+ + + diff --git a/demo/route-parameters/d3/script.ts b/demo/route-parameters/d3/script.ts new file mode 100644 index 00000000..848977b5 --- /dev/null +++ b/demo/route-parameters/d3/script.ts @@ -0,0 +1,14 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users', component: 'x-user-list' }, + { path: '/:user', component: 'x-user-profile' }, +]); +// end::snippet[] diff --git a/demo/route-parameters/d4/iframe.html b/demo/route-parameters/d4/iframe.html new file mode 100644 index 00000000..b4c6e73b --- /dev/null +++ b/demo/route-parameters/d4/iframe.html @@ -0,0 +1,16 @@ + + + + Route Parameters + + + + + Home + All Users + User 42 + Guest +
+ + + diff --git a/demo/route-parameters/d4/script.ts b/demo/route-parameters/d4/script.ts new file mode 100644 index 00000000..f1cc2b84 --- /dev/null +++ b/demo/route-parameters/d4/script.ts @@ -0,0 +1,16 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-numeric-view.js'; +import '@helpers/x-user-not-found-view.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', component: 'x-home-view' }, + { path: '/users/list', component: 'x-user-list' }, + { path: '/users/([0-9]+)', component: 'x-user-numeric-view' }, + { path: '/users/(.*)', component: 'x-user-not-found-view' }, +]); +// end::snippet[] diff --git a/demo/route-parameters/d5/iframe.html b/demo/route-parameters/d5/iframe.html new file mode 100644 index 00000000..e76076f6 --- /dev/null +++ b/demo/route-parameters/d5/iframe.html @@ -0,0 +1,17 @@ + + + + Route Parameters + + + + + +
+ + + diff --git a/demo/route-parameters/d5/script.ts b/demo/route-parameters/d5/script.ts new file mode 100644 index 00000000..fe788d7f --- /dev/null +++ b/demo/route-parameters/d5/script.ts @@ -0,0 +1,8 @@ +import '@helpers/iframe.script.js'; +import { Router } from '@vaadin/router'; +import './x-page-number-view.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([{ path: '/', component: 'x-page-number-view' }]); +// end::snippet[] diff --git a/demo/route-parameters/d5/x-page-number-view.ts b/demo/route-parameters/d5/x-page-number-view.ts new file mode 100644 index 00000000..955c9d00 --- /dev/null +++ b/demo/route-parameters/d5/x-page-number-view.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/class-methods-use-this */ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-page-number-view') +export default class PageNumberView extends LitElement implements WebComponentInterface { + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html`Page number: ${this.#getPageNumber()}`; + } + + #getPageNumber(): string { + return this.location?.searchParams.get('page') ?? 'none'; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-page-number-view': PageNumberView; + } +} diff --git a/demo/route-parameters/d6/iframe.html b/demo/route-parameters/d6/iframe.html new file mode 100644 index 00000000..a91806eb --- /dev/null +++ b/demo/route-parameters/d6/iframe.html @@ -0,0 +1,17 @@ + + + + Route Parameters + + + + + +
+ + + diff --git a/demo/route-parameters/d6/script.ts b/demo/route-parameters/d6/script.ts new file mode 100644 index 00000000..0024f35f --- /dev/null +++ b/demo/route-parameters/d6/script.ts @@ -0,0 +1,8 @@ +import '@helpers/iframe.script.js'; +import './x-hash-view.js'; +import { Router } from '@vaadin/router'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([{ path: '/', component: 'x-hash-view' }]); +// end::snippet[] diff --git a/demo/route-parameters/d6/x-hash-view.ts b/demo/route-parameters/d6/x-hash-view.ts new file mode 100644 index 00000000..8d9de197 --- /dev/null +++ b/demo/route-parameters/d6/x-hash-view.ts @@ -0,0 +1,20 @@ +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-hash-view') +export class HashView extends LitElement implements WebComponentInterface { + @property({ type: Object }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult { + return html` Current hash: ${this.location?.hash ?? 'unknown'} `; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-hash-view': HashView; + } +} diff --git a/demo/route-parameters/index.html b/demo/route-parameters/index.html new file mode 100644 index 00000000..072f7e1c --- /dev/null +++ b/demo/route-parameters/index.html @@ -0,0 +1,12 @@ + + + + Route Parameters + + + + + + + + diff --git a/demo/route-parameters/index.ts b/demo/route-parameters/index.ts new file mode 100644 index 00000000..7c5f23bf --- /dev/null +++ b/demo/route-parameters/index.ts @@ -0,0 +1,253 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; +import projectViewCode from './d2/x-project-view.ts?snippet'; + +import htmlCode3 from './d3/iframe.html?snippet'; +import tsCode3 from './d3/script.js?snippet'; + +import htmlCode4 from './d4/iframe.html?snippet'; +import tsCode4 from './d4/script.js?snippet'; + +import htmlCode5 from './d5/iframe.html?snippet'; +import tsCode5 from './d5/script.js?snippet'; +import pageNumberViewCode from './d5/x-page-number-view.ts?snippet'; + +import htmlCode6 from './d6/iframe.html?snippet'; +import tsCode6 from './d6/script.js?snippet'; +import hashViewCode from './d6/x-hash-view.ts?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-route-parameters': DemoRouteParameters; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.ts', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script.ts', + }, + { + id: 'project-view', + code: projectViewCode, + title: 'x-project-view.ts', + }, +]; + +const files3: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode3, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode3, + title: 'script.ts', + }, +]; + +const files4: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode4, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode4, + title: 'script.ts', + }, +]; + +const files5: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode5, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode5, + title: 'script.ts', + }, + { + id: 'page-number-view', + code: pageNumberViewCode, + title: 'x-page-number-view.ts', + }, +]; + +const files6: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode6, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode6, + title: 'script.ts', + }, + { + id: 'hash-view', + code: hashViewCode, + title: 'x-hash-view.ts', + }, +]; + +@customElement('vaadin-demo-route-parameters') +export default class DemoRouteParameters extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Route Parameters

+

Route parameters are useful when the same Web Component should be + rendered for a number of paths, where a part of the path is static, and + another part contains a parameter value. E.g. for both /user/1 + and /user/42 paths it's the same Web Component that + renders the content, the /user/ part is static, and 1 + and 42 are the parameter values.

+

Route parameters are defined using an express.js-like syntax. The + implementation is based on the path-to-regexp library that is commonly + used in modern front-end libraries and frameworks. All features are + supported: +

+
    +
  • named parameters: /profile/:user
  • +
  • optional parameters: /:size/:color?
  • +
  • zero-or-more segments: /kb/:path*
  • +
  • one-or-more segments: /kb/:path+
  • +
  • custom parameter patterns: /image-:size(\d+)px
  • +
  • unnamed parameters: /(user[s]?)/:id
  • +
+

+ + + + +

Accessing Route Parameters

+

+ Route parameters are bound to the location.params property of + the route Web Component + (location + is set on the route components by Vaadin Router). +

+
    +
  • Named parameters are accessible by a string key, e.g. + location.params.id or location.params['id']
  • +
  • Unnamed parameters are accessible by a numeric index, e.g. + location.params[0]
  • +
+

The example below shows how to access route parameters:

+ + + + +

Ambiguous Matches

+

+ Route matching rules can be ambiguous, so that several routes would match + the same path. In that case, the order in which the routes are defined is + important. The first route matching the path gets rendered (starting from + the top of the list / root of the tree).

+

+ The default route matching is exact, i.e. a + '/user' route (if it does not have children) matches only the + '/user' path, but not '/users' nor + '/user/42'. Trailing slashes are not significant in paths, + but are significant in routes, i.e. a '/user' route matches + both '/user' the '/user/', but a + '/user/' route matches only the '/user/' path. +

+

Prefix matching is used for routes with children, or if + the route explicitly indicates that trailing content is expected (e.g. + a '/users/(*.)' route matches any path starting with + '/users/'). +

+

+ Always place more specific routes before less specific: +

+
    +
  • {path: '/user/new', ...} - matches only + '/user/new'
  • +
  • {path: '/user/:user', ...} - matches + '/user/42', but not '/user/42/edit'
  • +
  • {path: '/user/(.*)', ...} - matches anything starting + with '/user/'
  • +
+

+ + + + +

Typed Route Parameters

+

+ The route can be configured so that only specific characters are accepted for a parameter value. + Other characters would not meet the check and the route resolution would continue to other routes. + You only can use unnamed parameters in this case, as it can only be achieved using the custom RegExp. + One possible alternative is to use Route Actions + and check the context.params.

+ + + + +

Search Query Parameters

+

+ The search query string (?example) URL part is considered + separate from the pathname. Hence, it does not participate in matching + the route path, and location.params does not + contain search query string parameters. +

+

+ Use location.search to access the raw search query string. + Use location.searchParams to get the URLSearchParams wrapper of the search query string. +

+ + + + +

Hash String

+

+ Likewise with the search query, the hash string (#example) + is separate from the pathname as well. Use location.hash + to access the hash string in the view component. +

+ + + `; + } +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 00000000..49aabcdc --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": ["../tsconfig.json"], + "compilerOptions": { + "paths": { + "@helpers/*": ["./@helpers/*"], + "@vaadin/router": ["../src/index.ts"] + } + }, + "include": ["./**/*", "./*"] +} diff --git a/demo/types.t.ts b/demo/types.t.ts new file mode 100644 index 00000000..596fc899 --- /dev/null +++ b/demo/types.t.ts @@ -0,0 +1,15 @@ +/* eslint-disable import/unambiguous */ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +declare module '*.css?ctr' { + const css: CSSStyleSheet; + export default css; +} + +declare module '*?snippet' { + import type { TemplateResult } from 'lit'; + + const snippets: [code: string, full: TemplateResult, ...rest: TemplateResult[]]; + export default snippets; +} diff --git a/demo/url-generation/d1/iframe.html b/demo/url-generation/d1/iframe.html new file mode 100644 index 00000000..dd4ca5d5 --- /dev/null +++ b/demo/url-generation/d1/iframe.html @@ -0,0 +1,12 @@ + + + + URL Generation + + + + +
+ + + diff --git a/demo/url-generation/d1/script.ts b/demo/url-generation/d1/script.ts new file mode 100644 index 00000000..8db697df --- /dev/null +++ b/demo/url-generation/d1/script.ts @@ -0,0 +1,26 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; +import './x-main-layout.js'; + +// tag::snippet[] +export type RouteExtension = Readonly<{ + router: Router; +}>; + +export const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { + path: '/', + component: 'x-main-layout', + router, + children: [ + { name: 'home', path: '/', component: 'x-home-view', router }, + { path: '/users', component: 'x-user-list', router }, + { path: '/users/:user', component: 'x-user-profile', router }, + ], + }, +]); +// end::snippet[] diff --git a/demo/url-generation/d1/x-main-layout.ts b/demo/url-generation/d1/x-main-layout.ts new file mode 100644 index 00000000..592e773f --- /dev/null +++ b/demo/url-generation/d1/x-main-layout.ts @@ -0,0 +1,29 @@ +import { html, LitElement, nothing, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouteExtension } from './script.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-main-layout') +export default class MainLayout extends LitElement implements WebComponentInterface { + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult | typeof nothing { + return this.location?.route + ? html` + Home + Users + My profile + + ` + : nothing; + } +} + +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-main-layout': MainLayout; + } +} diff --git a/demo/url-generation/d2/iframe.html b/demo/url-generation/d2/iframe.html new file mode 100644 index 00000000..dd4ca5d5 --- /dev/null +++ b/demo/url-generation/d2/iframe.html @@ -0,0 +1,12 @@ + + + + URL Generation + + + + +
+ + + diff --git a/demo/url-generation/d2/script.ts b/demo/url-generation/d2/script.ts new file mode 100644 index 00000000..e4932afb --- /dev/null +++ b/demo/url-generation/d2/script.ts @@ -0,0 +1,25 @@ +import '@helpers/iframe.script.js'; +import '@helpers/x-home-view.js'; +import '@helpers/x-user-list.js'; +import '@helpers/x-user-profile.js'; +import { Router } from '@vaadin/router'; +import './x-main-layout.js'; + +// tag::snippet[] +export type RouteExtension = Readonly<{ + router: Router; +}>; +export const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { + path: '/', + component: 'x-main-layout', + router, + children: [ + { path: '/', component: 'x-home-view', router }, + { path: '/users', component: 'x-user-list', router }, + { path: '/users/:user', component: 'x-user-profile', router }, + ], + }, +]); +// end::snippet[] diff --git a/demo/url-generation/d2/x-main-layout.ts b/demo/url-generation/d2/x-main-layout.ts new file mode 100644 index 00000000..19aecc66 --- /dev/null +++ b/demo/url-generation/d2/x-main-layout.ts @@ -0,0 +1,28 @@ +import { html, LitElement, nothing, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouteExtension } from './script.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-main-layout') +export default class MainLayout extends LitElement implements WebComponentInterface { + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult | typeof nothing { + return this.location?.route + ? html` + Home + Users + My profile + + ` + : nothing; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-main-layout': MainLayout; + } +} diff --git a/demo/url-generation/d3/iframe.html b/demo/url-generation/d3/iframe.html new file mode 100644 index 00000000..dd4ca5d5 --- /dev/null +++ b/demo/url-generation/d3/iframe.html @@ -0,0 +1,12 @@ + + + + URL Generation + + + + +
+ + + diff --git a/demo/url-generation/d3/script.ts b/demo/url-generation/d3/script.ts new file mode 100644 index 00000000..a3656847 --- /dev/null +++ b/demo/url-generation/d3/script.ts @@ -0,0 +1,16 @@ +import '@helpers/iframe.script.js'; +import { Router } from '@vaadin/router'; +import './x-user-layout-d3.js'; + +// tag::snippet[] +export type RouteExtension = Readonly<{ + router: Router; +}>; + +export const router = new Router(document.getElementById('outlet')); +await router.setRoutes([ + { path: '/', redirect: '/users/me', router }, + { path: '/users/:user', component: 'x-user-layout-d3', router }, +]); + +// end::snippet[] diff --git a/demo/url-generation/d3/x-user-layout-d3.ts b/demo/url-generation/d3/x-user-layout-d3.ts new file mode 100644 index 00000000..1df1fe75 --- /dev/null +++ b/demo/url-generation/d3/x-user-layout-d3.ts @@ -0,0 +1,34 @@ +import '@helpers/x-user-profile.js'; +import { html, LitElement, nothing, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import type { RouteExtension } from './script.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-user-layout-d3') +export default class UserLayoutD3 extends LitElement implements WebComponentInterface { + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult | typeof nothing { + return this.location?.route + ? html` + + My profile + Admin profile + + ` + : nothing; + } + + #getUrlForUser(user: string): string | undefined { + return this.location?.route?.router.urlForPath('/users/:user', { user }); + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-user-layout-d3': UserLayoutD3; + } +} diff --git a/demo/url-generation/d4/iframe.html b/demo/url-generation/d4/iframe.html new file mode 100644 index 00000000..dd4ca5d5 --- /dev/null +++ b/demo/url-generation/d4/iframe.html @@ -0,0 +1,12 @@ + + + + URL Generation + + + + +
+ + + diff --git a/demo/url-generation/d4/script.ts b/demo/url-generation/d4/script.ts new file mode 100644 index 00000000..8cf25ce5 --- /dev/null +++ b/demo/url-generation/d4/script.ts @@ -0,0 +1,17 @@ +import '@helpers/iframe.script.js'; +import { Router } from '@vaadin/router'; +import './x-user-layout-d4.js'; + +history.pushState(null, '', '/ui/'); + +// tag::snippet[] +export type RouteExtension = Readonly<{ + router: Router; +}>; + +export const router = new Router(document.getElementById('outlet'), { baseUrl: '/ui/' }); +await router.setRoutes([ + { path: '/', redirect: '/users/me', router }, + { name: 'users', path: '/users/:user', component: 'x-user-layout-d4', router }, +]); +// end::snippet[] diff --git a/demo/url-generation/d4/x-user-layout-d4.ts b/demo/url-generation/d4/x-user-layout-d4.ts new file mode 100644 index 00000000..9a786cfc --- /dev/null +++ b/demo/url-generation/d4/x-user-layout-d4.ts @@ -0,0 +1,29 @@ +import '@helpers/x-user-profile.js'; +import { html, LitElement, nothing, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { RouteExtension } from './script.js'; +import type { RouterLocation, WebComponentInterface } from '@vaadin/router'; + +// tag::snippet[] +@customElement('x-user-layout-d4') +export default class UserLayoutD4 extends LitElement implements WebComponentInterface { + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult | typeof nothing { + return this.location?.route + ? html` + + My profile + Manager profile + Admin profile + ` + : nothing; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-user-layout-d4': UserLayoutD4; + } +} diff --git a/demo/url-generation/d5/iframe.html b/demo/url-generation/d5/iframe.html new file mode 100644 index 00000000..dd4ca5d5 --- /dev/null +++ b/demo/url-generation/d5/iframe.html @@ -0,0 +1,12 @@ + + + + URL Generation + + + + +
+ + + diff --git a/demo/url-generation/d5/script.ts b/demo/url-generation/d5/script.ts new file mode 100644 index 00000000..78617d92 --- /dev/null +++ b/demo/url-generation/d5/script.ts @@ -0,0 +1,8 @@ +import '@helpers/iframe.script.js'; +import { Router } from '@vaadin/router'; +import './x-pages-menu.js'; + +// tag::snippet[] +const router = new Router(document.getElementById('outlet')); +await router.setRoutes([{ path: '/', component: 'x-pages-menu' }]); +// end::snippet[] diff --git a/demo/url-generation/d5/x-pages-menu.ts b/demo/url-generation/d5/x-pages-menu.ts new file mode 100644 index 00000000..c50e6887 --- /dev/null +++ b/demo/url-generation/d5/x-pages-menu.ts @@ -0,0 +1,52 @@ +import { css, html, LitElement, nothing, type TemplateResult } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { map } from 'lit/directives/map.js'; +import type { RouterLocation } from '@vaadin/router'; + +// tag::snippet[] +const pages = [1, 2, 3, 4, 5]; + +function urlForPageNumber(location: RouterLocation, pageNumber: number) { + const query = new URLSearchParams({ page: String(pageNumber) }).toString(); + return `${location.getUrl()}?${query}`; +} + +function urlForSection(location: RouterLocation, section: string) { + return `${location.getUrl()}#${section}`; +} + +@customElement('x-pages-menu') +export class PagesMenu extends LitElement { + static override styles = css` + nav { + display: flex; + gap: 0.5rem; + } + `; + + @property({ attribute: false }) accessor location: RouterLocation | undefined; + + override render(): TemplateResult | typeof nothing { + return this.location + ? html` + ` + : nothing; + } +} +// end::snippet[] + +declare global { + interface HTMLElementTagNameMap { + 'x-pages-menu': PagesMenu; + } +} diff --git a/demo/url-generation/index.html b/demo/url-generation/index.html new file mode 100644 index 00000000..7b9673b7 --- /dev/null +++ b/demo/url-generation/index.html @@ -0,0 +1,12 @@ + + + + URL Generation + + + + + + + + diff --git a/demo/url-generation/index.ts b/demo/url-generation/index.ts new file mode 100644 index 00000000..d2471ae6 --- /dev/null +++ b/demo/url-generation/index.ts @@ -0,0 +1,213 @@ +/* eslint-disable import/no-duplicates, import/default */ +import '@helpers/common.js'; +import '@helpers/vaadin-demo-layout.js'; +import '@helpers/vaadin-demo-code-snippet.js'; +import '@helpers/vaadin-presentation.js'; +import { html, LitElement, type TemplateResult } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +import htmlCode1 from './d1/iframe.html?snippet'; +import tsCode1 from './d1/script.js?snippet'; +import mainLayoutCode1 from './d1/x-main-layout.ts?snippet'; + +import htmlCode2 from './d2/iframe.html?snippet'; +import tsCode2 from './d2/script.js?snippet'; +import mainLayoutCode2 from './d2/x-main-layout.ts?snippet'; + +import htmlCode3 from './d3/iframe.html?snippet'; +import tsCode3 from './d3/script.js?snippet'; +import userLayoutD3Code from './d3/x-user-layout-d3.ts?snippet'; + +import htmlCode4 from './d4/iframe.html?snippet'; +import tsCode4 from './d4/script.js?snippet'; +import userLayoutD4Code from './d4/x-user-layout-d4.ts?snippet'; + +import htmlCode5 from './d5/iframe.html?snippet'; +import tsCode5 from './d5/script.js?snippet'; +import pagesMenuCode from './d5/x-pages-menu.ts?snippet'; + +import css from '@helpers/page.css?ctr'; +import type { CodeSnippet } from '@helpers/vaadin-demo-code-snippet.js'; + +declare global { + interface HTMLElementTagNameMap { + 'vaadin-demo-url-generation': DemoUrlGeneration; + } +} + +const files1: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode1, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode1, + title: 'script.ts', + }, + { + id: 'main-layout', + code: mainLayoutCode1, + title: 'x-main-layout.ts', + }, +]; + +const files2: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode2, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode2, + title: 'script.ts', + }, + { + id: 'main-layout', + code: mainLayoutCode2, + title: 'x-main-layout.ts', + }, +]; + +const files3: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode3, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode3, + title: 'script.ts', + }, + { + id: 'user-layout', + code: userLayoutD3Code, + title: 'x-user-layout.ts', + }, +]; + +const files4: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode4, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode4, + title: 'script.ts', + }, + { + id: 'user-layout', + code: userLayoutD4Code, + title: 'x-user-layout.ts', + }, +]; + +const files5: readonly CodeSnippet[] = [ + { + id: 'html', + code: htmlCode5, + title: 'index.html', + }, + { + id: 'ts', + code: tsCode5, + title: 'script.ts', + }, + { + id: 'pages-menu', + code: pagesMenuCode, + title: 'x-pages-menu.ts', + }, +]; + +@customElement('vaadin-demo-url-generation') +export default class DemoUrlGeneration extends LitElement { + static override styles = [css]; + + override render(): TemplateResult { + return html`

Named routes and the router.urlForName method

+

+ Vaadin Router supports referring to routes using string names. You can assign a name to a route using the + name property of a route object, then generate URLs for that route using the + + router.urlForName(name, parameters) + + helper instance method. +

+

Arguments:

+
    +
  • name — the route name
  • +
  • parameters — optional object with parameters for substitution in the route path
  • +
+

+ If the component property is specified on the route object, the name property could be + omitted. In that case, the component name could be used in the router.urlForName(). +

+ + + + +

The router.urlForPath method

+

+ + router.urlForPath(path, parameters) + + is a helper method that generates a URL for the given route path, optionally performing substitution of + parameters. +

+

Arguments:

+
    +
  • path — a string route path defined in express.js syntax
  • +
  • parameters — optional object with parameters for path substitution
  • +
+ + + + +

The location.getUrl method

+

+ + location.getUrl(params) + + is a method that returns a URL corresponding to the location. When given the params argument, it does parameter + substitution in the location’s chain of routes. +

+

Arguments:

+
    +
  • params — optional object with parameters to override the location parameters
  • +
+ + + + +

Base URL in URL generation

+

When base URL is set, the URL generation helpers return absolute pathnames, including the base.

+ + + + +

Generating URLs with search query parameters and hash string

+

+ At the moment, Vaadin Router does not provide URL generation APIs for appending search query parameters or hash + strings to the generated URLs. However, you could append those with string concatenation. +

+

+ For serialising parameters into a query string, use the native + URLSearchParams API. +

+ + + `; + } +} diff --git a/demo/vaadin-router-animated-transitions-demos.html b/demo/vaadin-router-animated-transitions-demos.html deleted file mode 100644 index 337756aa..00000000 --- a/demo/vaadin-router-animated-transitions-demos.html +++ /dev/null @@ -1,216 +0,0 @@ - - - - diff --git a/demo/vaadin-router-code-splitting-demos.html b/demo/vaadin-router-code-splitting-demos.html deleted file mode 100644 index 94291f11..00000000 --- a/demo/vaadin-router-code-splitting-demos.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - diff --git a/demo/vaadin-router-demo-shared-styles.html b/demo/vaadin-router-demo-shared-styles.html deleted file mode 100644 index ece6b07f..00000000 --- a/demo/vaadin-router-demo-shared-styles.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/demo/vaadin-router-getting-started-demos.html b/demo/vaadin-router-getting-started-demos.html deleted file mode 100644 index e35dafdf..00000000 --- a/demo/vaadin-router-getting-started-demos.html +++ /dev/null @@ -1,368 +0,0 @@ - - - - diff --git a/demo/vaadin-router-lifecycle-callbacks-demos.html b/demo/vaadin-router-lifecycle-callbacks-demos.html deleted file mode 100644 index aba23b71..00000000 --- a/demo/vaadin-router-lifecycle-callbacks-demos.html +++ /dev/null @@ -1,530 +0,0 @@ - - - - diff --git a/demo/vaadin-router-navigation-trigger-demos.html b/demo/vaadin-router-navigation-trigger-demos.html deleted file mode 100644 index 5856185d..00000000 --- a/demo/vaadin-router-navigation-trigger-demos.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - diff --git a/demo/vaadin-router-redirect-demos.html b/demo/vaadin-router-redirect-demos.html deleted file mode 100644 index 4679e871..00000000 --- a/demo/vaadin-router-redirect-demos.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - diff --git a/demo/vaadin-router-route-actions-demos.html b/demo/vaadin-router-route-actions-demos.html deleted file mode 100644 index 87f45f37..00000000 --- a/demo/vaadin-router-route-actions-demos.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - diff --git a/demo/vaadin-router-route-parameters-demos.html b/demo/vaadin-router-route-parameters-demos.html deleted file mode 100644 index 2c415acf..00000000 --- a/demo/vaadin-router-route-parameters-demos.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - diff --git a/demo/vaadin-router-url-generation-demos.html b/demo/vaadin-router-url-generation-demos.html deleted file mode 100644 index 72fe4c0e..00000000 --- a/demo/vaadin-router-url-generation-demos.html +++ /dev/null @@ -1,312 +0,0 @@ - - - - diff --git a/demo/vite.config.ts b/demo/vite.config.ts new file mode 100644 index 00000000..4f2c747f --- /dev/null +++ b/demo/vite.config.ts @@ -0,0 +1,41 @@ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { glob } from 'glob'; +import { mergeConfig } from 'vite'; +import { codeSnippetPlugin } from '../scripts/codeSnippet.js'; +import viteConfig from '../vite.config.js'; + +const root = new URL('./', import.meta.url); + +function convertToId(str: string) { + return str.replace(/[-/]./gu, (x) => x[1].toUpperCase()); +} + +const dirs = Object.fromEntries( + await glob('./**/{index,iframe}.html', { cwd: root }).then((files) => + files + .filter((file) => !file.startsWith('@') && file !== 'index.html') + .map((name) => [convertToId(dirname(name)), fileURLToPath(new URL(name, root))]), + ), +); + +export default mergeConfig(viteConfig, { + base: './', + build: { + outDir: fileURLToPath(new URL('../.docs', root)), + rollupOptions: { + input: { + main: fileURLToPath(new URL('./index.html', root)), + ...dirs, + }, + }, + }, + resolve: { + alias: { + '@helpers/': fileURLToPath(new URL('./@helpers/', root)), + '@vaadin/router': fileURLToPath(new URL('../src/index.js', root)), + }, + }, + plugins: [codeSnippetPlugin()], + root: fileURLToPath(root), +}); diff --git a/docs/iron-component-page/iron-component-page.html b/docs/iron-component-page/iron-component-page.html deleted file mode 100644 index fa45fc1a..00000000 --- a/docs/iron-component-page/iron-component-page.html +++ /dev/null @@ -1,8926 +0,0 @@ - \ No newline at end of file diff --git a/docs/polymer/lib/utils/boot.html b/docs/polymer/lib/utils/boot.html deleted file mode 100644 index d1f275c9..00000000 --- a/docs/polymer/lib/utils/boot.html +++ /dev/null @@ -1,65 +0,0 @@ - - diff --git a/docs/polymer/lib/utils/import-href.html b/docs/polymer/lib/utils/import-href.html deleted file mode 100644 index 80d354bc..00000000 --- a/docs/polymer/lib/utils/import-href.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - diff --git a/docs/vaadin-router/analysis.json b/docs/vaadin-router/analysis.json deleted file mode 100644 index c2758d61..00000000 --- a/docs/vaadin-router/analysis.json +++ /dev/null @@ -1,1615 +0,0 @@ -{ - "schema_version": "1.0.0", - "namespaces": [ - { - "name": "Router", - "description": "", - "summary": "", - "sourceRange": { - "file": "src/documentation/namespace.js", - "start": { - "line": 3, - "column": 0 - }, - "end": { - "line": 3, - "column": 18 - } - }, - "classes": [ - { - "description": "This is a type declaration. It exists for build-time type checking and\ndocumentation purposes. It should not be used in any source code, and it\ncertainly does not exist at the run time.\n\n`Location` describes the state of a router at a given point in time. It is\navailable for your application code in several ways:\n - as the `router.location` property\n - as the `location` property set by Vaadin Router on every view Web Component\n - as the `location` argument passed by Vaadin Router into view Web Component\n lifecycle callbacks\n - as the `event.detail.location` of the global Vaadin Router events", - "summary": "Type declaration for the `router.location` property.", - "path": "src/documentation/location.js", - "properties": [ - { - "name": "baseUrl", - "type": "string", - "description": "The base URL used in the router. See [the `baseUrl` property\n](#/classes/Router#property-baseUrl) in the Router.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 25, - "column": 4 - }, - "end": { - "line": 25, - "column": 17 - } - }, - "metadata": {} - }, - { - "name": "pathname", - "type": "!string", - "description": "The pathname as entered in the browser address bar\n(e.g. `/users/42/messages/12/edit`). It always starts with a `/` (slash).", - "privacy": "public", - "sourceRange": { - "start": { - "line": 34, - "column": 4 - }, - "end": { - "line": 34, - "column": 18 - } - }, - "metadata": {} - }, - { - "name": "search", - "type": "!string", - "description": "The query string portion of the current url.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 42, - "column": 4 - }, - "end": { - "line": 42, - "column": 16 - } - }, - "metadata": {} - }, - { - "name": "searchParams", - "type": "URLSearchParams", - "description": "The query search parameters of the current url.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 50, - "column": 4 - }, - "end": { - "line": 50, - "column": 22 - } - }, - "metadata": {} - }, - { - "name": "hash", - "type": "!string", - "description": "The fragment identifier (including hash character) for the current page.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 58, - "column": 4 - }, - "end": { - "line": 58, - "column": 14 - } - }, - "metadata": {} - }, - { - "name": "redirectFrom", - "type": "?string", - "description": "(optional) The original pathname string in case if this location is a\nresult of a redirect.\n\nE.g. with the routes config as below a navigation to `/u/12` produces a\nlocation with `pathname: '/user/12'` and `redirectFrom: '/u/12'`.\n\n```javascript\nsetRoutes([\n {path: '/u/:id', redirect: '/user/:id'},\n {path: '/user/:id', component: 'x-user-view'},\n]);\n```\n\nSee the **Redirects** section of the\n[live demos](#/classes/Router/demos/demo/index.html) for more\ndetails.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 81, - "column": 4 - }, - "end": { - "line": 81, - "column": 22 - } - }, - "metadata": {} - }, - { - "name": "route", - "type": "?Route", - "description": "(optional) The route object associated with `this` Web Component instance.\n\nThis property is defined in the `location` objects that are passed as\nparameters into Web Component lifecycle callbacks, and the `location`\nproperty set by Vaadin Router on the Web Components.\n\nThis property is undefined in the `location` objects that are available\nas `router.location`, and in the `location` that is included into the\nglobal router event details.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 97, - "column": 4 - }, - "end": { - "line": 97, - "column": 15 - } - }, - "metadata": {} - }, - { - "name": "routes", - "type": "!Array.", - "description": "A list of route objects that match the current pathname. This list has\none element for each route that defines a parent layout, and then the\nelement for the route that defines the view.\n\nSee the **Getting Started** section of the\n[live demos](#/classes/Router/demos/demo/index.html) for more\ndetails on child routes and nested layouts.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 111, - "column": 4 - }, - "end": { - "line": 111, - "column": 16 - } - }, - "metadata": {} - }, - { - "name": "params", - "type": "!IndexedParams", - "description": "A bag of key-value pairs with parameters for the current location. Named\nparameters are available by name, unnamed ones - by index (e.g. for the\n`/users/:id` route the `:id` parameter is available as `location.params.id`).\n\nSee the **Route Parameters** section of the\n[live demos](#/classes/Router/demos/demo/index.html) for more\ndetails.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 125, - "column": 4 - }, - "end": { - "line": 125, - "column": 16 - } - }, - "metadata": {} - } - ], - "methods": [ - { - "name": "getUrl", - "description": "Returns a URL corresponding to the route path and the parameters of this\nlocation. When the parameters object is given in the arguments,\nthe argument parameters override the location ones.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 140, - "column": 2 - }, - "end": { - "line": 140, - "column": 20 - } - }, - "metadata": {}, - "params": [ - { - "name": "params", - "type": "Params=", - "description": "optional object with parameters to override.\nNamed parameters are passed by name (`params[name] = value`), unnamed\nparameters are passed by index (`params[index] = value`)." - } - ], - "return": { - "type": "string" - } - } - ], - "staticMethods": [], - "demos": [], - "metadata": {}, - "sourceRange": { - "start": { - "line": 16, - "column": 7 - }, - "end": { - "line": 141, - "column": 1 - } - }, - "privacy": "public", - "name": "Router.Location" - } - ] - } - ], - "classes": [ - { - "description": "", - "summary": "", - "path": "dist/vaadin-router.js", - "properties": [], - "methods": [], - "staticMethods": [], - "demos": [], - "metadata": {}, - "sourceRange": { - "start": { - "line": 89, - "column": 28 - }, - "end": { - "line": 89, - "column": 51 - } - }, - "privacy": "public", - "name": "NotFoundResult" - }, - { - "description": "", - "summary": "", - "path": "dist/vaadin-router.js", - "properties": [ - { - "name": "__effectiveBaseUrl", - "type": "?", - "description": "If the baseUrl property is set, transforms the baseUrl and returns the full\nactual `base` string for using in the `new URL(path, base);` and for\nprepernding the paths with. The returned base ends with a trailing slash.\n\nOtherwise, returns empty string.\n ", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1038, - "column": 2 - }, - "end": { - "line": 1045, - "column": 3 - } - }, - "metadata": {} - } - ], - "methods": [ - { - "name": "getRoutes", - "description": "Returns the current list of routes (as a shallow copy). Adding / removing\nroutes to / from the returned array does not affect the routing config,\nbut modifying the route objects does.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 895, - "column": 2 - }, - "end": { - "line": 897, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "!Array." - } - }, - { - "name": "setRoutes", - "description": "Sets the routing config (replacing the existing one).", - "privacy": "public", - "sourceRange": { - "start": { - "line": 905, - "column": 2 - }, - "end": { - "line": 909, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "routes", - "type": "(!Array. | !Router.Route)", - "description": "a single route or an array of those\n (the array is shallow copied)" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "addRoutes", - "description": "Appends one or several routes to the routing config and returns the\neffective routing config after the operation.", - "privacy": "protected", - "sourceRange": { - "start": { - "line": 920, - "column": 2 - }, - "end": { - "line": 924, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "routes", - "type": "(!Array. | !Router.Route)", - "description": "a single route or an array of those\n (the array is shallow copied)" - } - ], - "return": { - "type": "!Array." - } - }, - { - "name": "removeRoutes", - "description": "Removes all existing routes from the routing config.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 929, - "column": 2 - }, - "end": { - "line": 931, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "void" - } - }, - { - "name": "resolve", - "description": "Asynchronously resolves the given pathname, i.e. finds all routes matching\nthe pathname and tries resolving them one after another in the order they\nare listed in the routes config until the first non-null result.\n\nReturns a promise that is fulfilled with the return value of an object that consists of the first\nroute handler result that returns something other than `null` or `undefined` and context used to get this result.\n\nIf no route handlers return a non-null result, or if no route matches the\ngiven pathname the returned promise is rejected with a 'page not found'\n`Error`.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 950, - "column": 2 - }, - "end": { - "line": 1022, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "pathnameOrContext", - "type": "(!string | !{pathname: !string})", - "description": "the pathname to\n resolve or a context object with a `pathname` property and other\n properties to pass to the route resolver functions." - } - ], - "return": { - "type": "!Promise." - } - }, - { - "name": "__normalizePathname", - "description": "If the baseUrl is set, matches the pathname with the router’s baseUrl,\nand returns the local pathname with the baseUrl stripped out.\n\nIf the pathname does not match the baseUrl, returns undefined.\n\nIf the `baseUrl` is not set, returns the unmodified pathname argument.", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1055, - "column": 2 - }, - "end": { - "line": 1070, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "pathname" - } - ] - } - ], - "staticMethods": [ - { - "name": "__createUrl", - "description": "URL constructor polyfill hook. Creates and returns an URL instance.", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1027, - "column": 2 - }, - "end": { - "line": 1029, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "args", - "rest": true - } - ] - } - ], - "demos": [], - "metadata": {}, - "sourceRange": { - "start": { - "line": 874, - "column": 0 - }, - "end": { - "line": 1071, - "column": 1 - } - }, - "privacy": "public", - "name": "Resolver" - }, - { - "description": "A simple client-side router for single-page applications. It uses\nexpress-style middleware and has a first-class support for Web Components and\nlazy-loading. Works great in Polymer and non-Polymer apps.\n\nUse `new Router(outlet, options)` to create a new Router instance.\n\n* The `outlet` parameter is a reference to the DOM node to render\n the content into.\n\n* The `options` parameter is an optional object with options. The following\n keys are supported:\n * `baseUrl` — the initial value for [\n the `baseUrl` property\n ](#/classes/Router#property-baseUrl)\n\nThe Router instance is automatically subscribed to navigation events\non `window`.\n\nSee [Live Examples](#/classes/Router/demos/demo/index.html) for the detailed usage demo and code snippets.\n\nSee also detailed API docs for the following methods, for the advanced usage:\n\n* [setOutlet](#/classes/Router#method-setOutlet) – should be used to configure the outlet.\n* [setTriggers](#/classes/Router#method-setTriggers) – should be used to configure the navigation events.\n* [setRoutes](#/classes/Router#method-setRoutes) – should be used to configure the routes.\n\nOnly `setRoutes` has to be called manually, others are automatically invoked when creating a new instance.", - "summary": "JavaScript class that renders different DOM content depending on\n a given path. It can re-render when triggered or automatically on\n 'popstate' and / or 'click' events.", - "path": "dist/vaadin-router.js", - "properties": [ - { - "name": "__effectiveBaseUrl", - "type": "?", - "description": "If the baseUrl property is set, transforms the baseUrl and returns the full\nactual `base` string for using in the `new URL(path, base);` and for\nprepernding the paths with. The returned base ends with a trailing slash.\n\nOtherwise, returns empty string.\n ", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1038, - "column": 2 - }, - "end": { - "line": 1045, - "column": 3 - } - }, - "metadata": {}, - "inheritedFrom": "Resolver" - }, - { - "name": "baseUrl", - "type": "string", - "description": "The base URL for all routes in the router instance. By default,\nif the base element exists in the ``, vaadin-router\ntakes the `` attribute value, resolved against the current\n`document.URL`.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1410, - "column": 4 - }, - "end": { - "line": 1410, - "column": 17 - } - }, - "metadata": {} - }, - { - "name": "ready", - "type": "!Promise.", - "description": "A promise that is settled after the current render cycle completes. If\nthere is no render cycle in progress the promise is immediately settled\nwith the last render cycle result.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1420, - "column": 4 - }, - "end": { - "line": 1420, - "column": 15 - } - }, - "metadata": {} - }, - { - "name": "location", - "type": "!RouterLocation", - "description": "Contains read-only information about the current router location:\npathname, active routes, parameters. See the\n[Location type declaration](#/classes/RouterLocation)\nfor more details.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1432, - "column": 4 - }, - "end": { - "line": 1432, - "column": 18 - } - }, - "metadata": {} - } - ], - "methods": [ - { - "name": "getRoutes", - "description": "Returns the current list of routes (as a shallow copy). Adding / removing\nroutes to / from the returned array does not affect the routing config,\nbut modifying the route objects does.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 895, - "column": 2 - }, - "end": { - "line": 897, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "!Array." - }, - "inheritedFrom": "Resolver" - }, - { - "name": "setRoutes", - "description": "Sets the routing config (replacing the existing one) and triggers a\nnavigation event so that the router outlet is refreshed according to the\ncurrent `window.location` and the new routing config.\n\nEach route object may have the following properties, listed here in the processing order:\n* `path` – the route path (relative to the parent route if any) in the\n[express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths\").\n\n* `children` – an array of nested routes or a function that provides this\narray at the render time. The function can be synchronous or asynchronous:\nin the latter case the render is delayed until the returned promise is\nresolved. The `children` function is executed every time when this route is\nbeing rendered. This allows for dynamic route structures (e.g. backend-defined),\nbut it might have a performance impact as well. In order to avoid calling\nthe function on subsequent renders, you can override the `children` property\nof the route object and save the calculated array there\n(via `context.route.children = [ route1, route2, ...];`).\nParent routes are fully resolved before resolving the children. Children\n'path' values are relative to the parent ones.\n\n* `action` – the action that is executed before the route is resolved.\nThe value for this property should be a function, accepting `context`\nand `commands` parameters described below. If present, this function is\nalways invoked first, disregarding of the other properties' presence.\nThe action can return a result directly or within a `Promise`, which\nresolves to the result. If the action result is an `HTMLElement` instance,\na `commands.component(name)` result, a `commands.redirect(path)` result,\nor a `context.next()` result, the current route resolution is finished,\nand other route config properties are ignored.\nSee also **Route Actions** section in [Live Examples](#/classes/Router/demos/demo/index.html).\n\n* `redirect` – other route's path to redirect to. Passes all route parameters to the redirect target.\nThe target route should also be defined.\nSee also **Redirects** section in [Live Examples](#/classes/Router/demos/demo/index.html).\n\n* `component` – the tag name of the Web Component to resolve the route to.\nThe property is ignored when either an `action` returns the result or `redirect` property is present.\nIf route contains the `component` property (or an action that return a component)\nand its child route also contains the `component` property, child route's component\nwill be rendered as a light dom child of a parent component.\n\n* `name` – the string name of the route to use in the\n[`router.urlForName(name, params)`](#/classes/Router#method-urlForName)\nnavigation helper method.\n\nFor any route function (`action`, `children`) defined, the corresponding `route` object is available inside the callback\nthrough the `this` reference. If you need to access it, make sure you define the callback as a non-arrow function\nbecause arrow functions do not have their own `this` reference.\n\n`context` object that is passed to `action` function holds the following properties:\n* `context.pathname` – string with the pathname being resolved\n\n* `context.search` – search query string\n\n* `context.hash` – hash string\n\n* `context.params` – object with route parameters\n\n* `context.route` – object that holds the route that is currently being rendered.\n\n* `context.next()` – function for asynchronously getting the next route\ncontents from the resolution chain (if any)\n\n`commands` object that is passed to `action` function has\nthe following methods:\n\n* `commands.redirect(path)` – function that creates a redirect data\nfor the path specified.\n\n* `commands.component(component)` – function that creates a new HTMLElement\nwith current context. Note: the component created by this function is reused if visiting the same path twice in row.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1609, - "column": 2 - }, - "end": { - "line": 1617, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "routes", - "type": "(!Array. | !Route)", - "description": "a single route or an array of those" - }, - { - "name": "skipRender", - "type": "?boolean", - "defaultValue": "false", - "description": "configure the router but skip rendering the\n route corresponding to the current `window.location` values" - } - ], - "return": { - "type": "!Promise." - } - }, - { - "name": "addRoutes", - "description": "Appends one or several routes to the routing config and returns the\neffective routing config after the operation.", - "privacy": "protected", - "sourceRange": { - "start": { - "line": 920, - "column": 2 - }, - "end": { - "line": 924, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "routes", - "type": "(!Array. | !Router.Route)", - "description": "a single route or an array of those\n (the array is shallow copied)" - } - ], - "return": { - "type": "!Array." - }, - "inheritedFrom": "Resolver" - }, - { - "name": "removeRoutes", - "description": "Removes all existing routes from the routing config.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 929, - "column": 2 - }, - "end": { - "line": 931, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "void" - }, - "inheritedFrom": "Resolver" - }, - { - "name": "resolve", - "description": "Asynchronously resolves the given pathname, i.e. finds all routes matching\nthe pathname and tries resolving them one after another in the order they\nare listed in the routes config until the first non-null result.\n\nReturns a promise that is fulfilled with the return value of an object that consists of the first\nroute handler result that returns something other than `null` or `undefined` and context used to get this result.\n\nIf no route handlers return a non-null result, or if no route matches the\ngiven pathname the returned promise is rejected with a 'page not found'\n`Error`.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 950, - "column": 2 - }, - "end": { - "line": 1022, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "pathnameOrContext", - "type": "(!string | !{pathname: !string})", - "description": "the pathname to\n resolve or a context object with a `pathname` property and other\n properties to pass to the route resolver functions." - } - ], - "return": { - "type": "!Promise." - }, - "inheritedFrom": "Resolver" - }, - { - "name": "__normalizePathname", - "description": "If the baseUrl is set, matches the pathname with the router’s baseUrl,\nand returns the local pathname with the baseUrl stripped out.\n\nIf the pathname does not match the baseUrl, returns undefined.\n\nIf the `baseUrl` is not set, returns the unmodified pathname argument.", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1055, - "column": 2 - }, - "end": { - "line": 1070, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "pathname" - } - ], - "inheritedFrom": "Resolver" - }, - { - "name": "__resolveRoute", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1444, - "column": 2 - }, - "end": { - "line": 1501, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "context" - } - ] - }, - { - "name": "setOutlet", - "description": "Sets the router outlet (the DOM node where the content for the current\nroute is inserted). Any content pre-existing in the router outlet is\nremoved at the end of each render pass.\n\nNOTE: this method is automatically invoked first time when creating a new Router instance.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1513, - "column": 2 - }, - "end": { - "line": 1518, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "outlet", - "type": "?Node", - "description": "the DOM node where the content for the current route\n is inserted." - } - ], - "return": { - "type": "void" - } - }, - { - "name": "getOutlet", - "description": "Returns the current router outlet. The initial value is `undefined`.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1525, - "column": 2 - }, - "end": { - "line": 1527, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "?Node", - "desc": "the current router outlet (or `undefined`)" - } - }, - { - "name": "render", - "description": "Asynchronously resolves the given pathname and renders the resolved route\ncomponent into the router outlet. If no router outlet is set at the time of\ncalling this method, or at the time when the route resolution is completed,\na `TypeError` is thrown.\n\nReturns a promise that is fulfilled with the router outlet DOM Node after\nthe route component is created and inserted into the router outlet, or\nrejected if no route matches the given path.\n\nIf another render pass is started before the previous one is completed, the\nresult of the previous render pass is ignored.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 1640, - "column": 2 - }, - "end": { - "line": 1724, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "pathnameOrContext", - "type": "(!string | !{pathname: !string, search: ?string, hash: ?string})", - "description": "the pathname to render or a context object with a `pathname` property,\n optional `search` and `hash` properties, and other properties\n to pass to the resolver." - }, - { - "name": "shouldUpdateHistory", - "type": "boolean=", - "description": "update browser history with the rendered location" - } - ], - "return": { - "type": "!Promise." - } - }, - { - "name": "__fullyResolveChain", - "description": "and 'redirect' callback results.", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1737, - "column": 2 - }, - "end": { - "line": 1785, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "topOfTheChainContextBeforeRedirects" - }, - { - "name": "contextBeforeRedirects", - "defaultValue": "topOfTheChainContextBeforeRedirects" - } - ] - }, - { - "name": "__findComponentContextAfterAllRedirects", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1787, - "column": 2 - }, - "end": { - "line": 1807, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "context" - } - ] - }, - { - "name": "__amendWithOnBeforeCallbacks", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1809, - "column": 2 - }, - "end": { - "line": 1816, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "contextWithFullChain" - } - ] - }, - { - "name": "__runOnBeforeCallbacks", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1818, - "column": 2 - }, - "end": { - "line": 1890, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "newContext" - } - ] - }, - { - "name": "__runOnBeforeLeaveCallbacks", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1892, - "column": 2 - }, - "end": { - "line": 1904, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "callbacks" - }, - { - "name": "newContext" - }, - { - "name": "commands" - }, - { - "name": "chainElement" - } - ] - }, - { - "name": "__runOnBeforeEnterCallbacks", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1906, - "column": 2 - }, - "end": { - "line": 1914, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "callbacks" - }, - { - "name": "newContext" - }, - { - "name": "commands" - }, - { - "name": "chainElement" - } - ] - }, - { - "name": "__isReusableElement", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1916, - "column": 2 - }, - "end": { - "line": 1923, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "element" - }, - { - "name": "otherElement" - } - ] - }, - { - "name": "__isLatestRender", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1925, - "column": 2 - }, - "end": { - "line": 1927, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "context" - } - ] - }, - { - "name": "__redirect", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1929, - "column": 2 - }, - "end": { - "line": 1943, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "redirectData" - }, - { - "name": "counter" - }, - { - "name": "renderId" - } - ] - }, - { - "name": "__ensureOutlet", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1945, - "column": 2 - }, - "end": { - "line": 1949, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "outlet", - "defaultValue": "this.__outlet" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "__updateBrowserHistory", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1951, - "column": 2 - }, - "end": { - "line": 1960, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "{\n pathname,\n search = '',\n hash = ''\n}" - }, - { - "name": "replace" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "__copyUnchangedElements", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1962, - "column": 2 - }, - "end": { - "line": 1978, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "context" - }, - { - "name": "previousContext" - } - ] - }, - { - "name": "__addAppearingContent", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1980, - "column": 2 - }, - "end": { - "line": 2018, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "context" - }, - { - "name": "previousContext" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "__removeDisappearingContent", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2020, - "column": 2 - }, - "end": { - "line": 2026, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "void" - } - }, - { - "name": "__removeAppearingContent", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2028, - "column": 2 - }, - "end": { - "line": 2034, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "void" - } - }, - { - "name": "__runOnAfterLeaveCallbacks", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2036, - "column": 2 - }, - "end": { - "line": 2062, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "currentContext" - }, - { - "name": "targetContext" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "__runOnAfterEnterCallbacks", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2064, - "column": 2 - }, - "end": { - "line": 2077, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "currentContext" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "__animateIfNeeded", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2079, - "column": 2 - }, - "end": { - "line": 2101, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "context" - } - ] - }, - { - "name": "subscribe", - "description": "Subscribes this instance to navigation events on the `window`.\n\nNOTE: beware of resource leaks. For as long as a router instance is\nsubscribed to navigation events, it won't be garbage collected.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 2109, - "column": 2 - }, - "end": { - "line": 2111, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "void" - } - }, - { - "name": "unsubscribe", - "description": "Removes the subscription to navigation events created in the `subscribe()`\nmethod.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 2117, - "column": 2 - }, - "end": { - "line": 2119, - "column": 3 - } - }, - "metadata": {}, - "params": [], - "return": { - "type": "void" - } - }, - { - "name": "__onNavigationEvent", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2121, - "column": 2 - }, - "end": { - "line": 2129, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "event" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "urlForName", - "description": "Generates a URL for the route with the given name, optionally performing\nsubstitution of parameters.\n\nThe route is searched in all the Router instances subscribed to\nnavigation events.\n\n**Note:** For child route names, only array children are considered.\nIt is not possible to generate URLs using a name for routes set with\na children function.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 2170, - "column": 2 - }, - "end": { - "line": 2178, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "name", - "type": "!string", - "description": "the route name or the route’s `component` name." - }, - { - "name": "params", - "type": "Params=", - "description": "Optional object with route path parameters.\nNamed parameters are passed by name (`params[name] = value`), unnamed\nparameters are passed by index (`params[index] = value`)." - } - ], - "return": { - "type": "string" - } - }, - { - "name": "urlForPath", - "description": "Generates a URL for the given route path, optionally performing\nsubstitution of parameters.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 2191, - "column": 2 - }, - "end": { - "line": 2196, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "path", - "type": "!string", - "description": "string route path declared in [express.js syntax](https://expressjs.com/en/guide/routing.html#route-paths\")." - }, - { - "name": "params", - "type": "Params=", - "description": "Optional object with route path parameters.\nNamed parameters are passed by name (`params[name] = value`), unnamed\nparameters are passed by index (`params[index] = value`)." - } - ], - "return": { - "type": "string" - } - } - ], - "staticMethods": [ - { - "name": "__createUrl", - "description": "URL constructor polyfill hook. Creates and returns an URL instance.", - "privacy": "private", - "sourceRange": { - "start": { - "line": 1027, - "column": 2 - }, - "end": { - "line": 1029, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "args", - "rest": true - } - ], - "inheritedFrom": "Resolver" - }, - { - "name": "setTriggers", - "description": "Configures what triggers Router navigation events:\n - `POPSTATE`: popstate events on the current `window`\n - `CLICK`: click events on `` links leading to the current page\n\nThis method is invoked with the pre-configured values when creating a new Router instance.\nBy default, both `POPSTATE` and `CLICK` are enabled. This setup is expected to cover most of the use cases.\n\nSee the `router-config.js` for the default navigation triggers config. Based on it, you can\ncreate the own one and only import the triggers you need, instead of pulling in all the code,\ne.g. if you want to handle `click` differently.\n\nSee also **Navigation Triggers** section in [Live Examples](#/classes/Router/demos/demo/index.html).", - "privacy": "public", - "sourceRange": { - "start": { - "line": 2147, - "column": 2 - }, - "end": { - "line": 2149, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "triggers", - "type": "...NavigationTrigger", - "rest": true - } - ], - "return": { - "type": "void" - } - }, - { - "name": "go", - "description": "Triggers navigation to a new path. Returns a boolean without waiting until\nthe navigation is complete. Returns `true` if at least one `Router`\nhas handled the navigation (was subscribed and had `baseUrl` matching\nthe `path` argument), otherwise returns `false`.", - "privacy": "public", - "sourceRange": { - "start": { - "line": 2209, - "column": 2 - }, - "end": { - "line": 2214, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "path", - "type": "(!string | !{pathname: !string, search: (string | undefined), hash: (string | undefined)})", - "description": "a new in-app path string, or an URL-like object with `pathname`\n string property, and optional `search` and `hash` string properties." - } - ], - "return": { - "type": "boolean" - } - }, - { - "name": "__removeDomNodes", - "description": "", - "privacy": "private", - "sourceRange": { - "start": { - "line": 2216, - "column": 2 - }, - "end": { - "line": 2225, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "nodes" - } - ], - "return": { - "type": "void" - } - } - ], - "demos": [ - { - "url": "demo/index.html", - "description": "" - }, - { - "url": "demo/index.html", - "description": "" - } - ], - "metadata": {}, - "sourceRange": { - "start": { - "line": 1374, - "column": 0 - }, - "end": { - "line": 2226, - "column": 1 - } - }, - "privacy": "public", - "superclass": "Resolver", - "name": "Router" - }, - { - "description": "This interface describes the lifecycle callbacks supported by Vaadin Router\non view Web Components. It exists only for documentation purposes, i.e.\nyou _do not need_ to extend it in your code—defining a method with a\nmatching name is enough (this class does not exist at the run time).\n\nIf any of the methods described below are defined in a view Web Component,\nVaadin Router calls them at the corresponding points of the view\nlifecycle. Each method can either be synchronous or asynchronous (i.e. return\na Promise). In the latter case Vaadin Router waits until the promise is\nresolved and continues the navigation after that.\n\nCheck the [documentation on the `Router` class](#/classes/Router)\nto learn more.\n\nLifecycle callbacks are executed after the new path is resolved and after all\n`action` callbacks of the routes in the new path are executed.\n\nExample:\n\nFor the following routes definition,\n```\n// router and action declarations are omitted for brevity\nrouter.setRoutes([\n {path: '/a', action: actionA, children: [\n {path: '/b', action: actionB, component: 'component-b'},\n {path: '/c', action: actionC, component: 'component-c'}\n ]}\n]);\n```\nif the router first navigates to `/a/b` path and there was no view rendered\nbefore, the following events happen:\n* actionA\n* actionB\n* onBeforeEnterB (if defined in component-b)\n* outlet contents updated with component-b\n* onAfterEnterB (if defined in component-b)\n\nthen, if the router navigates to `/a/c`, the following events take place:\n* actionA\n* actionC\n* onBeforeLeaveB (if defined in component-b)\n* onBeforeEnterC (if defined in component-c)\n* onAfterLeaveB (if defined in component-b)\n* outlet contents updated with component-c\n* onAfterEnterC (if defined in component-c)\n\nIf a `Promise` is returned by any of the callbacks, it is resolved before\nproceeding further.\n\nAny of the `onBefore...` callbacks have a possibility to prevent\nthe navigation and fall back to the previous navigation result. If there is\nno result and this is the first resolution, an exception is thrown.\n\n`onAfter...` callbacks are considered as non-preventable, and their return\nvalue is ignored.\n\nOther examples can be found in the\n[live demos](#/classes/Router/demos/demo/index.html) and tests.", - "summary": "", - "path": "src/documentation/web-component-interface.js", - "properties": [], - "methods": [ - { - "name": "onBeforeLeave", - "description": "Method that gets executed when user navigates away from the component\nthat had defined the method. The user can prevent the navigation\nby returning `commands.prevent()` from the method or same value wrapped\nin `Promise`. This effectively means that the corresponding component\nshould be resolved by the router before the method can be executed.\nIf the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. In case of navigating to a different path\nbut within the same route object, e.g. the path has parameter or wildcard,\nand this results in rendering the same component instance, the method is called if available.\nThe WebComponent instance on which the callback has been invoked is available inside the callback through\nthe `this` reference.\n\nReturn values:\n\n* if the `commands.prevent()` result is returned (immediately or\nas a Promise), the navigation is aborted and the outlet contents\nis not updated.\n* any other return value is ignored and Vaadin Router proceeds with\nthe navigation.\n\nArguments:", - "privacy": "public", - "sourceRange": { - "start": { - "line": 97, - "column": 2 - }, - "end": { - "line": 102, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "location", - "description": "the `RouterLocation` object" - }, - { - "name": "commands", - "description": "the commands object with the following methods:\n\n| Property | Description\n| -------------------|-------------\n| `commands.prevent()` | function that creates a special object that can be returned to abort the current navigation and fall back to the last one. If there is no existing one, an exception is thrown." - }, - { - "name": "router", - "description": "the `Router` instance" - } - ] - }, - { - "name": "onBeforeEnter", - "description": "Method that gets executed before the outlet contents is updated with\nthe new element. The user can prevent the navigation by returning\n`commands.prevent()` from the method or same value wrapped in `Promise`.\nIf the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. In case of navigating to a different path\nbut within the same route object, e.g. the path has parameter or wildcard,\nand this results in rendering the same component instance, the method is called if available.\nThe WebComponent instance on which the callback has been invoked is available inside the callback through\nthe `this` reference.\n\nReturn values:\n\n* if the `commands.prevent()` result is returned (immediately or\nas a Promise), the navigation is aborted and the outlet contents\nis not updated.\n* if the `commands.redirect(path)` result is returned (immediately or\nas a Promise), Vaadin Router ends navigation to the current path, and\nstarts a new navigation cycle to the new path.\n* any other return value is ignored and Vaadin Router proceeds with\nthe navigation.\n\nArguments:", - "privacy": "public", - "sourceRange": { - "start": { - "line": 141, - "column": 2 - }, - "end": { - "line": 146, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "location", - "description": "the `RouterLocation` object" - }, - { - "name": "commands", - "description": "the commands object with the following methods:\n\n| Property | Description\n| -------------------------|-------------\n| `commands.redirect(path)` | function that creates a redirect data for the path specified, to use as a return value from the callback.\n| `commands.prevent()` | function that creates a special object that can be returned to abort the current navigation and fall back to the last one. If there is no existing one, an exception is thrown." - }, - { - "name": "router", - "description": "the `Router` instance" - } - ] - }, - { - "name": "onAfterLeave", - "description": "Method that gets executed when user navigates away from the component that\nhad defined the method, just before the element is to be removed\nfrom the DOM. The difference between this method and `onBeforeLeave`\nis that when this method is executed, there is no way to abort\nthe navigation. This effectively means that the corresponding component\nshould be resolved by the router before the method can be executed.\nIf the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. The WebComponent instance on which the callback\nhas been invoked is available inside the callback through\nthe `this` reference.\n\nReturn values: any return value is ignored and Vaadin Router proceeds with the navigation.\n\nArguments:", - "privacy": "public", - "sourceRange": { - "start": { - "line": 171, - "column": 2 - }, - "end": { - "line": 174, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "location", - "description": "the `RouterLocation` object" - }, - { - "name": "commands", - "description": "empty object" - }, - { - "name": "router", - "description": "the `Router` instance" - } - ], - "return": { - "type": "void" - } - }, - { - "name": "onAfterEnter", - "description": "Method that gets executed after the outlet contents is updated with the new\nelement. If the router navigates to the same path twice in a row, and this results\nin rendering the same component name (if the component is created\nusing `component` property in the route object) or the same component instance\n(if the component is created and returned inside `action` property of the route object),\nin the second time the method is not called. The WebComponent instance on which the callback\nhas been invoked is available inside the callback through\nthe `this` reference.\n\nThis callback is called asynchronously after the native\n[`connectedCallback()`](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions)\ndefined by the Custom Elements spec.\n\nReturn values: any return value is ignored and Vaadin Router proceeds with the navigation.\n\nArguments:", - "privacy": "public", - "sourceRange": { - "start": { - "line": 198, - "column": 2 - }, - "end": { - "line": 201, - "column": 3 - } - }, - "metadata": {}, - "params": [ - { - "name": "location", - "description": "the `RouterLocation` object" - }, - { - "name": "commands", - "description": "empty object" - }, - { - "name": "router", - "description": "the `Router` instance" - } - ], - "return": { - "type": "void" - } - } - ], - "staticMethods": [], - "demos": [], - "metadata": {}, - "sourceRange": { - "start": { - "line": 61, - "column": 7 - }, - "end": { - "line": 202, - "column": 1 - } - }, - "privacy": "public", - "name": "WebComponentInterface" - } - ] -} diff --git a/docs/vaadin-router/demo/demo-elements/bundle-script.js b/docs/vaadin-router/demo/demo-elements/bundle-script.js deleted file mode 100644 index 57a7e56f..00000000 --- a/docs/vaadin-router/demo/demo-elements/bundle-script.js +++ /dev/null @@ -1 +0,0 @@ -window.bundleScriptTestVariable="Hello from bundle script!"; \ No newline at end of file diff --git a/docs/vaadin-router/demo/demo-elements/user.bundle.html b/docs/vaadin-router/demo/demo-elements/user.bundle.html deleted file mode 100644 index 9f346de8..00000000 --- a/docs/vaadin-router/demo/demo-elements/user.bundle.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/vaadin-router/demo/demo-elements/user.bundle.js b/docs/vaadin-router/demo/demo-elements/user.bundle.js deleted file mode 100644 index 2cc002bb..00000000 --- a/docs/vaadin-router/demo/demo-elements/user.bundle.js +++ /dev/null @@ -1 +0,0 @@ -function _createSuper(Derived){var hasNativeReflectConstruct=_isNativeReflectConstruct();return function _createSuperInternal(){var Super=babelHelpers.getPrototypeOf(Derived),result;if(hasNativeReflectConstruct){var NewTarget=babelHelpers.getPrototypeOf(this).constructor;result=Reflect.construct(Super,arguments,NewTarget)}else{result=Super.apply(this,arguments)}return babelHelpers.possibleConstructorReturn(this,result)}}function _isNativeReflectConstruct(){if("undefined"===typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"===typeof Proxy)return!0;try{Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}));return!0}catch(e){return!1}}(function(){var XUserJsBundleView=/*#__PURE__*/function(_Polymer$Element){"use strict";babelHelpers.inherits(XUserJsBundleView,_Polymer$Element);var _super=_createSuper(XUserJsBundleView);function XUserJsBundleView(){babelHelpers.classCallCheck(this,XUserJsBundleView);return _super.apply(this,arguments)}babelHelpers.createClass(XUserJsBundleView,null,[{key:"is",get:function get(){return"x-user-js-bundle-view"}},{key:"template",get:function get(){var tpl=document.createElement("template");tpl.innerHTML="\n

User Profile

\n

User id: [[location.params.id]]. This view was loaded using JS bundle.

\n ";return tpl}}]);return XUserJsBundleView}(Polymer.Element);customElements.define(XUserJsBundleView.is,XUserJsBundleView)})(); \ No newline at end of file diff --git a/docs/vaadin-router/demo/demo-elements/users-routes.js b/docs/vaadin-router/demo/demo-elements/users-routes.js deleted file mode 100644 index a4ef056f..00000000 --- a/docs/vaadin-router/demo/demo-elements/users-routes.js +++ /dev/null @@ -1 +0,0 @@ -(function(){window.Vaadin=window.Vaadin||{};Vaadin.Demo=Vaadin.Demo||{};Vaadin.Demo.moduleStorage=Vaadin.Demo.moduleStorage||[];var usersRoutes=[{path:"/",component:"x-user-home"},{path:"/:user",component:"x-user-profile"}];Vaadin.Demo.moduleStorage.push({default:usersRoutes})})(); \ No newline at end of file diff --git a/docs/vaadin-router/demo/demo-shell.html b/docs/vaadin-router/demo/demo-shell.html deleted file mode 100644 index e114810b..00000000 --- a/docs/vaadin-router/demo/demo-shell.html +++ /dev/null @@ -1,7705 +0,0 @@ - \ No newline at end of file diff --git a/docs/vaadin-router/demo/demos.json b/docs/vaadin-router/demo/demos.json deleted file mode 100644 index f357fd5e..00000000 --- a/docs/vaadin-router/demo/demos.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "name": "Vaadin Router", - "pages": [ - { - "name": "Getting Started", - "url": "vaadin-router-getting-started-demos", - "src": "vaadin-router-getting-started-demos.html", - "meta": { - "title": "Vaadin Router Getting Started Examples", - "description": "", - "image": "" - } - }, - { - "name": "Route Parameters", - "url": "vaadin-router-route-parameters-demos", - "src": "vaadin-router-route-parameters-demos.html", - "meta": { - "title": "Vaadin Router Route Parameters Examples", - "description": "", - "image": "" - } - }, - { - "name": "URL Generation", - "url": "vaadin-router-url-generation-demos", - "src": "vaadin-router-url-generation-demos.html", - "meta": { - "title": "Vaadin Router URL Generation Examples", - "description": "", - "image": "" - } - }, - { - "name": "Redirects", - "url": "vaadin-router-redirect-demos", - "src": "vaadin-router-redirect-demos.html", - "meta": { - "title": "Vaadin Router Redirect Examples", - "description": "", - "image": "" - } - }, - { - "name": "Lifecycle Callbacks", - "url": "vaadin-router-lifecycle-callbacks-demos", - "src": "vaadin-router-lifecycle-callbacks-demos.html", - "meta": { - "title": "Vaadin Router Lifecycle Callbacks Examples", - "description": "", - "image": "" - } - }, - { - "name": "Code Splitting", - "url": "vaadin-router-code-splitting-demos", - "src": "vaadin-router-code-splitting-demos.html", - "meta": { - "title": "Vaadin Router Code Splitting Examples", - "description": "", - "image": "" - } - }, - { - "name": "Animations", - "url": "vaadin-router-animated-transitions-demos", - "src": "vaadin-router-animated-transitions-demos.html", - "meta": { - "title": "Vaadin Router Transitions Examples", - "description": "", - "image": "" - } - }, - { - "name": "Route Actions", - "url": "vaadin-router-route-actions-demos", - "src": "vaadin-router-route-actions-demos.html", - "meta": { - "title": "Vaadin Router Route Actions Examples", - "description": "", - "image": "" - } - }, - { - "name": "Navigation Triggers", - "url": "vaadin-router-navigation-trigger-demos", - "src": "vaadin-router-navigation-trigger-demos.html", - "meta": { - "title": "Vaadin Router Navigation Trigger Examples", - "description": "", - "image": "" - } - } - ] -} diff --git a/docs/vaadin-router/demo/iframe.html b/docs/vaadin-router/demo/iframe.html deleted file mode 100644 index a72b9ad3..00000000 --- a/docs/vaadin-router/demo/iframe.html +++ /dev/null @@ -1,110 +0,0 @@ -Vaadin Router DemoWaiting for a 'set-template' message... \ No newline at end of file diff --git a/docs/vaadin-router/demo/index.html b/docs/vaadin-router/demo/index.html deleted file mode 100644 index 2837f8ec..00000000 --- a/docs/vaadin-router/demo/index.html +++ /dev/null @@ -1,110 +0,0 @@ -Vaadin Router Demos \ No newline at end of file diff --git a/docs/vaadin-router/demo/vaadin-router-animated-transitions-demos.html b/docs/vaadin-router/demo/vaadin-router-animated-transitions-demos.html deleted file mode 100644 index be83aef0..00000000 --- a/docs/vaadin-router/demo/vaadin-router-animated-transitions-demos.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-code-splitting-demos.html b/docs/vaadin-router/demo/vaadin-router-code-splitting-demos.html deleted file mode 100644 index f11678cb..00000000 --- a/docs/vaadin-router/demo/vaadin-router-code-splitting-demos.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-getting-started-demos.html b/docs/vaadin-router/demo/vaadin-router-getting-started-demos.html deleted file mode 100644 index 6306251a..00000000 --- a/docs/vaadin-router/demo/vaadin-router-getting-started-demos.html +++ /dev/null @@ -1,339 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-lifecycle-callbacks-demos.html b/docs/vaadin-router/demo/vaadin-router-lifecycle-callbacks-demos.html deleted file mode 100644 index 096bcdb4..00000000 --- a/docs/vaadin-router/demo/vaadin-router-lifecycle-callbacks-demos.html +++ /dev/null @@ -1,522 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-navigation-trigger-demos.html b/docs/vaadin-router/demo/vaadin-router-navigation-trigger-demos.html deleted file mode 100644 index f3623279..00000000 --- a/docs/vaadin-router/demo/vaadin-router-navigation-trigger-demos.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-redirect-demos.html b/docs/vaadin-router/demo/vaadin-router-redirect-demos.html deleted file mode 100644 index 67b08e5c..00000000 --- a/docs/vaadin-router/demo/vaadin-router-redirect-demos.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-route-actions-demos.html b/docs/vaadin-router/demo/vaadin-router-route-actions-demos.html deleted file mode 100644 index de4ac5bf..00000000 --- a/docs/vaadin-router/demo/vaadin-router-route-actions-demos.html +++ /dev/null @@ -1,308 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-route-parameters-demos.html b/docs/vaadin-router/demo/vaadin-router-route-parameters-demos.html deleted file mode 100644 index bf155c18..00000000 --- a/docs/vaadin-router/demo/vaadin-router-route-parameters-demos.html +++ /dev/null @@ -1,267 +0,0 @@ - - - - diff --git a/docs/vaadin-router/demo/vaadin-router-url-generation-demos.html b/docs/vaadin-router/demo/vaadin-router-url-generation-demos.html deleted file mode 100644 index 25db4abd..00000000 --- a/docs/vaadin-router/demo/vaadin-router-url-generation-demos.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - diff --git a/docs/vaadin-router/index.html b/docs/vaadin-router/index.html deleted file mode 100644 index cc18f5be..00000000 --- a/docs/vaadin-router/index.html +++ /dev/null @@ -1,111 +0,0 @@ -Vaadin Router \ No newline at end of file diff --git a/docs/webcomponentsjs/custom-elements-es5-adapter.js b/docs/webcomponentsjs/custom-elements-es5-adapter.js deleted file mode 100644 index 89aae0bd..00000000 --- a/docs/webcomponentsjs/custom-elements-es5-adapter.js +++ /dev/null @@ -1,15 +0,0 @@ -/** -@license @nocompile -Copyright (c) 2018 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ -(function () { -'use strict'; - -(function(){if(void 0===window.Reflect||void 0===window.customElements||window.customElements.hasOwnProperty('polyfillWrapFlushCallback'))return;const a=HTMLElement;window.HTMLElement=function HTMLElement(){return Reflect.construct(a,[],this.constructor)},HTMLElement.prototype=a.prototype,HTMLElement.prototype.constructor=HTMLElement,Object.setPrototypeOf(HTMLElement,a);})(); - -}()); diff --git a/docs/webcomponentsjs/gulpfile.js b/docs/webcomponentsjs/gulpfile.js deleted file mode 100644 index 7705ca0e..00000000 --- a/docs/webcomponentsjs/gulpfile.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * @license - * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt - */ - -'use strict'; - -/* eslint-env node */ -/* eslint-disable no-console */ - -const gulp = require('gulp'); -const sourcemaps = require('gulp-sourcemaps'); -const rename = require('gulp-rename'); -const rollup = require('rollup-stream'); -const source = require('vinyl-source-stream'); -const del = require('del'); -const runseq = require('run-sequence'); -const closure = require('google-closure-compiler').gulp(); -const babel = require('rollup-plugin-babel'); -const license = require('rollup-plugin-license'); - -function debugify(sourceName, fileName, extraRollupOptions) { - if (!fileName) - fileName = sourceName; - - const options = { - entry: `./entrypoints/${sourceName}-index.js`, - format: 'iife', - moduleName: 'webcomponentsjs' - }; - - Object.assign(options, extraRollupOptions); - - return rollup(options) - .pipe(source(`${sourceName}-index.js`), 'entrypoints') - .pipe(rename(fileName + '.js')) - .pipe(gulp.dest('./')) -} - -function closurify(sourceName, fileName) { - if (!fileName) { - fileName = sourceName; - } - - const closureOptions = { - compilation_level: 'ADVANCED', - language_in: 'ES6_STRICT', - language_out: 'ES5_STRICT', - isolation_mode: 'NONE', - output_wrapper_file: 'closure-output.txt', - assume_function_wrapper: true, - js_output_file: `${fileName}.js`, - warning_level: 'VERBOSE', - rewrite_polyfills: false, - module_resolution: 'NODE', - entry_point: `entrypoints/${sourceName}-index.js`, - dependency_mode: 'STRICT', - externs: [ - 'externs/webcomponents.js', - 'node_modules/@webcomponents/custom-elements/externs/custom-elements.js', - 'node_modules/@webcomponents/html-imports/externs/html-imports.js', - 'node_modules/@webcomponents/shadycss/externs/shadycss-externs.js', - 'node_modules/@webcomponents/shadydom/externs/shadydom.js' - ] - }; - - return gulp.src([ - 'entrypoints/*.js', - 'src/*.js', - 'node_modules/promise-polyfill/src/*.js', - 'node_modules/@webcomponents/**/*.js', - '!node_modules/@webcomponents/*/externs/*.js', - '!node_modules/@webcomponents/*/node_modules/**', - '!**/bower_components/**' - ], {base: './', follow: true}) - .pipe(sourcemaps.init()) - .pipe(closure(closureOptions)) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest('.')); -} - -gulp.task('debugify-hi', () => { - return debugify('webcomponents-hi') -}); - -gulp.task('debugify-hi-ce', () => { - return debugify('webcomponents-hi-ce') -}); - -gulp.task('debugify-hi-sd-ce', () => { - return debugify('webcomponents-hi-sd-ce') -}); - -gulp.task('debugify-hi-sd-ce-pf', () => { - // The es6-promise polyfill needs to set the correct context. - // See https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined - const extraOptions = {context: 'window'}; - return debugify('webcomponents-hi-sd-ce-pf', 'webcomponents-lite', extraOptions) -}); - -gulp.task('debugify-sd-ce', () => { - return debugify('webcomponents-sd-ce') -}); - -gulp.task('debugify-ce', () => { - return debugify('webcomponents-ce') -}); - -gulp.task('debugify-sd', () => { - return debugify('webcomponents-sd') -}); - -gulp.task('debugify-hi-sd', () => { - return debugify('webcomponents-hi-sd') -}); - -gulp.task('closurify-hi', () => { - return closurify('webcomponents-hi') -}); - -gulp.task('closurify-hi-ce', () => { - return closurify('webcomponents-hi-ce') -}); - -gulp.task('closurify-hi-sd-ce', () => { - return closurify('webcomponents-hi-sd-ce') -}); - -gulp.task('closurify-hi-sd-ce-pf', () => { - return closurify('webcomponents-hi-sd-ce-pf', 'webcomponents-lite') -}); - -gulp.task('closurify-sd-ce', () => { - return closurify('webcomponents-sd-ce') -}); - -gulp.task('closurify-hi-sd', () => { - return closurify('webcomponents-hi-sd') -}); - -gulp.task('closurify-ce', () => { - return closurify('webcomponents-ce') -}) - -gulp.task('closurify-sd', () => { - return closurify('webcomponents-sd') -}) - -const babelOptions = { - presets: [ - ['minify', {'keepFnName': true}], - ], -}; - -gulp.task('debugify-ce-es5-adapter', () => { - return debugify('custom-elements-es5-adapter', '', { - plugins: [ - babel(babelOptions), - license({ - banner: { - file: './license-header.txt' - } - }) - ]}); -}); - -gulp.task('default', ['closure']); - -gulp.task('clean-builds', () => { - return del(['custom-elements-es5-adapter.js{,.map}', 'webcomponents*.js{,.map}', '!webcomponents-loader.js']); -}); - -gulp.task('debug', (cb) => { - const tasks = [ - 'debugify-ce', - 'debugify-hi', - 'debugify-hi-ce', - 'debugify-hi-sd', - 'debugify-hi-sd-ce', - 'debugify-hi-sd-ce-pf', - 'debugify-sd', - 'debugify-sd-ce', - 'debugify-ce-es5-adapter' - ]; - runseq('clean-builds', tasks, cb); -}); - -gulp.task('closure', (cb) => { - const tasks = [ - 'closurify-ce', - 'closurify-hi', - 'closurify-hi-ce', - 'closurify-hi-sd', - 'closurify-hi-sd-ce', - 'closurify-hi-sd-ce-pf', - 'closurify-sd', - 'closurify-sd-ce', - 'debugify-ce-es5-adapter' - ]; - runseq('clean-builds', ...tasks, cb); -}); diff --git a/docs/webcomponentsjs/webcomponents-ce.js b/docs/webcomponentsjs/webcomponents-ce.js deleted file mode 100644 index b1bcb48a..00000000 --- a/docs/webcomponentsjs/webcomponents-ce.js +++ /dev/null @@ -1,57 +0,0 @@ -/** -@license @nocompile -Copyright (c) 2018 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ -(function(){'use strict';var aa=new Set("annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" "));function g(b){var a=aa.has(b);b=/^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$/.test(b);return!a&&b}function l(b){var a=b.isConnected;if(void 0!==a)return a;for(;b&&!(b.__CE_isImportDocument||b instanceof Document);)b=b.parentNode||(window.ShadowRoot&&b instanceof ShadowRoot?b.host:void 0);return!(!b||!(b.__CE_isImportDocument||b instanceof Document))} -function p(b,a){for(;a&&a!==b&&!a.nextSibling;)a=a.parentNode;return a&&a!==b?a.nextSibling:null} -function q(b,a,d){d=void 0===d?new Set:d;for(var c=b;c;){if(c.nodeType===Node.ELEMENT_NODE){var e=c;a(e);var f=e.localName;if("link"===f&&"import"===e.getAttribute("rel")){c=e.import;if(c instanceof Node&&!d.has(c))for(d.add(c),c=c.firstChild;c;c=c.nextSibling)q(c,a,d);c=p(b,e);continue}else if("template"===f){c=p(b,e);continue}if(e=e.__CE_shadowRoot)for(e=e.firstChild;e;e=e.nextSibling)q(e,a,d)}c=c.firstChild?c.firstChild:p(b,c)}}function t(b,a,d){b[a]=d};function u(){this.a=new Map;this.f=new Map;this.c=[];this.b=!1}function ba(b,a,d){b.a.set(a,d);b.f.set(d.constructorFunction,d)}function v(b,a){b.b=!0;b.c.push(a)}function w(b,a){b.b&&q(a,function(a){return x(b,a)})}function x(b,a){if(b.b&&!a.__CE_patched){a.__CE_patched=!0;for(var d=0;d=} visitedImports\n */\nexport function walkDeepDescendantElements(root, callback, visitedImports = new Set()) {\n let node = root;\n while (node) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = /** @type {!Element} */(node);\n\n callback(element);\n\n const localName = element.localName;\n if (localName === 'link' && element.getAttribute('rel') === 'import') {\n // If this import (polyfilled or not) has it's root node available,\n // walk it.\n const importNode = /** @type {!Node} */ (element.import);\n if (importNode instanceof Node && !visitedImports.has(importNode)) {\n // Prevent multiple walks of the same import root.\n visitedImports.add(importNode);\n\n for (let child = importNode.firstChild; child; child = child.nextSibling) {\n walkDeepDescendantElements(child, callback, visitedImports);\n }\n }\n\n // Ignore descendants of import links to prevent attempting to walk the\n // elements created by the HTML Imports polyfill that we just walked\n // above.\n node = nextSiblingOrAncestorSibling(root, element);\n continue;\n } else if (localName === 'template') {\n // Ignore descendants of templates. There shouldn't be any descendants\n // because they will be moved into `.content` during construction in\n // browsers that support template but, in case they exist and are still\n // waiting to be moved by a polyfill, they will be ignored.\n node = nextSiblingOrAncestorSibling(root, element);\n continue;\n }\n\n // Walk shadow roots.\n const shadowRoot = element.__CE_shadowRoot;\n if (shadowRoot) {\n for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {\n walkDeepDescendantElements(child, callback, visitedImports);\n }\n }\n }\n\n node = nextNode(root, node);\n }\n}\n\n/**\n * Used to suppress Closure's \"Modifying the prototype is only allowed if the\n * constructor is in the same scope\" warning without using\n * `@suppress {newCheckTypes, duplicate}` because `newCheckTypes` is too broad.\n *\n * @param {!Object} destination\n * @param {string} name\n * @param {*} value\n */\nexport function setPropertyUnchecked(destination, name, value) {\n destination[name] = value;\n}\n","import * as Utilities from './Utilities.js';\nimport CEState from './CustomElementState.js';\n\nexport default class CustomElementInternals {\n constructor() {\n /** @type {!Map} */\n this._localNameToDefinition = new Map();\n\n /** @type {!Map} */\n this._constructorToDefinition = new Map();\n\n /** @type {!Array} */\n this._patches = [];\n\n /** @type {boolean} */\n this._hasPatches = false;\n }\n\n /**\n * @param {string} localName\n * @param {!CustomElementDefinition} definition\n */\n setDefinition(localName, definition) {\n this._localNameToDefinition.set(localName, definition);\n this._constructorToDefinition.set(definition.constructorFunction, definition);\n }\n\n /**\n * @param {string} localName\n * @return {!CustomElementDefinition|undefined}\n */\n localNameToDefinition(localName) {\n return this._localNameToDefinition.get(localName);\n }\n\n /**\n * @param {!Function} constructor\n * @return {!CustomElementDefinition|undefined}\n */\n constructorToDefinition(constructor) {\n return this._constructorToDefinition.get(constructor);\n }\n\n /**\n * @param {!function(!Node)} listener\n */\n addPatch(listener) {\n this._hasPatches = true;\n this._patches.push(listener);\n }\n\n /**\n * @param {!Node} node\n */\n patchTree(node) {\n if (!this._hasPatches) return;\n\n Utilities.walkDeepDescendantElements(node, element => this.patch(element));\n }\n\n /**\n * @param {!Node} node\n */\n patch(node) {\n if (!this._hasPatches) return;\n\n if (node.__CE_patched) return;\n node.__CE_patched = true;\n\n for (let i = 0; i < this._patches.length; i++) {\n this._patches[i](node);\n }\n }\n\n /**\n * @param {!Node} root\n */\n connectTree(root) {\n const elements = [];\n\n Utilities.walkDeepDescendantElements(root, element => elements.push(element));\n\n for (let i = 0; i < elements.length; i++) {\n const element = elements[i];\n if (element.__CE_state === CEState.custom) {\n this.connectedCallback(element);\n } else {\n this.upgradeElement(element);\n }\n }\n }\n\n /**\n * @param {!Node} root\n */\n disconnectTree(root) {\n const elements = [];\n\n Utilities.walkDeepDescendantElements(root, element => elements.push(element));\n\n for (let i = 0; i < elements.length; i++) {\n const element = elements[i];\n if (element.__CE_state === CEState.custom) {\n this.disconnectedCallback(element);\n }\n }\n }\n\n /**\n * Upgrades all uncustomized custom elements at and below a root node for\n * which there is a definition. When custom element reaction callbacks are\n * assumed to be called synchronously (which, by the current DOM / HTML spec\n * definitions, they are *not*), callbacks for both elements customized\n * synchronously by the parser and elements being upgraded occur in the same\n * relative order.\n *\n * NOTE: This function, when used to simulate the construction of a tree that\n * is already created but not customized (i.e. by the parser), does *not*\n * prevent the element from reading the 'final' (true) state of the tree. For\n * example, the element, during truly synchronous parsing / construction would\n * see that it contains no children as they have not yet been inserted.\n * However, this function does not modify the tree, the element will\n * (incorrectly) have children. Additionally, self-modification restrictions\n * for custom element constructors imposed by the DOM spec are *not* enforced.\n *\n *\n * The following nested list shows the steps extending down from the HTML\n * spec's parsing section that cause elements to be synchronously created and\n * upgraded:\n *\n * The \"in body\" insertion mode:\n * https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody\n * - Switch on token:\n * .. other cases ..\n * -> Any other start tag\n * - [Insert an HTML element](below) for the token.\n *\n * Insert an HTML element:\n * https://html.spec.whatwg.org/multipage/syntax.html#insert-an-html-element\n * - Insert a foreign element for the token in the HTML namespace:\n * https://html.spec.whatwg.org/multipage/syntax.html#insert-a-foreign-element\n * - Create an element for a token:\n * https://html.spec.whatwg.org/multipage/syntax.html#create-an-element-for-the-token\n * - Will execute script flag is true?\n * - (Element queue pushed to the custom element reactions stack.)\n * - Create an element:\n * https://dom.spec.whatwg.org/#concept-create-element\n * - Sync CE flag is true?\n * - Constructor called.\n * - Self-modification restrictions enforced.\n * - Sync CE flag is false?\n * - (Upgrade reaction enqueued.)\n * - Attributes appended to element.\n * (`attributeChangedCallback` reactions enqueued.)\n * - Will execute script flag is true?\n * - (Element queue popped from the custom element reactions stack.\n * Reactions in the popped stack are invoked.)\n * - (Element queue pushed to the custom element reactions stack.)\n * - Insert the element:\n * https://dom.spec.whatwg.org/#concept-node-insert\n * - Shadow-including descendants are connected. During parsing\n * construction, there are no shadow-*excluding* descendants.\n * However, the constructor may have validly attached a shadow\n * tree to itself and added descendants to that shadow tree.\n * (`connectedCallback` reactions enqueued.)\n * - (Element queue popped from the custom element reactions stack.\n * Reactions in the popped stack are invoked.)\n *\n * @param {!Node} root\n * @param {{\n * visitedImports: (!Set|undefined),\n * upgrade: (!function(!Element)|undefined),\n * }=} options\n */\n patchAndUpgradeTree(root, options = {}) {\n const visitedImports = options.visitedImports || new Set();\n const upgrade = options.upgrade || (element => this.upgradeElement(element));\n\n const elements = [];\n\n const gatherElements = element => {\n if (element.localName === 'link' && element.getAttribute('rel') === 'import') {\n // The HTML Imports polyfill sets a descendant element of the link to\n // the `import` property, specifically this is *not* a Document.\n const importNode = /** @type {?Node} */ (element.import);\n\n if (importNode instanceof Node) {\n importNode.__CE_isImportDocument = true;\n // Connected links are associated with the registry.\n importNode.__CE_hasRegistry = true;\n }\n\n if (importNode && importNode.readyState === 'complete') {\n importNode.__CE_documentLoadHandled = true;\n } else {\n // If this link's import root is not available, its contents can't be\n // walked. Wait for 'load' and walk it when it's ready.\n element.addEventListener('load', () => {\n const importNode = /** @type {!Node} */ (element.import);\n\n if (importNode.__CE_documentLoadHandled) return;\n importNode.__CE_documentLoadHandled = true;\n\n // Clone the `visitedImports` set that was populated sync during\n // the `patchAndUpgradeTree` call that caused this 'load' handler to\n // be added. Then, remove *this* link's import node so that we can\n // walk that import again, even if it was partially walked later\n // during the same `patchAndUpgradeTree` call.\n const clonedVisitedImports = new Set(visitedImports);\n clonedVisitedImports.delete(importNode);\n\n this.patchAndUpgradeTree(importNode, {visitedImports: clonedVisitedImports, upgrade});\n });\n }\n } else {\n elements.push(element);\n }\n };\n\n // `walkDeepDescendantElements` populates (and internally checks against)\n // `visitedImports` when traversing a loaded import.\n Utilities.walkDeepDescendantElements(root, gatherElements, visitedImports);\n\n if (this._hasPatches) {\n for (let i = 0; i < elements.length; i++) {\n this.patch(elements[i]);\n }\n }\n\n for (let i = 0; i < elements.length; i++) {\n upgrade(elements[i]);\n }\n }\n\n /**\n * @param {!HTMLElement} element\n */\n upgradeElement(element) {\n const currentState = element.__CE_state;\n if (currentState !== undefined) return;\n\n // Prevent elements created in documents without a browsing context from\n // upgrading.\n //\n // https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-definition\n // \"If document does not have a browsing context, return null.\"\n //\n // https://html.spec.whatwg.org/multipage/window-object.html#dom-document-defaultview\n // \"The defaultView IDL attribute of the Document interface, on getting,\n // must return this Document's browsing context's WindowProxy object, if\n // this Document has an associated browsing context, or null otherwise.\"\n const ownerDocument = element.ownerDocument;\n if (\n !ownerDocument.defaultView &&\n !(ownerDocument.__CE_isImportDocument && ownerDocument.__CE_hasRegistry)\n ) return;\n\n const definition = this.localNameToDefinition(element.localName);\n if (!definition) return;\n\n definition.constructionStack.push(element);\n\n const constructor = definition.constructorFunction;\n try {\n try {\n let result = new (constructor)();\n if (result !== element) {\n throw new Error('The custom element constructor did not produce the element being upgraded.');\n }\n } finally {\n definition.constructionStack.pop();\n }\n } catch (e) {\n element.__CE_state = CEState.failed;\n throw e;\n }\n\n element.__CE_state = CEState.custom;\n element.__CE_definition = definition;\n\n if (definition.attributeChangedCallback) {\n const observedAttributes = definition.observedAttributes;\n for (let i = 0; i < observedAttributes.length; i++) {\n const name = observedAttributes[i];\n const value = element.getAttribute(name);\n if (value !== null) {\n this.attributeChangedCallback(element, name, null, value, null);\n }\n }\n }\n\n if (Utilities.isConnected(element)) {\n this.connectedCallback(element);\n }\n }\n\n /**\n * @param {!Element} element\n */\n connectedCallback(element) {\n const definition = element.__CE_definition;\n if (definition.connectedCallback) {\n definition.connectedCallback.call(element);\n }\n }\n\n /**\n * @param {!Element} element\n */\n disconnectedCallback(element) {\n const definition = element.__CE_definition;\n if (definition.disconnectedCallback) {\n definition.disconnectedCallback.call(element);\n }\n }\n\n /**\n * @param {!Element} element\n * @param {string} name\n * @param {?string} oldValue\n * @param {?string} newValue\n * @param {?string} namespace\n */\n attributeChangedCallback(element, name, oldValue, newValue, namespace) {\n const definition = element.__CE_definition;\n if (\n definition.attributeChangedCallback &&\n definition.observedAttributes.indexOf(name) > -1\n ) {\n definition.attributeChangedCallback.call(element, name, oldValue, newValue, namespace);\n }\n }\n}\n","/**\n * @enum {number}\n */\nconst CustomElementState = {\n custom: 1,\n failed: 2,\n};\n\nexport default CustomElementState;\n","import CustomElementInternals from './CustomElementInternals.js';\n\nexport default class DocumentConstructionObserver {\n constructor(internals, doc) {\n /**\n * @type {!CustomElementInternals}\n */\n this._internals = internals;\n\n /**\n * @type {!Document}\n */\n this._document = doc;\n\n /**\n * @type {MutationObserver|undefined}\n */\n this._observer = undefined;\n\n\n // Simulate tree construction for all currently accessible nodes in the\n // document.\n this._internals.patchAndUpgradeTree(this._document);\n\n if (this._document.readyState === 'loading') {\n this._observer = new MutationObserver(this._handleMutations.bind(this));\n\n // Nodes created by the parser are given to the observer *before* the next\n // task runs. Inline scripts are run in a new task. This means that the\n // observer will be able to handle the newly parsed nodes before the inline\n // script is run.\n this._observer.observe(this._document, {\n childList: true,\n subtree: true,\n });\n }\n }\n\n disconnect() {\n if (this._observer) {\n this._observer.disconnect();\n }\n }\n\n /**\n * @param {!Array} mutations\n */\n _handleMutations(mutations) {\n // Once the document's `readyState` is 'interactive' or 'complete', all new\n // nodes created within that document will be the result of script and\n // should be handled by patching.\n const readyState = this._document.readyState;\n if (readyState === 'interactive' || readyState === 'complete') {\n this.disconnect();\n }\n\n for (let i = 0; i < mutations.length; i++) {\n const addedNodes = mutations[i].addedNodes;\n for (let j = 0; j < addedNodes.length; j++) {\n const node = addedNodes[j];\n this._internals.patchAndUpgradeTree(node);\n }\n }\n }\n}\n","import CustomElementInternals from './CustomElementInternals.js';\nimport DocumentConstructionObserver from './DocumentConstructionObserver.js';\nimport Deferred from './Deferred.js';\nimport * as Utilities from './Utilities.js';\n\n/**\n * @unrestricted\n */\nexport default class CustomElementRegistry {\n\n /**\n * @param {!CustomElementInternals} internals\n */\n constructor(internals) {\n /**\n * @private\n * @type {boolean}\n */\n this._elementDefinitionIsRunning = false;\n\n /**\n * @private\n * @type {!CustomElementInternals}\n */\n this._internals = internals;\n\n /**\n * @private\n * @type {!Map>}\n */\n this._whenDefinedDeferred = new Map();\n\n /**\n * The default flush callback triggers the document walk synchronously.\n * @private\n * @type {!Function}\n */\n this._flushCallback = fn => fn();\n\n /**\n * @private\n * @type {boolean}\n */\n this._flushPending = false;\n\n /**\n * @private\n * @type {!Array}\n */\n this._pendingDefinitions = [];\n\n /**\n * @private\n * @type {!DocumentConstructionObserver}\n */\n this._documentConstructionObserver = new DocumentConstructionObserver(internals, document);\n }\n\n /**\n * @param {string} localName\n * @param {!Function} constructor\n */\n define(localName, constructor) {\n if (!(constructor instanceof Function)) {\n throw new TypeError('Custom element constructors must be functions.');\n }\n\n if (!Utilities.isValidCustomElementName(localName)) {\n throw new SyntaxError(`The element name '${localName}' is not valid.`);\n }\n\n if (this._internals.localNameToDefinition(localName)) {\n throw new Error(`A custom element with name '${localName}' has already been defined.`);\n }\n\n if (this._elementDefinitionIsRunning) {\n throw new Error('A custom element is already being defined.');\n }\n this._elementDefinitionIsRunning = true;\n\n let connectedCallback;\n let disconnectedCallback;\n let adoptedCallback;\n let attributeChangedCallback;\n let observedAttributes;\n try {\n /** @type {!Object} */\n const prototype = constructor.prototype;\n if (!(prototype instanceof Object)) {\n throw new TypeError('The custom element constructor\\'s prototype is not an object.');\n }\n\n function getCallback(name) {\n const callbackValue = prototype[name];\n if (callbackValue !== undefined && !(callbackValue instanceof Function)) {\n throw new Error(`The '${name}' callback must be a function.`);\n }\n return callbackValue;\n }\n\n connectedCallback = getCallback('connectedCallback');\n disconnectedCallback = getCallback('disconnectedCallback');\n adoptedCallback = getCallback('adoptedCallback');\n attributeChangedCallback = getCallback('attributeChangedCallback');\n observedAttributes = constructor['observedAttributes'] || [];\n } catch (e) {\n return;\n } finally {\n this._elementDefinitionIsRunning = false;\n }\n\n const definition = {\n localName,\n constructorFunction: constructor,\n connectedCallback,\n disconnectedCallback,\n adoptedCallback,\n attributeChangedCallback,\n observedAttributes,\n constructionStack: [],\n };\n\n this._internals.setDefinition(localName, definition);\n this._pendingDefinitions.push(definition);\n\n // If we've already called the flush callback and it hasn't called back yet,\n // don't call it again.\n if (!this._flushPending) {\n this._flushPending = true;\n this._flushCallback(() => this._flush());\n }\n }\n\n upgrade(element) {\n this._internals.patchAndUpgradeTree(element);\n }\n\n _flush() {\n // If no new definitions were defined, don't attempt to flush. This could\n // happen if a flush callback keeps the function it is given and calls it\n // multiple times.\n if (this._flushPending === false) return;\n this._flushPending = false;\n\n const pendingDefinitions = this._pendingDefinitions;\n\n /**\n * Unupgraded elements with definitions that were defined *before* the last\n * flush, in document order.\n * @type {!Array}\n */\n const elementsWithStableDefinitions = [];\n\n /**\n * A map from `localName`s of definitions that were defined *after* the last\n * flush to unupgraded elements matching that definition, in document order.\n * @type {!Map>}\n */\n const elementsWithPendingDefinitions = new Map();\n for (let i = 0; i < pendingDefinitions.length; i++) {\n elementsWithPendingDefinitions.set(pendingDefinitions[i].localName, []);\n }\n\n this._internals.patchAndUpgradeTree(document, {\n upgrade: element => {\n // Ignore the element if it has already upgraded or failed to upgrade.\n if (element.__CE_state !== undefined) return;\n\n const localName = element.localName;\n\n // If there is an applicable pending definition for the element, add the\n // element to the list of elements to be upgraded with that definition.\n const pendingElements = elementsWithPendingDefinitions.get(localName);\n if (pendingElements) {\n pendingElements.push(element);\n // If there is *any other* applicable definition for the element, add it\n // to the list of elements with stable definitions that need to be upgraded.\n } else if (this._internals.localNameToDefinition(localName)) {\n elementsWithStableDefinitions.push(element);\n }\n },\n });\n\n // Upgrade elements with 'stable' definitions first.\n for (let i = 0; i < elementsWithStableDefinitions.length; i++) {\n this._internals.upgradeElement(elementsWithStableDefinitions[i]);\n }\n\n // Upgrade elements with 'pending' definitions in the order they were defined.\n while (pendingDefinitions.length > 0) {\n const definition = pendingDefinitions.shift();\n const localName = definition.localName;\n\n // Attempt to upgrade all applicable elements.\n const pendingUpgradableElements = elementsWithPendingDefinitions.get(definition.localName);\n for (let i = 0; i < pendingUpgradableElements.length; i++) {\n this._internals.upgradeElement(pendingUpgradableElements[i]);\n }\n\n // Resolve any promises created by `whenDefined` for the definition.\n const deferred = this._whenDefinedDeferred.get(localName);\n if (deferred) {\n deferred.resolve(undefined);\n }\n }\n }\n\n /**\n * @param {string} localName\n * @return {Function|undefined}\n */\n get(localName) {\n const definition = this._internals.localNameToDefinition(localName);\n if (definition) {\n return definition.constructorFunction;\n }\n\n return undefined;\n }\n\n /**\n * @param {string} localName\n * @return {!Promise}\n */\n whenDefined(localName) {\n if (!Utilities.isValidCustomElementName(localName)) {\n return Promise.reject(new SyntaxError(`'${localName}' is not a valid custom element name.`));\n }\n\n const prior = this._whenDefinedDeferred.get(localName);\n if (prior) {\n return prior.toPromise();\n }\n\n const deferred = new Deferred();\n this._whenDefinedDeferred.set(localName, deferred);\n\n const definition = this._internals.localNameToDefinition(localName);\n // Resolve immediately only if the given local name has a definition *and*\n // the full document walk to upgrade elements with that local name has\n // already happened.\n if (definition && !this._pendingDefinitions.some(d => d.localName === localName)) {\n deferred.resolve(undefined);\n }\n\n return deferred.toPromise();\n }\n\n polyfillWrapFlushCallback(outer) {\n this._documentConstructionObserver.disconnect();\n const inner = this._flushCallback;\n this._flushCallback = flush => outer(() => inner(flush));\n }\n}\n\n// Closure compiler exports.\nwindow['CustomElementRegistry'] = CustomElementRegistry;\nCustomElementRegistry.prototype['define'] = CustomElementRegistry.prototype.define;\nCustomElementRegistry.prototype['upgrade'] = CustomElementRegistry.prototype.upgrade;\nCustomElementRegistry.prototype['get'] = CustomElementRegistry.prototype.get;\nCustomElementRegistry.prototype['whenDefined'] = CustomElementRegistry.prototype.whenDefined;\nCustomElementRegistry.prototype['polyfillWrapFlushCallback'] = CustomElementRegistry.prototype.polyfillWrapFlushCallback;\n","/**\n * @template T\n */\nexport default class Deferred {\n constructor() {\n /**\n * @private\n * @type {T|undefined}\n */\n this._value = undefined;\n\n /**\n * @private\n * @type {Function|undefined}\n */\n this._resolve = undefined;\n\n /**\n * @private\n * @type {!Promise}\n */\n this._promise = new Promise(resolve => {\n this._resolve = resolve;\n\n if (this._value) {\n resolve(this._value);\n }\n });\n }\n\n /**\n * @param {T} value\n */\n resolve(value) {\n if (this._value) {\n throw new Error('Already resolved.');\n }\n\n this._value = value;\n\n if (this._resolve) {\n this._resolve(value);\n }\n }\n\n /**\n * @return {!Promise}\n */\n toPromise() {\n return this._promise;\n }\n}\n","export default {\n Document_createElement: window.Document.prototype.createElement,\n Document_createElementNS: window.Document.prototype.createElementNS,\n Document_importNode: window.Document.prototype.importNode,\n Document_prepend: window.Document.prototype['prepend'],\n Document_append: window.Document.prototype['append'],\n DocumentFragment_prepend: window.DocumentFragment.prototype['prepend'],\n DocumentFragment_append: window.DocumentFragment.prototype['append'],\n Node_cloneNode: window.Node.prototype.cloneNode,\n Node_appendChild: window.Node.prototype.appendChild,\n Node_insertBefore: window.Node.prototype.insertBefore,\n Node_removeChild: window.Node.prototype.removeChild,\n Node_replaceChild: window.Node.prototype.replaceChild,\n Node_textContent: Object.getOwnPropertyDescriptor(window.Node.prototype, 'textContent'),\n Element_attachShadow: window.Element.prototype['attachShadow'],\n Element_innerHTML: Object.getOwnPropertyDescriptor(window.Element.prototype, 'innerHTML'),\n Element_getAttribute: window.Element.prototype.getAttribute,\n Element_setAttribute: window.Element.prototype.setAttribute,\n Element_removeAttribute: window.Element.prototype.removeAttribute,\n Element_getAttributeNS: window.Element.prototype.getAttributeNS,\n Element_setAttributeNS: window.Element.prototype.setAttributeNS,\n Element_removeAttributeNS: window.Element.prototype.removeAttributeNS,\n Element_insertAdjacentElement: window.Element.prototype['insertAdjacentElement'],\n Element_insertAdjacentHTML: window.Element.prototype['insertAdjacentHTML'],\n Element_prepend: window.Element.prototype['prepend'],\n Element_append: window.Element.prototype['append'],\n Element_before: window.Element.prototype['before'],\n Element_after: window.Element.prototype['after'],\n Element_replaceWith: window.Element.prototype['replaceWith'],\n Element_remove: window.Element.prototype['remove'],\n HTMLElement: window.HTMLElement,\n HTMLElement_innerHTML: Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'innerHTML'),\n HTMLElement_insertAdjacentElement: window.HTMLElement.prototype['insertAdjacentElement'],\n HTMLElement_insertAdjacentHTML: window.HTMLElement.prototype['insertAdjacentHTML'],\n};\n","/**\n * This class exists only to work around Closure's lack of a way to describe\n * singletons. It represents the 'already constructed marker' used in custom\n * element construction stacks.\n *\n * https://html.spec.whatwg.org/#concept-already-constructed-marker\n * @extends AlreadyConstructedMarkerType\n */\nclass AlreadyConstructedMarker {}\n\nexport default new AlreadyConstructedMarker();\n","import Native from './Native.js';\nimport CustomElementInternals from '../CustomElementInternals.js';\nimport CEState from '../CustomElementState.js';\nimport AlreadyConstructedMarker from '../AlreadyConstructedMarker.js';\n\n/**\n * @param {!CustomElementInternals} internals\n */\nexport default function(internals) {\n window['HTMLElement'] = (function() {\n /**\n * @type {function(new: HTMLElement): !HTMLElement}\n */\n function HTMLElement() {\n // This should really be `new.target` but `new.target` can't be emulated\n // in ES5. Assuming the user keeps the default value of the constructor's\n // prototype's `constructor` property, this is equivalent.\n const constructor = /** @type {!Function} */ (this.constructor);\n\n const definition = internals.constructorToDefinition(constructor);\n if (!definition) {\n throw new Error('The custom element being constructed was not registered with `customElements`.');\n }\n\n const constructionStack = definition.constructionStack;\n\n if (constructionStack.length === 0) {\n const element = /** @type {!HTMLElement} */ (Native.Document_createElement.call(document, definition.localName));\n Object.setPrototypeOf(element, constructor.prototype);\n element.__CE_state = CEState.custom;\n element.__CE_definition = definition;\n internals.patch(element);\n return element;\n }\n\n const lastIndex = constructionStack.length - 1;\n const element = constructionStack[lastIndex];\n if (element === AlreadyConstructedMarker) {\n throw new Error('The HTMLElement constructor was either called reentrantly for this constructor or called multiple times.');\n }\n const toConstructElement = /** @type {!HTMLElement} */ (element);\n constructionStack[lastIndex] = AlreadyConstructedMarker;\n\n Object.setPrototypeOf(toConstructElement, constructor.prototype);\n internals.patch(toConstructElement);\n\n return toConstructElement;\n }\n\n HTMLElement.prototype = Native.HTMLElement.prototype;\n // Safari 9 has `writable: false` on the propertyDescriptor\n // Make it writable so that TypeScript can patch up the\n // constructor in the ES5 compiled code.\n Object.defineProperty(HTMLElement.prototype, 'constructor', {\n writable: true,\n configurable: true,\n enumerable: false,\n value: HTMLElement\n });\n\n return HTMLElement;\n })();\n};\n","/**\n * @license\n * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt\n */\n\nimport CustomElementInternals from './CustomElementInternals.js';\nimport CustomElementRegistry from './CustomElementRegistry.js';\n\nimport PatchHTMLElement from './Patch/HTMLElement.js';\nimport PatchDocument from './Patch/Document.js';\nimport PatchDocumentFragment from './Patch/DocumentFragment.js';\nimport PatchNode from './Patch/Node.js';\nimport PatchElement from './Patch/Element.js';\n\nconst priorCustomElements = window['customElements'];\n\nif (!priorCustomElements ||\n priorCustomElements['forcePolyfill'] ||\n (typeof priorCustomElements['define'] != 'function') ||\n (typeof priorCustomElements['get'] != 'function')) {\n /** @type {!CustomElementInternals} */\n const internals = new CustomElementInternals();\n\n PatchHTMLElement(internals);\n PatchDocument(internals);\n PatchDocumentFragment(internals);\n PatchNode(internals);\n PatchElement(internals);\n\n // The main document is always associated with the registry.\n document.__CE_hasRegistry = true;\n\n /** @type {!CustomElementRegistry} */\n const customElements = new CustomElementRegistry(internals);\n\n Object.defineProperty(window, 'customElements', {\n configurable: true,\n enumerable: true,\n value: customElements,\n });\n}\n","import CustomElementInternals from '../../CustomElementInternals.js';\nimport * as Utilities from '../../Utilities.js';\n\n/**\n * @typedef {{\n * prepend: !function(...(!Node|string)),\n * append: !function(...(!Node|string)),\n * }}\n */\nlet ParentNodeNativeMethods;\n\n/**\n * @param {!CustomElementInternals} internals\n * @param {!Object} destination\n * @param {!ParentNodeNativeMethods} builtIn\n */\nexport default function(internals, destination, builtIn) {\n /**\n * @param {!function(...(!Node|string))} builtInMethod\n * @return {!function(...(!Node|string))}\n */\n function appendPrependPatch(builtInMethod) {\n return function(...nodes) {\n /**\n * A copy of `nodes`, with any DocumentFragment replaced by its children.\n * @type {!Array}\n */\n const flattenedNodes = [];\n\n /**\n * Elements in `nodes` that were connected before this call.\n * @type {!Array}\n */\n const connectedElements = [];\n\n for (var i = 0; i < nodes.length; i++) {\n const node = nodes[i];\n\n if (node instanceof Element && Utilities.isConnected(node)) {\n connectedElements.push(node);\n }\n\n if (node instanceof DocumentFragment) {\n for (let child = node.firstChild; child; child = child.nextSibling) {\n flattenedNodes.push(child);\n }\n } else {\n flattenedNodes.push(node);\n }\n }\n\n builtInMethod.apply(this, nodes);\n\n for (let i = 0; i < connectedElements.length; i++) {\n internals.disconnectTree(connectedElements[i]);\n }\n\n if (Utilities.isConnected(this)) {\n for (let i = 0; i < flattenedNodes.length; i++) {\n const node = flattenedNodes[i];\n if (node instanceof Element) {\n internals.connectTree(node);\n }\n }\n }\n };\n }\n\n if (builtIn.prepend !== undefined) {\n Utilities.setPropertyUnchecked(destination, 'prepend', appendPrependPatch(builtIn.prepend));\n }\n\n if (builtIn.append !== undefined) {\n Utilities.setPropertyUnchecked(destination, 'append', appendPrependPatch(builtIn.append));\n }\n};\n","import Native from './Native.js';\nimport CustomElementInternals from '../CustomElementInternals.js';\nimport * as Utilities from '../Utilities.js';\n\nimport PatchParentNode from './Interface/ParentNode.js';\n\n/**\n * @param {!CustomElementInternals} internals\n */\nexport default function(internals) {\n Utilities.setPropertyUnchecked(Document.prototype, 'createElement',\n /**\n * @this {Document}\n * @param {string} localName\n * @return {!Element}\n */\n function(localName) {\n // Only create custom elements if this document is associated with the registry.\n if (this.__CE_hasRegistry) {\n const definition = internals.localNameToDefinition(localName);\n if (definition) {\n return new (definition.constructorFunction)();\n }\n }\n\n const result = /** @type {!Element} */\n (Native.Document_createElement.call(this, localName));\n internals.patch(result);\n return result;\n });\n\n Utilities.setPropertyUnchecked(Document.prototype, 'importNode',\n /**\n * @this {Document}\n * @param {!Node} node\n * @param {boolean=} deep\n * @return {!Node}\n */\n function(node, deep) {\n const clone = /** @type {!Node} */ (Native.Document_importNode.call(this, node, !!deep));\n // Only create custom elements if this document is associated with the registry.\n if (!this.__CE_hasRegistry) {\n internals.patchTree(clone);\n } else {\n internals.patchAndUpgradeTree(clone);\n }\n return clone;\n });\n\n const NS_HTML = \"http://www.w3.org/1999/xhtml\";\n\n Utilities.setPropertyUnchecked(Document.prototype, 'createElementNS',\n /**\n * @this {Document}\n * @param {?string} namespace\n * @param {string} localName\n * @return {!Element}\n */\n function(namespace, localName) {\n // Only create custom elements if this document is associated with the registry.\n if (this.__CE_hasRegistry && (namespace === null || namespace === NS_HTML)) {\n const definition = internals.localNameToDefinition(localName);\n if (definition) {\n return new (definition.constructorFunction)();\n }\n }\n\n const result = /** @type {!Element} */\n (Native.Document_createElementNS.call(this, namespace, localName));\n internals.patch(result);\n return result;\n });\n\n PatchParentNode(internals, Document.prototype, {\n prepend: Native.Document_prepend,\n append: Native.Document_append,\n });\n};\n","import Native from './Native.js';\nimport CustomElementInternals from '../CustomElementInternals.js';\nimport * as Utilities from '../Utilities.js';\n\n/**\n * @param {!CustomElementInternals} internals\n */\nexport default function(internals) {\n // `Node#nodeValue` is implemented on `Attr`.\n // `Node#textContent` is implemented on `Attr`, `Element`.\n\n Utilities.setPropertyUnchecked(Node.prototype, 'insertBefore',\n /**\n * @this {Node}\n * @param {!Node} node\n * @param {?Node} refNode\n * @return {!Node}\n */\n function(node, refNode) {\n if (node instanceof DocumentFragment) {\n const insertedNodes = Array.prototype.slice.apply(node.childNodes);\n const nativeResult = Native.Node_insertBefore.call(this, node, refNode);\n\n // DocumentFragments can't be connected, so `disconnectTree` will never\n // need to be called on a DocumentFragment's children after inserting it.\n\n if (Utilities.isConnected(this)) {\n for (let i = 0; i < insertedNodes.length; i++) {\n internals.connectTree(insertedNodes[i]);\n }\n }\n\n return nativeResult;\n }\n\n const nodeWasConnected = Utilities.isConnected(node);\n const nativeResult = Native.Node_insertBefore.call(this, node, refNode);\n\n if (nodeWasConnected) {\n internals.disconnectTree(node);\n }\n\n if (Utilities.isConnected(this)) {\n internals.connectTree(node);\n }\n\n return nativeResult;\n });\n\n Utilities.setPropertyUnchecked(Node.prototype, 'appendChild',\n /**\n * @this {Node}\n * @param {!Node} node\n * @return {!Node}\n */\n function(node) {\n if (node instanceof DocumentFragment) {\n const insertedNodes = Array.prototype.slice.apply(node.childNodes);\n const nativeResult = Native.Node_appendChild.call(this, node);\n\n // DocumentFragments can't be connected, so `disconnectTree` will never\n // need to be called on a DocumentFragment's children after inserting it.\n\n if (Utilities.isConnected(this)) {\n for (let i = 0; i < insertedNodes.length; i++) {\n internals.connectTree(insertedNodes[i]);\n }\n }\n\n return nativeResult;\n }\n\n const nodeWasConnected = Utilities.isConnected(node);\n const nativeResult = Native.Node_appendChild.call(this, node);\n\n if (nodeWasConnected) {\n internals.disconnectTree(node);\n }\n\n if (Utilities.isConnected(this)) {\n internals.connectTree(node);\n }\n\n return nativeResult;\n });\n\n Utilities.setPropertyUnchecked(Node.prototype, 'cloneNode',\n /**\n * @this {Node}\n * @param {boolean=} deep\n * @return {!Node}\n */\n function(deep) {\n const clone = Native.Node_cloneNode.call(this, !!deep);\n // Only create custom elements if this element's owner document is\n // associated with the registry.\n if (!this.ownerDocument.__CE_hasRegistry) {\n internals.patchTree(clone);\n } else {\n internals.patchAndUpgradeTree(clone);\n }\n return clone;\n });\n\n Utilities.setPropertyUnchecked(Node.prototype, 'removeChild',\n /**\n * @this {Node}\n * @param {!Node} node\n * @return {!Node}\n */\n function(node) {\n const nodeWasConnected = Utilities.isConnected(node);\n const nativeResult = Native.Node_removeChild.call(this, node);\n\n if (nodeWasConnected) {\n internals.disconnectTree(node);\n }\n\n return nativeResult;\n });\n\n Utilities.setPropertyUnchecked(Node.prototype, 'replaceChild',\n /**\n * @this {Node}\n * @param {!Node} nodeToInsert\n * @param {!Node} nodeToRemove\n * @return {!Node}\n */\n function(nodeToInsert, nodeToRemove) {\n if (nodeToInsert instanceof DocumentFragment) {\n const insertedNodes = Array.prototype.slice.apply(nodeToInsert.childNodes);\n const nativeResult = Native.Node_replaceChild.call(this, nodeToInsert, nodeToRemove);\n\n // DocumentFragments can't be connected, so `disconnectTree` will never\n // need to be called on a DocumentFragment's children after inserting it.\n\n if (Utilities.isConnected(this)) {\n internals.disconnectTree(nodeToRemove);\n for (let i = 0; i < insertedNodes.length; i++) {\n internals.connectTree(insertedNodes[i]);\n }\n }\n\n return nativeResult;\n }\n\n const nodeToInsertWasConnected = Utilities.isConnected(nodeToInsert);\n const nativeResult = Native.Node_replaceChild.call(this, nodeToInsert, nodeToRemove);\n const thisIsConnected = Utilities.isConnected(this);\n\n if (thisIsConnected) {\n internals.disconnectTree(nodeToRemove);\n }\n\n if (nodeToInsertWasConnected) {\n internals.disconnectTree(nodeToInsert);\n }\n\n if (thisIsConnected) {\n internals.connectTree(nodeToInsert);\n }\n\n return nativeResult;\n });\n\n\n function patch_textContent(destination, baseDescriptor) {\n Object.defineProperty(destination, 'textContent', {\n enumerable: baseDescriptor.enumerable,\n configurable: true,\n get: baseDescriptor.get,\n set: /** @this {Node} */ function(assignedValue) {\n // If this is a text node then there are no nodes to disconnect.\n if (this.nodeType === Node.TEXT_NODE) {\n baseDescriptor.set.call(this, assignedValue);\n return;\n }\n\n let removedNodes = undefined;\n // Checking for `firstChild` is faster than reading `childNodes.length`\n // to compare with 0.\n if (this.firstChild) {\n // Using `childNodes` is faster than `children`, even though we only\n // care about elements.\n const childNodes = this.childNodes;\n const childNodesLength = childNodes.length;\n if (childNodesLength > 0 && Utilities.isConnected(this)) {\n // Copying an array by iterating is faster than using slice.\n removedNodes = new Array(childNodesLength);\n for (let i = 0; i < childNodesLength; i++) {\n removedNodes[i] = childNodes[i];\n }\n }\n }\n\n baseDescriptor.set.call(this, assignedValue);\n\n if (removedNodes) {\n for (let i = 0; i < removedNodes.length; i++) {\n internals.disconnectTree(removedNodes[i]);\n }\n }\n },\n });\n }\n\n if (Native.Node_textContent && Native.Node_textContent.get) {\n patch_textContent(Node.prototype, Native.Node_textContent);\n } else {\n internals.addPatch(function(element) {\n patch_textContent(element, {\n enumerable: true,\n configurable: true,\n // NOTE: This implementation of the `textContent` getter assumes that\n // text nodes' `textContent` getter will not be patched.\n get: /** @this {Node} */ function() {\n /** @type {!Array} */\n const parts = [];\n\n for (let i = 0; i < this.childNodes.length; i++) {\n parts.push(this.childNodes[i].textContent);\n }\n\n return parts.join('');\n },\n set: /** @this {Node} */ function(assignedValue) {\n while (this.firstChild) {\n Native.Node_removeChild.call(this, this.firstChild);\n }\n Native.Node_appendChild.call(this, document.createTextNode(assignedValue));\n },\n });\n });\n }\n};\n","import CustomElementInternals from '../../CustomElementInternals.js';\nimport * as Utilities from '../../Utilities.js';\n\n/**\n * @typedef {{\n * before: !function(...(!Node|string)),\n * after: !function(...(!Node|string)),\n * replaceWith: !function(...(!Node|string)),\n * remove: !function(),\n * }}\n */\nlet ChildNodeNativeMethods;\n\n/**\n * @param {!CustomElementInternals} internals\n * @param {!Object} destination\n * @param {!ChildNodeNativeMethods} builtIn\n */\nexport default function(internals, destination, builtIn) {\n /**\n * @param {!function(...(!Node|string))} builtInMethod\n * @return {!function(...(!Node|string))}\n */\n function beforeAfterPatch(builtInMethod) {\n return function(...nodes) {\n /**\n * A copy of `nodes`, with any DocumentFragment replaced by its children.\n * @type {!Array}\n */\n const flattenedNodes = [];\n\n /**\n * Elements in `nodes` that were connected before this call.\n * @type {!Array}\n */\n const connectedElements = [];\n\n for (var i = 0; i < nodes.length; i++) {\n const node = nodes[i];\n\n if (node instanceof Element && Utilities.isConnected(node)) {\n connectedElements.push(node);\n }\n\n if (node instanceof DocumentFragment) {\n for (let child = node.firstChild; child; child = child.nextSibling) {\n flattenedNodes.push(child);\n }\n } else {\n flattenedNodes.push(node);\n }\n }\n\n builtInMethod.apply(this, nodes);\n\n for (let i = 0; i < connectedElements.length; i++) {\n internals.disconnectTree(connectedElements[i]);\n }\n\n if (Utilities.isConnected(this)) {\n for (let i = 0; i < flattenedNodes.length; i++) {\n const node = flattenedNodes[i];\n if (node instanceof Element) {\n internals.connectTree(node);\n }\n }\n }\n };\n }\n\n if (builtIn.before !== undefined) {\n Utilities.setPropertyUnchecked(destination, 'before', beforeAfterPatch(builtIn.before));\n }\n\n if (builtIn.before !== undefined) {\n Utilities.setPropertyUnchecked(destination, 'after', beforeAfterPatch(builtIn.after));\n }\n\n if (builtIn.replaceWith !== undefined) {\n Utilities.setPropertyUnchecked(destination, 'replaceWith',\n /**\n * @param {...(!Node|string)} nodes\n */\n function(...nodes) {\n /**\n * A copy of `nodes`, with any DocumentFragment replaced by its children.\n * @type {!Array}\n */\n const flattenedNodes = [];\n\n /**\n * Elements in `nodes` that were connected before this call.\n * @type {!Array}\n */\n const connectedElements = [];\n\n for (var i = 0; i < nodes.length; i++) {\n const node = nodes[i];\n\n if (node instanceof Element && Utilities.isConnected(node)) {\n connectedElements.push(node);\n }\n\n if (node instanceof DocumentFragment) {\n for (let child = node.firstChild; child; child = child.nextSibling) {\n flattenedNodes.push(child);\n }\n } else {\n flattenedNodes.push(node);\n }\n }\n\n const wasConnected = Utilities.isConnected(this);\n\n builtIn.replaceWith.apply(this, nodes);\n\n for (let i = 0; i < connectedElements.length; i++) {\n internals.disconnectTree(connectedElements[i]);\n }\n\n if (wasConnected) {\n internals.disconnectTree(this);\n for (let i = 0; i < flattenedNodes.length; i++) {\n const node = flattenedNodes[i];\n if (node instanceof Element) {\n internals.connectTree(node);\n }\n }\n }\n });\n }\n\n if (builtIn.remove !== undefined) {\n Utilities.setPropertyUnchecked(destination, 'remove',\n function() {\n const wasConnected = Utilities.isConnected(this);\n\n builtIn.remove.call(this);\n\n if (wasConnected) {\n internals.disconnectTree(this);\n }\n });\n }\n};\n","import Native from './Native.js';\nimport CustomElementInternals from '../CustomElementInternals.js';\nimport CEState from '../CustomElementState.js';\nimport * as Utilities from '../Utilities.js';\n\nimport PatchParentNode from './Interface/ParentNode.js';\nimport PatchChildNode from './Interface/ChildNode.js';\n\n/**\n * @param {!CustomElementInternals} internals\n */\nexport default function(internals) {\n if (Native.Element_attachShadow) {\n Utilities.setPropertyUnchecked(Element.prototype, 'attachShadow',\n /**\n * @this {Element}\n * @param {!{mode: string}} init\n * @return {ShadowRoot}\n */\n function(init) {\n const shadowRoot = Native.Element_attachShadow.call(this, init);\n this.__CE_shadowRoot = shadowRoot;\n return shadowRoot;\n });\n }\n\n\n function patch_innerHTML(destination, baseDescriptor) {\n Object.defineProperty(destination, 'innerHTML', {\n enumerable: baseDescriptor.enumerable,\n configurable: true,\n get: baseDescriptor.get,\n set: /** @this {Element} */ function(htmlString) {\n const isConnected = Utilities.isConnected(this);\n\n // NOTE: In IE11, when using the native `innerHTML` setter, all nodes\n // that were previously descendants of the context element have all of\n // their children removed as part of the set - the entire subtree is\n // 'disassembled'. This work around walks the subtree *before* using the\n // native setter.\n /** @type {!Array|undefined} */\n let removedElements = undefined;\n if (isConnected) {\n removedElements = [];\n Utilities.walkDeepDescendantElements(this, element => {\n if (element !== this) {\n removedElements.push(element);\n }\n });\n }\n\n baseDescriptor.set.call(this, htmlString);\n\n if (removedElements) {\n for (let i = 0; i < removedElements.length; i++) {\n const element = removedElements[i];\n if (element.__CE_state === CEState.custom) {\n internals.disconnectedCallback(element);\n }\n }\n }\n\n // Only create custom elements if this element's owner document is\n // associated with the registry.\n if (!this.ownerDocument.__CE_hasRegistry) {\n internals.patchTree(this);\n } else {\n internals.patchAndUpgradeTree(this);\n }\n return htmlString;\n },\n });\n }\n\n if (Native.Element_innerHTML && Native.Element_innerHTML.get) {\n patch_innerHTML(Element.prototype, Native.Element_innerHTML);\n } else if (Native.HTMLElement_innerHTML && Native.HTMLElement_innerHTML.get) {\n patch_innerHTML(HTMLElement.prototype, Native.HTMLElement_innerHTML);\n } else {\n\n internals.addPatch(function(element) {\n patch_innerHTML(element, {\n enumerable: true,\n configurable: true,\n // Implements getting `innerHTML` by performing an unpatched `cloneNode`\n // of the element and returning the resulting element's `innerHTML`.\n // TODO: Is this too expensive?\n get: /** @this {Element} */ function() {\n return Native.Node_cloneNode.call(this, true).innerHTML;\n },\n // Implements setting `innerHTML` by creating an unpatched element,\n // setting `innerHTML` of that element and replacing the target\n // element's children with those of the unpatched element.\n set: /** @this {Element} */ function(assignedValue) {\n // NOTE: re-route to `content` for `template` elements.\n // We need to do this because `template.appendChild` does not\n // route into `template.content`.\n const isTemplate = (this.localName === 'template');\n /** @type {!Node} */\n const content = isTemplate ? (/** @type {!HTMLTemplateElement} */\n (this)).content : this;\n /** @type {!Node} */\n const rawElement = Native.Document_createElementNS.call(document,\n this.namespaceURI, this.localName);\n rawElement.innerHTML = assignedValue;\n\n while (content.childNodes.length > 0) {\n Native.Node_removeChild.call(content, content.childNodes[0]);\n }\n const container = isTemplate ? rawElement.content : rawElement;\n while (container.childNodes.length > 0) {\n Native.Node_appendChild.call(content, container.childNodes[0]);\n }\n },\n });\n });\n }\n\n\n Utilities.setPropertyUnchecked(Element.prototype, 'setAttribute',\n /**\n * @this {Element}\n * @param {string} name\n * @param {string} newValue\n */\n function(name, newValue) {\n // Fast path for non-custom elements.\n if (this.__CE_state !== CEState.custom) {\n return Native.Element_setAttribute.call(this, name, newValue);\n }\n\n const oldValue = Native.Element_getAttribute.call(this, name);\n Native.Element_setAttribute.call(this, name, newValue);\n newValue = Native.Element_getAttribute.call(this, name);\n internals.attributeChangedCallback(this, name, oldValue, newValue, null);\n });\n\n Utilities.setPropertyUnchecked(Element.prototype, 'setAttributeNS',\n /**\n * @this {Element}\n * @param {?string} namespace\n * @param {string} name\n * @param {string} newValue\n */\n function(namespace, name, newValue) {\n // Fast path for non-custom elements.\n if (this.__CE_state !== CEState.custom) {\n return Native.Element_setAttributeNS.call(this, namespace, name, newValue);\n }\n\n const oldValue = Native.Element_getAttributeNS.call(this, namespace, name);\n Native.Element_setAttributeNS.call(this, namespace, name, newValue);\n newValue = Native.Element_getAttributeNS.call(this, namespace, name);\n internals.attributeChangedCallback(this, name, oldValue, newValue, namespace);\n });\n\n Utilities.setPropertyUnchecked(Element.prototype, 'removeAttribute',\n /**\n * @this {Element}\n * @param {string} name\n */\n function(name) {\n // Fast path for non-custom elements.\n if (this.__CE_state !== CEState.custom) {\n return Native.Element_removeAttribute.call(this, name);\n }\n\n const oldValue = Native.Element_getAttribute.call(this, name);\n Native.Element_removeAttribute.call(this, name);\n if (oldValue !== null) {\n internals.attributeChangedCallback(this, name, oldValue, null, null);\n }\n });\n\n Utilities.setPropertyUnchecked(Element.prototype, 'removeAttributeNS',\n /**\n * @this {Element}\n * @param {?string} namespace\n * @param {string} name\n */\n function(namespace, name) {\n // Fast path for non-custom elements.\n if (this.__CE_state !== CEState.custom) {\n return Native.Element_removeAttributeNS.call(this, namespace, name);\n }\n\n const oldValue = Native.Element_getAttributeNS.call(this, namespace, name);\n Native.Element_removeAttributeNS.call(this, namespace, name);\n // In older browsers, `Element#getAttributeNS` may return the empty string\n // instead of null if the attribute does not exist. For details, see;\n // https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNS#Notes\n const newValue = Native.Element_getAttributeNS.call(this, namespace, name);\n if (oldValue !== newValue) {\n internals.attributeChangedCallback(this, name, oldValue, newValue, namespace);\n }\n });\n\n\n function patch_insertAdjacentElement(destination, baseMethod) {\n Utilities.setPropertyUnchecked(destination, 'insertAdjacentElement',\n /**\n * @this {Element}\n * @param {string} position\n * @param {!Element} element\n * @return {?Element}\n */\n function(position, element) {\n const wasConnected = Utilities.isConnected(element);\n const insertedElement = /** @type {!Element} */\n (baseMethod.call(this, position, element));\n\n if (wasConnected) {\n internals.disconnectTree(element);\n }\n\n if (Utilities.isConnected(insertedElement)) {\n internals.connectTree(element);\n }\n return insertedElement;\n });\n }\n\n if (Native.HTMLElement_insertAdjacentElement) {\n patch_insertAdjacentElement(HTMLElement.prototype, Native.HTMLElement_insertAdjacentElement);\n } else if (Native.Element_insertAdjacentElement) {\n patch_insertAdjacentElement(Element.prototype, Native.Element_insertAdjacentElement);\n } else {\n console.warn('Custom Elements: `Element#insertAdjacentElement` was not patched.');\n }\n\n\n function patch_insertAdjacentHTML(destination, baseMethod) {\n /**\n * Patches and upgrades all nodes which are siblings between `start`\n * (inclusive) and `end` (exclusive). If `end` is `null`, then all siblings\n * following `start` will be patched and upgraded.\n * @param {!Node} start\n * @param {?Node} end\n */\n function upgradeNodesInRange(start, end) {\n const nodes = [];\n for (let node = start; node !== end; node = node.nextSibling) {\n nodes.push(node);\n }\n for (let i = 0; i < nodes.length; i++) {\n internals.patchAndUpgradeTree(nodes[i]);\n }\n }\n\n Utilities.setPropertyUnchecked(destination, 'insertAdjacentHTML',\n /**\n * @this {Element}\n * @param {string} position\n * @param {string} text\n */\n function(position, text) {\n position = position.toLowerCase();\n\n if (position === \"beforebegin\") {\n const marker = this.previousSibling;\n baseMethod.call(this, position, text);\n upgradeNodesInRange(marker || /** @type {!Node} */ (this.parentNode.firstChild), this);\n } else if (position === \"afterbegin\") {\n const marker = this.firstChild;\n baseMethod.call(this, position, text);\n upgradeNodesInRange(/** @type {!Node} */ (this.firstChild), marker);\n } else if (position === \"beforeend\") {\n const marker = this.lastChild;\n baseMethod.call(this, position, text);\n upgradeNodesInRange(marker || /** @type {!Node} */ (this.firstChild), null);\n } else if (position === \"afterend\") {\n const marker = this.nextSibling;\n baseMethod.call(this, position, text);\n upgradeNodesInRange(/** @type {!Node} */ (this.nextSibling), marker);\n } else {\n throw new SyntaxError(`The value provided (${String(position)}) is ` +\n \"not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.\");\n }\n });\n }\n\n if (Native.HTMLElement_insertAdjacentHTML) {\n patch_insertAdjacentHTML(HTMLElement.prototype, Native.HTMLElement_insertAdjacentHTML);\n } else if (Native.Element_insertAdjacentHTML) {\n patch_insertAdjacentHTML(Element.prototype, Native.Element_insertAdjacentHTML);\n } else {\n console.warn('Custom Elements: `Element#insertAdjacentHTML` was not patched.');\n }\n\n\n PatchParentNode(internals, Element.prototype, {\n prepend: Native.Element_prepend,\n append: Native.Element_append,\n });\n\n PatchChildNode(internals, Element.prototype, {\n before: Native.Element_before,\n after: Native.Element_after,\n replaceWith: Native.Element_replaceWith,\n remove: Native.Element_remove,\n });\n};\n","import CustomElementInternals from '../CustomElementInternals.js';\nimport Native from './Native.js';\nimport PatchParentNode from './Interface/ParentNode.js';\n\n/**\n * @param {!CustomElementInternals} internals\n */\nexport default function(internals) {\n PatchParentNode(internals, DocumentFragment.prototype, {\n prepend: Native.DocumentFragment_prepend,\n append: Native.DocumentFragment_append,\n });\n};\n","/**\n@license\nCopyright (c) 2017 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt\nThe complete set of authors may be found at http://polymer.github.io/AUTHORS.txt\nThe complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt\nCode distributed by Google as part of the polymer project is also\nsubject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt\n*/\n'use strict';\n\n/*\n * Polyfills loaded: Custom Elements, Shady DOM/Shady CSS\n * Used in: Safari 9, Firefox, Edge\n */\n\nimport '../node_modules/@webcomponents/custom-elements/src/custom-elements.js';\n\nlet document = window.document;\n// global for (1) existence means `WebComponentsReady` will file,\n// (2) WebComponents.ready == true means event has fired.\nwindow.WebComponents = window.WebComponents || {};\n\nfunction fire() {\n window.WebComponents.ready = true;\n window.document.dispatchEvent(new CustomEvent('WebComponentsReady', { bubbles: true }));\n}\n\nfunction wait() {\n fire();\n document.removeEventListener('readystatechange', wait);\n}\n\nif (document.readyState !== 'loading') {\n fire();\n} else {\n document.addEventListener('readystatechange', wait);\n}"]} \ No newline at end of file diff --git a/docs/webcomponentsjs/webcomponents-hi-ce.js b/docs/webcomponentsjs/webcomponents-hi-ce.js deleted file mode 100644 index d2c0adbe..00000000 --- a/docs/webcomponentsjs/webcomponents-hi-ce.js +++ /dev/null @@ -1,82 +0,0 @@ -/** -@license @nocompile -Copyright (c) 2018 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ -(function(){/* - - Copyright (c) 2016 The Polymer Project Authors. All rights reserved. - This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt - The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt - The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt - Code distributed by Google as part of the polymer project is also - subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ -'use strict';(function(b){function a(a,b){if("function"===typeof window.CustomEvent)return new CustomEvent(a,b);var c=document.createEvent("CustomEvent");c.initCustomEvent(a,!!b.bubbles,!!b.cancelable,b.detail);return c}function c(a){if(J)return a.ownerDocument!==document?a.ownerDocument:null;var b=a.__importDoc;if(!b&&a.parentNode){b=a.parentNode;if("function"===typeof b.closest)b=b.closest("link[rel=import]");else for(;!h(b)&&(b=b.parentNode););a.__importDoc=b}return b}function d(a){var b=l(document, -"link[rel=import]:not([import-dependency])"),c=b.length;c?r(b,function(b){return k(b,function(){0===--c&&a()})}):a()}function e(b){function a(){"loading"!==document.readyState&&document.body&&(document.removeEventListener("readystatechange",a),b())}document.addEventListener("readystatechange",a);a()}function f(a){e(function(){return d(function(){return a&&a()})})}function k(a,b){if(a.__loaded)b&&b();else if("script"===a.localName&&!a.src||"style"===a.localName&&!a.firstChild)a.__loaded=!0,b&&b(); -else{var c=function(e){a.removeEventListener(e.type,c);a.__loaded=!0;b&&b()};a.addEventListener("load",c);K&&"style"===a.localName||a.addEventListener("error",c)}}function h(a){return a.nodeType===Node.ELEMENT_NODE&&"link"===a.localName&&"import"===a.rel}function g(){var a=this;this.a={};this.b=0;this.c=new MutationObserver(function(b){return a.s(b)});this.c.observe(document.head,{childList:!0,subtree:!0});this.loadImports(document)}function n(a){r(l(a,"template"),function(a){r(l(a.content,'script:not([type]),script[type="application/javascript"],script[type="text/javascript"],script[type="module"]'), -function(a){var b=document.createElement("script");r(a.attributes,function(a){return b.setAttribute(a.name,a.value)});b.textContent=a.textContent;a.parentNode.replaceChild(b,a)});n(a.content)})}function l(a,b){return a.childNodes.length?a.querySelectorAll(b):Aa}function r(a,b,c){var e=a?a.length:0,d=c?-1:1;for(c=c?e-1:0;c]*)(rel=['|"]?stylesheet['|"]?[^>]*>)/g,t={F:function(a,b){a.href&&a.setAttribute("href",t.j(a.getAttribute("href"),b));a.src&&a.setAttribute("src",t.j(a.getAttribute("src"),b));if("style"===a.localName){var c=t.B(a.textContent,b,Ba);a.textContent=t.B(c,b,Ca)}},B:function(a,b,c){return a.replace(c, -function(a,c,e,d){a=e.replace(/["']/g,"");b&&(a=t.j(a,b));return c+"'"+a+"'"+d})},j:function(a,b){if(void 0===t.m){t.m=!1;try{var c=new URL("b","http://a");c.pathname="c%20d";t.m="http://a/c%20d"===c.href}catch(Wa){}}if(t.m)return(new URL(a,b)).href;c=t.C;c||(c=document.implementation.createHTMLDocument("temp"),t.C=c,c.A=c.createElement("base"),c.head.appendChild(c.A),c.w=c.createElement("a"));c.A.href=b;c.w.href=a;return c.w.href||a}},X={async:!0,load:function(a,b,c){if(a)if(a.match(/^data:/)){a= -a.split(",");var e=a[1];e=-1d.status?b(e,a):c(e)};d.send()}else c("error: href must be specified")}},K=/Trident/.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent); -g.prototype.loadImports=function(a){var b=this;r(l(a,"link[rel=import]"),function(a){return b.g(a)})};g.prototype.g=function(a){var b=this,c=a.href;if(void 0!==this.a[c]){var e=this.a[c];e&&e.__loaded&&(a.__import=e,this.f(a))}else this.b++,this.a[c]="pending",X.load(c,function(a,e){a=b.u(a,e||c);b.a[c]=a;b.b--;b.loadImports(a);b.h()},function(){b.a[c]=null;b.b--;b.h()})};g.prototype.u=function(a,b){if(!a)return document.createDocumentFragment();K&&(a=a.replace(Da,function(a,b,c){return-1===a.indexOf("type=")? -b+" type=import-disable "+c:a}));var c=document.createElement("template");c.innerHTML=a;if(c.content)a=c.content,n(a);else for(a=document.createDocumentFragment();c.firstChild;)a.appendChild(c.firstChild);if(c=a.querySelector("base"))b=t.j(c.getAttribute("href"),b),c.removeAttribute("href");var e=0;r(l(a,'link[rel=import],link[rel=stylesheet][href][type=import-disable],style:not([type]),link[rel=stylesheet][href]:not([type]),script:not([type]),script[type="application/javascript"],script[type="text/javascript"],script[type="module"]'), -function(a){k(a);t.F(a,b);a.setAttribute("import-dependency","");if("script"===a.localName&&!a.src&&a.textContent){if("module"===a.type)throw Error("Inline module scripts are not supported in HTML Imports.");a.setAttribute("src","data:text/javascript;charset=utf-8,"+encodeURIComponent(a.textContent+("\n//# sourceURL="+b+(e?"-"+e:"")+".js\n")));a.textContent="";e++}});return a};g.prototype.h=function(){var a=this;if(!this.b){this.c.disconnect();this.flatten(document);var b=!1,c=!1,e=function(){c&& -b&&(a.loadImports(document),a.b||(a.c.observe(document.head,{childList:!0,subtree:!0}),a.o()))};this.G(function(){c=!0;e()});this.v(function(){b=!0;e()})}};g.prototype.flatten=function(a){var b=this;r(l(a,"link[rel=import]"),function(a){var c=b.a[a.href];(a.__import=c)&&c.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&(b.a[a.href]=a,a.readyState="loading",a.__import=a,b.flatten(c),a.appendChild(c))})};g.prototype.v=function(a){function b(d){if(d {\n\n /********************* base setup *********************/\n const link = document.createElement('link');\n const useNative = Boolean('import' in link);\n const emptyNodeList = link.querySelectorAll('*');\n\n // Polyfill `currentScript` for browsers without it.\n let currentScript = null;\n if ('currentScript' in document === false) {\n Object.defineProperty(document, 'currentScript', {\n get() {\n return currentScript ||\n // NOTE: only works when called in synchronously executing code.\n // readyState should check if `loading` but IE10 is\n // interactive when scripts run so we cheat. This is not needed by\n // html-imports polyfill but helps generally polyfill `currentScript`.\n (document.readyState !== 'complete' ?\n document.scripts[document.scripts.length - 1] : null);\n },\n configurable: true\n });\n }\n\n /**\n * @param {Array|NodeList|NamedNodeMap} list\n * @param {!Function} callback\n * @param {boolean=} inverseOrder\n */\n const forEach = (list, callback, inverseOrder) => {\n const length = list ? list.length : 0;\n const increment = inverseOrder ? -1 : 1;\n let i = inverseOrder ? length - 1 : 0;\n for (; i < length && i >= 0; i = i + increment) {\n callback(list[i], i);\n }\n };\n\n /**\n * @param {!Node} node\n * @param {!string} selector\n * @return {!NodeList}\n */\n const QSA = (node, selector) => {\n // IE 11 throws a SyntaxError if a node with no children is queried with\n // a selector containing the `:not([type])` syntax.\n if (!node.childNodes.length) {\n return emptyNodeList;\n }\n return node.querySelectorAll(selector);\n };\n\n /**\n * @param {!DocumentFragment} fragment\n */\n const replaceScripts = (fragment) => {\n forEach(QSA(fragment, 'template'), template => {\n forEach(QSA(template.content, scriptsSelector), script => {\n const clone = /** @type {!HTMLScriptElement} */\n (document.createElement('script'));\n forEach(script.attributes, attr => clone.setAttribute(attr.name, attr.value));\n clone.textContent = script.textContent;\n script.parentNode.replaceChild(clone, script);\n });\n replaceScripts(template.content);\n });\n };\n\n /********************* path fixup *********************/\n const CSS_URL_REGEXP = /(url\\()([^)]*)(\\))/g;\n const CSS_IMPORT_REGEXP = /(@import[\\s]+(?!url\\())([^;]*)(;)/g;\n const STYLESHEET_REGEXP = /(]*)(rel=['|\"]?stylesheet['|\"]?[^>]*>)/g;\n\n // path fixup: style elements in imports must be made relative to the main\n // document. We fixup url's in url() and @import.\n const Path = {\n\n fixUrls(element, base) {\n if (element.href) {\n element.setAttribute('href',\n Path.resolveUrl(element.getAttribute('href'), base));\n }\n if (element.src) {\n element.setAttribute('src',\n Path.resolveUrl(element.getAttribute('src'), base));\n }\n if (element.localName === 'style') {\n const r = Path.replaceUrls(element.textContent, base, CSS_URL_REGEXP);\n element.textContent = Path.replaceUrls(r, base, CSS_IMPORT_REGEXP);\n }\n },\n\n replaceUrls(text, linkUrl, regexp) {\n return text.replace(regexp, (m, pre, url, post) => {\n let urlPath = url.replace(/[\"']/g, '');\n if (linkUrl) {\n urlPath = Path.resolveUrl(urlPath, linkUrl);\n }\n return pre + '\\'' + urlPath + '\\'' + post;\n });\n },\n\n resolveUrl(url, base) {\n // Lazy feature detection.\n if (Path.__workingURL === undefined) {\n Path.__workingURL = false;\n try {\n const u = new URL('b', 'http://a');\n u.pathname = 'c%20d';\n Path.__workingURL = (u.href === 'http://a/c%20d');\n } catch (e) {}\n }\n\n if (Path.__workingURL) {\n return (new URL(url, base)).href;\n }\n\n // Fallback to creating an anchor into a disconnected document.\n let doc = Path.__tempDoc;\n if (!doc) {\n doc = document.implementation.createHTMLDocument('temp');\n Path.__tempDoc = doc;\n doc.__base = doc.createElement('base');\n doc.head.appendChild(doc.__base);\n doc.__anchor = doc.createElement('a');\n }\n doc.__base.href = base;\n doc.__anchor.href = url;\n return doc.__anchor.href || url;\n }\n };\n\n /********************* Xhr processor *********************/\n const Xhr = {\n\n async: true,\n\n /**\n * @param {!string} url\n * @param {!function(!string, string=)} success\n * @param {!function(!string)} fail\n */\n load(url, success, fail) {\n if (!url) {\n fail('error: href must be specified');\n } else if (url.match(/^data:/)) {\n // Handle Data URI Scheme\n const pieces = url.split(',');\n const header = pieces[0];\n let resource = pieces[1];\n if (header.indexOf(';base64') > -1) {\n resource = atob(resource);\n } else {\n resource = decodeURIComponent(resource);\n }\n success(resource);\n } else {\n const request = new XMLHttpRequest();\n request.open('GET', url, Xhr.async);\n request.onload = () => {\n // Servers redirecting an import can add a Location header to help us\n // polyfill correctly. Handle relative and full paths.\n // Prefer responseURL which already resolves redirects\n // https://xhr.spec.whatwg.org/#the-responseurl-attribute\n let redirectedUrl = request.responseURL || request.getResponseHeader('Location');\n if (redirectedUrl && redirectedUrl.indexOf('/') === 0) {\n // In IE location.origin might not work\n // https://connect.microsoft.com/IE/feedback/details/1763802/location-origin-is-undefined-in-ie-11-on-windows-10-but-works-on-windows-7\n const origin = (location.origin || location.protocol + '//' + location.host);\n redirectedUrl = origin + redirectedUrl;\n }\n const resource = /** @type {string} */ (request.response || request.responseText);\n if (request.status === 304 || request.status === 0 ||\n request.status >= 200 && request.status < 300) {\n success(resource, redirectedUrl);\n } else {\n fail(resource);\n }\n };\n request.send();\n }\n }\n };\n\n /********************* importer *********************/\n\n const isIE = /Trident/.test(navigator.userAgent) ||\n /Edge\\/\\d./i.test(navigator.userAgent);\n\n const importSelector = 'link[rel=import]';\n\n // Used to disable loading of resources.\n const importDisableType = 'import-disable';\n\n const disabledLinkSelector = `link[rel=stylesheet][href][type=${importDisableType}]`;\n\n const scriptsSelector = `script:not([type]),script[type=\"application/javascript\"],` +\n `script[type=\"text/javascript\"],script[type=\"module\"]`;\n\n const importDependenciesSelector = `${importSelector},${disabledLinkSelector},` +\n `style:not([type]),link[rel=stylesheet][href]:not([type]),` + scriptsSelector;\n\n const importDependencyAttr = 'import-dependency';\n\n const rootImportSelector = `${importSelector}:not([${importDependencyAttr}])`;\n\n const pendingScriptsSelector = `script[${importDependencyAttr}]`;\n\n const pendingStylesSelector = `style[${importDependencyAttr}],` +\n `link[rel=stylesheet][${importDependencyAttr}]`;\n\n /**\n * Importer will:\n * - load any linked import documents (with deduping)\n * - whenever an import is loaded, prompt the parser to try to parse\n * - observe imported documents for new elements (these are handled via the\n * dynamic importer)\n */\n class Importer {\n constructor() {\n this.documents = {};\n // Used to keep track of pending loads, so that flattening and firing of\n // events can be done when all resources are ready.\n this.inflight = 0;\n this.dynamicImportsMO = new MutationObserver(m => this.handleMutations(m));\n // Observe changes on .\n this.dynamicImportsMO.observe(document.head, {\n childList: true,\n subtree: true\n });\n // 1. Load imports contents\n // 2. Assign them to first import links on the document\n // 3. Wait for import styles & scripts to be done loading/running\n // 4. Fire load/error events\n this.loadImports(document);\n }\n\n /**\n * @param {!(HTMLDocument|DocumentFragment|Element)} doc\n */\n loadImports(doc) {\n const links = /** @type {!NodeList} */\n (QSA(doc, importSelector));\n forEach(links, link => this.loadImport(link));\n }\n\n /**\n * @param {!HTMLLinkElement} link\n */\n loadImport(link) {\n const url = link.href;\n // This resource is already being handled by another import.\n if (this.documents[url] !== undefined) {\n // If import is already loaded, we can safely associate it to the link\n // and fire the load/error event.\n const imp = this.documents[url];\n if (imp && imp['__loaded']) {\n link['__import'] = imp;\n this.fireEventIfNeeded(link);\n }\n return;\n }\n this.inflight++;\n // Mark it as pending to notify others this url is being loaded.\n this.documents[url] = 'pending';\n Xhr.load(url, (resource, redirectedUrl) => {\n const doc = this.makeDocument(resource, redirectedUrl || url);\n this.documents[url] = doc;\n this.inflight--;\n // Load subtree.\n this.loadImports(doc);\n this.processImportsIfLoadingDone();\n }, () => {\n // If load fails, handle error.\n this.documents[url] = null;\n this.inflight--;\n this.processImportsIfLoadingDone();\n });\n }\n\n /**\n * Creates a new document containing resource and normalizes urls accordingly.\n * @param {string=} resource\n * @param {string=} url\n * @return {!DocumentFragment}\n */\n makeDocument(resource, url) {\n if (!resource) {\n return document.createDocumentFragment();\n }\n\n if (isIE) {\n // should be appended to . Not doing so\n // in IE/Edge breaks the cascading order. We disable the loading by\n // setting the type before setting innerHTML to avoid loading\n // resources twice.\n resource = resource.replace(STYLESHEET_REGEXP, (match, p1, p2) => {\n if (match.indexOf('type=') === -1) {\n return `${p1} type=${importDisableType} ${p2}`;\n }\n return match;\n });\n }\n\n let content;\n const template = /** @type {!HTMLTemplateElement} */\n (document.createElement('template'));\n template.innerHTML = resource;\n if (template.content) {\n content = template.content;\n // Clone scripts inside templates since they won't execute when the\n // hosting template is cloned.\n replaceScripts(content);\n } else {\n //