@runopencode/rx-stencil 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @runopencode/rx-stencil
2
2
 
3
+ [![npm version](https://badge.fury.io/js/@runopencode%2Frx-stencil.svg)](https://badge.fury.io/js/@runopencode%2Frx-stencil)
4
+
3
5
  This is a small utility library which provides a set of useful functions for working
4
6
  with [Stencil](https://stenciljs.com) components in conjunction with [RxJS](https://rxjs.dev). RxJS is not, in general,
5
7
  well integrated with Stencil, nor core concepts of Stencil in regard to reactivity. However, certain problems can be
@@ -0,0 +1,69 @@
1
+ # Components
2
+
3
+ ## rx-async
4
+
5
+ `rx-async` is a simple component which accepts an observable as a property and renders the result of the observable. It
6
+ is similar to the `async` pipe in Angular. It is useful when you want to render the result of an observable in a
7
+ template.
8
+
9
+ **Of course, it will work only if the observable emits a scalar value.**
10
+
11
+ Component has only one property, `value`, which accepts:
12
+
13
+ - `null` or `undefined` - in this case, component will render nothing.
14
+ - `Observable` - in this case, component will render every value emitted by the observable.
15
+ - `Promise` - in this case, component will render the value resolved by the promise.
16
+
17
+ ### Usage example
18
+
19
+ Setting value in server-side rendered application:
20
+
21
+ ```html
22
+ <rx-async></rx-async>
23
+
24
+ <script type="text/javascript">
25
+ let counter$ = new Subject();
26
+
27
+ setInterval(() => {
28
+ counter$.next(Math.random());
29
+ }, 1000);
30
+
31
+ document.querySelector('rx-async').value = counter$;
32
+
33
+ </script>
34
+ ```
35
+
36
+ Or, per example, in a StencilJS component:
37
+
38
+ ```typescript jsx
39
+ import {
40
+ Component,
41
+ ComponentInterface,
42
+ Host,
43
+ h,
44
+ } from '@stencil/core';
45
+ import { Subject } from 'rxjs';
46
+
47
+ @Component({
48
+ tag: 'app-counter',
49
+ shadow: true,
50
+ })
51
+ class AppCounter implements ComponentInterface {
52
+
53
+ private readonly counter$: Subject<number> = new Subject();
54
+
55
+ public connectedCallback(): void {
56
+ setInterval((): void => {
57
+ this.counter$.next(Math.random());
58
+ }, 1000);
59
+ }
60
+
61
+ public render(): any {
62
+ return (
63
+ <Host>
64
+ <rx-async value={this.counter$}/>
65
+ </Host>
66
+ );
67
+ }
68
+ }
69
+ ```
@@ -0,0 +1,79 @@
1
+ # Creation operators
2
+
3
+ These are operators which create observables with common, predefined behaviour. Read more about creation operators in
4
+ official RxJS documentation: https://rxjs.dev/guide/operators.
5
+
6
+ ## mutationObservable()
7
+
8
+ Mutation observable is just a simple wrapper
9
+ around [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) which emits collection of
10
+ MutationRecord as changes in DOM subtree occur. This function is part of library as utility as some of the library
11
+ operators use it internally.
12
+
13
+ Function signature is: `mutationObservable(target: Node, options?: MutationObserverInit): Observable<MutationRecord[]>`
14
+
15
+ Details of this operator are not documented as behaviour is fully described in official documentation of
16
+ MutationObserver and its `MutationObserver.observe()`
17
+ method: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe.
18
+
19
+ ## propertyObservable()
20
+
21
+ Common use case for Stencil components is to observe changes of component properties decorated with either `@Prop()`
22
+ or `@State()` decorator. In order to react on those changes prior to rendering, it is required to create a method and
23
+ decorate it with `@Watch()` decorator, as described
24
+ here: https://stenciljs.com/docs/reactive-data#the-watch-decorator-watch.
25
+
26
+ Instead of that, you may use `propertyObservable()` operator which will create an observable which will emit new value
27
+ as soon as property changes. This operator can be used for watching changes of both `@Prop()` and `@State()` decorated
28
+ properties, as well as any other property of the component.
29
+
30
+ Function signature is: `propertyObservable<T = any>(cmp: ComponentInterface, property: string): Observable<T>`
31
+
32
+ There are many use cases where this operator can be used, however, a simple example where we are calculating, so called,
33
+ computed property is given in example below:
34
+
35
+ ```typescript jsx
36
+ import { Component, ComponentInterface, Prop } from '@stencil/core';
37
+ import { combineLatest, map } from 'rxjs';
38
+ import { propertyObservable, untilDisconnected, scheduleRender } from '@runopencode/rx-stencil';
39
+
40
+ @Component({
41
+ tag: 'my-component',
42
+ })
43
+ class MyComponent implements ComponentInterface {
44
+
45
+ @Prop()
46
+ public first: number = 0;
47
+
48
+ @Prop()
49
+ public second: number = 0;
50
+
51
+ private sum: number = 0;
52
+
53
+ public connectedCallback(): void {
54
+ combineLatest([
55
+ propertyObservable(this, 'first'),
56
+ propertyObservable(this, 'second'),
57
+ ]).pipe(
58
+ untilDisconnected(this),
59
+ map(([first, second]) => first + second),
60
+ scheduleRender(this),
61
+ ).subscribe(sum => this.sum = sum);
62
+ }
63
+
64
+ public render(): any {
65
+ return <div>{this.sum}</div>;
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## renderObservable()
71
+
72
+ This operator is used to create an observable which will emit new value after `render()` function of the component is
73
+ called. Note that this operator will emmit value on next micro-task (that is, after `Promise.resolve()`) to ensure that
74
+ resulting DOM is updated and in stable state.
75
+
76
+ This function is part of library as utility as some of the library operators use it internally.
77
+
78
+ Function signature is: `renderObservable(cmp: ComponentInterface): Observable<void>`
79
+
@@ -0,0 +1,36 @@
1
+ # Pipeable operators
2
+
3
+ Pipeable operators are functions which allows you to compose observables in a declarative way. They are used in
4
+ conjunction with `pipe()` function from RxJS. Read more about pipeable operators in official RxJS
5
+ documentation: https://rxjs.dev/guide/operators.
6
+
7
+ ## scheduleRender()
8
+
9
+ Operator which taps into observable and schedules rendering of component as soon as observable emits new value. By
10
+ default, rendering ill be requested on next micro-task (i.e. after `Promise.resolve()`), but you can disable this
11
+ behaviour by passing `false` as second parameter.
12
+
13
+ Function signature
14
+ is: `scheduleRender<T = unknown>(cmp: ComponentInterface, nextTick: boolean = true): MonoTypeOperatorFunction<T>`
15
+
16
+ ## toProperty()
17
+
18
+ Operator which taps into observable and sets value of component property as soon as observable emits new value.
19
+
20
+ Function signature is: `toProperty<T = any>(cmp: ComponentInterface, property: string): MonoTypeOperatorFunction<T>`
21
+
22
+ ## untilDisconnected()
23
+
24
+ Operator which taps into observable and unsubscribes from it as soon as component is disconnected from DOM. Operator
25
+ will try to monitor execution of the `disconnectedCallback()` method of the component in order to emmit event. However,
26
+ if method is not defined on the component, mutation observable will be used on parent node of the component in order to
27
+ detect when component is removed from DOM.
28
+
29
+ Monitoring of `disconnectCallback()` is much more reliable and performant, so it is recommended to define this method on
30
+ component if you are using this operator, because with mutation observer it is not possible to reliably detect when
31
+ component is re-attached to DOM.
32
+
33
+ Function signature is: `untilDisconnected<T = unknown>(cmp: ComponentInterface): MonoTypeOperatorFunction<T>`
34
+
35
+ Common use case for this operator is to use it as a cleanup operator for observables which are created
36
+ in `connectedCallback()` method of the component.
@@ -0,0 +1,154 @@
1
+ # @QuerySelectorAll()
2
+
3
+ `@QuerySelectorAll()` is a property decorator which can be used to query for a collection of elements in the component's
4
+ template. It is inspired by the `@ViewChildren()` and `@ContentChildren()` decorators in Angular. It accepts a CSS
5
+ selector as an argument which will be used to query for the elements. In Stencil, you can reference to a DOM
6
+ element in the component (see: https://stenciljs.com/docs/templating-jsx#getting-a-reference-to-a-dom-element for more
7
+ details), however, getting collection of elements is a bit tricky. Using this decorator, you can achieve that with ease,
8
+ and you are able to apply declarative style of programming when working with DOM elements.
9
+
10
+ `@QuerySelectorAll()` decorator accepts two arguments:
11
+
12
+ - `selector`, string, required - CSS selector which will be used to query for the elements. It can be any valid CSS
13
+ selector. Search will start from the component element, or shadow root if component is using shadow DOM and parameter
14
+ `options.shadowRoot` is set to `true`.
15
+ - `options`, `QuerySelectorAllOptions`, optional, default `undefined` which means that default options will be used.
16
+ This is an object with following properties:
17
+ - `shadowRoot`, boolean, optional, default `false` - if set to `true`, search will start from shadow root of the
18
+ component, instead of the component element itself. If component does not use shadow DOM, exception will be
19
+ thrown.
20
+ - `mutationObserver`, boolean, optional, default `false` - if set to `true`, decorator will use MutationObserver to
21
+ observe changes in the DOM subtree of the component. This is more reliable approach, but it is also more
22
+ expensive. It should be used when you are querying for elements which are projected into the component through
23
+ `<slot>` element, or if subtree of the component is changed by some other means, not via Stencil's reactivity. By
24
+ default, decorator will monitor execution of the `render()` function of the component and will query for the
25
+ elements after each execution.
26
+
27
+ `@QuerySelectorAll()` decorator must be used on any property of the component, and type of the property is `Observable`
28
+ from `rxjs` library. Observable will be used to emit reference to collection of queried elements, or empty collection,
29
+ if elements could not be found. Note that you should not set initial value for the property, decorator will do that for
30
+ you. Example:
31
+
32
+ ```typescript
33
+ class MyCmp {
34
+ @QuerySelectorAll('div')
35
+ private elements$: Observable<HTMLElement[]>;
36
+ }
37
+ ```
38
+
39
+ After each execution of the `render()` function of the component (or mutation of DOM subtree), decorator will query for
40
+ the elements and emit their reference. This value will be emitted only if collection differs from the previous one.
41
+ Emission is executed on next micro-task (i.e. after `Promise.resolve()`).
42
+
43
+ A fair warning: in theory, you are able to create infinite loop without noticing because value is emitted on next
44
+ micro-task, main browser thread will not be blocked.
45
+
46
+ ## Example
47
+
48
+ Similar to example given for `@QuerySelectorAll()` decorator, we are trying to build a search list, however, filters
49
+ will be projected through `<slot>` element. Only thing that we know is that filters will be either `<input>` or
50
+ `<select>`elements and their `name` attribute should be used as the name of query parameter in search URL. Such flexible
51
+ search component is fairly simple to write using RxJS and `@QuerySelectorAll()` decorator.
52
+
53
+ ```typescript jsx
54
+ import { Component, ComponentInterface, State } from '@stencil/core';
55
+ import {
56
+ combineLatest,
57
+ debounceTime,
58
+ distinctUntilChanged,
59
+ fromEvent,
60
+ map,
61
+ merge,
62
+ Observable, queue,
63
+ startWith,
64
+ switchMap,
65
+ } from 'rxjs';
66
+ import { fromFetch } from 'rxjs/internal/observable/dom/fetch';
67
+ import { untilDisconnected } from '@runopencode/rx-stencil';
68
+
69
+ @Component({
70
+ tag: 'app-search',
71
+ shadow: true,
72
+ })
73
+ class AppSearch implements ComponentInterface {
74
+
75
+ @QuerySelector('input, select')
76
+ private readonly fields$: Observable<HTMLInputElement[] | HTMLSelectElement[]>;
77
+
78
+ @State()
79
+ private result: string[] = [];
80
+
81
+ public connectedCallback(): void {
82
+ this.fields$.pipe(
83
+ // When inputs/selects are ready, we can start listening for `input` and `change` events.
84
+ switchMap((fields: HTMLInputElement[] | HTMLSelectElement[]): Observable<string> => {
85
+ // This is tricky part, we need to convert every field into observable which will emit
86
+ // tuple of field name and its value. Since we are going to use `combineLatest()` operator,
87
+ // we need to start with initial value for all, we will use `startWith()` operator for that.
88
+ let observables: Observable<[string, string]>[] = fields.map((input: HTMLInputElement | HTMLSelectElement): Observable<string> => {
89
+ return merge(
90
+ fromEvent(input, 'input'),
91
+ fromEvent(input, 'change'),
92
+ ).pipe(
93
+ startWith(),
94
+ map((): string => [input.name, input.value.trim()]),
95
+ );
96
+ });
97
+
98
+ // Now we can combine all observables into one. Note that because of `startWith()` operator,
99
+ // `combineLatest()` will emit initial value immediately. If you want to avoid that, you can
100
+ // use `skip(1)` operator.
101
+ return combineLatest(observables);
102
+ }),
103
+ // debounce input events to avoid sending too many requests
104
+ // and filter out same search terms sent in a row
105
+ debounceTime(300),
106
+ // convert array of tuples into query string
107
+ map((values: [string, string][]): string => {
108
+ let queryParams: string[] = [];
109
+
110
+ for (let [name, value] of values) {
111
+ queryParams.push(`${name}=${value}`);
112
+ }
113
+
114
+ return queryParams.join('&');
115
+ }),
116
+ // skip successive same query params
117
+ distinctUntilChanged(),
118
+ // send search request
119
+ switchMap((query: string): Observable<string[]> => {
120
+ return fromFetch(`https://api.example.com/search?${query}`).pipe(
121
+ switchMap((response: Response): Observable<string[]> => response.json()),
122
+ );
123
+ }),
124
+ // unsubscribe when component is disconnected
125
+ untilDisconnected(this),
126
+ ).subscribe((result: string[]): void => {
127
+ this.result = result;
128
+ });
129
+ }
130
+
131
+ public render(): any {
132
+ return (
133
+ <Host>
134
+ <slot />
135
+ {this.result.map((item): any => <div>{item}</div>)}
136
+ </Host>
137
+ );
138
+ }
139
+ }
140
+ ```
141
+
142
+ With this component, you can use any number of `<input>` and `<select>` elements as filters, and they will be queried by
143
+ the component.
144
+
145
+ ```html
146
+ <app-search>
147
+ <input type="search" name="term" />
148
+ <select name="category">
149
+ <option value="1">Category 1</option>
150
+ <option value="2">Category 2</option>
151
+ <option value="3">Category 3</option>
152
+ </select>
153
+ </app-search>
154
+ ```
@@ -0,0 +1,116 @@
1
+ # @QuerySelector()
2
+
3
+ `@QuerySelector()` is a property decorator which can be used to query for a specific element in the component's
4
+ template. It is inspired by the `@ViewChild()` and `@ContentChild()` decorators in Angular. It accepts a CSS selector as
5
+ an argument which will be used to query for the element. This is alternative approach to the getting reference to a DOM
6
+ element in the component (see: https://stenciljs.com/docs/templating-jsx#getting-a-reference-to-a-dom-element for more
7
+ details). Using this decorator, you are able to apply declarative style of programming when working with DOM elements.
8
+
9
+ `@QuerySelector()` decorator accepts two arguments:
10
+
11
+ - `selector`, string, required - CSS selector which will be used to query for the element. It can be any valid CSS
12
+ selector. Search will start from the component element, or shadow root if component is using shadow DOM and parameter
13
+ `options.shadowRoot` is set to `true`.
14
+ - `options`, `QuerySelectorOptions`, optional, default `undefined` which means that default options will be used. This
15
+ is an object with following properties:
16
+ - `shadowRoot`, boolean, optional, default `false` - if set to `true`, search will start from shadow root of the
17
+ component, instead of the component element itself. If component does not use shadow DOM, exception will be
18
+ thrown.
19
+ - `mutationObserver`, boolean, optional, default `false` - if set to `true`, decorator will use MutationObserver to
20
+ observe changes in the DOM subtree of the component. This is more reliable approach, but it is also more
21
+ expensive. It should be used when you are querying for element which is projected into the component through
22
+ `<slot>` element, or if subtree of the component is changed by some other means, not via Stencil's reactivity. By
23
+ default, decorator will monitor execution of the `render()` function of the component and will query for the
24
+ element after each execution.
25
+
26
+ `@QuerySelector()` decorator must be used on any property of the component, and type of the property is `Observable`
27
+ from `rxjs` library. Observable will be used to emit reference to queried element, or `null` if element does not exist.
28
+ Note that you should not set initial value for the property, decorator will do that for you. Example:
29
+
30
+ ```typescript
31
+ class MyCmp {
32
+ @QuerySelector('div')
33
+ private element$: Observable<HTMLElement | null>;
34
+ }
35
+ ```
36
+
37
+ After each execution of the `render()` function of the component (or mutation of DOM subtree), decorator will query for
38
+ the element and emit its reference, if it can be found, or `null`. This value will be emitted only if it is different
39
+ from the previous one and on next micro-task (i.e. after `Promise.resolve()`).
40
+
41
+ A fair warning: in theory, you are able to create infinite loop without noticing because value is emitted on next
42
+ micro-task, main browser thread will not be blocked.
43
+
44
+ ## Example
45
+
46
+ Let's say that your component has a template which consist of an input field for search and a list of results. You want
47
+ for user to be able to type term in the input field and to see results in the list. However, you would like to debounce
48
+ search as well as not to send same search term twice. Imperative approach would require a lot of code for this task,
49
+ however, with RxJS this can be done in a very elegant way.
50
+
51
+ ```typescript jsx
52
+ import {
53
+ Component,
54
+ ComponentInterface,
55
+ State,
56
+ h,
57
+ } from '@stencil/core';
58
+ import {
59
+ debounceTime,
60
+ distinctUntilChanged,
61
+ from,
62
+ fromEvent,
63
+ map,
64
+ Observable,
65
+ switchMap,
66
+ } from 'rxjs';
67
+ import { fromFetch } from 'rxjs/internal/observable/dom/fetch';
68
+ import { untilDisconnected } from '@runopencode/rx-stencil';
69
+
70
+ @Component({
71
+ tag: 'app-search',
72
+ shadow: true,
73
+ })
74
+ class AppSearch implements ComponentInterface {
75
+
76
+ @QuerySelector('input[type="search"]', true)
77
+ private readonly input$: Observable<HTMLInputElement>;
78
+
79
+ @State()
80
+ private result: string[] = [];
81
+
82
+ public connectedCallback(): void {
83
+ this.input$.pipe(
84
+ // when input is ready, we can start listening for `input` events
85
+ switchMap((input: HTMLInputElement): Observable<string> => {
86
+ return fromEvent(input, 'input').pipe(
87
+ map((): string => input.value.trim()),
88
+ );
89
+ }),
90
+ // debounce input events to avoid sending too many requests
91
+ // and filter out same search terms sent in a row
92
+ debounceTime(300),
93
+ distinctUntilChanged(),
94
+ // send search request
95
+ switchMap((term: string): Observable<string[]> => {
96
+ return fromFetch(`https://api.example.com/search?term=${term}`).pipe(
97
+ switchMap((response: Response): Observable<string[]> => response.json()),
98
+ );
99
+ }),
100
+ // unsubscribe when component is disconnected
101
+ untilDisconnected(this),
102
+ ).subscribe((result: string[]): void => {
103
+ this.result = result;
104
+ });
105
+ }
106
+
107
+ public render(): any {
108
+ return (
109
+ <Host>
110
+ <input type='search' />
111
+ {this.result.map((item): any => <div>{item}</div>)}
112
+ </Host>
113
+ );
114
+ }
115
+ }
116
+ ```
@@ -0,0 +1,49 @@
1
+ # Subscribers
2
+
3
+ These are functions which can be used as subscribers to observables for common tasks in order to decrease boilerplate
4
+ code and improve developer experience.
5
+
6
+ ## setProperty()
7
+
8
+ Function which can be used as subscriber to observable in order to set value of component property as soon as observable
9
+ emits new value. Common use case would be to use it in conjunction with `propertyObservable()` operator and set a value
10
+ of "computed" property of the component.
11
+
12
+ Function signature is: `setProperty<T = any>(cmp: ComponentInterface, property: string): Subscriber<T>`
13
+
14
+ Improved example from [propertyObservable()](creation-operators.md#propertyobservable) operator documentation:
15
+
16
+ ```typescript jsx
17
+ import { Component, ComponentInterface, Prop, State } from '@stencil/core';
18
+ import { combineLatest, map } from 'rxjs';
19
+ import { propertyObservable, untilDisconnected, setProperty } from '@runopencode/rx-stencil';
20
+
21
+ @Component({
22
+ tag: 'my-component',
23
+ })
24
+ class MyComponent implements ComponentInterface {
25
+
26
+ @Prop()
27
+ public first: number = 0;
28
+
29
+ @Prop()
30
+ public second: number = 0;
31
+
32
+ @State()
33
+ private sum: number = 0;
34
+
35
+ public connectedCallback(): void {
36
+ combineLatest([
37
+ propertyObservable(this, 'first'),
38
+ propertyObservable(this, 'second'),
39
+ ]).pipe(
40
+ untilDisconnected(this),
41
+ map(([first, second]) => first + second),
42
+ ).subscribe(setProperty(this, 'sum'));
43
+ }
44
+
45
+ public render(): any {
46
+ return <div>{this.sum}</div>;
47
+ }
48
+ }
49
+ ```
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@runopencode/rx-stencil",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "ReactiveX (RxJS) utilities for Stencil",
5
+ "keywords": [
6
+ "stencil",
7
+ "stenciljs",
8
+ "rxjs"
9
+ ],
5
10
  "main": "dist/index.cjs.js",
6
11
  "module": "dist/index.js",
7
12
  "es2015": "dist/esm/index.mjs",
@@ -16,7 +21,8 @@
16
21
  },
17
22
  "files": [
18
23
  "dist/",
19
- "loader/"
24
+ "loader/",
25
+ "docs/"
20
26
  ],
21
27
  "scripts": {
22
28
  "build": "stencil build --docs",