@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 +2 -0
- package/docs/components.md +69 -0
- package/docs/creation-operators.md +79 -0
- package/docs/pipeable-operators.md +36 -0
- package/docs/query-selector-all-decorator.md +154 -0
- package/docs/query-selector-decorator.md +116 -0
- package/docs/subscribers.md +49 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @runopencode/rx-stencil
|
|
2
2
|
|
|
3
|
+
[](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.
|
|
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",
|