├── .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` 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`` 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 | 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 | 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 | 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 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
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 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 | --------------------------------------------------------------------------------