├── applications
├── counter
│ ├── script.js
│ └── index.html
├── playground
│ ├── script.js
│ ├── index.html
│ ├── style.scss
│ └── playground.js
├── loading-states
│ ├── style.scss
│ ├── script.js
│ ├── index.html
│ └── utilities.js
├── basic-counter
│ ├── script.js
│ ├── utilities.js
│ └── index.html
├── pixel-editor
│ ├── style.scss
│ ├── index.html
│ ├── utilities.js
│ └── script.js
├── dog-facts
│ ├── style.scss
│ ├── index.html
│ ├── script.js
│ └── utilities.js
├── merging-timelines
│ ├── script.js
│ ├── index.html
│ └── utilities.js
├── basic-counter-with-switch-map
│ ├── utilities.js
│ ├── index.html
│ └── script.js
├── mapping
│ ├── script.js
│ ├── index.html
│ ├── utilities.js
│ └── star-wars-characters.json
├── pokemon-paginated
│ ├── index.html
│ └── script.js
├── pokemon-autocomplete
│ ├── index.html
│ └── script.js
├── from-event
│ ├── index.html
│ └── script.js
├── pokemon
│ ├── index.html
│ ├── script.js
│ ├── style.scss
│ └── utilities.js
├── pokemon-data-enhancement
│ ├── index.html
│ └── script.js
├── dog-facts-complete
│ ├── index.html
│ └── script.js
├── deep-thoughts
│ ├── script.js
│ ├── utilities.js
│ └── index.html
├── manipulating-time
│ ├── script.js
│ ├── index.html
│ └── utilities.js
├── mapping-complete
│ ├── index.html
│ ├── script.js
│ └── utilities.js
└── deep-thoughts-complete
│ ├── script.js
│ ├── utilities.js
│ └── index.html
├── .postcssrc
├── shared
├── colors.scss
├── fonts
│ ├── Press-Start-2P.ttf
│ ├── CourierPrime-Bold.ttf
│ ├── CourierPrime-Italic.ttf
│ ├── AveriaSerifLibre-Bold.ttf
│ ├── AveriaSerifLibre-Light.ttf
│ ├── CourierPrime-Regular.ttf
│ ├── AveriaSerifLibre-Italic.ttf
│ ├── AveriaSerifLibre-Regular.ttf
│ ├── CourierPrime-BoldItalic.ttf
│ ├── AveriaSerifLibre-BoldItalic.ttf
│ └── AveriaSerifLibre-LightItalic.ttf
├── home.scss
├── lesson.scss
├── lesson.html
├── application.scss
├── base.html
├── fonts.scss
├── favicon.svg
├── prism.css
└── style.scss
├── .editorconfig
├── .babelrc
├── .gitignore
├── .prettierrc
├── utilities
├── fibonacci.js
├── fibonacci.test.js
└── dom-manpulation.js
├── .posthtmlrc
├── .eleventy.cjs
├── content
├── _includes
│ └── layouts
│ │ └── lesson.njk
├── from-event-solution.md
├── switch-map-exercise.md
├── basic-async.md
├── manipulating-time-exercise.md
├── switch-map-solution.md
├── manipulating-time-solution.md
├── paginated-fetching.md
├── combining-operators.md
├── basic-counter-solution.md
├── pixel-editor.md
├── from-event-exercise.md
├── combining-operators-solution.md
├── autocomplete.md
├── creating-observables-exercise.md
├── basic-operators-solution.md
├── first-and-last-name.md
├── basic-async-solution.md
├── subjects.md
├── loading-state.md
├── basic-counter.md
├── creating-observables-solution.md
├── progressive-data-enhancement.md
├── switch-map.md
├── manipulating-time-follow-along.md
├── merging-timelines-follow-along.md
├── basic-operators.md
├── mapping-follow-along.md
├── fetching-dog-facts.md
└── _counter-follow-along.md
├── exercises
├── basic-observables.test.js
├── basic-operators.test.js
└── creating-observables.test.js
├── .eslintc
├── README.md
├── package.json
├── api
├── views
│ └── index.html
└── server.js
└── index.html
/applications/counter/script.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "postcss-fontpath": {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/shared/colors.scss:
--------------------------------------------------------------------------------
1 | $teal: #2aa298;
2 | $coral: #f76e6e;
3 | $blue: #6fbef6;
4 | $green: #08916a;
5 | $fuschia: #d6438a;
6 |
--------------------------------------------------------------------------------
/shared/fonts/Press-Start-2P.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/Press-Start-2P.ttf
--------------------------------------------------------------------------------
/shared/fonts/CourierPrime-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/CourierPrime-Bold.ttf
--------------------------------------------------------------------------------
/shared/fonts/CourierPrime-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/CourierPrime-Italic.ttf
--------------------------------------------------------------------------------
/applications/playground/script.js:
--------------------------------------------------------------------------------
1 | import { of } from 'rxjs';
2 | import './playground';
3 |
4 | export const example$ = of(1, 2, 3, 4);
5 |
--------------------------------------------------------------------------------
/shared/fonts/AveriaSerifLibre-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/AveriaSerifLibre-Bold.ttf
--------------------------------------------------------------------------------
/shared/fonts/AveriaSerifLibre-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/AveriaSerifLibre-Light.ttf
--------------------------------------------------------------------------------
/shared/fonts/CourierPrime-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/CourierPrime-Regular.ttf
--------------------------------------------------------------------------------
/shared/fonts/AveriaSerifLibre-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/AveriaSerifLibre-Italic.ttf
--------------------------------------------------------------------------------
/shared/fonts/AveriaSerifLibre-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/AveriaSerifLibre-Regular.ttf
--------------------------------------------------------------------------------
/shared/fonts/CourierPrime-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/CourierPrime-BoldItalic.ttf
--------------------------------------------------------------------------------
/shared/fonts/AveriaSerifLibre-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/AveriaSerifLibre-BoldItalic.ttf
--------------------------------------------------------------------------------
/shared/fonts/AveriaSerifLibre-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stevekinney/rxjs-fundamentals/HEAD/shared/fonts/AveriaSerifLibre-LightItalic.ttf
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "presets": ["@babel/preset-env"],
5 | "plugins": ["@babel/plugin-transform-runtime"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | .DS_Store
3 | .DS_Store
4 | .parcel-cache
5 | *.local
6 | dist
7 | dist-ssr
8 | node_modules
9 | public/
10 | yarn-error.log
11 | lessons/
12 |
--------------------------------------------------------------------------------
/applications/loading-states/style.scss:
--------------------------------------------------------------------------------
1 | @import '../../shared/colors.scss';
2 |
3 | .currently-loading {
4 | background: lighten($blue, 10%);
5 | border: 1px solid darken($blue, 10%);
6 | margin: 1em 0;
7 | padding: 1em;
8 | }
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "all",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false
10 | }
11 |
--------------------------------------------------------------------------------
/utilities/fibonacci.js:
--------------------------------------------------------------------------------
1 | export function* fibonacci() {
2 | let values = [0, 1];
3 |
4 | while (true) {
5 | let [current, next] = values;
6 |
7 | yield current;
8 |
9 | values = [next, current + next];
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/applications/basic-counter/script.js:
--------------------------------------------------------------------------------
1 | import { fromEvent, interval, merge, NEVER } from 'rxjs';
2 | import { setCount, startButton, pauseButton } from './utilities';
3 |
4 | const start$ = fromEvent(startButton, 'click');
5 | const pause$ = fromEvent(pauseButton, 'click');
6 |
--------------------------------------------------------------------------------
/applications/pixel-editor/style.scss:
--------------------------------------------------------------------------------
1 | @import '../../shared/colors.scss';
2 |
3 | #panel {
4 | position: absolute;
5 | left: 1100px;
6 | top: 180px;
7 | background: $blue;
8 | padding: 1em;
9 | border: 1px solid;
10 | &.moving {
11 | background: $fuschia;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.posthtmlrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "posthtml-doctype": {
4 | "doctype": "HTML 5"
5 | },
6 | "posthtml-extend": {
7 | "root": "./shared/"
8 | },
9 | "posthtml-prism": {
10 | "highlight": {
11 | "inline": true
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/applications/dog-facts/style.scss:
--------------------------------------------------------------------------------
1 | @import '../../shared/colors.scss';
2 |
3 | .controls {
4 | flex-direction: row;
5 | justify-content: center;
6 | width: 100%;
7 | margin-bottom: 1em;
8 | }
9 |
10 | .dog-fact {
11 | border: 1px solid $green;
12 | margin: 1em;
13 | padding: 1em;
14 | }
15 |
--------------------------------------------------------------------------------
/applications/merging-timelines/script.js:
--------------------------------------------------------------------------------
1 | import { fromEvent, merge, interval, concat, race, forkJoin } from 'rxjs';
2 | import { mapTo, startWith, take, map } from 'rxjs/operators';
3 | import {
4 | labelWith,
5 | startButton,
6 | pauseButton,
7 | setStatus,
8 | bootstrap,
9 | } from './utilities';
10 |
--------------------------------------------------------------------------------
/applications/basic-counter-with-switch-map/utilities.js:
--------------------------------------------------------------------------------
1 | export const count = document.getElementById('count');
2 | export const startButton = document.getElementById('start');
3 | export const pauseButton = document.getElementById('pause');
4 |
5 | export const setCount = (value) => {
6 | count.innerText = value;
7 | };
8 |
--------------------------------------------------------------------------------
/.eleventy.cjs:
--------------------------------------------------------------------------------
1 | module.exports = function (eleventyConfig) {
2 | eleventyConfig.addFilter('code', (content) => {
3 | return content.replace(/=>/g, '=>');
4 | });
5 |
6 | return {
7 | dir: {
8 | input: 'content',
9 | output: 'lessons',
10 | quiet: true,
11 | },
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/shared/home.scss:
--------------------------------------------------------------------------------
1 | @import './colors.scss';
2 |
3 | ul,
4 | ol {
5 | padding-left: 0;
6 | }
7 |
8 | li {
9 | font-size: 1em;
10 | font-size: 0.9em;
11 | text-indent: 0.5em;
12 | }
13 |
14 | ol > li {
15 | list-style: lower-roman;
16 | }
17 |
18 | .lessons,
19 | .applications {
20 | width: 100%;
21 | }
22 |
--------------------------------------------------------------------------------
/applications/basic-counter/utilities.js:
--------------------------------------------------------------------------------
1 | import { fromEvent, interval } from 'rxjs';
2 |
3 | export const count = document.querySelector('.count');
4 | export const startButton = document.getElementById('start');
5 | export const pauseButton = document.getElementById('pause');
6 |
7 | export const setCount = (value) => {
8 | count.innerText = value;
9 | };
10 |
--------------------------------------------------------------------------------
/applications/loading-states/script.js:
--------------------------------------------------------------------------------
1 | import { fromEvent, concat, of, race, timer } from 'rxjs';
2 | import { tap, exhaustMap, delay, shareReplay, first } from 'rxjs/operators';
3 |
4 | import {
5 | responseTimeField,
6 | showLoadingAfterField,
7 | showLoadingForAtLeastField,
8 | loadingStatus,
9 | showLoading,
10 | form,
11 | fetchData,
12 | } from './utilities';
13 |
--------------------------------------------------------------------------------
/content/_includes/layouts/lesson.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: RxJS Fundamentals
3 | ---
4 |
5 | Pixel Editor
5 | Pokémon API
5 |
9 |
10 |
11 |
12 | Pokémon API
5 |
9 |
10 |
11 |
12 | Notification Center
5 | Pokémon API
5 |
9 |
10 |
11 |
12 |
13 | Pokémon API
5 |
9 |
10 |
11 |
12 |
13 | Playground
5 | Dog Facts
5 | Counter
5 | Dog Facts
5 | Counter
5 | Notification Center
5 | Mapping
7 | Mapping
7 | Deep Thoughts
5 | Notification Center
5 | Merging Timelines
7 | Counter
5 | Loading States
5 |
6 |
31 |
32 |
${value}`;
26 | return element;
27 | }
28 |
29 | element.innerText = value;
30 | return element;
31 | };
32 |
33 | export const addElementToDOM = (target, value, attributes) => {
34 | const element = createElement(value, attributes);
35 | target.appendChild(element);
36 | };
37 |
38 | export const emptyElement = (target) => {
39 | target.innerText = '';
40 | };
41 |
42 | export const emptyElements = (targets) => {
43 | if (Array.isArray(targets)) return targets.forEach(emptyElement);
44 | if (isObject) return emptyElements(Object.values(targets));
45 | };
46 |
--------------------------------------------------------------------------------
/applications/pokemon-data-enhancement/script.js:
--------------------------------------------------------------------------------
1 | import { identity } from 'lodash';
2 | import {
3 | debounceTime,
4 | distinctUntilChanged,
5 | fromEvent,
6 | map,
7 | mergeMap,
8 | switchMap,
9 | tap,
10 | of,
11 | merge,
12 | from,
13 | pluck,
14 | take,
15 | exhaustMap,
16 | } from 'rxjs';
17 |
18 | import { fromFetch } from 'rxjs/fetch';
19 |
20 | import {
21 | renderPokemon,
22 | clearResults,
23 | endpoint,
24 | endpointFor,
25 | search,
26 | addDataToPokemon,
27 | form,
28 | } from '../pokemon/utilities';
29 |
30 | const getPokemon = (searchTerm) =>
31 | fromFetch(endpoint + searchTerm).pipe(
32 | mergeMap((response) => response.json()),
33 | );
34 |
35 | const getAdditionalData = (pokemon) =>
36 | fromFetch(endpointFor(pokemon.id)).pipe(
37 | mergeMap((response) => response.json()),
38 | );
39 |
40 | const search$ = fromEvent(form, 'submit').pipe(
41 | map(() => search.value),
42 | exhaustMap((searchTerm) =>
43 | getPokemon(searchTerm).pipe(
44 | pluck('pokemon'),
45 | mergeMap(identity),
46 | take(1),
47 | switchMap((pokemon) => {
48 | const pokemon$ = of(pokemon);
49 |
50 | const additionalData$ = getAdditionalData(pokemon).pipe(
51 | map((data) => ({ ...pokemon, data })),
52 | );
53 |
54 | return merge(pokemon$, additionalData$);
55 | }),
56 | ),
57 | ),
58 | tap(renderPokemon),
59 | );
60 |
61 | search$.subscribe(console.log);
62 |
--------------------------------------------------------------------------------
/content/pixel-editor.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Pixel Editor
3 | layout: layouts/lesson.njk
4 | ---
5 |
6 | ## Drawing Lines
7 |
8 | ```js
9 | const mousedown$ = fromEvent(canvas, 'mousedown').pipe(map(getCoordinates));
10 | const mousemove$ = fromEvent(canvas, 'mousemove').pipe(map(getCoordinates));
11 | const mouseup$ = fromEvent(canvas, 'mouseup').pipe(map(getCoordinates));
12 |
13 | const color$ = fromEvent(color, 'change', (event) => event.target.value).pipe(
14 | startWith(color.value),
15 | );
16 |
17 | const isDrawingLine$ = mousedown$.pipe(
18 | switchMap(() =>
19 | mousemove$.pipe(
20 | map(roundDownPoints),
21 | distinctUntilChanged(pointsAreEqual),
22 | takeUntil(mouseup$),
23 | ),
24 | ),
25 | withLatestFrom(color$),
26 | );
27 |
28 | isDrawingLine$.subscribe(drawLine);
29 | ```
30 |
31 | ## Drag and Drop Panel
32 |
33 | ```js
34 | const panelstart$ = fromEvent(panel, 'mousedown');
35 | const panelmove$ = fromEvent(document, 'mousemove');
36 | const panelend$ = fromEvent(document, 'mouseup');
37 |
38 | const isMovingPanel$ = panelstart$.pipe(
39 | switchMap((start) =>
40 | panelmove$.pipe(
41 | tap(() => panel.classList.add('moving')),
42 | map((event) => [event.x - start.offsetX, event.y - start.offsetY]),
43 | takeUntil(panelend$),
44 | finalize(() => panel.classList.remove('moving')),
45 | ),
46 | ),
47 | );
48 |
49 | isMovingPanel$.subscribe(([x, y]) => {
50 | panel.style.top = y + 'px';
51 | panel.style.left = x + 'px';
52 | });
53 | ```
54 |
--------------------------------------------------------------------------------
/applications/dog-facts-complete/script.js:
--------------------------------------------------------------------------------
1 | import { fromEvent, of, timer, merge, NEVER } from 'rxjs';
2 | import { fromFetch } from 'rxjs/fetch';
3 | import {
4 | catchError,
5 | exhaustMap,
6 | mapTo,
7 | mergeMap,
8 | retry,
9 | startWith,
10 | switchMap,
11 | tap,
12 | pluck,
13 | } from 'rxjs/operators';
14 |
15 | import {
16 | fetchButton,
17 | stopButton,
18 | clearError,
19 | clearFacts,
20 | addFacts,
21 | setError,
22 | } from '../dog-facts/utilities';
23 |
24 | const endpoint = 'http://localhost:3333/api/facts';
25 |
26 | const fetchData = () =>
27 | fromFetch(endpoint).pipe(
28 | mergeMap((response) => {
29 | if (response.ok) {
30 | return response.json();
31 | } else {
32 | throw new Error('Something went wrong!');
33 | }
34 | }),
35 | tap(console.log),
36 | retry(4),
37 | catchError((error) => {
38 | console.error(error);
39 | return of({ error: 'The stream caught an error. Cool, right?' });
40 | }),
41 | );
42 |
43 | const fetch$ = fromEvent(fetchButton, 'click').pipe(mapTo(true));
44 | const stop$ = fromEvent(stopButton, 'click').pipe(mapTo(false));
45 |
46 | const factStream$ = merge(fetch$, stop$).pipe(
47 | startWith(false),
48 | switchMap((shouldFetch) => {
49 | return shouldFetch
50 | ? timer(0, 5000).pipe(
51 | tap(() => clearError()),
52 | tap(() => clearFacts()),
53 | exhaustMap(fetchData),
54 | )
55 | : NEVER;
56 | }),
57 | );
58 |
59 | factStream$.subscribe(addFacts);
60 |
--------------------------------------------------------------------------------
/content/from-event-exercise.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: fromEvent (Exercise)
3 | layout: layouts/lesson.njk
4 | ---
5 |
6 | `fromEvent` allows you to create observables from event listeners. This might seem a little bit silly at first, but we'll see that this allows for us to do some sophisticated things with events later on.
7 |
8 | We can take an event listener that looks like this:
9 |
10 | ```js
11 | const button = document.querySelector('button');
12 |
13 | button.addEventListener('click', (event) => {
14 | console.log(event);
15 | });
16 | ```
17 |
18 | And we can use `fromEvent` instead.
19 |
20 | ```js
21 | const buttonClicks$ = fromEvent(button, 'click');
22 |
23 | buttonClicks$.subscribe(console.log);
24 | ```
25 |
26 | The one thing that you'll notice is that you need to subscribe to the observable in order for the event listener to be registered.
27 |
28 | You can even pass it a callback function if you want to format the event.
29 |
30 | ```js
31 | const inputChanges$ = fromEvent(input, 'input', (event) => {
32 | event.target.value;
33 | });
34 | ```
35 |
36 | ## Your Mission
37 |
38 | In [`applications/from-event`][exercise], we have an incredibly basic example. Can you use `fromEvent` as an alternative to an event listener?
39 |
40 | - Use `fromEvent` to create an observable that streams click events.
41 | - Subscribe to that observable.
42 | - Use `addMessageToDOM` to add a useless message to the DOM whenever the stream emits a value.
43 |
44 | [exercise]: https://github.com/stevekinney/rxjs-fundamentals/tree/master/applications/from-event
45 |
--------------------------------------------------------------------------------
/applications/mapping-complete/script.js:
--------------------------------------------------------------------------------
1 | import { of, from, interval, fromEvent, merge, NEVER } from 'rxjs';
2 | import {
3 | pluck,
4 | concatMap,
5 | take,
6 | map,
7 | combineLatestAll,
8 | startWith,
9 | mergeMap,
10 | shareReplay,
11 | mapTo,
12 | tap,
13 | switchMap,
14 | share,
15 | } from 'rxjs/operators';
16 |
17 | import {
18 | getCharacter,
19 | render,
20 | startButton,
21 | pauseButton,
22 | setStatus,
23 | } from './utilities';
24 |
25 | // const example$ = of('John', 'Paul', 'George', 'Ringo').pipe(
26 | // exhaustMap((beatle, index) =>
27 | // interval(index * 1000).pipe(
28 | // take(4),
29 | // map((i) => `${beatle} ${i}`),
30 | // ),
31 | // ),
32 | // );
33 |
34 | // const example$ = of('John', 'Paul', 'George', 'Ringo').pipe(
35 | // map((beatle, index) =>
36 | // interval(index * 1000).pipe(
37 | // startWith('(Not Started)'),
38 | // take(5),
39 | // map((i) => `${beatle} ${i}`),
40 | // ),
41 | // ),
42 | // combineLatestAll(),
43 | // );
44 |
45 | // example$.subscribe(render);
46 |
47 | const characters$ = interval(1000).pipe(mergeMap(getCharacter));
48 |
49 | const start$ = fromEvent(startButton, 'click').pipe(mapTo(true));
50 | const pause$ = fromEvent(pauseButton, 'click').pipe(mapTo(false));
51 |
52 | const isRunning$ = merge(start$, pause$).pipe(
53 | startWith(false),
54 | tap(setStatus),
55 | switchMap((isRunning) => (isRunning ? characters$ : NEVER)),
56 | pluck('name'),
57 | tap(render),
58 | );
59 |
60 | isRunning$.subscribe();
61 |
--------------------------------------------------------------------------------
/content/combining-operators-solution.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Combining Obervables with Operators (Solution)
3 | layout: layouts/lesson.njk
4 | ---
5 |
6 | When last we worked with our counter, we had something like this.
7 |
8 | ```js
9 | let interval$;
10 |
11 | startButtonClicks$.subscribe(() => {
12 | interval$ = interval(1000).subscribe((value) => (count.value = value));
13 | });
14 |
15 | stopButtonClicks$.subscribe(() => {
16 | interval$.unsubscribe();
17 | });
18 | ```
19 |
20 | But, now we have `skipUntil` and `takeUntil` at our disposal. What would it look like if we refactored our timer to _not_ use a global variable?
21 |
22 | We could try something like this:
23 |
24 | ```js
25 | const start$ = fromEvent(start, 'click');
26 | const pause$ = fromEvent(pause, 'click');
27 |
28 | const counter$ = interval(1000).pipe(skipUntil(start$), takeUntil(pause$));
29 | ```
30 |
31 | You'll notice that our count is a little weird. The `interval` is still going. We can use `scan` to fix this.
32 |
33 | ```js
34 | const start$ = fromEvent(start, 'click');
35 | const pause$ = fromEvent(pause, 'click');
36 |
37 | const counter$ = interval(1000).pipe(
38 | skipUntil(start$),
39 | scan((total) => total + 1, 0),
40 | takeUntil(pause$),
41 | );
42 | ```
43 |
44 | ## It Still Has Some Issues
45 |
46 | We've made our code cleaner and we've done things in a more idiomatic way, but we're not struggling with the fact that we have a single-use counter. There are a few ways that we can deal with this, but none of them are great. So, let's revisit this later, shall we?
47 |
--------------------------------------------------------------------------------
/shared/application.scss:
--------------------------------------------------------------------------------
1 | @import './colors.scss';
2 |
3 | body {
4 | background-color: lighten($teal, 50%);
5 | }
6 |
7 | .application {
8 | margin: 2em;
9 | background-color: white;
10 | padding: 24px;
11 | border: 4px solid $blue;
12 | box-shadow: 4px 4px 8px darken($blue, 50%);
13 | }
14 |
15 | .controls {
16 | display: flex;
17 | flex-direction: column;
18 | gap: 18px;
19 | }
20 |
21 | .control-set {
22 | display: flex;
23 | gap: 12px;
24 | justify-content: center;
25 | }
26 |
27 | .count {
28 | font-family: 'Press Start 2P', monospace;
29 | font-size: 96px;
30 | text-align: center;
31 | }
32 |
33 | .notifications {
34 | display: flex;
35 | gap: 12px;
36 | section {
37 | width: 100%;
38 | }
39 | article {
40 | background-color: lighten($blue, 10%);
41 | border: 2px solid $blue;
42 | margin-bottom: 1em;
43 | padding: 1em 1em;
44 | box-shadow: 4px 4px 4px;
45 | }
46 | }
47 |
48 | .stream-element {
49 | border: 1px solid black;
50 | padding: 12px;
51 | margin: 6px;
52 | pre {
53 | color: $fuschia;
54 | }
55 | }
56 |
57 | .stream-first {
58 | background-color: $coral;
59 | }
60 |
61 | .stream-second {
62 | background-color: $blue;
63 | }
64 |
65 | .stream-combined {
66 | background-color: $fuschia;
67 | }
68 |
69 | .stream-john {
70 | background-color: $coral;
71 | }
72 |
73 | .stream-paul {
74 | background-color: $blue;
75 | }
76 |
77 | .stream-george {
78 | background-color: $fuschia;
79 | }
80 |
81 | .stream-ringo {
82 | background-color: $green;
83 | }
84 |
85 | #deep-thoughts .column {
86 | border-width: 0;
87 | }
88 |
89 | #deep-thought-status {
90 | color: lighten($blue, 10%);
91 | font-style: italic;
92 | }
--------------------------------------------------------------------------------
/applications/pixel-editor/script.js:
--------------------------------------------------------------------------------
1 | import { fromEvent } from 'rxjs';
2 | import {
3 | switchMap,
4 | map,
5 | takeUntil,
6 | startWith,
7 | distinctUntilChanged,
8 | withLatestFrom,
9 | tap,
10 | finalize,
11 | } from 'rxjs/operators';
12 |
13 | import {
14 | canvas,
15 | color,
16 | panel,
17 | roundDown,
18 | roundDownPoints,
19 | pointsAreEqual,
20 | getCoordinates,
21 | drawLine,
22 | } from './utilities';
23 |
24 | const mousedown$ = fromEvent(canvas, 'mousedown').pipe(map(getCoordinates));
25 | const mousemove$ = fromEvent(canvas, 'mousemove').pipe(map(getCoordinates));
26 | const mouseup$ = fromEvent(canvas, 'mouseup').pipe(map(getCoordinates));
27 |
28 | const color$ = fromEvent(color, 'change', (event) => event.target.value).pipe(
29 | startWith(color.value),
30 | );
31 |
32 | const isDrawingLine$ = mousedown$.pipe(
33 | switchMap(() =>
34 | mousemove$.pipe(
35 | map(roundDownPoints),
36 | distinctUntilChanged(pointsAreEqual),
37 | takeUntil(mouseup$),
38 | ),
39 | ),
40 | withLatestFrom(color$),
41 | );
42 |
43 | isDrawingLine$.subscribe(drawLine);
44 |
45 | const panelstart$ = fromEvent(panel, 'mousedown');
46 | const panelmove$ = fromEvent(document, 'mousemove');
47 | const panelend$ = fromEvent(document, 'mouseup');
48 |
49 | const isMovingPanel$ = panelstart$.pipe(
50 | switchMap((start) =>
51 | panelmove$.pipe(
52 | tap(() => panel.classList.add('moving')),
53 | map((event) => [event.x - start.offsetX, event.y - start.offsetY]),
54 | takeUntil(panelend$),
55 | finalize(() => panel.classList.remove('moving')),
56 | ),
57 | ),
58 | );
59 |
60 | isMovingPanel$.subscribe(([x, y]) => {
61 | panel.style.top = y + 'px';
62 | panel.style.left = x + 'px';
63 | });
64 |
--------------------------------------------------------------------------------
/shared/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 | These are intended to be run locally. Clone the repository and learn more about getting started here.
21 |28 | This is a quick and simple API for the example applications in Steve's 29 | course on RxJS. 30 |
31 | 32 |/api/pokemon returns an array
37 | of 10 Pokemon and optionally a next page token.
38 | /api/pokemon?page={token}
43 | returns an array of all of the Pokemon starting from a provided page.
44 | /api/pokemon/1 return a
47 | single Pokemon.
48 | /api/search/:query searches
51 | by the Pokemon's name.
52 | ${pokemon.classification}
21 || 39 | Hit Points 40 | | 41 |42 | Height 43 | | 44 |45 | Weight 46 | | 47 |48 | Speed 49 | | 50 |
|---|---|---|---|
| 55 | ${data.hp} 56 | | 57 |58 | ${data.height_m} 59 | | 60 |61 | ${data.weight_kg} 62 | | 63 |64 | ${data.speed} 65 | | 66 |
21 | You can find the Github repository for the code and lessons
22 |
23 | here . You can learn more about
25 | getting started in the README.md
27 | .
29 |
31 | Watch the Rx.js fundamentals course here. 32 |
33 |These are intended to be run locally. Clone the repository and learn more about getting started here.
138 |