├── .editorconfig ├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── package-lock.json ├── package.json ├── prettier.config.js ├── readme.md ├── src ├── components.d.ts ├── components │ ├── my-component-child │ │ └── my-component-child.tsx │ ├── my-component-grandchild │ │ └── my-component-grandchild.tsx │ ├── my-component │ │ └── my-component.tsx │ ├── stencil-consumer │ │ └── stencil-consumer.tsx │ └── stencil-provider │ │ └── stencil-provider.tsx ├── index.html ├── index.ts └── utils │ ├── createContext.spec.tsx │ └── createContext.tsx ├── stencil.config.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | www/ 3 | loader/ 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.lock 9 | *.tmp 10 | *.tmp.* 11 | log.txt 12 | *.sublime-project 13 | *.sublime-workspace 14 | 15 | .stencil/ 16 | .idea/ 17 | .vscode/ 18 | .sass-cache/ 19 | .versions/ 20 | node_modules/ 21 | $RECYCLE.BIN/ 22 | 23 | .DS_Store 24 | Thumbs.db 25 | UserInterfaceState.xcuserstate 26 | .env 27 | coverage 28 | .coveralls.yml 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "stable" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.3](https://github.com/petermikitsh/stencil-context/compare/v0.0.2...v0.0.3) (2019-11-14) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * re-render deeply nested consumers when context changes ([95ea974](https://github.com/petermikitsh/stencil-context/commit/95ea9744d5bab876f264c92cbb796cffa9ff57b1)) 7 | 8 | 9 | 10 | ## [0.0.2](https://github.com/petermikitsh/stencil-context/compare/v0.0.1...v0.0.2) (2019-10-23) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * export createContext ([69f4c8f](https://github.com/petermikitsh/stencil-context/commit/69f4c8f0ece062294143e9d621aece75c04c7e31)) 16 | 17 | 18 | 19 | ## 0.0.1 (2019-10-22) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * ci build ([e3724df](https://github.com/petermikitsh/stencil-context/commit/e3724df13bd71d8a52385a8c55365f9855980e69)) 25 | * prefer parentNode over parentElement ([335e54e](https://github.com/petermikitsh/stencil-context/commit/335e54ebf1b1f632cc3ef30b32e1631f69b2b514)) 26 | * unit test suite ([2d9234b](https://github.com/petermikitsh/stencil-context/commit/2d9234b7157cd1889985014fd6ee33055ea8ee32)) 27 | 28 | 29 | ### Features 30 | 31 | * producer, consumer implementation ([b5456b5](https://github.com/petermikitsh/stencil-context/commit/b5456b55e5cec6c2674bd581c074f01ec82f3b75)) 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stencil-context", 3 | "version": "0.0.3", 4 | "description": "Stencil Component Starter", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "es2015": "dist/esm/index.mjs", 8 | "es2017": "dist/esm/index.mjs", 9 | "types": "dist/types/index.d.ts", 10 | "collection": "dist/collection/collection-manifest.json", 11 | "collection:main": "dist/collection/index.js", 12 | "unpkg": "dist/stencil-context/stencil-context.js", 13 | "files": [ 14 | "dist/", 15 | "loader/" 16 | ], 17 | "scripts": { 18 | "build": "stencil build", 19 | "clean": "rimraf .stencil ndist loader", 20 | "coverage:report": "cat ./coverage/lcov.info | coveralls", 21 | "start": "stencil build --dev --watch --serve", 22 | "pretest": "npm run build", 23 | "test": "stencil test --spec --e2e --coverage --collectCoverageFrom=src/components/stencil-*/*.ts{,x} --collectCoverageFrom=src/utils/**.ts{,x}", 24 | "test.watch": "stencil test --spec --e2e --watchAll", 25 | "generate": "stencil generate", 26 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" 27 | }, 28 | "devDependencies": { 29 | "@stencil/core": "^1.8.0", 30 | "@types/jest": "24.0.23", 31 | "@types/puppeteer": "1.20.2", 32 | "conventional-changelog-cli": "^2.0.27", 33 | "coveralls": "^3.0.7", 34 | "jest": "24.9.0", 35 | "jest-cli": "24.9.0", 36 | "puppeteer": "2.0.0", 37 | "rimraf": "^3.0.0" 38 | }, 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: "all", 6 | arrowParens: "always" 7 | }; 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `stencil-context` 2 | 3 | [![npm package][npm-image]][npm-url] 4 | [![Build Status][travis-image]][travis-url] 5 | [![Coverage Status][coveralls-image]][coveralls-url] 6 | [![Dependencies Status][david-image]][david-url] 7 | 8 | A react-like context implementation for Stencil.js. 9 | 10 | ## Usage 11 | 12 | ```jsx 13 | import { Component, h } from '@stencil/core'; 14 | import { createContext } from 'stencil-context'; 15 | 16 | const defaultValue = { foo: 'bar' }; 17 | 18 | const { Provider, Consumer } = createContext(defaultValue); 19 | 20 | @Component({ 21 | tag: 'my-app', 22 | }) 23 | export class MyApp { 24 | render() { 25 | return ( 26 | 27 | {({ foo }) =>
{foo}
}
28 |
29 | ); 30 | } 31 | } 32 | ``` 33 | 34 | ## Usage (Advanced) 35 | 36 | You can define nested `Provider` and `Consumer`, 37 | 38 | ```jsx 39 | import { Component, h } from '@stencil/core'; 40 | import { createContext } from 'stencil-context'; 41 | 42 | const defaultValue = { foo: 'foo' }; 43 | const { Provider, Consumer } = createContext(defaultValue); 44 | 45 | @Component({ 46 | tag: 'my-app', 47 | }) 48 | export class MyApp { 49 | render() { 50 | return ( 51 | 52 | 53 | {({ foo }) => [ 54 |
{foo}
, 55 | 56 | {({ foo }) =>
{foo}
}
57 |
, 58 | ]} 59 |
60 |
61 | ); 62 | } 63 | } 64 | ``` 65 | 66 | ## Note 67 | 68 | You may see the error message below when defining JSX children that are functions (e.g., when using Consumer). 69 | 70 | This usage is normal; the message is a bug. 71 | 72 | ``` 73 | [STENCIL-DEV-MODE] vNode passed as children has unexpected type. 74 | Make sure it's using the correct h() function. 75 | Empty objects can also be the cause, look for JSX comments that became objects. 76 | ``` 77 | 78 | [npm-image]: https://img.shields.io/npm/v/stencil-context.svg 79 | [npm-url]: https://www.npmjs.com/package/stencil-context 80 | [travis-image]: https://travis-ci.org/petermikitsh/stencil-context.svg?branch=master 81 | [travis-url]: https://travis-ci.org/petermikitsh/stencil-context 82 | [david-image]: https://david-dm.org/petermikitsh/stencil-context/status.svg 83 | [david-url]: https://david-dm.org/petermikitsh/stencil-context 84 | [coveralls-image]: https://coveralls.io/repos/github/petermikitsh/stencil-context/badge.svg?branch=master 85 | [coveralls-url]: https://coveralls.io/github/petermikitsh/stencil-context?branch=master 86 | -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** 4 | * This is an autogenerated file created by the Stencil compiler. 5 | * It contains typing information for all components that exist in this project. 6 | */ 7 | 8 | 9 | import { HTMLStencilElement, JSXBase } from '@stencil/core/internal'; 10 | 11 | 12 | export namespace Components { 13 | interface MyComponent {} 14 | interface MyComponentChild {} 15 | interface MyComponentGrandchild {} 16 | interface StencilConsumer { 17 | 'renderer': any; 18 | } 19 | interface StencilProvider { 20 | 'STENCIL_CONTEXT': { [key: string]: any }; 21 | } 22 | } 23 | 24 | declare global { 25 | 26 | 27 | interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement {} 28 | var HTMLMyComponentElement: { 29 | prototype: HTMLMyComponentElement; 30 | new (): HTMLMyComponentElement; 31 | }; 32 | 33 | interface HTMLMyComponentChildElement extends Components.MyComponentChild, HTMLStencilElement {} 34 | var HTMLMyComponentChildElement: { 35 | prototype: HTMLMyComponentChildElement; 36 | new (): HTMLMyComponentChildElement; 37 | }; 38 | 39 | interface HTMLMyComponentGrandchildElement extends Components.MyComponentGrandchild, HTMLStencilElement {} 40 | var HTMLMyComponentGrandchildElement: { 41 | prototype: HTMLMyComponentGrandchildElement; 42 | new (): HTMLMyComponentGrandchildElement; 43 | }; 44 | 45 | interface HTMLStencilConsumerElement extends Components.StencilConsumer, HTMLStencilElement {} 46 | var HTMLStencilConsumerElement: { 47 | prototype: HTMLStencilConsumerElement; 48 | new (): HTMLStencilConsumerElement; 49 | }; 50 | 51 | interface HTMLStencilProviderElement extends Components.StencilProvider, HTMLStencilElement {} 52 | var HTMLStencilProviderElement: { 53 | prototype: HTMLStencilProviderElement; 54 | new (): HTMLStencilProviderElement; 55 | }; 56 | interface HTMLElementTagNameMap { 57 | 'my-component': HTMLMyComponentElement; 58 | 'my-component-child': HTMLMyComponentChildElement; 59 | 'my-component-grandchild': HTMLMyComponentGrandchildElement; 60 | 'stencil-consumer': HTMLStencilConsumerElement; 61 | 'stencil-provider': HTMLStencilProviderElement; 62 | } 63 | } 64 | 65 | declare namespace LocalJSX { 66 | interface MyComponent {} 67 | interface MyComponentChild {} 68 | interface MyComponentGrandchild {} 69 | interface StencilConsumer { 70 | 'onMountConsumer'?: (event: CustomEvent) => void; 71 | 'renderer'?: any; 72 | } 73 | interface StencilProvider { 74 | 'STENCIL_CONTEXT'?: { [key: string]: any }; 75 | 'onMountConsumer'?: (event: CustomEvent) => void; 76 | } 77 | 78 | interface IntrinsicElements { 79 | 'my-component': MyComponent; 80 | 'my-component-child': MyComponentChild; 81 | 'my-component-grandchild': MyComponentGrandchild; 82 | 'stencil-consumer': StencilConsumer; 83 | 'stencil-provider': StencilProvider; 84 | } 85 | } 86 | 87 | export { LocalJSX as JSX }; 88 | 89 | 90 | declare module "@stencil/core" { 91 | export namespace JSX { 92 | interface IntrinsicElements { 93 | 'my-component': LocalJSX.MyComponent & JSXBase.HTMLAttributes; 94 | 'my-component-child': LocalJSX.MyComponentChild & JSXBase.HTMLAttributes; 95 | 'my-component-grandchild': LocalJSX.MyComponentGrandchild & JSXBase.HTMLAttributes; 96 | 'stencil-consumer': LocalJSX.StencilConsumer & JSXBase.HTMLAttributes; 97 | 'stencil-provider': LocalJSX.StencilProvider & JSXBase.HTMLAttributes; 98 | } 99 | } 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/components/my-component-child/my-component-child.tsx: -------------------------------------------------------------------------------- 1 | import { Component, State, h } from '@stencil/core'; 2 | import { createContext } from '../../utils/createContext'; 3 | 4 | const { Provider, Consumer } = createContext({ defaultValue: 'foo' }); 5 | 6 | interface Context { 7 | defaultValue: string; 8 | } 9 | 10 | @Component({ 11 | tag: 'my-component-child', 12 | }) 13 | export class MyComponentChild { 14 | @State() childProvider?: Context; 15 | @State() clear: boolean = false; 16 | 17 | constructor() { 18 | window.setTimeout(() => { 19 | this.childProvider = { defaultValue: 'first-level-updated-4sec' }; 20 | }, 4000); 21 | 22 | window.setTimeout(() => { 23 | this.clear = true; 24 | }, 6000); 25 | } 26 | render() { 27 | return ( 28 | 29 | {({ defaultValue }: Context) => [ 30 |
2. {defaultValue} (child)
, 31 | 38 | 39 | {this.clear ? null : {() => {}}} 40 | , 41 | ]} 42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/my-component-grandchild/my-component-grandchild.tsx: -------------------------------------------------------------------------------- 1 | import { Component, h } from '@stencil/core'; 2 | import { createContext } from '../../utils/createContext'; 3 | 4 | const { Consumer } = createContext({ defaultValue: 'foo' }); 5 | 6 | interface Context { 7 | defaultValue: string; 8 | } 9 | 10 | @Component({ 11 | tag: 'my-component-grandchild', 12 | }) 13 | export class MyComponentGrandchild { 14 | render() { 15 | return ( 16 | 17 | {({ defaultValue }: Context) => ( 18 |
3. {defaultValue} (grandchild)
19 | )} 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/my-component/my-component.tsx: -------------------------------------------------------------------------------- 1 | import { Component, State, h } from '@stencil/core'; 2 | import { createContext } from '../../utils/createContext'; 3 | 4 | const { Provider, Consumer } = createContext({ defaultValue: 'first-level' }); 5 | 6 | interface Context { 7 | defaultValue: string; 8 | } 9 | 10 | @Component({ 11 | tag: 'my-component', 12 | }) 13 | export class MyComponent { 14 | constructor() { 15 | setTimeout(() => { 16 | this.firstLevel = { defaultValue: 'first-level-updated-2sec' }; 17 | }, 2000); 18 | } 19 | 20 | @State() firstLevel: Context = { defaultValue: 'first-level' }; 21 | 22 | render() { 23 | return ( 24 | 25 | 26 | {({ defaultValue }: Context) => ( 27 |
28 |
1. {defaultValue}
29 | 30 |
31 | )} 32 |
33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/stencil-consumer/stencil-consumer.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Event, EventEmitter, Prop, State } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'stencil-consumer', 5 | }) 6 | export class StencilConsumer { 7 | @Prop() renderer: any; 8 | @State() context: any; 9 | @Event({ eventName: 'mountConsumer' }) mountEmitter: EventEmitter; 10 | @State() promise: Promise; 11 | @State() resolvePromise: any; 12 | 13 | constructor() { 14 | this.promise = new Promise((resolve) => { 15 | this.resolvePromise = resolve; 16 | }); 17 | } 18 | 19 | setContext = async (context: any) => { 20 | this.context = context; 21 | return this.promise; 22 | }; 23 | 24 | componentWillLoad() { 25 | this.mountEmitter.emit(this.setContext); 26 | } 27 | 28 | componentDidUnload() { 29 | this.resolvePromise(); 30 | } 31 | 32 | render() { 33 | if (!this.context) { 34 | return null; 35 | } 36 | return this.renderer(this.context); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/stencil-provider/stencil-provider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Event, 4 | EventEmitter, 5 | h, 6 | Listen, 7 | Prop, 8 | State, 9 | Watch, 10 | } from '@stencil/core'; 11 | 12 | interface ConsumerEvent extends Event { 13 | detail: Function; 14 | } 15 | 16 | @Component({ 17 | tag: 'stencil-provider', 18 | }) 19 | export class StencilProvider { 20 | @Prop() STENCIL_CONTEXT: { [key: string]: any }; 21 | @State() consumers: Function[] = []; 22 | 23 | @Watch('STENCIL_CONTEXT') 24 | watchContext(newContext) { 25 | this.consumers.forEach((consumer) => consumer(newContext)); 26 | } 27 | @Event({ eventName: 'mountConsumer' }) mountEmitter: EventEmitter; 28 | 29 | @Listen('mountConsumer') 30 | async mountConsumer(event: ConsumerEvent) { 31 | event.stopPropagation(); 32 | this.consumers = this.consumers.slice().concat([event.detail]); 33 | await event.detail(this.STENCIL_CONTEXT); 34 | const index = this.consumers.indexOf(event.detail); 35 | const newConsumers = this.consumers 36 | .slice(0, index) 37 | .concat(this.consumers.slice(index + 1, this.consumers.length)); 38 | this.consumers = newConsumers; 39 | } 40 | 41 | componentDidUnload() { 42 | this.consumers.map((consumer) => this.mountEmitter.emit(consumer)); 43 | } 44 | 45 | render() { 46 | return ; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stencil Component Starter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/stencil-consumer/stencil-consumer"; 2 | export * from "./components/stencil-provider/stencil-provider"; 3 | export * from "./utils/createContext"; 4 | -------------------------------------------------------------------------------- /src/utils/createContext.spec.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from './createContext'; 2 | 3 | describe('createContext', () => { 4 | it('returns Provider and Consumer', () => { 5 | const context = createContext({}); 6 | expect(context.Provider).toBeTruthy(); 7 | expect(context.Consumer).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/utils/createContext.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionalComponent, h } from '@stencil/core'; 2 | 3 | export const createContext = ( 4 | defaultValue: T, 5 | ) => { 6 | const Provider: FunctionalComponent<{ 7 | value?: T; 8 | }> = (props, children) => { 9 | let resolvedValue: T = (props && props.value) || defaultValue; 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | const Consumer: FunctionalComponent = (props, children) => { 19 | if (!children.length) { 20 | return console.warn( 21 | '[stencil-context] You must pass a single child that is a Function.', 22 | ); 23 | } 24 | 25 | const renderer = children[0]; 26 | 27 | if (!(renderer instanceof Function)) { 28 | return console.warn( 29 | '[stencil-context] first child must be a Function.', 30 | ); 31 | } 32 | 33 | return ; 34 | }; 35 | 36 | return { 37 | Provider, 38 | Consumer, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /stencil.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@stencil/core'; 2 | 3 | export const config: Config = { 4 | namespace: 'stencil-context', 5 | outputTargets: [ 6 | { 7 | type: 'dist', 8 | esmLoaderPath: '../loader' 9 | }, 10 | { 11 | type: 'www', 12 | serviceWorker: null // disable service workers 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2017" 10 | ], 11 | "moduleResolution": "node", 12 | "module": "esnext", 13 | "target": "es2017", 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "jsx": "react", 17 | "jsxFactory": "h" 18 | }, 19 | "include": [ 20 | "src", 21 | "types/jsx.d.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------