├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .github
└── workflows
│ └── deploy-docs.yaml
├── .gitignore
├── .gitpod.yml
├── .prettierrc
├── .vscode
└── launch.json
├── .yarn
├── .gitignore
├── plugins
│ └── @yarnpkg
│ │ └── plugin-interactive-tools.cjs
└── releases
│ └── yarn-berry.cjs
├── .yarnrc.yml
├── LICENSE.txt
├── README.md
├── context-consumer.js
├── context-provider.js
├── controllers.js
├── core.js
├── docs
├── .vitepress
│ ├── .gitignore
│ └── config.js
├── context-id.md
├── controllers.md
├── core.md
├── dedicated-elements.md
├── generic-mixin.md
├── getting-started.md
├── index.md
├── lit-integration.md
├── storybook.md
└── testing.md
├── examples
├── dedicated-elements
│ ├── index.html
│ └── my-app.js
├── index.html
├── lazy-data
│ ├── cat-facts.js
│ ├── dataProvider.js
│ ├── dataService.js
│ ├── index.html
│ └── my-app.js
├── lit-light-dom
│ ├── index.html
│ └── src
│ │ ├── context-example.js
│ │ ├── styles.js
│ │ ├── theme-consumer.js
│ │ ├── theme-provider.js
│ │ └── title-theme-consumer.js
├── lit-shadow-dom
│ ├── index.html
│ └── src
│ │ ├── shadow-dom-example.js
│ │ ├── styles.js
│ │ ├── theme-consumer.js
│ │ └── theme-switcher.js
└── testing
│ ├── test-controller.js
│ ├── test-property.js
│ ├── vitest.test.js
│ └── wtr.test.js
├── lit.js
├── mixin.js
├── package.json
├── retype.yml
├── test
├── consumer.spec.js
├── context-consumer.spec.js
├── context-provider.spec.js
├── core.spec.js
├── lit.spec.js
├── mixin.spec.js
├── provider.spec.js
└── utils.js
├── tsconfig.types.json
├── types
├── context-consumer.d.ts
├── context-consumer.d.ts.map
├── context-provider.d.ts
├── context-provider.d.ts.map
├── controllers.d.ts
├── controllers.d.ts.map
├── core.d.ts
├── core.d.ts.map
├── lit.d.ts
├── lit.d.ts.map
├── mixin.d.ts
└── mixin.d.ts.map
├── vite.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | charset = utf-8
15 | trim_trailing_whitespace = true
16 | insert_final_newline = true
17 |
18 | [*.md]
19 | trim_trailing_whitespace = false
20 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "prettier"
5 | ],
6 | "parserOptions": {
7 | "ecmaVersion": "latest",
8 | "sourceType": "module"
9 | },
10 | "env": {
11 | "es2021": true,
12 | "browser": true
13 | },
14 | "overrides": [
15 | {
16 | "files": [
17 | "test/**/*.js"
18 | ],
19 | "env": {
20 | "jest": true
21 | }
22 | }
23 | ],
24 | "rules": {}
25 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Automatically normalize line endings for all text-based files
2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion
3 | * text=auto
4 |
5 | # For the following file types, normalize line endings to LF on
6 | # checkin and prevent conversion to CRLF when they are checked out
7 | # (this is required in order to prevent newline related issues like,
8 | # for example, after the build script is run)
9 | .* text eol=lf
10 | *.css text eol=lf
11 | *.ejs text eol=lf
12 | *.js text eol=lf
13 | *.md text eol=lf
14 | *.txt text eol=lf
15 | *.json text eol=lf
16 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-docs.yaml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages
2 | #
3 | name: Deploy Documentation to Github Pages
4 |
5 | on:
6 | # Runs on pushes with changes to docs folder
7 | push:
8 | paths:
9 | - 'docs/**'
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
21 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
22 | concurrency:
23 | group: pages
24 | cancel-in-progress: false
25 |
26 | jobs:
27 | # Build job
28 | build:
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Checkout
32 | uses: actions/checkout@v3
33 | with:
34 | fetch-depth: 0 # Not needed if lastUpdated is not enabled
35 | # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm
36 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
37 | - name: Setup Node
38 | uses: actions/setup-node@v3
39 | with:
40 | node-version: 18
41 | cache: yarn
42 | - name: Setup Pages
43 | uses: actions/configure-pages@v3
44 | - name: Install dependencies
45 | run: yarn install --immutable
46 | - name: Build with VitePress
47 | run: |
48 | npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
49 | touch docs/.vitepress/dist/.nojekyll
50 | - name: Upload artifact
51 | uses: actions/upload-pages-artifact@v2
52 | with:
53 | path: docs/.vitepress/dist
54 |
55 | # Deployment job
56 | deploy:
57 | environment:
58 | name: github-pages
59 | url: ${{ steps.deployment.outputs.page_url }}
60 | needs: build
61 | runs-on: ubuntu-latest
62 | name: Deploy
63 | steps:
64 | - name: Deploy to GitHub Pages
65 | id: deployment
66 | uses: actions/deploy-pages@v2
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Include your project-specific ignores in this file
2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
3 |
4 | coverage
5 | dist
6 | docs/dist
7 | node_modules
8 | npm-debug.log
9 | /.idea/
10 | /yarn-error.log
11 | /.retype/
12 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | # This configuration file was automatically generated by Gitpod.
2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
3 | # and commit this file to your remote git repository to share the goodness with others.
4 |
5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
6 |
7 | tasks:
8 | - init: yarn install
9 | command: yarn run start
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "endOfLine": "auto"
5 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Jest All",
11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest",
12 | "args": ["--runInBand"],
13 | "console": "integratedTerminal",
14 | "internalConsoleOptions": "neverOpen"
15 | },
16 | {
17 | "type": "node",
18 | "request": "launch",
19 | "name": "Jest Current File",
20 | "program": "${workspaceFolder}/node_modules/jest/bin/jest",
21 | "args": ["${relativeFile}"],
22 | "console": "integratedTerminal",
23 | "internalConsoleOptions": "neverOpen"
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/.yarn/.gitignore:
--------------------------------------------------------------------------------
1 | /cache/
2 | /install-state.gz
3 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | plugins:
4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
5 | spec: "@yarnpkg/plugin-interactive-tools"
6 |
7 | yarnPath: .yarn/releases/yarn-berry.cjs
8 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2015-2016 Konstantin Tarkus, Kriasoft LLC. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wc-context
2 |
3 | > A comprehensive context implementation for web components
4 |
5 | ### Features
6 |
7 | ✓ Small and fast. No Internet Explorer support
8 | ✓ Flexible ways to define context providers and consumers
9 | ✓ Ability to provide or consume one or more contexts per element
10 | ✓ Context can be provided or consumed by any HTML element
11 | ✓ Context can be identified by string or unique identifier
12 | ✓ Works with shadow dom and slotted content (handles timing issues)
13 | ✓ Easy to implement unit tests. Most of the time, same as components without context
14 | ✓ Builtin integration with LitElement
15 | ✓ Builtin ContextProvider ([Reactive Controller](https://lit.dev/docs/composition/controllers/)) with primitives for lazy loading
16 | ✓ Builtin context-provider and context-consumer elements
17 | ✓ Conforms with the [Context protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md)
18 | ✓ Full code coverage
19 |
20 | ### Live example
21 |
22 | - Lit integration: [Shadow DOM](https://codesandbox.io/p/sandbox/wc-context-example-shadow-dom-vite-nkymx3) /
23 |
24 | ### Usage
25 |
26 | Check the documentation https://blikblum.github.io/wc-context/
27 |
28 | ### License
29 |
30 | MIT
31 | Copyright © 2023 Luiz Américo Pereira Câmara aka blikblum
32 |
--------------------------------------------------------------------------------
/context-consumer.js:
--------------------------------------------------------------------------------
1 | import { observeContext, unobserveContext } from './core.js'
2 |
3 | class ContextUpdateEvent extends Event {
4 | constructor(context, value) {
5 | super('context-update', { bubbles: true })
6 | this.context = context
7 | this.value = value
8 | }
9 | }
10 |
11 | function setValueDispatchEvent(consumer, value, context) {
12 | consumer.value = value
13 | consumer.dispatchEvent(new ContextUpdateEvent(context, value))
14 | }
15 |
16 | class ContextConsumer extends HTMLElement {
17 | static get observedAttributes() {
18 | return ['context']
19 | }
20 |
21 | attributeChangedCallback(name, oldValue, value) {
22 | this[name] = value
23 | }
24 |
25 | connectedCallback() {
26 | this._context = this.context
27 | if (this._context) {
28 | observeContext(this, this._context, this._context, setValueDispatchEvent)
29 | }
30 | }
31 |
32 | disconnectedCallback() {
33 | if (this._context) {
34 | unobserveContext(this, this._context)
35 | }
36 | }
37 | }
38 |
39 | customElements.define('context-consumer', ContextConsumer)
40 |
--------------------------------------------------------------------------------
/context-provider.js:
--------------------------------------------------------------------------------
1 | import { registerContext, updateContext } from './core.js'
2 |
3 | // custom element that publishes an arbitrary context key and value
4 |
5 | function getFromProperty(provider, prop) {
6 | return provider[prop]
7 | }
8 |
9 | class ContextProvider extends HTMLElement {
10 | static get observedAttributes() {
11 | return ['context', 'value']
12 | }
13 |
14 | get context() {
15 | return this._context
16 | }
17 |
18 | set context(context) {
19 | if (!this._context && context) {
20 | // register context once
21 | this._context = context
22 | registerContext(this, this._context, 'value', getFromProperty)
23 | }
24 | }
25 |
26 | attributeChangedCallback(name, oldValue, value) {
27 | this[name] = value
28 | }
29 |
30 | set value(value) {
31 | this._value = value
32 | if (this._context) {
33 | updateContext(this, this._context)
34 | }
35 | }
36 |
37 | get value() {
38 | return this._value
39 | }
40 | }
41 |
42 | customElements.define('context-provider', ContextProvider)
43 |
--------------------------------------------------------------------------------
/controllers.js:
--------------------------------------------------------------------------------
1 | import {
2 | observeContext,
3 | unobserveContext,
4 | onContextObserve,
5 | registerContext,
6 | updateContext,
7 | } from './core.js'
8 |
9 | /**
10 | * @typedef { import('./core.js').Context } Context
11 | */
12 |
13 | function setValue(host, value, instance) {
14 | instance._value = value
15 | if (typeof instance.callback === 'function') {
16 | instance.callback.call(host, value)
17 | } else {
18 | host.requestUpdate()
19 | }
20 | }
21 |
22 | /**
23 | * @callback ContextConsumerCallback
24 | * @param {HTMLElement} host
25 | * @param {*} [value]
26 | * @returns {void}
27 | */
28 |
29 | class ContextConsumer {
30 | /**
31 | * Creates an instance of ContextProvider.
32 | * @param {HTMLElement} host
33 | * @param {string | Context} context Context identifier
34 | * @param {ContextConsumerCallback} callback
35 | */
36 | constructor(host, context, callback) {
37 | host.addController(this)
38 | this.host = host
39 | this.context = context
40 | this.callback = callback
41 | this._value = undefined
42 | }
43 |
44 | get value() {
45 | return this._value
46 | }
47 |
48 | hostConnected() {
49 | observeContext(this.host, this.context, this, setValue)
50 | }
51 |
52 | hostDisconnected() {
53 | unobserveContext(this.host, this.context)
54 | }
55 | }
56 |
57 | function getFromValue(host, instance) {
58 | return instance._value
59 | }
60 |
61 | class ContextProvider {
62 | /**
63 | * Creates an instance of ContextProvider.
64 | * @param {HTMLElement} host
65 | * @param {string | Context} context Context identifier
66 | * @param {*} initialValue
67 | */
68 | constructor(host, context, initialValue) {
69 | if (typeof host.addController === 'function') {
70 | host.addController(this)
71 | }
72 | this.host = host
73 | this.context = context
74 | this._value = initialValue
75 | this._initialized = false
76 | this._finalized = false
77 | registerContext(host, context, this, getFromValue)
78 | onContextObserve(host, context, () => {
79 | if (!this._initialized) {
80 | this._initialized = true
81 | this.initialize()
82 | }
83 | })
84 | }
85 |
86 | get value() {
87 | return this._value
88 | }
89 |
90 | set value(value) {
91 | this._value = value
92 | updateContext(this.host, this.context)
93 | }
94 |
95 | connect() {
96 | if (this._finalized) {
97 | this.initialize()
98 | this._finalized = false
99 | this._initialized = true
100 | }
101 | }
102 |
103 | disconnect() {
104 | if (this._initialized) {
105 | this._initialized = false
106 | this._finalized = true
107 | this.finalize()
108 | }
109 | }
110 |
111 | hostConnected() {
112 | this.connect()
113 | }
114 |
115 | hostDisconnected() {
116 | this.disconnect()
117 | }
118 |
119 | initialize() {}
120 |
121 | finalize() {}
122 | }
123 |
124 | export { ContextConsumer, ContextProvider }
125 |
--------------------------------------------------------------------------------
/core.js:
--------------------------------------------------------------------------------
1 | const noContext = Symbol('noContext')
2 |
3 | const orphanMap = {}
4 |
5 | const resolved = Promise.resolve()
6 |
7 | const orphanResolveQueue = {
8 | contexts: new Set(),
9 | running: false,
10 | add(context) {
11 | this.contexts.add(context)
12 | if (!this.running) {
13 | this.running = true
14 | resolved.then(() => {
15 | this.contexts.forEach((context) => {
16 | const orphans = orphanMap[context]
17 | orphans.forEach(({ setter, payload }, orphan) => {
18 | const handled = sendContextEvent(orphan, context, payload, setter)
19 | if (handled) {
20 | orphans.delete(orphan)
21 | }
22 | })
23 | })
24 | this.contexts.clear()
25 | this.running = false
26 | })
27 | }
28 | },
29 | }
30 |
31 | function addOrphan(el, name, payload, setter) {
32 | const orphans = orphanMap[name] || (orphanMap[name] = new Map())
33 | orphans.set(el, { setter, payload })
34 | }
35 |
36 | function removeOrphan(el, name) {
37 | const orphans = orphanMap[name]
38 | if (orphans) {
39 | orphans.delete(el)
40 | }
41 | }
42 |
43 | export class ContextRequestEvent extends Event {
44 | constructor(context, callback, subscribe) {
45 | super('context-request', {
46 | bubbles: true,
47 | cancelable: true,
48 | composed: true,
49 | })
50 | this.context = context
51 | this.callback = callback
52 | this.subscribe = subscribe
53 | }
54 | }
55 |
56 | function sendContextEvent(consumer, context, payload, setter) {
57 | let handled
58 | const callback = (value, unsubscribe) => {
59 | if (!handled) {
60 | registerProvider(consumer, context, unsubscribe)
61 | }
62 | setter(consumer, value, payload)
63 | handled = true
64 | }
65 | const event = new ContextRequestEvent(context, callback, true)
66 | consumer.dispatchEvent(event)
67 | return handled
68 | }
69 |
70 | /**
71 | * @typedef {Object} Context
72 | */
73 |
74 | /**
75 | * @typedef {Object} ContextGetter
76 | * @property {Function} getter Function that is called in provider
77 | * @property {any} [payload] Payload passed to getter
78 | */
79 |
80 | /**
81 | * @param {string} key Identify the context
82 | * @return {Context}
83 | */
84 | function createContext(key) {
85 | return {
86 | key,
87 | toString() {
88 | return this.key
89 | },
90 | }
91 | }
92 |
93 | /**
94 | * @param {HTMLElement} provider HTMLElement acting as a context provider
95 | * @param {Object} options
96 | * @param {Function} options.getter
97 | * @param {*} [options.payload]
98 | * @return {*}
99 | */
100 | function getProviderValue(provider, { getter, payload }) {
101 | return getter(provider, payload)
102 | }
103 |
104 | /**
105 | * @description Default context getter implementation. Just returns the payload
106 | * @param {HTMLElement} provider HTMLElement acting as a context provider
107 | * @param {*} payload Options passed to the callback
108 | * @return {*}
109 | */
110 | function providerGetter(provider, payload) {
111 | return payload
112 | }
113 |
114 | /**
115 | * @param {HTMLElement} provider HTMLElement acting as a context provider
116 | * @param {string | Context} context Context identifier
117 | * @param {*} payload Value passed to getter
118 | * @param {Function} [getter=providerGetter]
119 | */
120 | function registerContext(provider, context, payload, getter = providerGetter) {
121 | const observerMap =
122 | provider.__wcContextObserverMap || (provider.__wcContextObserverMap = {})
123 | const providedContexts =
124 | provider.__wcContextProvided || (provider.__wcContextProvided = {})
125 | providedContexts[context] = { getter, payload }
126 | const observers = observerMap[context] || (observerMap[context] = [])
127 | const orphans = orphanMap[context]
128 | provider.addEventListener(`context-request`, (event) => {
129 | const { target, callback, subscribe } = event
130 | if (event.context !== context || typeof callback !== 'function') {
131 | return
132 | }
133 | event.stopPropagation()
134 | const value = getProviderValue(provider, providedContexts[context])
135 | if (subscribe || value === noContext) {
136 | const unsubscribe = () => {
137 | removeObserver(provider, context, target)
138 | }
139 | observers.push({
140 | consumer: target,
141 | callback,
142 | unsubscribe,
143 | once: !subscribe,
144 | })
145 | if (value !== noContext) {
146 | callback(value, unsubscribe)
147 | }
148 | runListeners(provider, context, 'observe', observers.length)
149 | } else {
150 | callback(value)
151 | }
152 | })
153 | if (orphans && orphans.size) {
154 | orphanResolveQueue.add(context)
155 | }
156 | }
157 |
158 | /**
159 | * @param {HTMLElement} provider HTMLElement that provides a context
160 | * @param {string | Context} context Context identifier
161 | * @param {string} caller Function caller identifier
162 | * @return {ContextGetter}
163 | */
164 | function getProvidedContext(provider, context, caller) {
165 | const providedContexts = provider.__wcContextProvided
166 | const providedContext = providedContexts && providedContexts[context]
167 |
168 | if (!providedContext) {
169 | throw new Error(`${caller}: "${context.name || context}" is not registered`)
170 | }
171 |
172 | return providedContext
173 | }
174 |
175 | /**
176 | * @param {HTMLElement} provider HTMLElement that provides a context
177 | * @param {string | Context} context Context identifier
178 | * @param {*} [payload=context] Value passed to provider context getter
179 | */
180 | function updateContext(provider, context, payload) {
181 | const observerMap = provider.__wcContextObserverMap
182 | const providedContext = getProvidedContext(provider, context, 'updateContext')
183 |
184 | if (payload !== undefined) {
185 | providedContext.payload = payload
186 | }
187 |
188 | const value = getProviderValue(provider, providedContext)
189 |
190 | if (value === noContext) {
191 | return
192 | }
193 |
194 | const observers = observerMap && observerMap[context]
195 | // if we got here, observers is necessarily defined
196 | observers.forEach(({ consumer, callback, unsubscribe, once }) => {
197 | if (once) {
198 | unsubscribe()
199 | unsubscribe = undefined
200 | }
201 | callback.call(consumer, value, unsubscribe)
202 | })
203 | }
204 |
205 | function consumerSetter(consumer, value, name) {
206 | const oldValue = consumer[name]
207 | if (oldValue !== value) {
208 | consumer[name] = value
209 | if (typeof consumer.contextChangedCallback === 'function') {
210 | consumer.contextChangedCallback(name, oldValue, value)
211 | }
212 | }
213 | }
214 |
215 | function runListeners(provider, context, type, count) {
216 | const providedContext = getProvidedContext(provider, context, 'runListeners')
217 |
218 | const listeners = providedContext.listeners
219 | if (listeners) {
220 | for (const listener of listeners) {
221 | if (listener.type === type) {
222 | listener.callback.call(provider, { count })
223 | }
224 | }
225 | }
226 | }
227 |
228 | function registerProvider(consumer, context, provider) {
229 | const providerMap =
230 | consumer.__wcContextProviderMap || (consumer.__wcContextProviderMap = {})
231 | providerMap[context] = provider
232 | }
233 |
234 | /**
235 | * @description Observes a context in a consumer. Optionally define how the context value is set
236 | * @param {HTMLElement} consumer HTMLElement that consumes a context
237 | * @param {string | Context} context Context identifier
238 | * @param {*} [payload=context] Value passed to setter
239 | * @param {Function} [setter=consumerSetter]
240 | */
241 | function observeContext(
242 | consumer,
243 | context,
244 | payload = context,
245 | setter = consumerSetter
246 | ) {
247 | const handled = sendContextEvent(consumer, context, payload, setter)
248 | if (!handled) {
249 | addOrphan(consumer, context, payload, setter)
250 | }
251 | }
252 |
253 | function removeObserver(provider, context, consumer) {
254 | const observerMap = provider.__wcContextObserverMap
255 | const observers = observerMap[context]
256 | const consumerIndex = observers.findIndex(
257 | (observer) => observer.consumer === consumer
258 | )
259 | if (consumerIndex !== -1) {
260 | observers.splice(consumerIndex, 1)
261 | }
262 | runListeners(provider, context, 'unobserve', observers.length)
263 | }
264 |
265 | /**
266 | * @description Unobserves a context in a consumer
267 | * @param {HTMLElement} consumer HTMLElement that consumes a context
268 | * @param {string | Context} context Context identifier
269 | */
270 | function unobserveContext(consumer, context) {
271 | const providerMap = consumer.__wcContextProviderMap
272 | if (providerMap) {
273 | const unsubscribe = providerMap[context]
274 | if (unsubscribe) {
275 | unsubscribe()
276 | providerMap[context] = undefined
277 | }
278 | }
279 |
280 | removeOrphan(consumer, context)
281 | }
282 |
283 | /**
284 | * @param {HTMLElement} provider
285 | * @param {string | Context} context Context identifier
286 | * @param {Function} callback
287 | */
288 | function onContextObserve(provider, context, callback) {
289 | const providedContext = getProvidedContext(
290 | provider,
291 | context,
292 | 'onContextObserve'
293 | )
294 | const listeners =
295 | providedContext.listeners || (providedContext.listeners = [])
296 | listeners.push({ callback, type: 'observe' })
297 | }
298 |
299 | /**
300 | * @param {HTMLElement} provider
301 | * @param {string | Context} context Context identifier
302 | * @param {Function} callback
303 | */
304 | function onContextUnobserve(provider, context, callback) {
305 | const providedContext = getProvidedContext(
306 | provider,
307 | context,
308 | 'onContextUnobserve'
309 | )
310 |
311 | const listeners =
312 | providedContext.listeners || (providedContext.listeners = [])
313 | listeners.push({ callback, type: 'unobserve' })
314 | }
315 |
316 | /**
317 | *
318 | *
319 | * @param {HTMLElement} consumer
320 | * @param {Context | string} context
321 | * @return {*}
322 | */
323 | async function getContext(consumer, context) {
324 | return new Promise((resolve) => {
325 | const event = new ContextRequestEvent(context, resolve, false)
326 | consumer.dispatchEvent(event)
327 | })
328 | }
329 |
330 | export {
331 | noContext,
332 | createContext,
333 | registerContext,
334 | updateContext,
335 | observeContext,
336 | unobserveContext,
337 | consumerSetter,
338 | providerGetter,
339 | onContextObserve,
340 | onContextUnobserve,
341 | getContext,
342 | }
343 |
--------------------------------------------------------------------------------
/docs/.vitepress/.gitignore:
--------------------------------------------------------------------------------
1 | /cache/
2 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: 'wc-context',
3 | description: 'Context for web components',
4 | base: '/wc-context/',
5 | themeConfig: {
6 | sidebar: [
7 | {
8 | text: 'Introduction',
9 | items: [
10 | { text: 'About wc-context', link: '/' },
11 | { text: 'Getting Started', link: '/getting-started' },
12 | ],
13 | },
14 | {
15 | text: 'Usage',
16 | items: [
17 | { text: 'Context identification', link: '/context-id' },
18 | { text: 'Lit integration', link: '/lit-integration' },
19 | { text: 'Reactive controllers', link: '/controllers' },
20 | { text: 'Generic mixin', link: '/generic-mixin' },
21 | { text: 'Dedicated elements', link: '/dedicated-elements' },
22 | { text: 'Core API', link: '/core' },
23 | ],
24 | },
25 | {
26 | text: 'Guides',
27 | items: [
28 | { text: 'Testing', link: '/testing' },
29 | { text: 'Storybook', link: '/storybook' },
30 | ],
31 | },
32 | ],
33 | footer: {
34 | message: 'Released under the MIT License.',
35 | copyright: 'Copyright © 2018-present Luiz Américo Pereira Câmara',
36 | },
37 | },
38 | }
39 |
--------------------------------------------------------------------------------
/docs/context-id.md:
--------------------------------------------------------------------------------
1 | # Context identification
2 |
3 | Its possible to identify the context either by a string or an unique identifier, with different in functionality
4 |
5 | ## String
6 |
7 | A context can be identified by a string value
8 |
9 | ```js
10 | import { LitElement } from 'lit'
11 | import { ContextProvider, ContextConsumer } from 'wc-context/controllers.js'
12 |
13 |
14 | // provides "theme" context
15 | class RootElement extends HTMLElement {
16 | themeProvider = new ContextProvider(this, 'theme', 'light')
17 | }
18 |
19 | // consumes "theme" context
20 | class ConsumerElement extends LitElement {
21 | themeConsumer = new ContextConsumer(this, 'theme')
22 | }
23 | ```
24 |
25 | ## Unique identifier
26 |
27 | Use `createContext` function to create a unique context identifier
28 |
29 | ```js
30 | import { LitElement } from 'lit'
31 | import { ContextProvider, ContextConsumer } from 'wc-context/controllers.js'
32 | import { createContext } from 'wc-context'
33 |
34 | const themeContext = createContext()
35 |
36 | // provides `themeContext` context
37 | class RootElement extends HTMLElement {
38 | themeProvider = new ContextProvider(this, themeContext, 'light')
39 | }
40 |
41 | // consumes `themeContext` context
42 | class ConsumerElement extends LitElement {
43 | themeConsumer = new ContextConsumer(this, themeContext)
44 | }
45 | ```
--------------------------------------------------------------------------------
/docs/controllers.md:
--------------------------------------------------------------------------------
1 | # Reactive controllers
2 |
3 | wc-context comes with [Reactive controllers](https://lit.dev/docs/composition/controllers/) to provide or consume a context
4 |
5 | ## ContextProvider
6 |
7 | With `ContextProvider` controller is possible to define and update a context
8 |
9 | The constructor first parameter is the element where the context will be bound, the second parameter is the context id / name and
10 | the third is the default value
11 |
12 | The `context` instance property identifies the context
13 |
14 | To update the context value set the `value` property of controller instance
15 |
16 | > For basic usage (provide and update context), `ContextProvider` does not require that the component implements [ReactiveControllerHost](https://lit.dev/docs/api/controllers/#ReactiveControllerHost) interface
17 |
18 | ```js
19 | import { ContextProvider } from 'wc-context/controllers.js'
20 |
21 | // ContextProvider can be used with any custom element for basic usage
22 | class RootElement extends HTMLElement {
23 | themeProvider = new ContextProvider(this, 'theme', 'light')
24 |
25 | setDarkTheme() {
26 | this.themeProvider.value = 'dark'
27 | }
28 | }
29 | ```
30 |
31 | ### Subclassing ContextProvider
32 |
33 | By subclassing `ContextProvider` is possible detect when context is first requested / not needed anymore with `initialize` and `finalize` methods. It allows, e.g., to load and dispose data on demand or subscribe to a realtime API.
34 |
35 | ::: code-group
36 |
37 | ```js [Lazy data]
38 | import { ContextProvider } from 'wc-context/controllers.js'
39 | import { fetchData } from './dataService.js'
40 |
41 | export class LazyDataProvider extends ContextProvider {
42 | async initialize() {
43 | // the context value will be updated asynchronously
44 | this.value = await fetchData({ key: this.context })
45 | }
46 | }
47 | ```
48 |
49 | ```js [Lazy data+]
50 | import { ContextProvider } from 'wc-context/controllers.js'
51 | import { fetchData } from './dataService.js'
52 |
53 | export class LazyDataPlusProvider extends ContextProvider {
54 | async initialize() {
55 | // initialize with a loading flag
56 | this.value = { loading: true }
57 | // the context value will be updated asynchronously
58 | try {
59 | const data = await fetchData({ key: this.context })
60 | this.value = { data, loading: false }
61 | } catch (error) {
62 | // set the error
63 | this.value = { error, loading: false }
64 | }
65 | }
66 | }
67 | ```
68 |
69 | ```js [Realtime]
70 | import { ContextProvider } from 'wc-context/controllers.js'
71 | import { observeData } from './dataService.js'
72 |
73 | export class RealTimeProvider extends ContextProvider {
74 | initialize() {
75 | // observeData is a real time api
76 | this.unsubscribe = observeData((value) => {
77 | this.value = value
78 | })
79 | }
80 |
81 | finalize() {
82 | if (this.unsubscribe) {
83 | this.unsubscribe()
84 | this.unsubscribe = undefined
85 | }
86 | }
87 | }
88 | ```
89 |
90 | :::
91 |
92 | ## ContextConsumer
93 |
94 | With `ContextConsumer` controller is possible to consume a context
95 |
96 | The constructor first parameter is the element, the second parameter is the context id / name.
97 |
98 | Optionally is possible to pass a third parameter, an callback called when context value changes
99 |
100 | > When the callback parameter is omitted, the controller calls `requestUpdate` method when context changes.
101 | > If a callback is passed, `requestUpdate` method is not called. If necessary to trigger a new render,
102 | > `requestUpdate` should be called manually or a reactive property should be updated
103 |
104 | ::: code-group
105 |
106 | ```js [No callback]
107 | import { LitElement } from 'lit'
108 | import { ContextConsumer } from 'wc-context/controllers.js'
109 |
110 | class ConsumerElement extends LitElement {
111 | themeConsumer = new ContextConsumer(this, 'theme')
112 |
113 | render() {
114 | return html`
Theme: ${this.themeConsumer.value}
`
115 | }
116 | }
117 | ```
118 |
119 | ```js [With callback]
120 | import { LitElement } from 'lit'
121 | import { ContextConsumer } from 'wc-context/controllers.js'
122 |
123 | class ConsumerElement extends LitElement {
124 | themeConsumer = new ContextConsumer(this, 'theme', (value) => {
125 | // if theme was declared as a reactive property, no manual request update would be necessary
126 | this.theme = value
127 | this.requestUpdate()
128 | })
129 |
130 | render() {
131 | return html`Theme: ${this.theme}
`
132 | }
133 | }
134 | ```
135 |
136 | :::
137 |
--------------------------------------------------------------------------------
/docs/core.md:
--------------------------------------------------------------------------------
1 | # Core (low level) API
2 |
3 | The low level functions are exported in `wc-context` and can be used to handle specific cases or create a new interface / integration, like [the one for storybook](./storybook.md#context-consumed-with-controllerdedicated-elements).
4 |
5 | For example, is possible to provide a context in body element to be consumed anywhere in the page.
6 |
7 | ```javascript
8 | import { registerContext, updateContext } from 'wc-context'
9 |
10 | registerContext(document.body, 'theme', 'light')
11 |
12 | document.querySelector('#theme-toggle-button').addEventListener('click', () => {
13 | updateContext(document.body, 'theme', 'dark')
14 | })
15 | ```
16 |
17 | > TBD: properly document the functions, how to customize setting / getting context values
--------------------------------------------------------------------------------
/docs/dedicated-elements.md:
--------------------------------------------------------------------------------
1 | # Dedicated custom elements
2 |
3 | wc-context comes with two built in components that can be used to provide or consume a context declaratively.
4 |
5 | ## context-provider
6 |
7 | The `context-provider` component is exported in 'wc-context/context-provider.js'
8 |
9 | It accepts the attribute `context` that identifies the context to be provided and the attribute / property `value` that defines the context value
10 |
11 | > The `context` attribute cannot be modified. Setting its value after initial name / id definition has no effect.
12 |
13 | ## context-consumer
14 |
15 | The `context-consumer` component is exported in 'wc-context/context-consumer.js'
16 |
17 | It accepts the attribute `context` that identifies the context to be consumed
18 |
19 | An `context-update` event is triggered on `context-consumer` when context value changes. The event has `context` and `value` properties
20 |
21 | ## Example
22 |
23 | ```javascript
24 | import 'wc-context/context-provider.js'
25 | import 'wc-context/context-consumer.js'
26 |
27 | document.body.innerHTML = `
28 |
29 |
30 |
31 |
32 | `
33 |
34 | const provider = document.querySelector('context-provider')
35 | const consumer = document.querySelector('context-consumer')
36 |
37 | consumer.addEventListener('context-update', ({ context, value }) => {
38 | console.log(`Context ${context}:${value}`)
39 | })
40 |
41 | provider.value = 'dark'
42 | ```
43 |
--------------------------------------------------------------------------------
/docs/generic-mixin.md:
--------------------------------------------------------------------------------
1 | # Generic custom element mixin
2 |
3 | The `withContext` class mixin exported in the root namespace, implements an API similar to DOM `observedAttributes`/`attributeChangedCallback`.
4 |
5 | > This mixin can be used in any custom element / web component regardless of library / framework.
6 |
7 | ## Providing a context
8 |
9 | Contexts are provided in an custom element through static `providedContexts` field where the key is the context name and value holds a configuration object. The configuration can have a `value` property defining the default context value or a `property` one defining from what component property the context will retrieve its value.
10 |
11 | The `updateContext` method updates the context value. It accepts the context name / id as first argument and value as second. If it was configured to read the value from a property the second argument should be omitted.
12 |
13 | ```javascript
14 | import { withContext } from 'wc-context/mixin.js'
15 |
16 | class Provider extends withContext(HTMLElement) {
17 | static providedContexts = {
18 | theme: { value: 'blue' },
19 | title: 'activeTitle', // shorthand for { property: 'activeTitle' }
20 | }
21 |
22 | get activeTitle() {
23 | return this._activeTitle
24 | }
25 |
26 | set activeTitle(value) {
27 | this._activeTitle = value
28 | this.updateContext('title')
29 | }
30 |
31 | toggleTheme() {
32 | this.updateContext('theme', 'newtheme')
33 | }
34 |
35 | toggleTitle() {
36 | this.activeTitle = 'New title'
37 | }
38 | }
39 | ```
40 |
41 | ## Consuming a context
42 |
43 | To consume a context, is necessary to define a static property `observedContexts` with an array of context names / ids.
44 |
45 | When a observed context value changes, the `contextChangedCallback` is called with arguments context (name or id), oldValue, newValue
46 |
47 | Optionally is possible to define the observed context with an array where the first item is the context name / id and the second a property name to be updated with the context value each time a context change occurs
48 |
49 | > When overriding `connectedCallback` or `disconnectedCallback` is necessary to call the respective super methods.
50 |
51 | ```javascript
52 | import { withContext } from 'wc-context/mixin.js'
53 |
54 | class Consumer extends withContext(HTMLElement) {
55 | static observedContexts = ['theme', ['title', 'titleProp']]
56 |
57 | contextChangedCallback(name, oldValue, value) {
58 | console.log(`theme changed from "${oldValue}" to "${value}"`)
59 | // updates el accordingly
60 | }
61 |
62 | get titleProp() {
63 | return this._titleProp
64 | }
65 |
66 | set titleProp(value) {
67 | // this setter is called when theme context changes
68 | this._titleProp = value
69 | }
70 |
71 | connectedCallback() {
72 | super.connectedCallback()
73 | this.innerHTML = `Theme is ${this.theme}, title is ${this.titleProp}
`
74 | }
75 | }
76 | ```
77 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | ## Install
4 |
5 | ::: code-group
6 |
7 | ```sh [yarn]
8 | yarn add wc-context
9 | ```
10 |
11 | ```sh [npm]
12 | npm install wc-context
13 | ```
14 |
15 | :::
16 |
17 | ## Usage
18 |
19 | Check the documentation on how to [identify a context](./context-id.md) and how to provide / consume it using one of the interfaces: [Lit integration](lit-integration.md), [Generic custom element mixin](generic-mixin.md), [Dedicated custom elements](dedicated-elements.md) or [Reactive controllers](controllers.md)
20 |
21 | > Its possible to mix different interfaces in same project, e.g., provide a context using Lit integration and consume with the generic mixin or vice versa
22 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: About
3 | ---
4 |
5 | # wc-context
6 |
7 | wc-context is a Javascript library that implements a way to share data between Web Components (in fact, between any HTMLElement). While is inspired by React Context, hence the name, it is designed to fit the most common usage patterns in Web Components ecosystem.
8 |
9 | ## Features
10 |
11 | ### Small and fast
12 |
13 | The code base is small and fast, just a thin layer on top of native events. No Internet Explorer support, avoiding meaningless extra code.
14 |
15 | ### Flexible and comprehensive
16 |
17 | It is possible to define context providers and consumers in many ways: Lit integration, Web Component mixin, Reactive Controllers, Dedicated elements and core API. All compatible with each other, allowing to mix and match in same project.
18 |
19 | Also, does not limit how can be used:
20 |
21 | - Ability to provide or consume one or more contexts per element
22 | - Context can be provided or consumed by any HTML element
23 | - Context can be identified by string or unique identifier
24 |
25 | ### Compatible
26 |
27 | Conforms with the [Context protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md)
28 |
29 | ### Works with Shadow DOM
30 |
31 | The context can ben consumed in any level of child node tree regardless of shadow dom boundaries.
32 |
33 | It also handles the timing issue when consuming a context inside a slotted content
34 |
35 | ### Easy to implement unit tests
36 |
37 | Most of the time there's no need of special handling to write unit tests. Just set the property that should receive the context value.
38 |
39 | When the context is deep in the node tree or are not linked to a property use Lit `contextProvider` directive or the [core API](./testing.md#context-consumed-with-controllerdedicated-elements)
40 |
41 | ### Lazy loading context data
42 |
43 | With `ContextProvider` ([Reactive Controller](https://lit.dev/docs/composition/controllers/)) is possible to implement [data lazy loading](./controllers.md#subclassing-contextprovider).
44 |
45 | ### Well tested
46 |
47 | 100% code coverage
48 |
49 | ## License
50 |
51 | MIT
52 |
53 | Copyright © 2018 - present Luiz Américo Pereira Câmara aka blikblum
54 |
--------------------------------------------------------------------------------
/docs/lit-integration.md:
--------------------------------------------------------------------------------
1 | # Lit Integration
2 |
3 | wc-context provides out of box [Lit](https://lit.dev/) integration.
4 |
5 | Live example: [version 1](https://codesandbox.io/s/8n89qz95q2) /
6 | [version 2](https://codesandbox.io/s/wq6jyo3jvw)
7 |
8 | ## Class mixin
9 |
10 | The `withContext` class mixin / decorator augments Lit components allowing to connect reactive properties to contexts as a consumer or a provider.
11 |
12 | > The Lit class mixin is exported by 'wc-context/lit.js'. Do not confuse with the generic class mixin exported by 'wc-context/mixin.js'.
13 |
14 | ### Providing a context
15 |
16 | To provide a context add `providedContext` to the property declaration. Changes to the property are reflected in the related context.
17 |
18 | ```javascript
19 | import { withContext } from 'wc-context/lit.js'
20 | import { LitElement } from 'lit'
21 |
22 | class ThemeTitleProvider extends withContext(LitElement) {
23 | static properties = {
24 | appTheme: { type: String, providedContext: 'theme' },
25 | activeTitle: { type: String, providedContext: 'title' },
26 | }
27 |
28 | toggleTheme() {
29 | this.appTheme = 'newtheme'
30 | }
31 |
32 | toggleTitle() {
33 | this.activeTitle = 'New title'
34 | }
35 | }
36 | ```
37 |
38 | #### Consuming a context
39 |
40 | To consume a context add `context` to the property declaration. When the related context value changes, the property is updated triggering the component reactivity.
41 |
42 | ```javascript
43 | import { withContext } from 'wc-context/lit.js'
44 | import { LitElement, html } from 'lit'
45 |
46 | class Consumer extends withContext(LitElement) {
47 | static properties = {
48 | theme: { type: String, context: 'theme' },
49 | titleProp: { type: String, context: 'title' },
50 | }
51 |
52 | render() {
53 | return html`Theme is ${this.theme}, title is ${this.titleProp}
`
54 | }
55 | }
56 | ```
57 |
58 | > The property name does not need to match the context name / id
59 |
60 | ## Directive
61 |
62 | The `contextProvider` [directive](https://lit.dev/docs/templates/custom-directives/) defines a context provider linked to the element in which is declared. It accepts as parameters the context name / id and the value.
63 |
64 | > This directive, updates the context value when the component render method is called. Since Lit renders asynchronously, is possible to have a delay between setting the context provider value and the consumers being notified.
65 |
66 | ```javascript
67 | import { contextProvider } from 'wc-context/lit.js'
68 | import { LitElement, html } from 'lit'
69 |
70 | class Consumer extends withContext(LitElement) {
71 | static properties = {
72 | theme: { type: String },
73 | }
74 |
75 | render() {
76 | return html`
77 |
78 |
79 |
80 |
81 |
82 |
83 | `
84 | }
85 | }
86 | ```
87 |
--------------------------------------------------------------------------------
/docs/storybook.md:
--------------------------------------------------------------------------------
1 | # Storybook
2 |
3 | Demoing components that uses contexts is not different from standard ones in most cases.
4 |
5 | ## Context consumed with class mixins
6 |
7 | If the context is consumed using class mixins, i.e., reflected in a property, just pass the value using `args`:
8 |
9 | ```js
10 | export default {
11 | title: 'Components/MyComponent',
12 | component: 'my-component',
13 | // my-component consumes a context reflected in foo property
14 | args: {
15 | foo: 'bar',
16 | },
17 | }
18 |
19 | export const Default = {
20 | args: {},
21 | }
22 | ```
23 |
24 | ## Context consumed with controller/dedicated elements
25 |
26 | For contexts of child components, consumed with `ContextConsumer` controller or `context-consumer` element, is necessary to provide the contexts in the root component. This can be accomplished using a [custom render function](https://storybook.js.org/docs/web-components/api/csf#custom-render-functions) and same techniques described in [testing guide](./testing.md#context-consumed-with-controllerdedicated-elements).
27 |
28 | While it works, it makes harder to create stories. Using wc-context low level API is possible to create a story decorator that accepts a `contexts` hash parameter and provides the respective contexts:
29 |
30 | ```js
31 | import { registerContext } from 'wc-context'
32 |
33 | export function withContexts = (story, context) => {
34 | const el = story()
35 |
36 | const contexts = context.parameters.contexts
37 | if (contexts) {
38 | for (const [context, value] of Object.entries(contexts)) {
39 | registerContext(el, context, value)
40 | }
41 | }
42 |
43 | return el
44 | }
45 | ```
46 |
47 | To use, register the decorator for each story or globally in storybook 'preview.js'
48 |
49 | ```js
50 | import { withContexts } from './withContexts.js'
51 |
52 | export default {
53 | title: 'Components/MyComponent',
54 | component: 'my-component',
55 | decorators: [withContexts],
56 | // my-component consumes the "foo" context using controller or has a child element that consumes it
57 | parameters: {
58 | contexts: {
59 | foo: 'bar'
60 | }
61 | },
62 | }
63 |
64 | export const Default = {
65 | args: {},
66 | }
67 |
68 | ```
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | Components that use contexts can be tested with minimal, if none, adaptations when compared with standard ones.
4 |
5 | ## TLDR
6 |
7 | Check [Vitest](https://vitest.dev/) and [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/) test suites at [testing example folder](https://github.com/blikblum/wc-context/tree/master/examples/testing). The library itself is [tested](https://github.com/blikblum/wc-context/tree/master/test) with [Jest](https://jestjs.io/).
8 |
9 | > The examples use @open-wc/testing that provides time saving utilities
10 |
11 | ## Context consumed with class mixins
12 |
13 | Testing a component with a context consumed using the class mixins (both Lit or generic) is no different of testing a simple property.
14 |
15 | Component to be tested definition
16 |
17 | ```js
18 | import { LitElement } from 'lit'
19 | import { withContext } from 'wc-context/lit.js'
20 |
21 | class MyEl extends withContext(LitElement) {
22 | static properties = {
23 | foo: { type: String, context: 'fooContext' },
24 | }
25 | }
26 |
27 | customElements.define('my-el', MyEl)
28 | ```
29 |
30 | Test code
31 |
32 | ```js
33 | import { html, fixture } from '@open-wc/testing'
34 |
35 | it('test foo property that consumes a context', async () => {
36 | const el = await fixture(html` `)
37 | expect(el.foo).to.equal('bar')
38 | })
39 | ```
40 |
41 | ## Context consumed with controller/dedicated elements
42 |
43 | Testing a component whose context value is not reflected in a property, e.g., when using `ContextConsumer` controller, requires that the context be provided explicitly.
44 |
45 | Component to be tested definition
46 |
47 | ```js
48 | import { LitElement } from 'lit'
49 | import { ContextConsumer } from 'wc-context/controllers.js'
50 |
51 | class MyEl extends LitElement {
52 | fooContextConsumer = new ContextConsumer(this, 'fooContext')
53 |
54 | render() {
55 | return html`fooContext: ${this.fooContextConsumer.value}
`
56 | }
57 | }
58 |
59 | customElements.define('my-el', MyEl)
60 | ```
61 |
62 | Provide context with lit provider directive
63 |
64 | ```js
65 | import { html, fixture } from '@open-wc/testing'
66 | import { contextProvider } from 'wc-context/lit.js'
67 |
68 | it('test foo property that consumes a context', async () => {
69 | const el = await fixture(
70 | html` `
71 | )
72 | expect(el).shadowDom.to.equal('fooContext: bar
')
73 | })
74 | ```
75 |
76 | Provide context using `registerContext` in a parent node
77 |
78 | ```js
79 | import { expect, fixture, html } from '@open-wc/testing'
80 | import { registerContext } from 'wc-context'
81 |
82 | function createContextNode(contexts) {
83 | const el = document.createElement('div')
84 | for (const [context, value] of Object.entries(contexts)) {
85 | registerContext(el, context, value)
86 | }
87 | return el
88 | }
89 |
90 | it('test foo property that consumes a context', async () => {
91 | const el = await fixture(html` `, {
92 | parentNode: createContextNode({ fooContext: 'bar' }),
93 | })
94 | expect(el).shadowDom.to.equal('fooContext: bar
')
95 | })
96 | ```
97 |
--------------------------------------------------------------------------------
/examples/dedicated-elements/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Context through custom elements
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/dedicated-elements/my-app.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit'
2 | import { createContext } from 'wc-context'
3 | import 'wc-context/context-provider.js'
4 | import 'wc-context/context-consumer.js'
5 |
6 | const numCtx = createContext('num') // context could be also a string
7 |
8 | class MyApp extends LitElement {
9 | static properties = {
10 | providedNumber: {},
11 | consumedNumber: {},
12 | }
13 |
14 | toggleNumberClick() {
15 | this.providedNumber = Math.random()
16 | }
17 |
18 | numContextUpdate(e) {
19 | console.log(
20 | `Context value updated | context: ${e.context} | value: ${e.value}`
21 | )
22 | this.consumedNumber = e.value
23 | }
24 |
25 | render() {
26 | return html`
27 | Toggle provided number
28 |
29 |
30 | Provided number: ${this.providedNumber}
31 |
32 |
36 |
Consumed number ${this.consumedNumber}
37 |
38 | `
39 | }
40 | }
41 |
42 | customElements.define('my-app', MyApp)
43 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | wc-context demos
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/lazy-data/cat-facts.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit'
2 | import { ContextConsumer } from 'wc-context/controllers.js'
3 |
4 | // no need to use withContext mixin when using reactive controller
5 | class CatFacts extends LitElement {
6 | // using controller
7 | catFactContext = new ContextConsumer(this, 'catFact')
8 |
9 | render() {
10 | if (!this.catFactContext.value) {
11 | return html`No fact defined. Wait 10 seconds
`
12 | }
13 |
14 | const { error, data, loading } = this.catFactContext.value
15 |
16 | if (error) {
17 | return html`Error: ${error}
`
18 | }
19 |
20 | if (loading) {
21 | return html`Loading ...
`
22 | }
23 |
24 | return html`Fact: ${data.fact}
`
25 | }
26 | }
27 |
28 | customElements.define('cat-facts', CatFacts)
29 |
--------------------------------------------------------------------------------
/examples/lazy-data/dataProvider.js:
--------------------------------------------------------------------------------
1 | import { ContextProvider } from 'wc-context/controllers.js'
2 | import { observeData } from './dataService.js'
3 |
4 | export class DataProvider extends ContextProvider {
5 | initialize() {
6 | this.unsubscribe = observeData((value) => {
7 | this.value = value
8 | })
9 | }
10 |
11 | finalize() {
12 | if (this.unsubscribe) {
13 | this.unsubscribe()
14 | this.unsubscribe = undefined
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/examples/lazy-data/dataService.js:
--------------------------------------------------------------------------------
1 | // from https://codingwithspike.wordpress.com/2018/03/10/making-settimeout-an-async-await-function/
2 | async function wait(ms) {
3 | return new Promise((resolve) => {
4 | setTimeout(resolve, ms)
5 | })
6 | }
7 |
8 | let nextIsError = false
9 |
10 | export function queueError() {
11 | nextIsError = true
12 | }
13 |
14 | // simulate an realtime API
15 | export function observeData(subscriber) {
16 | console.log('starting to observe data')
17 | const intervalId = setInterval(async () => {
18 | subscriber({ data: {}, error: null, loading: true })
19 | await wait(600)
20 | if (nextIsError) {
21 | nextIsError = false
22 | subscriber({ data: {}, error: 'An arbitray error', loading: false })
23 | return
24 | }
25 | try {
26 | const response = await fetch('https://catfact.ninja/fact')
27 | const data = await response.json()
28 | subscriber({ data, error: null, loading: false })
29 | } catch (error) {
30 | subscriber({ data: {}, error, loading: false })
31 | }
32 | }, 10000)
33 |
34 | subscriber(undefined)
35 |
36 | return function () {
37 | console.log('ending observe data')
38 | clearInterval(intervalId)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/lazy-data/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Lazy context initialization demo
9 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/lazy-data/my-app.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit'
2 | import { DataProvider } from './dataProvider.js'
3 | import './cat-facts.js'
4 | import { queueError } from './dataService.js'
5 |
6 | class MyApp extends LitElement {
7 | dataProvider = new DataProvider(this, 'catFact')
8 |
9 | addCatFactsClick(e) {
10 | e.preventDefault()
11 | this.renderRoot.appendChild(document.createElement('cat-facts'))
12 | }
13 |
14 | queueErrorClick(e) {
15 | e.preventDefault()
16 | queueError()
17 | }
18 |
19 | render() {
20 | return html`
21 | Add cat facts element Queue error `
23 | }
24 | }
25 |
26 | customElements.define('my-app', MyApp)
27 |
--------------------------------------------------------------------------------
/examples/lit-light-dom/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | LitElement Context Demo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/lit-light-dom/src/context-example.js:
--------------------------------------------------------------------------------
1 | import { css, html, LitElement } from 'lit'
2 | import { withContext, contextProvider } from 'wc-context/lit.js'
3 | import { ContextProvider } from 'wc-context/controllers.js'
4 |
5 | import './theme-provider.js'
6 | import './theme-consumer.js'
7 | import './title-theme-consumer.js'
8 |
9 | const alternateThemes = ['blue', 'yellow', 'red']
10 |
11 | class ContextExample extends withContext(LitElement) {
12 | static styles = [
13 | css`
14 | .subtitle {
15 | margin-top: 8px;
16 | }
17 | `,
18 | ]
19 |
20 | titleProvider = new ContextProvider(this, 'title', 'one title')
21 |
22 | toggleTitle() {
23 | const currentTitle = this.titleProvider.value
24 | this.titleProvider.value =
25 | currentTitle === 'one title' ? 'another title' : 'one title'
26 | }
27 |
28 | render() {
29 | return html`
30 |
31 |
32 |
33 |
34 |
35 | Nested providers (the closest provider is used)
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Consume two contexts (one provided by Lit property other by
44 | controller)
45 |
46 |
47 | Toggle title
48 |
49 |
50 |
Using directive
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | `
59 | }
60 | }
61 |
62 | customElements.define('context-example', ContextExample)
63 |
--------------------------------------------------------------------------------
/examples/lit-light-dom/src/styles.js:
--------------------------------------------------------------------------------
1 | const styles = {
2 | dark: 'background-color: black; color: white;',
3 | light: 'background-color: white; color: black;',
4 | blue: 'background-color: blue; color: white;',
5 | yellow: 'background-color: yellow; color: black;',
6 | red: 'background-color: red; color: white;',
7 | }
8 |
9 | export { styles }
10 |
--------------------------------------------------------------------------------
/examples/lit-light-dom/src/theme-consumer.js:
--------------------------------------------------------------------------------
1 | import { html, LitElement } from 'lit'
2 | import { withContext } from 'wc-context/lit.js'
3 | import { styles } from './styles.js'
4 |
5 | class ThemeConsumer extends withContext(LitElement) {
6 | static properties = {
7 | theme: { type: String, context: 'theme' },
8 | }
9 |
10 | contextChangedCallback(name, oldValue, value) {
11 | console.log(
12 | this.constructor.name,
13 | `context "${name}" changed from "${oldValue}" to "${value}"`
14 | )
15 | }
16 |
17 | render() {
18 | return html`${this.theme}
`
19 | }
20 | }
21 |
22 | customElements.define('theme-consumer', ThemeConsumer)
23 |
--------------------------------------------------------------------------------
/examples/lit-light-dom/src/theme-provider.js:
--------------------------------------------------------------------------------
1 | import { html, LitElement } from 'lit'
2 | import { withContext } from 'wc-context/lit.js'
3 |
4 | class ThemeProvider extends withContext(LitElement) {
5 | static properties = {
6 | themes: { type: Array },
7 | theme: { type: String, providedContext: 'theme' },
8 | }
9 |
10 | constructor() {
11 | super()
12 | this.themes = ['light', 'dark']
13 | }
14 |
15 | toggleTheme() {
16 | let newIndex = this.themes.indexOf(this.theme) + 1
17 | if (newIndex >= this.themes.length) {
18 | newIndex = 0
19 | }
20 | this.theme = this.themes[newIndex]
21 | }
22 |
23 | willUpdate(changed) {
24 | if (changed.has('themes')) {
25 | this.toggleTheme()
26 | }
27 | }
28 |
29 | render() {
30 | return html`
31 | Toggle theme
32 |
33 | `
34 | }
35 | }
36 |
37 | customElements.define('theme-provider', ThemeProvider)
38 |
--------------------------------------------------------------------------------
/examples/lit-light-dom/src/title-theme-consumer.js:
--------------------------------------------------------------------------------
1 | import { html, LitElement } from 'lit'
2 | import { withContext } from 'wc-context/lit.js'
3 | import { styles } from './styles.js'
4 |
5 | class TitleThemeConsumer extends withContext(LitElement) {
6 | static properties = {
7 | title: { context: 'title' },
8 | theme: { context: 'theme' },
9 | }
10 |
11 | contextChangedCallback(name, oldValue, value) {
12 | console.log(
13 | this.constructor.name,
14 | `context "${name}" changed from "${oldValue}" to "${value}"`
15 | )
16 | }
17 |
18 | render() {
19 | return html`
20 | ${this.title}
21 | ${this.theme}
22 | `
23 | }
24 | }
25 |
26 | customElements.define('titletheme-consumer', TitleThemeConsumer)
27 |
--------------------------------------------------------------------------------
/examples/lit-shadow-dom/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Provider in Shadow DOM / Consumer in Light DOM
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/lit-shadow-dom/src/shadow-dom-example.js:
--------------------------------------------------------------------------------
1 | import { html, LitElement, css } from 'lit'
2 | import { withContext } from 'wc-context/lit.js'
3 |
4 | import './theme-switcher.js'
5 | import './theme-consumer.js'
6 |
7 | const alternateThemes = ['blue', 'yellow', 'red']
8 |
9 | class ShadowDomExample extends withContext(LitElement) {
10 | static get styles() {
11 | return [
12 | css`
13 | .margin-top {
14 | margin-top: 8px;
15 | }
16 | `,
17 | ]
18 | }
19 |
20 | render() {
21 | return html`
22 |
23 |
24 | Provider in shadow dom and consumer in light dom
25 |
26 |
27 | This poses some challenge because the consumer is connected to the DOM
28 | earlier thus requesting the context before is available / provider
29 | element is connected
30 |
31 |
32 |
33 |
34 |
35 |
Nested providers
36 |
37 |
38 |
39 |
40 |
41 |
42 | `
43 | }
44 | }
45 |
46 | customElements.define('shadow-dom-example', ShadowDomExample)
47 |
--------------------------------------------------------------------------------
/examples/lit-shadow-dom/src/styles.js:
--------------------------------------------------------------------------------
1 | const styles = {
2 | dark: 'background-color: black; color: white;',
3 | light: 'background-color: white; color: black;',
4 | blue: 'background-color: blue; color: white;',
5 | yellow: 'background-color: yellow; color: black;',
6 | red: 'background-color: red; color: white;',
7 | }
8 |
9 | export { styles }
10 |
--------------------------------------------------------------------------------
/examples/lit-shadow-dom/src/theme-consumer.js:
--------------------------------------------------------------------------------
1 | import { html, LitElement } from 'lit'
2 | import { withContext } from 'wc-context/lit.js'
3 | import { styles } from './styles.js'
4 |
5 | class ThemeConsumer extends withContext(LitElement) {
6 | static observedContexts = ['theme']
7 |
8 | contextChangedCallback(name, oldValue, value) {
9 | console.log(
10 | this.constructor.name,
11 | `(${this.id}) `,
12 | `context "${name}" changed from "${oldValue}" to "${value}"`
13 | )
14 | this.requestUpdate()
15 | }
16 |
17 | render() {
18 | return html`${this.theme}
`
19 | }
20 | }
21 |
22 | customElements.define('theme-consumer', ThemeConsumer)
23 |
--------------------------------------------------------------------------------
/examples/lit-shadow-dom/src/theme-switcher.js:
--------------------------------------------------------------------------------
1 | import { html, LitElement } from 'lit'
2 |
3 | import 'wc-context/context-provider.js'
4 |
5 | class ThemeSwitcher extends LitElement {
6 | static properties = {
7 | themes: { type: Array },
8 | theme: { type: String, providedContext: 'theme' },
9 | }
10 |
11 | constructor() {
12 | super()
13 | this.themes = ['light', 'dark']
14 | }
15 |
16 | toggleTheme() {
17 | let newIndex = this.themes.indexOf(this.theme) + 1
18 | if (newIndex >= this.themes.length) {
19 | newIndex = 0
20 | }
21 | this.theme = this.themes[newIndex]
22 | }
23 |
24 | willUpdate(changed) {
25 | if (changed.has('themes')) {
26 | this.toggleTheme()
27 | }
28 | }
29 |
30 | render() {
31 | return html`
32 |
33 | Toggle theme
34 |
35 |
36 | `
37 | }
38 | }
39 |
40 | customElements.define('theme-switcher', ThemeSwitcher)
41 |
--------------------------------------------------------------------------------
/examples/testing/test-controller.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit'
2 | import { ContextConsumer } from 'wc-context/controllers.js'
3 |
4 | class TestController extends LitElement {
5 | fooContextConsumer = new ContextConsumer(this, 'fooContext')
6 |
7 | render() {
8 | return html`fooContext: ${this.fooContextConsumer.value}
`
9 | }
10 | }
11 |
12 | customElements.define('test-controller', TestController)
--------------------------------------------------------------------------------
/examples/testing/test-property.js:
--------------------------------------------------------------------------------
1 | import { LitElement } from 'lit'
2 | import { withContext } from 'wc-context/lit.js'
3 |
4 | class TestProperty extends withContext(LitElement) {
5 | static properties = {
6 | foo: { type: String, context: 'fooContext' },
7 | }
8 | }
9 |
10 | customElements.define('test-property', TestProperty)
--------------------------------------------------------------------------------
/examples/testing/vitest.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'vitest'
2 | import { html, fixture, expect } from '@open-wc/testing'
3 | import { contextProvider } from 'wc-context/lit.js'
4 | import { registerContext } from 'wc-context'
5 |
6 | import './test-controller.js'
7 | import './test-property.js'
8 |
9 | function createContextNode(contexts) {
10 | const el = document.createElement('div')
11 | for (const [context, value] of Object.entries(contexts)) {
12 | registerContext(el, context, value)
13 | }
14 | return el
15 | }
16 |
17 | describe('test-property', () => {
18 | it('set property', async () => {
19 | const el = await fixture(html`
20 |
21 | `)
22 |
23 | expect(el.foo).to.equal('bar')
24 | })
25 |
26 | it('parentNode', async () => {
27 | const el = await fixture(html` `, {
28 | parentNode: createContextNode({ fooContext: 'bar' }),
29 | })
30 |
31 | expect(el.foo).to.equal('bar')
32 | })
33 |
34 | it('directive', async () => {
35 | const el = await fixture(html`
36 |
37 | `)
38 |
39 | expect(el.foo).to.equal('bar')
40 | })
41 | })
42 |
43 | describe('test-controller', () => {
44 | it('parentNode', async () => {
45 | const el = await fixture(html` `, {
46 | parentNode: createContextNode({ fooContext: 'bar' }),
47 | })
48 |
49 | expect(el).shadowDom.to.equal('fooContext: bar
')
50 | })
51 |
52 | it('directive', async () => {
53 | const el = await fixture(html`
54 |
57 | `)
58 |
59 | expect(el).shadowDom.to.equal('fooContext: bar
')
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/examples/testing/wtr.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect } from '@open-wc/testing'
2 | import { contextProvider } from 'wc-context/lit.js'
3 | import { registerContext } from 'wc-context'
4 |
5 | import './test-controller.js'
6 | import './test-property.js'
7 |
8 | function createContextNode(contexts) {
9 | const el = document.createElement('div')
10 | for (const [context, value] of Object.entries(contexts)) {
11 | registerContext(el, context, value)
12 | }
13 | return el
14 | }
15 |
16 | describe('test-property', () => {
17 | it('set property', async () => {
18 | const el = await fixture(html`
19 |
20 | `)
21 |
22 | expect(el.foo).to.equal('bar')
23 | })
24 |
25 | it('parentNode', async () => {
26 | const el = await fixture(html` `, {
27 | parentNode: createContextNode({ fooContext: 'bar' }),
28 | })
29 |
30 | expect(el.foo).to.equal('bar')
31 | })
32 |
33 | it('directive', async () => {
34 | const el = await fixture(html`
35 |
36 | `)
37 |
38 | expect(el.foo).to.equal('bar')
39 | })
40 | })
41 |
42 | describe('test-controller', () => {
43 | it('parentNode', async () => {
44 | const el = await fixture(html` `, {
45 | parentNode: createContextNode({ fooContext: 'bar' }),
46 | })
47 |
48 | expect(el).shadowDom.to.equal('fooContext: bar
')
49 | })
50 |
51 | it('directive', async () => {
52 | const el = await fixture(html`
53 |
56 | `)
57 |
58 | expect(el).shadowDom.to.equal('fooContext: bar
')
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/lit.js:
--------------------------------------------------------------------------------
1 | import { noChange } from 'lit'
2 | import { Directive, directive } from 'lit/directive.js'
3 |
4 | import {
5 | observeContext,
6 | unobserveContext,
7 | registerContext,
8 | updateContext,
9 | createContext,
10 | } from './core.js'
11 |
12 | /**
13 | * @typedef { import('./core.js').Context } Context
14 | * @typedef { import('lit').ElementPart } ElementPart
15 | **/
16 |
17 | function getFromProperty(provider, property) {
18 | return provider[property]
19 | }
20 |
21 | /**
22 | * @template {typeof HTMLElement } BaseClass
23 | * @param {BaseClass} Base - Base element class
24 | * @returns {BaseClass}
25 | */
26 |
27 | function withContext(Base) {
28 | return class extends Base {
29 | static getPropertyDescriptor(name, key, options) {
30 | const defaultDescriptor = super.getPropertyDescriptor(name, key, options)
31 |
32 | if (!options.providedContext) return defaultDescriptor
33 |
34 | const setter = defaultDescriptor.set
35 | return {
36 | get: defaultDescriptor.get,
37 | set(value) {
38 | setter.call(this, value)
39 |
40 | updateContext(this, options.providedContext)
41 | },
42 | configurable: true,
43 | enumerable: true,
44 | }
45 | }
46 |
47 | static finalize() {
48 | const result = super.finalize()
49 |
50 | // const observedContexts = this.observedContexts || (this.observedContexts = [])
51 |
52 | const contexts = []
53 |
54 | this.elementProperties.forEach(({ context }, name) => {
55 | // todo: build also providedContext ?
56 | if (context) {
57 | contexts.push([context, name])
58 | }
59 | })
60 |
61 | if (contexts.length) {
62 | const observedContexts =
63 | this.observedContexts || (this.observedContexts = [])
64 | observedContexts.push(...contexts)
65 | }
66 |
67 | return result
68 | }
69 |
70 | constructor() {
71 | super()
72 | this.constructor.elementProperties.forEach(
73 | ({ providedContext }, name) => {
74 | if (providedContext) {
75 | registerContext(this, providedContext, name, getFromProperty)
76 | }
77 | }
78 | )
79 | }
80 |
81 | connectedCallback() {
82 | super.connectedCallback()
83 | const observedContexts = this.constructor.observedContexts
84 | if (observedContexts) {
85 | observedContexts.forEach((context) => {
86 | if (Array.isArray(context)) {
87 | observeContext(this, context[0], context[1])
88 | } else {
89 | observeContext(this, context)
90 | }
91 | })
92 | }
93 | }
94 |
95 | disconnectedCallback() {
96 | super.disconnectedCallback()
97 | const observedContexts = this.constructor.observedContexts
98 | if (observedContexts) {
99 | observedContexts.forEach((context) => {
100 | if (Array.isArray(context)) {
101 | unobserveContext(this, context[0])
102 | } else {
103 | unobserveContext(this, context)
104 | }
105 | })
106 | }
107 | }
108 | }
109 | }
110 |
111 | class ContextProviderDirective extends Directive {
112 | /**
113 | * @type {string | Context}
114 | */
115 | context
116 |
117 | /**
118 | * @type {any}
119 | * @memberof ContextProviderDirective
120 | */
121 | value
122 |
123 | /**
124 | * @param {ElementPart} part
125 | * @param {[Context | string, *]} [context, value] directive arguments
126 | * @return {*}
127 | * @memberof ContextProviderDirective
128 | */
129 | update(part, [context, value]) {
130 | if (!this.context) {
131 | registerContext(part.element, context, value)
132 | this.context = context
133 | } else if (this.value !== value) {
134 | updateContext(part.element, this.context, value)
135 | }
136 | this.value = value
137 | return noChange
138 | }
139 | }
140 |
141 | const contextProvider = directive(ContextProviderDirective)
142 |
143 | export { withContext, contextProvider, createContext }
144 |
--------------------------------------------------------------------------------
/mixin.js:
--------------------------------------------------------------------------------
1 | import {
2 | observeContext,
3 | unobserveContext,
4 | registerContext,
5 | updateContext,
6 | createContext,
7 | } from './core.js'
8 |
9 | function getWithConfig(provider, config) {
10 | const property = typeof config === 'string' ? config : config.property
11 | if (property) return provider[property]
12 | return config.value
13 | }
14 |
15 | /**
16 | * @template {typeof HTMLElement } BaseClass
17 | * @param {BaseClass} Base - Base element class
18 | * @returns {BaseClass}
19 | */
20 |
21 | const withContext = (Base) => {
22 | return class extends Base {
23 | constructor() {
24 | super()
25 | const providedContexts = this.constructor.providedContexts
26 | if (providedContexts) {
27 | Object.keys(providedContexts).forEach((name) => {
28 | const config = providedContexts[name]
29 | registerContext(this, name, config, getWithConfig)
30 | })
31 | }
32 | }
33 |
34 | updateContext(name, value) {
35 | const providedContexts = this.constructor.providedContexts
36 | if (providedContexts) {
37 | const config = providedContexts[name]
38 | const property = typeof config === 'string' ? config : config.property
39 | updateContext(this, name, property || { value })
40 | }
41 | }
42 |
43 | connectedCallback() {
44 | super.connectedCallback && super.connectedCallback()
45 | const observedContexts = this.constructor.observedContexts
46 | if (observedContexts) {
47 | observedContexts.forEach((context) => {
48 | if (Array.isArray(context)) {
49 | observeContext(this, context[0], context[1])
50 | } else {
51 | observeContext(this, context)
52 | }
53 | })
54 | }
55 | }
56 |
57 | disconnectedCallback() {
58 | super.disconnectedCallback && super.disconnectedCallback()
59 | const observedContexts = this.constructor.observedContexts
60 | if (observedContexts) {
61 | observedContexts.forEach((context) => {
62 | if (Array.isArray(context)) {
63 | unobserveContext(this, context[0])
64 | } else {
65 | unobserveContext(this, context)
66 | }
67 | })
68 | }
69 | }
70 | }
71 | }
72 |
73 | export { withContext, createContext }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wc-context",
3 | "version": "1.0.0",
4 | "description": "Context for HTML custom elements / web components",
5 | "repository": "blikblum/wc-context",
6 | "author": "Luiz Américo Pereira Câmara",
7 | "contributors": [
8 | "Luiz Américo Pereira Câmara"
9 | ],
10 | "license": "MIT",
11 | "keywords": [
12 | "webcomponent",
13 | "custom-element",
14 | "context",
15 | "lit-element",
16 | "lit"
17 | ],
18 | "type": "module",
19 | "main": "core.js",
20 | "files": [
21 | "context-provider.js",
22 | "context-consumer.js",
23 | "core.js",
24 | "lit.js",
25 | "controllers.js",
26 | "mixin.js",
27 | "types"
28 | ],
29 | "jest": {
30 | "modulePathIgnorePatterns": [
31 | "/dist/",
32 | "/examples/"
33 | ],
34 | "testEnvironment": "jsdom"
35 | },
36 | "devDependencies": {
37 | "@esm-bundle/chai": "4.3.4-fix.0",
38 | "@open-wc/testing": "^3.2.0",
39 | "@types/istanbul-lib-report": "^3.0.0",
40 | "@types/istanbul-reports": "^3.0.1",
41 | "@types/jsdom": "^21.1.1",
42 | "@types/node": "^20.4.8",
43 | "@web/dev-server": "^0.3.0",
44 | "@web/test-runner": "^0.17.0",
45 | "@web/test-runner-puppeteer": "^0.14.0",
46 | "eslint": "^8.46.0",
47 | "eslint-config-prettier": "^9.0.0",
48 | "jest": "^29.6.2",
49 | "jest-environment-jsdom": "^29.6.2",
50 | "jsdom": "^22.1.0",
51 | "lit": "^2.8.0",
52 | "prettier": "^2.8.8",
53 | "typescript": "^5.1.6",
54 | "vite": "^4.4.9",
55 | "vitepress": "^1.0.0-rc.25",
56 | "vitest": "beta",
57 | "vue": "^3.3.4"
58 | },
59 | "exports": {
60 | ".": {
61 | "types": "./types/core.d.ts",
62 | "default": "./core.js"
63 | },
64 | "./mixin.js": {
65 | "types": "./types/mixin.d.ts",
66 | "default": "./mixin.js"
67 | },
68 | "./lit.js": {
69 | "types": "./types/lit.d.ts",
70 | "default": "./lit.js"
71 | },
72 | "./controllers.js": {
73 | "types": "./types/controllers.d.ts",
74 | "default": "./controllers.js"
75 | },
76 | "./context-consumer.js": {
77 | "types": "./types/context-consumer.d.ts",
78 | "default": "./context-consumer.js"
79 | },
80 | "./context-provider.js": {
81 | "types": "./types/context-provider.d.ts",
82 | "default": "./context-provider.js"
83 | }
84 | },
85 | "scripts": {
86 | "docs:dev": "vitepress dev docs",
87 | "docs:build": "vitepress build docs",
88 | "docs:preview": "vitepress preview docs",
89 | "format": "prettier lit-element.js lit.js wc-context.js core.js test --write",
90 | "lint": "eslint lit-element.js lit.js wc-context.js core.js test",
91 | "start": "web-dev-server --open examples/ --node-resolve",
92 | "start:watch": "web-dev-server --open examples/ --node-resolve --watch",
93 | "test": "yarn node --experimental-vm-modules $(yarn bin jest)",
94 | "test-example:wtr": "web-test-runner examples/testing/wtr.test.js --node-resolve --puppeteer",
95 | "test-example:vitest": "vitest vitest.test.js --run --environment jsdom",
96 | "types": "tsc --project tsconfig.types.json"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/retype.yml:
--------------------------------------------------------------------------------
1 | input: docs
2 | output: .retype
3 | url: # Add your website address here
4 | branding:
5 | title: wc-context
6 | label: Docs
7 | links:
8 | - text: Getting Started
9 | link: https://retype.com/guides/getting-started/
10 | footer:
11 | copyright: '© Copyright Luiz Américo Pereira Câmara {{ year }}. All rights reserved.'
12 |
--------------------------------------------------------------------------------
/test/consumer.spec.js:
--------------------------------------------------------------------------------
1 | import { jest } from '@jest/globals'
2 | import { registerContext, updateContext } from 'wc-context/core.js'
3 | import { ContextConsumer } from 'wc-context/controllers.js'
4 | import { LitElement } from 'lit'
5 |
6 | const ComponentWithConsumer = class extends LitElement {}
7 |
8 | customElements.define('lit-controller-consumer', ComponentWithConsumer)
9 |
10 | describe('ContextConsumer', () => {
11 | let grandfatherEl
12 | let parentEl
13 |
14 | beforeEach(() => {
15 | document.body.innerHTML = `
16 |
26 | `
27 | grandfatherEl = document.getElementById('grandfather')
28 | parentEl = document.getElementById('parent')
29 | })
30 |
31 | it('should consume parent context when connected', () => {
32 | registerContext(grandfatherEl, 'key', 'value')
33 | const component = new ComponentWithConsumer()
34 | const ctxConsumer = new ContextConsumer(component, 'key')
35 | parentEl.appendChild(component)
36 | expect(ctxConsumer.value).toBe('value')
37 | })
38 |
39 | it('should update its value when context changes', () => {
40 | registerContext(grandfatherEl, 'key', 'value')
41 | const component = new ComponentWithConsumer()
42 | const ctxConsumer = new ContextConsumer(component, 'key')
43 | parentEl.appendChild(component)
44 | updateContext(grandfatherEl, 'key', 'value2')
45 | expect(ctxConsumer.value).toBe('value2')
46 | })
47 |
48 | it('should call callback param when context changes', async () => {
49 | registerContext(grandfatherEl, 'key', 'value')
50 | const component = new ComponentWithConsumer()
51 | const callback = jest.fn()
52 | new ContextConsumer(component, 'key', callback)
53 | parentEl.appendChild(component)
54 | expect(callback).toHaveBeenCalledTimes(1)
55 | expect(callback).toHaveBeenCalledWith('value')
56 | callback.mockClear()
57 | await component.updateComplete
58 | updateContext(grandfatherEl, 'key', 'value2')
59 | expect(callback).toHaveBeenCalledTimes(1)
60 | expect(callback).toHaveBeenCalledWith('value2')
61 | })
62 |
63 | it('should call host requestUpdate when context changes and callback is ommited', async () => {
64 | registerContext(grandfatherEl, 'key', 'value')
65 | const component = new ComponentWithConsumer()
66 | new ContextConsumer(component, 'key')
67 | parentEl.appendChild(component)
68 | await component.updateComplete
69 | const updateFn = jest.spyOn(component, 'requestUpdate')
70 | updateContext(grandfatherEl, 'key', 'value2')
71 | expect(updateFn).toHaveBeenCalledTimes(1)
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/test/context-consumer.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { jest } from '@jest/globals'
3 | import { registerContext, updateContext } from '../core'
4 |
5 | import '../context-consumer.js'
6 |
7 | describe('context-consumer', () => {
8 | let grandfatherEl
9 | let grandfather2El
10 | let parentEl
11 | let childEl
12 |
13 | beforeEach(() => {
14 | document.body.innerHTML = `
15 |
30 | `
31 | grandfatherEl = document.getElementById('grandfather')
32 | grandfather2El = document.getElementById('grandfather2')
33 | parentEl = document.getElementById('parent')
34 | childEl = document.getElementById('child')
35 | })
36 |
37 | describe('when is a child of a context provider', () => {
38 | beforeEach(() => {
39 | registerContext(grandfatherEl, 'key', 'value')
40 | })
41 |
42 | test('should be acessible in all children nodes', () => {
43 | expect(parentEl.value).toBe('value')
44 | expect(childEl.value).toBe('value')
45 | })
46 |
47 | test('should not be acessible in sibling nodes', () => {
48 | expect(grandfather2El.value).toBeUndefined()
49 | })
50 |
51 | test('should update its value when context value is updated', () => {
52 | updateContext(grandfatherEl, 'key', 'value2')
53 | expect(parentEl.value).toBe('value2')
54 | expect(childEl.value).toBe('value2')
55 | })
56 |
57 | test('should trigger context-update event when context value is updated', () => {
58 | const spy = jest.fn()
59 | function callback(e) {
60 | spy(e.context, e.value, e.target.id)
61 | }
62 | grandfatherEl.addEventListener('context-update', callback)
63 | updateContext(grandfatherEl, 'key', 'value2')
64 | expect(spy).toHaveBeenCalledTimes(2)
65 | expect(spy).toHaveBeenCalledWith('key', 'value2', 'parent')
66 | expect(spy).toHaveBeenCalledWith('key', 'value2', 'child')
67 | })
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/test/context-provider.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { jest } from '@jest/globals'
3 | import {
4 | registerContext,
5 | observeContext,
6 | unobserveContext,
7 | createContext,
8 | } from 'wc-context'
9 |
10 | import '../context-provider.js'
11 |
12 | describe('context-provider', () => {
13 | let rootEl
14 | let grandfatherEl
15 | let grandfather2El
16 | let grandfather3El
17 | let parentEl
18 | let childEl
19 | let child3El
20 | let child4El
21 |
22 | beforeEach(() => {
23 | document.body.innerHTML = `
24 |
25 |
26 |
29 |
32 |
33 |
34 |
37 |
38 |
43 |
44 | `
45 | rootEl = document.getElementById('root')
46 | grandfatherEl = document.getElementById('grandfather')
47 | grandfather2El = document.getElementById('grandfather2')
48 | grandfather3El = document.getElementById('grandfather3')
49 | parentEl = document.getElementById('parent')
50 | childEl = document.getElementById('child')
51 | child3El = document.getElementById('child3')
52 | child4El = document.getElementById('child4')
53 | })
54 |
55 | describe('when using a string as context key', () => {
56 | beforeEach(() => {
57 | grandfatherEl.context = 'key'
58 | grandfatherEl.value = 'value'
59 | })
60 |
61 | test('should be acessible in all children nodes', () => {
62 | observeContext(parentEl, 'key')
63 | observeContext(childEl, 'key')
64 | expect(parentEl.key).toBe('value')
65 | expect(childEl.key).toBe('value')
66 | })
67 |
68 | test('should return context value in context property', () => {
69 | expect(grandfatherEl.context).toBe('key')
70 | })
71 |
72 | test('should not be acessible in parent nodes', () => {
73 | observeContext(rootEl, 'key')
74 | expect(rootEl.key).toBeUndefined()
75 | })
76 |
77 | test('should not be acessible in sibling nodes', () => {
78 | observeContext(grandfather2El, 'key')
79 | observeContext(child3El, 'key')
80 | expect(grandfather3El.key).toBeUndefined()
81 | expect(child4El.key).toBeUndefined()
82 | })
83 |
84 | test('should return same value when called repeatedly', () => {
85 | observeContext(childEl, 'key')
86 | expect(childEl.key).toBe('value')
87 | expect(childEl.key).toBe('value')
88 | expect(childEl.key).toBe('value')
89 | })
90 |
91 | it('should allow to update the context using value property', () => {
92 | observeContext(parentEl, 'key')
93 | grandfatherEl.value = 'value2'
94 | expect(parentEl.key).toBe('value2')
95 | })
96 |
97 | it('should not allow to change the context key', () => {
98 | grandfatherEl.context = 'newKey'
99 | observeContext(parentEl, 'key')
100 | expect(parentEl.key).toBe('value')
101 | observeContext(childEl, 'newKey')
102 | expect(childEl.key).toBeUndefined()
103 | })
104 |
105 | describe('and have a sibling component ', () => {
106 | test('should keep independent values', () => {
107 | observeContext(childEl, 'key')
108 | observeContext(child3El, 'key')
109 | expect(childEl.key).toBe('value')
110 | expect(child3El.key).toBe('value2')
111 | })
112 | })
113 |
114 | describe('and context is updated after unobserved', () => {
115 | beforeEach(() => {
116 | observeContext(parentEl, 'key')
117 | unobserveContext(parentEl, 'key')
118 | parentEl.key = 'none'
119 | grandfatherEl.value = 'value2'
120 | })
121 | it('should not update the observer context value', () => {
122 | expect(parentEl.key).toBe('none')
123 | })
124 | })
125 |
126 | describe('and observed by a child node after context is updated', () => {
127 | beforeEach(() => {
128 | grandfatherEl.value = 'value2'
129 | observeContext(parentEl, 'key')
130 | })
131 | it('should provide updated value', () => {
132 | expect(parentEl.key).toBe('value2')
133 | })
134 | })
135 |
136 | describe('and observed by a child node', () => {
137 | let callback
138 | beforeEach(() => {
139 | callback = jest.fn()
140 | childEl.contextChangedCallback = callback
141 | observeContext(childEl, 'key')
142 | })
143 |
144 | test('should call contextChangedCallback in the observer', () => {
145 | expect(callback).toHaveBeenCalledTimes(1)
146 | expect(callback).toHaveBeenCalledWith('key', undefined, 'value')
147 | })
148 |
149 | test('should call contextChangedCallback when context is updated', () => {
150 | callback.mockClear()
151 | grandfatherEl.value = 'value2'
152 | expect(callback).toHaveBeenCalledTimes(1)
153 | expect(callback).toHaveBeenCalledWith('key', 'value', 'value2')
154 | })
155 |
156 | test('should not call contextChangedCallback when context is updated with same value', () => {
157 | callback.mockClear()
158 | grandfatherEl.value = 'value'
159 | expect(callback).not.toHaveBeenCalled()
160 | })
161 | })
162 | })
163 |
164 | describe('when using identifier returned by createContext as key', () => {
165 | let ctx
166 | beforeEach(() => {
167 | ctx = createContext('myContext')
168 | grandfatherEl.context = ctx
169 | grandfatherEl.value = 'value'
170 | })
171 |
172 | test('should be acessible in all children nodes', () => {
173 | observeContext(parentEl, ctx, 'key')
174 | observeContext(childEl, ctx, 'key')
175 | expect(parentEl.key).toBe('value')
176 | expect(childEl.key).toBe('value')
177 | })
178 |
179 | test('should not be acessible in parent nodes', () => {
180 | observeContext(rootEl, ctx, 'key')
181 |
182 | expect(rootEl.key).toBeUndefined()
183 | })
184 |
185 | test('should not be acessible in sibling nodes', () => {
186 | observeContext(grandfather2El, ctx, 'key')
187 |
188 | observeContext(child3El, ctx, 'key')
189 | expect(grandfather2El.key).toBeUndefined()
190 | expect(child3El.key).toBeUndefined()
191 | })
192 |
193 | describe('and registered to a child node with different identifier', () => {
194 | let ctx2
195 | beforeEach(() => {
196 | ctx2 = createContext('myContext')
197 | registerContext(parentEl, ctx2, 'value2')
198 | })
199 |
200 | test('should not override parent context', () => {
201 | observeContext(childEl, ctx, 'key')
202 | expect(childEl.key).toBe('value')
203 | })
204 | })
205 | })
206 |
207 | describe('when context key is not defined', () => {
208 | beforeEach(() => {
209 | grandfatherEl.value = 'value'
210 | })
211 |
212 | it('should not update the context', () => {
213 | observeContext(parentEl, 'key')
214 | observeContext(childEl, 'key')
215 | expect(parentEl.key).toBeUndefined()
216 | expect(childEl.key).toBeUndefined()
217 | })
218 | })
219 | })
220 |
--------------------------------------------------------------------------------
/test/core.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { jest } from '@jest/globals'
3 | import {
4 | noContext,
5 | ContextRequestEvent,
6 | registerContext,
7 | observeContext,
8 | unobserveContext,
9 | updateContext,
10 | createContext,
11 | onContextObserve,
12 | onContextUnobserve,
13 | getContext,
14 | } from 'wc-context'
15 |
16 | describe('context', () => {
17 | let rootEl
18 | let grandfatherEl
19 | let grandfather2El
20 | let parentEl
21 | let childEl
22 | let child3El
23 |
24 | beforeEach(() => {
25 | document.body.innerHTML = `
26 |
41 | `
42 | rootEl = document.getElementById('root')
43 | grandfatherEl = document.getElementById('grandfather')
44 | grandfather2El = document.getElementById('grandfather2')
45 | parentEl = document.getElementById('parent')
46 | childEl = document.getElementById('child')
47 | child3El = document.getElementById('child3')
48 | })
49 |
50 | describe('when registered in a node', () => {
51 | beforeEach(() => {
52 | registerContext(grandfatherEl, 'key', 'value')
53 | })
54 |
55 | test('should be acessible in all children nodes', () => {
56 | observeContext(parentEl, 'key')
57 | observeContext(childEl, 'key')
58 | expect(parentEl.key).toBe('value')
59 | expect(childEl.key).toBe('value')
60 | })
61 |
62 | test('should not be acessible in parent nodes', () => {
63 | observeContext(rootEl, 'key')
64 | expect(rootEl.key).toBeUndefined()
65 | })
66 |
67 | test('should not be acessible in sibling nodes', () => {
68 | observeContext(grandfather2El, 'key')
69 | observeContext(child3El, 'key')
70 | expect(grandfather2El.key).toBeUndefined()
71 | expect(child3El.key).toBeUndefined()
72 | })
73 |
74 | test('should return same value when called repeatedly', () => {
75 | observeContext(childEl, 'key')
76 | expect(childEl.key).toBe('value')
77 | expect(childEl.key).toBe('value')
78 | expect(childEl.key).toBe('value')
79 | })
80 |
81 | test('should allow to configure how context value is set', () => {
82 | function setElProperty(el, value, arg) {
83 | el[arg] = value
84 | }
85 |
86 | observeContext(childEl, 'key', 'otherProp', setElProperty)
87 | expect(childEl.otherProp).toBe('value')
88 | })
89 |
90 | test('should not crash when unobserving a not observed context', () => {
91 | unobserveContext(childEl, 'key')
92 | })
93 |
94 | test('should throw when trying to update a not registered context', () => {
95 | expect(() => {
96 | updateContext(grandfatherEl, 'key2')
97 | }).toThrow('updateContext: "key2" is not registered')
98 | expect(() => {
99 | const ctx = createContext('hello')
100 | updateContext(grandfatherEl, ctx)
101 | }).toThrow('updateContext: "hello" is not registered')
102 | })
103 |
104 | test('should trigger "context-request" event when observing context', () => {
105 | const listener = jest.fn((event) => {
106 | expect(event.subscribe).toBe(true)
107 | if (event.context === 'manualKey') {
108 | event.callback('manualValue')
109 | }
110 | })
111 | grandfatherEl.addEventListener('context-request', listener, {
112 | once: true,
113 | })
114 |
115 | observeContext(parentEl, 'manualKey')
116 | expect(listener).toHaveBeenCalledTimes(1)
117 | expect(parentEl.manualKey).toBe('manualValue')
118 | })
119 |
120 | test('should handle "context-request" event without subscribe', () => {
121 | const callback = jest.fn((value) => {
122 | expect(value).toBe('value')
123 | })
124 | const event = new ContextRequestEvent('key', callback)
125 | parentEl.dispatchEvent(event)
126 | expect(callback).toHaveBeenCalledTimes(1)
127 | updateContext(grandfatherEl, 'key', 'newValue')
128 | expect(callback).toHaveBeenCalledTimes(1)
129 | })
130 |
131 | test('should handle "context-request" event with subscribe - unsubscribe called in first request', () => {
132 | const callback = jest.fn((value, unsubscribe) => {
133 | expect(value).toBe('value')
134 | unsubscribe()
135 | })
136 | const event = new ContextRequestEvent('key', callback, true)
137 | parentEl.dispatchEvent(event)
138 | expect(callback).toHaveBeenCalledTimes(1)
139 | updateContext(grandfatherEl, 'key', 'newValue')
140 | expect(callback).toHaveBeenCalledTimes(1)
141 | })
142 |
143 | test('should handle "context-request" event with subscribe - unsubscribe called subsequent request', () => {
144 | const callback = jest
145 | .fn()
146 | .mockImplementationOnce((value) => {
147 | expect(value).toBe('value')
148 | })
149 | .mockImplementation((value, unsubscribe) => {
150 | expect(value).toBe('newValue')
151 | unsubscribe()
152 | })
153 | const event = new ContextRequestEvent('key', callback, true)
154 | parentEl.dispatchEvent(event)
155 | expect(callback).toHaveBeenCalledTimes(1)
156 | updateContext(grandfatherEl, 'key', 'newValue')
157 | expect(callback).toHaveBeenCalledTimes(2)
158 | updateContext(grandfatherEl, 'key', 'yetAnotherValue')
159 | expect(callback).toHaveBeenCalledTimes(2)
160 | })
161 |
162 | test('should allow to call unsubscribe more than once', () => {
163 | const callback = jest.fn((value, unsubscribe) => {
164 | unsubscribe()
165 | unsubscribe()
166 | unsubscribe()
167 | })
168 |
169 | const event = new ContextRequestEvent('key', callback, true)
170 | parentEl.dispatchEvent(event)
171 | expect(callback).toHaveBeenCalledTimes(1)
172 | updateContext(grandfatherEl, 'key', 'newValue')
173 | expect(callback).toHaveBeenCalledTimes(1)
174 | })
175 |
176 | test('should respond to getContext call', async () => {
177 | const value = await getContext(childEl, 'key')
178 | expect(value).toBe('value')
179 | })
180 |
181 | describe('and registered to a child node', () => {
182 | beforeEach(() => {
183 | registerContext(parentEl, 'key', 'value2')
184 | })
185 |
186 | test('should override parent context', () => {
187 | observeContext(childEl, 'key')
188 | expect(childEl.key).toBe('value2')
189 | })
190 | })
191 |
192 | describe('and registered to a child node with different key', () => {
193 | beforeEach(() => {
194 | registerContext(parentEl, 'key2', 'value2')
195 | })
196 |
197 | test('should not override parent context', () => {
198 | observeContext(childEl, 'key')
199 | expect(childEl.key).toBe('value')
200 | })
201 | })
202 |
203 | describe('and registered to a sibling node', () => {
204 | beforeEach(() => {
205 | registerContext(grandfather2El, 'key', 'value2')
206 | })
207 |
208 | test('should keep independent values', () => {
209 | observeContext(childEl, 'key')
210 | observeContext(child3El, 'key')
211 | expect(childEl.key).toBe('value')
212 | expect(child3El.key).toBe('value2')
213 | })
214 | })
215 |
216 | describe('and context is updated', () => {
217 | beforeEach(() => {
218 | observeContext(parentEl, 'key')
219 | updateContext(grandfatherEl, 'key', 'value2')
220 | })
221 | it('should update the observer context value', () => {
222 | expect(parentEl.key).toBe('value2')
223 | })
224 | })
225 |
226 | describe('and context is updated after unobserved', () => {
227 | beforeEach(() => {
228 | observeContext(parentEl, 'key')
229 | unobserveContext(parentEl, 'key')
230 | parentEl.key = 'none'
231 | updateContext(grandfatherEl, 'key', 'value2')
232 | })
233 | it('should not update the observer context value', () => {
234 | expect(parentEl.key).toBe('none')
235 | })
236 | })
237 |
238 | describe('and observed by a child node after context is updated', () => {
239 | beforeEach(() => {
240 | updateContext(grandfatherEl, 'key', 'value2')
241 | observeContext(parentEl, 'key')
242 | })
243 | it('should provide updated value', () => {
244 | expect(parentEl.key).toBe('value2')
245 | })
246 | })
247 |
248 | describe('and observed by a child node', () => {
249 | let callback
250 | beforeEach(() => {
251 | callback = jest.fn()
252 | childEl.contextChangedCallback = callback
253 | observeContext(childEl, 'key')
254 | })
255 |
256 | test('should call contextChangedCallback in the observer', () => {
257 | expect(callback).toHaveBeenCalledTimes(1)
258 | expect(callback).toHaveBeenCalledWith('key', undefined, 'value')
259 | })
260 |
261 | test('should call contextChangedCallback when context is updated', () => {
262 | callback.mockClear()
263 | updateContext(grandfatherEl, 'key', 'value2')
264 | expect(callback).toHaveBeenCalledTimes(1)
265 | expect(callback).toHaveBeenCalledWith('key', 'value', 'value2')
266 | })
267 |
268 | test('should not call contextChangedCallback when context is updated with same value', () => {
269 | callback.mockClear()
270 | updateContext(grandfatherEl, 'key', 'value')
271 | expect(callback).not.toHaveBeenCalled()
272 | })
273 | })
274 |
275 | describe('and a observe listener is registered with onContextObserve', () => {
276 | let callback
277 | beforeEach(() => {
278 | callback = jest.fn()
279 |
280 | onContextObserve(grandfatherEl, 'key', callback)
281 | })
282 |
283 | test('should call listener callback when a context is observed', () => {
284 | observeContext(childEl, 'key')
285 | expect(callback).toHaveBeenCalledTimes(1)
286 | expect(callback).toHaveBeenCalledWith({ count: 1 })
287 | })
288 |
289 | test('should not call listener callback when a context is unobserved', () => {
290 | observeContext(childEl, 'key')
291 | callback.mockClear()
292 | unobserveContext(childEl, 'key')
293 | expect(callback).toHaveBeenCalledTimes(0)
294 | })
295 | })
296 |
297 | describe('and a unobserve listener is registered with onContextUnobserve', () => {
298 | let callback
299 | beforeEach(() => {
300 | callback = jest.fn()
301 |
302 | onContextUnobserve(grandfatherEl, 'key', callback)
303 | })
304 |
305 | test('should not call listener callback when a context is observed', () => {
306 | observeContext(childEl, 'key')
307 | expect(callback).toHaveBeenCalledTimes(0)
308 | })
309 |
310 | test('should call listener callback when a context is unobserved', () => {
311 | observeContext(childEl, 'key')
312 | callback.mockClear()
313 | unobserveContext(childEl, 'key')
314 | expect(callback).toHaveBeenCalledTimes(1)
315 | expect(callback).toHaveBeenCalledWith({ count: 0 })
316 | })
317 | })
318 | })
319 |
320 | describe('when registered in a node with noContext', () => {
321 | beforeEach(() => {
322 | registerContext(grandfatherEl, 'key', noContext)
323 | })
324 |
325 | test('should not call "context-request" callback while context value === noContext (no subscribe)', () => {
326 | const callback = jest.fn((value) => {
327 | expect(value).toBe('value')
328 | })
329 | const event = new ContextRequestEvent('key', callback)
330 | parentEl.dispatchEvent(event)
331 | expect(callback).toHaveBeenCalledTimes(0)
332 | updateContext(grandfatherEl, 'key', noContext)
333 | expect(callback).toHaveBeenCalledTimes(0)
334 | updateContext(grandfatherEl, 'key', 'value')
335 | expect(callback).toHaveBeenCalledTimes(1)
336 | updateContext(grandfatherEl, 'key', 'newValue')
337 | expect(callback).toHaveBeenCalledTimes(1)
338 | })
339 |
340 | test('should not call "context-request" callback while context value === noContext (subscribe)', () => {
341 | const callback = jest.fn().mockImplementation((value) => {
342 | expect(value).toBe('value')
343 | })
344 |
345 | const event = new ContextRequestEvent('key', callback, true)
346 | parentEl.dispatchEvent(event)
347 | expect(callback).toHaveBeenCalledTimes(0)
348 | updateContext(grandfatherEl, 'key', noContext)
349 | expect(callback).toHaveBeenCalledTimes(0)
350 | updateContext(grandfatherEl, 'key', 'value')
351 | expect(callback).toHaveBeenCalledTimes(1)
352 | })
353 |
354 | test('should resolve getContext when context value is updated', async () => {
355 | const contextResolved = jest.fn()
356 |
357 | const promise = new Promise((resolve) => {
358 | getContext(childEl, 'key').then((value) => {
359 | contextResolved()
360 | expect(value).toBe('value')
361 | resolve()
362 | })
363 | })
364 |
365 | updateContext(grandfatherEl, 'key', noContext)
366 | await new Promise((resolve) => {
367 | setTimeout(resolve, 1)
368 | })
369 |
370 | expect(contextResolved).toHaveBeenCalledTimes(0)
371 |
372 | updateContext(grandfatherEl, 'key', 'value')
373 |
374 | await new Promise((resolve) => {
375 | setTimeout(resolve, 1)
376 | })
377 |
378 | expect(contextResolved).toHaveBeenCalledTimes(1)
379 | return promise
380 | })
381 | })
382 |
383 | describe('when registered in a node with a custom getter', () => {
384 | let getter
385 | beforeEach(() => {
386 | getter = jest.fn()
387 | registerContext(grandfatherEl, 'key', 'value', getter)
388 | })
389 |
390 | test('should call the getter with payload when observed by a child', () => {
391 | expect(getter).toHaveBeenCalledTimes(0)
392 | observeContext(parentEl, 'key')
393 | expect(getter).toHaveBeenCalledTimes(1)
394 | expect(getter).toHaveBeenCalledWith(grandfatherEl, 'value')
395 | })
396 |
397 | test('should call the getter with payload when context is updated', () => {
398 | expect(getter).toHaveBeenCalledTimes(0)
399 | observeContext(parentEl, 'key')
400 | expect(getter).toHaveBeenCalledTimes(1)
401 | updateContext(grandfatherEl, 'key', 'value2')
402 | expect(getter).toHaveBeenCalledTimes(2)
403 | expect(getter).toHaveBeenCalledWith(grandfatherEl, 'value')
404 | })
405 |
406 | test('should update the consumer with the value returned by getter', () => {
407 | getter.mockReturnValue(1)
408 | observeContext(parentEl, 'key')
409 | expect(parentEl.key).toBe(1)
410 | })
411 | })
412 |
413 | describe('when registered in a node with identifier returned by createContext', () => {
414 | let ctx
415 | beforeEach(() => {
416 | ctx = createContext('myContext')
417 | registerContext(grandfatherEl, ctx, 'value')
418 | })
419 |
420 | test('should be acessible in all children nodes', () => {
421 | observeContext(parentEl, ctx, 'key')
422 | observeContext(childEl, ctx, 'key')
423 | expect(parentEl.key).toBe('value')
424 | expect(childEl.key).toBe('value')
425 | })
426 |
427 | test('should not be acessible in parent nodes', () => {
428 | observeContext(rootEl, ctx, 'key')
429 |
430 | expect(rootEl.key).toBeUndefined()
431 | })
432 |
433 | test('should not be acessible in sibling nodes', () => {
434 | observeContext(grandfather2El, ctx, 'key')
435 |
436 | observeContext(child3El, ctx, 'key')
437 | expect(grandfather2El.key).toBeUndefined()
438 | expect(child3El.key).toBeUndefined()
439 | })
440 |
441 | describe('and registered to a child node with different identifier', () => {
442 | let ctx2
443 | beforeEach(() => {
444 | ctx2 = createContext('myContext')
445 | registerContext(parentEl, ctx2, 'value2')
446 | })
447 |
448 | test('should not override parent context', () => {
449 | observeContext(childEl, ctx, 'key')
450 | expect(childEl.key).toBe('value')
451 | })
452 | })
453 | })
454 |
455 | describe('when registered to a node after a child try to observe', () => {
456 | let callback
457 | beforeEach(() => {
458 | callback = jest.fn()
459 | parentEl.contextChangedCallback = callback
460 |
461 | observeContext(child3El, 'key')
462 | observeContext(child3El, 'otherKey')
463 | observeContext(parentEl, 'key')
464 | registerContext(grandfatherEl, 'key', 'value')
465 | registerContext(grandfather2El, 'otherKey', 'otherValue')
466 | })
467 |
468 | test('should call contextChangedCallback in the observer', () => {
469 | expect(callback).toHaveBeenCalledTimes(1)
470 | expect(callback).toHaveBeenCalledWith('key', undefined, 'value')
471 | })
472 |
473 | test('should update the observer context', () => {
474 | expect(parentEl.key).toBe('value')
475 | expect(child3El.otherKey).toBe('otherValue')
476 | })
477 |
478 | test('should not update the observer context of not children', () => {
479 | expect(child3El.key).toBeUndefined
480 | })
481 |
482 | describe('and context is updated after unobserved', () => {
483 | beforeEach(() => {
484 | unobserveContext(parentEl, 'key')
485 | parentEl.key = 'none'
486 | updateContext(grandfatherEl, 'key', 'value2')
487 | })
488 | it('should not update the observer context value', () => {
489 | expect(parentEl.key).toBe('none')
490 | })
491 | })
492 | })
493 | })
494 |
--------------------------------------------------------------------------------
/test/lit.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { withContext, contextProvider } from 'wc-context/lit'
3 | import { LitElement, html, render } from 'lit'
4 |
5 | const Component = withContext(LitElement)
6 |
7 | class ProviderComponent extends Component {
8 | static properties = {
9 | myProp: { providedContext: 'propertyContext' },
10 | myOtherProp: { providedContext: 'otherContext' },
11 | }
12 |
13 | constructor() {
14 | super()
15 | this.myProp = 'test'
16 | this.myOtherProp = 'xxx'
17 | }
18 | }
19 |
20 | class ConsumerComponent extends Component {
21 | static properties = {
22 | myProp: { context: 'otherContext' },
23 | }
24 |
25 | static observedContexts = ['propertyContext']
26 | }
27 |
28 | class ConsumerOnlyDecoratorComponent extends Component {
29 | static properties = {
30 | myProp: { context: 'otherContext' },
31 | }
32 | }
33 |
34 | class NestedComponent extends Component {
35 | static properties = {
36 | myProp: { providedContext: 'propertyContext' },
37 | }
38 |
39 | constructor() {
40 | super()
41 | this.myProp = 'test'
42 | }
43 |
44 | get consumerEl() {
45 | return this.renderRoot.querySelector('lit-consumer')
46 | }
47 |
48 | render() {
49 | return html` `
50 | }
51 | }
52 |
53 | customElements.define('lit-component', Component)
54 | customElements.define('lit-provider', ProviderComponent)
55 | customElements.define('lit-consumer', ConsumerComponent)
56 | customElements.define('lit-consumer2', ConsumerOnlyDecoratorComponent)
57 | customElements.define('lit-nested', NestedComponent)
58 |
59 | // unable to create custom elements with jsdom
60 | describe('withContext', () => {
61 | let grandfatherEl
62 | let parentEl
63 | let parent3El
64 | beforeEach(() => {
65 | document.body.innerHTML = `
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | `
76 | grandfatherEl = document.getElementById('grandfather')
77 | parentEl = document.getElementById('parent')
78 | parent3El = document.getElementById('parent3')
79 | })
80 |
81 | describe('with providedContext property declaration', () => {
82 | test('should provide contexts to child element', async () => {
83 | expect(parentEl.propertyContext).toBe('test')
84 | expect(parentEl.myProp).toBe('xxx')
85 | expect(parent3El.myProp).toBe('xxx')
86 | })
87 |
88 | test('should provide contexts to child element inside shadow dom', async () => {
89 | const nestedEl = document.querySelector('lit-nested')
90 | await nestedEl.updateComplete
91 | expect(nestedEl.consumerEl.propertyContext).toBe('test')
92 | })
93 |
94 | test('should update contexts in child element when updating providedContext property', async () => {
95 | grandfatherEl.myProp = 2
96 | grandfatherEl.myOtherProp = 'zzz'
97 | expect(parentEl.propertyContext).toBe(2)
98 | expect(parentEl.myProp).toBe('zzz')
99 | })
100 | })
101 | })
102 |
103 | function contextProviderTemplate(value) {
104 | return html``
108 | }
109 |
110 | describe('contextProvider', () => {
111 | it('should provide context to child nodes', () => {
112 | const litRoot = document.getElementById('lit-root')
113 | render(contextProviderTemplate(2), litRoot)
114 | const consumer = litRoot.querySelector('lit-consumer')
115 | expect(consumer.propertyContext).toBe(2)
116 | expect(consumer.myProp).toBe(undefined)
117 |
118 | // update with same value
119 | render(contextProviderTemplate(2), litRoot)
120 | expect(consumer.propertyContext).toBe(2)
121 |
122 | render(contextProviderTemplate(10), litRoot)
123 | expect(consumer.propertyContext).toBe(10)
124 | })
125 | })
126 |
--------------------------------------------------------------------------------
/test/mixin.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import { LitElement } from 'lit'
3 | import { withContext } from 'wc-context/mixin.js'
4 |
5 | const Component = withContext(HTMLElement)
6 | const LitComponent = withContext(LitElement)
7 |
8 | class ProviderComponent extends Component {
9 | static providedContexts = {
10 | valueContext: { value: 'value' },
11 | propertyContext: { property: 'myProp' },
12 | shorthandContext: 'myOtherProp',
13 | }
14 |
15 | myProp = 'test'
16 | myOtherProp = 'xxx'
17 | }
18 |
19 | class ConsumerComponent extends Component {
20 | static observedContexts = [
21 | 'valueContext',
22 | 'propertyContext',
23 | 'shorthandContext',
24 | ['propertyContext', 'localPropertyContext'],
25 | ]
26 | }
27 |
28 | class LitConsumerComponent extends LitComponent {
29 | static observedContexts = [
30 | 'valueContext',
31 | 'propertyContext',
32 | 'shorthandContext',
33 | ['propertyContext', 'localPropertyContext'],
34 | ]
35 | }
36 |
37 | customElements.define('vanilla-component', Component)
38 | customElements.define('vanilla-provider', ProviderComponent)
39 | customElements.define('vanilla-consumer', ConsumerComponent)
40 | customElements.define('lit-vanilla-consumer', LitConsumerComponent)
41 |
42 | // unable to create custom elements with jsdom
43 | describe('withContext', () => {
44 | let grandfatherEl
45 | let parentEl
46 | beforeEach(() => {
47 | document.body.innerHTML = `
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | `
56 | grandfatherEl = document.getElementById('grandfather')
57 | parentEl = document.getElementById('parent')
58 | })
59 |
60 | test('should define a updateContext method in element', () => {
61 | const el = new Component()
62 | expect(el.updateContext).toBeInstanceOf(Function)
63 | })
64 |
65 | test('should not crash when updateContext is called in a element without provided context', () => {
66 | const el = new Component()
67 | el.updateContext('key', 'value')
68 | })
69 |
70 | describe('with providedContexts static property', () => {
71 | test('should provide contexts to child vanilla component', async () => {
72 | expect(parentEl.valueContext).toBe('value')
73 | expect(parentEl.propertyContext).toBe('test')
74 | expect(parentEl.shorthandContext).toBe('xxx')
75 | expect(parentEl.localPropertyContext).toBe('test')
76 | })
77 |
78 | test('should provide contexts to child LitElement component', async () => {
79 | const litEl = document.querySelector('lit-vanilla-consumer')
80 | expect(litEl.valueContext).toBe('value')
81 | expect(litEl.propertyContext).toBe('test')
82 | expect(litEl.shorthandContext).toBe('xxx')
83 | expect(litEl.localPropertyContext).toBe('test')
84 | })
85 |
86 | test('should update contexts in child element when calling updateContext', async () => {
87 | grandfatherEl.updateContext('valueContext', 1)
88 | expect(parentEl.valueContext).toBe(1)
89 | grandfatherEl.myOtherProp = 3
90 | grandfatherEl.updateContext('shorthandContext')
91 | expect(parentEl.shorthandContext).toBe(3)
92 | })
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/test/provider.spec.js:
--------------------------------------------------------------------------------
1 | import { jest } from '@jest/globals'
2 | import { observeContext } from 'wc-context/core.js'
3 | import { ContextProvider } from 'wc-context/controllers.js'
4 | import { LitElement } from 'lit'
5 |
6 | const ProviderComponent = class extends LitElement {
7 | ctxProvider = new ContextProvider(this, 'controllerContext', 7)
8 | }
9 |
10 | customElements.define('lit-controller-provider', ProviderComponent)
11 |
12 | describe('ContextProvider', () => {
13 | let rootEl
14 | let grandfatherEl
15 | let parentEl
16 | let childEl
17 |
18 | beforeEach(() => {
19 | document.body.innerHTML = `
20 |
30 | `
31 | rootEl = document.getElementById('root')
32 | grandfatherEl = document.getElementById('grandfather')
33 | parentEl = document.getElementById('parent')
34 | childEl = document.getElementById('child')
35 | })
36 |
37 | it('should provide context for children with', () => {
38 | new ContextProvider(grandfatherEl, 'key', 'value')
39 | observeContext(parentEl, 'key')
40 | expect(parentEl.key).toBe('value')
41 | })
42 |
43 | it('should return initial value in value property', () => {
44 | const provider = new ContextProvider(grandfatherEl, 'key', 'value')
45 | expect(provider.value).toBe('value')
46 | })
47 |
48 | it('should update context when value property is set', () => {
49 | const provider = new ContextProvider(grandfatherEl, 'key', 'value')
50 | observeContext(parentEl, 'key')
51 | provider.value = 'value2'
52 | expect(parentEl.key).toBe('value2')
53 | })
54 |
55 | it('should call initialize once when context is first observed', () => {
56 | const initialize = jest.fn()
57 | class CustomProvider extends ContextProvider {
58 | initialize() {
59 | initialize()
60 | }
61 | }
62 | new CustomProvider(grandfatherEl, 'key', 'value')
63 | expect(initialize).toHaveBeenCalledTimes(0)
64 | observeContext(parentEl, 'key')
65 | expect(initialize).toHaveBeenCalledTimes(1)
66 | observeContext(childEl, 'key')
67 | expect(initialize).toHaveBeenCalledTimes(1)
68 | })
69 |
70 | it('should call finalize once when disconnect is called after initialized', () => {
71 | const finalize = jest.fn()
72 | class CustomProvider extends ContextProvider {
73 | finalize() {
74 | finalize()
75 | }
76 | }
77 | const provider = new CustomProvider(grandfatherEl, 'key', 'value')
78 | provider.disconnect()
79 | expect(finalize).toHaveBeenCalledTimes(0)
80 | observeContext(parentEl, 'key')
81 | expect(finalize).toHaveBeenCalledTimes(0)
82 | provider.disconnect()
83 | expect(finalize).toHaveBeenCalledTimes(1)
84 | provider.disconnect()
85 | expect(finalize).toHaveBeenCalledTimes(1)
86 | })
87 |
88 | it('should call initialize again after disconnect followed by connect are called', () => {
89 | const initialize = jest.fn()
90 | class CustomProvider extends ContextProvider {
91 | initialize() {
92 | initialize()
93 | }
94 | }
95 | const provider = new CustomProvider(grandfatherEl, 'key', 'value')
96 | observeContext(parentEl, 'key')
97 | expect(initialize).toHaveBeenCalledTimes(1)
98 | provider.disconnect()
99 | expect(initialize).toHaveBeenCalledTimes(1)
100 | provider.connect()
101 | expect(initialize).toHaveBeenCalledTimes(2)
102 | })
103 |
104 | it('should call connect and disconnect when ReactiveControllerHost element is connected / disconnected', () => {
105 | const el = document.createElement('lit-controller-provider')
106 | const controller = el.ctxProvider
107 | const connectFn = jest.spyOn(controller, 'connect')
108 | const disconnectFn = jest.spyOn(controller, 'disconnect')
109 | rootEl.appendChild(el)
110 | expect(connectFn).toHaveBeenCalledTimes(1)
111 | expect(disconnectFn).toHaveBeenCalledTimes(0)
112 | el.remove()
113 | expect(connectFn).toHaveBeenCalledTimes(1)
114 | expect(disconnectFn).toHaveBeenCalledTimes(1)
115 | })
116 | })
117 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | const uniqueMap = new Map()
2 |
3 | export function uniqueName(name) {
4 | let index = uniqueMap.get(name) || 0
5 | index++
6 | uniqueMap.set(name, index)
7 | return name + index
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.types.json:
--------------------------------------------------------------------------------
1 | {
2 | // Change this to match your project
3 | "include": [
4 | "*.js"
5 | ],
6 | "exclude": [
7 | "node_modules",
8 | "vite.config.js",
9 | "*.d.ts"
10 | ],
11 | "compilerOptions": {
12 | "target": "ES2017",
13 | "lib": [
14 | "es2020",
15 | "DOM"
16 | ],
17 | "module": "NodeNext",
18 | "outDir": "types",
19 | // Tells TypeScript to read JS files, as
20 | // normally they are ignored as source files
21 | "allowJs": true,
22 | // Generate d.ts files
23 | "declaration": true,
24 | // This compiler run should
25 | // only output d.ts files
26 | "emitDeclarationOnly": true,
27 | // go to js file when using IDE functions like
28 | // "Go to Definition" in VSCode
29 | "declarationMap": true
30 | }
31 | }
--------------------------------------------------------------------------------
/types/context-consumer.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | //# sourceMappingURL=context-consumer.d.ts.map
--------------------------------------------------------------------------------
/types/context-consumer.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"context-consumer.d.ts","sourceRoot":"","sources":["../context-consumer.js"],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/types/context-provider.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | //# sourceMappingURL=context-provider.d.ts.map
--------------------------------------------------------------------------------
/types/context-provider.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"context-provider.d.ts","sourceRoot":"","sources":["../context-provider.js"],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/types/controllers.d.ts:
--------------------------------------------------------------------------------
1 | export type Context = import('./core.js').Context;
2 | export type ContextConsumerCallback = (host: HTMLElement, value?: any) => void;
3 | /**
4 | * @callback ContextConsumerCallback
5 | * @param {HTMLElement} host
6 | * @param {*} [value]
7 | * @returns {void}
8 | */
9 | export class ContextConsumer {
10 | /**
11 | * Creates an instance of ContextProvider.
12 | * @param {HTMLElement} host
13 | * @param {string | Context} context Context identifier
14 | * @param {ContextConsumerCallback} callback
15 | */
16 | constructor(host: HTMLElement, context: string | Context, callback: ContextConsumerCallback);
17 | host: HTMLElement;
18 | context: any;
19 | callback: ContextConsumerCallback;
20 | _value: any;
21 | get value(): any;
22 | hostConnected(): void;
23 | hostDisconnected(): void;
24 | }
25 | export class ContextProvider {
26 | /**
27 | * Creates an instance of ContextProvider.
28 | * @param {HTMLElement} host
29 | * @param {string | Context} context Context identifier
30 | * @param {*} initialValue
31 | */
32 | constructor(host: HTMLElement, context: string | Context, initialValue: any);
33 | host: HTMLElement;
34 | context: any;
35 | _value: any;
36 | _initialized: boolean;
37 | _finalized: boolean;
38 | set value(arg: any);
39 | get value(): any;
40 | connect(): void;
41 | disconnect(): void;
42 | hostConnected(): void;
43 | hostDisconnected(): void;
44 | initialize(): void;
45 | finalize(): void;
46 | }
47 | //# sourceMappingURL=controllers.d.ts.map
--------------------------------------------------------------------------------
/types/controllers.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"controllers.d.ts","sourceRoot":"","sources":["../controllers.js"],"names":[],"mappings":"sBASc,OAAO,WAAW,EAAE,OAAO;6CAc9B,WAAW,kBAET,IAAI;AAJjB;;;;;GAKG;AAEH;IACE;;;;;OAKG;IACH,kBAJW,WAAW,WACX,MAAM,GAAG,OAAO,YAChB,uBAAuB,EAQjC;IAJC,kBAAgB;IAChB,aAAsB;IACtB,kCAAwB;IACxB,YAAuB;IAGzB,iBAEC;IAED,sBAEC;IAED,yBAEC;CACF;AAMD;IACE;;;;;OAKG;IACH,kBAJW,WAAW,WACX,MAAM,GAAG,OAAO,qBAmB1B;IAZC,kBAAgB;IAChB,aAAsB;IACtB,YAA0B;IAC1B,sBAAyB;IACzB,oBAAuB;IAczB,oBAGC;IAPD,iBAEC;IAOD,gBAMC;IAED,mBAMC;IAED,sBAEC;IAED,yBAEC;IAED,mBAAe;IAEf,iBAAa;CACd"}
--------------------------------------------------------------------------------
/types/core.d.ts:
--------------------------------------------------------------------------------
1 | export class ContextRequestEvent extends Event {
2 | constructor(context: any, callback: any, subscribe: any);
3 | context: any;
4 | callback: any;
5 | subscribe: any;
6 | }
7 | export type Context = any;
8 | export type ContextGetter = {
9 | /**
10 | * Function that is called in provider
11 | */
12 | getter: Function;
13 | /**
14 | * Payload passed to getter
15 | */
16 | payload?: any;
17 | };
18 | export const noContext: unique symbol;
19 | /**
20 | * @typedef {Object} Context
21 | */
22 | /**
23 | * @typedef {Object} ContextGetter
24 | * @property {Function} getter Function that is called in provider
25 | * @property {any} [payload] Payload passed to getter
26 | */
27 | /**
28 | * @param {string} key Identify the context
29 | * @return {Context}
30 | */
31 | export function createContext(key: string): Context;
32 | /**
33 | * @param {HTMLElement} provider HTMLElement acting as a context provider
34 | * @param {string | Context} context Context identifier
35 | * @param {*} payload Value passed to getter
36 | * @param {Function} [getter=providerGetter]
37 | */
38 | export function registerContext(provider: HTMLElement, context: string | Context, payload: any, getter?: Function): void;
39 | /**
40 | * @param {HTMLElement} provider HTMLElement that provides a context
41 | * @param {string | Context} context Context identifier
42 | * @param {*} [payload=context] Value passed to provider context getter
43 | */
44 | export function updateContext(provider: HTMLElement, context: string | Context, payload?: any): void;
45 | /**
46 | * @description Observes a context in a consumer. Optionally define how the context value is set
47 | * @param {HTMLElement} consumer HTMLElement that consumes a context
48 | * @param {string | Context} context Context identifier
49 | * @param {*} [payload=context] Value passed to setter
50 | * @param {Function} [setter=consumerSetter]
51 | */
52 | export function observeContext(consumer: HTMLElement, context: string | Context, payload?: any, setter?: Function): void;
53 | /**
54 | * @description Unobserves a context in a consumer
55 | * @param {HTMLElement} consumer HTMLElement that consumes a context
56 | * @param {string | Context} context Context identifier
57 | */
58 | export function unobserveContext(consumer: HTMLElement, context: string | Context): void;
59 | export function consumerSetter(consumer: any, value: any, name: any): void;
60 | /**
61 | * @description Default context getter implementation. Just returns the payload
62 | * @param {HTMLElement} provider HTMLElement acting as a context provider
63 | * @param {*} payload Options passed to the callback
64 | * @return {*}
65 | */
66 | export function providerGetter(provider: HTMLElement, payload: any): any;
67 | /**
68 | * @param {HTMLElement} provider
69 | * @param {string | Context} context Context identifier
70 | * @param {Function} callback
71 | */
72 | export function onContextObserve(provider: HTMLElement, context: string | Context, callback: Function): void;
73 | /**
74 | * @param {HTMLElement} provider
75 | * @param {string | Context} context Context identifier
76 | * @param {Function} callback
77 | */
78 | export function onContextUnobserve(provider: HTMLElement, context: string | Context, callback: Function): void;
79 | /**
80 | *
81 | *
82 | * @param {HTMLElement} consumer
83 | * @param {Context | string} context
84 | * @return {*}
85 | */
86 | export function getContext(consumer: HTMLElement, context: Context | string): any;
87 | //# sourceMappingURL=core.d.ts.map
--------------------------------------------------------------------------------
/types/core.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../core.js"],"names":[],"mappings":"AA0CA;IACE,yDASC;IAHC,aAAsB;IACtB,cAAwB;IACxB,eAA0B;CAE7B;;;;;;;;;;cAuBa,GAAG;;AA5EjB,sCAAqC;AAqErC;;GAEG;AAEH;;;;GAIG;AAEH;;;GAGG;AACH,mCAHW,MAAM,GACL,OAAO,CASlB;AAuBD;;;;;GAKG;AACH,0CALW,WAAW,WACX,MAAM,GAAG,OAAO,yCAwC1B;AAmBD;;;;GAIG;AACH,wCAJW,WAAW,WACX,MAAM,GAAG,OAAO,uBA0B1B;AA+BD;;;;;;GAMG;AACH,yCALW,WAAW,WACX,MAAM,GAAG,OAAO,0CAc1B;AAcD;;;;GAIG;AACH,2CAHW,WAAW,WACX,MAAM,GAAG,OAAO,QAa1B;AA5ED,2EAQC;AA7GD;;;;;GAKG;AACH,yCAJW,WAAW,qBAMrB;AA2KD;;;;GAIG;AACH,2CAJW,WAAW,WACX,MAAM,GAAG,OAAO,4BAY1B;AAED;;;;GAIG;AACH,6CAJW,WAAW,WACX,MAAM,GAAG,OAAO,4BAa1B;AAED;;;;;;GAMG;AACH,qCAJW,WAAW,WACX,OAAO,GAAG,MAAM,OAQ1B"}
--------------------------------------------------------------------------------
/types/lit.d.ts:
--------------------------------------------------------------------------------
1 | export type Context = import('./core.js').Context;
2 | export type ElementPart = import('lit').ElementPart;
3 | export type ClassDescriptor = import('@lit/reactive-element/decorators/base.js').ClassDescriptor;
4 | /**
5 | * @typedef {import('@lit/reactive-element/decorators/base.js').ClassDescriptor} ClassDescriptor
6 | */
7 | /**
8 | * @template {typeof HTMLElement } BaseClass
9 | * @param {BaseClass} classOrDescriptor - Base element class
10 | * @returns {BaseClass}
11 | */
12 | export function withContext(classOrDescriptor: BaseClass): BaseClass;
16 | export const contextProvider: (...values: unknown[]) => import("lit-html/directive.js").DirectiveResult;
17 | import { createContext } from './core.js';
18 | declare class ContextProviderDirective extends Directive {
19 | /**
20 | * @type {string | Context}
21 | */
22 | context: string | Context;
23 | /**
24 | * @type {any}
25 | * @memberof ContextProviderDirective
26 | */
27 | value: any;
28 | /**
29 | * @param {ElementPart} part
30 | * @param {[Context | string, *]} [context, value] directive arguments
31 | * @return {*}
32 | * @memberof ContextProviderDirective
33 | */
34 | update(part: ElementPart, [context, value]?: [Context | string, any]): any;
35 | }
36 | import { Directive } from 'lit/directive.js';
37 | export { createContext };
38 | //# sourceMappingURL=lit.d.ts.map
--------------------------------------------------------------------------------
/types/lit.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"lit.d.ts","sourceRoot":"","sources":["../lit.js"],"names":[],"mappings":"sBAYc,OAAO,WAAW,EAAE,OAAO;0BAC3B,OAAO,KAAK,EAAE,WAAW;8BA4F1B,OAAO,0CAA0C,EAAE,eAAe;AAD/E;;GAEG;AAIH;;;;GAIG;AAEH;;;4CAiBC;AA+BD,yIAA2D;8BA3JpD,WAAW;AA8HlB;IACE;;OAEG;IACH,SAFU,MAAM,GAAG,OAAO,CAEnB;IAEP;;;OAGG;IACH,OAHU,GAAG,CAGR;IAEL;;;;;OAKG;IACH,aALW,WAAW,qBACX,CAAC,OAAO,GAAG,MAAM,MAAI,OAY/B;CACF;0BAjKoC,kBAAkB"}
--------------------------------------------------------------------------------
/types/mixin.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @template {typeof HTMLElement } BaseClass
3 | * @param {BaseClass} Base - Base element class
4 | * @returns {BaseClass}
5 | */
6 | export function withContext(Base: BaseClass): BaseClass;
10 | import { createContext } from './core.js';
11 | export { createContext };
12 | //# sourceMappingURL=mixin.d.ts.map
--------------------------------------------------------------------------------
/types/mixin.d.ts.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"mixin.d.ts","sourceRoot":"","sources":["../mixin.js"],"names":[],"mappings":"AAcA;;;;GAIG;AAEH;;;+BAkDC;8BAhEM,WAAW"}
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import path from 'path'
3 |
4 | export default defineConfig({
5 | resolve: {
6 | alias: {
7 | // workaround to bug
8 | 'wc-context': path.resolve('.'),
9 | },
10 | },
11 | })
12 |
--------------------------------------------------------------------------------