diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 16eaa87..15b4646 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -21,6 +21,6 @@ jobs:
run: |
npm set audit false
npm install
- pika build
+ node_modules/.bin/pika build
env:
CI: true
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 2d93a34..b5202ce 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -37,6 +37,7 @@
+
diff --git a/DECORATORS.md b/DECORATORS.md
new file mode 100644
index 0000000..9522d28
--- /dev/null
+++ b/DECORATORS.md
@@ -0,0 +1,97 @@
+# Experimental Decorators
+
+Bubblesub comes with experimental `@pub` and `@sub` decorators. The interop with LitElement is not great. It is not clear if they are a good idea in the end
+
+## Example Usage
+
+Here is an example on StackBlitz that integrates with [lit-element](https://lit-element.polymer-project.org/) :
+[StackBlitz Example](https://stackblitz.com/edit/bubblesub-demo)
+
+## Publishing
+
+Here is an example of a higher-level component that publishes a calculator as a service.
+
+```typescript
+import { LitElement, customElement, html } from 'lit-element'
+import { pub } from 'bubblesub'
+import { Calculator } from 'calculator'
+
+//Parent calculator component
+@customElement('demo-calculator')
+class App extends LitElement {
+
+ @pub()
+ calc: Calculator = new Calculator()
+
+ render() {
+ return html`
Calculator
`;
+ }
+}
+
+```
+
+## Subscribing
+
+Here is a child element that can be repeated as many times as is needed.
+
+```typescript
+import { LitElement, customElement, html } from 'lit-element'
+import { sub } from 'bubblesub'
+import { Calculator } from 'calculator'
+
+@customElement('demo-val')
+class Value extends LitElement {
+
+ @sub()
+ calc: Calculator
+
+ render() {
+ return html`
+ { this.calc.set(parseInt(this.id), parseInt(e.target.value)) }}" >
+ `
+ }
+}
+
+```
+
+Here is a child element that displays the total of the calculation
+
+```typescript
+import { LitElement, customElement, html } from 'lit-element'
+import { sub } from 'bubblesub'
+import { Calculator } from 'calculator'
+
+@customElement('demo-total')
+class Total extends LitElement {
+
+ @sub({ update: function (calc: Calculator) { calc.onChange(() => { this.requestUpdate() }) } })
+ calc: Calculator
+
+ render() {
+ return html`
+
+ `
+ }
+}
+```
+
+## HTML
+
+Here the components are combined. The `demo-val` elements find the calculator service because they have an ancestor, demo-calculator that provides the service.
+
+```html
+
+
+ +
+
+ +
+
+ =
+
+
+```
diff --git a/README.md b/README.md
index d585b1f..e789d95 100644
--- a/README.md
+++ b/README.md
@@ -2,21 +2,23 @@
***NOTE: This library is experimental!***
-Bubblesub is a simple library to create observables. It leverages the DOM and DOM events to reduce coupling to make things simple and lightweight.
+Bubblesub is a simple and lightweight library to manage observables. It builds on top of the DOM and event web standards. It's reduces the code needed without introducing a large library. It targets mid-range problems such as:
+* building web components libraries
+* micro frontends
+* create observable contexts that are neither bound to a whole app or to a whole page.
-With it, you can easily implement the following:
-* dependency injection
-* state management
-* data streaming
+Bubblesub is flexible enough to accomplish different types of integrations:
+* share a singleton services or factory
+* share data
+* stream data from a websocket or SSE to multiple listening components
+
+Bubblesub is written in Typescript but useable as a JS or TS dependency. It is published using ES 6 modules
Bubblesub is inspired by a conference talk given by Justin Fagnani (@justinfugnani) who works on Polymer's lit-element and lit-html: [Polymer - Dependency Injection](https://youtu.be/6o5zaKHedTE)
## It's Easy
-Here is an example on StackBlitz that integrates with [lit-element](https://lit-element.polymer-project.org/) :
-[StackBlitz Example](https://stackblitz.com/edit/bubblesub-demo)
-
-There are some other examples in this repo that are implemented as standalone Web Components. Finde them [here](src/example).
+There are some examples in this repo that are implemented as standalone Web Components. Finde them [here](src/example).
## How it works
@@ -26,93 +28,74 @@ There are some other examples in this repo that are implemented as standalone We
* Bubblesub is written in Typescript and is provided with ES module bundling and d.ts files
* Bubblesub has zero dependencies and targets web component (ie: custom elements, shadow dom) development.
-### Publishing
-
-Here is an example of a higher-level component that publishes a calculator as a service.
+### Publishing a Stream of updates
+Do the following to publish a stream of data or update a single value as it changes
+
+#### publishing
```typescript
-import { LitElement, customElement, html } from 'lit-element'
-import { pub } from 'bubblesub'
-import { Calculator } from 'calculator'
-
-//Parent calculator component
-@customElement('demo-calculator')
-class App extends LitElement {
+import { Publication } from './publication'
+import { publish } from './publish'
- @pub()
- calc: Calculator = new Calculator()
+interface Price { name: string, price: number }
+
+const pub: Publication = publish(document.body).create('prices')
+pub.update({name: 'goog', price: 1273.74})
+pub.update({name: 'fb', price: 193.62})
- render() {
- return html`
Calculator
`;
- }
-}
+pub.close()
```
-### Subscribing
-
-Here is a child element that can be repeated as many times as is needed.
+#### subscribing
+You can subscribe for all updates, just the first, or just the last. The last update is only published if the publication is closed.
```typescript
-import { LitElement, customElement, html } from 'lit-element'
-import { sub } from 'bubblesub'
-import { Calculator } from 'calculator'
-
-@customElement('demo-val')
-class Value extends LitElement {
+import { subscribe } from './subscribe'
- @sub()
- calc: Calculator
+interface Price { name: string, price: number }
- render() {
- return html`
- { this.calc.set(parseInt(this.id), parseInt(e.target.value)) }}" >
- `
- }
-}
+subscribe(this)
+ .to('prices')
+ .map((price: Price) => { /*do something with all the price*/})
+ .mapFirst((price: Price) => { /*do something with the initial price*/})
+ .mapLast((price: Price) => { /*do something with the final price*/})
```
-Here is a child element that displays the total of the calculation
+### Publishing a service
+
+Publishing a service or a factory is very similar to a stream of values. It's simply not expected that a service or factory is updated more than once.
+
+#### publishing
+
+To publish a shared service or factory do the same as with a stream.
```typescript
-import { LitElement, customElement, html } from 'lit-element'
-import { sub } from 'bubblesub'
-import { Calculator } from 'calculator'
-
-@customElement('demo-total')
-class Total extends LitElement {
-
- @sub({ update: function (calc: Calculator) { calc.onChange(() => { this.requestUpdate() }) } })
- calc: Calculator
-
- render() {
- return html`
-
- `
- }
-}
+import {ServiceImpl, ServiceInterface } from 'my-app'
+import { publish } from 'bubblesub'
+
+const pub = publish(document.body).create('service')
+pub.update(new ServiceImpl())
+
```
-### HTML
+#### subscribing
+
+To subscribe to a service or factory use async/await with the `.toPromise()` function to resolve the first update. Or you can use the `.mapFirst()` to process the update in a callback.
+
+```typescript
+import { subscribe } from 'bubblesub'
+import { ServiceInterface } from 'my-app'
-Here the components are combined. The `demo-val` elements find the calculator service because they have an ancestor, demo-calculator that provides the service.
+const service:ServiceInterface = await subscribe(this)
+ .to('service')
+ .toPromise()
-```html
-
-
- +
-
- +
-
- =
-
-
+subscribe(this)
+ .to('service')
+ .mapFirst((service:ServiceInterface)=>{/* do something with the service*/})
+
```
## Leveraging the DOM and events
@@ -124,7 +107,11 @@ So...
* Bubblesub requires that the publisher of a Publication be an ancestor of the subscriber
* Bubblesub relies on the hierarchical nature of the DOM to bind publisher and subscriber. A subscriber is bound to the closest ancestor that publishes the wanted Publication.
* There is no central registry of Publications. This means that your Bubblesub bindings can be encapsulated within a parent component, leak nothing, require nothing from outside.
-
+* bubblesub does not require that subscribing happen after an observable is published. The subcriber will keep on trying to find the observable assuming it will eventually appear. On the other hand a late subscriber will receive all the updates that the observable has accumulated before the subscription was established.
+
+## Decorators
+
+see: [experimental decorators](DECORATORS.md)
## Usage
@@ -135,9 +122,13 @@ The only required coupling is on the name of the dependency. Bubblesub is implem
A set of examples devised for demonstrating and testing is available if you checkout the project and build it. For the sake of clarity the examples are implemented using vanilla JS web components.
```shell script
-yarn build.example
+## build the library, the tests, and the examples
+yarn build:serve
+
+## serve the built examples
yarn serve
-## open browser at http://localhost:8080
+
+## open browser at http://localhost:8888
```
[Docs on Examples](src/example/README.md)
diff --git a/package.json b/package.json
index 70b84e4..c65fa23 100644
--- a/package.json
+++ b/package.json
@@ -1,52 +1,52 @@
{
- "name": "bubblesub",
- "version": "1.2.0",
- "description": "Observables based on DOM events",
- "repository": "https://github.com/zenwork/bubblesub.git",
- "author": "Florian Hehlen ",
- "license": "MIT",
- "keywords": [
+ "name" : "bubblesub",
+ "version" : "1.2.0",
+ "description" : "Observables based on DOM events",
+ "repository" : "https://github.com/zenwork/bubblesub.git",
+ "author" : "Florian Hehlen ",
+ "license" : "MIT",
+ "keywords" : [
"typescript",
"DI",
"publisher",
"subscriber",
"event"
],
- "scripts": {
- "build": "pika build",
- "build:serve": "npm run build && mkdir -p .serve/node_modules && rsync -cavzur pkg/ .serve/ > /dev/null 2>&1 && rsync -cavzur ./node_modules/lit-html .serve/node_modules > /dev/null 2>&1 && rsync -cavzur ./node_modules/lit-element .serve/node_modules > /dev/null 2>&1 ",
- "build:watch": "watch \"npm run build:serve\" ./src ./assets",
- "lint": "tslint -c tslint.json 'src/**/*.ts'",
- "minify": "terser --source-map includeSources pkg/dist-web/index.js -o pkg/dist-web/index.min.js; terser --source-map includeSources pkg/dist-web/index.bundled.js -o pkg/dist-web/index.bundled.min.js",
- "prepare": "npm run build:serve; npm run lint",
- "serve": "es-dev-server --config es-dev-server.config.js --root-dir .serve",
- "test": "npm run test:headless",
+ "scripts" : {
+ "build" : "pika build",
+ "build:serve" : "npm run build && mkdir -p .serve/node_modules && rsync -cavzur pkg/ .serve/ > /dev/null 2>&1 && rsync -cavzur ./node_modules/lit-html .serve/node_modules > /dev/null 2>&1 && rsync -cavzur ./node_modules/lit-element .serve/node_modules > /dev/null 2>&1 ",
+ "build:watch" : "watch \"npm run build:serve\" ./src ./assets",
+ "lint" : "tslint -c tslint.json 'src/**/*.ts'",
+ "minify" : "terser --source-map includeSources pkg/dist-web/index.js -o pkg/dist-web/index.min.js; terser --source-map includeSources pkg/dist-web/index.bundled.js -o pkg/dist-web/index.bundled.min.js",
+ "prepare" : "npm run build:serve; npm run lint",
+ "serve" : "es-dev-server --config es-dev-server.config.js --root-dir .serve",
+ "test" : "npm run test:headless",
"test:headless": "mocha-headless-chrome -f http://localhost:8888/assets/test-bundled.html",
- "test:watch": "watch \"npm run build:serve && npm run test:headless\" ./src ./assets",
- "version": "npm run build"
+ "test:watch" : "watch \"npm run build:serve && npm run test:headless\" ./src ./assets",
+ "version" : "npm run build"
},
"devDependencies": {
- "@pika/pack": "^0.5.0",
- "@pika/plugin-build-node": "^0.6.1",
- "@pika/plugin-build-web": "^0.6.1",
- "@pika/plugin-bundle-web": "^0.6.1",
- "@pika/plugin-copy-assets": "^0.6.1",
+ "@pika/pack" : "^0.5.0",
+ "@pika/plugin-build-node" : "^0.6.1",
+ "@pika/plugin-build-web" : "^0.6.1",
+ "@pika/plugin-bundle-web" : "^0.6.1",
+ "@pika/plugin-copy-assets" : "^0.6.1",
"@pika/plugin-ts-standard-pkg": "^0.6.1",
- "@pika/web": "^0.6.1",
- "@types/chai": "^4.2.3",
- "@types/mocha": "^5.2.7",
- "chai": "^4.2.0",
- "es-dev-server": "^1.18.4",
- "lit-element": "^2.2.1",
- "lit-html": "^1.1.2",
- "mocha": "^6.2.1",
- "mocha-headless-chrome": "^2.0.3",
- "terser": "^4.3.9",
- "tslint": "^5.20.0",
- "typescript": "^3.6.4",
- "watch": "^1.0.2"
+ "@pika/web" : "^0.6.1",
+ "@types/chai" : "^4.2.3",
+ "@types/mocha" : "^5.2.7",
+ "chai" : "^4.2.0",
+ "es-dev-server" : "^1.18.4",
+ "lit-element" : "^2.2.1",
+ "lit-html" : "^1.1.2",
+ "mocha" : "^6.2.1",
+ "mocha-headless-chrome" : "^2.0.3",
+ "terser" : "^4.3.9",
+ "tslint" : "^5.20.0",
+ "typescript" : "^3.6.4",
+ "watch" : "^1.0.2"
},
- "@pika/pack": {
+ "@pika/pack" : {
"pipeline": [
[
"@pika/plugin-ts-standard-pkg"
diff --git a/src/decorators.ts b/src/decorators.ts
index 48c4746..db1e442 100644
--- a/src/decorators.ts
+++ b/src/decorators.ts
@@ -1,5 +1,6 @@
-import { Publication, publisher } from './publisher.js'
-import { subscriber, Update } from './subscriber.js'
+import { Publication } from './publication'
+import { publish } from './publish.js'
+import { subscribe, Update } from './subscribe.js'
export class SubConfig {
name?: string | undefined
@@ -32,7 +33,7 @@ export function sub(config: SubConfig = {}) {
} else {
up = (v: any) => value = v
}
- if (conf.name != null) {subscriber(this).request(conf.name, up)}
+ if (conf.name != null) {subscribe(this).to(conf.name).map(up)}
subscribed = true
}
if (isGet) {
@@ -73,9 +74,9 @@ export function pub(config: PubConfig = {}) {
if (!publication) {
if (conf.pubTarget) {
- publication = publisher(conf.pubTarget).create(conf.name, conf.initialValue)
+ publication = publish(conf.pubTarget).create(conf.name, conf.initialValue)
} else {
- publication = publisher(this).create(conf.name, conf.initialValue)
+ publication = publish(this).create(conf.name, conf.initialValue)
}
}
diff --git a/src/example/di/CounterView.ts b/src/example/di/CounterView.ts
index 42b0ec8..353a77a 100644
--- a/src/example/di/CounterView.ts
+++ b/src/example/di/CounterView.ts
@@ -1,6 +1,5 @@
import { customElement, html, LitElement, property } from 'lit-element'
-import { sub } from '../../decorators.js'
-import { subscriber } from '../../subscriber.js'
+import { subscribe } from '../../subscribe.js'
import { SequenceService } from './index.js'
@customElement('counter-view')
@@ -9,12 +8,19 @@ export class CounterView extends LitElement {
@property({attribute: false})
private counter: number = 0
- @sub({name: 'service.counter'})
+ @property({attribute: false})
private service: SequenceService
+ async connectedCallback() {
+ super.connectedCallback()
+ this.service = await subscribe(this)
+ .to('service.counter')
+ .toPromise()
+ }
+
protected render() {
return html`
-
+