From 6615bb633444b6b9a2999b27100a129c99ba38a5 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Tue, 9 May 2023 16:56:50 -0700 Subject: [PATCH 01/33] Copy over all unversioned content into a versioned directory The only file changed in the move is the root `docs.json` file. Instead a `v2.json` file was added. --- .../lit-dev-content/site/docs/components/components.json | 1 - packages/lit-dev-content/site/docs/docs.json | 5 ++--- packages/lit-dev-content/site/docs/{ => v2}/api/api.html | 4 ++-- packages/lit-dev-content/site/docs/{ => v2}/api/index.md | 0 .../site/docs/{ => v2}/components/decorators.md | 0 .../site/docs/{ => v2}/components/defining.md | 0 .../lit-dev-content/site/docs/{ => v2}/components/events.md | 0 .../lit-dev-content/site/docs/{ => v2}/components/index.md | 0 .../site/docs/{ => v2}/components/lifecycle.md | 0 .../site/docs/{ => v2}/components/overview.md | 0 .../site/docs/{ => v2}/components/properties.md | 0 .../site/docs/{ => v2}/components/rendering.md | 0 .../site/docs/{ => v2}/components/shadow-dom.md | 0 .../lit-dev-content/site/docs/{ => v2}/components/styles.md | 0 .../site/docs/{ => v2}/composition/component-composition.md | 0 .../site/docs/{ => v2}/composition/controllers.md | 0 .../lit-dev-content/site/docs/{ => v2}/composition/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/composition/mixins.md | 0 .../site/docs/{ => v2}/composition/overview.md | 0 packages/lit-dev-content/site/docs/{ => v2}/data/context.md | 0 packages/lit-dev-content/site/docs/{ => v2}/data/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/frameworks/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/frameworks/react.md | 0 .../lit-dev-content/site/docs/{ => v2}/getting-started.md | 0 packages/lit-dev-content/site/docs/{ => v2}/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/internal/demos.md | 0 .../lit-dev-content/site/docs/{ => v2}/internal/styles.md | 0 packages/lit-dev-content/site/docs/{ => v2}/introduction.md | 0 .../lit-dev-content/site/docs/{ => v2}/libraries/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/libraries/labs.md | 0 .../site/docs/{ => v2}/libraries/standalone-templates.md | 0 .../site/docs/{ => v2}/localization/best-practices.md | 0 .../site/docs/{ => v2}/localization/cli-and-config.md | 0 .../lit-dev-content/site/docs/{ => v2}/localization/index.md | 0 .../site/docs/{ => v2}/localization/overview.md | 0 .../site/docs/{ => v2}/localization/runtime-mode.md | 0 .../site/docs/{ => v2}/localization/transform-mode.md | 0 .../lit-dev-content/site/docs/{ => v2}/releases/index.md | 0 .../site/docs/{ => v2}/releases/release-notes/1.2.0.md | 0 .../site/docs/{ => v2}/releases/release-notes/1.3.0.md | 0 .../docs/{ => v2}/releases/release-notes/release-notes.json | 0 .../lit-dev-content/site/docs/{ => v2}/releases/upgrade.md | 0 .../site/docs/{ => v2}/resources/community.md | 0 .../lit-dev-content/site/docs/{ => v2}/resources/index.md | 0 packages/lit-dev-content/site/docs/{ => v2}/ssr/authoring.md | 0 .../lit-dev-content/site/docs/{ => v2}/ssr/client-usage.md | 0 .../lit-dev-content/site/docs/{ => v2}/ssr/dom-emulation.md | 0 packages/lit-dev-content/site/docs/{ => v2}/ssr/index.md | 0 packages/lit-dev-content/site/docs/{ => v2}/ssr/overview.md | 0 .../lit-dev-content/site/docs/{ => v2}/ssr/server-usage.md | 0 .../site/docs/{ => v2}/templates/conditionals.md | 0 .../site/docs/{ => v2}/templates/custom-directives.md | 0 .../site/docs/{ => v2}/templates/directives.md | 0 .../site/docs/{ => v2}/templates/expressions.md | 0 .../lit-dev-content/site/docs/{ => v2}/templates/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/templates/lists.md | 0 .../lit-dev-content/site/docs/{ => v2}/templates/overview.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/adding-lit.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/development.md | 0 packages/lit-dev-content/site/docs/{ => v2}/tools/index.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/overview.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/production.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/publishing.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/requirements.md | 0 .../lit-dev-content/site/docs/{ => v2}/tools/starter-kits.md | 0 packages/lit-dev-content/site/docs/{ => v2}/tools/testing.md | 0 packages/lit-dev-content/site/docs/v2/v2.json | 3 +++ 67 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 packages/lit-dev-content/site/docs/components/components.json rename packages/lit-dev-content/site/docs/{ => v2}/api/api.html (78%) rename packages/lit-dev-content/site/docs/{ => v2}/api/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/decorators.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/defining.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/events.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/lifecycle.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/overview.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/properties.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/rendering.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/shadow-dom.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/components/styles.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/composition/component-composition.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/composition/controllers.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/composition/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/composition/mixins.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/composition/overview.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/data/context.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/data/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/frameworks/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/frameworks/react.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/getting-started.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/internal/demos.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/internal/styles.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/introduction.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/libraries/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/libraries/labs.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/libraries/standalone-templates.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/localization/best-practices.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/localization/cli-and-config.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/localization/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/localization/overview.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/localization/runtime-mode.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/localization/transform-mode.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/releases/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/releases/release-notes/1.2.0.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/releases/release-notes/1.3.0.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/releases/release-notes/release-notes.json (100%) rename packages/lit-dev-content/site/docs/{ => v2}/releases/upgrade.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/resources/community.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/resources/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/ssr/authoring.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/ssr/client-usage.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/ssr/dom-emulation.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/ssr/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/ssr/overview.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/ssr/server-usage.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/conditionals.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/custom-directives.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/directives.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/expressions.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/lists.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/templates/overview.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/adding-lit.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/development.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/index.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/overview.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/production.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/publishing.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/requirements.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/starter-kits.md (100%) rename packages/lit-dev-content/site/docs/{ => v2}/tools/testing.md (100%) create mode 100644 packages/lit-dev-content/site/docs/v2/v2.json diff --git a/packages/lit-dev-content/site/docs/components/components.json b/packages/lit-dev-content/site/docs/components/components.json deleted file mode 100644 index 0967ef424..000000000 --- a/packages/lit-dev-content/site/docs/components/components.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/packages/lit-dev-content/site/docs/docs.json b/packages/lit-dev-content/site/docs/docs.json index 8d90bcdb3..891f1b7c3 100644 --- a/packages/lit-dev-content/site/docs/docs.json +++ b/packages/lit-dev-content/site/docs/docs.json @@ -1,4 +1,3 @@ { - "layout": "docs", - "collection": "docs-v2" -} + "layout": "docs" +} \ No newline at end of file diff --git a/packages/lit-dev-content/site/docs/api/api.html b/packages/lit-dev-content/site/docs/v2/api/api.html similarity index 78% rename from packages/lit-dev-content/site/docs/api/api.html rename to packages/lit-dev-content/site/docs/v2/api/api.html index 11299de03..b0cd8cebc 100644 --- a/packages/lit-dev-content/site/docs/api/api.html +++ b/packages/lit-dev-content/site/docs/v2/api/api.html @@ -7,12 +7,12 @@ alias: data addAllPagesToCollections: true -permalink: "docs/api/{{ data.slug }}/" +permalink: "docs/v2/api/{{ data.slug }}/" eleventyComputed: title: "{{ data.title }}" eleventyNavigation: key: "{{ data.title }}" parent: API - apiPath: /docs/api + apiPath: /docs/v2/api --- diff --git a/packages/lit-dev-content/site/docs/api/index.md b/packages/lit-dev-content/site/docs/v2/api/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/api/index.md rename to packages/lit-dev-content/site/docs/v2/api/index.md diff --git a/packages/lit-dev-content/site/docs/components/decorators.md b/packages/lit-dev-content/site/docs/v2/components/decorators.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/decorators.md rename to packages/lit-dev-content/site/docs/v2/components/decorators.md diff --git a/packages/lit-dev-content/site/docs/components/defining.md b/packages/lit-dev-content/site/docs/v2/components/defining.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/defining.md rename to packages/lit-dev-content/site/docs/v2/components/defining.md diff --git a/packages/lit-dev-content/site/docs/components/events.md b/packages/lit-dev-content/site/docs/v2/components/events.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/events.md rename to packages/lit-dev-content/site/docs/v2/components/events.md diff --git a/packages/lit-dev-content/site/docs/components/index.md b/packages/lit-dev-content/site/docs/v2/components/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/index.md rename to packages/lit-dev-content/site/docs/v2/components/index.md diff --git a/packages/lit-dev-content/site/docs/components/lifecycle.md b/packages/lit-dev-content/site/docs/v2/components/lifecycle.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/lifecycle.md rename to packages/lit-dev-content/site/docs/v2/components/lifecycle.md diff --git a/packages/lit-dev-content/site/docs/components/overview.md b/packages/lit-dev-content/site/docs/v2/components/overview.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/overview.md rename to packages/lit-dev-content/site/docs/v2/components/overview.md diff --git a/packages/lit-dev-content/site/docs/components/properties.md b/packages/lit-dev-content/site/docs/v2/components/properties.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/properties.md rename to packages/lit-dev-content/site/docs/v2/components/properties.md diff --git a/packages/lit-dev-content/site/docs/components/rendering.md b/packages/lit-dev-content/site/docs/v2/components/rendering.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/rendering.md rename to packages/lit-dev-content/site/docs/v2/components/rendering.md diff --git a/packages/lit-dev-content/site/docs/components/shadow-dom.md b/packages/lit-dev-content/site/docs/v2/components/shadow-dom.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/shadow-dom.md rename to packages/lit-dev-content/site/docs/v2/components/shadow-dom.md diff --git a/packages/lit-dev-content/site/docs/components/styles.md b/packages/lit-dev-content/site/docs/v2/components/styles.md similarity index 100% rename from packages/lit-dev-content/site/docs/components/styles.md rename to packages/lit-dev-content/site/docs/v2/components/styles.md diff --git a/packages/lit-dev-content/site/docs/composition/component-composition.md b/packages/lit-dev-content/site/docs/v2/composition/component-composition.md similarity index 100% rename from packages/lit-dev-content/site/docs/composition/component-composition.md rename to packages/lit-dev-content/site/docs/v2/composition/component-composition.md diff --git a/packages/lit-dev-content/site/docs/composition/controllers.md b/packages/lit-dev-content/site/docs/v2/composition/controllers.md similarity index 100% rename from packages/lit-dev-content/site/docs/composition/controllers.md rename to packages/lit-dev-content/site/docs/v2/composition/controllers.md diff --git a/packages/lit-dev-content/site/docs/composition/index.md b/packages/lit-dev-content/site/docs/v2/composition/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/composition/index.md rename to packages/lit-dev-content/site/docs/v2/composition/index.md diff --git a/packages/lit-dev-content/site/docs/composition/mixins.md b/packages/lit-dev-content/site/docs/v2/composition/mixins.md similarity index 100% rename from packages/lit-dev-content/site/docs/composition/mixins.md rename to packages/lit-dev-content/site/docs/v2/composition/mixins.md diff --git a/packages/lit-dev-content/site/docs/composition/overview.md b/packages/lit-dev-content/site/docs/v2/composition/overview.md similarity index 100% rename from packages/lit-dev-content/site/docs/composition/overview.md rename to packages/lit-dev-content/site/docs/v2/composition/overview.md diff --git a/packages/lit-dev-content/site/docs/data/context.md b/packages/lit-dev-content/site/docs/v2/data/context.md similarity index 100% rename from packages/lit-dev-content/site/docs/data/context.md rename to packages/lit-dev-content/site/docs/v2/data/context.md diff --git a/packages/lit-dev-content/site/docs/data/index.md b/packages/lit-dev-content/site/docs/v2/data/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/data/index.md rename to packages/lit-dev-content/site/docs/v2/data/index.md diff --git a/packages/lit-dev-content/site/docs/frameworks/index.md b/packages/lit-dev-content/site/docs/v2/frameworks/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/frameworks/index.md rename to packages/lit-dev-content/site/docs/v2/frameworks/index.md diff --git a/packages/lit-dev-content/site/docs/frameworks/react.md b/packages/lit-dev-content/site/docs/v2/frameworks/react.md similarity index 100% rename from packages/lit-dev-content/site/docs/frameworks/react.md rename to packages/lit-dev-content/site/docs/v2/frameworks/react.md diff --git a/packages/lit-dev-content/site/docs/getting-started.md b/packages/lit-dev-content/site/docs/v2/getting-started.md similarity index 100% rename from packages/lit-dev-content/site/docs/getting-started.md rename to packages/lit-dev-content/site/docs/v2/getting-started.md diff --git a/packages/lit-dev-content/site/docs/index.md b/packages/lit-dev-content/site/docs/v2/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/index.md rename to packages/lit-dev-content/site/docs/v2/index.md diff --git a/packages/lit-dev-content/site/docs/internal/demos.md b/packages/lit-dev-content/site/docs/v2/internal/demos.md similarity index 100% rename from packages/lit-dev-content/site/docs/internal/demos.md rename to packages/lit-dev-content/site/docs/v2/internal/demos.md diff --git a/packages/lit-dev-content/site/docs/internal/styles.md b/packages/lit-dev-content/site/docs/v2/internal/styles.md similarity index 100% rename from packages/lit-dev-content/site/docs/internal/styles.md rename to packages/lit-dev-content/site/docs/v2/internal/styles.md diff --git a/packages/lit-dev-content/site/docs/introduction.md b/packages/lit-dev-content/site/docs/v2/introduction.md similarity index 100% rename from packages/lit-dev-content/site/docs/introduction.md rename to packages/lit-dev-content/site/docs/v2/introduction.md diff --git a/packages/lit-dev-content/site/docs/libraries/index.md b/packages/lit-dev-content/site/docs/v2/libraries/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/libraries/index.md rename to packages/lit-dev-content/site/docs/v2/libraries/index.md diff --git a/packages/lit-dev-content/site/docs/libraries/labs.md b/packages/lit-dev-content/site/docs/v2/libraries/labs.md similarity index 100% rename from packages/lit-dev-content/site/docs/libraries/labs.md rename to packages/lit-dev-content/site/docs/v2/libraries/labs.md diff --git a/packages/lit-dev-content/site/docs/libraries/standalone-templates.md b/packages/lit-dev-content/site/docs/v2/libraries/standalone-templates.md similarity index 100% rename from packages/lit-dev-content/site/docs/libraries/standalone-templates.md rename to packages/lit-dev-content/site/docs/v2/libraries/standalone-templates.md diff --git a/packages/lit-dev-content/site/docs/localization/best-practices.md b/packages/lit-dev-content/site/docs/v2/localization/best-practices.md similarity index 100% rename from packages/lit-dev-content/site/docs/localization/best-practices.md rename to packages/lit-dev-content/site/docs/v2/localization/best-practices.md diff --git a/packages/lit-dev-content/site/docs/localization/cli-and-config.md b/packages/lit-dev-content/site/docs/v2/localization/cli-and-config.md similarity index 100% rename from packages/lit-dev-content/site/docs/localization/cli-and-config.md rename to packages/lit-dev-content/site/docs/v2/localization/cli-and-config.md diff --git a/packages/lit-dev-content/site/docs/localization/index.md b/packages/lit-dev-content/site/docs/v2/localization/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/localization/index.md rename to packages/lit-dev-content/site/docs/v2/localization/index.md diff --git a/packages/lit-dev-content/site/docs/localization/overview.md b/packages/lit-dev-content/site/docs/v2/localization/overview.md similarity index 100% rename from packages/lit-dev-content/site/docs/localization/overview.md rename to packages/lit-dev-content/site/docs/v2/localization/overview.md diff --git a/packages/lit-dev-content/site/docs/localization/runtime-mode.md b/packages/lit-dev-content/site/docs/v2/localization/runtime-mode.md similarity index 100% rename from packages/lit-dev-content/site/docs/localization/runtime-mode.md rename to packages/lit-dev-content/site/docs/v2/localization/runtime-mode.md diff --git a/packages/lit-dev-content/site/docs/localization/transform-mode.md b/packages/lit-dev-content/site/docs/v2/localization/transform-mode.md similarity index 100% rename from packages/lit-dev-content/site/docs/localization/transform-mode.md rename to packages/lit-dev-content/site/docs/v2/localization/transform-mode.md diff --git a/packages/lit-dev-content/site/docs/releases/index.md b/packages/lit-dev-content/site/docs/v2/releases/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/releases/index.md rename to packages/lit-dev-content/site/docs/v2/releases/index.md diff --git a/packages/lit-dev-content/site/docs/releases/release-notes/1.2.0.md b/packages/lit-dev-content/site/docs/v2/releases/release-notes/1.2.0.md similarity index 100% rename from packages/lit-dev-content/site/docs/releases/release-notes/1.2.0.md rename to packages/lit-dev-content/site/docs/v2/releases/release-notes/1.2.0.md diff --git a/packages/lit-dev-content/site/docs/releases/release-notes/1.3.0.md b/packages/lit-dev-content/site/docs/v2/releases/release-notes/1.3.0.md similarity index 100% rename from packages/lit-dev-content/site/docs/releases/release-notes/1.3.0.md rename to packages/lit-dev-content/site/docs/v2/releases/release-notes/1.3.0.md diff --git a/packages/lit-dev-content/site/docs/releases/release-notes/release-notes.json b/packages/lit-dev-content/site/docs/v2/releases/release-notes/release-notes.json similarity index 100% rename from packages/lit-dev-content/site/docs/releases/release-notes/release-notes.json rename to packages/lit-dev-content/site/docs/v2/releases/release-notes/release-notes.json diff --git a/packages/lit-dev-content/site/docs/releases/upgrade.md b/packages/lit-dev-content/site/docs/v2/releases/upgrade.md similarity index 100% rename from packages/lit-dev-content/site/docs/releases/upgrade.md rename to packages/lit-dev-content/site/docs/v2/releases/upgrade.md diff --git a/packages/lit-dev-content/site/docs/resources/community.md b/packages/lit-dev-content/site/docs/v2/resources/community.md similarity index 100% rename from packages/lit-dev-content/site/docs/resources/community.md rename to packages/lit-dev-content/site/docs/v2/resources/community.md diff --git a/packages/lit-dev-content/site/docs/resources/index.md b/packages/lit-dev-content/site/docs/v2/resources/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/resources/index.md rename to packages/lit-dev-content/site/docs/v2/resources/index.md diff --git a/packages/lit-dev-content/site/docs/ssr/authoring.md b/packages/lit-dev-content/site/docs/v2/ssr/authoring.md similarity index 100% rename from packages/lit-dev-content/site/docs/ssr/authoring.md rename to packages/lit-dev-content/site/docs/v2/ssr/authoring.md diff --git a/packages/lit-dev-content/site/docs/ssr/client-usage.md b/packages/lit-dev-content/site/docs/v2/ssr/client-usage.md similarity index 100% rename from packages/lit-dev-content/site/docs/ssr/client-usage.md rename to packages/lit-dev-content/site/docs/v2/ssr/client-usage.md diff --git a/packages/lit-dev-content/site/docs/ssr/dom-emulation.md b/packages/lit-dev-content/site/docs/v2/ssr/dom-emulation.md similarity index 100% rename from packages/lit-dev-content/site/docs/ssr/dom-emulation.md rename to packages/lit-dev-content/site/docs/v2/ssr/dom-emulation.md diff --git a/packages/lit-dev-content/site/docs/ssr/index.md b/packages/lit-dev-content/site/docs/v2/ssr/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/ssr/index.md rename to packages/lit-dev-content/site/docs/v2/ssr/index.md diff --git a/packages/lit-dev-content/site/docs/ssr/overview.md b/packages/lit-dev-content/site/docs/v2/ssr/overview.md similarity index 100% rename from packages/lit-dev-content/site/docs/ssr/overview.md rename to packages/lit-dev-content/site/docs/v2/ssr/overview.md diff --git a/packages/lit-dev-content/site/docs/ssr/server-usage.md b/packages/lit-dev-content/site/docs/v2/ssr/server-usage.md similarity index 100% rename from packages/lit-dev-content/site/docs/ssr/server-usage.md rename to packages/lit-dev-content/site/docs/v2/ssr/server-usage.md diff --git a/packages/lit-dev-content/site/docs/templates/conditionals.md b/packages/lit-dev-content/site/docs/v2/templates/conditionals.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/conditionals.md rename to packages/lit-dev-content/site/docs/v2/templates/conditionals.md diff --git a/packages/lit-dev-content/site/docs/templates/custom-directives.md b/packages/lit-dev-content/site/docs/v2/templates/custom-directives.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/custom-directives.md rename to packages/lit-dev-content/site/docs/v2/templates/custom-directives.md diff --git a/packages/lit-dev-content/site/docs/templates/directives.md b/packages/lit-dev-content/site/docs/v2/templates/directives.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/directives.md rename to packages/lit-dev-content/site/docs/v2/templates/directives.md diff --git a/packages/lit-dev-content/site/docs/templates/expressions.md b/packages/lit-dev-content/site/docs/v2/templates/expressions.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/expressions.md rename to packages/lit-dev-content/site/docs/v2/templates/expressions.md diff --git a/packages/lit-dev-content/site/docs/templates/index.md b/packages/lit-dev-content/site/docs/v2/templates/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/index.md rename to packages/lit-dev-content/site/docs/v2/templates/index.md diff --git a/packages/lit-dev-content/site/docs/templates/lists.md b/packages/lit-dev-content/site/docs/v2/templates/lists.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/lists.md rename to packages/lit-dev-content/site/docs/v2/templates/lists.md diff --git a/packages/lit-dev-content/site/docs/templates/overview.md b/packages/lit-dev-content/site/docs/v2/templates/overview.md similarity index 100% rename from packages/lit-dev-content/site/docs/templates/overview.md rename to packages/lit-dev-content/site/docs/v2/templates/overview.md diff --git a/packages/lit-dev-content/site/docs/tools/adding-lit.md b/packages/lit-dev-content/site/docs/v2/tools/adding-lit.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/adding-lit.md rename to packages/lit-dev-content/site/docs/v2/tools/adding-lit.md diff --git a/packages/lit-dev-content/site/docs/tools/development.md b/packages/lit-dev-content/site/docs/v2/tools/development.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/development.md rename to packages/lit-dev-content/site/docs/v2/tools/development.md diff --git a/packages/lit-dev-content/site/docs/tools/index.md b/packages/lit-dev-content/site/docs/v2/tools/index.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/index.md rename to packages/lit-dev-content/site/docs/v2/tools/index.md diff --git a/packages/lit-dev-content/site/docs/tools/overview.md b/packages/lit-dev-content/site/docs/v2/tools/overview.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/overview.md rename to packages/lit-dev-content/site/docs/v2/tools/overview.md diff --git a/packages/lit-dev-content/site/docs/tools/production.md b/packages/lit-dev-content/site/docs/v2/tools/production.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/production.md rename to packages/lit-dev-content/site/docs/v2/tools/production.md diff --git a/packages/lit-dev-content/site/docs/tools/publishing.md b/packages/lit-dev-content/site/docs/v2/tools/publishing.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/publishing.md rename to packages/lit-dev-content/site/docs/v2/tools/publishing.md diff --git a/packages/lit-dev-content/site/docs/tools/requirements.md b/packages/lit-dev-content/site/docs/v2/tools/requirements.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/requirements.md rename to packages/lit-dev-content/site/docs/v2/tools/requirements.md diff --git a/packages/lit-dev-content/site/docs/tools/starter-kits.md b/packages/lit-dev-content/site/docs/v2/tools/starter-kits.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/starter-kits.md rename to packages/lit-dev-content/site/docs/v2/tools/starter-kits.md diff --git a/packages/lit-dev-content/site/docs/tools/testing.md b/packages/lit-dev-content/site/docs/v2/tools/testing.md similarity index 100% rename from packages/lit-dev-content/site/docs/tools/testing.md rename to packages/lit-dev-content/site/docs/v2/tools/testing.md diff --git a/packages/lit-dev-content/site/docs/v2/v2.json b/packages/lit-dev-content/site/docs/v2/v2.json new file mode 100644 index 000000000..4a1be1b03 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v2/v2.json @@ -0,0 +1,3 @@ +{ + "collection": "docs-v2" +} From 19a07fb3b2338d047896186be67d53e8076694a8 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Tue, 9 May 2023 17:00:09 -0700 Subject: [PATCH 02/33] Add initial build-unversioned-docs script. This copies over and adds the correct permalinks to the selected version content. Currently moving `v2` -> `unversioned`. --- .gitignore | 4 + packages/lit-dev-content/.eleventy.js | 22 ++- packages/lit-dev-content/package.json | 14 +- .../src/build-unversioned-docs.ts | 154 ++++++++++++++++++ 4 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 packages/lit-dev-tools-esm/src/build-unversioned-docs.ts diff --git a/.gitignore b/.gitignore index 8f464b6da..35c4ebb08 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ packages/lit-dev-content/site/fonts/manrope packages/lit-dev-content/temp packages/lit-dev-content/samples/js packages/lit-dev-content/src/public-vars.ts +# The unversioned docs are generated by: +# +# `npm run build:unversioned-docs -w lit-dev-content` +packages/lit-dev-content/site/docs/unversioned packages/lit-dev-api/api-data/*/repo/ packages/lit-dev-api/api-data/*/INSTALLED diff --git a/packages/lit-dev-content/.eleventy.js b/packages/lit-dev-content/.eleventy.js index 8f7e453e5..3cd758c37 100644 --- a/packages/lit-dev-content/.eleventy.js +++ b/packages/lit-dev-content/.eleventy.js @@ -42,6 +42,10 @@ const DEV = ENV.eleventyMode === 'dev'; const cspInlineScriptHashes = new Set(); +/** + * @param {import("@11ty/eleventy/src/UserConfig")} eleventyConfig + * @returns {ReturnType} + */ module.exports = function (eleventyConfig) { // https://github.com/JordanShurmer/eleventy-plugin-toc#readme eleventyConfig.addPlugin(pluginTOC, { @@ -194,7 +198,19 @@ ${content} eleventyConfig.addCollection('docs-v2', function (collection) { const docs = collection - .getFilteredByGlob(['site/docs/*', 'site/docs/!(v1)/**']) + .getFilteredByGlob(['site/docs/v2/**']) + .sort(sortDocs); + for (const page of docs) { + documentByUrl.set(page.url, page); + } + return docs; + }); + + // Collection that contains the built duplicate docs for the current + // recommended version of Lit. + eleventyConfig.addCollection('docs-unversioned', function (collection) { + const docs = collection + .getFilteredByGlob(['site/docs/unversioned/**']) .sort(sortDocs); for (const page of docs) { documentByUrl.set(page.url, page); @@ -487,8 +503,10 @@ ${content} ENV.eleventyOutDir + '/docs/*/index.html', ENV.eleventyOutDir + '/docs/v1/introduction.html', ENV.eleventyOutDir + '/docs/v1/*/index.html', + ENV.eleventyOutDir + '/docs/v2/introduction.html', + ENV.eleventyOutDir + '/docs/v2/*/index.html', ], - {ignore: ENV.eleventyOutDir + '/docs/v1/index.html'} + {ignore: ENV.eleventyOutDir + '/docs/(v1|v2)/index.html'} ) ).filter( // TODO(aomarks) This is brittle, we need a way to annotate inside an md diff --git a/packages/lit-dev-content/package.json b/packages/lit-dev-content/package.json index 5ee0a1080..61cb00cab 100644 --- a/packages/lit-dev-content/package.json +++ b/packages/lit-dev-content/package.json @@ -12,6 +12,7 @@ "build:ts": "wireit", "build:rollup": "wireit", "build:samples": "wireit", + "build:unversioned-docs": "wireit", "dev:build": "wireit", "dev:serve": "wireit", "prod:build": "wireit", @@ -36,6 +37,7 @@ "dependencies": [ "build:ts", "fonts:manrope", + "build:unversioned-docs", { "script": "build:samples", "cascade": false @@ -86,7 +88,8 @@ "build:ts", "build:samples", "build:rollup", - "fonts:manrope" + "fonts:manrope", + "build:unversioned-docs" ] }, "fonts:manrope": { @@ -135,6 +138,15 @@ "output": [ "samples/js/**" ] + }, + "build:unversioned-docs": { + "command": "node ../lit-dev-tools-esm/lib/build-unversioned-docs.js", + "dependencies": [ + "../lit-dev-tools-esm:build" + ], + "output": [ + "site/docs/unversioned" + ] } }, "devDependencies": { diff --git a/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts b/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts new file mode 100644 index 000000000..55cf205ad --- /dev/null +++ b/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {fileURLToPath} from 'url'; +import * as pathlib from 'path'; +import * as fs from 'fs'; +import * as fsPromise from 'fs/promises'; + +const THIS_DIR = pathlib.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = pathlib.resolve(THIS_DIR, '..', '..', '..'); +const CONTENT_PKG = pathlib.resolve(REPO_ROOT, 'packages', 'lit-dev-content'); +const SITE_JSON = pathlib.resolve(CONTENT_PKG, 'site', 'site.json'); + +interface SiteJSON { + selectedVersion: 'v1' | 'v2'; +} + +type EleventyFrontMatterData = string[]; + +const SITE_SELECTED_VERSION = ( + JSON.parse(fs.readFileSync(SITE_JSON, 'utf8')) as SiteJSON +).selectedVersion; +const SELECTED_VERSION_CONTENT = pathlib.resolve( + CONTENT_PKG, + 'site', + 'docs', + SITE_SELECTED_VERSION +); +console.log(SELECTED_VERSION_CONTENT); +const UNVERSIONED_VERSION_LOCATION = pathlib.resolve( + CONTENT_PKG, + 'site', + 'docs', + 'unversioned' +); + +/** + * This script builds our unversioned latest documentation for lit.dev. It + * locates the current selectedVersion of Lit to show from `site.json`, then + * copies that documentation from the versioned subdirectory into an unversioned + * subdirectory. + * + * The following transforms are then applied on the files: + * - Permalink frontmatter is added to strip the version from the URL. E.g., + * /docs/v2/* becomes /docs/*. + * - Versioned cross links are detected and made unversioned. + */ +const buildAndTransformUnverionedDocs = async () => { + const walk = async (dirPath: string): Promise => { + const entries = await fsPromise.readdir(dirPath, {withFileTypes: true}); + await Promise.all( + entries.map(async (entry) => { + const childPath = pathlib.join(dirPath, entry.name); + if (entry.isDirectory()) { + return walk(childPath); + } else { + transformFile(childPath); + } + }) + ); + }; + + console.log('Starting build-unversioned-docs.js script'); + await walk(SELECTED_VERSION_CONTENT); + console.log('Completed build-unversioned-docs.js script'); +}; + +/** + * Transform the given versioned content file and copy it to the unversioned + * directory. + */ +function transformFile(path: string) { + let fileContents = fs.readFileSync(path, {encoding: 'utf8'}); + const relativeChildPath = pathlib.relative(SELECTED_VERSION_CONTENT, path); + const ext = pathlib.extname(relativeChildPath); + let unversionedLocation = pathlib.join( + UNVERSIONED_VERSION_LOCATION, + relativeChildPath + ); + + if (ext === '.md') { + const fileName = pathlib.basename(unversionedLocation, '.md'); + const [frontMatterData, restOfFile] = getFrontMatterData(fileContents); + const existingPermalink = frontMatterData.findIndex((val) => + val.includes('permalink:') + ); + if (existingPermalink !== -1) { + throw new Error( + 'Unhandled case: Handle this by transforming the permalink here.' + ); + } + if (fileName === 'index') { + frontMatterData.push( + `permalink: docs/${relativeChildPath.slice(0, -3)}.html` + ); + } else { + frontMatterData.push( + `permalink: docs/${relativeChildPath.slice(0, -3)}/index.html` + ); + } + + fileContents = writeFrontMatter(frontMatterData) + restOfFile; + } else if (ext === '.json') { + if ( + pathlib.basename(unversionedLocation, '.json') === SITE_SELECTED_VERSION + ) { + unversionedLocation = pathlib.join( + pathlib.dirname(unversionedLocation), + 'unversioned.json' + ); + fileContents = JSON.stringify({ + collection: 'docs-unversioned', + selectedversion: SITE_SELECTED_VERSION, + }); + } + } else if (ext === '.html') { + if (pathlib.basename(unversionedLocation, '.html') === 'api') { + const [frontMatterData, _] = getFrontMatterData(fileContents); + const transformedFrontMatter = frontMatterData.map((line) => { + return line.replace(`/${SITE_SELECTED_VERSION}/`, '/'); + }); + fileContents = writeFrontMatter(transformedFrontMatter); + } else { + throw new Error(`Unhandled html document: '${path}'`); + } + } else { + throw new Error(`Unhandled extension '${ext}' for '${path}'`); + } + + fs.mkdirSync(pathlib.dirname(unversionedLocation), {recursive: true}); + fs.writeFileSync(unversionedLocation, fileContents); +} + +/** + * Retrieve the 11ty frontmatter from our docs. This is not a full fledged YAML + * parser. + */ +function getFrontMatterData( + fileData: string +): [EleventyFrontMatterData, string] { + const splitFile = fileData.split('---'); + const frontMatterData = splitFile[1].split('\n').slice(1, -1); + const restOfFile = splitFile.slice(2).join('---'); + return [frontMatterData, restOfFile]; +} + +function writeFrontMatter(frontmatter: EleventyFrontMatterData): string { + return '---\n' + frontmatter.join('\n') + '\n---'; +} + +buildAndTransformUnverionedDocs(); From abfec328dac37066bdbeb770db0765e241f86fb5 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Wed, 10 May 2023 09:51:36 -0700 Subject: [PATCH 03/33] Fix wireit so npm run dev doesnt spin infinitely. --- packages/lit-dev-content/package.json | 6 ++++- .../src/build-unversioned-docs.ts | 24 +++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/lit-dev-content/package.json b/packages/lit-dev-content/package.json index 61cb00cab..6dc04edbf 100644 --- a/packages/lit-dev-content/package.json +++ b/packages/lit-dev-content/package.json @@ -144,8 +144,12 @@ "dependencies": [ "../lit-dev-tools-esm:build" ], + "files": [ + "site/docs/**", + "!site/docs/unversioned" + ], "output": [ - "site/docs/unversioned" + "site/docs/unversioned/**" ] } }, diff --git a/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts b/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts index 55cf205ad..54b8d33c8 100644 --- a/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts +++ b/packages/lit-dev-tools-esm/src/build-unversioned-docs.ts @@ -15,21 +15,21 @@ const CONTENT_PKG = pathlib.resolve(REPO_ROOT, 'packages', 'lit-dev-content'); const SITE_JSON = pathlib.resolve(CONTENT_PKG, 'site', 'site.json'); interface SiteJSON { - selectedVersion: 'v1' | 'v2'; + latestVersion: 'v1' | 'v2'; } type EleventyFrontMatterData = string[]; -const SITE_SELECTED_VERSION = ( +const SITE_LATEST_VERSION = ( JSON.parse(fs.readFileSync(SITE_JSON, 'utf8')) as SiteJSON -).selectedVersion; -const SELECTED_VERSION_CONTENT = pathlib.resolve( +).latestVersion; +const LATEST_VERSION_CONTENT = pathlib.resolve( CONTENT_PKG, 'site', 'docs', - SITE_SELECTED_VERSION + SITE_LATEST_VERSION ); -console.log(SELECTED_VERSION_CONTENT); +console.log(LATEST_VERSION_CONTENT); const UNVERSIONED_VERSION_LOCATION = pathlib.resolve( CONTENT_PKG, 'site', @@ -39,7 +39,7 @@ const UNVERSIONED_VERSION_LOCATION = pathlib.resolve( /** * This script builds our unversioned latest documentation for lit.dev. It - * locates the current selectedVersion of Lit to show from `site.json`, then + * locates the current latestVersion of Lit to show from `site.json`, then * copies that documentation from the versioned subdirectory into an unversioned * subdirectory. * @@ -64,7 +64,7 @@ const buildAndTransformUnverionedDocs = async () => { }; console.log('Starting build-unversioned-docs.js script'); - await walk(SELECTED_VERSION_CONTENT); + await walk(LATEST_VERSION_CONTENT); console.log('Completed build-unversioned-docs.js script'); }; @@ -74,7 +74,7 @@ const buildAndTransformUnverionedDocs = async () => { */ function transformFile(path: string) { let fileContents = fs.readFileSync(path, {encoding: 'utf8'}); - const relativeChildPath = pathlib.relative(SELECTED_VERSION_CONTENT, path); + const relativeChildPath = pathlib.relative(LATEST_VERSION_CONTENT, path); const ext = pathlib.extname(relativeChildPath); let unversionedLocation = pathlib.join( UNVERSIONED_VERSION_LOCATION, @@ -105,7 +105,7 @@ function transformFile(path: string) { fileContents = writeFrontMatter(frontMatterData) + restOfFile; } else if (ext === '.json') { if ( - pathlib.basename(unversionedLocation, '.json') === SITE_SELECTED_VERSION + pathlib.basename(unversionedLocation, '.json') === SITE_LATEST_VERSION ) { unversionedLocation = pathlib.join( pathlib.dirname(unversionedLocation), @@ -113,14 +113,14 @@ function transformFile(path: string) { ); fileContents = JSON.stringify({ collection: 'docs-unversioned', - selectedversion: SITE_SELECTED_VERSION, + latestVersion: SITE_LATEST_VERSION, }); } } else if (ext === '.html') { if (pathlib.basename(unversionedLocation, '.html') === 'api') { const [frontMatterData, _] = getFrontMatterData(fileContents); const transformedFrontMatter = frontMatterData.map((line) => { - return line.replace(`/${SITE_SELECTED_VERSION}/`, '/'); + return line.replace(`/${SITE_LATEST_VERSION}/`, '/'); }); fileContents = writeFrontMatter(transformedFrontMatter); } else { From e56c92bea52a5bb710b2dc0510c808a681fc5ddc Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Wed, 10 May 2023 11:14:48 -0700 Subject: [PATCH 04/33] Add rel=canonical link from latestVersion to unversioned page. --- packages/lit-dev-content/.eleventy.js | 21 +++++++++++++++++++ .../lit-dev-content/site/_includes/docs.html | 7 +++++++ 2 files changed, 28 insertions(+) diff --git a/packages/lit-dev-content/.eleventy.js b/packages/lit-dev-content/.eleventy.js index 3cd758c37..0a119efa7 100644 --- a/packages/lit-dev-content/.eleventy.js +++ b/packages/lit-dev-content/.eleventy.js @@ -162,6 +162,27 @@ ${content} return content.replace(//g, ''); }); + /** + * For the latest versioned urls, this filter returns the unversioned url + * which is used in the rel=canonical link. + */ + eleventyConfig.addFilter( + 'removeLatestVersionFromUrl', + function (url, latestVersion) { + if (!latestVersion) { + throw new Error( + `No latestVersion provided to 'removeLatestVersionFromUrl` + ); + } + if (!url.includes(`/${latestVersion}/`)) { + throw new Error( + `'${url}' does not include the latestVersion versioned path segment` + ); + } + return url.replace(`/${latestVersion}/`, '/'); + } + ); + eleventyConfig.addFilter('removeExtension', function (url) { const extension = path.extname(url); return url.substring(0, url.length - extension.length); diff --git a/packages/lit-dev-content/site/_includes/docs.html b/packages/lit-dev-content/site/_includes/docs.html index d35c3a7f9..bd5b69279 100644 --- a/packages/lit-dev-content/site/_includes/docs.html +++ b/packages/lit-dev-content/site/_includes/docs.html @@ -20,6 +20,13 @@ {% endif %} + {% if selectedVersion == latestVersion and page.url.includes("/" + latestVersion + "/") %} + + + {% endif %} {% endblock %} {% block content %} From 487ac0ca286e424ceaf1ea18f8f0ffdb80b79820 Mon Sep 17 00:00:00 2001 From: Andrew Jakubowicz Date: Wed, 10 May 2023 12:22:45 -0700 Subject: [PATCH 05/33] Make all authored cross links versioned. Unversioned links will be generated automatically by the generated unversioned pages. --- .../lit-dev-api/api-data/lit-2/pages.json | 30 +++++++-------- .../site/docs/v2/components/decorators.md | 20 +++++----- .../site/docs/v2/components/events.md | 4 +- .../site/docs/v2/components/lifecycle.md | 16 ++++---- .../site/docs/v2/components/overview.md | 10 ++--- .../site/docs/v2/components/properties.md | 12 +++--- .../site/docs/v2/components/rendering.md | 18 ++++----- .../site/docs/v2/components/shadow-dom.md | 10 ++--- .../site/docs/v2/components/styles.md | 6 +-- .../v2/composition/component-composition.md | 4 +- .../site/docs/v2/composition/controllers.md | 10 ++--- .../site/docs/v2/composition/mixins.md | 10 ++--- .../site/docs/v2/composition/overview.md | 6 +-- .../site/docs/v2/data/context.md | 2 +- .../site/docs/v2/getting-started.md | 4 +- .../lit-dev-content/site/docs/v2/index.md | 16 ++++---- .../site/docs/v2/libraries/labs.md | 4 +- .../docs/v2/libraries/standalone-templates.md | 20 +++++----- .../docs/v2/localization/cli-and-config.md | 2 +- .../site/docs/v2/localization/overview.md | 12 +++--- .../site/docs/v2/localization/runtime-mode.md | 4 +- .../docs/v2/localization/transform-mode.md | 2 +- .../docs/v2/releases/release-notes/1.2.0.md | 8 ++-- .../site/docs/v2/ssr/authoring.md | 2 +- .../site/docs/v2/ssr/client-usage.md | 2 +- .../site/docs/v2/ssr/overview.md | 2 +- .../site/docs/v2/ssr/server-usage.md | 2 +- .../site/docs/v2/templates/conditionals.md | 6 +-- .../docs/v2/templates/custom-directives.md | 2 +- .../site/docs/v2/templates/directives.md | 4 +- .../site/docs/v2/templates/expressions.md | 38 +++++++++---------- .../site/docs/v2/templates/overview.md | 12 +++--- .../site/docs/v2/tools/adding-lit.md | 4 +- .../site/docs/v2/tools/development.md | 2 +- .../site/docs/v2/tools/overview.md | 14 +++---- .../site/docs/v2/tools/production.md | 8 ++-- .../site/docs/v2/tools/publishing.md | 4 +- .../site/docs/v2/tools/requirements.md | 2 +- .../site/docs/v2/tools/starter-kits.md | 8 ++-- .../site/docs/v2/tools/testing.md | 6 +-- .../src/api-docs/configs/lit-2.ts | 2 +- 41 files changed, 175 insertions(+), 175 deletions(-) diff --git a/packages/lit-dev-api/api-data/lit-2/pages.json b/packages/lit-dev-api/api-data/lit-2/pages.json index e4c34714b..81406b2b1 100644 --- a/packages/lit-dev-api/api-data/lit-2/pages.json +++ b/packages/lit-dev-api/api-data/lit-2/pages.json @@ -12,7 +12,7 @@ "name": "LitElement", "comment": { "shortText": "Base element class that manages element properties and attributes, and\nrenders a lit-html template.", - "text": "To define a component, subclass `LitElement` and implement a\n`render` method to provide the component's template. Define properties\nusing the [`properties`](/docs/api/LitElement/#LitElement.properties) property or the\n[`property`](/docs/api/decorators/#property) decorator.\n" + "text": "To define a component, subclass `LitElement` and implement a\n`render` method to provide the component's template. Define properties\nusing the [`properties`](/docs/v2/api/LitElement/#LitElement.properties) property or the\n[`property`](/docs/v2/api/decorators/#property) decorator.\n" }, "sources": [ { @@ -949,7 +949,7 @@ "tag": "@nocollapse" } ], - "shortText": "Creates a property accessor on the element prototype if one does not exist\nand stores a [`PropertyDeclaration`](/docs/api/ReactiveElement/#PropertyDeclaration) for the property with the\ngiven options. The property setter calls the property's `hasChanged`\nproperty option or uses a strict identity check to determine whether or not\nto request an update.", + "shortText": "Creates a property accessor on the element prototype if one does not exist\nand stores a [`PropertyDeclaration`](/docs/v2/api/ReactiveElement/#PropertyDeclaration) for the property with the\ngiven options. The property setter calls the property's `hasChanged`\nproperty option or uses a strict identity check to determine whether or not\nto request an update.", "text": "This method may be overridden to customize properties; however,\nwhen doing so, it's important to call `super.createProperty` to ensure\nthe property is setup correctly. This method calls\n`getPropertyDescriptor` internally to get a descriptor to install.\nTo customize what properties do when they are get or set, override\n`getPropertyDescriptor`. To customize the options for a property,\nimplement `createProperty` like this:\n```ts\nstatic createProperty(name, options) {\n options = Object.assign(options, {myOption: true});\n super.createProperty(name, options);\n}\n```\n" }, "sources": [ @@ -1222,7 +1222,7 @@ } ], "shortText": "Returns the property options associated with the given property.\nThese options are defined with a `PropertyDeclaration` via the `properties`\nobject or the `@property` decorator and are registered in\n`createProperty(...)`.", - "text": "Note, this method should be considered \"final\" and not overridden. To\ncustomize the options for a given property, override\n[`createProperty`](/docs/api/LitElement/#LitElement.createProperty).\n" + "text": "Note, this method should be considered \"final\" and not overridden. To\ncustomize the options for a given property, override\n[`createProperty`](/docs/v2/api/LitElement/#LitElement.createProperty).\n" }, "sources": [ { @@ -1760,7 +1760,7 @@ "tag": "@nocollapse" } ], - "shortText": "Array of styles to apply to the element. The styles should be defined\nusing the [`css`](/docs/api/styles/#css) tag function, via constructible stylesheets, or\nimported from native CSS module scripts.", + "shortText": "Array of styles to apply to the element. The styles should be defined\nusing the [`css`](/docs/v2/api/styles/#css) tag function, via constructible stylesheets, or\nimported from native CSS module scripts.", "text": "Note on Content Security Policy:\nElement styles are implemented with ` +
template content
+ `; +} +``` + +
+ +**Limitations in the ShadyCSS polyfill around per instance styling.** Per instance styling is not supported using the ShadyCSS polyfill. See the [ShadyCSS limitations](https://github.com/webcomponents/polyfills/tree/master/packages/shadycss#limitations) for details. + +
+ +#### Expressions and style elements + +Using expressions inside style elements has some important limitations and performance issues. + +```js +render() { + return html` + +
template content
+ `; +} +``` + +
+ +**Limitations in the ShadyCSS polyfill around expressions.** Expressions in ``; + return html`${this.red ? redStyle : ''}` + +``` + +### Import an external stylesheet (not recommended) {#external-stylesheet} + +While you can include an external style sheet in your template with a ``, we do not recommend this approach. Instead, styles should be placed in the [static `styles` class field](#add-styles). + +
+ +**External stylesheet caveats.** + +* The [ShadyCSS polyfill](https://github.com/webcomponents/polyfills/tree/master/packages/shadycss#limitations) doesn't support external style sheets. +* External styles can cause a flash-of-unstyled-content (FOUC) while they load. +* The URL in the `href` attribute is relative to the **main document**. This is okay if you're building an app and your asset URLs are well-known, but avoid using external style sheets when building a reusable element. + +
+ +## Dynamic classes and styles + +One way to make styles dynamic is to add expressions to the `class` or `style` attributes in your template. + +Lit offers two directives, `classMap` and `styleMap`, to conveniently apply classes and styles in HTML templates. + +For more information on these and other directives, see the documentation on [built-in directives](/docs/v2/templates/directives/). + +To use `styleMap` and/or `classMap`: + +1. Import `classMap` and/or `styleMap`: + + ```js + import { classMap } from 'lit/directives/class-map.js'; + import { styleMap } from 'lit/directives/style-map.js'; + ``` + +2. Use `classMap` and/or `styleMap` in your element template: + +{% playground-example "docs/components/style/maps" "my-element.ts" %} + +See [classMap](/docs/v2/templates/directives/#classmap) and [styleMap](/docs/v2/templates/directives/#stylemap) for more information. + +## Theming {#theming} + +By using [CSS inheritance](#inheritance) and [CSS variables and custom properties](#customprops) together, it's easy to create themable elements. By applying css selectors to customize CSS custom properties, tree-based and per-instance theming is straightforward to apply. Here's an example: + +{% playground-example "docs/components/style/theming" "my-element.ts" %} + +### CSS inheritance {#inheritance} + +CSS inheritance lets parent and host elements propagate certain CSS properties to their descendants. + +Not all CSS properties inherit. Inherited CSS properties include: + +* `color` +* `font-family` and other `font-*` properties +* All CSS custom properties (`--*`) + +See [CSS Inheritance on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance) for more information. + +You can use CSS inheritance to set styles on an ancestor element that are inherited by its descendants: + +```html + + + #shadow-root + Will be green + +``` + +### CSS custom properties {#customprops} + +All CSS custom properties (--custom-property-name) inherit. You can use this to make your component's styles configurable from outside. + +The following component sets its background color to a CSS variable. The CSS variable uses the value of `--my-background` if it's been set by a selector matching an ancestor in the DOM tree, and otherwise defaults to `yellow`: + +```js +class MyElement extends LitElement { + static styles = css` + :host { + background-color: var(--my-background, yellow); + } + `; + render() { + return html`

Hello world

`; + } +} +``` + +Users of this component can set the value of `--my-background`, using the `my-element` tag as a CSS selector: + +```html + + +``` + +`--my-background` is configurable per instance of `my-element`: + +```html + + + +``` + +See [CSS Custom Properties on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) for more information. diff --git a/packages/lit-dev-content/site/docs/v3/composition/component-composition.md b/packages/lit-dev-content/site/docs/v3/composition/component-composition.md new file mode 100644 index 000000000..68cb2921e --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/composition/component-composition.md @@ -0,0 +1,111 @@ +--- +title: Component composition +eleventyNavigation: + parent: Composition + key: Component composition + order: 2 +--- + +The most common way to handle complexity and factor Lit code into separate units is _component composition_: that is, the process of building a large, complex component out of smaller, simpler components. Imagine you've been tasked with implementing a screen of UI: + +![Screenshot of an application that displays a set of animal photos. The application has a top bar with a title ("Fuzzy") and a menu button. A left menu drawer is open, showing a set of options.](/images/docs/composition/fuzzy-screenshot.png) + + +You can probably identify the areas which will involve some complexity to implement. Chances are, those could be components. + +By isolating the complexity into specific components, you make the job much simpler, and you can then compose these components together to create the overall design. + +For example, the fairly simple screenshot above involves a number of possible components: a top bar, a menu button, a drawer with menu items for navigating the current section; and a main content area. Each of these could be represented by a component. A complex component, like a drawer with a navigation menu, might be broken into many smaller components: the drawer itself, a button to open and close the drawer, the menu, individual menu items. + +Lit lets you compose by adding elements to your template—whether those are built-in HTML elements or custom elements. + +```js +render() { + return html` + + + Fuzzy + + `; +} +``` + +## What makes a good component + +When deciding how to break up functionality, there are several things that help identify when to make a new component. A piece of UI may be a good candidate for a component if one or more of the following applies: + +* It has its own state. +* It has its own template. +* It's used in more than one place, either in this component or in multiple components. +* It focuses on doing one thing well. +* It has a well-defined API. + +Reusable controls like buttons, checkboxes, and input fields can make great components. But more complex UI pieces like drawers and carousels are also great candidates for componentization. + + +## Passing data up and down the tree + +When exchanging data with subcomponents, the general rule is to follow the model of the DOM: _properties down_, _events up_. + +* Properties down. Setting properties on a subcomponent is usually preferable to calling methods on the subcomponent. It's easy to set properties in Lit templates and other declarative template systems. + +* Events up. In the web platform, firing events is the default method for elements to send information up the tree, often in response to user interactions. This lets the host component respond to the event, or transform or re-fire the event for ancestors farther up the tree. + +A few implications of this model: + +* A component should be the source of truth for the subcomponents in its shadow DOM. Subcomponents shouldn't set properties or call methods on their host component. + +* If a component changes a public property on itself, it should fire an event to notify components higher in the tree. Generally these changes will be the result of user actions—like pressing a button or selecting a menu item. Think of the native `input` element, which fires an event when the user changes the value of the input. + +Consider a menu component that includes a set of menu items and exposes `items` and `selectedItem` properties as part of its public API. Its DOM structure might look like this: + + +![A hierarchy of DOM nodes representing a menu. The top node, my-menu, has a ShadowRoot, which contains three my-item elements.](/images/docs/composition/composition-menu-component.png) + +When the user selects an item, the `my-menu` element should update its `selectedItem` property. It should also fire an event to notify any owning component that the selection has changed. The complete sequence would be something like this: + +- The user interacts with an item, causing an event to fire (either a standard event like `click`, or some event specific to the `my-item` component). +- The `my-menu` element gets the event, and updates its `selectedItem` property. It may also change some state so that the selected item is highlighted. +- The `my-menu` element fires a semantic event indicating that the selection has changed. This event might be called `selected-item-changed`, for example. Since this event is part of the API for `my-menu`, it should be semantically meaningful in that context. + +For more information on dispatching and listening for events, see [Events](/docs/v2/components/events/). + + +## Passing data across the tree + +Properties down and events up is a good rule to start with. But what if you need to exchange data between two components that don't have a direct descendant relationship? For example, two components that are siblings in the shadow tree? + +One solution to this problem is to use the _mediator pattern_. In the mediator pattern, peer components don't communicate with each other directly. Instead, interactions are _mediated_ by a third party. + +A simple way to implement the mediator pattern is by having the owning component handle events from its children, and in turn update the state of its children as necessary by passing changed data back down the tree. By adding a mediator, you can pass data across the tree using the familiar events-up, properties-down principle. + +In the following example, the mediator element listens for events from the input and button elements in its shadow DOM. It controls the enabled state of the button so the user can only click **Submit** when there's text in the input. + +{% playground-example "docs/composition/mediator-pattern" "mediator-element.ts" %} + +Other mediator patterns include flux/Redux-style patterns where a store mediates changes and updates components via subscriptions. Having components directly subscribe to changes can help avoid needing every parent to pass along all data required by its children. + +## Light DOM children + +In addition to the nodes in your shadow DOM, you can render child nodes provided by the component user, like the standard ` +

The width is ${this._textSize.contentRect?.width}

+ `; + } +} +``` + +```js +class MyElement extends LitElement { + _textSize = new ResizeController(this); + + render() { + return html` + +

The width is ${this._textSize.contentRect?.width}

+ `; + } +} +``` + +{% endswitchable-sample %} + +To implement this, you create a directive and call it from a method: + +```ts +class ResizeDirective { + /* ... */ +} +const resizeDirective = directive(ResizeDirective); + +export class ResizeController { + /* ... */ + observe() { + // Pass a reference to the controller so the directive can + // notify the controller on size changes. + return resizeDirective(this); + } +} +``` + +{% todo %} + +- Review and cleanup this example + +{% endtodo %} + +## Use cases + +Reactive controllers are very general and have a very broad set of possible use cases. They are particularly good for connecting a component to an external resource, like user input, state management, or remote APIs. Here are a few common use cases. + +### External inputs + +Reactive controllers can be used to connect to external inputs. For example, keyboard and mouse events, resize observers, or mutation observers. The controller can provide the current value of the input to use in rendering, and request a host update when the value changes. + +#### Example: MouseMoveController + +This example shows how a controller can perform setup and cleanup work when its host is connected and disconnected, and request an update when an input changes: + +{% playground-ide "docs/controllers/mouse" "my-element.ts" %} + +### Asynchronous tasks + +Asynchronous tasks, such as long running computations or network I/O, typically have state that changes over time, and will need to notify the host when the task state changes (completes, errors, etc.). + +Controllers are a great way to bundle task execution and state to make it easy to use inside a component. A task written as a controller usually has inputs that a host can set, and outputs that a host can render. + +`@lit-labs/task` contains a generic `Task` controller that can pull inputs from the host, execute a task function, and render different templates depending on the task state. + +You can use `Task` to create a custom controller with an API tailored for your specific task. Here we wrap `Task` in a `NamesController` that can fetch one of a specified list of names from a demo REST API. `NameController` exposes a `kind` property as an input, and a `render()` method that can render one of four templates depending on the task state. The task logic, and how it updates the host, are abstracted from the host component. + +{% playground-ide "docs/controllers/names" %} + +{% todo %} + +- Animations + +{% endtodo %} + +## See also + +* [Reactive update cycle](/docs/v2/components/lifecycle/#reactive-update-cycle) +* [@lit-labs/task](https://www.npmjs.com/package/@lit-labs/task) diff --git a/packages/lit-dev-content/site/docs/v3/composition/index.md b/packages/lit-dev-content/site/docs/v3/composition/index.md new file mode 100644 index 000000000..5512bfb29 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/composition/index.md @@ -0,0 +1,10 @@ +--- +title: Composition +eleventyNavigation: + title: Composition + key: Composition + order: 4 +--- + + diff --git a/packages/lit-dev-content/site/docs/v3/composition/mixins.md b/packages/lit-dev-content/site/docs/v3/composition/mixins.md new file mode 100644 index 000000000..99f7bd651 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/composition/mixins.md @@ -0,0 +1,262 @@ +--- +title: Mixins +eleventyNavigation: + parent: Composition + key: Mixins + order: 3 +--- + +Class mixins are a pattern for sharing code between classes using standard JavaScript. As opposed to "has-a" composition patterns like [reactive +controllers](/docs/v2/composition/controllers/), where a class can _own_ a controller to add +behavior, mixins implement "is-a" composition, where the mixin causes the class +itself to _be_ an instance of the behavior being shared. + +You can use mixins to customize a Lit component by adding API or overriding its lifecycle callbacks. + +## Mixin basics + +Mixins can be thought of as "subclass factories" that override the class they +are applied to and return a subclass, extended with the behavior in the mixin. +Because mixins are implemented using standard JavaScript class expressions, they +can use all of the idioms available to subclassing, such as adding new +fields/methods, overriding existing superclass methods, and using `super`. + +
+ +For ease of reading, the samples on this page elide some of the TypeScript types +for mixin functions. See [Mixins in TypeScript](#mixins-in-typescript) for details on proper +typing of mixins in TypeScript. + +
+ +To define a mixin, write a function that takes a +`superClass`, and returns a new class that extends it, adding fields and methods +as needed: + +```ts +const MyMixin = (superClass) => class extends superClass { + /* class fields & methods to extend superClass with */ +}; +``` + +To apply a mixin, simply pass a class to generate a subclass with the mixin +applied. Most commonly, users will apply the mixin directly to a base class when defining +a new class: + +```ts +class MyElement extends MyMixin(LitElement) { + /* user code */ +} +``` + +Mixins can also be used to create concrete subclasses that users can then extend +like a normal class, where the mixin is an implementation detail: + +```ts +export const LitElementWithMixin = MyMixin(LitElement); +``` + +```ts +import {LitElementWithMixin} from './lit-element-with-mixin.js'; + +class MyElement extends LitElementWithMixin { + /* user code */ +} +``` + +Because class mixins are a standard JavaScript pattern and not Lit-specific, +there is a good deal of information in the community on leveraging mixins for +code reuse. For more reading on mixins, here are a few good references: + +* [Class mixins](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#mix-ins) on MDN +* [Real Mixins with JavaScript + Classes](https://justinfagnani.com/2015/12/21/real-mixins-with-JavaScript-classes/) + by Justin Fagnani +* [Mixins](https://www.TypeScriptlang.org/docs/handbook/mixins.html) in the TypeScript handbook. +* [Dedupe mixin library](https://open-wc.org/docs/development/dedupe-mixin/) by + open-wc, including a discussion of when mixin usage may lead to duplication, + and how to use a deduping library to avoid it. +* [Mixin conventions](https://component.kitchen/elix/mixins) followed by Elix + web component library. While not Lit-specific, contains thoughtful suggestions + around applying conventions when defining mixins for web components. + +## Creating mixins for LitElement + +Mixins applied to LitElement can implement or override any of the standard +[custom element lifecycle](/docs/v2/components/lifecycle/#custom-element-lifecycle) +callbacks like the `constructor()` or `connectedCallback()`, as well as any of +the [reactive update lifecycle](/docs/v2/components/lifecycle/#reactive-update-cycle) +callbacks like `render()` or `updated()`. + +For example, the following mixin would log when the element is created, +connected, and updated: + +```ts +const LoggingMixin = (superClass) => class extends superClass { + constructor() { + super(); + console.log(`${this.localName} was created`); + } + connectedCallback() { + super.connectedCallback(); + console.log(`${this.localName} was connected`); + } + updated(changedProperties) { + super.updated?.(changedProperties); + console.log(`${this.localName} was updated`); + } +} +``` + +Note that a mixin should always make a super call to the standard custom element lifecycle +methods implemented by `LitElement`. When overriding a reactive update lifecycle +callback, it is good practice to call the super method if it already exists on +the superclass (as shown above with the optional-chaining call to +`super.updated?.()`). + +Also note that mixins can choose to do work either before or after the base +implementation of the standard lifecycle callbacks via its choice of when to +make the super call. + +Mixins can also add [reactive properties](/docs/v2/components/properties/), +[styles](/docs/v2/components/styles/), and API to the subclassed element. + +The mixin in the example below adds a `highlight` reactive property to the +element and a `renderHighlight()` method that the user can call to wrap some +content. The wrapped content is styled yellow when the `highlight` property/attribute is set. + +{% playground-ide "docs/mixins/highlightable/" "highlightable.ts" %} + +Note in the example above, the user of the mixin is expected to call the +`renderHighlight()` method from their `render()` method, as well as take care to add +the `static styles` defined by the mixin to the subclass styles. The nature of +this contract between mixin and user is up to the mixin definition and should be +documented by the mixin author. + +## Mixins in TypeScript + +When writing `LitElement` mixins in TypeScript, there are a few details to be +aware of. + +### Typing the superclass + +You should constrain the `superClass` argument to the type of class you expect +users to extend, if any. This can be accomplished using a generic `Constructor` +helper type as shown below: + +```ts +import {LitElement} from 'lit'; + +type Constructor = new (...args: any[]) => T; + +export const MyMixin = >(superClass: T) => { + class MyMixinClass extends superClass { + /* ... */ + }; + return MyMixinClass as /* see "typing the subclass" below */; +} +``` + +The above example ensures that the class being passed to the mixin extends from +`LitElement`, so that your mixin can rely on callbacks and other API provided by +Lit. + +### Typing the subclass + +Although TypeScript has basic support for inferring the return type for the +subclass generated using the mixin pattern, it has a severe limitation in that +the inferred class must not contain members with `private` or `protected` +access modifiers. + +
+ +Because `LitElement` itself does have private and protected members, by default +TypeScript will error with _"Property '...' of exported class expression may not +be private or protected."_ when returning a class that extends `LitElement`. + +
+ +There are two workarounds that both involve casting the return type +from the mixin function to avoid the error above. + +#### When a mixin does not add new public/protected API + +If your mixin only overrides `LitElement` methods or properties and does not +add any new API of its own, you can simply cast the generated class to the super +class type `T` that was passed in: + +```ts +export const MyMixin = >(superClass: T) => { + class MyMixinClass extends superClass { + connectedCallback() { + super.connectedCallback(); + this.doSomethingPrivate(); + } + private doSomethingPrivate() { + /* does not need to be part of the interface */ + } + }; + // Cast return type to the superClass type passed in + return MyMixinClass as T; +} +``` + +#### When a mixin adds new public/protected API + +If your mixin does add new protected or public API that you need users to be +able to use on their class, you need to define the interface for the mixin +separately from the implementation, and cast the return type as the intersection +of your mixin interface and the super class type: + +```ts +// Define the interface for the mixin +export declare class MyMixinInterface { + highlight: boolean; + protected renderHighlight(): unknown; +} + +export const MyMixin = >(superClass: T) => { + class MyMixinClass extends superClass { + @property() highlight = false; + protected renderHighlight() { + /* ... */ + } + }; + // Cast return type to your mixin's interface intersected with the superClass type + return MyMixinClass as Constructor & T; +} +``` + +### Applying decorators in mixins + +Due to limitations of TypeScript's type system, decorators (such as +`@property()`) must be applied to a class declaration statement and not a class +expression. + +In practice this means mixins in TypeScript need to declare a class +and then return it, rather than return a class expression directly from the +arrow function. + +Supported: +```ts +export const MyMixin = (superClass: T) => { + // ✅ Defining a class in a function body, and then returning it + class MyMixinClass extends superClass { + @property() + mode = 'on'; + /* ... */ + }; + return MyMixinClass; +} +``` + +Not supported: +```ts +export const MyMixin = (superClass: T) => + // ❌ Returning class expression directly using arrow-function shorthand + class extends superClass { + @property() + mode = 'on'; + /* ... */ + } +``` diff --git a/packages/lit-dev-content/site/docs/v3/composition/overview.md b/packages/lit-dev-content/site/docs/v3/composition/overview.md new file mode 100644 index 000000000..8e80c1a4a --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/composition/overview.md @@ -0,0 +1,38 @@ +--- +title: Composition overview +eleventyNavigation: + parent: Composition + key: Overview + order: 1 +--- + +Composition is a strategy for managing complexity and organizing code into reusable pieces. Lit provides a few options for composition and code reuse: + +* Component composition. +* Reactive controllers. +* Class mixins. + +[_Component composition_](/docs/v2/composition/component-composition/) is the process of assembling complex components from simpler components. A component can use subcomponents in its template. Components can use standard DOM mechanisms to communicate: setting properties on subcomponents, and listening for events from subcomponents. + +Although component composition is the default way to think about breaking a complex Lit project down into smaller units, there are two other notable code patterns useful for factoring your Lit code: + +[_Reactive controllers_](/docs/v2/composition/controllers/) are objects that can hook into the update lifecycle of a Lit component, encapsulating state and behavior related to a feature into a separate unit of code. + +[_Class mixins_](/docs/v2/composition/mixins/) let you write reusable partial component definitions and "mix them in" to a component's inheritance chain. + +Both mixins and reactive controllers let you factor component logic related to a given feature into a reusable unit. See the next section for a comparison of controllers and mixins. + +## Controllers and mixins + +Controllers and class mixins are very similar in some ways. They both can hook into a host component's lifecycle, maintain state, and trigger host updates. + +The primary difference between controllers and mixins is their relationship with the component. A component has a "has-a" relationship with a reactive controller, since it owns the controller. A component has an "is-a" relationship with a mixin, since the component is an instance of the mixin class. + +A reactive controller is a separate object owned by a component. The controller can access methods and fields on the component, and the component can access methods and fields on the controller. But the controller can't (easily) be accessed by someone using the component, unless the component exposes a public API to it. The controller's lifecycle methods are called _before_ the corresponding lifecycle method on the component. + +A mixin, on the other hand, becomes part of the component's prototype chain. Any public fields or methods defined by the mixin are part of the component's API. And because a mixin is part of the prototype chain, your component has some control of when the mixin's lifecycle callbacks are called. + +In general, if you're trying to decide whether to package a feature as a controller or a mixin, you should choose a controller _unless_ the feature requires one of the following: + +* Adding public API to the component. +* Very granular access to the component lifecycle. diff --git a/packages/lit-dev-content/site/docs/v3/data/context.md b/packages/lit-dev-content/site/docs/v3/data/context.md new file mode 100644 index 000000000..fdfb12e74 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/data/context.md @@ -0,0 +1,525 @@ +--- +title: Context +eleventyNavigation: + key: Context + parent: Managing Data + order: 1 + labs: true +--- + +{% labs-disclaimer %} + +Context is a way of making data available to entire component subtrees without having to manually bind properties to every component. The data is "contextually" available, such that ancestor elements in between a provider of data and consumer of data aren't even aware of it. + +Lit's context implementation is part of [Lit Labs](/docs/v2/libraries/labs/) and available in the `@lit-labs/context` package: + +```bash +npm i @lit-labs/context +``` + +Context is useful for data that needs to be consumed by a wide variety and large number of components - things like an app's data store, the current user, a UI theme - or when data-binding isn't an option, such as when an element needs to provide data to its light DOM children. + +Context is very similar to React's Context, or to dependency injection systems like Angular's, with some important differences that make Context work with the dynamic nature of the DOM, and enable interoperability across different web components libraries, frameworks and plain JavaScript. + +## Example + +Using context involves a _context object_ (sometimes called a key), a _provider_ and a _consumer_, which communicate using the context object. + +Context definition (`logger-context.ts`): +```ts +import {createContext} from '@lit-labs/context'; +import type {Logger} from 'my-logging-library'; +export type {Logger} from 'my-logging-library'; +export const loggerContext = createContext('logger'); +``` + +Provider: +```ts +import {LitElement, property, html} from 'lit'; +import {provide} from '@lit-labs/context'; + +import {Logger} from 'my-logging-library'; +import {loggerContext} from './logger-context.js'; + +@customElement('my-app') +class MyApp extends LitElement { + + @provide({context: loggerContext}) + logger = new Logger(); + + render() { + return html`...`; + } +} +``` + +Consumer: +```ts +import {LitElement, property} from 'lit'; +import {consume} from '@lit-labs/context'; + +import {type Logger, loggerContext} from './logger-context.js'; + +export class MyElement extends LitElement { + + @consume({context: loggerContext}) + @property({attribute: false}) + public logger?: Logger; + + private doThing() { + this.logger?.log('A thing was done'); + } +} +``` + +## Key Concepts + +### Context Protocol +Lit's context is based on the [Context Community Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md) by the W3C's [Web Components Community Group](https://www.w3.org/community/webcomponents/). + +This protocol enables interoperability between elements (or even non-element code) regardless of how they were built. Via the context protocol, a Lit-based element can provide data to a consumer not built with Lit, or vice versa. + +The Context Protocol is based on DOM events. A consumer fires a `context-request` event that carries the context key that it wants, and any element above it can listen for the `context-request` event and provide data for that context key. + +`@lit-labs/context` implements this event-based protocol and makes it available via a few reactive controllers and decorators. + +### Context Objects + +Contexts are identified by _context objects_ or _context keys_. They are objects that represent some potential data to be shared by the context object identity. You can think of them as similar to Map keys. + +### Providers + +Providers are usually elements (but can be any event handler code) that provide data for specific context keys. + +### Consumers + +Consumers request data for specific context keys. + +### Subscriptions + +When a consumer requests data for a context, it can tell the provider that it wants to _subscribe_ to changes in the context. If the provider has new data, the consumer will be notified and can automatically update. + +## Usage + +### Defining a context + +Every usage of context must have a context object to coordinate the data request. This context object represents the identity and type of data that is provided. + +Context objects are created with the `createContext()` function: + +```ts +export const myContext = createContext(Symbol('my-context')); +``` + +It is recommended to put context objects in their own module so that they're importable independent of specific providers and consumers. + +#### Context type-checking + +`createContext()` takes any value and returns it directly. In TypeScript, the value is cast to a typed `Context` object, which carries the type of the context _value_ with it. + +In case of a mistake like this: +```ts +const myContext = createContext(Symbol('logger')); + +class MyElement extends LitElement { + @provide({context: myContext}) + name: string +} +``` + +TypeScript will warn that the type `string` is not assignable to the type `Logger`. + +#### Context equality + +Context objects are used by providers to match a context request event to a value. Contexts are compared with strict equality (`===`), so a provider will only handle a context request if its context key equals the context key of the request. + +This means that there are two main ways to create a context object: +1. With a value that is globally unique, like an object (`{}`) or symbol (`Symbol()`) +2. With a value that is not globally unique, so that it can be equal under strict equality, like a string (`'logger'`) or _global_ symbol (`Symbol.for('logger')`). + +If you want two _separate_ `createContext()` calls to refer to the same +context, then use a key that will be equal under strict equality like a +string: +```ts +// true +createContext('my-context') === createContext('my-context') +``` + +Beware though that two modules in your app could use the same context key to refer to different objects. To avoid unintended collisions you may want to use a relatively unique string, e.g. like `'console-logger'` instead of `'logger'`. + +Usually it's best to use a globally unique context object. Symbols are one of the easiest ways to do this. + +### Providing a context + +There are two ways in `@lit-labs/context` to provide a context value: the ContextProvider controller and the `@provide()` decorator. + +#### `@provide()` + +The `@provide()` decorator is the easiest way to provide a value if you're using decorators. It creates a ContextProvider controller for you. + +Decorate a property with `@provide()` and give it the context key: +```ts +import {LitElement, html} from 'lit'; +import {property} from 'lit/decorators.js'; +import {provide} from '@lit-labs/context'; +import {myContext, MyData} from './my-context.js'; + +class MyApp extends LitElement { + @provide({context: myContext}) + myData: MyData; +} +``` + +You can make the property also a reactive property with `@property()` or `@state()` so that setting it will update the provider element as well as context consumers. + +```ts + @provide({context: myContext}) + @property({attribute: false}) + myData: MyData; +``` + +Context properties are often intended to be private. You can make private properties reactive with `@state()`: + +```ts + @provide({context: myContext}) + @state() + private _myData: MyData; +``` + +Making a context property public lets an element provide a public field to its child tree: + +```ts + html`` +``` + +#### ContextProvider + +`ContextProvider` is a reactive controller that manages `context-request` event handlers for you. + +```ts +import {LitElement, html} from 'lit'; +import {ContextProvider} from '@lit-labs/context'; +import {myContext, MyData} from './my-context.js'; + +export class MyApp extends LitElement { + private _provider = new ContextProvider(this, myContext); +} +``` + +ContextProvider can take an initial value in its constructor: + +```ts + private _provider = new ContextProvider(this, myContext, initialData); +``` + +Or you can call `setValue()`: +```ts + this._provider.setValue(myData); +``` + +### Consuming a context + +#### `@consume()` decorator + +The `@consume()` decorator is the easiest way to consume a value if you're using decorators. It creates a ContextConsumer controller for you. + +Decorate a property with `@consume()` and give it the context key: +```ts +import {LitElement, html} from 'lit'; +import {consume} from '@lit-labs/context'; +import {myContext, MyData} from './my-context.js'; + +class MyElement extends LitElement { + @consume({context: myContext}) + myData: MyData; +} +``` + +When this element is connected to the document, it will automatically fire a `context-request` event, get a provided value, assign it to the property, and trigger an update of the element. + +#### ContextConsumer + +ContextConsumer is a reactive controller that manages dispatching the `context-request` event for you. The controller will cause the host element to update when new values are provided. The provided value is then available at the `.value` property of the controller. + +```ts +import {LitElement, property} from 'lit'; +import {ContextConsumer} from '@lit-labs/context'; +import {Logger, loggerContext} from './logger.js'; + +export class MyElement extends LitElement { + private _myData = new ContextConsumer(this, myContext); + + render() { + const myData = this._myData.value; + return html`...`; + } +} +``` + +#### Subscribing to contexts + +Consumers can subscribe to context values so that if a provider has a new value, it can give it to all subscribed consumers, causing them to update. + +You can subscribe with the `@consume()` decorator: + +```ts + @consume({context: myContext, subscribe: true}) + myData: MyData; +``` + +and the ContextConsumer controller: + +```ts + private _myData = new ContextConsumer(this, + myContext, + undefined, /* callback */ + true /* subscribe */ + ); +``` + +## Example Use Cases + +### Current user, locale, etc. + +The most common context use cases involve data that is global to a page and possibly only sparsely needed in components throughout the page. Without context it's possible that most or all components would need to accept and propagate reactive properties for the data. + +### Services + +App-global services, like loggers, analytics, data stores, can be provided by context. An advantage of context over importing from a common module are the late coupling and tree-scoping that context provides. Tests can easily provide mock services, or different parts of the page can be given different service instances. + +### Themes + +Themes are sets of styles that apply to the entire page or entire subtrees within the page - exactly the kind of scope of data that context provides. + +One way of building a theme system would be to define a `Theme` type that containers can provide that holds named styles. Elements that want to apply a theme can consume the theme object and look up styles by name. Custom theme reactive controllers can wrap ContextProvider and ContextConsumer to reduce boilerplate. + +### HTML-based plugins + +Context can be used to pass data from a parent to its light DOM children. Since the parent does usually not create the light DOM children, it cannot leverage template-based data-binding to pass data to them, but it can listen to and respond to `context-request` events. + +For example, consider a code editor element with plugins for different language modes. You can make a plain HTML system for adding features using context: + +```html + + + + +``` + +In this case `` would provide an API for adding language modes via context, and plugin elements would consume that API and add themselves to the editor. + +### Data formatters, link generators, etc. + +Sometimes reusable components will need to format data or URLs in an application-specific way. For example, a documentation viewer that renders a link to another item. The component will not know the URL space of the application. + +In these cases the component can depend on a context-provided function that will apply the application-specific formatting to the data or link. + +## API + +
+ +These API docs are a summary until generated API docs are available + +
+ +### `createContext()` + +Creates a typed Context object + +**Import**: + +```ts +import {property} from '@lit-labs/context'; +``` + +**Signature**: + +```ts +function createContext(key: K): Context; +``` + + +Contexts are compared with with strict equality. + +If you want two separate `createContext()` calls to referrer to the same context, then use a key that will by equal under strict equality like a string for `Symbol.for()`: + +```ts +// true +createContext('my-context') === createContext('my-context') +// true +createContext(Symbol.for('my-context')) === createContext(Symbol.for('my-context')) +``` + +If you want a context to be unique so that it's guaranteed to not collide with other contexts, use a key that's unique under strict equality, like a `Symbol()` or object.: + +```ts +// false +createContext(Symbol('my-context')) === createContext(Symbol('my-context')) +// false +createContext({}) === createContext({}) +``` + +The `ValueType` type parameter is the type of value that can be provided by this context. It's uses to provide accurate types in the other context APIs. + +### `@provide()` + +A property decorator that adds a ContextConsumer controller to the component which will try and retrieve a value for the property via the Context API. + +**Import**: + +```ts +import {provide} from '@lit-labs/context'; +``` + +**Signature**: + +```ts +@provide({context: Context}) +``` + +### `@consume()` + +A property decorator that adds a ContextConsumer controller to the component which will retrieve a value for the property via the Context protocol. + +**Import**: + +```ts +import {consume} from '@lit-labs/context'; +``` + +**Signature**: + +```ts +@consume({context: Context, subscribe?: boolean}) +``` + +`subscribe` is `false` by default. Set it to `true` to subscribe to updates to the context provided value. + +### `ContextProvider` + +A ReactiveController which adds context provider behavior to a custom element by listening to `context-request` events. + +**Import**: + +```ts +import {ContextProvider} from '@lit-labs/context'; +``` + +**Constructor**: + +```ts +ContextProvider( + host: ReactiveElement, + context: T, + initialValue?: ContextType +) +``` + +**Members** + +- `setValue(v: T, force = false): void` + + Sets the value provided, and notifies any subscribed consumers of the new value if the value changed. `force` causes a notification even if the value didn't change, which can be useful if an object had a deep property change. + + +### `ContextConsumer` + +A ReactiveController which adds context consuming behavior to a custom element by dispatching `context-request` events. + +**Import**: + +```ts +import {ContextConsumer} from '@lit-labs/context'; +``` + +**Constructor**: +```ts +ContextConsumer( + host: HostElement, + context: C, + callback?: (value: ContextType, dispose?: () => void) => void, + subscribe: boolean = false +) +``` + +**Members** + +- `value: ContextType` + + The current value for the context. + +When the host element is connected to the document it will emit a `context-request` event with its context key. When the context request is satisfied the controller will invoke the callback, if present, and trigger a host update so it can respond to the new value. + +It will also call the dispose method given by the provider when the host element is disconnected. + +### `ContextRoot` + +A ContextRoot can be used to gather unsatisfied context requests and re-dispatch them when new providers which satisfy matching context keys are available. This allows providers to be added to a DOM tree, or upgraded, after the consumers. + +**Import**: + +```ts +import {ContextRoot} from '@lit-labs/context'; +``` + +**Constructor**: +```ts +ContextRoot() +``` + +**Members** + +- `attach(element: HTMLElement): void` + + Attaches the ContextRoot to this element and starts listening to `context-request` events. + +- `detach(element: HTMLElement): void` + + Detaches the ContextRoot from this element, stops listening to `context-request` events. + +### `ContextRequestEvent` + +The event fired by consumers to request a context value. The API and behavior of this event is specified by the [Context Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md). + +**Import**: + +```ts +import {ContextRequestEvent} from '@lit-labs/context'; +``` + +The `context-request` bubbles and is composed. + +**Members** + +- `readonly context: C` + + The context object this event is requesting a value for + +- `readonly callback: ContextCallback>` + + The function to call to provide a context value + +- `readonly subscribe?: boolean` + + Whether the consumers wants to subscribe to new context values + +### `ContextCallback` + +A callback which is provided by a context requester and is called with the value satisfying the request. + +This callback can be called multiple times by context providers as the requested value is changed. + +**Import**: + +```ts +import {type ContextCallback} from '@lit-labs/context'; +``` + +**Signature**: + +```ts +type ContextCallback = ( + value: ValueType, + unsubscribe?: () => void +) => void; +``` diff --git a/packages/lit-dev-content/site/docs/v3/data/index.md b/packages/lit-dev-content/site/docs/v3/data/index.md new file mode 100644 index 000000000..0d7b0628d --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/data/index.md @@ -0,0 +1,9 @@ +--- +title: Managing Data +eleventyNavigation: + key: Managing Data + order: 5 +--- + + diff --git a/packages/lit-dev-content/site/docs/v3/frameworks/index.md b/packages/lit-dev-content/site/docs/v3/frameworks/index.md new file mode 100644 index 000000000..2d806e432 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/frameworks/index.md @@ -0,0 +1,9 @@ +--- +title: Frameworks +eleventyNavigation: + key: Frameworks + order: 8 +--- + + diff --git a/packages/lit-dev-content/site/docs/v3/frameworks/react.md b/packages/lit-dev-content/site/docs/v3/frameworks/react.md new file mode 100644 index 000000000..5c987d43a --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/frameworks/react.md @@ -0,0 +1,210 @@ +--- +title: React +eleventyNavigation: + key: React + parent: Frameworks + order: 1 + labs: true +--- + +{% labs-disclaimer %} + +The [@lit-labs/react](https://github.com/lit/lit/tree/main/packages/labs/react) package provides utilities to create React wrapper components for web components, and custom hooks from [reactive controllers](../../composition/controllers/). + +The React component wrapper enables setting properties on custom elements (instead of just attributes), mapping DOM events to React-style callbacks, and enables correct type-checking in JSX by TypeScript. + +The wrappers are targeted at two different audiences: +- Users of web components can wrap components and controllers for their own use in their own React projects. +- Vendors of components can publish React wrappers so that their React users have idiomatic versions of their components. + +### Why are wrappers needed? + +React can already render web components, since custom elements are just HTML elements and React knows how to render HTML. But React makes some assumptions about HTML elements that don't always hold for custom elements, and it treats lower-case tag names differently from upper-case component names in ways that can make custom elements harder than necessary to use. + +For instance, React assumes that all JSX properties map to HTML element attributes, and provides no way to set properties. This makes it difficult to pass complex data (like objects, arrays, or functions) to web components. React also assumes that all DOM events have corresponding "event properties" (`onclick`, `onmousemove`, etc), and uses those instead of calling `addEventListener()`. This means that to properly use more complex web components you often have to use `ref()` and imperative code. (For more information on the limitations of React's web component integration, see [Custom Elements Everywhere](https://custom-elements-everywhere.com/libraries/react/results/results.html).) + +React is working on fixes to these issues, but in the meantime, our wrappers take care of setting properties and listening to events for you. + +The `@lit-labs/react` package provides two main exports: + +- `createComponent()` creates a React component that _wraps_ an existing web component. The wrapper allows you to set props on the component and add event listeners to the component like you would any other React component. + +- `useController()` lets you use a Lit reactive controller as a React hook. + +## createComponent + +The `createComponent()` function makes a React component wrapper for a custom element class. The wrapper correctly passes React `props` to properties accepted by the custom element and listens for events dispatched by the custom element. + +### Usage + +Import `React`, a custom element class, and `createComponent`. + +```js +import React from 'react'; +import {createComponent} from '@lit-labs/react'; +import {MyElement} from './my-element.js'; + +export const MyElementComponent = createComponent({ + tagName: 'my-element', + elementClass: MyElement, + react: React, + events: { + onactivate: 'activate', + onchange: 'change', + }, +}); +``` + +After defining the React component, you can use it just as you would any other React component. + +```jsx + setIsActive(e.active)} + onchange={handleChange} +/> +``` + +{% aside "positive" "no-header" %} + +See it in action in the [React playground examples](/playground/#sample=examples/react-basics). + +{% endaside %} + +#### Options + +`createComponent` takes an options object with the following properties: + +- `tagName`: The custom element's tag name. +- `elementClass`: The custom element class. +- `react`: The imported `React` object. This is used to create the wrapper component with the user supplied `React`. This can also be an import of `preact-compat`. +- `events`: An object that maps an event handler prop to an event name fired by the custom element. + +#### Using slots + +Children of component created with `createComponent()` will render to the default slot of the custom element. + +```jsx + +

This will render in the default slot.

+
+``` + +To render the child to a specific named slot, the standard `slot` attribute can be added. + +```jsx + +

This will render in the slot named "foo".

+
+``` + +Since React components are not themselves HTML elements, they usually cannot directly have a `slot` attribute. To render into a named slot, the component will need to be wrapped with a container element that has a `slot` attribute. If a wrapper element interferes with styling, like for grid and flexbox layouts, giving it a `display: contents;` style ([See MDN for details](https://developer.mozilla.org/en-US/docs/Web/CSS/display#box)) will remove the container from rendering, and only render its children. + +```jsx + +
+ +
+
+``` + +{% aside "positive" "no-header" %} + +Try it out in the [React slots playground example](/playground/#sample=examples/react-slots). + +{% endaside %} + +#### Events + +The `events` option takes an object that maps React prop names to event names. When a component user passes a callback prop with one of the event prop names, the wrapper will add it as an event handler for the corresponding event. + +While the the React prop name can be whatever you want, the recommended convention is to add `on` in front of the event name. This matches how React is planning to implement event support for custom elements. You should also make sure this prop name does not collide with any existing properties on the element. + +In TypeScript, the event type can be specified by casting the event name to the `EventName` utility type. This is a good practice to do so that React users will get the most accurate types for their event callbacks. + +The `EventName` type is a string that takes an event interface as a type parameter. Here we cast the `'my-event'` name to an `EventName` to provide the right event type: + +```ts + +import React from 'react'; +import {createComponent} from '@lit-labs/react'; +import {MyElement, type EventName} from './my-element.js'; + +export const MyElementComponent = createComponent({ + tagName: 'my-element', + elementClass: MyElement, + react: React, + events: { + 'onmy-event': 'my-event' as EventName, + }, +}); +``` + +Casting the event name to `EventName` causes the React component to have an `onMyEvent` callback prop that accepts a `MyEvent` parameter instead of a plain `Event`: + +```tsx + { + console.log(e.myEventData); + }} +/> +``` + +### How it works + +During a render, the wrapper receives props from React and based on the options and the custom element class, changes the behavior of some of the props: + +* If a prop name is a property on the custom element, as determined with an `in` check, the wrapper sets that property on the element to the prop value +* If a prop name is an event name passed to the `events` option, the prop value is passed to `addEventListener()` with the name of the event. +* Otherwise the prop is passed to React's `createElement()` to be rendered as an attribute. + +Both properties and events are added in `componentDidMount()` and `componentDidUpdate()` callbacks, because the element must have already been instantiated by React in order to access it. + +For events, `createComponent()` accepts a mapping of React event prop names to events fired by the custom element. For example passing `{onfoo: 'foo'}` means a function passed via a prop named `onfoo` will be called when the custom element fires the `foo` event with the event as an argument. + +## useController + +Reactive controllers allow developers to hook in to a component's lifecycle to bundle +together state and behavior related to a feature. They are similar to React +hooks in the user cases and capabilities, but are plain JavaScript objects +instead of functions with hidden state. + +`useController()` lets you make React hooks out of reactive controllers allowing for the sharing of state and behaviors across web components and React. + +### Usage + +```jsx +import React from 'react'; +import {useController} from '@lit-labs/react/use-controller.js'; +import {MouseController} from '@example/mouse-controller'; + +// Write a custom React hook function: +const useMouse = () => { + // Use useController to create and store a controller instance: + const controller = useController(React, (host) => new MouseController(host)); + // Return relevant data for consumption by the component: + return controller.pos; +}; + +// Now use the new hook in a React component: +const Component = (props) => { + const mousePosition = useMouse(); + return ( +
+      x: {mousePosition.x}
+      y: {mousePosition.y}
+    
+ ); +}; +``` + +See the [mouse controller example](../../composition/controllers/#example:-mousemovecontroller) in the reactive controller docs for its implementation. + +### How it works + +`useController()` creates a custom host object for the controller passed to it and drives the controller's lifecycle by using React hooks. + +- `useState()` is used to store an instance of a controller and a `ReactControllerHost` +- The hook body and `useLayoutEffect()` callbacks emulate the `ReactiveElement` lifecycle as closely as possible. +- `ReactControllerHost` implements `addController()` so that controller composition works and nested controller lifecycles are called correctly. +- `ReactControllerHost` also implements `requestUpdate()` by calling a `useState()` setter, so that a controller can cause its host component to re-render. diff --git a/packages/lit-dev-content/site/docs/v3/getting-started.md b/packages/lit-dev-content/site/docs/v3/getting-started.md new file mode 100644 index 000000000..514736a2f --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/getting-started.md @@ -0,0 +1,105 @@ +--- +title: Getting Started +eleventyNavigation: + key: Getting Started + parent: Introduction + order: 3 +versionLinks: + v1: getting-started/ +--- + +There are many ways to get started using Lit, from our Playground and interactive tutorial to installing into an existing project. + +## Lit Playground + +Get started right away with the interactive playground and examples. Start with "[Hello World](/playground)", then customize it or move on to more examples. + +## Interactive tutorial + +Take our [step-by-step tutorial](/tutorials/intro-to-lit) to learn how to build a Lit component in minutes. + +## Lit starter kits + +We provide TypeScript and JavaScript component starter kits for creating standalone reusable components. See [Starter Kits](/docs/v2/tools/starter-kits/). + +## Install locally from npm + +Lit is available as the `lit` package via npm. + +```sh +npm i lit +``` + +Then import into JavaScript or TypeScript files: + +{% switchable-sample %} + +```ts +import {LitElement, html} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +``` + +```js +import {LitElement, html} from 'lit'; +``` + +{% endswitchable-sample %} + +## Use bundles + +Lit is also available as pre-built, single-file bundles. These are provided for +more flexibility around development workflows: for example, if you would prefer +to download a single file rather than use npm and build tools. The bundles are +standard JavaScript modules with no dependencies - any modern browser should be +able to import and run the bundles from within a ` + + + + +``` + +If you are [bundling](/docs/v2/tools/production/) your code, make sure the `lit/expriemntal-hydrate-support.js` is imported first: +```js +// index.js +import 'lit/experimental-hydrate-support.js'; +import './app-components.js'; +``` + +### Using the `template-shadowroot` polyfill +The HTML snippet below includes an optional strategy to hide the body until the polyfill is loaded to prevent layout shifts. + +```html + + + + + + + + + + + + + + + + +``` + +### Combined example +This example shows a strategy that combines both the `lit/experimental-hydrate-support.js` and the `template-shadowroot` polyfill loading and serves a page with a SSRed component to hydrate client-side. + +[Lit SSR in a Koa server](https://stackblitz.com/edit/lit-ssr-global?file=src/server.js) diff --git a/packages/lit-dev-content/site/docs/v3/ssr/dom-emulation.md b/packages/lit-dev-content/site/docs/v3/ssr/dom-emulation.md new file mode 100644 index 000000000..a4665255f --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/ssr/dom-emulation.md @@ -0,0 +1,21 @@ +--- +title: Lit SSR DOM emulation +eleventyNavigation: + key: DOM emulation + parent: Server rendering + order: 5 +--- + +{% labs-disclaimer %} + +When running in Node, Lit automatically imports and uses a set of DOM shims, and defines the `customElements` global. Only the minimal DOM interfaces needed to define and register components are implemented. These include a few key DOM classes and a roughly functioning `CustomElementRegistry`. + +✅ signifies item is implemented to be functionally the same as in the browser. + + +| Property | Notes | +|-|-| +| `Element` | ⚠️ Partial
`attributes`
`shadowRoot`⚠️ Returns `{host: this}` if `attachShadow()` was called with `{mode: 'open'}`
`setAttribute()`
`removeAttribute()`
`hasAttribute()`
`attachShadow()`⚠️ Returns `{host: this}`
`getAttribute()`
| +| `HTMLElement` | ⚠️ Empty class | +| `CustomElementRegistry` |
`define()`
`get()`
| +| `customElements` | Instance of `CustomElementRegistry` | diff --git a/packages/lit-dev-content/site/docs/v3/ssr/index.md b/packages/lit-dev-content/site/docs/v3/ssr/index.md new file mode 100644 index 000000000..63dcb2a2b --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/ssr/index.md @@ -0,0 +1,10 @@ +--- +title: Server rendering +eleventyNavigation: + key: Server rendering + order: 7 + labs: true +--- + + diff --git a/packages/lit-dev-content/site/docs/v3/ssr/overview.md b/packages/lit-dev-content/site/docs/v3/ssr/overview.md new file mode 100644 index 000000000..b50f89af5 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/ssr/overview.md @@ -0,0 +1,38 @@ +--- +title: Server-side rendering (SSR) +eleventyNavigation: + key: Overview + parent: Server rendering + order: 1 +--- + +{% labs-disclaimer %} + +Server-side rendering (SSR) is a technique for generating and serving the HTML of your components, including shadow DOM and styles, before their JavaScript implementations have loaded and executed. + +You can use SSR for a variety of reasons: +- Performance. Some sites can render faster if they render static HTML first without waiting for JavaScript to load, then (optionally) load the page's JavaScript and hydrate the components. +- SEO and web crawlers. While the major search-engine web crawlers render pages with full JavaScript-enabled browsers, not all web crawlers support JavaScript. +- Robustness. Static HTML renders even if the JavaScript fails to load or the user has JavaScript disabled. + +For a deeper dive into server-side rendering concepts and techniques generally, see [Rendering on the Web](https://web.dev/rendering-on-the-web/) on web.dev. + +Lit supports server-side rendering through the [Lit SSR](https://github.com/lit/lit/tree/main/packages/labs/ssr#readme) package. Lit SSR renders Lit components and templates to static HTML markup in non-browser JavaScript environments like Node. It works without fully emulating the browser's DOM, and takes advantage of Lit's declarative template format to enable fast performance, achieve low time-to-first-byte, and support streaming. + +Lit SSR is a low-level library that you can use directly in your Node-based server or site generator. Check out [an example of Lit SSR used in a Koa server](https://stackblitz.com/edit/lit-ssr-global?file=src/server.js). + +A number of integrations have also been published which make Lit SSR work out-of-the-box: +- [Lit Eleventy Plugin](https://github.com/lit/lit/tree/main/packages/labs/eleventy-plugin-lit#lit-labseleventy-plugin-lit) +- [Astro integration for Lit](https://docs.astro.build/en/guides/integrations-guide/lit/) +- [Rocket](https://rocket.modern-web.dev/) +- ...and more under development! + +## Library status + +This library is under active development with some notable limitations we hope to resolve: + +- Async component work is not supported. See issues [#3219](https://github.com/lit/lit/issues/3219), [#2469](https://github.com/lit/lit/issues/2469). +- Only Lit components using shadow DOM is supported. See issue [#3080](https://github.com/lit/lit/issues/3080). +- Integration with other SSR frameworks are being worked on. See issues for [NextJS](https://github.com/lit/lit/issues/2391) and [Nuxt](https://github.com/lit/lit/issues/3049). +- Declarative shadow DOM is not implemented in all major browsers yet, though a polyfill is available. Read more about it in [client usage](/docs/v2/ssr/client-usage#lit-components). +- There are also open discussions that need to happen regarding `ElementRendererRegistry` for interop with other custom elements. diff --git a/packages/lit-dev-content/site/docs/v3/ssr/server-usage.md b/packages/lit-dev-content/site/docs/v3/ssr/server-usage.md new file mode 100644 index 000000000..c62842e5a --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/ssr/server-usage.md @@ -0,0 +1,212 @@ +--- +title: Lit SSR server usage +eleventyNavigation: + key: Server usage + parent: Server rendering + order: 2 +--- + +{% labs-disclaimer %} + +## Rendering templates + +Server rendering begins with rendering a Lit _template_ with a server-specific `render()` function provided in the `@lit-labs/ssr` package. + +The signature of the render function is: + +```ts +render(value: unknown, renderInfo?: Partial): RenderResult +``` + +Typically `value` is a `TemplateResult` produced by a Lit template expression, like: + +```ts +html`

Hello

` +``` + +The template can contain custom elements, which are rendered in turn, along with their templates. + +```ts +import {render} from '@lit-labs/ssr'; + +const result = render(html` +

Hello SSR!

+ +`); +``` + +To render a single element, you render a template that only contains that element: + +```ts +const result = render(html``); +``` + +### Handling RenderResults + +`render()` returns a `RenderResult`: an iterable of values that can be streamed or concatenated into a string. + +A `RenderResult` can contain strings, nested render results, or Promises of strings or render results. Not all render results contain Promises—those can occur when custom elements perform async tasks, like fetching data—but because a `RenderResult` can contain Promises, processing it into a string or an HTTP response is _potentially_ an async operation. + +Even though a `RenderResult` can contain Promises, it is still a sync iterable, not an async iterable. This is because sync iterables are faster than async iterables and many server renders will not require async rendering, and so shouldn't pay the overhead of an async iterable. + +Allowing Promises in a sync iterable creates a kind of hybrid sync / async iteration protocol. When consuming a `RenderResult`, you must check each value to see if it is a Promise or iterable and wait or recurse as needed. + +`@lit-labs/ssr` contains three utilities to do this for you: + +- `RenderResultReadable` +- `collectResult()` +- `collectResultSync()` + +#### `RenderResultReadable` + +`RenderResultReadable` is a Node `Readable` stream implementation that provides values from a `RenderResult`. This can be piped into a `Writable` stream, or passed to web server frameworks like Koa. + +This is the preferred way to handle SSR results when integrating with a streaming HTTP server or other stream-supprting API. + +```ts +import {render} from '@lit-labs/ssr'; +import {RenderResultReadable} from '@lit-labs/ssr/lib/render-result-readable.js'; + +// Using Koa to stream +app.use(async (ctx) => { + const result = render(html``); + ctx.type = 'text/html'; + ctx.body = new RenderResultReadable(result); +}); +``` + +#### `collectResult()` + +`collectResult(result: RenderResult): Promise` + +`collectResult()` is an async function that takes a `RenderResult` and joins it into a string. It waits for Promises and recurses into nested iterables. + +##### Example +```ts +import {render} from '@lit-labs/ssr'; +import {collectResult} from '@lit-labs/ssr/lib/render-result.js'; + +const result = render(html``); +const html = await collectResult(result); +``` + +#### `collectResultSync()` + +`collectResultSync(result: RenderResult): Promise` + +`collectResultSync()` is a sync function that takes a `RenderResult` and joins it into a string. It recurses into nested iterables, but _throws_ when it encounters a Promise. + +Because this function doesn't support async rendering, it's recommended to only use it when you can't await async functions. + +```ts +import {render} from '@lit-labs/ssr'; +import {collectResultSync} from '@lit-labs/ssr/lib/render-result.js'; + +const result = render(html``); +// Throws if `result` contains a Promise! +const html = collectResultSync(result); +``` + +### Render options + +The second argument to `render()` is a `RenderInfo` object that is used to pass options and current render state to components and sub-templates. + +The main options that can be set by callers are: + +* `deferHydration`: controls whether the top-level custom elements have a `defer-hydration` attribute added to signal that the elements should not automatically hydrate. This defaults to `false` so that top-level elements _do_ automatically hydrate. +* `elementRenderers`: An array of `ElementRenderer` classes to use for rendering custom elements. By default this contains `LitElementRenderer` to render Lit elements. It can be set to include custom `ElementRenderer` instances (documentation forthcoming), or set to an empty array to disable custom element rendering altogether. + +## Running SSR in a VM module or the global scope + +In order to render custom elements in Node, they must first be defined and registered with the global `customElements` API, which is a browser-only feature. As such, when Lit runs in Node, it automatically uses a set of minimal DOM APIs necessary to render Lit on the server, and defines the `customElements` global. (For a list of emulated APIs, see [DOM emulation](/docs/v2/ssr/dom-emulation).) + +Lit SSR provides two different ways of rendering custom elements server-side: rendering in the [global scope](#global-scope) or via [VM modules](#vm-module). VM modules utilizes Node's [`vm.Module`](https://nodejs.org/api/vm.html#class-vmmodule) API, which enables running code within V8 Virtual Machine contexts. The two methods differ primarily in how global state, such as the custom elements registry, are shared. + +When rendering in the global scope, a single shared `customElements` registry will be defined and shared across all render requests, along with any other global state that your component code might set. + +Rendering with VM modules allows each render request to have its own context with a separate global from the main Node process. The `customElements` registry will only be installed within that context, and other global state will also be isolated to that context. VM modules are an experimental Node feature. + +| Global | VM Module | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Pros:
  • Easy to use. Can import component modules directly and call `render()` with templates.
Cons:
  • Custom elements are registered in a shared registry across different render requests.
| Pros:
  • Isolates contexts across different render requests.
Cons:
  • Less intuitive usage. Need to write and specify a module file with a function to call.
  • Slower due the module graph needing to be re-evaluated per request.
| + +### Global Scope + +When using the global scope, you can just call `render()` with a template to get a `RenderResult` and pass that to your server: + +```js +import {render} from '@lit-labs/ssr'; +import {RenderResultReadable} from '@lit-labs/ssr/lib/render-result-readable.js'; +import {myTemplate} from './my-template.js'; + +// ... + +// within a Koa middleware, for example +app.use(async (ctx) => { + const ssrResult = render(myTemplate(data)); + ctx.type = 'text/html'; + ctx.body = new RenderResultReadable(ssrResult); +}); +``` + +### VM Module + +Lit also provide a way to load application code into, and render from, a separate VM context with its own global object. + +```js +// render-template.js +import {render} from '@lit-labs/ssr'; +import {myTemplate} from './my-template.js'; + +export const renderTemplate = (someData) => { + return render(myTemplate(someData)); +}; +``` + +{% switchable-sample %} + +```ts +// server.js +import {ModuleLoader} from '@lit-labs/ssr/lib/module-loader.js'; +import {RenderResultReadable} from '@lit-labs/ssr/lib/render-result-readable.js'; + +// ... + +// within a Koa middleware, for example +app.use(async (ctx) => { + const moduleLoader = new ModuleLoader(); + const importResult = await moduleLoader.importModule( + './render-template.js', // Module to load in VM context + import.meta.url // Referrer URL for module + ); + const {renderTemplate} = importResult.module.namespace + as typeof import('./render-template.js') + const ssrResult = await renderTemplate({some: "data"}); + ctx.type = 'text/html'; + ctx.body = new RenderResultReadable(ssrResult); +}); +``` + +```js +// server.js +import {ModuleLoader} from '@lit-labs/ssr/lib/module-loader.js'; + +// ... + +// within a Koa middleware, for example +app.use(async (ctx) => { + const moduleLoader = new ModuleLoader(); + const importResult = await moduleLoader.importModule( + './render-template.js', // Module to load in VM context + import.meta.url // Referrer URL for module + ); + const {renderTemplate} = importResult.module.namespace; + const ssrResult = await renderTemplate({some: "data"}); + ctx.type = 'text/html'; + ctx.body = Readable.from(ssrResult); +}); +``` + +{% endswitchable-sample %} + +Note: Using this feature requires Node 14+ and passing the `--experimental-vm-modules` flag to Node because of its use of experimental VM modules for creating a module-compatible VM context. diff --git a/packages/lit-dev-content/site/docs/v3/templates/conditionals.md b/packages/lit-dev-content/site/docs/v3/templates/conditionals.md new file mode 100644 index 000000000..46e461ab4 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/templates/conditionals.md @@ -0,0 +1,95 @@ +--- +title: Conditionals +eleventyNavigation: + key: Conditionals + parent: Templates + order: 3 +versionLinks: + v1: components/templates/#use-properties-loops-and-conditionals-in-a-template +--- + +Since Lit leverages normal Javascript expressions, you can use standard Javascript control flow constructs like [conditional operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator), function calls, and `if` or `switch` statements to render conditional content. + +JavaScript conditionals also let you combine nested template expressions, and you can even store template results in variables to use elsewhere. + +## Conditionals with the conditional (ternary) operator + +Ternary expressions with the conditional operator, `?`, are a great way to add inline conditionals: + +```ts +render() { + return this.userName + ? html`Welcome ${this.userName}` + : html`Please log in `; +} +``` + +## Conditionals with if statements + +You can express conditional logic with if statements outside of a template to compute values to use inside of the template: + +```ts +render() { + let message; + if (this.userName) { + message = html`Welcome ${this.userName}`; + } else { + message = html`Please log in `; + } + return html`

${message}

`; +} +``` + +Alternately, you can factor logic into a separate function to simplify your template: + +```ts +getUserMessage() { + if (this.userName) { + return html`Welcome ${this.userName}`; + } else { + return html`Please log in `; + } +} +render() { + return html`

${this.getUserMessage()}

`; +} +``` + +## Caching template results: the cache directive + +In most cases, JavaScript conditionals are all you need for conditional templates. However, if you're switching between large, complicated templates, you might want to save the cost of recreating DOM on each switch. + +In this case, you can use the `cache` _directive_. The cache directive caches DOM for templates that aren't being rendered currently. + +```ts +render() { + return html`${cache(this.userName ? + html`Welcome ${this.userName}`: + html`Please log in `) + }`; +} +``` + +See the [cache directive](/docs/v2/templates/directives/#cache) for more information. + +## Conditionally rendering nothing { #conditionally-rendering-nothing } + +Sometimes, you may want to render nothing in one branch of a conditional operator. This is commonly needed for child expressions and also sometimes needed in attribute expressions. + +For child expressions, the values `undefined`, `null`, the empty string (`''`), and Lit's [nothing](/docs/v2/api/templates/#nothing) sentinel value all render no nodes. See [Removing child content](/docs/v2/templates/expressions/#removing-child) for more information. + +This example renders a value if it exists, and otherwise renders nothing: + +```ts +render() { + return html`${this.userName ?? nothing}`; +} +``` + +For attribute expressions, Lit's [nothing](/docs/v2/api/templates/#nothing) sentinel value removes the attribute. See [Removing an attribute](/docs/v2/templates/expressions/#removing-attribute) for more information. + +This example conditionally renders the `aria-label` attribute: + +```ts +html`` +``` diff --git a/packages/lit-dev-content/site/docs/v3/templates/custom-directives.md b/packages/lit-dev-content/site/docs/v3/templates/custom-directives.md new file mode 100644 index 000000000..3a07df9f0 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/templates/custom-directives.md @@ -0,0 +1,503 @@ +--- +title: Custom directives +eleventyNavigation: + parent: Templates + title: Custom directives + key: Custom directives + order: 6 +versionLinks: + v1: lit-html/creating-directives/ +--- + +Directives are functions that can extend Lit by customizing how a template expression renders. Directives are useful and powerful because they can be stateful, access the DOM, be notified when templates are disconnected and reconnected, and independently update expressions outside of a render call. + +Using a directive in your template is as simple as calling a function in a template expression: + +```js +html`
+ ${fancyDirective('some text')} +
` +``` + +Lit ships with a number of [built-in directives](/docs/v2/templates/directives/) like [`repeat()`](/docs/v2/templates/directives/#repeat) and [`cache()`](/docs/v2/templates/directives/#cache). Users can also write their own custom directives. + +There are two kinds of directives: + +- Simple functions +- Class-based directives + +A simple function returns a value to render. It can take any number of arguments, or no arguments at all. + +```js +export noVowels = (str) => str.replaceAll(/[aeiou]/ig,'x'); +``` + +A class-based directive lets you do things that a simple function can't. Use a class based directive to: + +- Access the rendered DOM directly (for example, add, remove, or reorder rendered DOM nodes). +- Persist state between renders. +- Update the DOM asynchronously, outside of a render call. +- Clean up resources when the directive is disconnected from the DOM + +The rest of this page describes class-based directives. + +## Creating class-based directives + +To create a class-based directive: + +* Implement the directive as a class that extends the {% api "Directive" %} class. +* Pass your class to the {% api "directive()" "directive" %} factory to create a directive function that can be used in Lit template expressions. + +```js +import {Directive, directive} from 'lit/directive.js'; + +// Define directive +class HelloDirective extends Directive { + render() { + return `Hello!`; + } +} +// Create the directive function +const hello = directive(HelloDirective); + +// Use directive +const template = html`
${hello()}
`; +``` + +When this template is evaluated, the directive _function_ (`hello()`) returns a `DirectiveResult` object, which instructs Lit to create or update an instance of the directive _class_ (`HelloDirective`). Lit then calls methods on the directive instance to run its update logic. + +Some directives need to update the DOM asynchronously, outside of the normal update cycle. To create an _async directive_, extend the `AsyncDirective` base class instead of `Directive`. See [Async directives](#async-directives) for details. + +## Lifecycle of a class-based directive + +The directive class has a few built-in lifecycle methods: + +* The class constructor, for one-time initialization. +* `render()`, for declarative rendering. +* `update()`, for imperative DOM access. + +You must implement the `render()` callback for all directives. Implementing `update()` is optional. The default implementation of `update()` calls and returns the value from `render()`. + +Async directives, which can update the DOM outside of the normal update cycle, use some additional lifecycle callbacks. See [Async directives](#async-directives) for details. + +### One-time setup: constructor() + +When Lit encounters a `DirectiveResult` in an expression for the first time, it will construct an instance of the corresponding directive class (causing the directive's constructor and any class field initializers to run): + +{% switchable-sample %} + +```ts +class MyDirective extends Directive { + // Class fields will be initialized once and can be used to persist + // state between renders + value = 0; + // Constructor is only run the first time a given directive is used + // in an expression + constructor(partInfo: PartInfo) { + super(partInfo); + console.log('MyDirective created'); + } + ... +} +``` + +```js +class MyDirective extends Directive { + // Class fields will be initialized once and can be used to persist + // state between renders + value = 0; + // Constructor is only run the first time a given directive is used + // in an expression + constructor(partInfo) { + super(partInfo); + console.log('MyDirective created'); + } + ... +} +``` + +{% endswitchable-sample %} + +As long as the same directive function is used in the same expression each render, the previous instance is reused, thus the state of the instance persists between renders. + +The constructor receives a single `PartInfo` object, which provides metadata about the expression the directive was used in. This can be useful for providing error checking in the cases where a directive is designed to be used only in specific types of expressions (see [Limiting a directive to one expression type](#limiting-a-directive-to-one-expression-type)). + +### Declarative rendering: render() + +The `render()` method should return the value to render into the DOM. It can return any renderable value, including another `DirectiveResult`. + +In addition to referring to state on the directive instance, the `render()` method can also accept arbitrary arguments passed in to the directive function: + +```js +const template = html`
${myDirective(name, rank)}
` +``` + +The parameters defined for the `render()` method determine the signature of the directive function: + +{% switchable-sample %} + +```ts +class MaxDirective extends Directive { + maxValue = Number.MIN_VALUE; + // Define a render method, which may accept arguments: + render(value: number, minValue = Number.MIN_VALUE) { + this.maxValue = Math.max(value, this.maxValue, minValue); + return this.maxValue; + } +} +const max = directive(MaxDirective); + +// Call the directive with `value` and `minValue` arguments defined for `render()`: +const template = html`
${max(someNumber, 0)}
`; +``` + +```js +class MaxDirective extends Directive { + maxValue = Number.MIN_VALUE; + // Define a render method, which may accept arguments: + render(value, minValue = Number.MIN_VALUE) { + this.maxValue = Math.max(value, this.maxValue, minValue); + return this.maxValue; + } +} +const max = directive(MaxDirective); + +// Call the directive with `value` and `minValue` arguments defined for `render()`: +const template = html`
${max(someNumber, 0)}
`; +``` + +{% endswitchable-sample %} + +### Imperative DOM access: update() + +In more advanced use cases, your directive may need to access the underlying DOM and imperatively read from or mutate it. You can achieve this by overriding the `update()` callback. + +The `update()` callback receives two arguments: + +* A `Part` object with an API for directly managing the DOM associated with the expression. +* An array containing the `render()` arguments. + +Your `update()` method should return something Lit can render, or the special value `noChange` if no re-rendering is required. The `update()` callback is quite flexible, but typical uses include: + +- Reading data from the DOM, and using it to generate a value to render. +- Imperatively updating the DOM using the `element` or `parentNode` reference on the `Part` object. In this case, `update()` usually returns `noChange`, indicating that Lit doesn't need to take any further action to render the directive. + +#### Parts + +Each expression position has its own specific `Part` object: + +* {% api "ChildPart" %} for expressions in HTML child position. +* {% api "AttributePart" %} for expressions in HTML attribute value position. +* {% api "BooleanAttributePart" %} for expressions in a boolean attribute value (name prefixed with `?`). +* {% api "EventPart" %} for expressions in an event listener position (name prefixed with `@`). +* {% api "PropertyPart" %} for expressions in property value position (name prefixed with `.`). +* {% api "ElementPart" %} for expressions on the element tag. + +In addition to the part-specific metadata contained in `PartInfo`, all `Part` types provide access to the DOM `element` associated with the expression (or `parentNode`, in the case of `ChildPart`), which may be directly accessed in `update()`. For example: + +{% switchable-sample %} + +```ts +// Renders attribute names of parent element to textContent +class AttributeLogger extends Directive { + attributeNames = ''; + update(part: ChildPart) { + this.attributeNames = (part.parentNode as Element).getAttributeNames?.().join(' '); + return this.render(); + } + render() { + return this.attributeNames; + } +} +const attributeLogger = directive(AttributeLogger); + +const template = html`
${attributeLogger()}
`; +// Renders: `
a b
` +``` + +```js +// Renders attribute names of parent element to textContent +class AttributeLogger extends Directive { + attributeNames = ''; + update(part) { + this.attributeNames = part.parentNode.getAttributeNames?.().join(' '); + return this.render(); + } + render() { + return this.attributeNames; + } +} +const attributeLogger = directive(AttributeLogger); + +const template = html`
${attributeLogger()}
`; +// Renders: `
a b
` +``` + +{% endswitchable-sample %} + +In addition, the `directive-helpers.js` module includes a number of helper functions which act on `Part` objects, and can be used to dynamically create, insert, and move parts within a directive's `ChildPart`. + +#### Calling render() from update() + +The default implementation of `update()` simply calls and returns the value from `render()`. If you override `update()` and still want to call `render()` to generate a value, you need to call `render()` explicitly. + +The `render()` arguments are passed into `update()` as an array. You can pass the arguments to `render()` like this: + +{% switchable-sample %} + +```ts +class MyDirective extends Directive { + update(part: Part, [fish, bananas]: DirectiveParameters) { + // ... + return this.render(fish, bananas); + } + render(fish: number, bananas: number) { ... } +} +``` + +```js +class MyDirective extends Directive { + update(part, [fish, bananas]) { + // ... + return this.render(fish, bananas); + } + render(fish, bananas) { ... } +} +``` + +{% endswitchable-sample %} + +### Differences between update() and render() + +While the `update()` callback is more powerful than the `render()` callback, there is an important distinction: When using the `@lit-labs/ssr` package for server-side rendering (SSR), _only_ the `render()` method is called on the server. To be compatible with SSR, directives should return values from `render()` and only use `update()` for logic that requires access to the DOM. + +## Signaling no change + +Sometimes a directive may have nothing new for Lit to render. You signal this by returning `noChange` from the `update()` or `render()` method. This is different from returning `undefined`, which causes Lit to clear the `Part` associated with the directive. Returning `noChange` leaves the previously rendered value in place. + +There are several common reasons for returning `noChange`: + +* Based on the input values, there's nothing new to render. +* The `update()` method updated the DOM imperatively. +* In an async directive, a call to `update()` or `render()` may return `noChange` because there's nothing to render _yet_. + +For example, a directive can keep track of the previous values passed in to it, and perform its own dirty checking to determine whether the directive's output needs to be updated. The `update()` or `render()` method can return `noChange` to signal that the directive's output doesn't need to be re-rendered. + +{% switchable-sample %} + +```ts +import {Directive} from 'lit/directive.js'; +import {noChange} from 'lit'; +class CalculateDiff extends Directive { + a?: string; + b?: string; + render(a: string, b: string) { + if (this.a !== a || this.b !== b) { + this.a = a; + this.b = b; + // Expensive & fancy text diffing algorithm + return calculateDiff(a, b); + } + return noChange; + } +} +``` + +```js +import {Directive} from 'lit/directive.js'; +import {noChange} from 'lit'; +class CalculateDiff extends Directive { + render(a, b) { + if (this.a !== a || this.b !== b) { + this.a = a; + this.b = b; + // Expensive & fancy text diffing algorithm + return calculateDiff(a, b); + } + return noChange; + } +} +``` + +{% endswitchable-sample %} + +## Limiting a directive to one expression type + +Some directives are only useful in one context, such as an attribute expression or a child expression. If placed in the wrong context, the directive should throw an appropriate error. + +For example, the `classMap` directive validates that it is only used in an `AttributePart` and only for the `class` attribute`: + +{% switchable-sample %} + +```ts +class ClassMap extends Directive { + constructor(partInfo: PartInfo) { + super(partInfo); + if ( + partInfo.type !== PartType.ATTRIBUTE || + partInfo.name !== 'class' + ) { + throw new Error('The `classMap` directive must be used in the `class` attribute'); + } + } + ... +} +``` + +```js +class ClassMap extends Directive { + constructor(partInfo) { + super(partInfo); + if ( + partInfo.type !== PartType.ATTRIBUTE || + partInfo.name !== 'class' + ) { + throw new Error('The `classMap` directive must be used in the `class` attribute'); + } + } + ... +} +``` + +{% endswitchable-sample %} + +## Async directives + +The previous example directives are synchronous: they return values synchronously from their `render()`/`update()` lifecycle callbacks, so their results are written to the DOM during the component's `update()` callback. + +Sometimes, you want a directive to be able to update the DOM asynchronously—for example, if it depends on an asynchronous event like a network request. + +To update a directive's result asynchronously, a directive needs to extend the {% api "AsyncDirective" %} base class, which provides a `setValue()` API. `setValue()` allows a directive to "push" a new value into its template expression, outside of the template's normal `update`/`render` cycle. + +Here's an example of a simple async directive that renders a Promise value: + +{% switchable-sample %} + +```ts +class ResolvePromise extends AsyncDirective { + render(promise: Promise) { + Promise.resolve(promise).then((resolvedValue) => { + // Rendered asynchronously: + this.setValue(resolvedValue); + }); + // Rendered synchronously: + return `Waiting for promise to resolve`; + } +} +export const resolvePromise = directive(ResolvePromise); +``` + +```js +class ResolvePromise extends AsyncDirective { + render(promise) { + Promise.resolve(promise).then((resolvedValue) => { + // Rendered asynchronously: + this.setValue(resolvedValue); + }); + // Rendered synchronously: + return `Waiting for promise to resolve`; + } +} +export const resolvePromise = directive(ResolvePromise); +``` + +{% endswitchable-sample %} + +Here, the rendered template shows "Waiting for promise to resolve," followed by the resolved value of the promise, whenever it resolves. + +Async directives often need to subscribe to external resources. To prevent memory leaks async directives should unsubscribe or dispose of resources when the directive instance is no longer in use. For this purpose, `AsyncDirective` provides the following extra lifecycle callbacks and API: + +* `disconnected()`: Called when a directive is no longer in use. Directive instances are disconnected in three cases: + - When the DOM tree the directive is contained in is removed from the DOM + - When the directive's host element is disconnected + - When the expression that produced the directive no longer resolves to the same directive. + + After a directive receives a `disconnected` callback, it should release all resources it may have subscribed to during `update` or `render` to prevent memory leaks. + +* `reconnected()`: Called when a previously disconnected directive is being returned to use. Because DOM subtrees can be temporarily disconnected and then reconnected again later, a disconnected directive may need to react to being reconnected. Examples of this include when DOM is removed and cached for later use, or when a host element is moved causing a disconnection and reconnection. The `reconnected()` callback should always be implemented alongside `disconnected()`, in order to restore a disconnected directive back to its working state. + +* `isConnected`: Reflects the current connection state of the directive. + +
+ +Note that it is possible for an `AsyncDirective` to continue receiving updates while it is disconnected if its containing tree is re-rendered. Because of this, `update` and/or `render` should always check the `this.isConnected` flag before subscribing to any long-held resources to prevent memory leaks. + +
+ +Below is an example of a directive that subscribes to an `Observable` and handles disconnection and reconnection appropriately: + +{% switchable-sample %} + +```ts +class ObserveDirective extends AsyncDirective { + observable: Observable | undefined; + unsubscribe: (() => void) | undefined; + // When the observable changes, unsubscribe to the old one and + // subscribe to the new one + render(observable: Observable) { + if (this.observable !== observable) { + this.unsubscribe?.(); + this.observable = observable + if (this.isConnected) { + this.subscribe(observable); + } + } + return noChange; + } + // Subscribes to the observable, calling the directive's asynchronous + // setValue API each time the value changes + subscribe(observable: Observable) { + this.unsubscribe = observable.subscribe((v: unknown) => { + this.setValue(v); + }); + } + // When the directive is disconnected from the DOM, unsubscribe to ensure + // the directive instance can be garbage collected + disconnected() { + this.unsubscribe!(); + } + // If the subtree the directive is in was disconnected and subsequently + // re-connected, re-subscribe to make the directive operable again + reconnected() { + this.subscribe(this.observable!); + } +} +export const observe = directive(ObserveDirective); +``` + +```js +class ObserveDirective extends AsyncDirective { + // When the observable changes, unsubscribe to the old one and + // subscribe to the new one + render(observable) { + if (this.observable !== observable) { + this.unsubscribe?.(); + this.observable = observable + if (this.isConnected) { + this.subscribe(observable); + } + } + return noChange; + } + // Subscribes to the observable, calling the directive's asynchronous + // setValue API each time the value changes + subscribe(observable) { + this.unsubscribe = observable.subscribe((v) => { + this.setValue(v); + }); + } + // When the directive is disconnected from the DOM, unsubscribe to ensure + // the directive instance can be garbage collected + disconnected() { + this.unsubscribe(); + } + // If the subtree the directive is in was disconneted and subsequently + // re-connected, re-subscribe to make the directive operable again + reconnected() { + this.subscribe(this.observable); + } +} +export const observe = directive(ObserveDirective); +``` + +{% endswitchable-sample %} diff --git a/packages/lit-dev-content/site/docs/v3/templates/directives.md b/packages/lit-dev-content/site/docs/v3/templates/directives.md new file mode 100644 index 000000000..f23e1c4a3 --- /dev/null +++ b/packages/lit-dev-content/site/docs/v3/templates/directives.md @@ -0,0 +1,1994 @@ +--- +title: Built-in directives +eleventyNavigation: + key: Built-in directives + parent: Templates + order: 5 +versionLinks: + v1: lit-html/template-reference/#built-in-directives +--- + +Directives are functions that can extend Lit by customizing the way an expression renders. +Lit includes a number of built-in directives to help with a variety of rendering needs: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveSummary
+ + Styling + +
+ + [`classMap`](#classmap) + + + + Assigns a list of classes to an element based on an object.
+ + [`styleMap`](#stylemap) + + + + Sets a list of style properties to an element based on an object.
+ + Loops and Conditionals + +
+ + [`when`](#when) + + Renders one of two templates based on a condition.
+ + [`choose`](#choose) + + Renders one of many templates based on a key value.
+ + [`map`](#map) + + Transforms an iterable with a function.
+ + [`repeat`](#repeat) + + Renders values from an iterable into the DOM, with optional keying to enable data diffing and DOM stability.
+ + [`join`](#join) + + Interleave values from an iterable with a joiner value.
+ + [`range`](#range) + + Creates an iterable of numbers in a sequence, useful for iterating a specific number of times.
+ + [`ifDefined`](#ifdefined) + + Sets an attribute if the value is defined and removes the attribute if undefined.
+ + Caching and change detection + +
+ + [`cache`](#cache) + + Caches rendered DOM when changing templates rather than discarding the DOM.
+ + [`keyed`](#keyed) + + Associates a renderable value with a unique key, forcing the DOM to re-render if the key changes.
+ + [`guard`](#guard) + + Only re-evaluates the template when one of its dependencies changes.
+ + [`live`](#live) + + Sets an attribute or property if it differs from the live DOM value rather than the last-rendered value.
+ + Referencing rendered DOM + +
+ + [`ref`](#ref) + + Gets a reference to an element rendered in the template.
+ + Rendering special values + +
+ + [`templateContent`](#templatecontent) + + + + Renders the content of a `