diff --git a/README.md b/README.md index b2c8321d..ebe5370d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Learn RxJS +# Introduction Clear examples, explanations, and resources for RxJS. -_By [@btroncone](https://twitter.com/BTroncone)_ +_By_ [_@btroncone_](https://twitter.com/BTroncone) ## Introduction @@ -20,7 +20,7 @@ Learning RxJS and reactive programming is [hard](https://twitter.com/hoss/status/742643506536153088). There's the multitude of concepts, large API surface, and fundamental shift in mindset from an -[imperative to declarative style](http://codenugget.co/2015/03/05/declarative-vs-imperative-programming-web.html). +[imperative to declarative style](https://tylermcginnis.com/imperative-vs-declarative-programming/). This site focuses on making these concepts approachable, the examples clear and easy to explore, and features references throughout to the best RxJS related material on the web. The goal is to supplement the @@ -28,7 +28,10 @@ material on the web. The goal is to supplement the while offering a new, fresh perspective to clear any hurdles and tackle the pain points. Learning Rx may be difficult but it is certainly worth the effort! -
+### Brand New to RxJS? + +Start getting familiar with all the key concepts needed to be productive with +our [RxJS Primer](/concepts/rxjs-primer.md)! ## Content @@ -36,26 +39,36 @@ points. Learning Rx may be difficult but it is certainly worth the effort! Operators are the horse-power behind observables, providing an elegant, declarative solution to complex asynchronous tasks. This section contains all -[RxJS 5 operators](/operators/README.md), included with clear, executable -examples in both [JSBin](https://jsbin.com) and -[JSFiddle](https://jsfiddle.net). Links to additional resources and recipes for -each operator are also provided, when applicable. - -##### Categories - -* [Combination](/operators/combination/README.md) -* [Conditional](/operators/conditional/README.md) -* [Creation](/operators/creation/README.md) -* [Error Handling](/operators/error_handling/README.md) -* [Multicasting](/operators/multicasting/README.md) -* [Filtering](/operators/filtering/README.md) -* [Transformation](/operators/transformation/README.md) -* [Utility](/operators/utility/README.md) +[RxJS operators](/operators/README.md), included with clear, executable +examples. Links to additional resources and recipes for each operator are also +provided, when applicable. + +##### Operator Categories + +- [Combination](/operators/combination/README.md) +- [Conditional](/operators/conditional/README.md) +- [Creation](/operators/creation/README.md) +- [Error Handling](/operators/error_handling/README.md) +- [Multicasting](/operators/multicasting/README.md) +- [Filtering](/operators/filtering/README.md) +- [Transformation](/operators/transformation/README.md) +- [Utility](/operators/utility/README.md) **OR...** [Complete listing in alphabetical order](/operators/complete.md) +#### Understanding Subjects + +A Subject is a special type of Observable which shares a single execution path +among observers. + +- [Overview](/subjects/README.md) +- [AsyncSubject](/subjects/asyncsubject.md) +- [BehaviorSubject](/subjects/behaviorsubject.md) +- [ReplaySubject](/subjects/replaysubject.md) +- [Subject](/subjects/subject.md) + #### Concepts Without a solid base knowledge of how Observables work behind the scenes, it's @@ -63,61 +76,108 @@ easy for much of RxJS to feel like 'magic'. This section helps solidify the major concepts needed to feel comfortable with reactive programming and Observables. -[Complete Concept Listing](/concepts/README.md) +- [RxJS Primer](/concepts/rxjs-primer.md) +- [Get started transforming streams with map, pluck, and mapTo](/concepts/get-started-transforming.md) +- [Time based operators comparison](/concepts/time-based-operators-comparison.md) +- [RxJS v5 -> v6 Upgrade](/concepts/rxjs5-6.md) #### Recipes Recipes for common use-cases and interesting solutions with RxJS. -[Complete Recipe Listing](/recipes/README.md) +- [Alphabet Invasion Game](/recipes/alphabet-invasion-game.md) +- [Battleship Game](/recipes/battleship-game.md) +- [Breakout Game](/recipes/breakout-game.md) +- [Car Racing Game](/recipes/car-racing-game.md) +- [Catch The Dot Game](/recipes/catch-the-dot-game.md) +- [Click Ninja Game](/recipes/click-ninja-game.md) +- [Flappy Bird Game](/recipes/flappy-bird-game.md) +- [Game Loop](/recipes/gameloop.md) +- [Horizontal Scroll Indicator](/recipes/horizontal-scroll-indicator.md) +- [HTTP Polling](/recipes/http-polling.md) +- [Lockscreen](/recipes/lockscreen.md) +- [Matrix Digital Rain](/recipes/matrix-digital-rain.md) +- [Memory Game](/recipes/memory-game.md) +- [Mine Sweeper Game](/recipes/mine-sweeper-game.md) +- [Platform Jumper Game](/recipes/platform-jumper-game.md) +- [Progress Bar](/recipes/progressbar.md) +- [Save Indicator](/recipes/save-indicator.md) +- [Smart Counter](/recipes/smartcounter.md) +- [Stop Watch](/recipes/stop-watch.md) +- [Space Invaders Game](/recipes/space-invaders-game.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) +- [Tank Battle Game](/recipes/tank-battle-game.md) +- [Tetris Game](/recipes/tetris-game.md) +- [Type Ahead](/recipes/type-ahead.md) +- [Uncover Image Game](/recipes/uncover-image-game.md) ## Introductory Resources New to RxJS and reactive programming? In addition to the content found on this -site, these excellent articles and videos will help jump start your learning -experience! +site, these excellent resources will help jump start your learning experience! + +#### Conferences + +- [RxJS Live](https://www.youtube.com/@rxjslive2237) - RxJS specific conference #### Reading -* [RxJS Introduction](http://reactivex.io/rxjs/manual/overview.html#introduction) - +- [RxJS Introduction](https://rxjs-dev.firebaseapp.com/guide/overview) - Official Docs -* [The Introduction to Reactive Programming You've Been Missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) - + +- [The Introduction to Reactive Programming You've Been Missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) - André Staltz +- [RxJS: Observables, Observers and Operators Introduction](https://ultimatecourses.com/blog/rxjs-observables-observers-operators) - + Todd Motto + #### Videos -* [Asynchronous Programming: The End of The Loop](https://egghead.io/courses/mastering-asynchronous-programming-the-end-of-the-loop) - +- [Ultimate RxJS](https://ultimatecourses.com/courses/rxjs?ref=4) 💵 - Brian + Troncone + +- [Asynchronous Programming: The End of The Loop](https://egghead.io/courses/asynchronous-programming-the-end-of-the-loop) - Jafar Husain -* [What is RxJS?](https://egghead.io/lessons/rxjs-what-is-rxjs) - Ben Lesh -* [Creating Observable from Scratch](https://egghead.io/lessons/rxjs-creating-observable-from-scratch) - + +- [What is RxJS?](https://egghead.io/lessons/rxjs-what-is-rxjs) - Ben Lesh +- [Creating Observable from Scratch](https://egghead.io/lessons/rxjs-creating-observable-from-scratch) - Ben Lesh -* [Introduction to RxJS Marble Testing](https://egghead.io/lessons/rxjs-introduction-to-rxjs-marble-testing) - - Brian Troncone -* [Introduction to Reactive Programming](https://egghead.io/courses/introduction-to-reactive-programming) - :dollar: - André Staltz -* [Reactive Programming using Observables](https://www.youtube.com/watch?v=HT7JiiqnYYc&feature=youtu.be) - + +- [Introduction to RxJS Marble Testing](https://egghead.io/lessons/rxjs-introduction-to-rxjs-marble-testing) + 💵 - Brian Troncone + +- [Introduction to Reactive Programming](https://egghead.io/courses/introduction-to-reactive-programming) + 💵 - André Staltz + +- [Reactive Programming using Observables](https://www.youtube.com/watch?v=HT7JiiqnYYc&feature=youtu.be) - Jeremy Lund #### Exercises -* [Functional Programming in JavaScript](http://reactivex.io/learnrx/) - Jafar +- [Functional Programming in JavaScript](http://reactivex.io/learnrx/) - Jafar Husain #### Tools -* [Rx Marbles - Interactive diagrams of Rx Observables](http://rxmarbles.com/) - +- [Rx Marbles - Interactive diagrams of Rx Observables](http://rxmarbles.com/) - André Staltz -* [Rx Visualizer - Animated playground for Rx Observables](https://rxviz.com) - + +- [Rx Visualizer - Animated playground for Rx Observables](https://rxviz.com) - Misha Moroshko -* [Reactive.how - Animated cards to learn Reactive Programming](http://reactive.how) - + +- [Reactive.how - Animated cards to learn Reactive Programming](http://reactive.how) - Cédric Soulas -_Interested in RxJS 4? Check out [Denis Stoyanov's](https://github.com/xgrommx) -excellent [eBook](https://xgrommx.github.io/rx-book/)!_ +- [Rx Visualization - Visualizes programming with RxJS](https://fingerpich.github.io/rx-visualization/) - + Mojtaba Zarei + +_Interested in RxJS 4? Check out_ +[_Denis Stoyanov's_](https://github.com/xgrommx) _excellent_ +[_eBook_](https://xgrommx.github.io/rx-book/)_!_ ## Translations -* [简体中文](https://rxjs-cn.github.io/learn-rxjs-operators) +- [简体中文](https://rxjs-cn.github.io/learn-rxjs-operators) ### A Note On References @@ -125,3 +185,7 @@ All references included in this GitBook are resources, both free and paid, that helped me tremendously while learning RxJS. If you come across an article or video that you think should be included, please use the _edit this page_ link in the top menu and submit a pull request. Your feedback is appreciated! + +--- + +Thank you to [Gitbook](https://www.gitbook.com/) for sponsoring this documentation! \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md index 66d88481..1356c47a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,102 +1,148 @@ -# Summary +# Table of contents -### Learn RxJS +- [Introduction](README.md) -- [Operators](/operators/README.md) - - [Combination](/operators/combination/README.md) - - [combineAll](/operators/combination/combineall.md) - - [combineLatest](/operators/combination/combinelatest.md) - - [concat](/operators/combination/concat.md) - - [concatAll](/operators/combination/concatall.md) - - [forkJoin](/operators/combination/forkjoin.md) - - [merge](/operators/combination/merge.md) - - [mergeAll](/operators/combination/mergeall.md) - - [pairwise](/operators/combination/pairwise.md) - - [race](/operators/combination/race.md) - - [startWith](/operators/combination/startwith.md) - - [withLatestFrom](/operators/combination/withlatestfrom.md) - - [zip](/operators/combination/zip.md) - - [Conditional](/operators/conditional/README.md) - - [defaultIfEmpty](/operators/conditional/defaultifempty.md) - - [every](/operators/conditional/every.md) - - [Creation](/operators/creation/README.md) - - [create](/operators/creation/create.md) - - [empty](/operators/creation/empty.md) - - [from](/operators/creation/from.md) - - [fromEvent](/operators/creation/fromevent.md) - - [interval](/operators/creation/interval.md) - - [of](/operators/creation/of.md) - - [range](/operators/creation/range.md) - - [throw](/operators/creation/throw.md) - - [timer](/operators/creation/timer.md) - - [Error Handling](/operators/error_handling/README.md) - - [catch / catchError](/operators/error_handling/catch.md) - - [retry](/operators/error_handling/retry.md) - - [retryWhen](/operators/error_handling/retrywhen.md) - - [Multicasting](/operators/multicasting/README.md) - - [publish](/operators/multicasting/publish.md) - - [multicast](/operators/multicasting/multicast.md) - - [share](/operators/multicasting/share.md) - - [shareReplay](/operators/multicasting/sharereplay.md) - - [Filtering](/operators/filtering/README.md) - - [audit](/operators/filtering/audit.md) - - [auditTime](/operators/filtering/audittime.md) - - [debounce](/operators/filtering/debounce.md) - - [debounceTime](/operators/filtering/debouncetime.md) - - [distinctUntilChanged](/operators/filtering/distinctuntilchanged.md) - - [filter](/operators/filtering/filter.md) - - [first](/operators/filtering/first.md) - - [ignoreElements](/operators/filtering/ignoreelements.md) - - [last](/operators/filtering/last.md) - - [sample](/operators/filtering/sample.md) - - [single](/operators/filtering/single.md) - - [skip](/operators/filtering/skip.md) - - [skipUntil](/operators/filtering/skipuntil.md) - - [skipWhile](/operators/filtering/skipwhile.md) - - [take](/operators/filtering/take.md) - - [takeUntil](/operators/filtering/takeuntil.md) - - [takeWhile](/operators/filtering/takewhile.md) - - [throttle](/operators/filtering/throttle.md) - - [throttleTime](/operators/filtering/throttletime.md) - - [Transformation](/operators/transformation/README.md) - - [buffer](/operators/transformation/buffer.md) - - [bufferCount](/operators/transformation/buffercount.md) - - [bufferTime](/operators/transformation/buffertime.md) - - [bufferToggle](/operators/transformation/buffertoggle.md) - - [bufferWhen](/operators/transformation/bufferwhen.md) - - [concatMap](/operators/transformation/concatmap.md) - - [concatMapTo](/operators/transformation/concatmapto.md) - - [exhaustMap](/operators/transformation/exhaustmap.md) - - [expand](/operators/transformation/expand.md) - - [groupBy](/operators/transformation/groupby.md) - - [map](/operators/transformation/map.md) - - [mapTo](/operators/transformation/mapto.md) - - [mergeMap / flatMap](/operators/transformation/mergemap.md) - - [partition](/operators/transformation/partition.md) - - [pluck](/operators/transformation/pluck.md) - - [reduce](/operators/transformation/reduce.md) - - [scan](/operators/transformation/scan.md) - - [switchMap](/operators/transformation/switchmap.md) - - [window](/operators/transformation/window.md) - - [windowCount](/operators/transformation/windowcount.md) - - [windowTime](/operators/transformation/windowtime.md) - - [windowToggle](/operators/transformation/windowtoggle.md) - - [windowWhen](/operators/transformation/windowwhen.md) - - [Utility](/operators/utility/README.md) - - [do / tap](/operators/utility/do.md) - - [delay](/operators/utility/delay.md) - - [delayWhen](/operators/utility/delaywhen.md) - - [dematerialize](/operators/utility/dematerialize.md) +## Learn RxJS + +- [Operators](operators/README.md) + - [Combination](operators/combination/README.md) + - [combineLatestAll](operators/combination/combineall.md) + - [combineLatest](operators/combination/combinelatest.md) + - [concat](operators/combination/concat.md) + - [concatAll](operators/combination/concatall.md) + - [endWith](operators/combination/endwith.md) + - [forkJoin](operators/combination/forkjoin.md) + - [merge](operators/combination/merge.md) + - [mergeAll](operators/combination/mergeall.md) + - [pairwise](operators/combination/pairwise.md) + - [race](operators/combination/race.md) + - [startWith](operators/combination/startwith.md) + - [withLatestFrom](operators/combination/withlatestfrom.md) + - [zip](operators/combination/zip.md) + - [Conditional](operators/conditional/README.md) + - [defaultIfEmpty](operators/conditional/defaultifempty.md) + - [every](operators/conditional/every.md) + - [iif](operators/conditional/iif.md) + - [sequenceEqual](operators/conditional/sequenceequal.md) + - [Creation](operators/creation/README.md) + - [ajax](operators/creation/ajax.md) + - [create](operators/creation/create.md) + - [defer](operators/creation/defer.md) + - [empty](operators/creation/empty.md) + - [from](operators/creation/from.md) + - [fromEvent](operators/creation/fromevent.md) + - [generate](operators/creation/generate.md) + - [interval](operators/creation/interval.md) + - [of](operators/creation/of.md) + - [range](operators/creation/range.md) + - [throwError](operators/creation/throw.md) + - [timer](operators/creation/timer.md) + - [Error Handling](operators/error_handling/README.md) + - [catch / catchError](operators/error_handling/catch.md) + - [retry](operators/error_handling/retry.md) + - [retryWhen](operators/error_handling/retrywhen.md) + - [Multicasting](operators/multicasting/README.md) + - [publish](operators/multicasting/publish.md) + - [multicast](operators/multicasting/multicast.md) + - [share](operators/multicasting/share.md) + - [shareReplay](operators/multicasting/sharereplay.md) + - [Filtering](operators/filtering/README.md) + - [audit](operators/filtering/audit.md) + - [auditTime](operators/filtering/audittime.md) + - [debounce](operators/filtering/debounce.md) + - [debounceTime](operators/filtering/debouncetime.md) + - [distinct](operators/filtering/distinct.md) + - [distinctUntilChanged](operators/filtering/distinctuntilchanged.md) + - [distinctUntilKeyChanged](operators/filtering/distinctuntilkeychanged.md) + - [filter](operators/filtering/filter.md) + - [find](operators/filtering/find.md) + - [first](operators/filtering/first.md) + - [ignoreElements](operators/filtering/ignoreelements.md) + - [last](operators/filtering/last.md) + - [sample](operators/filtering/sample.md) + - [single](operators/filtering/single.md) + - [skip](operators/filtering/skip.md) + - [skipUntil](operators/filtering/skipuntil.md) + - [skipWhile](operators/filtering/skipwhile.md) + - [take](operators/filtering/take.md) + - [takeLast](operators/filtering/takelast.md) + - [takeUntil](operators/filtering/takeuntil.md) + - [takeWhile](operators/filtering/takewhile.md) + - [throttle](operators/filtering/throttle.md) + - [throttleTime](operators/filtering/throttletime.md) + - [Transformation](operators/transformation/README.md) + - [buffer](operators/transformation/buffer.md) + - [bufferCount](operators/transformation/buffercount.md) + - [bufferTime](operators/transformation/buffertime.md) + - [bufferToggle](operators/transformation/buffertoggle.md) + - [bufferWhen](operators/transformation/bufferwhen.md) + - [concatMap](operators/transformation/concatmap.md) + - [concatMapTo](operators/transformation/concatmapto.md) + - [exhaustMap](operators/transformation/exhaustmap.md) + - [expand](operators/transformation/expand.md) + - [groupBy](operators/transformation/groupby.md) + - [map](operators/transformation/map.md) + - [mapTo](operators/transformation/mapto.md) + - [mergeMap / flatMap](operators/transformation/mergemap.md) + - [mergeScan](operators/transformation/mergescan.md) + - [partition](operators/transformation/partition.md) + - [pluck](operators/transformation/pluck.md) + - [reduce](operators/transformation/reduce.md) + - [scan](operators/transformation/scan.md) + - [switchMap](operators/transformation/switchmap.md) + - [switchMapTo](operators/transformation/switchmapto.md) + - [toArray](operators/transformation/toarray.md) + - [window](operators/transformation/window.md) + - [windowCount](operators/transformation/windowcount.md) + - [windowTime](operators/transformation/windowtime.md) + - [windowToggle](operators/transformation/windowtoggle.md) + - [windowWhen](operators/transformation/windowwhen.md) + - [Utility](operators/utility/README.md) + - [tap / do](operators/utility/do.md) + - [delay](operators/utility/delay.md) + - [delayWhen](operators/utility/delaywhen.md) + - [dematerialize](operators/utility/dematerialize.md) - [finalize / finally](operators/utility/finalize.md) - - [let](/operators/utility/let.md) - - [timeout](/operators/utility/timeout.md) - - [toPromise](/operators/utility/topromise.md) - - [Full Listing](/operators/complete.md) -- [Recipes](/recipes/README.md) - - [Http Polling](/recipes/http-polling.md) - - [Game Loop](/recipes/gameloop.md) - - [Progress Bar](/recipes/progressbar.md) - - [Smart Counter](/recipes/smartcounter.md) -- [Concepts](/concepts/README.md) - - [RxJS v5 -> v6 Upgrade](/concepts/rxjs5-6.md) - - [Understanding Operator Imports](/concepts/operator-imports.md) + - [let](operators/utility/let.md) + - [repeat](operators/utility/repeat.md) + - [timeInterval](operators/utility/timeinterval.md) + - [timeout](operators/utility/timeout.md) + - [timeoutWith](operators/utility/timeoutwith.md) + - [toPromise](operators/utility/topromise.md) + - [Full Listing](operators/complete.md) +- [Subjects](subjects/README.md) + - [AsyncSubject](subjects/asyncsubject.md) + - [BehaviorSubject](subjects/behaviorsubject.md) + - [ReplaySubject](subjects/replaysubject.md) + - [Subject](subjects/subject.md) +- [Recipes](recipes/README.md) + - [Alphabet Invasion Game](recipes/alphabet-invasion-game.md) + - [Battleship Game](recipes/battleship-game.md) + - [Breakout Game](recipes/breakout-game.md) + - [Car Racing Game](recipes/car-racing-game.md) + - [Catch The Dot Game](recipes/catch-the-dot-game.md) + - [Click Ninja Game](recipes/click-ninja-game.md) + - [Flappy Bird Game](recipes/flappy-bird-game.md) + - [Game Loop](recipes/gameloop.md) + - [Horizontal Scroll Indicator](recipes/horizontal-scroll-indicator.md) + - [Http Polling](recipes/http-polling.md) + - [Lockscreen](recipes/lockscreen.md) + - [Matrix Digital Rain](recipes/matrix-digital-rain.md) + - [Memory Game](recipes/memory-game.md) + - [Mine Sweeper Game](recipes/mine-sweeper-game.md) + - [Platform Jumper Game](recipes/platform-jumper-game.md) + - [Progress Bar](recipes/progressbar.md) + - [Save Indicator](recipes/save-indicator.md) + - [Smart Counter](recipes/smartcounter.md) + - [Space Invaders Game](recipes/space-invaders-game.md) + - [Stop Watch](recipes/stop-watch.md) + - [Swipe To Refresh](recipes/swipe-to-refresh.md) + - [Tank Battle Game](recipes/tank-battle-game.md) + - [Tetris Game](recipes/tetris-game.md) + - [Type Ahead](recipes/type-ahead.md) + - [Uncover Image Game](recipes/uncover-image-game.md) +- [Concepts](concepts/README.md) + - [RxJS Primer](concepts/rxjs-primer.md) + - [Get started transforming streams with map, pluck, and mapTo](concepts/get-started-transforming.md) + - [Time based operators comparison](concepts/time-based-operators-comparison.md) + - [RxJS v5 -> v6 Upgrade](concepts/rxjs5-6.md) diff --git a/book.json b/book.json index a79aa724..b6357175 100644 --- a/book.json +++ b/book.json @@ -21,12 +21,25 @@ "url": "https://github.com/btroncone/learn-rxjs/" }, "ga": { - "token": "UA-54001708-2" + "token": "G-VR4XDP7ZBH" }, "github-buttons": { - "repo": "btroncone/learn-rxjs", - "types": ["star", "watch"], - "size": "large" + "buttons": [ + { + "user": "btroncone", + "repo": "learn-rxjs", + "type": "star", + "size": "small", + "count": true + }, + { + "user": "btroncone", + "repo": "learn-rxjs", + "type": "watch", + "size": "small", + "count": true + } + ] } } } diff --git a/concepts/README.md b/concepts/README.md index c3627632..550aba95 100644 --- a/concepts/README.md +++ b/concepts/README.md @@ -4,5 +4,7 @@ Short explanations of common RxJS scenarios and use-cases. ### Contents -* [RxJS v5 -> v6 Upgrade](rxjs5-6.md) -* [Understanding Operator Imports](operator-imports.md) +- [RxJS Primer](rxjs-primer.md) +- [Get started transforming streams with map, pluck, and mapTo](get-started-transforming.md) +- [Time Based Operator Comparison](time-based-operators-comparison.md) +- [RxJS v5 -> v6 Upgrade](rxjs5-6.md) diff --git a/concepts/get-started-transforming.md b/concepts/get-started-transforming.md new file mode 100644 index 00000000..21a21e33 --- /dev/null +++ b/concepts/get-started-transforming.md @@ -0,0 +1,408 @@ +# Get started transforming streams with map, pluck, and mapTo + +When working with observables one of the most common use cases you will +encounter is the need to transform a stream of some value type into a stream of +another value type. For instance, you may have an observable of click events +that you wish to transform into an observable of objects containing just the +`clientX` and `clientY` coordinates. Or maybe you need to extract a value from a +stream of input events to perform a calculation or initiate a request. Or +perhaps you just want to extract a single property from an object, like a key +code, to perform another action down the (pipe)line. The scenarios for +transforming streams are endless. + +In this article, we are going to learn about the most common operator used to +transform streams, the `map` operator. We will start by taking a look at +`Array.map` to build a general understanding of the `map` operation. Next, we +will explore how we can apply this approach to observables by using the RxJS +`map` operator. Finally, we will check out a few helper operators that can be +used in place of `map` should the right scenario present itself, exploring +common use cases along the way. Let's get started! + +## Introducing `map` + +If you have spent time working with JavaScript arrays you may already be +familiar with `Array.map`. When dealing with arrays, the `map` method lets you +transform an array by applying a provided function (often referred to as a +'projection' function) to each item within the array. For instance, let's say we +have an array of numbers `1-5`: + +```js +const numbers = [1, 2, 3, 4, 5]; +``` + +If we wanted to transform this into an array of each number multiplied by ten, +we could use the `map` method. To do this, we call `map` on our numbers array, +passing it a function which will be invoked with each value of the source array, +returning the number multiplied by ten: + +```js +const numbers = [1, 2, 3, 4, 5]; +const numbersTimesTen = numbers.map(number => number * 10); + +// [10,20,30,40,50] +console.log(numbersTimesTen); +``` + +The `map` method does not mutate the existing array, but instead returns a new +array. For example, if we were to log the `numbers` array after calling `map`, +we can see that it's unchanged: + +```js +const numbers = [1, 2, 3, 4, 5]; +const numbersTimesTen = numbers.map(number => number * 10); + +// [10,20,30,40,50] +console.log(numbersTimesTen); + +// [1,2,3,4,5] +console.log(numbers); +``` + +To understand this better, let's walk through what a naive implementation of +`Array.map` could look like. + +1. We create a new array. +2. For every item contained in the source array we apply the provided function. +3. We then push the result of this function to a temporary `resultArray`. +4. After doing this for every item, we return the new array. + +```js +Array.prototype.map = function(projectFn) { + let resultArray = []; + // loop through each item + this.forEach(item => { + // apply the provided project function + let result = projectFn(item); + // push the result to our new array + resultArray.push(result); + }); + // return the array containing transformed values + return resultArray; +}; +``` + +While the +[real implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) +of `Array.map` includes features like index tracking and proper error +management, this gives us a general sense of how things work behind the scenes. + +{% hint style="info" %} + +RxJS also offers Observable variants of other popular array methods, like +[`filter`](../operators/filtering/filter.md), +[`reduce`](../operators/transformation/reduce.md), and +[`find`](../operators/filtering/find.md)! + +{% endhint %} + +So what are some other common scenarios where we could put the `map` method to +use? Using `Array.map`, we may also want to transform objects. For instance, +suppose we have an array of objects with a first and last name property and we +want to tack on a full name property to each object. We could accomplish this by +supplying a function that accepts each object and _maps_ it to a new object that +includes all current properties plus the new `fullName` property. In this +example we are using the +[object spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) +and +[template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), +but you could also explicitly rewrite the properties: + +```js +const people = [ + { firstName: 'Brian', lastName: 'Troncone' }, + { firstName: 'Todd', lastName: 'Motto' } +]; +const peopleWithFullName = people.map(person => ({ + ...person, + fullName: `${person.firstName} ${person.lastName}` +})); + +// [{ firstName: 'Brian', lastName: 'Troncone', fullName: 'Brian Troncone' }, {firstName: 'Todd', lastName: 'Motto', fullName: 'Todd Motto' }] +console.log(peopleWithFullName); +``` + +Another common use case for `map` is extracting a single property from an +object. For example, given the sample above suppose we decided we only _really_ +need the last name property for display. Instead of our function returning a new +object, we can instead return just the property we need from that object: + +```js +const people = [ + { firstName: 'Brian', lastName: 'Troncone' }, + { firstName: 'Todd', lastName: 'Motto' } +]; +const lastNames = people.map(person => person.lastName); + +// [ 'Troncone', 'Motto' ] +console.log(lastNames); +``` + +At this point, we are transforming an array of people objects into an array of +string last names. + +As you can see, the `map` method is extremely flexible with a wide variety of +use cases, but how does this translate to `map` with RxJS, and when would you +put this to use with observables? + +## Introducing the RxJS `map` operator + +![map](https://drive.google.com/uc?export=view&id=1fbxzA5p0FFFUTo0dOAanoq6s1LBip3Ga) + +The `map` operator in RxJS transforms values emitted from the source observable +based on a provided projection function. This is similar to `Array.map`, except +we are operating on each value emitted from an observable as it occurs rather +than each value contained within an array. + +For instance, let's start with our initial example, but instead of transforming +an array of numbers let's transform an observable of numbers. To do this, we +will use the [`from`](../operators/creation/from.mdl) creation operator to first +convert our numbers array into an observable: + +```js +import { from } from 'rxjs'; + +const numbers = [1, 2, 3, 4, 5]; +const number$ = from(numbers); +``` + +When provided an array, the `from` creation operator will loop through +(synchronously) emitting each item in sequence. When we subscribe we can see +each value printed to the console: + +```js +import { from } from 'rxjs'; + +const numbers = [1, 2, 3, 4, 5]; +const number$ = from(numbers); + +/* + * 1 + * 2 + * 3 + * 4 + * 5 + */ +number$.subscribe(console.log); +``` + +**Tip:** _If you want to see how `from` handles each value type behind the +scenes, you can check out the +[`subscribeTo`](https://github.com/ReactiveX/rxjs/blob/e17df333fec66ea3d79e3f70565064f757c3a4fe/src/internal/util/subscribeTo.ts#L14-L29), +and associated helper functions. In this case, +[subscribeToArray](https://github.com/ReactiveX/rxjs/blob/e17df333fec66ea3d79e3f70565064f757c3a4fe/src/internal/util/subscribeToArray.ts#L7-L12) +is used. This same helper function is also used to deal with non-observable +return values of flattening operators, such as +[mergeMap](../operators/transformation/mergemap.md)_ + +If we then wanted to transform this observable into the emitted values +multiplied by ten, we could use the `map` operator. Just like `Array.map`, the +`map` operator accepts a project function which describes how each value from +the source will be transformed. In this case, we will provide a function that +accepts the emitted value from the source observable and returns that value +multipled: + +```js +import { from } from 'rxjs'; + +const numbers = [1, 2, 3, 4, 5]; +const number$ = from(numbers); +const numbersMultipliedByTen$ = number$.pipe(map(number => number * 10)); + +/* + * 10 + * 20 + * 30 + * 40 + * 50 + */ +numbersMultipliedByTen$.subscribe(console.log); +``` + +Instead of the function being applied to each item of an array, before a new +array is returned, with observables the project function is applied and the +result emitted in real-time as values blast through your streams. We can confirm +this in the RxJS source code by seeing the function we provide is invoked, with +the result being passed on to the subscriber (destination): + +[(Source Code)](https://github.com/ReactiveX/rxjs/blob/e17df333fec66ea3d79e3f70565064f757c3a4fe/packages/rxjs/src/internal/operators/map.ts#L81-L91) + +```ts +protected _next(value: T) { + let result: any; + try { + // project is the function we pass to the map operator + result = this.project.call(this.thisArg, value, this.count++); + } catch (err) { + // forward any errors that occur + this.destination.error(err); + return; + } + // emit the result of calling our project function to the subscriber + this.destination.next(result); + } +``` + +Similar to our array example with objects, we may also want to transform an +observable of objects with the `map` operator. For instance, suppose we have an +observable of `click` events that we wish to transform into an observable of +objects containing just the `clientX` and `clientY` coordinates of these events. +To do this we could apply the `map` operator, providing a function that returns +an object with just these properties: + +```js +import { fromEvent } from 'rxjs'; +import { map } from 'rxjs/operators'; + +const click$ = fromEvent(document, 'click'); + +click$ + .pipe( + map(event => ({ + x: event.clientX, + y: event.clientY + })) + // { x: 12, y: 45 }, { x: 23, y: 132 } + ) + .subscribe(console.log); +``` + +There may also be times we want to grab a single property from an object using +`map`. For example, we may have a use case for an observable of just the `code` +property from `keyup` events, so we can take action when the user types a +particular character or key. To do this we can apply the `map` operator +returning just the property we are interested in: + +```js +import { fromEvent } from 'rxjs'; +import { map } from 'rxjs/operators'; + +const keyup$ = fromEvent(document, 'keyup'); + +keyup$ + .pipe(map(event => event.code)) + // 'Space', 'Enter' + .subscribe(console.log); +``` + +While `map` works perfectly fine in these situations, RxJS also surfaces helper +operators for cases where you just want to _map_ to a single property or when +you _always_ want to map to the same value on any event. First, let's take a +look at the single property scenario. + +## Extract a single property with `pluck` + +![pluck](https://drive.google.com/uc?export=view&id=1-TdTqWb-qoif4FJojY3sC0oS81EkB65z) + +RxJS features many operators that are simply shortcuts for other operators. For +example, any time we just want to grab a single property from an emitted value, +instead of using `map` we could use `pluck`. The `pluck` operator accepts a list +of values which describe the property you wish to grab from the emitted item. +For instance, using our event code example from above we could use `pluck` +instead of `map` to extract the `code` property from the `event` object: + +```js +import { fromEvent } from 'rxjs'; +import { pluck } from 'rxjs/operators'; + +const keyup$ = fromEvent(document, 'keyup'); + +keyup$ + .pipe(pluck('code')) + // 'Space', 'Enter' + .subscribe(console.log); +``` + +We can also pass `pluck` multiple values to grab a nested property within an +object. For example, if we wanted to grab the `nodeName` from the `target` +element on click, we could pass both of these properties to `pluck` in order: + +```js +import { fromEvent } from 'rxjs'; +import { pluck } from 'rxjs/operators'; + +const click$ = fromEvent(document, 'click'); + +click$ + .pipe(pluck('target', 'nodeName')) + // 'DIV', 'MAIN' + .subscribe(console.log); +``` + +Like many other helper operators in RxJS, behind the scenes `pluck` is simply +reusing the `map` operator, passing it a function to grab the appropriate +property: + +[(Source Code)](https://github.com/ReactiveX/rxjs/blob/e17df333fec66ea3d79e3f70565064f757c3a4fe/packages/rxjs/src/internal/operators/pluck.ts#L48-L54) + +```ts +export function pluck(...properties: string[]): OperatorFunction { + const length = properties.length; + if (length === 0) { + throw new Error('list of properties cannot be empty.'); + } + return (source: Observable) => + map(plucker(properties, length))(source as any); +} +``` + +Functionally, `map` and `pluck` will operate the same in these scenarios, I +would suggest using whichever you feel most comfortable reading at a glance. + +Lastly, there may also be times where you **always** want to map to a single +value, no matter the input. For these situations, you can use the `mapTo` +operator. + +## Mapping to a constant value with `mapTo` + +![mapTo](https://drive.google.com/uc?export=view&id=1329klWDEvjgjh3JVJkzHoZDc7CpcLZvO) + +For situations where you find yourself always wanting to map to a specific +value, one way you could handle it is by simply using `map` and ignoring the +input: + +```js +import { fromEvent } from 'rxjs'; +import { map } from 'rxjs/operators'; + +const click$ = fromEvent(document, 'click'); + +click$ + .pipe(map(() => 'You clicked!')) + // 'You clicked!', 'You clicked!' + .subscribe(console.log); +``` + +While this works, the wrapping function isn't necessary since we are ignoring +the received value. For these scenarios you can replace `map` with `mapTo`, and +simply provide the value you wish to return on all emissions: + +```js +import { fromEvent } from 'rxjs'; +import { mapTo } from 'rxjs/operators'; + +const click$ = fromEvent(document, 'click'); + +click$ + .pipe(mapTo('You clicked!')) + // 'You clicked!', 'You clicked!' + .subscribe(console.log); +``` + +Like `pluck`, `mapTo` provides no real benefit functionally over returning a +constant value with `map`, but syntactically it may prove slightly easier to +consume and read at a glance. + +## Conclusion + +In conclusion, `map` is a versatile operator which lets you transform a stream +using a provided projection function. Whether it's mapping to a keycode, value +updates from an input box, or reshaping an object, `map` will be one of the most +used operators in your day-to-day RxJS toolbox. For scenarios where you just +need to map to a single property, or always want to map to a constant value, you +can also check out the [`pluck`](../operators/transformation/pluck.md) and +[`mapTo`](../operators/transformation/mapto.md) helper operators. + +For a full list of transformation operators with examples, including operators +which manage mapping to more complex values such as other observables, check out +the [transformation operator section](../operators/transformation/README.md). We +will explore these topics in detail in future posts! diff --git a/concepts/operator-imports.md b/concepts/operator-imports.md deleted file mode 100644 index efad80f5..00000000 --- a/concepts/operator-imports.md +++ /dev/null @@ -1,126 +0,0 @@ -# Understanding Operator Imports - -A problem you may have run into in the past when consuming or creating a public library that depends on RxJS is handling operator inclusion. The most predominant way to include operators in your project is to import them like below: - -```js -import 'rxjs/add/operator/take'; -``` - -This adds the imported operator to the `Observable` prototype for use throughout your project: - -[(Source)](https://github.com/ReactiveX/rxjs/blob/master/src/add/operator/take.ts) - -```js -import { Observable } from '../../Observable'; -import { take } from '../../operator/take'; - -Observable.prototype.take = take; - -declare module '../../Observable' { - interface Observable { - take: typeof take; - } -} -``` - -This method is generally *OK* for private projects and modules, the issue arises when you are using these imports in say, an [npm](https://www.npmjs.com/) package or library to be consumed throughout your organization. - -
- -### A Quick Example - -To see where a problem can spring up, let's imagine **Person A** is creating a public Angular component library. In this library you need a few operators so you add the typical imports: - -*some-public-library.ts* -```js -import 'rxjs/add/operator/take'; -import 'rxjs/add/operator/concatMap'; -import 'rxjs/add/operator/switchMap'; -``` - -**Person B** comes along and includes your library. They now have access to these operators even though they did not personally import them. *Probably not a huge deal but it can be confusing.* You use the library and operators, life goes on... - -A month later **Person A** decides to update their library. They no longer need `switchMap` or `concatMap` so they remove the imports: - -*some-public-library.ts* -```js -import 'rxjs/add/operator/take'; -``` - -**Person B** upgrades the dependency, builds their project, which now fails. They never included `switchMap` or `concatMap` themselves, it was **just working** based on the inclusion of a 3rd party dependency. If you were not aware this could be an issue it may take a bit to track down. - -### The Solution - -Instead of importing operators like: - -```js -import 'rxjs/add/operator/take'; -``` - -We can instead import them like: - -```js -import { take } from 'rxjs/operator/take'; -``` - -This keeps them off the `Observable` prototype and let's us call them directly: - -```js -import { take } from 'rxjs/operator/take'; -import { of } from 'rxjs/observable/of'; - -take.call( - of(1,2,3), - 2 -); -``` - -This quickly gets **ugly** however, imagine we have a longer chain: - -```js -import { take } from 'rxjs/operator/take'; -import { map } from 'rxjs/operator/map'; -import { of } from 'rxjs/observable/of'; - -map.call( - take.call( - of(1,2,3), - 2 - ), - val => val + 2 -); -``` - -Pretty soon we have a block of code that is near impossible to understand. How can we get the best of both worlds? - -### Pipeable Operators - -RxJS now comes with a [`pipe`](https://github.com/ReactiveX/rxjs/blob/755df9bf908108974e38aaff79887279f2cde008/src/Observable.ts#L305-L329) helper on `Observable` that alleviates the pain of not having operators on the prototype. We can take the ugly block of code from above: - -```js -import { take, map } from 'rxjs/operators'; -import { of } from 'rxjs/observable/of'; - -map.call( - take.call( - of(1,2,3), - 2 - ), - val => val + 2 -); -``` - -And transform it into: - -```js -import { take, map } from 'rxjs/operators'; -import { of } from 'rxjs/observable/of'; - -of(1,2,3) - .pipe( - take(2), - map(val => val + 2) - ); -``` - -Much easier to read, right? This also has the benefit of greatly reducing the RxJS bundle size in your application. For more on this, check out [Ashwin Sureshkumar's](https://twitter.com/Sureshkumar_Ash) excellent article [Reduce Angular app bundle size using lettable operators](https://hackernoon.com/rxjs-reduce-bundle-size-using-lettable-operators-418307295e85). diff --git a/concepts/rxjs-primer.md b/concepts/rxjs-primer.md new file mode 100644 index 00000000..f5a3240c --- /dev/null +++ b/concepts/rxjs-primer.md @@ -0,0 +1,524 @@ +# RxJS Primer + +Brand new to RxJS? In this article we will take a crash course through all the +major concepts you will need to begin getting a grasp on, and start being +productive with RxJS. Hold on tight and let's get started! + +## What is an Observable? + +An observable represents a stream, or source of data that can arrive over time. +You can create an observable from nearly anything, but the most common use case +in RxJS is from events. This can be anything from mouse moves, button clicks, +input into a text field, or even route changes. The easiest way to create an +observable is through the built in creation functions. For example, we can use +the [`fromEvent`](../operators/creation/fromevent.md) helper function to create +an observable of mouse click events: + +```js +// import the fromEvent operator +import { fromEvent } from 'rxjs'; + +// grab button reference +const button = document.getElementById('myButton'); + +// create an observable of button clicks +const myObservable = fromEvent(button, 'click'); +``` + +At this point we have an observable but it's not doing anything. **This is +because observables are cold, or do not activate a producer (like wiring up an +event listener), until there is a...** + +## Subscription + +Subscriptions are what set everything in motion. You can think of this like a +faucet, you have a stream of water ready to be tapped (observable), someone just +needs to turn the handle. In the case of observables, that role belongs to the +`subscriber`. + +To create a subscription, you call the `subscribe` method, supplying a function +(or object) - also known as an `observer`. This is where you can decide how to +**react**(-ive programming) to each event. Let's walk through what happens in +the previous scenario when a subscription is created: + +```js +// import the fromEvent operator +import { fromEvent } from 'rxjs'; + +// grab button reference +const button = document.getElementById('myButton'); + +// create an observable of button clicks +const myObservable = fromEvent(button, 'click'); + +// for now, let's just log the event on each click +const subscription = myObservable.subscribe(event => console.log(event)); +``` + +In the example above, calling `myObservable.subscribe()` will: + +1. Set up an event listener on our button for click events. +2. Call the function we passed to the subscribe method (observer) on each click + event. +3. Return a subscription object with an `unsubscribe` which contains clean up + logic, like removing appropriate event listeners. + +The subscribe method also accepts an object map to handle the case of error or +completion. You probably won't use this as much as simply supplying a function, +but it's good to be aware of should the need arise: + +```js +// instead of a function, we will pass an object with next, error, and complete methods +const subscription = myObservable.subscribe({ + // on successful emissions + next: event => console.log(event), + // on errors + error: error => console.log(error), + // called once on completion + complete: () => console.log('complete!') +}); +``` + +It's important to note that each subscription will create a new execution +context. This means calling `subscribe` a second time will create a new event +listener: + +```js +// addEventListener called +const subscription = myObservable.subscribe(event => console.log(event)); + +// addEventListener called again! +const secondSubscription = myObservable.subscribe(event => console.log(event)); + +// clean up with unsubscribe +subscription.unsubscribe(); +secondSubscription.unsubscribe(); +``` + +By default, a subscription creates a one on one, one-sided conversation between +the observable and observer. This is like your boss (the observable) yelling +(emitting) at you (the observer) for merging a bad PR. This is also known as +**unicasting**. If you prefer a conference talk scenario - one observable, many +observers - you will take a different approach which includes **multicasting** +with `Subjects` (either explicitly or behind the scenes). More on that in a +future article! + +It's worth noting that when we discuss an Observable source emitting data to +observers, this is a push based model. The source doesn't know or care what +subscribers do with the data, it simply pushes it down the line. + +While working on a stream of events is nice, it's only so useful on its own. +**What makes RxJS the "lodash for events" are its...** + +## Operators + +Operators offer a way to manipulate values from a source, returning an +observable of the transformed values. Many of the RxJS operators will look +familiar if you are used to JavaScripts `Array` methods. For instance, if you +want to transform emitted values from an observable source, you can use +[`map`](../operators/transformation/map.md): + +```js +import { of } from 'rxjs'; +import { map } from 'rxjs/operators'; +/* + * 'of' allows you to deliver values in a sequence + * In this case, it will emit 1,2,3,4,5 in order. + */ +const dataSource = of(1, 2, 3, 4, 5); + +// subscribe to our source observable +const subscription = dataSource + .pipe( + // add 1 to each emitted value + map(value => value + 1) + ) + // log: 2, 3, 4, 5, 6 + .subscribe(value => console.log(value)); +``` + +Or if you want to filter for specific values, you can use +[`filter`](../operators/filtering/filter.md): + +```js +import { of } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +const dataSource = of(1, 2, 3, 4, 5); + +// subscribe to our source observable +const subscription = dataSource + .pipe( + // only accept values 2 or greater + filter(value => value >= 2) + ) + // log: 2, 3, 4, 5 + .subscribe(value => console.log(value)); +``` + +In practice, if there is a problem you need to solve, it's more than likely +**there is an operator for that**. And while the sheer number of operators can +be overwhelming as you begin your RxJS journey, you can narrow it down to a +small handful (and we will) to start being effective. Over time, you will come +to appreciate the flexibility of the operator library when obscure scenarios +inevitably arrive. + +**One thing you may have noticed in the example above, is operators exist within +a...** + +## Pipe + +The `pipe` function is the assembly line from your observable data source +through your operators. Just like raw material in a factory goes through a +series of stops before it becomes a finished product, source data can pass +through a `pipe`-line of operators where you can manipulate, filter, and +transform the data to fit your use case. It's not uncommon to use 5 (or more) +operators within an observable chain, contained within the `pipe` function. + +For instance, a typeahead solution built with observables may use a group of +operators to optimize both the request and display process: + +```js +// observable of values from a text box, pipe chains operators together +inputValue + .pipe( + // wait for a 200ms pause + debounceTime(200), + // if the value is the same, ignore + distinctUntilChanged(), + // if an updated value comes through while request is still active cancel previous request and 'switch' to new observable + switchMap(searchTerm => typeaheadApi.search(searchTerm)) + ) + // create a subscription + .subscribe(results => { + // update the dom + }); +``` + +**But how do you know which operator fits your use-case? The good news is...** + +## Operators can be grouped into common categories + +The first stop when looking for the correct operator is finding a related +category. Need to filter data from a source? Check out the +[`filtering`](../operators/filtering/README.md) operators. Trying to track down +a bug, or debug the flow of data through your observable stream? There are +[`utility`](../operators/utility/README.md) operators that will do the trick. +**The operator categories include...** + +### [Creation operators](../operators/creation/README.md) + +These operators allow the creation of an observable from nearly anything. From +generic to specific use-cases you are free to turn everything into a stream. + +For example, suppose we are creating a progress bar as a user scrolls through an +article. We could turn the scroll event into a stream by utilizing the +[`fromEvent`](../operators/creation/fromevent.md) operator: + +```js +fromEvent(scrollContainerElement, 'scroll') + .pipe( + // we will discuss cleanup strategies like this in future article + takeUntil(userLeavesArticle) + ) + .subscribe(event => { + // calculate and update DOM + }); +``` + +The most commonly used creation operators are +[`of`](../operators/creation/of.md), [`from`](../operators/creation/from.md), +and [`fromEvent`](../operators/creation/fromevent.md). + +### [Combination operators](../operators/combination/README.md) + +The combination operators allow the joining of information from multiple +observables. Order, time, and structure of emitted values is the primary +variation among these operators. + +For example, we can combine updates from multiple data sources to perform a +calculation: + +```js +// give me the last emitted value from each source, whenever either source emits +combineLatest(sourceOne, sourceTwo).subscribe( + ([latestValueFromSourceOne, latestValueFromSourceTwo]) => { + // perform calculation + } +); +``` + +The most commonly used combination operators are +[`combineLatest`](../operators/combination/combinelatest.md), +[`concat`](../operators/combination/concat.md), +[`merge`](../operators/combination/merge.md), +[`startWith`](../operators/combination/startwith.md), and +[`withLatestFrom`](../operators/combination/withlatestfrom.md). + +### [Error handling operators](../operators/error_handling/README.md) + +The error handling operators provide effective ways to gracefully handle errors +and perform retries, should they occur. + +For example, we can use +[`catchError`](../operators/error_handling/catch.md) to safeguard against +failed network requests: + +```js +source + .pipe( + mergeMap(value => { + return makeRequest(value).pipe( + catchError(handleErrorByReturningObservable) + ); + }) + ) + .subscribe(value => { + // take action + }); +``` + +The most commonly used error handling operators is +[`catchError`](../operators/error_handling/catch.md). + +### [Filtering operators](../operators/filtering/README.md) + +The filtering operators provide techniques for accepting - or declining - values +from an observable source and dealing with backpressure, or the build up of +values within a stream. + +For example, we can use the [`take`](../operators/filtering/take.md) operator to +capture only the first `5` emitted values from a source: + +```js +source.pipe(take(5)).subscribe(value => { + // take action +}); +``` + +The most commonly used filtering operators are +[`debounceTime`](../operators/filtering/debouncetime.md), +[`distinctUntilChanged`](../operators/filtering/distinctuntilchanged.md), +[`filter`](../operators/filtering/filter.md), +[`take`](../operators/filtering/take.md), and +[`takeUntil`](../operators/filtering/takeuntil.md). + +### [Multicasting operators](../operators/multicasting/README.md) + +In RxJS observables are cold, or unicast (one source per subscriber) by default. +These operators can make an observable hot, or multicast, allowing side-effects +to be shared among multiple subscribers. + +For example, we may want late subscribers to share, and receive the last emitted +value from an active source: + +```js +const source = data.pipe(shareReplay()); + +const firstSubscriber = source.subscribe(value => { + // perform some action +}); + +// sometime later... + +// second subscriber gets last emitted value on subscription, shares execution context with 'firstSubscriber' +const secondSubscriber = source.subscribe(value => { + // perform some action +}); +``` + +The most commonly used multicasting operator is +[`shareReplay`](../operators/multicasting/sharereplay.md). + +### [Transformation operators](../operators/transformation/README.md) + +Transforming values as they pass through an operator chain is a common task. +These operators provide transformation techniques for nearly any use-case you +will encounter. + +For example, we may want to accumulate a state object from a source over time, +similar to [Redux](https://redux.js.org/): + +```js +source + .pipe( + scan((accumulatedState, currentState) => { + return { ...accumulatedState, ...currentState }; + }) + ) + .subscribe(); +``` + +The most commonly used transformation operators are +[`concatMap`](../operators/transformation/concatmap.md), +[`map`](../operators/transformation/map.md), +[`mergeMap`](../operators/transformation/mergemap.md), +[`scan`](../operators/transformation/scan.md), and +[`switchMap`](../operators/transformation/switchmap.md). + +## Operators have common behaviors + +While operators can be grouped into common categories, operators within a +category often share common behavior. By recognizing this common behavior you +can start creating a +[_'choose your own operator'_ tree](https://rxjs-dev.firebaseapp.com/operator-decision-tree) +in your mind. + +**For instance, a large amount of operators can be grouped into...** + +#### Operators that flatten + +Or, in other words, operators that manage the subscription of an inner +observable, emitting those values into a single observable source. One common +use case for flattening operators is handling HTTP requests in a observable or +promise based API, but that is really just scratching the surface: + +```js +fromEvent(button, 'click') + .pipe( + mergeMap(value => { + // this 'inner' subscription is managed by mergeMap, with response value emitted to observer + return makeHttpRequest(value); + }) + ) + .subscribe(response => { + // do something + }); +``` + +**We can then divide the flattening operators into common behaviors like...** + +#### Operators that `switch` + +Like a light switch, `switch` based operators will turn off (unsubscribe) the +current observable and turn on a new observable on emissions from the source. +Switch operators are useful in situations you don't want (or need) more than one +active observable at a time: + +```js +inputValueChanges + // only the last value is important, if new value comes through cancel previous request / observable + .pipe( + // make GET request for data + switchMap(requestObservable) + ) + .subscribe(); +``` + +Switch based operators include `switchAll`, +[`switchMap`](../operators/transformation/switchmap.md), and +[`switchMapTo`](../operators/transformation/switchmapto.md). + +#### Operators that `concat` + +Comparable to a line at the ATM machine, the next transaction can't begin until +the previous completes. In observable terms, only one subscription will occur at +a time, in order, triggered by the completion of the previous. These are useful +in situations where order of execution is important: + +```js +concat( + firstObservable, + // will begin when 'firstObservable` completes + secondObservable, + // will begin when 'secondObservable` completes + thirdObservable +).subscribe(values => { + // take action +}); +``` + +Concat based operators include [`concat`](../operators/combination/concat.md), +[`concatAll`](../operators/combination/concatall.md), +[`concatMap`](../operators/transformation/concatmap.md), and +[`concatMapTo`](../operators/transformation/concatmapto.md). + +#### Operators that `merge` + +Like your merging lane on the interstate, merge based operators support multiple +active observables flowing into one lane in a first come first serve basis. +Merge operators are useful in situations where you want to trigger an action +when an event from one of many sources occurs: + +```js +merge(firstObservable, secondObservable) + // any emissions from first or second observable as they occur + .pipe(mergeMap(saveActivity)) + .subscribe(); +``` + +Merge based operators include [`merge`](../operators/combination/merge.md), +[`mergeMap`](../operators/transformation/mergemap.md), `mergeMapTo` and +[`mergeAll`](../operators/combination/mergeall.md). + +## Other similarities between operators + +There are also operators that share a similar goal but offer flexibility in +their triggers. For instance, for unsubscribing from an observable after a +specific condition is met, we could use: + +1. [`take`](../operators/filtering/take.md) when we know we only ever want `n` + values. +2. [`takeLast`](../operators/filtering/takelast.md) when you just want the last + `n` values. +3. [`takeWhile`](../operators/filtering/takewhile.md) when we have a predicate + expression to supply. +4. [`takeUntil`](../operators/filtering/takeuntil.md) when we only want the + source to remain active until another source emits. + +While the number of RxJS operators can seem overhelming at first, these common +behaviors and patterns can bridge the gap rather quickly while learning RxJS. + +## What does this get me? + +As you become more familiar with push based programming through Observables, you +can begin to model all async behavior in your applications through observable +streams. This opens up simple solutions and flexibility for notably complex +behavior. + +For instance, suppose we wanted to make a request which saved user activity when +they answered a quiz question. Our initial implementation may use the +[`mergeMap`](../operators/transformation/mergemap.md) operator, which fires off +a save request on each event: + +```js +const formEvents = fromEvent(formField, 'click'); +const subscription = formEvents + .pipe( + map(convertToAppropriateValue), + mergeMap(saveRequest) + ) + .subscribe(); +``` + +Later, it's determined that we need to ensure order of these saves. Armed with +the knowledge of operator behavior from above, instead of implementing a complex +queueing system we can instead replace the +[`mergeMap`](../operators/transformation/mergemap.md) operator with +[`concatMap`](../operators/transformation/concatmap.md) and push up our changes: + +```js +const formEvents = fromEvent(formField, 'click'); +const subscription = formEvents + .pipe( + map(convertToAppropriateValue), + // now the next request won't begin until the previous completes + concatMap(saveRequest) + ) + .subscribe(); +``` + +With the change of one word we are now queueing our event requests, and this is +just scratching the surface of what is possible! + +## Keep Going! + +Learning RxJS can be intimidating, but it's a path I promise is worth the +investment. If some of these concepts are still fuzzy (or make no sense at +all!), don't worry! It will all click soon. + +Start checking out the operators on the left hand side of the site for common +examples and use-cases, as well as the additional +[introductory resources](../README.md#introductory-resources) we have collected +from across the web. Good luck and enjoy your journey to becoming a reactive +programming expert! diff --git a/concepts/rxjs5-6.md b/concepts/rxjs5-6.md index 8e6b821a..6ea40840 100644 --- a/concepts/rxjs5-6.md +++ b/concepts/rxjs5-6.md @@ -8,7 +8,7 @@ handy: TsLint rules for migration to RxJS 6. Auto-update project for new import paths and transition to pipeable operators. -### [RxJS v5.x to v6 Update Guide](https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/migration.md) +### [RxJS v5.x to v6 Update Guide](https://github.com/ReactiveX/rxjs/blob/6.6.7/docs_app/content/guide/v6/migration.md) Comprehensive guide for updating your project from RxJS v5 to 6 diff --git a/concepts/time-based-operators-comparison.md b/concepts/time-based-operators-comparison.md new file mode 100644 index 00000000..e3b2236f --- /dev/null +++ b/concepts/time-based-operators-comparison.md @@ -0,0 +1,78 @@ +# Time based operators comparison + +RxJS offers a rich selection of time based operators but this diversity can come at cost when choosing the right operator for a task at hand. Below is a visual comparison of popular time based operators. + +Compared operators: + +- [auditTime](../operators/filtering/audittime.md) +- [bufferTime](../operators/transformation/buffertime.md) +- [debounceTime](../operators/filtering/debouncetime.md) +- sampleTime +- [throttleTime](../operators/filtering/throttletime.md) + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-time-based-operators-comparison?file=index.ts&devtoolsheight=100)) + +```js +/* +interval ^0-------1-------2-------3-------4-------5-------6-------7-------8-------9------| +auditTime ^------------------------3-------------------------------7----------------------| +bufferTime ^----------------[0,1]-------------------[2,3,4]-----------------[5,6,7]-[8,9]--| +debounceTime ^------------------------------------------------------------------------9------| +sampleTime ^----------------1-----------------------4-----------------------7--------------| +throttleTime ^0-------------------------------4-------------------------------8--------------| +*/ + +// RxJS v6+ +import { interval, merge } from 'rxjs'; +import { auditTime, bufferTime, debounceTime, sampleTime, throttleTime, tap, take } from 'rxjs/operators'; + +const takeValue = 10; +const intrvl = 1000; +const time = 3000; + +const intervaled = (operator, operatorName) => + interval(intrvl) + .pipe( + take(takeValue), + operator, + tap(x => console.log(`${operatorName}:${x}`)) + ); + +merge( + interval(intrvl).pipe(take(takeValue), tap(v => console.log(`i: ${v}`))), + intervaled(auditTime(time), "auditTime"), + intervaled(bufferTime(time), "bufferTime"), + intervaled(debounceTime(time), "debounceTime"), + intervaled(sampleTime(time), "sampleTime"), + intervaled(throttleTime(time), "throttleTime") +).subscribe(); + +// output +/* +i: 0 +throttleTime:0 +i: 1 +i: 2 +bufferTime:0,1 +sampleTime:1 +i: 3 +auditTime:3 +i: 4 +throttleTime:4 +i: 5 +bufferTime:2,3,4 +sampleTime:4 +i: 6 +i: 7 +auditTime:7 +i: 8 +bufferTime:5,6,7 +sampleTime:7 +throttleTime:8 +i: 9 +bufferTime:8,9 +debounceTime:9 + +*/ +``` diff --git a/operators/README.md b/operators/README.md index 8a958b47..1d8cda8d 100644 --- a/operators/README.md +++ b/operators/README.md @@ -1,6 +1,6 @@ -# RxJS 5 Operators By Example +# RxJS Operators By Example -A complete list of RxJS 5 operators with clear explanations, relevant resources, +A complete list of RxJS operators with clear explanations, relevant resources, and executable examples. _[Prefer a complete list in alphabetical order?](complete.md)_ @@ -8,41 +8,51 @@ _[Prefer a complete list in alphabetical order?](complete.md)_ ### Contents (By Operator Type) - [Combination](combination/README.md) - - [combineAll](combination/combineall.md) - - [combineLatest](combination/combinelatest.md) :star: - - [concat](combination/concat.md) :star: + - [combineLatestAll](combination/combineall.md) + - [combineLatest](combination/combinelatest.md) ⭐ + - [concat](combination/concat.md) ⭐ - [concatAll](combination/concatall.md) + - [endWith](combination/endwith.md) - [forkJoin](combination/forkjoin.md) - - [merge](combination/merge.md) :star: + - [merge](combination/merge.md) ⭐ - [mergeAll](combination/mergeall.md) + - [pairwise](combination/pairwise.md) - [race](combination/race.md) - - [startWith](combination/startwith.md) :star: - - [withLatestFrom](combination/withlatestfrom.md) :star: + - [startWith](combination/startwith.md) ⭐ + - [withLatestFrom](combination/withlatestfrom.md) ⭐ - [zip](combination/zip.md) - [Conditional](conditional/README.md) - [defaultIfEmpty](conditional/defaultifempty.md) - [every](conditional/every.md) + - [iif](conditional/iif.md) + - [sequenceequal](conditional/sequenceequal.md) - [Creation](creation/README.md) + - [ajax](creation/ajax.md) ⭐ - [create](creation/create.md) + - [defer](creation/defer.md) - [empty](creation/empty.md) - - [from](creation/from.md) :star: + - [from](creation/from.md) ⭐ - [fromEvent](creation/fromevent.md) + - [generate](creation/generate.md) - [interval](creation/interval.md) - - [of](creation/of.md) :star: + - [of](creation/of.md) ⭐ - [range](creation/range.md) - - [throw](creation/throw.md) + - [throwError](creation/throw.md) - [timer](creation/timer.md) - [Error Handling](error_handling/README.md) - - [catch / catchError](error_handling/catch.md) :star: + - [catch / catchError](error_handling/catch.md) ⭐ - [retry](error_handling/retry.md) - [retryWhen](error_handling/retrywhen.md) - [Filtering](filtering/README.md) - [audit](filtering/audit.md) - [auditTime](filtering/audittime.md) - [debounce](filtering/debounce.md) - - [debounceTime](filtering/debouncetime.md) :star: - - [distinctUntilChanged](filtering/distinctuntilchanged.md) :star: - - [filter](filtering/filter.md) :star: + - [debounceTime](filtering/debouncetime.md) ⭐ + - [distinct](filtering/distinct.md) + - [distinctUntilChanged](filtering/distinctuntilchanged.md) ⭐ + - [distinctUntilKeyChanged](filtering/distinctuntilkeychanged.md) + - [filter](filtering/filter.md) ⭐ + - [find](filtering/find.md) - [first](filtering/first.md) - [ignoreElements](filtering/ignoreelements.md) - [last](filtering/last.md) @@ -51,54 +61,63 @@ _[Prefer a complete list in alphabetical order?](complete.md)_ - [skip](filtering/skip.md) - [skipUntil](filtering/skipuntil.md) - [skipWhile](filtering/skipwhile.md) - - [take](filtering/take.md) :star: - - [takeUntil](filtering/takeuntil.md) :star: + - [take](filtering/take.md) ⭐ + - [takeLast](filtering/takelast.md) + - [takeUntil](filtering/takeuntil.md) ⭐ - [takeWhile](filtering/takewhile.md) - [throttle](filtering/throttle.md) - [throttleTime](filtering/throttletime.md) - [Multicasting](multicasting/README.md) - [multicast](multicasting/multicast.md) - [publish](multicasting/publish.md) - - [share](multicasting/share.md) :star: - - [shareReplay](multicasting/sharereplay.md) :star: + - [share](multicasting/share.md) ⭐ + - [shareReplay](multicasting/sharereplay.md) ⭐ - [Transformation](transformation/README.md) - [buffer](transformation/buffer.md) - [bufferCount](transformation/buffercount.md) - - [bufferTime](transformation/buffertime.md) :star: + - [bufferTime](transformation/buffertime.md) ⭐ - [bufferToggle](transformation/buffertoggle.md) - [bufferWhen](transformation/bufferwhen.md) - - [concatMap](transformation/concatmap.md) :star: + - [concatMap](transformation/concatmap.md) ⭐ - [concatMapTo](transformation/concatmapto.md) - [expand](transformation/expand.md) - [exhaustMap](transformation/exhaustmap.md) - [groupBy](transformation/groupby.md) - - [map](transformation/map.md) :star: + - [map](transformation/map.md) ⭐ - [mapTo](transformation/mapto.md) - - [mergeMap / flatMap](transformation/mergemap.md) :star: + - [mergeMap / flatMap](transformation/mergemap.md) ⭐ + - [mergeScan](transformation/mergescan.md) - [partition](transformation/partition.md) - [pluck](transformation/pluck.md) - [reduce](transformation/reduce.md) - - [scan](transformation/scan.md) :star: - - [switchMap](transformation/switchmap.md) :star: + - [scan](transformation/scan.md) ⭐ + - [switchMap](transformation/switchmap.md) ⭐ + - [switchMapTo](transformation/switchmapto.md) + - [toArray](transformation/toarray.md) - [window](transformation/window.md) - [windowCount](transformation/windowcount.md) - [windowTime](transformation/windowtime.md) - [windowToggle](transformation/windowtoggle.md) - [windowWhen](transformation/windowwhen.md) - [Utility](utility/README.md) - - [do / tap](utility/do.md) :star: + - [tap / do](utility/do.md) ⭐ - [delay](utility/delay.md) - [delayWhen](utility/delaywhen.md) - - [finalize / finally](finalize.md) + - [dematerialize](utility/dematerialize.md) + - [finalize / finally](utility/finalize.md) - [let](utility/let.md) - - [toPromise](utility/topromise.md) + - [repeat](utility/repeat.md) + - [repeatWhen](utility/repeatwhen.md) + - [timeInterval](utility/timeinterval.md) - [timeout](utility/timeout.md) + - [timeoutWith](utility/timeoutwith.md) + - [toPromise](utility/topromise.md) -:star: - _commonly used_ +⭐ - _commonly used_ ### Additional Resources - [What Are Operators?](http://reactivex.io/rxjs/manual/overview.html#operators) - :newspaper: - Official Docs + 📰 - Official Docs - [What Operators Are](https://egghead.io/lessons/rxjs-what-rxjs-operators-are) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz diff --git a/operators/combination/README.md b/operators/combination/README.md index cd5a48f7..61b89212 100644 --- a/operators/combination/README.md +++ b/operators/combination/README.md @@ -1,20 +1,23 @@ # Combination Operators -The combination operators allow the joining of information from multiple observables. -Order, time, and structure of emitted values is the primary variation among these operators. +The combination operators allow the joining of information from multiple +observables. Order, time, and structure of emitted values is the primary +variation among these operators. ## Contents -* [combineAll](combineall.md) -* [combineLatest](combinelatest.md) :star: -* [concat](concat.md) :star: -* [concatAll](concatall.md) -* [forkJoin](forkjoin.md) -* [merge](merge.md) :star: -* [mergeAll](mergeall.md) -* [pairwise](pairwise.md) -* [race](race.md) -* [startWith](startwith.md) :star: -* [withLatestFrom](withlatestfrom.md) :star: -* [zip](zip.md) -:star: - *commonly used* +- [combineLatestAll](combineall.md) +- [combineLatest](combinelatest.md) ⭐ +- [concat](concat.md) ⭐ +- [concatAll](concatall.md) +- [endWith](endwith.md) +- [forkJoin](forkjoin.md) +- [merge](merge.md) ⭐ +- [mergeAll](mergeall.md) +- [pairwise](pairwise.md) +- [race](race.md) +- [startWith](startwith.md) ⭐ +- [withLatestFrom](withlatestfrom.md) ⭐ +- [zip](zip.md) + +⭐ - _commonly used_ diff --git a/operators/combination/combineall.md b/operators/combination/combineall.md index ec9b9cc7..da9bfc51 100644 --- a/operators/combination/combineall.md +++ b/operators/combination/combineall.md @@ -1,61 +1,92 @@ -# combineAll +# combineLatestAll -#### signature: `combineAll(project: function): Observable` +## Signature -## When source observable completes use [combineLatest](combinelatest.md) with collected observables. +```typescript +combineLatestAll(): OperatorFunction, T[]> +``` + +Flattens a higher-order observable by applying [combineLatest](./combinelatest.md) when the outer observable completes. + +--- + +💡 `combineLatestAll` is best used when you're working with a **higher-order observable** (an observable that emits other observables) and need to track the most recent values from each inner observable +💡 Unlike [`mergeAll`](./mergeall) which emits values as soon as any inner observable emits, `combineLatestAll` waits for **all inner observables to emit at least once** before producing output +💡 If you need only the final values when all observables complete (not ongoing updates), consider [`forkJoin`](../combination/forkjoin) instead + +--- + +## Why use combineLatestAll? + +Think of `combineLatestAll` as organizing a panel discussion where speakers join at different times. You can't start the broadcast until everyone has joined, but after that, whenever anyone speaks again, you broadcast the most recent statement from each panelist. That's exactly how `combineLatestAll` manages your observable streams. + +This operator shines when you're dealing with **dynamic collections of observables**, or situations where you don't know upfront how many observables you'll have, but they're being emitted by a source stream. A practical example is when you [map a stream of events to interval observables](#example-1-mapping-to-inner-interval-observable) and want to monitor the latest value from each one simultaneously. It's particularly useful for scenarios like tracking multiple user sessions, monitoring real-time dashboard widgets that get added dynamically, or combining results from a variable number of API calls. + +Keep in mind that `combineLatestAll` won't produce any values until two conditions are met: the outer observable must **complete** (signaling that no more inner observables will be emitted), and every inner observable must have **emitted at least once**. This "wait for everyone" behavior can be a gotcha if one of your inner observables never emits or never completes—your stream will remain silent. -
+In essence, `combineLatestAll` transforms a stream of streams into a single stream that keeps you updated with the latest snapshot from each member of your dynamic collection. -### Examples +--- -( -[example tests](https://github.com/btroncone/learn-rxjs/blob/master/operators/specs/combination/combineall-spec.ts) -) +## Examples -##### Example 1: Mapping to inner interval observable +### Example 1: Mapping to inner interval observable -( -[StackBlitz](https://stackblitz.com/edit/typescript-fbxfyh?file=index.ts&devtoolsheight=100) -) +([StackBlitz](https://stackblitz.com/edit/typescript-bzwkrl?file=index.ts)) -```js -// RxJS v6+ -import { take, map, combineAll } from 'rxjs/operators'; +```typescript import { interval } from 'rxjs'; +import { take, map, combineLatestAll } from 'rxjs/operators'; + +// Emit every 1s, take 2 +const source$ = interval(1000).pipe(take(2)); -//emit every 1s, take 2 -const source = interval(1000).pipe(take(2)); -//map each emitted value from source to interval observable that takes 5 values -const example = source.pipe( - map(val => interval(1000).pipe(map(i => `Result (${val}): ${i}`), take(5))) +// Map each emitted value from source to interval observable that takes 5 values +const example$ = source$.pipe( + map(val => + interval(1000).pipe( + map(i => `Result (${val}): ${i}`), + take(5) + ) + ) ); + /* - 2 values from source will map to 2 (inner) interval observables that emit every 1s - combineAll uses combineLatest strategy, emitting the last value from each + 2 values from source will map to 2 (inner) interval observables that emit every 1s. + combineLatestAll uses combineLatest strategy, emitting the last value from each whenever either observable emits a value */ -const combined = example.pipe(combineAll()); -/* - output: - ["Result (0): 0", "Result (1): 0"] - ["Result (0): 1", "Result (1): 0"] - ["Result (0): 1", "Result (1): 1"] - ["Result (0): 2", "Result (1): 1"] - ["Result (0): 2", "Result (1): 2"] - ["Result (0): 3", "Result (1): 2"] - ["Result (0): 3", "Result (1): 3"] - ["Result (0): 4", "Result (1): 3"] - ["Result (0): 4", "Result (1): 4"] -*/ -const subscribe = combined.subscribe(val => console.log(val)); +example$ + .pipe(combineLatestAll()) + /* + output: + ["Result (0): 0", "Result (1): 0"] + ["Result (0): 1", "Result (1): 0"] + ["Result (0): 1", "Result (1): 1"] + ["Result (0): 2", "Result (1): 1"] + ["Result (0): 2", "Result (1): 2"] + ["Result (0): 3", "Result (1): 2"] + ["Result (0): 3", "Result (1): 3"] + ["Result (0): 4", "Result (1): 3"] + ["Result (0): 4", "Result (1): 4"] + */ + .subscribe(console.log); ``` -### Additional Resources +--- + +## Related Recipes + +- [Smart Counter](../../recipes/smartcounter.md) +- [HTTP Polling](../../recipes/http-polling.md) + +--- + +## Additional Resources -* [combineAll](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-combineAll) - :newspaper: - Official docs +- [combineLatestAll](https://rxjs.dev/api/index/function/combineLatestAll) 📰 - Official docs +- [combineLatest](./combinelatest) - Learn about the underlying combination strategy --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/combineAll.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/combineAll.ts) +📁 **Source Code**: [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/combineLatestAll.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/combineLatestAll.ts) \ No newline at end of file diff --git a/operators/combination/combinelatest.md b/operators/combination/combinelatest.md index 16f16243..14a909e7 100644 --- a/operators/combination/combinelatest.md +++ b/operators/combination/combinelatest.md @@ -2,13 +2,11 @@ #### signature: `combineLatest(observables: ...Observable, project: function): Observable` -## When any observable emits a value, emit the latest value from each. +## When any observable emits a value, emit the last emitted value from each. --- -:bulb: This operator can be used as either a static or instance method! - -:bulb: [combineAll](combineall.md) can be used to apply combineLatest to emitted +💡 [combineLatestAll](combineall.md) can be used to apply combineLatest to emitted observables when a source completes! --- @@ -16,7 +14,7 @@ observables when a source completes! ### Why use `combineLatest`? This operator is best used when you have multiple, long-lived observables that -rely on eachother for some calculation or determination. Basic examples of this +rely on each other for some calculation or determination. Basic examples of this can be seen in [example three](#example-3-combining-events-from-2-buttons), where events from multiple buttons are being combined to produce a count of each and an overall total, or a @@ -33,42 +31,35 @@ Lastly, if you are working with observables that only emit one value, or you only require the last value of each before completion, [`forkJoin`](forkjoin.md) is likely a better option. -
-### Examples -( -[example tests](https://github.com/btroncone/learn-rxjs/blob/master/operators/specs/combination/combinelatest-spec.ts) -) +### Examples ##### Example 1: Combining observables emitting at 3 intervals ( [StackBlitz](https://stackblitz.com/edit/typescript-vadvm2?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/tinumesuda/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/mygy9j86/69/) ) +) ```js // RxJS v6+ import { timer, combineLatest } from 'rxjs'; -//timerOne emits first value at 1s, then once every 4s -const timerOne = timer(1000, 4000); -//timerTwo emits first value at 2s, then once every 4s -const timerTwo = timer(2000, 4000); -//timerThree emits first value at 3s, then once every 4s -const timerThree = timer(3000, 4000); +// timerOne emits first value at 1s, then once every 4s +const timerOne$ = timer(1000, 4000); +// timerTwo emits first value at 2s, then once every 4s +const timerTwo$ = timer(2000, 4000); +// timerThree emits first value at 3s, then once every 4s +const timerThree$ = timer(3000, 4000); -//when one timer emits, emit the latest values from each timer as an array -const combined = combineLatest(timerOne, timerTwo, timerThree); - -const subscribe = combined.subscribe( +// when one timer emits, emit the latest values from each timer as an array +combineLatest(timerOne$, timerTwo$, timerThree$).subscribe( ([timerValOne, timerValTwo, timerValThree]) => { /* Example: - timerOne first tick: 'Timer One Latest: 1, Timer Two Latest:0, Timer Three Latest: 0 - timerTwo first tick: 'Timer One Latest: 1, Timer Two Latest:1, Timer Three Latest: 0 - timerThree first tick: 'Timer One Latest: 1, Timer Two Latest:1, Timer Three Latest: 1 + timerThree first tick: 'Timer One Latest: 0, Timer Two Latest: 0, Timer Three Latest: 0 + timerOne second tick: 'Timer One Latest: 1, Timer Two Latest: 0, Timer Three Latest: 0 + timerTwo second tick: 'Timer One Latest: 1, Timer Two Latest: 1, Timer Three Latest: 0 */ console.log( `Timer One Latest: ${timerValOne}, @@ -83,90 +74,91 @@ const subscribe = combined.subscribe( ( [StackBlitz](https://stackblitz.com/edit/typescript-prtbvd?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/codotapula/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/uehasmb6/) ) +) ```js // RxJS v6+ import { timer, combineLatest } from 'rxjs'; -//timerOne emits first value at 1s, then once every 4s -const timerOne = timer(1000, 4000); -//timerTwo emits first value at 2s, then once every 4s -const timerTwo = timer(2000, 4000); -//timerThree emits first value at 3s, then once every 4s -const timerThree = timer(3000, 4000); - -//combineLatest also takes an optional projection function -const combinedProject = combineLatest( - timerOne, - timerTwo, - timerThree, +const timerOne$ = timer(1000, 4000); +const timerTwo$ = timer(2000, 4000); +const timerThree$ = timer(3000, 4000); + +combineLatest( + timerOne$, + timerTwo$, + timerThree$, + // combineLatest also takes an optional projection function (one, two, three) => { return `Timer One (Proj) Latest: ${one}, Timer Two (Proj) Latest: ${two}, Timer Three (Proj) Latest: ${three}`; } -); -//log values -const subscribe = combinedProject.subscribe(latestValuesProject => - console.log(latestValuesProject) -); +).subscribe(console.log); ``` ##### Example 3: Combining events from 2 buttons ( [StackBlitz](https://stackblitz.com/edit/typescript-ihcxud?file=index.ts&devtoolsheight=50) -| [jsBin](http://jsbin.com/buridepaxi/edit?html,js,output) | -[jsFiddle](https://jsfiddle.net/btroncone/9rsf6t9v/14/) ) +) ```js // RxJS v6+ import { fromEvent, combineLatest } from 'rxjs'; import { mapTo, startWith, scan, tap, map } from 'rxjs/operators'; -// helper function to set HTML -const setHtml = id => val => (document.getElementById(id).innerHTML = val); +// elem refs +const redTotal = document.getElementById('red-total'); +const blackTotal = document.getElementById('black-total'); +const total = document.getElementById('total'); const addOneClick$ = id => fromEvent(document.getElementById(id), 'click').pipe( // map every click to 1 mapTo(1), - startWith(0), // keep a running total - scan((acc, curr) => acc + curr), - // set HTML for appropriate element - tap(setHtml(`${id}Total`)) + scan((acc, curr) => acc + curr, 0), + startWith(0) ); -const combineTotal$ = combineLatest(addOneClick$('red'), addOneClick$('black')) - .pipe(map(([val1, val2]) => val1 + val2)) - .subscribe(setHtml('total')); +combineLatest(addOneClick$('red'), addOneClick$('black')).subscribe( + ([red, black]: any) => { + redTotal.innerHTML = red; + blackTotal.innerHTML = black; + total.innerHTML = red + black; + } +); ``` ###### HTML ```html
- - + +
-
Red:
-
Black:
-
Total:
+
Red:
+
Black:
+
Total:
``` +### Related Recipes + +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Tank Battle Game](../../recipes/tank-battle-game.md) +- [Tetris Game](../../recipes/tetris-game.md) + ### Additional Resources -- [combineLatest](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-combineLatest) - :newspaper: - Official docs -- [Combining streams with combineLatest](https://egghead.io/lessons/rxjs-combining-streams-with-combinelatest?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist -- [Combination operator: combineLatest](https://egghead.io/lessons/rxjs-combination-operator-combinelatest?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [combineLatest](https://rxjs.dev/api/index/function/combineLatest) 📰 - + Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/combineLatest.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/combineLatest.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/combineLatest.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/combineLatest.ts) diff --git a/operators/combination/concat.md b/operators/combination/concat.md index 5f9961c7..e681dba7 100644 --- a/operators/combination/concat.md +++ b/operators/combination/concat.md @@ -2,131 +2,106 @@ #### signature: `concat(observables: ...*): Observable` -## Subscribe to observables in order as previous completes, emit values. +## Subscribe to observables in order as previous completes --- -:bulb: You can think of concat like a line at a ATM, the next transaction +💡 You can think of concat like a line at a ATM, the next transaction (subscription) cannot start until the previous completes! -:bulb: This operator can be used as either a static or instance method! - -:bulb: If throughput, not order, is a primary concern, try [merge](merge.md) +💡 If throughput, not order, is a primary concern, try [merge](merge.md) instead! --- -
+### Why use `concat`? +The concat operator is best used when you need to combine multiple observables, but you want their emissions to be in a specific order, one after the other. It's like putting together a puzzle where the pieces must come together sequentially to create the full picture. An example of this can be seen in a real-world scenario, like downloading and displaying several images in the correct order, where you don't want the next image to load until the current one is fully loaded. -### Examples +Keep in mind that concat will only start emitting values from the next observable once the previous one has completed. This means that if one of your observables never completes, the subsequent observables will never emit any values. This behavior can be a _gotcha_, as there will be no output and no error, but one (or more) of your inner observables might not be functioning as intended, or a subscription is not being set up correctly. -( -[example tests](https://github.com/btroncone/learn-rxjs/blob/master/operators/specs/combination/concat-spec.ts) -) +In contrast, if you need to combine observables that emit values concurrently, or you require the latest values from multiple observables whenever any of them emit a new value, [combineLatest](combinelatest.md) or [withLatestFrom](withlatestfrom.md) might be more suitable options. -##### Example 1: concat 2 basic observables -( -[StackBlitz](https://stackblitz.com/edit/typescript-ec6wed?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/gegubutele/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/rxwnr3hh/) ) -```js -// RxJS v6+ -import { concat } from 'rxjs/operators'; -import { of } from 'rxjs'; - -//emits 1,2,3 -const sourceOne = of(1, 2, 3); -//emits 4,5,6 -const sourceTwo = of(4, 5, 6); -//emit values from sourceOne, when complete, subscribe to sourceTwo -const example = sourceOne.pipe(concat(sourceTwo)); -//output: 1,2,3,4,5,6 -const subscribe = example.subscribe(val => - console.log('Example: Basic concat:', val) -); -``` +### Examples -##### Example 2: concat as static method +##### Example 1: Basic concat usage with three observables ( [StackBlitz](https://stackblitz.com/edit/typescript-ks8chl?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/xihagewune/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/5qdtvhu8/) ) +) ```js // RxJS v6+ import { of, concat } from 'rxjs'; -//emits 1,2,3 -const sourceOne = of(1, 2, 3); -//emits 4,5,6 -const sourceTwo = of(4, 5, 6); - -//used as static -const example = concat(sourceOne, sourceTwo); -//output: 1,2,3,4,5,6 -const subscribe = example.subscribe(val => console.log(val)); +concat( + of(1, 2, 3), + // subscribed after first completes + of(4, 5, 6), + // subscribed after second completes + of(7, 8, 9) +) + // log: 1, 2, 3, 4, 5, 6, 7, 8, 9 + .subscribe(console.log); ``` -##### Example 3: concat with delayed source +##### Example 2: Display message using concat with delayed observables -( -[StackBlitz](https://stackblitz.com/edit/typescript-vsphry?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/nezonosubi/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/L2s49msx/) ) +( [StackBlitz](https://stackblitz.com/edit/typescript-jtzuaa?file=index.ts) ) + +![Example 2](https://drive.google.com/uc?export=view&id=1fKsYUKXkSWEDLdii-5rmOAgqy6sUGNjl) ```js // RxJS v6+ -import { delay, concat } from 'rxjs/operators'; -import { of } from 'rxjs'; - -//emits 1,2,3 -const sourceOne = of(1, 2, 3); -//emits 4,5,6 -const sourceTwo = of(4, 5, 6); - -//delay 3 seconds then emit -const sourceThree = sourceOne.pipe(delay(3000)); -//sourceTwo waits on sourceOne to complete before subscribing -const example = sourceThree.pipe(concat(sourceTwo)); -//output: 1,2,3,4,5,6 -const subscribe = example.subscribe(val => - console.log('Example: Delayed source one:', val) -); +import { concat, empty } from 'rxjs'; +import { delay, startWith } from 'rxjs/operators'; + +// elems +const userMessage = document.getElementById('message'); +// helper +const delayedMessage = (message, delayedTime = 1000) => { + return empty().pipe(startWith(message), delay(delayedTime)); +}; + +concat( + delayedMessage('Get Ready!'), + delayedMessage(3), + delayedMessage(2), + delayedMessage(1), + delayedMessage('Go!'), + delayedMessage('', 2000) +).subscribe((message: any) => (userMessage.innerHTML = message)); ``` -##### Example 4: concat with source that does not complete +##### Example 3: (Warning!) concat with source that does not complete ( [StackBlitz](https://stackblitz.com/edit/typescript-njc2jw?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/vixajoxaze/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/4bhtb81u/) ) +) ```js // RxJS v6+ import { interval, of, concat } from 'rxjs'; -//when source never completes, the subsequent observables never runs -const source = concat(interval(1000), of('This', 'Never', 'Runs')); -//outputs: 0,1,2,3,4.... -const subscribe = source.subscribe(val => - console.log( - 'Example: Source never completes, second observable never runs:', - val - ) -); +// when source never completes, any subsequent observables never run +concat(interval(1000), of('This', 'Never', 'Runs')) + // log: 1,2,3,4..... + .subscribe(console.log); ``` +### Related Recipes + +- [Battleship Game](../../recipes/battleship-game.md) +- [Save Indicator](../../recipes/save-indicator.md) + ### Additional Resources -- [concat](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-concat) - :newspaper: - Official docs +- [concat](https://rxjs.dev/api/index/function/concat) 📰 - Official docs - [Combination operator: concat, startWith](https://egghead.io/lessons/rxjs-combination-operators-concat-startwith?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concat.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concat.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concat.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concat.ts) diff --git a/operators/combination/concatall.md b/operators/combination/concatall.md index cd33809f..a057c92d 100644 --- a/operators/combination/concatall.md +++ b/operators/combination/concatall.md @@ -6,16 +6,23 @@ --- -:warning: Be wary of +⚠ Be wary of [backpressure](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/backpressure.md) when the source emits at a faster pace than inner observables complete! -:bulb: In many cases you can use [concatMap](../transformation/concatmap.md) as -a single operator instead! +💡 In many cases you can use [concatMap](../transformation/concatmap.md) as a +single operator instead! --- -
+### Why use `concatAll`? +This operator is best used when you have multiple observables that need to be processed sequentially, ensuring that each observable completes before moving on to the next. Real-world examples of this can be seen in scenarios such as uploading multiple files to a server one-by-one, or displaying a sequence of animations in order. + +Bear in mind that `concatAll` will only start processing the next observable when the current one completes. This is an important consideration if you have observables that emit values indefinitely or take a long time to complete, as it may cause a delay in processing subsequent observables. + +Additionally, if you're working with observables that can emit values concurrently and don't need to wait for one to complete before processing another, [mergeAll](mergeall.md) might be a more suitable choice. Similarly, if you only need to combine the values of multiple observables at the point when they all complete, [forkJoin](forkjoin.md) could be a better option. + + ### Examples @@ -87,7 +94,7 @@ const subscribe = example.subscribe(val => ```js // RxJS v6+ import { take, concatAll } from 'rxjs/operators'; -import { interval, of } from 'rxjs/observable/interval'; +import { interval, of } from 'rxjs'; const obs1 = interval(1000).pipe(take(5)); const obs2 = interval(500).pipe(take(2)); @@ -114,12 +121,11 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [concatAll](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-concatAll) - :newspaper: - Official docs +- [concatAll](https://rxjs.dev/api/operators/concatAll) 📰 - Official docs - [Flatten a higher order observable with concatAll in RxJS](https://egghead.io/lessons/rxjs-flatten-a-higher-order-observable-with-concatall-in-rxjs?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concatAll.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concatAll.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concatAll.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concatAll.ts) diff --git a/operators/combination/endwith.md b/operators/combination/endwith.md new file mode 100644 index 00000000..a7262edb --- /dev/null +++ b/operators/combination/endwith.md @@ -0,0 +1,100 @@ +# endWith + +#### signature: `endWith(an: Values): Observable` + +## Emit given value(s) on completion. + +--- + +💡 If you want to start with a value instead, check out +[`startWith`](startwith.md)! + +💡 If you want to perform an action on completion, but do not want to emit a +value, check out [`finalize`](../utility/finalize.md)! + +--- + +### Why use `endWith`? +The `endWith` operator is especially handy when you want to ensure that a specific value is emitted after the source observable completes. Think of it as the closing credits of a movie, signaling that the story has reached its conclusion. Real-world examples of endWith can be found in scenarios where you want to append a specific message or status update after a series of events, such as a file download that ends with a "Download Complete" notification or a countdown timer that finishes with a "Time's Up!" alert. + +Keep in mind that endWith only emits the specified value when the source observable completes. This means that if your source observable does not complete, the value provided to `endWith` will not be emitted. To avoid surprises, make sure to check that your source observable is designed to complete at some point. + +In cases where you want to prepend a value at the beginning of an observable sequence instead of appending it at the end, consider using the [startWith](startwith.md) operator. + + + +### Examples + +##### Example 1: Basic `endWith` example + +( +[StackBlitz](https://stackblitz.com/edit/typescript-gexe9u?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { endWith } from 'rxjs/operators'; +import { of } from 'rxjs'; + +const source$ = of('Hello', 'Friend', 'Goodbye'); + +source$ + // emit on completion + .pipe(endWith('Friend')) + // 'Hello', 'Friend', 'Goodbye', 'Friend' + .subscribe(console.log(val)); +``` + +##### Example 2: endWith multiple values + +( +[StackBlitz](https://stackblitz.com/edit/typescript-dyed7x?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { endWith } from 'rxjs/operators'; +import { of } from 'rxjs'; + +const source$ = of('Hello', 'Friend'); + +source$ + // emit on completion + .pipe(endWith('Goodbye', 'Friend')) + // 'Hello', 'Friend', 'Goodbye', 'Friend' + .subscribe(console.log(val)); +``` + +##### Example 3: Comparison to [`finalize`](../utility/finalize.md) + +( +[StackBlitz](https://stackblitz.com/edit/typescript-lkk1pj?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { endWith, finalize } from 'rxjs/operators'; +import { of } from 'rxjs'; + +const source$ = of('Hello', 'Friend'); + +source$ + // emit on completion + .pipe( + endWith('Goodbye', 'Friend'), + // this function is invoked when unsubscribe methods are called + finalize(() => console.log('Finally')) + ) + // 'Hello', 'Friend', 'Goodbye', 'Friend' + .subscribe(val => console.log(val)); +// 'Finally' +``` + +### Additional Resources + +- [endWith](https://rxjs.dev/api/operators/endWith) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/endWith.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/endWith.ts) diff --git a/operators/combination/forkjoin.md b/operators/combination/forkjoin.md index 9e165a20..ddf22717 100644 --- a/operators/combination/forkjoin.md +++ b/operators/combination/forkjoin.md @@ -6,11 +6,10 @@ --- -:bulb: If you want corresponding emissions from multiple observables as they -occur, try [zip](zip.md)! +💡 If you want corresponding emissions from multiple observables as they occur, +try [zip](zip.md)! -:warning: If an inner observable does not complete `forkJoin` will never emit a -value! +⚠ If an inner observable does not complete `forkJoin` will never emit a value! --- @@ -19,7 +18,7 @@ value! This operator is best used when you have a group of observables and only care about the final emitted value of each. One common use case for this is if you wish to issue multiple requests on page load (or some other event) and only want -to take action when a response has been receieved for all. In this way it is +to take action when a response has been received for all. In this way it is similar to how you might use [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). @@ -32,24 +31,49 @@ can [catch the error on the outside](#example-3-handling-errors-on-outside). It's also worth noting that if you have an observable that emits more than one item, and you are concerned with the previous emissions `forkJoin` is not the -correct choice. In these cases you may better off with an operator like +correct choice. In these cases you may be better off with an operator like [combineLatest](combinelatest.md) or [zip](zip.md). -
+ ### Examples -##### Example 1: Observables completing after different durations +##### Example 1: Using a dictionary of sources to make AJAX request + +( +[StackBlitz](https://stackblitz.com/edit/typescript-u5pzuf?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6.5+ +import { ajax } from 'rxjs/ajax'; +import { forkJoin } from 'rxjs'; + +/* + when all observables complete, provide the last + emitted value from each as dictionary +*/ +forkJoin( + // as of RxJS 6.5+ we can use a dictionary of sources + { + google: ajax.getJSON('https://api.github.com/users/google'), + microsoft: ajax.getJSON('https://api.github.com/users/microsoft'), + users: ajax.getJSON('https://api.github.com/users') + } +) + // { google: object, microsoft: object, users: array } + .subscribe(console.log); +``` + +##### Example 2: Observables completing after different durations ( -[StackBlitz](https://stackblitz.com/edit/typescript-bqxg9x?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/remiduhimu/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/5fj77920/81/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-c3f62b?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ +import { interval, forkJoin, of } from 'rxjs'; import { delay, take } from 'rxjs/operators'; -import { forkJoin, of, interval } from 'rxjs'; const myPromise = val => new Promise(resolve => @@ -60,28 +84,35 @@ const myPromise = val => when all observables complete, give the last emitted value from each as an array */ -const example = forkJoin( +const example = forkJoin({ //emit 'Hello' immediately - of('Hello'), + sourceOne: of('Hello'), //emit 'World' after 1 second - of('World').pipe(delay(1000)), + sourceTwo: of('World').pipe(delay(1000)), //emit 0 after 1 second - interval(1000).pipe(take(1)), + sourceThree: interval(1000).pipe(take(1)), //emit 0...1 in 1 second interval - interval(1000).pipe(take(2)), + sourceFour: interval(1000).pipe(take(2)), //promise that resolves to 'Promise Resolved' after 5 seconds - myPromise('RESULT') -); -//output: ["Hello", "World", 0, 1, "Promise Resolved: RESULT"] + sourceFive: myPromise('RESULT') +}); +/* + * Output: + * { + * sourceOne: "Hello", + * sourceTwo: "World", + * sourceThree: 0, + * sourceFour: 1, + * sourceFive: "Promise Resolved: RESULT" + * } + */ const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 2: Making a variable number of requests +##### Example 3: Making a variable number of requests (uses deprecated API) ( -[StackBlitz](https://stackblitz.com/edit/typescript-3mbbjw?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/febejakapi/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/0b8Lnh7s/1/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-3mbbjw?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ @@ -109,12 +140,10 @@ const example = source.pipe(mergeMap(q => forkJoin(...q.map(myPromise)))); const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 3: Handling errors on outside +##### Example 4: Handling errors on outside ( -[StackBlitz](https://stackblitz.com/edit/typescript-xgskpm?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/gugawucixi/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/6vz7tjx2/1/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-petcwk?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ @@ -122,27 +151,26 @@ import { delay, catchError } from 'rxjs/operators'; import { forkJoin, of, throwError } from 'rxjs'; /* - when all observables complete, give the last - emitted value from each as an array + If any inner observables error, the error result + will be emitted by catchError. */ -const example = forkJoin( - //emit 'Hello' immediately - of('Hello'), - //emit 'World' after 1 second - of('World').pipe(delay(1000)), +const example = forkJoin({ + // emit 'Hello' immediately + sourceOne: of('Hello'), + // emit 'World' after 1 second + sourceTwo: of('World').pipe(delay(1000)), // throw error - _throw('This will error') -).pipe(catchError(error => of(error))); -//output: 'This will Error' + sourceThree: throwError('This will error') +}).pipe(catchError(error => of(error))); + +// output: 'This will Error' const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 4: Getting successful results when one inner observable errors +##### Example 5: Getting successful results when one inner observable errors ( -[StackBlitz](https://stackblitz.com/edit/typescript-hydgiu?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/memajepefe/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/emdu4doy/1/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-7qcyvz?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ @@ -150,24 +178,31 @@ import { delay, catchError } from 'rxjs/operators'; import { forkJoin, of, throwError } from 'rxjs'; /* - when all observables complete, give the last - emitted value from each as an array + Emit values from successfully completed + inner observables. */ -const example = forkJoin( - //emit 'Hello' immediately - of('Hello'), - //emit 'World' after 1 second - of('World').pipe(delay(1000)), +const example = forkJoin({ + // emit 'Hello' immediately + sourceOne: of('Hello'), + // emit 'World' after 1 second + sourceTwo: of('World').pipe(delay(1000)), // throw error - _throw('This will error').pipe(catchError(error => of(error))) -); -//output: ["Hello", "World", "This will error"] + sourceThree: throwError('This will error').pipe(catchError(error => of(error))) +}); + +/* + * Output: + * { + * sourceOne: "Hello", + * sourceTwo: "World", + * sourceThree: "This will error" + * } + */ const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 5: forkJoin in Angular +##### Example 6: forkJoin in Angular -( [plunker](https://plnkr.co/edit/ElTrOg8NfR3WbbAfjBXQ?p=preview) ) ```js @Injectable() @@ -201,15 +236,15 @@ export class App { ngOnInit() { // simulate 3 requests with different delays - forkJoin( - this._myService.makeRequest('Request One', 2000), - this._myService.makeRequest('Request Two', 1000), - this._myService.makeRequest('Request Three', 3000) - ) - .subscribe(([res1, res2, res3]) => { - this.propOne = res1; - this.propTwo = res2; - this.propThree = res3; + forkJoin({ + requestOne: this._myService.makeRequest('Request One', 2000), + requestTwo: this._myService.makeRequest('Request Two', 1000), + requestThree: this._myService.makeRequest('Request Three', 3000) + }) + .subscribe(({requestOne, requestTwo, requestThree}) => { + this.propOne = requestOne; + this.propTwo = requestTwo; + this.propThree = requestThree; }); } } @@ -217,10 +252,9 @@ export class App { ### Additional Resources -- [forkJoin](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-forkJoin) - :newspaper: - Official docs +- [forkJoin](https://rxjs.dev/api/index/function/forkJoin) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/observable/ForkJoinObservable.ts](https://github.com/ReactiveX/rxjs/blob/master/src/observable/ForkJoinObservable.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/forkJoin.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/forkJoin.ts) diff --git a/operators/combination/merge.md b/operators/combination/merge.md index 01343198..4834ee79 100644 --- a/operators/combination/merge.md +++ b/operators/combination/merge.md @@ -6,14 +6,23 @@ --- -:bulb: This operator can be used as either a static or instance method! +💡 This operator can be used as either a static or instance method! -:bulb: If order not throughput is a primary concern, try [concat](concat.md) +💡 If order not throughput is a primary concern, try [concat](concat.md) instead! --- -
+### Why use merge? +The merge operator is your go-to solution when you have multiple observables that produce values independently and you want to combine their output into a single stream. Think of it as a highway merger, where multiple roads join together to form a single, unified road - the traffic (data) from each road (observable) flows seamlessly together. + +A real-world example can be seen in a chat application, where you have separate observables for receiving messages from multiple users. By using `merge`, you can bring all those message streams into a single unified stream for displaying the messages in the chat window. + +Keep in mind that `merge` will emit values as soon as any of the observables emit a value. This is different from combineLatest or withLatestFrom, which wait for each observable to emit at least one value before emitting a combined value. + +Lastly, if you're dealing with observables that emit values at specific intervals and you need to combine them based on time, consider using the [zip](zip.md) operator instead. + + ### Examples @@ -73,20 +82,24 @@ const subscribe = example.subscribe(val => console.log(val)); ### Related Recipes +- [Battleship Game](../../recipes/battleship-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) - [HTTP Polling](../../recipes/http-polling.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) +- [Save Indicator](../../recipes/save-indicator.md) ### Additional Resources -- [merge](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-merge) - :newspaper: - Official docs +- [merge](https://rxjs.dev/api/index/function/merge) 📰 - Official docs - [Handling multiple streams with merge](https://egghead.io/lessons/rxjs-handling-multiple-streams-with-merge?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist - [Sharing network requests with merge](https://egghead.io/lessons/rxjs-reactive-programming-sharing-network-requests-with-rxjs-merge?course=introduction-to-reactive-programming) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz - [Combination operator: merge](https://egghead.io/lessons/rxjs-combination-operator-merge?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/merge.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/merge.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/merge.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/merge.ts) diff --git a/operators/combination/mergeall.md b/operators/combination/mergeall.md index 1e551d6a..17b8343a 100644 --- a/operators/combination/mergeall.md +++ b/operators/combination/mergeall.md @@ -6,12 +6,26 @@ --- -:bulb: In many cases you can use [mergeMap](../transformation/mergemap.md) as a +💡 In many cases you can use [mergeMap](../transformation/mergemap.md) as a single operator instead! --- -
+### Why use mergeAll? +This operator is best used when you have multiple, short-lived observables that produce values independently and you want to flatten them into a single output stream. Real-world examples of this can be seen in scenarios like processing multiple requests simultaneously, where each request is an observable and the responses need to be combined into a single stream. + +Consider a web page that displays live news updates from different sources. Each source produces updates as separate observables, and you want to merge them all together to show a single feed of news updates. In this case, mergeAll can come to the rescue. + +It's essential to note that `mergeAll` will start emitting values as soon as any of the inner observables emit a value. This is different from combineLatest, which waits for each observable to emit at least one value before producing an output. + +When deciding between mergeAll and other operators, keep in mind the following: + +- If you need to merge multiple observables that rely on each other for calculations or decisions, [`combineLatest`](combinelatest.md) may be more suitable. +- If you're working with observables that only emit one value or you only require the last value of each before completion, [`forkJoin`](forkjoin.md) is likely a better choice. +- If you need to merge observables that produce values independently and are short-lived, `mergeAll` is the operator to reach for. + + + ### Examples @@ -73,12 +87,7 @@ const source = interval(500).pipe(take(5)); */ const example = source .pipe( - map(val => - source.pipe( - delay(1000), - take(3) - ) - ), + map(val => source.pipe(delay(1000), take(3))), mergeAll(2) ) .subscribe(val => console.log(val)); @@ -89,12 +98,11 @@ const example = source ### Additional Resources -- [mergeAll](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeAll) - :newspaper: - Official docs +- [mergeAll](https://rxjs.dev/api/operators/mergeAll) 📰 - Official docs - [Flatten a higher order observable with mergeAll in RxJS](https://egghead.io/lessons/rxjs-flatten-a-higher-order-observable-with-mergeall-in-rxjs?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/mergeAll.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/mergeAll.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mergeAll.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mergeAll.ts) diff --git a/operators/combination/pairwise.md b/operators/combination/pairwise.md index 0579e66b..b926f6bb 100644 --- a/operators/combination/pairwise.md +++ b/operators/combination/pairwise.md @@ -4,16 +4,21 @@ ## Emit the previous and current values as an array. -
+### Why use pairwise? +The pairwise operator is best suited when you need to compare or perform calculations between the current and previous values emitted by an observable. Real-world examples can be seen in scenarios like tracking mouse movement, where the previous and current positions are used to determine the direction or speed of the cursor, or in financial applications, where consecutive stock price updates are compared to calculate the change or percentage change. + +Keep in mind that `pairwise` will not emit an initial value until the observable emits at least two values. This behavior can lead to confusion, as there will be no output and no error, but the observable might not be functioning as intended or is waiting for more values. + +Lastly, if you're working with observables that emit multiple values but you only want to compare the last two emitted values, consider using the [bufferCount](../transformation/buffercount.md) operator with a buffer size of 2 and a start buffer count of 1 as an alternative approach. + + ### Examples ##### Example 1: ( -[StackBlitz](https://stackblitz.com/edit/typescript-tkuydr?file=index.ts&devtoolsheight=50) -| [jsBin](http://jsbin.com/keteyahido/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/8va47bq3/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-tkuydr?file=index.ts&devtoolsheight=50)) ```js // RxJS v6+ @@ -22,19 +27,15 @@ import { interval } from 'rxjs'; //Returns: [0,1], [1,2], [2,3], [3,4], [4,5] interval(1000) - .pipe( - pairwise(), - take(5) - ) + .pipe(pairwise(), take(5)) .subscribe(console.log); ``` ### Additional Resources -- [pairwise](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-pairwise) - :newspaper: - Official docs +- [pairwise](https://rxjs.dev/api/operators/pairwise) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/pairwise.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/pairwise.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/pairwise.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/pairwise.ts) diff --git a/operators/combination/race.md b/operators/combination/race.md index 9cd3ebd3..6cfef349 100644 --- a/operators/combination/race.md +++ b/operators/combination/race.md @@ -4,7 +4,16 @@ ## The observable to emit first is used. -
+### Why use race? +The `race` operator is the go-to choice when you want to work with multiple observables that compete against each other, and you're only interested in the first one to emit a value. It's like a competitive race, where the first runner to cross the finish line claims the victory, and the others don't matter anymore (if you're not first you're last?). + +A relatable example of using `race` can be observed in an image loading scenario. Imagine you have two sources to load an image from, and you want to display the image as soon as possible. You can use the race operator to subscribe to both sources, and once the first source successfully loads the image, it will emit the value, and the subscription to the other source will be automatically unsubscribed. + +It's crucial to remember that `race` only pays attention to the first emitted value from the competing observables. Once an observable wins the race, the other observables are disregarded, and their potential future emissions will have no impact on the output. + +If your use case involves working with multiple observables that should all emit values and complete, or you need to process the emitted values in a specific order, consider using operators like [combineLatest](combinelatest.md) or [forkJoin](forkjoin.md) instead. + + ### Examples @@ -62,10 +71,10 @@ race(first, second, third).subscribe(val => console.log(val)); ### Additional Resources -- [race](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-race) - :newspaper: - Official docs +- [race](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-race) 📰 - Official docs + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/race.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/race.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/race.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/race.ts) diff --git a/operators/combination/startwith.md b/operators/combination/startwith.md index 303619ad..3235db51 100644 --- a/operators/combination/startwith.md +++ b/operators/combination/startwith.md @@ -6,13 +6,20 @@ --- -:bulb: A +💡 A [BehaviorSubject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md) can also start with an initial value! --- -
+### Why use startWith? +The `startWith` operator is a great tool when you need to provide an initial value to an observable sequence, ensuring that the consumer always receives a value upon subscription. It's a handy way to set a default state or value for your observables, making it easier for subscribers to handle the data and minimizing the chances of encountering unexpected scenarios. + +A real-world example can be seen in a search functionality, where the search results should display a list of popular items as a default state before the user starts typing their query. By using startWith, you can seamlessly provide this default data to your subscribers. + +Keep in mind that startWith emits the initial value immediately upon subscription. This behavior is helpful when you want to make sure your subscribers receive a value right away, even before the source observable starts emitting values. + + ### Examples @@ -91,20 +98,28 @@ const subscribe = example.subscribe(val => console.log(val)); ### Related Recipes +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) - [Smart Counter](../../recipes/smartcounter.md) +- [Space Invaders Game](../../recipes/space-invaders-game.md) +- [Stop Watch](../../recipes/stop-watch.md) +- [Tank Battle Game](../../recipes/tank-battle-game.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) ### Additional Resources -- [startWith](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-startWith) - :newspaper: - Official docs +- [startWith](https://rxjs.dev/api/operators/startWith) 📰 - Official docs - [Displaying initial data with startWith](https://egghead.io/lessons/rxjs-displaying-initial-data-with-startwith?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist - [Clear data while loading with startWith](https://egghead.io/lessons/rxjs-reactive-programming-clear-data-while-loading-with-rxjs-startwith?course=introduction-to-reactive-programming) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz - [Combination operator: concat, startWith](https://egghead.io/lessons/rxjs-combination-operators-concat-startwith?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/startWith.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/startWith.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/startWith.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/startWith.ts) diff --git a/operators/combination/withlatestfrom.md b/operators/combination/withlatestfrom.md index 1bbd2263..9d8bd4dc 100644 --- a/operators/combination/withlatestfrom.md +++ b/operators/combination/withlatestfrom.md @@ -6,12 +6,21 @@ --- -:bulb: If you want the last emission any time a variable number of observables +💡 If you want the last emission any time a variable number of observables emits, try [combinelatest](combinelatest.md)! --- -
+### Why use withLatestFrom? +The `withLatestFrom` operator is your best friend when you have one main observable whose emissions depend on the latest values from one or more other observables. Think of it as a one-way data flow, where the primary observable takes the lead and other observables chime in with their most recent values. + +A classic example to remember `withLatestFrom` is a chat application that needs to send a message with a user's current location. The message sending event (main observable) combines with the latest location data (another observable) to form the final message object. + +Keep in mind that `withLatestFrom` only emits a value when the main observable emits, and after each additional observable has emitted at least once. This can catch you off guard, as you might not see any output or errors while one of the observables isn't behaving as expected, or a subscription is delayed. + +If you need to combine values from multiple observables that emit more than once and are interdependent, consider using [`combineLatest`](combinelatest.md) instead. And for scenarios where observables emit only once or you just need their last values, [`forkJoin`](forkjoin.md) might be a more suitable choice. + + ### Examples @@ -83,15 +92,17 @@ const subscribe = example.subscribe(val => console.log(val)); - [Progress Bar](../../recipes/progressbar.md) - [Game Loop](../../recipes/gameloop.md) +- [Space Invaders Game](/recipes/space-invaders-game.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) ### Additional Resources -- [withLatestFrom](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-withLatestFrom) - :newspaper: - Official docs +- [withLatestFrom](https://rxjs.dev/api/operators/withLatestFrom) 📰 - Official + docs - [Combination operator: withLatestFrom](https://egghead.io/lessons/rxjs-combination-operator-withlatestfrom?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/withLatestFrom.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/withLatestFrom.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/withLatestFrom.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/withLatestFrom.ts) diff --git a/operators/combination/zip.md b/operators/combination/zip.md index eda2bc4a..54ff4636 100644 --- a/operators/combination/zip.md +++ b/operators/combination/zip.md @@ -6,13 +6,20 @@ --- -:bulb: Combined with [interval](../creation/interval) or +💡 Combined with [interval](../creation/interval.md) or [timer](../creation/timer.md), zip can be used to time output from another source! --- -
+### Why use zip? +This operator is ideal when you want to combine values from multiple observables in a pairwise fashion, like zipping together the teeth of a zipper. Imagine having two observables, where one emits values like "hot", "warm", "cold", and the other emits values like "coffee", "tea", "lemonade". Using the zip operator, you'd pair them together as "hot coffee", "warm tea", and "cold lemonade". + +An everyday example is when you want to match information from two sources, like pairing names with their corresponding scores in a game. Picture an observable emitting player names and another emitting their scores. With zip, you can easily create pairs of `[player, score]`, ensuring each player is associated with the correct score. + +Be mindful that zip will only emit a value when all input observables have emitted a corresponding value. This means if one observable has emitted more values than another, the unmatched values will be held back until the other observable emits its next value. In some cases, this could lead to unpaired values, making it important to ensure your observables are synchronized. + + ### Examples @@ -63,14 +70,57 @@ const example = zip(source, source.pipe(take(2))); const subscribe = example.subscribe(val => console.log(val)); ``` +##### Example 3: get X/Y coordinates of drag start/finish (mouse down/up) + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-zip-mousedownup-coordinates?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent, zip } from 'rxjs'; +import { map } from 'rxjs/operators'; + +const documentEvent = eventName => + fromEvent(document, eventName).pipe( + map((e: MouseEvent) => ({ x: e.clientX, y: e.clientY })) + ); + +zip(documentEvent('mousedown'), documentEvent('mouseup')).subscribe(e => + console.log(JSON.stringify(e)) +); +``` + +##### Example 4: mouse click duration + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-zip-mouseclickduration?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent, zip } from 'rxjs'; +import { map } from 'rxjs/operators'; + +const eventTime = eventName => + fromEvent(document, eventName).pipe(map(() => new Date())); + +const mouseClickDuration = zip( + eventTime('mousedown'), + eventTime('mouseup') +).pipe(map(([start, end]) => Math.abs(start.getTime() - end.getTime()))); + +mouseClickDuration.subscribe(console.log); +``` + ### Additional Resources - [zip](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-zip) - :newspaper: - Official docs + 📰 - Official docs - [Combination operator: zip](https://egghead.io/lessons/rxjs-combination-operator-zip?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/zip.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/zip.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/zip.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/zip.ts) diff --git a/operators/complete.md b/operators/complete.md index f94ebb38..f5000950 100644 --- a/operators/complete.md +++ b/operators/complete.md @@ -1,95 +1,112 @@ -# RxJS 5 Operators By Example +# RxJS Operators By Example -A complete list of RxJS 5 operators with clear explanations, relevant resources, +A complete list of RxJS operators with clear explanations, relevant resources, and executable examples. _[Prefer a split by operator type?](README.md)_ ### Contents (In Alphabetical Order) +- [ajax](creation/ajax.md) ⭐ - [audit](filtering/audit.md) - [auditTime](filtering/audittime.md) - [buffer](transformation/buffer.md) - [bufferCount](transformation/buffercount.md) -- [bufferTime](transformation/buffertime.md) :star: +- [bufferTime](transformation/buffertime.md) ⭐ - [bufferToggle](transformation/buffertoggle.md) - [bufferWhen](transformation/bufferwhen.md) -- [catch / catchError](error_handling/catch.md) :star: -- [combineAll](combination/combineall.md) -- [combineLatest](combination/combinelatest.md) :star: -- [concat](combination/concat.md) :star: +- [catch / catchError](error_handling/catch.md) ⭐ +- [combineLatestAll](combination/combineall.md) +- [combineLatest](combination/combinelatest.md) ⭐ +- [concat](combination/concat.md) ⭐ - [concatAll](combination/concatall.md) -- [concatMap](transformation/concatmap.md) :star: +- [concatMap](transformation/concatmap.md) ⭐ - [concatMapTo](transformation/concatmapto.md) - [create](creation/create.md) - [debounce](filtering/debounce.md) -- [debounceTime](filtering/debouncetime.md) :star: +- [debounceTime](filtering/debouncetime.md) ⭐ - [defaultIfEmpty](conditional/defaultifempty.md) +- [defer](creation/defer.md) - [delay](utility/delay.md) - [delayWhen](utility/delaywhen.md) -- [distinctUntilChanged](filtering/distinctuntilchanged.md) :star: -- [do / tap](utility/do.md) :star: +- [distinct](filtering/distinct.md) +- [distinctUntilChanged](filtering/distinctuntilchanged.md) ⭐ +- [distinctUntilKeyChanged](filtering/distinctuntilkeychanged.md) +- [endWith](combination/endwith.md) +- [tap / do](utility/do.md) ⭐ - [empty](creation/empty.md) - [every](conditional/every.md) - [exhaustMap](transformation/exhaustmap.md) - [expand](transformation/expand.md) -- [filter](filtering/filter.md) :star: +- [filter](filtering/filter.md) ⭐ - [finalize / finally](utility/finalize.md) +- [find](filtering/find.md) - [first](filtering/first.md) - [forkJoin](combination/forkjoin.md) -- [from](creation/from.md) :star: +- [from](creation/from.md) ⭐ - [fromEvent](creation/fromevent.md) +- [generate](creation/generate.md) - [groupBy](transformation/groupby.md) +- [iif](conditional/iif.md) - [ignoreElements](filtering/ignoreelements.md) - [interval](creation/interval.md) - [last](filtering/last.md) - [let](utility/let.md) -- [map](transformation/map.md) :star: +- [map](transformation/map.md) ⭐ - [mapTo](transformation/mapto.md) -- [merge](combination/merge.md) :star: +- [merge](combination/merge.md) ⭐ - [mergeAll](combination/mergeall.md) -- [mergeMap / flatMap](transformation/mergemap.md) :star: +- [mergeMap / flatMap](transformation/mergemap.md) ⭐ +- [mergeScan](transformation/mergescan.md) - [multicast](multicasting/multicast.md) -- [of](creation/of.md) :star: +- [of](creation/of.md) ⭐ - [partition](transformation/partition.md) - [pluck](transformation/pluck.md) - [publish](multicasting/publish.md) - [race](combination/race.md) - [range](creation/range.md) +- [repeat](utility/repeat.md) +- [repeatWhen](utility/repeatwhen.md) - [retry](error_handling/retry.md) - [retryWhen](error_handling/retrywhen.md) - [sample](filtering/sample.md) -- [scan](transformation/scan.md) :star: -- [share](multicasting/share.md) :star: -- [shareReplay](multicasting/sharereplay.md) :star: +- [scan](transformation/scan.md) ⭐ +- [sequenceequal](conditional/sequenceequal.md) +- [share](multicasting/share.md) ⭐ +- [shareReplay](multicasting/sharereplay.md) ⭐ - [single](filtering/single.md) - [skip](filtering/skip.md) - [skipUntil](filtering/skipuntil.md) - [skipWhile](filtering/skipwhile.md) -- [startWith](combination/startwith.md) :star: -- [switchMap](transformation/switchmap.md) :star: -- [take](filtering/take.md) :star: -- [takeUntil](filtering/takeuntil.md) :star: +- [startWith](combination/startwith.md) ⭐ +- [switchMap](transformation/switchmap.md) ⭐ +- [switchMapTo](transformation/switchmapto.md) +- [take](filtering/take.md) ⭐ +- [takeLast](filtering/takelast.md) +- [takeUntil](filtering/takeuntil.md) ⭐ - [takeWhile](filtering/takewhile.md) - [throttle](filtering/throttle.md) - [throttleTime](filtering/throttletime.md) -- [throw](creation/throw.md) -- [timeout](utilit/timeout.md) +- [throwError](creation/throw.md) +- [timeInterval](utility/timeinterval.md) +- [timeout](utility/timeout.md) +- [timeoutWith](utility/timeoutwith.md) - [timer](creation/timer.md) +- [toArray](transformation/toarray.md) - [toPromise](utility/topromise.md) - [window](transformation/window.md) - [windowCount](transformation/windowcount.md) - [windowTime](transformation/windowtime.md) - [windowToggle](transformation/windowtoggle.md) - [windowWhen](transformation/windowwhen.md) -- [withLatestFrom](combination/withlatestfrom.md) :star: +- [withLatestFrom](combination/withlatestfrom.md) ⭐ - [zip](combination/zip.md) -:star: - _commonly used_ +⭐ - _commonly used_ ### Additional Resources - [What Are Operators?](http://reactivex.io/rxjs/manual/overview.html#operators) - :newspaper: - Official Docs + 📰 - Official Docs - [What Operators Are](https://egghead.io/lessons/rxjs-what-rxjs-operators-are) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz diff --git a/operators/conditional/README.md b/operators/conditional/README.md index 7abcaecd..75c1c6ff 100644 --- a/operators/conditional/README.md +++ b/operators/conditional/README.md @@ -4,4 +4,6 @@ For use-cases that depend on a specific condition to be met, these operators do ## Contents * [defaultIfEmpty](defaultifempty.md) -* [every](every.md) \ No newline at end of file +* [every](every.md) +* [iif](iif.md) +* [sequenceequal](sequenceequal.md) diff --git a/operators/conditional/defaultifempty.md b/operators/conditional/defaultifempty.md index 7b63189b..c40308e3 100644 --- a/operators/conditional/defaultifempty.md +++ b/operators/conditional/defaultifempty.md @@ -4,7 +4,7 @@ ## Emit given value if nothing is emitted before completion. -
+ ### Examples @@ -46,10 +46,10 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [defaultIfEmpty](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-defaultIfEmpty) - :newspaper: - Official docs +- [defaultIfEmpty](https://rxjs.dev/api/operators/defaultIfEmpty) 📰 - Official + docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/defaultIfEmpty.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/defaultIfEmpty.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/defaultIfEmpty.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/defaultIfEmpty.ts) diff --git a/operators/conditional/every.md b/operators/conditional/every.md index 230c7e9f..77767fa4 100644 --- a/operators/conditional/every.md +++ b/operators/conditional/every.md @@ -4,7 +4,7 @@ ## If all values pass predicate before completion emit true, else false. -
+ ### Examples @@ -52,12 +52,43 @@ const example = allEvens.pipe( const subscribe = example.subscribe(val => console.log(val)); ``` +##### Example 3: Values arriving over time and completing stream prematurely due to every returning false + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-every-example?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +console.clear(); +import { concat, of } from 'rxjs'; +import { every, delay, tap } from 'rxjs/operators'; + +const log = console.log; +const returnCode = request => (Number.isInteger(request) ? 200 : 400); +const fakeRequest = request => + of({ code: returnCode(request) }).pipe( + tap(_ => log(request)), + delay(1000) + ); + +const apiCalls$ = concat( + fakeRequest(1), + fakeRequest('invalid payload'), + fakeRequest(2) //this won't execute as every will return false for previous line +).pipe( + every(e => e.code === 200), + tap(e => log(`all request successful: ${e}`)) +); + +apiCalls$.subscribe(); +``` + ### Additional Resources -- [every](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-every) - :newspaper: - Official docs +- [every](https://rxjs.dev/api/operators/every) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/every.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/every.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/every.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/every.ts) diff --git a/operators/conditional/iif.md b/operators/conditional/iif.md new file mode 100644 index 00000000..7e19384a --- /dev/null +++ b/operators/conditional/iif.md @@ -0,0 +1,92 @@ +# iif + +#### signature: `iif(condition: () => boolean, trueResult: SubscribableOrPromise = EMPTY, falseResult: SubscribableOrPromise = EMPTY): Observable` + +## Subscribe to first or second observable based on a condition + + + +### Examples + +##### Example 1: simple iif + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-iif?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { iif, of, interval } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +const r$ = of('R'); +const x$ = of('X'); + +interval(1000) + .pipe(mergeMap(v => iif(() => v % 4 === 0, r$, x$))) + .subscribe(console.log); + +// output: R, X, X, X, R, X, X, X, etc... +``` + +##### Example 2: iif with mouse moves + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-iif-mousemoves?file=index.ts?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent, iif, of } from 'rxjs'; +import { mergeMap, map, throttleTime, filter } from 'rxjs/operators'; + +const r$ = of(`I'm saying R!!`); +const x$ = of(`X's always win!!`); + +fromEvent(document, 'mousemove') + .pipe( + throttleTime(50), + filter((move: MouseEvent) => move.clientY < 210), + map((move: MouseEvent) => move.clientY), + mergeMap(yCoord => iif(() => yCoord < 110, r$, x$)) + ) + .subscribe(console.log); +``` + +##### Example 3: iif with default + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-iif-pqmw2f?file=index.ts?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { fromEvent, iif, of, interval, pipe } from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +interval(1000) + .pipe( + mergeMap(v => + iif( + () => !!(v % 2), + of(v) + // if not supplied defaults to EMPTY + ) + ) + // output: 1,3,5... + ) + .subscribe(console.log); +``` + +### Related Recipes + +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) + +### Additional Resources + +- [iif](https://rxjs.dev/api/index/function/iif) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/iif.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/iif.ts) diff --git a/operators/conditional/sequenceequal.md b/operators/conditional/sequenceequal.md new file mode 100644 index 00000000..17086456 --- /dev/null +++ b/operators/conditional/sequenceequal.md @@ -0,0 +1,73 @@ +# sequenceEqual + +#### signature: `sequenceEqual(compareTo: Observable, comparor?: (a, b) => boolean): Observable` + +## Compares emitted sequence to expected sequence for match + + + +### Examples + +##### Example 1: simple sequenceEqual + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-sequenceequal?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { of, from } from 'rxjs'; +import { sequenceEqual, switchMap } from 'rxjs/operators'; + +const expectedSequence = from([4, 5, 6]); + +of([1, 2, 3], [4, 5, 6], [7, 8, 9]) + .pipe(switchMap(arr => from(arr).pipe(sequenceEqual(expectedSequence)))) + .subscribe(console.log); + +//output: false, true, false +``` + +##### Example 2: sequenceEqual with keyboard events + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-sequenceequal-buffercount?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { from, fromEvent } from 'rxjs'; +import { sequenceEqual, map, bufferCount, mergeMap, tap } from 'rxjs/operators'; + +const expectedSequence = from(['q', 'w', 'e', 'r', 't', 'y']); +const setResult = text => (document.getElementById('result').innerText = text); + +fromEvent(document, 'keydown') + .pipe( + map((e: KeyboardEvent) => e.key), + tap(v => setResult(v)), + bufferCount(6), + mergeMap(keyDowns => + from(keyDowns).pipe( + sequenceEqual(expectedSequence), + tap(isItQwerty => setResult(isItQwerty ? 'WELL DONE!' : 'TYPE AGAIN!')) + ) + ) + ) + .subscribe(e => console.log(`did you say qwerty? ${e}`)); +``` + +### Related Recipes + +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) + +### Additional Resources + +- [sequenceEqual](https://rxjs.dev/api/operators/sequenceEqual) 📰 - Official + docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/sequenceEqual.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/sequenceEqual.ts) diff --git a/operators/creation/README.md b/operators/creation/README.md index 0ddafabd..9ca3b6a2 100644 --- a/operators/creation/README.md +++ b/operators/creation/README.md @@ -6,19 +6,22 @@ generic to specific use-cases you are free, and encouraged, to turn ## Contents +- [ajax](ajax.md) ⭐ - [create](create.md) +- [defer](defer.md) - [empty](empty.md) -- [from](from.md) :star: +- [from](from.md) ⭐ - [fromEvent](fromevent.md) +- [generate](generate.md) - [interval](interval.md) -- [of](of.md) :star: +- [of](of.md) ⭐ - [range](range.md) -- [throw](throw.md) +- [throwError](throw.md) - [timer](timer.md) -:star: - _commonly used_ +⭐ - _commonly used_ ### Additional Resources - [Creating Observables From Scratch](https://egghead.io/courses/rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz diff --git a/operators/creation/ajax.md b/operators/creation/ajax.md new file mode 100644 index 00000000..107feec9 --- /dev/null +++ b/operators/creation/ajax.md @@ -0,0 +1,167 @@ +# ajax + +#### signature: `ajax(urlOrRequest: string | AjaxRequest)` + +## Create an observable for an Ajax request with either a request object with url, headers, etc or a string for a URL. + + + +### Examples + +##### Example 1: Observable that emits the response object that is being returned from the request. + +( [StackBlitz](https://stackblitz.com/edit/rxjs-raqi89) ) + +```js +// RxJS v6+ +import { ajax } from 'rxjs/ajax'; + +const githubUsers = `https://api.github.com/users?per_page=2`; + +const users = ajax(githubUsers); + +const subscribe = users.subscribe( + res => console.log(res), + err => console.error(err) +); + +/* output +{ + "originalEvent":{ + "isTrusted":true + }, + "xhr":{ + + }, + "request":{ + "async":true, + "crossDomain":true, + "withCredentials":false, + "headers":{ + + }, + "method":"GET", + "responseType":"json", + "timeout":0, + "url":"https://api.github.com/users?per_page=2" + }, + "status":200, + "responseType":"json", + "response":[ + { + "login":"mojombo", + "id":1, + "node_id":"MDQ6VXNlcjE=", + "avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4", + "gravatar_id":"", + ... + }, + { + "login":"defunkt", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://avatars0.githubusercontent.com/u/2?v=4", + "gravatar_id":"", + "... + } + ] +} +*/ +``` + +##### Example 2: Observable that emits only the json key of the response object that is being returned from the request. + +( [StackBlitz](https://stackblitz.com/edit/rxjs-8jkrhl) ) + +```js +// RxJS v6+ +import { ajax } from 'rxjs/ajax'; + +const githubUsers = `https://api.github.com/users?per_page=2`; + +const users = ajax.getJSON(githubUsers); + +const subscribe = users.subscribe( + res => console.log(res), + err => console.error(err) +); + +/* output +[ + { + "login":"mojombo", + "id":1, + "node_id":"MDQ6VXNlcjE=", + "avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4", + "gravatar_id":"", + "... + }, + { + "login":"defunkt", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://avatars0.githubusercontent.com/u/2?v=4", + "gravatar_id":"", + ... + } +] +*/ +``` + +##### Example 3: Observable that emits the error object that is being returned from the request. + +( [StackBlitz](https://stackblitz.com/edit/rxjs-vnxkth) ) + +```js +// RxJS v6+ +import { ajax } from 'rxjs/ajax'; + +const githubUsers = `https://api.github.com/error`; + +const users = ajax.getJSON(githubUsers); + +const subscribe = users.subscribe( + res => console.log(res), + err => console.error(err) +); + +/* output +Error: ajax error 404 +*/ +``` + +##### Example 4: Ajax operator with object as input. + +( [StackBlitz](https://stackblitz.com/edit/rxjs-vqnnot) ) + +```js +// RxJS v6+ +import { ajax } from 'rxjs/ajax'; + +const githubUsers = `https://api.github.com/error`; + +const users = ajax({ + url: githubUsers, + method: 'GET', + headers: { + /*some headers*/ + }, + body: { + /*in case you need a body*/ + } +}); + +const subscribe = users.subscribe( + res => console.log(res), + err => console.error(err) +); +``` + +### Additional Resources + +- [ajax](https://rxjs.dev/api/ajax/ajax) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/6.4.0/src/internal/observable/dom/ajax.ts#L20-L19](https://github.com/ReactiveX/rxjs/blob/6.4.0/src/internal/observable/dom/ajax.ts#L20-L19) diff --git a/operators/creation/create.md b/operators/creation/create.md index 2f56e31e..74158151 100644 --- a/operators/creation/create.md +++ b/operators/creation/create.md @@ -1,79 +1,214 @@ # create -#### signature: `create(subscribe: function)` +## Deprecated: Use `new Observable()` instead -## Create an observable with given subscription function. +> ⚠️ **Important**: `Observable.create()` was deprecated in RxJS v6.4.0. Use the `new Observable()` constructor instead. -
+**signature**: `new Observable(subscribe: (observer: Observer) => TeardownLogic)` -### Examples +```typescript +new Observable(subscribe: (observer: Observer) => TeardownLogic) +``` + +Create a custom observable by defining subscription behavior. + +--- -##### Example 1: Observable that emits multiple values +## 💡 Tips + +- **Consider creation operators first**: Before creating a custom observable, check if operators like [defer](./defer.md), [from](./from.md), [fromEvent](./fromevent.md), or [interval](./interval.md) already solve your use case +- **Use for bridging non-reactive APIs**: Custom observables shine when wrapping callbacks, event emitters, WebSockets, or other non-observable data sources +- **Always return teardown logic**: Return a cleanup function to prevent memory leaks when subscriptions end + +--- -( -[StackBlitz](https://stackblitz.com/edit/typescript-baxh98?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/qorugiwaba/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/td5107he/) ) +## Why use a custom observable? + +Custom observables are your bridge between the non-reactive world and RxJS. Think of them as adapters—when you have a data source that doesn't speak "Observable" (like a WebSocket connection, a third-party library with callbacks, or a browser API like Geolocation), creating a custom observable lets you wrap it up and make it play nicely with the rest of your reactive code. + +Here's the thing: **you'll rarely need to create custom observables**. RxJS already provides creation operators for most common scenarios. Timers, events, promises, arrays, and more. But when you encounter an API that doesn't fit any existing operator, custom observables give you fine-grained control. You decide exactly when to emit values (observer.next()), how to handle errors (observer.error()), and what cleanup should happen when someone unsubscribes. + +One way to think about this is like writing a translator. The non-reactive API speaks one language, your Observable streams speak another, and your custom observable sits in the middle making sure they understand each other. When implementing [the cleanup function](#example-2-observable-with-proper-cleanup), remember that this is your chance to be a good citizen. Close connections, cancel timers, remove listeners. It's like turning off the lights when you leave a room. + +In essence, custom observables are powerful but should be used sparingly. If an existing creation operator can do the job, use that. But when you need that extra control—when you're integrating with legacy code, third-party libraries, or unusual data sources, this is your tool. + +--- -```js +## Examples + +### Example 1: Simple custom observable that emits multiple values + +([StackBlitz](https://stackblitz.com/edit/typescript-meebpsdr?file=index.ts)) + +```typescript // RxJS v6+ import { Observable } from 'rxjs'; + /* - Create an observable that emits 'Hello' and 'World' on - subscription. + Create an observable that emits 'Hello' and 'World' on subscription. + Note: Using the modern constructor syntax, not the deprecated Observable.create() */ -const hello = Observable.create(function(observer) { +const hello$ = new Observable(observer => { observer.next('Hello'); observer.next('World'); + observer.complete(); }); -//output: 'Hello'...'World' -const subscribe = hello.subscribe(val => console.log(val)); +// Output: 'Hello'...'World' +const subscription = hello$.subscribe(val => console.log(val)); ``` -##### Example 2: Observable that emits even numbers on timer +### Example 2: Observable with proper cleanup -( -[StackBlitz](https://stackblitz.com/edit/typescript-xvezxn?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/lodilohate/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/vtozg6uf/) ) +([StackBlitz](https://stackblitz.com/edit/typescript-wbwrxn67?file=index.ts)) -```js +```typescript // RxJS v6+ import { Observable } from 'rxjs'; /* - Increment value every 1s, emit even numbers. + Create an observable that emits even numbers every second. + Demonstrates the importance of returning a teardown function. */ -const evenNumbers = Observable.create(function(observer) { +const evenNumbers$ = new Observable(observer => { let value = 0; - const interval = setInterval(() => { + + const intervalId = setInterval(() => { if (value % 2 === 0) { observer.next(value); } value++; }, 1000); - - return () => clearInterval(interval); + + // Return cleanup function - called when unsubscribe() is invoked + return () => { + console.log('Cleaning up interval'); + clearInterval(intervalId); + }; }); -//output: 0...2...4...6...8 -const subscribe = evenNumbers.subscribe(val => console.log(val)); -//unsubscribe after 10 seconds + +// Output: 0...2...4...6...8 +const subscription = evenNumbers$.subscribe(val => console.log(val)); + +// Unsubscribe after 10 seconds - triggers cleanup setTimeout(() => { - subscribe.unsubscribe(); + subscription.unsubscribe(); }, 10000); ``` -### Additional Resources +### Example 3: Wrapping a callback-based API + +([StackBlitz](https://stackblitz.com/edit/typescript-ury4kzws?file=index.ts)) + +```typescript +// RxJS v6+ +import { Observable } from 'rxjs'; + +/* + Wrap the browser's Geolocation API into an observable. + This demonstrates how custom observables bridge non-reactive APIs. +*/ +function getCurrentPosition(): Observable { + return new Observable(observer => { + // Check if geolocation is available + if (!navigator.geolocation) { + observer.error(new Error('Geolocation not supported')); + return; + } + + // Get position and emit it + const watchId = navigator.geolocation.watchPosition( + position => observer.next(position), + error => observer.error(error), + { enableHighAccuracy: true } + ); + + // Return cleanup to stop watching position when unsubscribed + return () => { + navigator.geolocation.clearWatch(watchId); + }; + }); +} + +// Usage +const position$ = getCurrentPosition(); +const subscription = position$.subscribe({ + next: position => { + console.log('Latitude:', position.coords.latitude); + console.log('Longitude:', position.coords.longitude); + }, + error: err => console.error('Error getting position:', err) +}); + +// Stop watching after 30 seconds +setTimeout(() => subscription.unsubscribe(), 30000); +``` + +### Example 4: Creating an observable from a WebSocket + + +```typescript +// RxJS v6+ +import { Observable } from 'rxjs'; + +/* + Wrap a WebSocket connection in an observable. + Demonstrates managing complex async resources with proper cleanup. +*/ +function createWebSocketObservable(url: string): Observable { + return new Observable(observer => { + const socket = new WebSocket(url); + + socket.onopen = () => { + console.log('WebSocket connected'); + }; + + socket.onmessage = (event) => { + observer.next(event); + }; + + socket.onerror = (error) => { + observer.error(error); + }; + + socket.onclose = () => { + observer.complete(); + }; + + // Cleanup: close the socket when unsubscribed + return () => { + if (socket.readyState === WebSocket.OPEN) { + socket.close(); + } + }; + }); +} + +// Usage +const messages$ = createWebSocketObservable('wss://example.com/socket'); +const subscription = messages$.subscribe({ + next: event => console.log('Message received:', event.data), + error: err => console.error('WebSocket error:', err), + complete: () => console.log('WebSocket closed') +}); + +// Close connection after 60 seconds +setTimeout(() => subscription.unsubscribe(), 60000); +``` + +--- + +## Related Recipes + +- [Smart Counter](../../recipes/smartcounter.md) + +--- + +## Additional Resources -- [create](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-create) - :newspaper: - Official docs -- [Creation operators: Create()](https://egghead.io/lessons/rxjs-creation-operator-create?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz -- [Using Observable.create for fine-grained control](https://egghead.io/lessons/rxjs-using-observable-create-for-fine-grained-control) - :video_camera: :dollar: - Shane Osbourne +- [Observable](https://rxjs.dev/api/index/class/Observable) 📰 - Official docs +- [Observable Constructor](https://rxjs.dev/api/index/class/Observable#constructor) 📰 - Official constructor docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/GenerateObservable.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/GenerateObservable.ts) +**📁 Source Code:** [https://github.com/ReactiveX/rxjs/blob/master/packages/observable/src/observable.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/observable/src/observable.ts) \ No newline at end of file diff --git a/operators/creation/defer.md b/operators/creation/defer.md new file mode 100644 index 00000000..23927b6a --- /dev/null +++ b/operators/creation/defer.md @@ -0,0 +1,65 @@ +# defer + +#### signature: `defer(observableFactory: function(): SubscribableOrPromise): Observable` + +## Create an observable with given subscription function. + +--- + +💡 +[`defer`](https://github.com/ReactiveX/rxjs/blob/ecc73d2a1564d0d3edffba90eec76510e509236c/src/internal/observable/iif.ts#L94-L100) +is used as part of the [`iif`](../conditional/iif.md) operator! + +--- + + + +### Examples + +##### Example 1: Defer to get current date/time at the time of subscription + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-defer-example?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { defer, of, timer, merge } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +const s1 = of(new Date()); //will capture current date time +const s2 = defer(() => of(new Date())); //will capture date time at the moment of subscription + +console.log(new Date()); + +timer(2000) + .pipe(switchMap(_ => merge(s1, s2))) + .subscribe(console.log); + +/* +OUTPUT => +2019-02-10T12:38:30.000Z (currrent date/time from first console log) +2019-02-10T12:38:30.000Z (date/time in s1 console log, captured date/time at the moment of observable creation) +2019-02-10T12:38:32.000Z (date/time in s2 console log, captured date/time at the moment of subscription) +*/ + +/*//NOTE: 'traditional' js equivalent of timer code above is: +setTimeout(() => { + s1.subscribe(console.log); + s2.subscribe(console.log); +}, 2000); +*/ +``` + +### Related Recipes + +- [Save Indicator](../../recipes/save-indicator.md) + +### Additional Resources + +- [defer](https://rxjs.dev/api/index/function/defer) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/defer.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/defer.ts) diff --git a/operators/creation/empty.md b/operators/creation/empty.md index c47e4ab1..3ae977fc 100644 --- a/operators/creation/empty.md +++ b/operators/creation/empty.md @@ -4,7 +4,7 @@ ## Observable that immediately completes. -
+ ### Examples @@ -57,14 +57,18 @@ const timer$ = merge(pause$, resume$) .subscribe(setHTML('remaining')); ``` +### Related Recipes + +- [Memory Game](../../recipes/memory-game.md) +- [Save Indicator](../../recipes/save-indicator.md) + ### Additional Resources -- [empty](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-empty) - :newspaper: - Official docs +- [empty](https://rxjs.dev/api/index/function/empty) 📰 - Official docs - [Creation operators: empty, never, and throw](https://egghead.io/lessons/rxjs-creation-operators-empty-never-throw?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: +> 📁 Source Code: > [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/EmptyObservable.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/EmptyObservable.ts) diff --git a/operators/creation/from.md b/operators/creation/from.md index f229360c..514f4022 100644 --- a/operators/creation/from.md +++ b/operators/creation/from.md @@ -1,4 +1,4 @@ -# from +``# from #### signature: `from(ish: ObservableInput, mapFn: function, thisArg: any, scheduler: Scheduler): Observable` @@ -6,17 +6,15 @@ --- -:bulb: This operator can be used to convert a promise to an observable! +💡 This operator can be used to convert a promise to an observable! -:bulb: For arrays and iterables, all contained values will be emitted as a -sequence! +💡 For arrays and iterables, all contained values will be emitted as a sequence! -:bulb: This operator can also be used to emit a string as a sequence of -characters! +💡 This operator can also be used to emit a string as a sequence of characters! --- -
+ ### Examples @@ -94,17 +92,18 @@ const subscribe = source.subscribe(val => console.log(val)); ### Related Recipes -- [Progress Bar](../../recipes/progressbar.md) - [HTTP Polling](../../recipes/http-polling.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) +- [Progress Bar](../../recipes/progressbar.md) ### Additional Resources -- [from](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-from) - :newspaper: - Official docs +- [from](https://rxjs.dev/api/index/function/from) 📰 - Official docs - [Creation operators: from, fromArray, fromPromise](https://egghead.io/lessons/rxjs-creation-operators-from-fromarray-frompromise?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: +> 📁 Source Code: > [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/from.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/from.ts) diff --git a/operators/creation/fromevent.md b/operators/creation/fromevent.md index 6cb19a64..b7f68ed0 100644 --- a/operators/creation/fromevent.md +++ b/operators/creation/fromevent.md @@ -4,7 +4,7 @@ ## Turn event into observable sequence. -
+ ### Examples @@ -30,17 +30,36 @@ const subscribe = example.subscribe(val => console.log(val)); ### Related Recipes -- [Smart Counter](../../recipes/smartcounter.md) -- [Progress Bar](../../recipes/progressbar.md) +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Catch The Dot Game](../../recipes/catch-the-dot-game.md) +- [Click Ninja Game](../../recipes/click-ninja-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) - [Game Loop](../../recipes/gameloop.md) +- [Horizontal Scroll Indicator](../../recipes/horizontal-scroll-indicator.md) - [HTTP Polling](../../recipes/http-polling.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Progress Bar](../../recipes/progressbar.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Smart Counter](../../recipes/smartcounter.md) +- [Space Invaders Game](../../recipes/space-invaders-game.md) +- [Stop Watch](../../recipes/stop-watch.md) +- [Swipe To Refresh](../../recipes/swipe-to-refresh.md) +- [Tank Battle Game](../../recipes/tank-battle-game.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Type Ahead](../../recipes/type-ahead.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) ### Additional Resources -- [fromEvent](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-fromEvent) - :newspaper: - Official docs +- [fromEvent](https://rxjs.dev/api/index/function/fromEvent) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/FromEventObservable.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/FromEventObservable.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/fromEvent.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/fromEvent.ts) diff --git a/operators/creation/generate.md b/operators/creation/generate.md new file mode 100644 index 00000000..7b692b35 --- /dev/null +++ b/operators/creation/generate.md @@ -0,0 +1,82 @@ +# generate + +#### signature: `generate(initialStateOrOptions: GenerateOptions, condition?: ConditionFunc, iterate?: IterateFunc, resultSelectorOrObservable?: (ResultFunc) | SchedulerLike, scheduler?: SchedulerLike): Observable` + +## Generates an observable sequence by running a state-driven loop producing the sequence's elements, using the specified scheduler to send out observer messages. + + + +### Examples + +##### Example 1: Generate + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-generate?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { generate } from 'rxjs'; + +generate( + 2, + x => x <= 8, + x => x + 3 +).subscribe(console.log); + +/* +OUTPUT: +2 +5 +8 +*/ +``` + +##### Example 2: Generate with result selector + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-generate-result-selector?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { generate } from 'rxjs'; + +generate( + 2, + x => x <= 38, + x => x + 3, + x => '.'.repeat(x) +).subscribe(console.log); + +/* +OUTPUT: +.. +..... +........ +........... +.............. +................. +.................... +....................... +.......................... +............................. +................................ +................................... +...................................... +*/ +``` + +### Related Recipes + +- [Breakout Game](../../recipes/breakout-game.md) +- [Memory Game](../../recipes/memory-game.md) + +### Additional Resources + +- [generate](https://rxjs.dev/api/index/function/generate) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/generate.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/generate.ts) diff --git a/operators/creation/interval.md b/operators/creation/interval.md index ee514647..23b15080 100644 --- a/operators/creation/interval.md +++ b/operators/creation/interval.md @@ -4,7 +4,7 @@ ## Emit numbers in sequence based on provided timeframe. -
+ ### Examples @@ -25,14 +25,30 @@ const source = interval(1000); const subscribe = source.subscribe(val => console.log(val)); ``` +### Related Recipes + +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Catch The Dot Game](../../recipes/catch-the-dot-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) +- [Matrix Digital Rain](../../recipes/matrix-digital-rain.md) +- [Memory Game](../../recipes/memory-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Space Invaders Game](../../recipes/space-invaders-game.md) +- [Stop Watch](../../recipes/stop-watch.md) +- [Tank Battle Game](../../recipes/tank-battle-game.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) + ### Additional Resources -- [interval](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-interval) - :newspaper: - Official docs +- [interval](https://rxjs.dev/api/index/function/interval) 📰 - Official docs - [Creation operators: interval and timer](https://egghead.io/lessons/rxjs-creation-operators-interval-and-timer?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: +> 📁 Source Code: > [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/interval.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/interval.ts) diff --git a/operators/creation/of.md b/operators/creation/of.md index 26ddb6a0..472ececa 100644 --- a/operators/creation/of.md +++ b/operators/creation/of.md @@ -2,7 +2,7 @@ #### signature: `of(...values, scheduler: Scheduler): Observable` -## Emit variable amount of values in a sequence. +## Emit variable amount of values in a sequence and then emits a complete notification. ### Examples @@ -36,18 +36,29 @@ import { of } from 'rxjs'; const source = of({ name: 'Brian' }, [1, 2, 3], function hello() { return 'Hello'; }); -//output: {name: 'Brian}, [1,2,3], function hello() { return 'Hello' } +//output: {name: 'Brian'}, [1,2,3], function hello() { return 'Hello' } const subscribe = source.subscribe(val => console.log(val)); ``` +### Related Recipes + +- [Battleship Game](../../recipes/battleship-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Type Ahead](../../recipes/type-ahead.md) + ### Additional Resources -- [of](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-of) - :newspaper: - Official docs +- [of](https://rxjs.dev/api/index/function/of) 📰 - Official docs - [Creation operators: of](https://egghead.io/lessons/rxjs-creation-operator-of?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: +> 📁 Source Code: > [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/of.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/of.ts) diff --git a/operators/creation/range.md b/operators/creation/range.md index b58881cd..3f268207 100644 --- a/operators/creation/range.md +++ b/operators/creation/range.md @@ -4,7 +4,7 @@ ## Emit numbers in provided range in sequence. -
+ ### Examples @@ -27,10 +27,9 @@ const example = source.subscribe(val => console.log(val)); ### Additional Resources -- [range](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-range) - :newspaper: - Official docs +- [range](https://rxjs.dev/api/index/function/range) 📰 - Official docs --- -> :file_folder: Source Code: +> 📁 Source Code: > [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/range.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/range.ts) diff --git a/operators/creation/throw.md b/operators/creation/throw.md index c3772233..0d5bfbb7 100644 --- a/operators/creation/throw.md +++ b/operators/creation/throw.md @@ -1,46 +1,157 @@ -# throw +# throwError -#### signature: `throw(error: any, scheduler: Scheduler): Observable` +```typescript +throwError(errorOrErrorFactory: (() => any) | any): Observable +``` + +**Creates an Observable that immediately emits an error notification upon subscription.** + +--- + +💡 **When should you use throwError vs. just throwing an error?** + +In most cases within operator callbacks (like `map`, `tap`, `mergeMap`), you can simply use JavaScript's native `throw` statement since RxJS wraps these in try-catch blocks. Use `throwError()` specifically when you need to **return an Observable that errors** - particularly in operators like `switchMap`, `mergeMap`, or `concatMap` where an Observable is expected as the return value. + +💡 **Factory function recommended** + +As of RxJS 7+, pass a factory function `() => error` rather than the error directly. This creates the error at the moment of subscription, providing better stack traces: `throwError(() => new Error('message'))`. + +--- -## Emit error on subscription. +## Why use throwError? -
+Think of `throwError` as your "error signal generator" - it creates an Observable that does nothing but immediately send out an error signal. It's like having a specialized alarm button: when you press it, it doesn't emit any values or complete normally, it just triggers the error path. -### Examples +You'll reach for `throwError` when you're working in Observable pipelines where you need to return an Observable, but something has gone wrong and you want to propagate that error downstream. For instance, in a [conditional API call](#example-2-conditional-error-in-switchmap) where you validate input before making a request, or when [implementing retry logic](#example-3-using-throwerror-with-retry-strategy) where you want to signal specific failures. -##### Example 1: Throw error on subscription +Here's a key insight: while JavaScript's native `throw` statement works great inside operators like `map` or `tap`, `throwError()` shines when you're in operators that expect you to return an Observable - like `switchMap`, `mergeMap`, or inside a custom creation function. In those cases, just throwing would break the chain; you need to return an error Observable instead. It's the difference between throwing an error in your code versus constructing an Observable that represents an error state. + +--- + +## Examples + +### Example 1: Basic error emission -( [StackBlitz](https://stackblitz.com/edit/typescript-5d3stz?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/punubequju/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/mks82xqz/) ) -```js -// RxJS v6+ +```typescript import { throwError } from 'rxjs'; -//emits an error with specified value on subscription -const source = throwError('This is an error!'); -//output: 'Error: This is an error!' -const subscribe = source.subscribe({ - next: val => console.log(val), - complete: () => console.log('Complete!'), - error: val => console.log(`Error: ${val}`) +// Create an observable that immediately emits an error +const error$ = throwError(() => new Error('Something went wrong!')); + +// Subscribe to see the error +error$.subscribe({ + next: val => console.log('Next:', val), // Won't be called + error: err => console.error('Error caught:', err.message), + complete: () => console.log('Complete!') // Won't be called +}); + +// Output: "Error caught: Something went wrong!" +``` + +### Example 2: Conditional error in switchMap + +[StackBlitz](https://stackblitz.com/edit/typescript-oulg71yj?file=index.ts) + +```typescript +import { of, throwError } from 'rxjs'; +import { mergeMap, catchError } from 'rxjs/operators'; + +interface User { + id: number; + name: string; +} + +// Simulate fetching user data +function fetchUser(id: number) { + return of({ id, name: `User ${id}` }); +} + +// Validate user ID before fetching - handle errors per item +of(0, 5, -1, 10) + .pipe( + mergeMap((id) => { + // Create the source observable based on validation + const source$ = + id <= 0 + ? throwError(() => new Error(`Invalid user ID: ${id}`)) + : fetchUser(id); + + // Handle errors for each item individually + return source$.pipe( + catchError((err) => { + console.error('Caught:', err()); + // Provide fallback user for this item only + return of({ id: 0, name: 'Guest User' } as User); + }) + ); + }) + ) + .subscribe((user) => console.log('User:', user.name)); + +/* Output: + Caught: Invalid user ID: 0 + User: Guest User + User: User 5 + Caught: Invalid user ID: -1 + User: Guest User + User: User 10 +*/ +``` + +### Example 3: Using throwError with retry strategy + +[StackBlitz](https://stackblitz.com/edit/typescript-gtwzvj3b?file=index.ts) + +```typescript +import { of, throwError, timer } from 'rxjs'; +import { mergeMap, retry, tap } from 'rxjs/operators'; + +let attemptCount = 0; + +// Simulate an unreliable API call +function unreliableApiCall() { + attemptCount++; + console.log(`API call attempt #${attemptCount}`); + + // Fail first 2 attempts, succeed on 3rd + return attemptCount < 3 + ? throwError(() => new Error('Network timeout')) + : of({ data: 'Success!' }); +} + +// Try the API call with retry logic +of(null).pipe( + mergeMap(() => unreliableApiCall()), + retry(2) // Retry up to 2 times on error +).subscribe({ + next: result => console.log('Result:', result.data), + error: err => console.error('Final error:', err.message) }); + +/* Output: + API call attempt #1 + API call attempt #2 + API call attempt #3 + Result: Success! +*/ ``` -### Related Examples +--- + +## Related Recipes -- [Throwing after 3 retries](../error_handling/retrywhen.md) +- [Smart Counter](https://www.learnrxjs.io/learn-rxjs/recipes/smartcounter) +- [HTTP Polling](https://www.learnrxjs.io/learn-rxjs/recipes/http-polling) + +--- -### Additional Resources +## Additional Resources -- [throw](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-throw) - :newspaper: - Official docs -- [Creation operators: empty, never, and throw](https://egghead.io/lessons/rxjs-creation-operators-empty-never-throw?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz +- [throwError](https://rxjs.dev/api/index/function/throwError) 📰 - Official docs +- [Error Handling in RxJS](https://blog.angular-university.io/rxjs-error-handling/) 📰 - Comprehensive guide --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/throwError.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/throwError.ts) +📁 **Source Code:** [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/throwError.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/throwError.ts) \ No newline at end of file diff --git a/operators/creation/timer.md b/operators/creation/timer.md index e3df897e..75582f15 100644 --- a/operators/creation/timer.md +++ b/operators/creation/timer.md @@ -4,7 +4,7 @@ ## After given duration, emit numbers in sequence every specified duration. -
+ ### Examples @@ -52,12 +52,11 @@ const subscribe = source.subscribe(val => console.log(val)); ### Additional Resources -- [timer](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-timer) - :newspaper: - Official docs +- [timer](https://rxjs.dev/api/index/function/timer) 📰 - Official docs - [Creation operators: interval and timer](https://egghead.io/lessons/rxjs-creation-operators-interval-and-timer?course=rxjs-beyond-the-basics-creating-observables-from-scratch) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: +> 📁 Source Code: > [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/timer.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/timer.ts) diff --git a/operators/error_handling/README.md b/operators/error_handling/README.md index 54b3ff3e..bb21e84f 100644 --- a/operators/error_handling/README.md +++ b/operators/error_handling/README.md @@ -5,8 +5,8 @@ effective ways to gracefully handle errors and retry logic, should they occur. ## Contents -* [catch / catchError](catch.md) :star: -* [retry](retry.md) -* [retryWhen](retrywhen.md) +- [catchError](catch.md) ⭐ +- [retry](retry.md) +- [retryWhen](retrywhen.md) -:star: - _commonly used_ +⭐ - _commonly used_ diff --git a/operators/error_handling/catch.md b/operators/error_handling/catch.md index 30402a7b..0be51209 100644 --- a/operators/error_handling/catch.md +++ b/operators/error_handling/catch.md @@ -1,75 +1,181 @@ -# catch / catchError +# catchError -#### signature: `catchError(project : function): Observable` +## signature: `catchError(project: (err: any, caught: Observable) => ObservableInput): Observable` -## Gracefully handle errors in an observable sequence. +### Gracefully handle errors in an observable sequence. --- -:warning: Remember to return an observable from the catchError function! +💡 Need to retry a failed operation? Check out [**retry**](./retry.md) or [**retryWhen**](./retrywhen.md)! + +💡 For resource cleanup regardless of error, use [**finalize**](../utility/finalize.md)! + +⚠ Remember to return an observable from the catchError function! --- -
+## Why use catchError? + +Think of `catchError` as your observable's safety net. When your stream encounters an error, whether from a failed HTTP request, unexpected data, or any other issue, `catchError` gives you a chance to recover gracefully instead of letting your entire observable sequence crash and burn. It's the difference between your app showing a friendly "Something went wrong" message versus a blank screen. -### Examples +What makes `catchError` particularly valuable is its flexibility in how you respond to errors. You can provide [fallback data from a cache](#example-1-catching-error-from-observable), you can [transform errors into user-friendly messages](#example-2-catching-rejected-promise), or you can even decide to retry the operation (though [`retry`](./retry.md) is usually better for that). The key insight is understanding *where* you place `catchError` in your operator chain. Put it at the outer level and your entire stream ends when an error occurs, but place it inside operators like `switchMap` and [only that inner operation fails while your stream continues](#example-3-catching-errors-comparison-when-using-switchmapmergemapconcatmapexhaustmap). + +In essence, `catchError` keeps your reactive applications resilient by ensuring that errors are handled on your terms, not left to crash your user experience. + +--- -( -[example tests](https://github.com/btroncone/learn-rxjs/blob/master/operators/specs/error_handling/catch-spec.ts) -) +## Examples -##### Example 1: Catching error from observable +### Example 1: Catching error from observable -( -[StackBlitz](https://stackblitz.com/edit/typescript-auc2u2?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/porevoxelu/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/wk4oLLqc/) ) +( [StackBlitz](https://stackblitz.com/edit/typescript-auc2u2?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ import { throwError, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; -//emit error + +// emit error const source = throwError('This is an error!'); -//gracefully handle error, returning observable with error message -const example = source.pipe(catchError(val => of(`I caught: ${val}`))); -//output: 'I caught: This is an error' + +// gracefully handle error, returning observable with error message +const example = source.pipe( + catchError(val => of(`I caught: ${val}`)) +); + +// output: 'I caught: This is an error' const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 2: Catching rejected promise +### Example 2: Catching rejected promise -( -[StackBlitz](https://stackblitz.com/edit/typescript-nte3xs?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/rusaxubanu/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/sLq92gLv/) ) +( [StackBlitz](https://stackblitz.com/edit/typescript-nte3xs?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ import { timer, from, of } from 'rxjs'; import { mergeMap, catchError } from 'rxjs/operators'; -//create promise that immediately rejects +// create promise that immediately rejects const myBadPromise = () => new Promise((resolve, reject) => reject('Rejected!')); -//emit single value after 1 second + +// emit single value after 1 second const source = timer(1000); -//catch rejected promise, returning observable containing error message + +// catch rejected promise, returning observable containing error message const example = source.pipe( mergeMap(_ => - from(myBadPromise()).pipe(catchError(error => of(`Bad Promise: ${error}`))) + from(myBadPromise()).pipe( + catchError(error => of(`Bad Promise: ${error}`)) + ) ) ); -//output: 'Bad Promise: Rejected' + +// output: 'Bad Promise: Rejected' const subscribe = example.subscribe(val => console.log(val)); ``` -### Additional Resources +### Example 3: Catching errors comparison when using switchMap/mergeMap/concatMap/exhaustMap + +( [StackBlitz](https://stackblitz.com/edit/rxjs-catcherror-withmapoperators?file=index.ts&devtoolsheight=80) ) + +```js +// RxJS v6+ +import { throwError, fromEvent, of } from 'rxjs'; +import { + catchError, + tap, + switchMap, + mergeMap, + concatMap, + exhaustMap +} from 'rxjs/operators'; + +// simulate an API call that throws an error +const fakeRequest$ = of().pipe( + tap(_ => console.log('fakeRequest')), + throwError +); + +/* + * Placement of catchError MATTERS! + * + * When catchError is placed INSIDE the switchMap, only the inner + * observable errors out and the outer stream continues. + * You can keep clicking and making new requests. + */ +const iWillContinueListening$ = fromEvent( + document.getElementById('continued'), + 'click' +).pipe( + switchMap(_ => + fakeRequest$.pipe( + catchError(_ => of('keep on clicking!!!')) + ) + ) +); + +/* + * When catchError is placed at the OUTER level, after switchMap, + * the entire stream errors out and terminates. + * After the first click, the stream is dead and won't respond to more clicks. + */ +const iWillStopListening$ = fromEvent( + document.getElementById('stopped'), + 'click' +).pipe( + switchMap(_ => fakeRequest$), + catchError(_ => of('no more requests!!!')) +); + +iWillContinueListening$.subscribe(console.log); +iWillStopListening$.subscribe(console.log); +``` + +### Example 4: Providing fallback data on HTTP error + +( [StackBlitz](https://stackblitz.com/edit/typescript-n9llq6e3?file=index.ts) ) + +```js +// RxJS v6+ +import { ajax } from 'rxjs/ajax'; +import { of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +// attempt to fetch user data from API +const userData$ = ajax.getJSON('https://api.example.com/user/123').pipe( + map(response => response.data), + // if the request fails, provide cached or default data + catchError(error => { + console.error('Failed to fetch user, using cached data', error); + return of({ + id: 123, + name: 'Cached User', + status: 'offline' + }); + }) +); + +// user always gets data, even if the API is down +userData$.subscribe(user => console.log('User:', user)); +``` + +--- + +## Related Recipes + +- [Smart Counter](../../recipes/smartcounter.md) +- [HTTP Polling](../../recipes/http-polling.md) + +--- + +## Additional Resources -- [Error handling operator: catch](https://egghead.io/lessons/rxjs-error-handling-operator-catch?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [catchError](https://rxjs.dev/api/operators/catchError) 📰 - Official docs +- [Error handling operator: catch](https://egghead.io/lessons/rxjs-error-handling-operator-catch?course=rxjs-beyond-the-basics-operators-in-depth) 🎥 💵 - André Staltz +- [Error Handling in RxJS](https://blog.angular-university.io/rxjs-error-handling/) 📰 - Angular University --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/catchError.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/catchError.ts) +📁 **Source Code:** [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/catchError.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/catchError.ts) \ No newline at end of file diff --git a/operators/error_handling/retry.md b/operators/error_handling/retry.md index deeb61de..3c0ffb51 100644 --- a/operators/error_handling/retry.md +++ b/operators/error_handling/retry.md @@ -4,7 +4,18 @@ ## Retry an observable sequence a specific number of times should an error occur. -
+--- + +💡 Useful for retrying HTTP requests! + +💡 If you only want to retry in certain cases, check out +[`retryWhen`](./retrywhen.md)! + +💡 For non error cases check out [`repeat`](../utility/repeat.md)! + +--- + + ### Examples @@ -48,12 +59,11 @@ const subscribe = example.subscribe({ ### Additional Resources -- [retry](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-retry) - :newspaper: - Official docs +- [retry](https://rxjs.dev/api/operators/retry) 📰 - Official docs - [Error handling operator: retry and retryWhen](https://egghead.io/lessons/rxjs-error-handling-operator-retry-and-retrywhen?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/retry.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/retry.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/retry.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/retry.ts) diff --git a/operators/error_handling/retrywhen.md b/operators/error_handling/retrywhen.md index 4adbd3fe..cd96ab23 100644 --- a/operators/error_handling/retrywhen.md +++ b/operators/error_handling/retrywhen.md @@ -4,7 +4,7 @@ ## Retry an observable sequence on error based on custom criteria. -
+ ### Examples @@ -34,7 +34,7 @@ const example = source.pipe( errors.pipe( //log error message tap(val => console.log(`Value ${val} was too high!`)), - //restart in 5 seconds + //restart in 6 seconds delayWhen(val => timer(val * 1000)) ) ) @@ -48,7 +48,7 @@ const example = source.pipe( 4 5 "Value 6 was too high!" - --Wait 5 seconds then repeat + --Wait 6 seconds then repeat */ const subscribe = example.subscribe(val => console.log(val)); ``` @@ -60,7 +60,7 @@ const subscribe = example.subscribe(val => console.log(val)); ) ```js -import { Observable, _throw, timer } from 'rxjs'; +import { Observable, throwError, timer } from 'rxjs'; import { mergeMap, finalize } from 'rxjs/operators'; export const genericRetryStrategy = ({ @@ -81,7 +81,7 @@ export const genericRetryStrategy = ({ retryAttempt > maxRetryAttempts || excludedStatusCodes.find(e => e === error.status) ) { - return _throw(error); + return throwError(error); } console.log( `Attempt ${retryAttempt}: retrying in ${retryAttempt * @@ -139,12 +139,11 @@ export class AppComponent implements OnInit { ### Additional Resources -- [retryWhen](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-retryWhen) - :newspaper: - Official docs +- [retryWhen](https://rxjs.dev/api/operators/retryWhen) 📰 - Official docs - [Error handling operator: retry and retryWhen](https://egghead.io/lessons/rxjs-error-handling-operator-retry-and-retrywhen?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/retryWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/retryWhen.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/retryWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/retryWhen.ts) diff --git a/operators/filtering/README.md b/operators/filtering/README.md index 4fe79ec0..f811c385 100644 --- a/operators/filtering/README.md +++ b/operators/filtering/README.md @@ -9,24 +9,28 @@ with ## Contents -* [audit](audit.md) -* [auditTime](audittime.md) -* [debounce](debounce.md) -* [debounceTime](debouncetime.md) :star: -* [distinctUntilChanged](distinctuntilchanged.md) :star: -* [filter](filter.md) :star: -* [first](first.md) -* [ignoreElements](ignoreelements.md) -* [last](last.md) -* [sample](sample.md) -* [single](single.md) -* [skip](skip.md) -* [skipUntil](skipuntil.md) -* [skipWhile](skipwhile.md) -* [take](take.md) :star: -* [takeUntil](takeuntil.md) :star: -* [takeWhile](takewhile.md) -* [throttle](throttle.md) -* [throttleTime](throttletime.md) +- [audit](audit.md) +- [auditTime](audittime.md) +- [debounce](debounce.md) +- [debounceTime](debouncetime.md) ⭐ +- [distinct](distinct.md) +- [distinctUntilChanged](distinctuntilchanged.md) ⭐ +- [distinctUntilKeyChanged](distinctuntilkeychanged.md) +- [filter](filter.md) ⭐ +- [find](find.md) +- [first](first.md) +- [ignoreElements](ignoreelements.md) +- [last](last.md) +- [sample](sample.md) +- [single](single.md) +- [skip](skip.md) +- [skipUntil](skipuntil.md) +- [skipWhile](skipwhile.md) +- [take](take.md) ⭐ +- [takeLast](takelast.md) +- [takeUntil](takeuntil.md) ⭐ +- [takeWhile](takewhile.md) +- [throttle](throttle.md) +- [throttleTime](throttletime.md) -:star: - _commonly used_ +⭐ - _commonly used_ diff --git a/operators/filtering/audit.md b/operators/filtering/audit.md index 2a7994da..57b8b5e3 100644 --- a/operators/filtering/audit.md +++ b/operators/filtering/audit.md @@ -8,10 +8,10 @@ ### Additional Resources -* [audit](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-audit) - :newspaper: - Official docs +* [audit](https://rxjs.dev/api/operators/audit) + 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/audit.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/audit.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/audit.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/audit.ts) diff --git a/operators/filtering/audittime.md b/operators/filtering/audittime.md index 631ce0bf..83f7aa6e 100644 --- a/operators/filtering/audittime.md +++ b/operators/filtering/audittime.md @@ -1,17 +1,44 @@ # auditTime -#### signature: `audit(duration: number, scheduler?: Scheduler): Observable` +#### signature: `auditTime(duration: number, scheduler?: Scheduler): Observable` ## Ignore for given time then emit most recent value -### [ Examples Coming Soon! ] +### Why use `auditTime` + +When you are interested in ignoring a source observable for a given amount of time, you can use `auditTime`. A possible use case is to only emit certain events (i.e. mouse clicks) at a maximum rate per second. After the specified duration has passed, the timer is disabled and the most recent source value is emitted on the output Observable, and this process repeats for the next source value. + +--- + +💡 If you want the timer to reset whenever a new event occurs on the source observable, you can use [debounceTime](debouncetime.md) + +--- + +### Examples + +##### Example 1: Emit clicks at a rate of at most one click per second + +( [stackBlitz](https://stackblitz.com/edit/typescript-skykxw) ) + +```js +import { fromEvent } from 'rxjs'; +import { auditTime } from 'rxjs/operators'; + +// Create observable that emits click events +const source = fromEvent(document, 'click'); +// Emit clicks at a rate of at most one click per second +const example = source.pipe(auditTime(1000)) +// Output (example): '(1s) --- Clicked --- (1s) --- Clicked' +const subscribe = example.subscribe(val => console.log('Clicked')); +``` ### Additional Resources -* [auditTime](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-auditTime) - :newspaper: - Official docs +* [auditTime](https://rxjs.dev/api/operators/auditTime) + 📰 - Official docs +* [Time based operators comparison](../../concepts/time-based-operators-comparison.md) --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/auditTime.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/auditTime.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/auditTime.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/auditTime.ts) diff --git a/operators/filtering/debounce.md b/operators/filtering/debounce.md index 7444cf85..0c9cd4f2 100644 --- a/operators/filtering/debounce.md +++ b/operators/filtering/debounce.md @@ -6,16 +6,20 @@ --- -:bulb: Though not as widely used as [debounceTime](debouncetime.md), -**debounce** is important when the debounce rate is variable! +💡 Though not as widely used as [debounceTime](debouncetime.md), **debounce** is +important when the debounce rate is variable! --- + + ### Examples ##### Example 1: Debounce on timer -( [StackBlitz](https://stackblitz.com/edit/typescript-dzjbra?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/sorimeyoro/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-dzjbra?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/sorimeyoro/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/e5698yow/) ) ```js @@ -39,7 +43,9 @@ const subscribe = debouncedExample.subscribe(val => console.log(val)); ##### Example 2: Debounce at increasing interval -( [StackBlitz](https://stackblitz.com/edit/typescript-qnfidr?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/sotaretese/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-qnfidr?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/sotaretese/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/6ab34nq6/) ) ```js @@ -63,12 +69,11 @@ const subscribe = debouncedInterval.subscribe(val => ### Additional Resources -* [debounce](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-debounce) - :newspaper: - Official docs -* [Transformation operator: debounce and debounceTime](https://egghead.io/lessons/rxjs-transformation-operators-debounce-and-debouncetime?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [debounce](https://rxjs.dev/api/operators/debounce) 📰 - Official docs +- [Transformation operator: debounce and debounceTime](https://egghead.io/lessons/rxjs-transformation-operators-debounce-and-debouncetime?course=rxjs-beyond-the-basics-operators-in-depth) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/debounce.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/debounce.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/debounce.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/debounce.ts) diff --git a/operators/filtering/debouncetime.md b/operators/filtering/debouncetime.md index c53c5e95..a2989705 100644 --- a/operators/filtering/debouncetime.md +++ b/operators/filtering/debouncetime.md @@ -6,46 +6,70 @@ --- -:bulb: This operator is popular in scenarios such as type-ahead where the rate -of user input must be controlled! +💡 This operator is popular in scenarios such as type-ahead where the rate of +user input must be controlled! --- +### Why use `debounceTime`? + +Think of `debounceTime` like taking a pause in a conversation to let the other person finish their thought. This operator is incredibly handy when you're dealing with rapid sequences of events and only care about acting upon the last event after a specified duration. + +A classic real-world application is in form inputs, particularly in search bars. Imagine you're typing into a search box. Instead of firing off an API call with every keystroke (which can be overwhelming and inefficient), you'd want to wait a bit after the user stops typing to ensure you're fetching data based on their complete thought. That "waiting a bit" is where `debounceTime` shines. For instance, by setting `debounceTime(300)`, the system will wait for 300 milliseconds after the last keystroke before it proceeds. + +In Angular, when dealing with reactive forms, `debounceTime` is a lifesaver. By adding this operator to a form control's value changes observable, you can efficiently handle values only after users finish their input. Check out the below example: +```typescript +this.myFormControl.valueChanges.pipe( + debounceTime(300) +).subscribe(value => { + // handle the value after 300ms of inactivity +}); +``` + + + + ### Examples ##### Example 1: Debouncing based on time between input -( [StackBlitz](https://stackblitz.com/edit/typescript-adheqt?file=index.ts&devtoolsheight=50) | [jsBin](http://jsbin.com/kacijarogi/1/edit?js,console,output) | -[jsFiddle](https://jsfiddle.net/btroncone/7kbg4q2e/) ) +( +[StackBlitz](https://stackblitz.com/edit/typescript-adheqt?file=index.ts&devtoolsheight=50) +) ```js // RxJS v6+ -import { fromEvent, timer } from 'rxjs'; +import { fromEvent } from 'rxjs'; import { debounceTime, map } from 'rxjs/operators'; -const input = document.getElementById('example'); - -//for every keyup, map to current input value -const example = fromEvent(input, 'keyup').pipe(map(i => i.currentTarget.value)); +// elem ref +const searchBox = document.getElementById('search'); -//wait .5s between keyups to emit current value -//throw away all other values -const debouncedInput = example.pipe(debounceTime(500)); +// streams +const keyup$ = fromEvent(searchBox, 'keyup'); -//log values -const subscribe = debouncedInput.subscribe(val => { - console.log(`Debounced Input: ${val}`); -}); +// wait .5s between keyups to emit current value +keyup$ + .pipe( + map((i: any) => i.currentTarget.value), + debounceTime(500) + ) + .subscribe(console.log); ``` +### Related Recipes + +- [Save Indicator](../../recipes/save-indicator.md) +- [Type Ahead](../../recipes/type-ahead.md) + ### Additional Resources -* [debounceTime](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-debounceTime) - :newspaper: - Official docs -* [Transformation operator: debounce and debounceTime](https://egghead.io/lessons/rxjs-transformation-operators-debounce-and-debouncetime?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [debounceTime](https://rxjs.dev/api/operators/debounceTime) 📰 - Official docs +- [Transformation operator: debounce and debounceTime](https://egghead.io/lessons/rxjs-transformation-operators-debounce-and-debouncetime?course=rxjs-beyond-the-basics-operators-in-depth) + 🎥 💵 - André Staltz +- [Time based operators comparison](../../concepts/time-based-operators-comparison.md) --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/debounceTime.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/debounceTime.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/debounceTime.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/debounceTime.ts) diff --git a/operators/filtering/distinct.md b/operators/filtering/distinct.md new file mode 100644 index 00000000..fb294f5c --- /dev/null +++ b/operators/filtering/distinct.md @@ -0,0 +1,62 @@ +# distinct + +#### signature: `distinct(keySelector?, flushes?): Observable` + +## Emits items emitted that are distinct based on any previously emitted item. + + + +### Examples + +##### Example 1: distinct without selector + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-distinct-example-wphfch?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { of } from 'rxjs'; +import { distinct } from 'rxjs/operators'; + +of(1, 2, 3, 4, 5, 1, 2, 3, 4, 5) + .pipe(distinct()) + // OUTPUT: 1,2,3,4,5 + .subscribe(console.log); +``` + +##### Example 2: distinct with key selector + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-distinct-example?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { from } from 'rxjs'; +import { distinct } from 'rxjs/operators'; + +const obj1 = { id: 3, name: 'name 1' }; +const obj2 = { id: 4, name: 'name 2' }; +const obj3 = { id: 3, name: 'name 3' }; +const vals = [obj1, obj2, obj3]; + +from(vals) + .pipe(distinct(e => e.id)) + .subscribe(console.log); + +/* +OUTPUT: +{id: 3, name: "name 1"} +{id: 4, name: "name 2"} + */ +``` + +### Additional Resources + +- [distinct](https://rxjs.dev/api/operators/distinct) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/distinct.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/distinct.ts) diff --git a/operators/filtering/distinctuntilchanged.md b/operators/filtering/distinctuntilchanged.md index a6ea1bc6..a4c5ec2f 100644 --- a/operators/filtering/distinctuntilchanged.md +++ b/operators/filtering/distinctuntilchanged.md @@ -6,40 +6,55 @@ --- -:bulb: distinctUntilChanged uses `===` comparison by default, object references -must match! +💡 distinctUntilChanged uses `===` comparison by default, object references must +match! + +💡 If you want to compare based on an object property, you can use +[`distinctUntilKeyChanged`](distinctuntilkeychanged.md) instead! --- +### Why use `distinctUntilChanged`? + +This operator stands guard, ensuring that you're not bombarded with repetitive information. Imagine if your best friend kept repeating the same story to you every time you met. It'd get old, right? The `distinctUntilChanged` operator does just that; it prevents subsequent identical emissions from an observable. + +Think about a search bar on a website. As users type their queries, you don't want to send a server request for the same input value multiple times in a row, it would be redundant and inefficient. Here’s where `distinctUntilChanged` combined with [`debounceTime`](debouncetime.md) could shine. Imagine a user typing in a search term and then slightly hesitating before adding another letter. You might want to wait just a little bit (that’s (`debounceTime`)[debouncetime.md] doing its magic) and then, before firing off a request, ensure the term is actually different than the previous one - this is where you can utilize `distinctUntilChanged`. + +For example, if a user is searching for "apple" and they type "app" -> wait a bit -> "appl" -> backtrack to "app" -> type again "appl", without `distinctUntilChanged`, you might end up sending redundant requests. But with it, once "appl" is recognized as previously processed, it won't send the redundant search request again. + +It's important to remember that **`distinctUntilChanged` compares the current value with the last emitted value**. It doesn’t keep a long history. So, if an observable emitted the values 1, 2, 2, 3, 3, 2 - you’d get 1, 2, 3, 2 in return. By default, it also uses simple equality to compare values. If you're working with objects or arrays, you might need to provide a custom comparison function to determine whether values are distinct. + +In essence, when you're looking to filter out consecutive duplicate emissions from your observables, think of `distinctUntilChanged` as your go-to option. + + + ### Examples ##### Example 1: distinctUntilChanged with basic values -( [StackBlitz](https://stackblitz.com/edit/typescript-bsb8mw?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/qoyoxeheva/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/xc2vzct7/) ) +( +[StackBlitz](https://stackblitz.com/edit/typescript-bsb8mw?file=index.ts&devtoolsheight=100) +) ```js // RxJS v6+ import { from } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; -//only output distinct values, based on the last emitted value -const myArrayWithDuplicatesInARow = from([1, 1, 2, 2, 3, 1, 2, 3]); +// only output distinct values, based on the last emitted value +const source$ = from([1, 1, 2, 2, 3, 3]); -const distinctSub = myArrayWithDuplicatesInARow +source$ .pipe(distinctUntilChanged()) - //output: 1,2,3,1,2,3 - .subscribe(val => console.log('DISTINCT SUB:', val)); - -const nonDistinctSub = myArrayWithDuplicatesInARow - //output: 1,1,2,2,3,1,2,3 - .subscribe(val => console.log('NON DISTINCT SUB:', val)); + // output: 1,2,3 + .subscribe(console.log); ``` ##### Example 2: distinctUntilChanged with objects -( [StackBlitz](https://stackblitz.com/edit/typescript-moe7mh?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/mexocipave/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/t4ava5b4/) ) +( +[StackBlitz](https://stackblitz.com/edit/typescript-moe7mh?file=index.ts&devtoolsheight=100) +) ```js // RxJS v6+ @@ -47,27 +62,57 @@ import { from } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; const sampleObject = { name: 'Test' }; + //Objects must be same reference -const myArrayWithDuplicateObjects = from([ - sampleObject, - sampleObject, - sampleObject -]); -//only out distinct objects, based on last emitted value -const nonDistinctObjects = myArrayWithDuplicateObjects +const source$ = from([sampleObject, sampleObject, sampleObject]); + +// only emit distinct objects, based on last emitted value +source$ .pipe(distinctUntilChanged()) - //output: 'DISTINCT OBJECTS: {name: 'Test'} - .subscribe(val => console.log('DISTINCT OBJECTS:', val)); + // output: {name: 'Test'} + .subscribe(console.log); ``` +##### Example 3: Using custom comparer function + +( +[StackBlitz](https://stackblitz.com/edit/typescript-hzta27?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { from } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; + +// only output distinct values, based on the last emitted value +const source$ = from([ + { name: 'Brian' }, + { name: 'Joe' }, + { name: 'Joe' }, + { name: 'Sue' } +]); + +source$ + // custom compare for name + .pipe(distinctUntilChanged((prev, curr) => prev.name === curr.name)) + // output: { name: 'Brian }, { name: 'Joe' }, { name: 'Sue' } + .subscribe(console.log); +``` + +### Related Recipes + +- [Lockscreen](../../recipes/lockscreen.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Type Ahead](../../recipes/type-ahead.md) + ### Additional Resources -* [distinctUntilChanged](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-distinctUntilChanged) - :newspaper: - Official docs -* [Filtering operator: distinct and distinctUntilChanged](https://egghead.io/lessons/rxjs-filtering-operators-distinct-and-distinctuntilchanged?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [distinctUntilChanged](https://rxjs.dev/api/operators/distinctUntilChanged) + 📰 - Official docs +- [Filtering operator: distinct and distinctUntilChanged](https://egghead.io/lessons/rxjs-filtering-operators-distinct-and-distinctuntilchanged?course=rxjs-beyond-the-basics-operators-in-depth) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/distinctUntilChanged.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/distinctUntilChanged.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/distinctUntilChanged.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/distinctUntilChanged.ts) diff --git a/operators/filtering/distinctuntilkeychanged.md b/operators/filtering/distinctuntilkeychanged.md new file mode 100644 index 00000000..ebf95c9a --- /dev/null +++ b/operators/filtering/distinctuntilkeychanged.md @@ -0,0 +1,117 @@ +# distinctUntilKeyChanged + +#### signature: `distinctUntilKeyChanged(key, compare?: fn): Observable` + +## Only emit when the specified key value changes + +💡 If you're comparing entire values, use [distinctUntilChanged](./distinctuntilchanged.md) instead! +💡 For truly unique values across the entire stream (not just consecutive), check out [distinct](./distinct.md)! + +### Why use distinctUntilKeyChanged? + +When you're working with streams of objects—think user profiles, product items, or API responses, you often care about changes to a specific property rather than the entire object. Maybe you're tracking users by their ID, filtering products by SKU, or monitoring status updates. That's where `distinctUntilKeyChanged` shines. + +Think of it like having a smart doorman at a club who recognizes people by their membership cards. Even if someone changes their outfit or hairstyle (other object properties), the doorman only cares about the card number. If you try to enter twice in a row with the same card, they'll stop you the second time: "You're already inside!" But if someone else with a different card shows up, they're let through. That's exactly what `distinctUntilKeyChanged` does—it checks a specific "card" (key) on each object and only lets through consecutive duplicates. + +This is incredibly useful for scenarios like live data feeds where objects might be re-emitted frequently but you only want to react when a specific property actually changes. For instance, you might receive user objects from a WebSocket connection, but you only want to update your UI when the user's status field changes from 'online' to 'offline', not every time the object is re-broadcast. By using `distinctUntilKeyChanged('status')`, you filter out the noise and [react only to meaningful changes](#example-1-compare-based-on-key). + +### Examples + +##### Example 1: Compare based on key + +( +[StackBlitz](https://stackblitz.com/edit/typescript-bpl1gwyk?file=index.ts) +) + +```js +// RxJS v6+ +import { from } from 'rxjs'; +import { distinctUntilKeyChanged } from 'rxjs/operators'; + +// only output distinct values based on the 'name' key +const source$ = from([ + { name: 'Brian' }, + { name: 'Joe' }, + { name: 'Joe' }, + { name: 'Sue' } +]); + +source$ + // only emit when name property changes + .pipe(distinctUntilKeyChanged('name')) + // output: { name: 'Brian' }, { name: 'Joe' }, { name: 'Sue' } + .subscribe(console.log); +``` + +##### Example 2: Keyboard events + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-distinctuntilkeychanged?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { distinctUntilKeyChanged, map } from 'rxjs/operators'; + +// listen to keyboard events +const keys$ = fromEvent(document, 'keyup').pipe( + // only emit when the 'code' property changes (ignore repeated key holds) + distinctUntilKeyChanged('code'), + // extract the actual key value for display + map(e => e?.key) +); + +keys$.subscribe(console.log); +``` + +##### Example 3: Custom comparison function + +( +[StackBlitz](https://stackblitz.com/edit/typescript-exygmrr6?file=index.ts) +) + +```js +// RxJS v6+ +import { of } from 'rxjs'; +import { distinctUntilKeyChanged } from 'rxjs/operators'; + +interface Person { + age: number; + name: string; +} + +// stream of person objects +const people$ = of( + { age: 4, name: 'Foo1' }, + { age: 7, name: 'Bar' }, + { age: 5, name: 'Foo2' }, + { age: 6, name: 'Foo3' } +); + +people$ + .pipe( + // only emit when first 3 letters of name change + distinctUntilKeyChanged( + 'name', + (x: string, y: string) => x.substring(0, 3) === y.substring(0, 3) + ) + ) + // output: { age: 4, name: 'Foo1' }, { age: 7, name: 'Bar' }, { age: 5, name: 'Foo2' } + .subscribe(console.log); +``` + +### Related Recipes + +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Battleship Game](../../recipes/battleship-game.md) + +### Additional Resources + +- [distinctUntilKeyChanged](https://rxjs.dev/api/operators/distinctUntilKeyChanged) 📰 - Official docs +- [Filtering operator: distinct, distinctUntilChanged, distinctUntilKeyChanged](https://egghead.io/lessons/rxjs-filtering-operators-distinct-distinctuntilchanged-distinctuntilkeychanged) 🎥 - Egghead.io + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/distinctUntilKeyChanged.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/distinctUntilKeyChanged.ts) \ No newline at end of file diff --git a/operators/filtering/filter.md b/operators/filtering/filter.md index 002c47e1..7b0a35a5 100644 --- a/operators/filtering/filter.md +++ b/operators/filtering/filter.md @@ -6,16 +6,28 @@ --- -:bulb: If you would like to complete an observable when a condition fails, check -out [takeWhile](takewhile.md)! +💡 If you would like to complete an observable when a condition fails, check out +[takeWhile](takewhile.md)! --- +### Why use `filter`? + +This operator is your go-to when you need to sift out unwanted values from an observable stream. Think of it as a fisherman's net, catching only the types of fish you desire while allowing others to slip through. + +**The critical point to remember** is that `filter` will only emit values that meet the specified condition. If no values in the observable satisfy the condition, nothing gets emitted. It's a strict bouncer at a club's entrance, only letting in those who fit the criteria. + +Also, for scenarios where you not only want to filter values but also transform them, [`map`](../transformation/map.md) is an ideal companion to `filter`. Use them in tandem to both shape and refine your data streams. + + + ### Examples ##### Example 1: filter for even numbers -( [StackBlitz](https://stackblitz.com/edit/typescript-4g4cys?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/vafogoluye/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-4g4cys?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/vafogoluye/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/tkz0fuy2/) ) ```js @@ -33,7 +45,9 @@ const subscribe = example.subscribe(val => console.log(`Even number: ${val}`)); ##### Example 2: filter objects based on property -( [StackBlitz](https://stackblitz.com/edit/typescript-n73fsn?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/qihagaxuso/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-n73fsn?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/qihagaxuso/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/yjdsoug1/) ) ```js @@ -42,7 +56,10 @@ import { from } from 'rxjs'; import { filter } from 'rxjs/operators'; //emit ({name: 'Joe', age: 31}, {name: 'Bob', age:25}) -const source = from([{ name: 'Joe', age: 31 }, { name: 'Bob', age: 25 }]); +const source = from([ + { name: 'Joe', age: 31 }, + { name: 'Bob', age: 25 } +]); //filter out people with age under 30 const example = source.pipe(filter(person => person.age >= 30)); //output: "Over 30: Joe" @@ -51,7 +68,9 @@ const subscribe = example.subscribe(val => console.log(`Over 30: ${val.name}`)); ##### Example 3: filter for number greater than specified value -( [StackBlitz](https://stackblitz.com/edit/typescript-eyvvfu?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/rakabaheyu/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-eyvvfu?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/rakabaheyu/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/g1tgreha/) ) ```js @@ -76,19 +95,22 @@ const subscribe = example.subscribe(val => ### Related Recipes -* [HTTP Polling](../../recipes/http-polling.md) -* [Game Loop](../../recipes/gameloop.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [HTTP Polling](../../recipes/http-polling.md) +- [Game Loop](../../recipes/gameloop.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Save Indicator](../../recipes/save-indicator.md) ### Additional Resources -* [filter](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-filter) - :newspaper: - Official docs -* [Adding conditional logic with filter](https://egghead.io/lessons/rxjs-adding-conditional-logic-with-filter?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist -* [Filtering operator: filter](https://egghead.io/lessons/rxjs-filtering-operator-filter?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [filter](https://rxjs.dev/api/operators/filter) 📰 - Official docs +- [Adding conditional logic with filter](https://egghead.io/lessons/rxjs-adding-conditional-logic-with-filter?course=step-by-step-async-javascript-with-rxjs) + 🎥 💵 - John Linquist +- [Filtering operator: filter](https://egghead.io/lessons/rxjs-filtering-operator-filter?course=rxjs-beyond-the-basics-operators-in-depth) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/filter.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/filter.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/filter.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/filter.ts) diff --git a/operators/filtering/find.md b/operators/filtering/find.md new file mode 100644 index 00000000..8cb18f32 --- /dev/null +++ b/operators/filtering/find.md @@ -0,0 +1,53 @@ +# find + +#### signature: `find(predicate: function)` + +## Emit the first item that passes predicate then complete. + +--- + +💡 If you always want the first item emitted, regardless of condition, try +[`first()`](first.md)! + +--- + + + +### Examples + +##### Example 1: Find click inside box, repeat when a click occurs outside of box + +( [StackBlitz](https://stackblitz.com/edit/rxjs-hd63we?file=index.ts)) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { find, repeatWhen, mapTo, startWith, filter } from 'rxjs/operators'; + +// elem ref +const status = document.getElementById('status'); + +// streams +const clicks$ = fromEvent(document, 'click'); + +clicks$ + .pipe( + find((event: any) => event.target.id === 'box'), + mapTo('Found!'), + startWith('Find me!'), + // reset when click outside box + repeatWhen(() => + clicks$.pipe(filter((event: any) => event.target.id !== 'box')) + ) + ) + .subscribe(message => (status.innerHTML = message)); +``` + +### Additional Resources + +- [find](https://rxjs.dev/api/operators/find) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/find.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/find.ts) diff --git a/operators/filtering/first.md b/operators/filtering/first.md index 7a1130c4..0da4effb 100644 --- a/operators/filtering/first.md +++ b/operators/filtering/first.md @@ -6,11 +6,15 @@ --- -:bulb: The counterpart to first is [**last**](last.md)! +💡 The counterpart to first is [**last**](last.md)! + +💡 `First` will deliver an EmptyError to the Observer's error callback if the +Observable completes before any next notification was sent. If you don't want +this behavior, use `take(1)` instead. --- -
+ ### Examples @@ -79,12 +83,11 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [first](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-first) - :newspaper: - Official docs +- [first](https://rxjs.dev/api/operators/first) 📰 - Official docs - [Filtering operator: take, first, skip](https://egghead.io/lessons/rxjs-filtering-operators-take-first-skip?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/first.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/first.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/first.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/first.ts) diff --git a/operators/filtering/ignoreelements.md b/operators/filtering/ignoreelements.md index 5cd30bdc..5d3efd3e 100644 --- a/operators/filtering/ignoreelements.md +++ b/operators/filtering/ignoreelements.md @@ -4,16 +4,14 @@ ## Ignore everything but complete and error. -
+ ### Examples ##### Example 1: Ignore all elements from source ( -[StackBlitz](https://stackblitz.com/edit/typescript-jpjcpg?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/yiyefelubi/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/59scjqss/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-w6rndwxh?file=index.ts)) ```js // RxJS v6+ @@ -23,10 +21,7 @@ import { take, ignoreElements } from 'rxjs/operators'; //emit value every 100ms const source = interval(100); //ignore everything but complete -const example = source.pipe( - take(5), - ignoreElements() -); +const example = source.pipe(take(5), ignoreElements()); //output: "COMPLETE!" const subscribe = example.subscribe( val => console.log(`NEXT: ${val}`), @@ -38,9 +33,7 @@ const subscribe = example.subscribe( ##### Example 2: Only displaying error ( -[StackBlitz](https://stackblitz.com/edit/typescript-3yxv9z?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/gogonawuze/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/srcwdgw6/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-3yxv9z?file=index.ts&devtoolsheight=100)) ```js // RxJS v6+ @@ -69,10 +62,9 @@ const subscribe = error.subscribe( ### Additional Resources -- [ignoreElements](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-ignoreElements) - :newspaper: - Official docs +- [ignoreElements](https://rxjs.dev/api/operators/ignoreElements) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/ignoreElements.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/ignoreElements.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/ignoreElements.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/ignoreElements.ts) diff --git a/operators/filtering/last.md b/operators/filtering/last.md index 0b4e104a..c57e043f 100644 --- a/operators/filtering/last.md +++ b/operators/filtering/last.md @@ -6,11 +6,11 @@ --- -:bulb: The counterpart to last is [**first**](first.md)! +💡 The counterpart to last is [**first**](first.md)! --- -
+ ### Examples @@ -24,7 +24,7 @@ ```js // RxJS v6+ import { from } from 'rxjs'; -import { last } 'rxjs/operators'; +import { last } from 'rxjs/operators'; const source = from([1, 2, 3, 4, 5]); //no arguments, emit last value @@ -43,7 +43,7 @@ const subscribe = example.subscribe(val => console.log(`Last value: ${val}`)); ```js // RxJS v6+ import { from } from 'rxjs'; -import { last } 'rxjs/operators'; +import { last } from 'rxjs/operators'; const source = from([1, 2, 3, 4, 5]); //emit last even number @@ -64,7 +64,7 @@ const subscribeTwo = exampleTwo.subscribe(val => ```js // RxJS v6+ import { from } from 'rxjs'; -import { last } 'rxjs/operators'; +import { last } from 'rxjs/operators'; const source = from([1, 2, 3, 4, 5]); //no values will pass given predicate, emit default @@ -75,12 +75,11 @@ const subscribeTwo = exampleTwo.subscribe(val => console.log(val)); ### Additional Resources -- [last](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-last) - :newspaper: - Official docs +- [last](https://rxjs.dev/api/operators/last) 📰 - Official docs - [Filtering operator: takeLast, last](https://egghead.io/lessons/rxjs-filtering-operators-takelast-last?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/last.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/last.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/last.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/last.ts) diff --git a/operators/filtering/sample.md b/operators/filtering/sample.md index c030207b..9a97f037 100644 --- a/operators/filtering/sample.md +++ b/operators/filtering/sample.md @@ -4,7 +4,7 @@ ## Sample from source when provided observable emits. -
+ ### Examples @@ -18,7 +18,7 @@ ```js // RxJS v6+ import { interval } from 'rxjs'; -import { sample } 'rxjs/operators'; +import { sample } from 'rxjs/operators'; //emit value every 1s const source = interval(1000); @@ -79,10 +79,9 @@ const listener = merge( ### Additional Resources -- [sample](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-sample) - :newspaper: - Official docs +- [sample](https://rxjs.dev/api/operators/sample) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/sample.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/sample.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/sample.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/sample.ts) diff --git a/operators/filtering/single.md b/operators/filtering/single.md index 17417c2c..29ccfa28 100644 --- a/operators/filtering/single.md +++ b/operators/filtering/single.md @@ -4,7 +4,7 @@ ## Emit single item that passes expression. -
+ ### Examples @@ -30,10 +30,9 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [single](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-single) - :newspaper: - Official docs +- [single](https://rxjs.dev/api/operators/single) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/single.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/single.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/single.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/single.ts) diff --git a/operators/filtering/skip.md b/operators/filtering/skip.md index 353e712c..b7b28326 100644 --- a/operators/filtering/skip.md +++ b/operators/filtering/skip.md @@ -16,16 +16,14 @@ emissions. You could mimic `skip` by using [`filter`](./filter.md) with indexes. Ex. `.filter((val, index) => index > 1)` -
+ ### Examples ##### Example 1: Skipping values before emission ( -[StackBlitz](https://stackblitz.com/edit/typescript-o5ydjf?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/hacepudabi/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/ar1eqbya/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-o5ydjf?file=index.ts&devtoolsheight=100)) ```js // RxJS v6+ @@ -67,12 +65,11 @@ const filterObs = numArrayObs ### Additional Resources -- [skip](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-skip) - :newspaper: - Official docs +- [skip](https://rxjs.dev/api/operators/skip) 📰 - Official docs - [Filtering operator: take, first, skip](https://egghead.io/lessons/rxjs-filtering-operators-take-first-skip?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/skip.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/skip.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/skip.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/skip.ts) diff --git a/operators/filtering/skipuntil.md b/operators/filtering/skipuntil.md index ffa427b0..7e5589ef 100644 --- a/operators/filtering/skipuntil.md +++ b/operators/filtering/skipuntil.md @@ -4,6 +4,8 @@ ## Skip emitted values from source until provided observable emits. + + ### Examples ##### Example 1: Skip until observable emits @@ -28,10 +30,9 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [skipUntil](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-skipUntil) - :newspaper: - Official docs +- [skipUntil](https://rxjs.dev/api/operators/skipUntil) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/skipUntil.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/skipUntil.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/skipUntil.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/skipUntil.ts) diff --git a/operators/filtering/skipwhile.md b/operators/filtering/skipwhile.md index 6335f52a..53c6eb31 100644 --- a/operators/filtering/skipwhile.md +++ b/operators/filtering/skipwhile.md @@ -4,7 +4,7 @@ ## Skip emitted values from source until provided expression is false. -
+ ### Examples @@ -30,10 +30,9 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [skipWhile](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-skipWhile) - :newspaper: - Official docs +- [skipWhile](https://rxjs.dev/api/operators/skipWhile) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/skipWhile.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/skipWhile.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/skipWhile.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/skipWhile.ts) diff --git a/operators/filtering/take.md b/operators/filtering/take.md index 732e7b88..77195920 100644 --- a/operators/filtering/take.md +++ b/operators/filtering/take.md @@ -4,28 +4,29 @@ ## Emit provided number of values before completing. -### Why use `take` +### Why use `take`? -When you are interested in only the first set number of emission, you want to -use `take`. Maybe you want to see what the user first clicked on when he/she -first entered the page, you would want to subscribe to the click event and just -take the first emission. There is a race and you want to observe the race, but -you're only interested in the first who crosses the finish line. This operator -is clear and straight forward, you just want to see the first _n_ numbers of -emission to do whatever it is you need. +When you are interested in only the first emission, you want to use `take`. +Maybe you want to see what the user first clicked on when they entered the page, +or you would want to subscribe to the click event and just take the first +emission. Another use-case is when you need to take a snapshot of data at a +particular point in time but do not require further emissions. For example, a +stream of user token updates, or a route guard based on a stream in an Angular +application. --- -:bulb: If you want to take a variable number of values based on some logic, or +💡 If you want to take a variable number of values based on some logic, or another observable, you can use [takeUntil](takeuntil.md) or [takeWhile](takewhile.md)! -:bulb: `take` is the opposite of `skip` where `take` will take the first _n_ -number of emissions while `skip` will skip the first _n_ number of emissions. +💡 `take` is the opposite of [`skip`](./skip.md) where `take` will take the +first _n_ number of emissions while `skip` will skip the first _n_ number of +emissions. --- -
+ ### Examples @@ -97,14 +98,18 @@ const oneClickEvent = fromEvent(document, 'click').pipe( const subscribe = oneClickEvent.subscribe(); ``` +### Related Recipes + +- [Battleship Game](../../recipes/battleship-game.md) +- [Memory Game](../../recipes/memory-game.md) + ### Additional Resources -- [take](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-take) - :newspaper: - Official docs +- [take](https://rxjs.dev/api/operators/take) 📰 - Official docs - [Filtering operator: take, first, skip](https://egghead.io/lessons/rxjs-filtering-operators-take-first-skip?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/take.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/take.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/take.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/take.ts) diff --git a/operators/filtering/takelast.md b/operators/filtering/takelast.md new file mode 100644 index 00000000..5f302cb6 --- /dev/null +++ b/operators/filtering/takelast.md @@ -0,0 +1,44 @@ +# takeLast + +#### signature: `takeLast(count: number): Observable` + +## Emit the last n emitted values before completion + +--- + +💡 If you want only the last emission from multiple observables, on completion +of multiple observables, try [forkJoin](../combination/forkjoin.md)! + +--- + + + +### Examples + +##### Example 1: take the last 2 emitted values before completion + +( +[StackBlitz](https://stackblitz.com/edit/typescript-zss7oo?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { of } from 'rxjs'; +import { takeLast } from 'rxjs/operators'; + +const source = of('Ignore', 'Ignore', 'Hello', 'World!'); +// take the last 2 emitted values +const example = source.pipe(takeLast(2)); +// Hello, World! +const subscribe = example.subscribe(val => console.log(val)); +``` + +### Additional Resources + +- [takeLast](https://rxjs-dev.firebaseapp.com/api/operators/takeLast) 📰 - + Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/takeLast.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/takeLast.ts) diff --git a/operators/filtering/takeuntil.md b/operators/filtering/takeuntil.md index 285f1b0a..d8189859 100644 --- a/operators/filtering/takeuntil.md +++ b/operators/filtering/takeuntil.md @@ -6,11 +6,34 @@ --- -:bulb: If you only need a specific number of values, try [take](take.md)! +💡 If you only need a specific number of values, try [take](take.md)! --- -
+### Why use `takeUntil`? + +Consider a day at your workplace: You're anticipating an important email, but you've decided that once the clock hits 5 pm, you're clocking out, regardless of whether you've received that email or not. In this RxJS analogy, the anticipation of the email is one observable, while the 5 pm clock-out time is another. The `takeUntil` operator ensures you're alert for the email's potential arrival, but the moment 5 pm arrives, you stop checking (unsubscribe). + +In real-world applications, think of a scenario where you're monitoring server responses on a dashboard. However, you want this monitoring to cease once a specific "Stop Monitoring" button is clicked. That's where `takeUntil` shines. + +In the context of Angular, `takeUntil` is particularly handy for auto-unsubscribing from observables when a component is destroyed. This is achieved by leveraging the `ngOnDestroy` lifecycle hook. You'd typically create a `Subject`, often named `destroy$`, and use it with `takeUntil`: + +```typescript +private destroy$ = new Subject(); + +observable$ + .pipe(takeUntil(this.destroy$)) + .subscribe(data => console.log(data)); + +ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); +} +``` + +With this setup, as soon as the `ngOnDestroy` method is called (when the component is about to be destroyed), the observables using `takeUntil` with the `destroy$` subject will automatically unsubscribe, ensuring that no unwanted memory leaks or unexpected behavior occurs. + + ### Examples @@ -76,17 +99,54 @@ const example = evenSource.pipe( const subscribe = example.subscribe(val => console.log(val)); ``` -### Additional Resources +##### Example 3: Take mouse events on mouse down until mouse up -- [takeUntil](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-takeUntil) - :newspaper: - Official docs +( +[StackBlitz](https://stackblitz.com/edit/rxjs-ug2ezf?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { takeUntil, mergeMap, map } from 'rxjs/operators'; + +const mousedown$ = fromEvent(document, 'mousedown'); +const mouseup$ = fromEvent(document, 'mouseup'); +const mousemove$ = fromEvent(document, 'mousemove'); + +// after mousedown, take position until mouse up +mousedown$ + .pipe( + mergeMap(_ => { + return mousemove$.pipe( + map((e: any) => ({ + x: e.clientX, + y: e.clientY + })), + // complete inner observable on mouseup event + takeUntil(mouseup$) + ); + }) + ) + .subscribe(console.log); +``` +### Related Recipes + +- [Lockscreen](../../recipes/lockscreen.md) +- [Space Invaders Game](/recipes/space-invaders-game.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) + +### Additional Resources + +- [takeUntil](https://rxjs.dev/api/operators/takeUntil) 📰 - Official docs * [Avoiding takeUntil leaks](https://blog.angularindepth.com/rxjs-avoiding-takeuntil-leaks-fb5182d047ef) - Angular in Depth * [Stopping a stream with takeUntil](https://egghead.io/lessons/rxjs-stopping-a-stream-with-takeuntil?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/takeUntil.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/takeUntil.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/takeUntil.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/takeUntil.ts) diff --git a/operators/filtering/takewhile.md b/operators/filtering/takewhile.md index 9d323d28..96f5a5f2 100644 --- a/operators/filtering/takewhile.md +++ b/operators/filtering/takewhile.md @@ -1,10 +1,17 @@ # takeWhile -#### signature: `takeWhile(predicate: function(value, index): boolean): Observable` +#### signature: `takeWhile(predicate: function(value, index): boolean, inclusive?: boolean): Observable` ## Emit values until provided expression is false. -
+--- + +💡 When the optional `inclusive` parameter is set to `true` it will also emit +the first item that didn't pass the predicate. + +--- + + ### Examples @@ -21,14 +28,36 @@ import { of } from 'rxjs'; import { takeWhile } from 'rxjs/operators'; //emit 1,2,3,4,5 -const source = of(1, 2, 3, 4, 5); +const source$ = of(1, 2, 3, 4, 5); + //allow values until value from source is greater than 4, then complete -const example = source.pipe(takeWhile(val => val <= 4)); -//output: 1,2,3,4 -const subscribe = example.subscribe(val => console.log(val)); +source$ + .pipe(takeWhile(val => val <= 4)) + // log: 1,2,3,4 + .subscribe(val => console.log(val)); +``` + +##### Example 2: (v6.4+) takeWhile with inclusive flag + +( +[StackBlitz](https://stackblitz.com/edit/typescript-3bwfup?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6.4+ +import { of } from 'rxjs'; +import { takeWhile, filter } from 'rxjs/operators'; + +const source$ = of(1, 2, 3, 9); + +source$ + // with inclusive flag, the value causing the predicate to return false will also be emitted + .pipe(takeWhile(val => val <= 3, true)) + // log: 1, 2, 3, 9 + .subscribe(console.log); ``` -##### Example 2: Difference between takeWhile() and filter() +##### Example 3: Difference between `takeWhile` and [`filter`](filter.md) ( [StackBlitz](https://stackblitz.com/edit/typescript-roozza?file=index.ts&devtoolsheight=100) @@ -41,32 +70,44 @@ import { of } from 'rxjs'; import { takeWhile, filter } from 'rxjs/operators'; // emit 3, 3, 3, 9, 1, 4, 5, 8, 96, 3, 66, 3, 3, 3 -const source = of(3, 3, 3, 9, 1, 4, 5, 8, 96, 3, 66, 3, 3, 3); +const source$ = of(3, 3, 3, 9, 1, 4, 5, 8, 96, 3, 66, 3, 3, 3); // allow values until value from source equals 3, then complete -// output: [3, 3, 3] -source +source$ .pipe(takeWhile(it => it === 3)) + // log: 3, 3, 3 .subscribe(val => console.log('takeWhile', val)); -// output: [3, 3, 3, 3, 3, 3, 3] -source +source$ .pipe(filter(it => it === 3)) + // log: 3, 3, 3, 3, 3, 3, 3 .subscribe(val => console.log('filter', val)); ``` ### Related Recipes +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Catch The Dot Game](../../recipes/catch-the-dot-game.md) +- [Click Ninja Game](../../recipes/click-ninja-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) - [Smart Counter](../../recipes/smartcounter.md) +- [Swipe To Refresh](../../recipes/swipe-to-refresh.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) ### Additional Resources -- [takeWhile](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-takeWhile) - :newspaper: - Official docs +- [takeWhile](https://rxjs-dev.firebaseapp.com/api/operators/takeWhile) 📰 - + Official docs - [Completing a stream with takeWhile](https://egghead.io/lessons/rxjs-completing-a-stream-with-takewhile?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/takeWhile.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/takeWhile.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/takeWhile.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/takeWhile.ts) diff --git a/operators/filtering/throttle.md b/operators/filtering/throttle.md index 2112354c..63d0d4da 100644 --- a/operators/filtering/throttle.md +++ b/operators/filtering/throttle.md @@ -4,7 +4,7 @@ ## Emit value on the leading edge of an interval, but suppress new values until `durationSelector` has completed. -
+ ### Examples @@ -58,12 +58,11 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -- [throttle](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-throttle) - :newspaper: - Official docs +- [throttle](https://rxjs.dev/api/operators/throttle) 📰 - Official docs - [Filtering operator: throttle and throttleTime](https://egghead.io/lessons/rxjs-filtering-operators-throttle-and-throttletime?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/throttle.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/throttle.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/throttle.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/throttle.ts) diff --git a/operators/filtering/throttletime.md b/operators/filtering/throttletime.md index e5d09c61..08e6cff6 100644 --- a/operators/filtering/throttletime.md +++ b/operators/filtering/throttletime.md @@ -1,68 +1,69 @@ # throttleTime -#### signature: `throttleTime(duration: number, scheduler: Scheduler): Observable` +#### signature: `throttleTime(duration: number, scheduler: Scheduler, config: ThrottleConfig): Observable` + +## Emit first value then ignore for specified duration -## Emit latest value when specified duration has passed. -
### Examples -##### Example 1: Receive latest value every 5 seconds +##### Example 1: Emit first value, ignore for 5s window ( [StackBlitz](https://stackblitz.com/edit/typescript-en2zqe?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/koqujayizo/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/4zysLc3y/) ) +) ```js // RxJS v6+ import { interval } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; -//emit value every 1 second +// emit value every 1 second const source = interval(1000); /* - throttle for five seconds - last value emitted before throttle ends will be emitted from source + emit the first value, then ignore for 5 seconds. repeat... */ const example = source.pipe(throttleTime(5000)); -//output: 0...6...12 +// output: 0...6...12 const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 2: Throttle merged observable +##### Example 2: Emit on trailing edge using config ( -[StackBlitz](https://stackblitz.com/edit/typescript-bkcjfj?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/takipadaza/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/xhd1zy3m/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-5rwl6i?file=index.ts&devtoolsheight=100) +) ```js // RxJS v6+ -import { interval, merge } from 'rxjs'; -import { throttleTime, ignoreElements } from 'rxjs/operators'; - -const source = merge( - //emit every .75 seconds - interval(750), - //emit every 1 second - interval(1000) +import { interval, asyncScheduler } from 'rxjs'; +import { throttleTime } from 'rxjs/operators'; + +const source = interval(1000); +/* + emit the first value, then ignore for 5 seconds. repeat... +*/ +const example = source.pipe( + throttleTime(5000, asyncScheduler, { trailing: true }) ); -//throttle in middle of emitted values -const example = source.pipe(throttleTime(1200)); -//output: 0...1...4...4...8...7 +// output: 5...11...17 const subscribe = example.subscribe(val => console.log(val)); ``` +### Related Recipes + +- [Horizontal Scroll Indicator](../../recipes/horizontal-scroll-indicator.md) +- [Lockscreen](../../recipes/lockscreen.md) + ### Additional Resources -- [throttleTime](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-throttleTime) - :newspaper: - Official docs +- [throttleTime](https://rxjs.dev/api/operators/throttleTime) 📰 - Official docs - [Filtering operator: throttle and throttleTime](https://egghead.io/lessons/rxjs-filtering-operators-throttle-and-throttletime?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz +- [Time based operators comparison](../../concepts/time-based-operators-comparison.md) --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/throttleTime.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/throttleTime.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/throttleTime.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/throttleTime.ts) diff --git a/operators/multicasting/README.md b/operators/multicasting/README.md index 3f079d61..9bdb2ae8 100644 --- a/operators/multicasting/README.md +++ b/operators/multicasting/README.md @@ -6,18 +6,18 @@ subscribers. ## Contents -* [publish](publish.md) -* [multicast](multicast.md) -* [share](share.md) :star: -* [shareReplay](sharereplay.md) :star: +- [publish](publish.md) +- [multicast](multicast.md) +- [share](share.md) ⭐ +- [shareReplay](sharereplay.md) ⭐ -:star: - _commonly used_ +⭐ - _commonly used_ ### Additional Resources -* [Hot vs Cold Observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339#.8x9uam5rg) - :newspaper: - Ben Lesh -* [Unicast v Multicast](https://github.com/zenparsing/es-observable/issues/66) - :newspaper: - GitHub Discussion -* [Demystifying Hot and Cold Observables](https://egghead.io/lessons/rxjs-demystifying-cold-and-hot-observables-in-rxjs) - :video_camera: - André Staltz +- [Hot vs Cold Observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339#.8x9uam5rg) + 📰 - Ben Lesh +- [Unicast v Multicast](https://github.com/zenparsing/es-observable/issues/66) + 📰 - GitHub Discussion +- [Demystifying Hot and Cold Observables](https://egghead.io/lessons/rxjs-demystifying-cold-and-hot-observables-in-rxjs) + 🎥 - André Staltz diff --git a/operators/multicasting/multicast.md b/operators/multicasting/multicast.md index d634bda7..79191613 100644 --- a/operators/multicasting/multicast.md +++ b/operators/multicasting/multicast.md @@ -4,7 +4,7 @@ ## Share source utilizing the provided Subject. -
+ ### Examples @@ -82,10 +82,9 @@ setTimeout(() => { ### Additional Resources -- [multicast](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-multicast) - :newspaper: - Official docs +- [multicast](https://rxjs.dev/api/operators/multicast) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/multicast.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/multicast.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/multicast.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/multicast.ts) diff --git a/operators/multicasting/publish.md b/operators/multicasting/publish.md index 06562f32..a1917445 100644 --- a/operators/multicasting/publish.md +++ b/operators/multicasting/publish.md @@ -4,7 +4,7 @@ ## Share source and make hot by calling connect. -
+ ### Examples @@ -22,12 +22,11 @@ import { publish, tap } from 'rxjs/operators'; //emit value every 1 second const source = interval(1000); -const example = source.pipe( +//do nothing until connect() is called +const example = publish()(source.pipe( //side effects will be executed once tap(_ => console.log('Do Something!')), - //do nothing until connect() is called - publish() -); +)); /* source will not emit values until connect() is called @@ -50,12 +49,12 @@ const subscribeTwo = example.subscribe(val => setTimeout(() => { example.connect(); }, 5000); + ``` ### Additional Resources -- [publish](http://reactivex-rxjs5.surge.sh/function/index.html#static-function-publish) - :newspaper: - Official docs +- [publish](https://rxjs.dev/api/operators/publish) 📰 - Official docs -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/publish.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/publish.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/publish.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/publish.ts) diff --git a/operators/multicasting/share.md b/operators/multicasting/share.md index fc343151..7eac07b3 100644 --- a/operators/multicasting/share.md +++ b/operators/multicasting/share.md @@ -6,11 +6,11 @@ --- -:bulb: share is like [multicast](multicast.md) with a Subject and refCount! +💡 share is like [multicast](multicast.md) with a Subject and refCount! --- -
+ ### Examples @@ -62,15 +62,15 @@ const subscribeFour = sharedExample.subscribe(val => console.log(val)); - [Progress Bar](../../recipes/progressbar.md) - [Game Loop](../../recipes/gameloop.md) +- [Save Indicator](../../recipes/save-indicator.md) ### Additional Resources -- [share](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-share) - :newspaper: - Official docs +- [share](https://rxjs.dev/api/operators/share) 📰 - Official docs - [Sharing streams with share](https://egghead.io/lessons/rxjs-sharing-streams-with-share?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/share.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/share.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/share.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/share.ts) diff --git a/operators/multicasting/sharereplay.md b/operators/multicasting/sharereplay.md index 8194eaee..c6ee8332 100644 --- a/operators/multicasting/sharereplay.md +++ b/operators/multicasting/sharereplay.md @@ -96,7 +96,7 @@ being used. When a subscription is made, `shareReplay` will subscribe to the source, sending values through an internal `ReplaySubject`: ( -[source](https://github.com/ReactiveX/rxjs/blob/b25db9f369b07f26cf2fc11714ec1990b78a4536/src/internal/operators/shareReplay.ts#L26-L37) +[source](https://github.com/ReactiveX/rxjs/blob/b25db9f369b07f26cf2fc11714ec1990b78a4536/packages/rxjs/src/internal/operators/shareReplay.ts#L26-L37) ) ```js @@ -133,7 +133,7 @@ source, sending values through an internal `ReplaySubject`: } ``` -
+ ### Examples @@ -168,10 +168,9 @@ const lateSubscriber = lastUrl.subscribe(console.log); ### Additional Resources -- [shareReplay](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-shareReplay) - :newspaper: - Official docs +- [shareReplay](https://rxjs.dev/api/operators/shareReplay) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/shareReplay.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/shareReplay.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/shareReplay.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/shareReplay.ts) diff --git a/operators/transformation/README.md b/operators/transformation/README.md index ca22e753..33e6121b 100644 --- a/operators/transformation/README.md +++ b/operators/transformation/README.md @@ -4,30 +4,40 @@ Transforming values as they pass through the operator chain is a common task. These operators provide transformation techniques for nearly any use-case you will encounter. +{% hint style="info" %} + +New to transformation operators? Check out the article +[Get started transforming streams with map, pluck, and mapTo](../../concepts/get-started-transforming.md)! + +{% endhint %} + ## Contents -* [buffer](buffer.md) -* [bufferCount](buffercount.md) -* [bufferTime](buffertime.md) :star: -* [bufferToggle](buffertoggle.md) -* [bufferWhen](bufferwhen.md) -* [concatMap](concatmap.md) :star: -* [concatMapTo](concatmapto.md) -* [exhaustMap](exhaustmap.md) -* [expand](expand.md) -* [groupBy](groupby.md) -* [map](map.md) :star: -* [mapTo](mapto.md) -* [mergeMap / flatMap](mergemap.md) :star: -* [partition](partition.md) -* [pluck](pluck.md) -* [reduce](reduce.md) -* [scan](scan.md) :star: -* [switchMap](switchmap.md) :star: -* [window](window.md) -* [windowCount](windowcount.md) -* [windowTime](windowtime.md) -* [windowToggle](windowtoggle.md) -* [windowWhen](windowwhen.md) +- [buffer](buffer.md) +- [bufferCount](buffercount.md) +- [bufferTime](buffertime.md) ⭐ +- [bufferToggle](buffertoggle.md) +- [bufferWhen](bufferwhen.md) +- [concatMap](concatmap.md) ⭐ +- [concatMapTo](concatmapto.md) +- [exhaustMap](exhaustmap.md) +- [expand](expand.md) +- [groupBy](groupby.md) +- [map](map.md) ⭐ +- [mapTo](mapto.md) +- [mergeMap / flatMap](mergemap.md) ⭐ +- [mergeScan](mergescan.md) +- [partition](partition.md) +- [pluck](pluck.md) +- [reduce](reduce.md) +- [scan](scan.md) ⭐ +- [switchMap](switchmap.md) ⭐ +- [switchMapTo](switchmapto.md) +- [toArray](toarray.md) +- [window](window.md) +- [windowCount](windowcount.md) +- [windowTime](windowtime.md) +- [windowToggle](windowtoggle.md) +- [windowWhen](windowwhen.md) -:star: - _commonly used_ +⭐ - _commonly used_ diff --git a/operators/transformation/buffer.md b/operators/transformation/buffer.md index 240fbd82..d997c3c2 100644 --- a/operators/transformation/buffer.md +++ b/operators/transformation/buffer.md @@ -4,14 +4,47 @@ ## Collect output values until provided observable emits, emit as array. -
+### Why use `buffer`? +The buffer operator in RxJS stands out for its ability to accumulate emitted values into an array until a specified notifier emits. Think of it as a "collect and release" mechanism. This aligns well with use cases where you want to group events based on a certain condition, such as time or user actions. + +Buffer shines in scenarios like batching multiple events together before processing, which can optimize performance and reduce workload. For example, when monitoring clicks on a web page, buffer can gather a series of clicks before sending them for analytics, rather than doing so individually. + +However, keep in mind that buffer may not be the best fit when you require immediate processing of each emitted value. One way to think about this is "buffer is about batching". Remember, buffer is all about accumulating and releasing values based on specific triggers, making it a powerful tool for managing grouped events. + + ### Examples -##### Example 1: Buffer until document click +##### Example 1: Using buffer to recognize double clicks + +( +[StackBlitz](https://stackblitz.com/edit/typescript-x5zyn5?file=index.ts&devtoolsheight=50)) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { buffer, filter, throttleTime } from 'rxjs/operators'; + +// streams +const clicks$ = fromEvent(document, 'click'); + +/* +Collect clicks that occur, after 250ms emit array of clicks +*/ +clicks$ + .pipe( + buffer(clicks$.pipe(throttleTime(250))), + // if array is greater than 1, double click occured + filter(clickArray => clickArray.length > 1) + ) + .subscribe(() => console.log('Double Click!')); +``` + +##### Example 2: Buffer until document click -( [StackBlitz](https://stackblitz.com/edit/typescript-nwp2cl?file=index.ts&devtoolsheight=50) | -[jsBin](http://jsbin.com/fazimarajo/edit?js,console,output) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-nwp2cl?file=index.ts&devtoolsheight=50) +| [jsBin](http://jsbin.com/fazimarajo/edit?js,console,output) | [jsFiddle](https://jsfiddle.net/btroncone/7451s67k/) ) ```js @@ -36,16 +69,16 @@ const subscribe = myBufferedInterval.subscribe(val => ### Related Recipes -* [Game Loop](../../recipes/gameloop.md) +- [Game Loop](../../recipes/gameloop.md) ### Additional Resources -* [buffer](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-buffer) - :newspaper: - Official docs -* [Transformation operator: buffer](https://egghead.io/lessons/rxjs-transformation-operator-buffer?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [buffer](https://rxjs-dev.firebaseapp.com/api/operators/buffer) 📰 - Official + docs +- [Transformation operator: buffer](https://egghead.io/lessons/rxjs-transformation-operator-buffer?course=rxjs-beyond-the-basics-operators-in-depth) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/buffer.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/buffer.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/buffer.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/buffer.ts) diff --git a/operators/transformation/buffercount.md b/operators/transformation/buffercount.md index f92536f6..a28ad2cd 100644 --- a/operators/transformation/buffercount.md +++ b/operators/transformation/buffercount.md @@ -4,14 +4,24 @@ ## Collect emitted values until provided number is fulfilled, emit as array. -
+### Why use `bufferCount`? +The key distinction between bufferCount and other buffering operators lies in its count-based buffering approach. Upon reaching the specified count of emissions, bufferCount groups and emits the values as an array. Think of it as collecting items in groups of the specified count. + +This operator proves advantageous in scenarios where processing data in chunks is more efficient, such as bulk updates or batch processing. By contrast, the buffer operator relies on a closing notifier to define the buffering window, which may not suit all use cases. + +Keep in mind, though, that bufferCount may not be the best choice when the buffering strategy requires time-based or event-driven windows. In such instances, consider using [buffer](buffer.md) or [buffertime](bufferTime.md) instead. Remember, bufferCount organizes values based on emission count, as illustrated clearly in the first example. + +Exercise caution in situations where buffering strategy plays a critical role in the desired output, as choosing the wrong operator might lead to unexpected behavior. Familiarize yourself with the various buffering operators to make informed decisions based on your specific requirements. + + ### Examples ##### Example 1: Collect buffer and emit after specified number of values -( [StackBlitz](https://stackblitz.com/edit/typescript-osryhu?file=index.ts&devtoolsheight=50) | -[jsBin](http://jsbin.com/suveqaromu/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-osryhu?file=index.ts&devtoolsheight=50) +| [jsBin](http://jsbin.com/suveqaromu/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/ky9myc5b/) ) ```js @@ -32,8 +42,9 @@ const subscribe = bufferThree.subscribe(val => ##### Example 2: Overlapping buffers -( [StackBlitz](https://stackblitz.com/edit/typescript-vvccar?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/kiloxiraya/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-vvccar?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/kiloxiraya/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/3c67qcz1/) ) ```js @@ -67,12 +78,39 @@ const subscribe = bufferEveryOne.subscribe(val => ); ``` +##### Example 3: Last n keyboard presses tracking + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-buffecount-keypresses-tracking?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent, of } from 'rxjs'; +import { bufferCount, map, mergeMap, tap } from 'rxjs/operators'; + +const fakeKeyPressesPost = keypresses => + of(201).pipe( + tap(_ => { + console.log(`received key presses are: ${keypresses}`); + document.getElementById('output').innerText = keypresses; + }) + ); + +fromEvent(document, 'keydown') + .pipe( + map((e: KeyboardEvent) => e.key), + bufferCount(5), + mergeMap(fakeKeyPressesPost) + ) + .subscribe(); +``` + ### Additional Resources -* [bufferCount](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-bufferCount) - :newspaper: - Official docs +- [bufferCount](https://rxjs.dev/api/operators/bufferCount) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferCount.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferCount.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferCount.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferCount.ts) diff --git a/operators/transformation/buffertime.md b/operators/transformation/buffertime.md index 63b4df46..8c9b8584 100644 --- a/operators/transformation/buffertime.md +++ b/operators/transformation/buffertime.md @@ -4,12 +4,18 @@ ## Collect emitted values until provided time has passed, emit as array. +### Why use `bufferTime`? +The key distinction between `bufferTime` and other buffering operators lies in its time-based buffering approach. `bufferTime` accumulates values from the source observable in an array over a specified time duration before emitting the buffered array. + +This operator is particularly well-suited for scenarios where you need to batch or throttle emissions from high-frequency observables, such as monitoring user interactions, tracking mouse movements, or dealing with rapidly updating data streams. `bufferTime` provides an efficient way to handle and process these emissions in a more manageable, time-based manner. + +Remember, `bufferTime` allows you to manage data emissions effectively by collecting and emitting them in time-based batches, as illustrated in the first example. Be mindful of its implications, though, and choose the right operator according to your specific use case. + ### Examples ##### Example 1: Buffer for 2 seconds -( [StackBlitz](https://stackblitz.com/edit/typescript-haqxd1?file=index.ts&devtoolsheight=50) | [jsBin](http://jsbin.com/bafakiyife/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/vx7vwg01/) ) +( [StackBlitz](https://stackblitz.com/edit/typescript-haqxd1?file=index.ts&devtoolsheight=50)) ```js // RxJS v6+ @@ -29,8 +35,7 @@ const subscribe = example.subscribe(val => ##### Example 2: Multiple active buffers -( [StackBlitz](https://stackblitz.com/edit/typescript-9blquz?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/tadiwiniri/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/7k4ygj1x/) ) +( [StackBlitz](https://stackblitz.com/edit/typescript-9blquz?file=index.ts&devtoolsheight=100)) ```js // RxJS v6+ @@ -53,10 +58,11 @@ const subscribe = example.subscribe(val => ### Additional Resources -* [bufferTime](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-bufferTime) - :newspaper: - Official docs +* [bufferTime](https://rxjs.dev/api/operators/bufferTime) + 📰 - Official docs +* [Time based operators comparison](../../concepts/time-based-operators-comparison.md) --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferTime.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferTime.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferTime.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferTime.ts) diff --git a/operators/transformation/buffertoggle.md b/operators/transformation/buffertoggle.md index fa896ec2..be5fc680 100644 --- a/operators/transformation/buffertoggle.md +++ b/operators/transformation/buffertoggle.md @@ -4,13 +4,15 @@ ## Toggle on to catch emitted values from source, toggle off to emit buffered values as array. -
+ ### Examples ##### Example 1: Toggle buffer on and off at interval -( [StackBlitz](https://stackblitz.com/edit/typescript-xu3sq8?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/relavezugo/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-xu3sq8?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/relavezugo/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/6ad3w3wf/) ) ```js @@ -29,10 +31,7 @@ const closingInterval = val => { }; //every 5s a new buffer will start, collecting emitted values for 3s then emitting buffered values const bufferToggleInterval = sourceInterval.pipe( - bufferToggle( - startInterval, - closingInterval - ) + bufferToggle(startInterval, closingInterval) ); //log to console //ex. emitted buffers [4,5,6]...[9,10,11] @@ -41,12 +40,30 @@ const subscribe = bufferToggleInterval.subscribe(val => ); ``` +##### Example 2: Toggle buffer on and off on mouse down/up + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-buffertoggle-mousemove?file=index.ts&devtoolsheight=50) +) + +```js +import { fromEvent } from 'rxjs'; +import { bufferToggle } from 'rxjs/operators'; + +fromEvent(document, 'mousemove') + .pipe( + bufferToggle(fromEvent(document, 'mousedown'), _ => + fromEvent(document, 'mouseup') + ) + ) + .subscribe(console.log); +``` + ### Additional Resources -* [bufferToggle](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-bufferToggle) - :newspaper: - Official docs +- [bufferToggle](https://rxjs.dev/api/operators/bufferToggle) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferToggle.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferToggle.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferToggle.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferToggle.ts) diff --git a/operators/transformation/bufferwhen.md b/operators/transformation/bufferwhen.md index 6f63f1f5..591fb085 100644 --- a/operators/transformation/bufferwhen.md +++ b/operators/transformation/bufferwhen.md @@ -4,13 +4,15 @@ ## Collect all values until closing selector emits, emit buffered values. -
+ ### Examples ##### Example 1: Emit buffer based on interval -( [StackBlitz](https://stackblitz.com/edit/typescript-f4a2fu?file=index.ts&devtoolsheight=10) | [jsBin](http://jsbin.com/vugerupube/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-f4a2fu?file=index.ts&devtoolsheight=10) +| [jsBin](http://jsbin.com/vugerupube/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/nr9agfuL/) ) ```js @@ -23,7 +25,9 @@ const oneSecondInterval = interval(1000); //return an observable that emits value every 5 seconds const fiveSecondInterval = () => interval(5000); //every five seconds, emit buffered values -const bufferWhenExample = oneSecondInterval.pipe(bufferWhen(fiveSecondInterval)); +const bufferWhenExample = oneSecondInterval.pipe( + bufferWhen(fiveSecondInterval) +); //log values //ex. output: [0,1,2,3]...[4,5,6,7,8] const subscribe = bufferWhenExample.subscribe(val => @@ -33,10 +37,9 @@ const subscribe = bufferWhenExample.subscribe(val => ### Additional Resources -* [bufferWhen](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-bufferWhen) - :newspaper: - Official docs +- [bufferWhen](https://rxjs.dev/api/operators/bufferWhen) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/bufferWhen.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/bufferWhen.ts) diff --git a/operators/transformation/concatmap.md b/operators/transformation/concatmap.md index 662c4e00..50f16f15 100644 --- a/operators/transformation/concatmap.md +++ b/operators/transformation/concatmap.md @@ -4,15 +4,27 @@ ## Map values to inner observable, subscribe and emit in order. -
+### Why use `concatMap`? + +This operator is your go-to when you have an observable that emits values and, for each of those values, you want to execute another observable sequence, ensuring they are processed in order and not concurrently. Think of it like waiting in line at a bakery: even if multiple customers arrive at once, they're served one by one. So, for instance, if you have a stream of user click events and for each click you want to initiate an HTTP request, but you need those requests to happen sequentially (one completing before the next begins), `concatMap` is what you need. + +A practical scenario might be ordering saves sequentialy, without overwhelming the server with simultaneous requests. + +Keep in mind that **if the inner observable takes a significant time to complete, it can lead to a backlog of outer values waiting to be processed**. In such cases, it may seem as if your application is lagging or stuck, when in reality, `concatMap` is diligently processing each value in order. For scenarios where you'd prefer to handle the most recent value and discard previous ones, consider using [`switchMap`](switchmap.md) instead. + +Lastly, if you're not concerned about the order of processing and just want everything to execute as it arrives, [`mergeMap`](mergemap.md) might be the better choice. + + ### Examples ##### Example 1: Demonstrating the difference between `concatMap` and [`mergeMap`](./mergemap.md) -( [StackBlitz](https://stackblitz.com/edit/typescript-pkyxa1?file=index.ts&devtoolsheight=100) ) +( +[StackBlitz](https://stackblitz.com/edit/typescript-pkyxa1?file=index.ts&devtoolsheight=100) +) -:bulb: Note the difference between `concatMap` and [`mergeMap`](./mergemap.md). +💡 Note the difference between `concatMap` and [`mergeMap`](./mergemap.md). Because `concatMap` does not subscribe to the next observable until the previous completes, the value from the source delayed by 2000ms will be emitted first. Contrast this with [`mergeMap`](./mergemap.md) which subscribes immediately to @@ -47,8 +59,9 @@ const mergeMapExample = source ##### Example 2: Map to promise -( [StackBlitz](https://stackblitz.com/edit/typescript-rv9byk?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/celixodeba/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-rv9byk?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/celixodeba/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/Lym33L97//) ) ```js @@ -70,8 +83,9 @@ const subscribe = example.subscribe(val => ##### Example 3: Supplying a projection function -( [StackBlitz](https://stackblitz.com/edit/typescript-2elzt7?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/vihacewozo/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-2elzt7?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/vihacewozo/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/5sr5zzgy/) ) ```js @@ -85,7 +99,10 @@ const source = of('Hello', 'Goodbye'); const examplePromise = val => new Promise(resolve => resolve(`${val} World!`)); //result of first param passed to second param selector function before being returned const example = source.pipe( - concatMap(val => examplePromise(val), result => `${result} w/ selector!`) + concatMap( + val => examplePromise(val), + result => `${result} w/ selector!` + ) ); //output: 'Example w/ Selector: 'Hello w/ Selector', Example w/ Selector: 'Goodbye w/ Selector' const subscribe = example.subscribe(val => @@ -95,12 +112,11 @@ const subscribe = example.subscribe(val => ### Additional Resources -* [concatMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-concatMap) - :newspaper: - Official docs -* [Use RxJS concatMap to map and concat higher order observables](https://egghead.io/lessons/rxjs-use-rxjs-concatmap-to-map-and-concat-high-order-observables?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz +- [concatMap](https://rxjs.dev/api/operators/concatMap) 📰 - Official docs +- [Use RxJS concatMap to map and concat higher order observables](https://egghead.io/lessons/rxjs-use-rxjs-concatmap-to-map-and-concat-high-order-observables?course=use-higher-order-observables-in-rxjs-effectively) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concatMap.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concatMap.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concatMap.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concatMap.ts) diff --git a/operators/transformation/concatmapto.md b/operators/transformation/concatmapto.md index 9c83a53e..fa328189 100644 --- a/operators/transformation/concatmapto.md +++ b/operators/transformation/concatmapto.md @@ -4,13 +4,15 @@ ## Subscribe to provided observable when previous completes, emit values. -
+ ### Examples ##### Example 1: Map to basic observable (simulating request) -( [StackBlitz](https://stackblitz.com/edit/typescript-fkkh6c?file=index.ts&devtoolsheight=50) ) +( +[StackBlitz](https://stackblitz.com/edit/typescript-fkkh6c?file=index.ts&devtoolsheight=50) +) ```js // RxJS v6+ @@ -29,8 +31,9 @@ const subscribe = example.subscribe(val => console.log(val)); ##### Example 2: Using projection with `concatMap` -( [StackBlitz](https://stackblitz.com/edit/typescript-8kcfm1?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/fogefebisu/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-8kcfm1?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/fogefebisu/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/s19wtscb/) ) ```js @@ -69,10 +72,9 @@ const subscribe = example.subscribe(val => console.log(val)); ### Additional Resources -* [concatMapTo](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-concatMapTo) - :newspaper: - Official docs +- [concatMapTo](https://rxjs.dev/api/operators/concatMapTo) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concatMapTo.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/concatMapTo.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concatMapTo.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/concatMapTo.ts) diff --git a/operators/transformation/exhaustmap.md b/operators/transformation/exhaustmap.md index a60c3d54..641a60d7 100644 --- a/operators/transformation/exhaustmap.md +++ b/operators/transformation/exhaustmap.md @@ -4,14 +4,25 @@ ## Map to inner observable, ignore other values until that observable completes. -
+### Why use `exhaustMap`? + +Imagine you're at a coffee shop where each customer is allowed to place only one order at a time and must wait until that order is fully prepared before making another. If they try to order again while their coffee is still brewing, the barista simply ignores them. That's the essence of `exhaustMap`. + +This operator is perfect for handling events that might be triggered multiple times in rapid succession but where only the initial trigger should be acted upon until it completes. A prime example would be ignoring clicks where a user might impatiently tap a 'Submit' button multiple times. Instead of sending multiple network requests, you'd ideally only want the first click to initiate the action and ignore subsequent clicks until the request is done. + +Take note that **`exhaustMap` will ignore source values while the previous inner observable is still active**. This means that if the inner observable hasn't completed, any new values emitted from the source will be discarded without any mapping. + +If you want to handle every single event, even if previous ones haven't completed, other operators like [`switchMap`](switchmap.md) or [`mergeMap`](mergemap.md) might be more appropriate. But when ensuring one task is exhausted before moving to the next, `exhaustMap` is your go-to choice. + + ### Examples ##### Example 1: exhaustMap with interval -( [Stackblitz](https://stackblitz.com/edit/typescript-3qydhn?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/woposeqobo/1/edit?js,console) | +( +[Stackblitz](https://stackblitz.com/edit/typescript-3qydhn?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/woposeqobo/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/9ovzapp9/) ) ```js @@ -30,24 +41,25 @@ const exhaustSub = merge( ) .pipe(exhaustMap(_ => sourceInterval.pipe(take(5)))) /* - * The first emitted value (of(true)) will be mapped - * to an interval observable emitting 1 value every - * second, completing after 5. - * Because the emissions from the delayed interval - * fall while this observable is still active they will be ignored. - * - * Contrast this with concatMap which would queue, - * switchMap which would switch to a new inner observable each emission, - * and mergeMap which would maintain a new subscription for each emitted value. - */ + * The first emitted value (of(true)) will be mapped + * to an interval observable emitting 1 value every + * second, completing after 5. + * Because the emissions from the delayed interval + * fall while this observable is still active they will be ignored. + * + * Contrast this with concatMap which would queue, + * switchMap which would switch to a new inner observable each emission, + * and mergeMap which would maintain a new subscription for each emitted value. + */ // output: 0, 1, 2, 3, 4 .subscribe(val => console.log(val)); ``` ##### Example 2: Another exhaustMap with interval -( [Stackblitz](https://stackblitz.com/edit/typescript-vxussb?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/fizuduzuti/1/edit?js,console) | +( +[Stackblitz](https://stackblitz.com/edit/typescript-vxussb?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/fizuduzuti/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/5ck8yg5k/3/) ) ```js @@ -88,6 +100,10 @@ const exhaustSub = firstInterval .subscribe(s => console.log(s)); ``` +### Related Recipes + +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) + ### Outside Examples ##### `exhaustMap` for login effect in [@ngrx example app](https://github.com/ngrx/platform/tree/a9e522953832b09bb329bac4524637bc608c450a/example-app) @@ -114,10 +130,10 @@ const exhaustSub = firstInterval ### Additional Resources -* [exhaustMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-exhaustMap) - :newspaper: - Official docs +- [exhaustMap](https://rxjs.dev/api/operators/exhaustMap) 📰 - Official docs + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/exhaustMap.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/exhaustMap.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/exhaustMap.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/exhaustMap.ts) diff --git a/operators/transformation/expand.md b/operators/transformation/expand.md index c71a4a22..d7ffaad7 100644 --- a/operators/transformation/expand.md +++ b/operators/transformation/expand.md @@ -4,14 +4,15 @@ ## Recursively call provided function. -
+ ### Examples ##### Example 1: Add one for each invocation -( [StackBlitz](https://stackblitz.com/edit/typescript-ntgecj?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/fuxocepazi/1/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-ntgecj?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/fuxocepazi/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/nu4apbLt/) ) ```js @@ -50,14 +51,13 @@ const subscribe = example.subscribe(val => console.log(`RESULT: ${val}`)); ### Related Recipes -* [Game Loop](../../recipes/gameloop.md) +- [Game Loop](../../recipes/gameloop.md) ### Additional Resources -* [expand](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-expand) - :newspaper: - Official docs +- [expand](https://rxjs.dev/api/operators/expand) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/expand.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/expand.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/expand.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/expand.ts) diff --git a/operators/transformation/groupby.md b/operators/transformation/groupby.md index cdf6b6ed..5c41fade 100644 --- a/operators/transformation/groupby.md +++ b/operators/transformation/groupby.md @@ -4,14 +4,15 @@ ## Group into observables based on provided value. -
+ ### Examples ##### Example 1: Group by property -( [StackBlitz](https://stackblitz.com/edit/typescript-dozkcg?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/buworowuye/edit?js,console) | +( +[StackBlitz](https://stackblitz.com/edit/typescript-dozkcg?file=index.ts&devtoolsheight=100) +| [jsBin](http://jsbin.com/buworowuye/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/utncxxvf/) ) ```js @@ -42,16 +43,51 @@ const example = source.pipe( const subscribe = example.subscribe(val => console.log(val)); ``` +##### Example 2: Group by into key - values + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-groupby-key-vals?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { from, of, zip } from 'rxjs'; +import { groupBy, mergeMap, toArray } from 'rxjs/operators'; + +const people = [ + { name: 'Sue', age: 25 }, + { name: 'Joe', age: 30 }, + { name: 'Frank', age: 25 }, + { name: 'Sarah', age: 35 } +]; + +from(people) + .pipe( + groupBy( + person => person.age, + p => p.name + ), + mergeMap(group => zip(of(group.key), group.pipe(toArray()))) + ) + .subscribe(console.log); + +/* + output: + [25, ["Sue", "Frank"]] + [30, ["Joe"]] + [35, ["Sarah"]] +*/ +``` + ### Additional Resources -* [groupBy](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-groupBy) - :newspaper: - Official docs -* [Group higher order observables with RxJS groupBy](https://egghead.io/lessons/rxjs-group-higher-order-observables-with-rxjs-groupby?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz -* [Use groupBy in real RxJS applications](https://egghead.io/lessons/rxjs-use-groupby-in-real-rxjs-applications?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz +- [groupBy](https://rxjs.dev/api/operators/groupBy) 📰 - Official docs +- [Group higher order observables with RxJS groupBy](https://egghead.io/lessons/rxjs-group-higher-order-observables-with-rxjs-groupby?course=use-higher-order-observables-in-rxjs-effectively) + 🎥 💵 - André Staltz +- [Use groupBy in real RxJS applications](https://egghead.io/lessons/rxjs-use-groupby-in-real-rxjs-applications?course=use-higher-order-observables-in-rxjs-effectively) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/groupBy.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/groupBy.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/groupBy.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/groupBy.ts) diff --git a/operators/transformation/map.md b/operators/transformation/map.md index 9e9e6178..c5d03277 100644 --- a/operators/transformation/map.md +++ b/operators/transformation/map.md @@ -1,20 +1,53 @@ # map -#### signature: `map(project: Function, thisArg: any): Observable` +## signature: `map(project: (value: T, index: number) => R): Observable` -## Apply projection with each value from source. +### Apply projection with each value from source. -
+{% hint style="info" %} -### Examples +New to transformation operators? Check out the article +[Get started transforming streams with map, pluck, and mapTo](../../concepts/get-started-transforming.md)! -##### Example 1: Add 10 to each number +{% endhint %} -( [StackBlitz](https://stackblitz.com/edit/typescript-a7bnxb?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/padasukano/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/yd38awLa/) ) +--- + +💡 Similar to the well-known Array.prototype.map, this is the operator you'll use most often to transform values in your streams! + +💡 Need to extract a single property from an object? Before RxJS v7, you might have used `pluck`, but now `map` with destructuring or property access is the recommended approach. + +💡 If your transformation function returns an Observable (like an HTTP call), you don't want `map` – you want one of the flattening operators: `mergeMap`, `switchMap`, `concatMap`, or `exhaustMap`. + +💡 The `map` operator applies to each value individually. If you need to transform or combine multiple values together, look at operators like `scan`, `reduce`, or `combineLatest`. + +--- + +### Why use map? + +`map` is your go-to operator for transforming data as it flows through your observable streams. Think of it as a factory assembly line where each item gets modified before moving along. Just as a stamping machine presses a logo onto every product passing through, `map` applies your transformation function to every value that comes down the stream. + +This operator truly shines when you need to reshape data to fit your application's needs. Imagine you're building a search feature for a shopping site. Your API returns complex product objects with dozens of fields – inventory counts, warehouse locations, internal IDs, and more – but your UI only needs the product name, price, and image URL. Rather than lugging around all that extra data through your application, you use `map` right after the HTTP call to [extract just what you need](#example-3-mapping-api-response-to-ui-model). This keeps your data flow clean and your components focused. + +One of the most common patterns you'll encounter is using `map` to [extract properties from objects](#example-2-map-to-single-property) or responses. Whether it's grabbing the `data` property from an API response wrapper, pulling user IDs from a collection of user objects, or transforming form values into the shape your backend expects, `map` handles these everyday transformation needs. It's also perfect for [simple calculations](#example-5-calculations-and-formatting) – adding tax to prices, converting timestamps to formatted dates, or transforming coordinates for a mapping library. + +What makes `map` particularly powerful is its predictability and simplicity. Unlike operators that manage multiple emissions or timing concerns, `map` has a straightforward one-to-one relationship: one input value produces one output value, synchronously. This makes it easy to reason about and debug. If a value goes in, a transformed value comes out immediately, no subscriptions required, no async complexity. + +`map` is equally useful for transforming events into data you actually care about. When working with DOM events, you rarely need the entire event object – just specific properties like [mouse coordinates](#example-4-transform-dom-events) or keyboard input values. `map` lets you cleanly extract exactly what matters. -```js +A critical point to remember: `map` is for *synchronous* transformations. If your transformation function needs to call an API, query a database, or perform any asynchronous operation that returns an Observable, `map` is the wrong choice. When you map to an Observable, you create a "higher-order Observable" (an Observable of Observables), which isn't what you want. For those scenarios, reach for `mergeMap`, `switchMap`, `concatMap`, or `exhaustMap` – the flattening operators that handle inner Observables. + +In essence, whenever you find yourself thinking "I need to transform each value in this stream," `map` should be your first instinct. It's the Swiss Army knife of RxJS – simple, reliable, and perfect for the vast majority of transformation tasks you'll encounter in reactive programming. + +--- + +## Examples + +### Example 1: Add 10 to each number + +( [StackBlitz](https://stackblitz.com/edit/typescript-a7bnxb?file=index.ts&devtoolsheight=100) ) + +```javascript // RxJS v6+ import { from } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -27,13 +60,11 @@ const example = source.pipe(map(val => val + 10)); const subscribe = example.subscribe(val => console.log(val)); ``` -##### Example 2: Map to single property +### Example 2: Map to single property -( [StackBlitz](https://stackblitz.com/edit/typescript-qgpnju?file=index.ts&devtoolsheight=100) | -[jsBin](http://jsbin.com/detozumale/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/tdLd5tgc/) ) +( [StackBlitz](https://stackblitz.com/edit/typescript-qgpnju?file=index.ts&devtoolsheight=100) ) -```js +```javascript // RxJS v6+ import { from } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -44,28 +75,126 @@ const source = from([ { name: 'Frank', age: 20 }, { name: 'Ryan', age: 50 } ]); -//grab each persons name, could also use pluck for this scenario +//extract each person's name using destructuring const example = source.pipe(map(({ name }) => name)); //output: "Joe","Frank","Ryan" const subscribe = example.subscribe(val => console.log(val)); ``` +### Example 3: Mapping API Response to UI Model + +( [StackBlitz](https://stackblitz.com/edit/typescript-kus9nthn?file=index.ts) ) + +```javascript +// RxJS v6+ +import { of } from 'rxjs'; +import { map } from 'rxjs/operators'; + +// Simulate an API response with extra metadata +const apiResponse = of({ + status: 'success', + timestamp: 1634567890, + data: { + id: 123, + firstName: 'Jane', + lastName: 'Doe', + email: 'jane@example.com', + internalFields: { /* ... */ } + } +}); + +// Extract and reshape just what the UI needs +const uiModel = apiResponse.pipe( + map(response => ({ + fullName: `${response.data.firstName} ${response.data.lastName}`, + email: response.data.email, + id: response.data.id + })) +); + +// output: { fullName: 'Jane Doe', email: 'jane@example.com', id: 123 } +const subscribe = uiModel.subscribe(val => console.log(val)); +``` + +### Example 4: Transform DOM Events + +( [StackBlitz](https://stackblitz.com/edit/typescript-a8rwwcaj?file=index.ts) ) + +```javascript +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { map } from 'rxjs/operators'; + +// Track mouse position from click events +const clicks = fromEvent(document, 'click'); + +const positions = clicks.pipe( + map((event: MouseEvent) => ({ + x: event.clientX, + y: event.clientY, + timestamp: Date.now() + })) +); + +// output: { x: 234, y: 567, timestamp: 1634567891234 } +positions.subscribe(pos => console.log('Click position:', pos)); +``` + +### Example 5: Calculations and Formatting + +( [StackBlitz](https://stackblitz.com/edit/typescript-rfbasqua?file=index.ts) ) + +```javascript +// RxJS v6+ +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; + +// Shopping cart items +const cartItems = from([ + { name: 'Laptop', price: 999.99, quantity: 1 }, + { name: 'Mouse', price: 29.99, quantity: 2 }, + { name: 'Keyboard', price: 79.99, quantity: 1 } +]); + +// Calculate total and format for display +const itemsWithTotal = cartItems.pipe( + map(item => ({ + ...item, + total: item.price * item.quantity, + displayPrice: `$${(item.price * item.quantity).toFixed(2)}` + })) +); + +/* output: + { name: 'Laptop', price: 999.99, quantity: 1, total: 999.99, displayPrice: '$999.99' } + { name: 'Mouse', price: 29.99, quantity: 2, total: 59.98, displayPrice: '$59.98' } + { name: 'Keyboard', price: 79.99, quantity: 1, total: 79.99, displayPrice: '$79.99' } +*/ +const subscribe = itemsWithTotal.subscribe(item => console.log(item)); +``` + +--- + ### Related Recipes -* [Smart Counter](../../recipes/smartcounter.md) -* [Game Loop](../../recipes/gameloop.md) -* [HTTP Polling](../../recipes/http-polling.md) +* [Alphabet Invasion Game](https://www.learnrxjs.io/learn-rxjs/recipes/alphabet-invasion-game) +* [Battleship Game](https://www.learnrxjs.io/learn-rxjs/recipes/battleship-game) +* [Catch The Dot Game](https://www.learnrxjs.io/learn-rxjs/recipes/catch-the-dot-game) +* [Save Indicator](https://www.learnrxjs.io/learn-rxjs/recipes/save-indicator) +* [Smart Counter](https://www.learnrxjs.io/learn-rxjs/recipes/smartcounter) +* [Space Invaders Game](https://www.learnrxjs.io/learn-rxjs/recipes/space-invaders-game) +* [Stop Watch](https://www.learnrxjs.io/learn-rxjs/recipes/stop-watch) +* [Swipe To Refresh](https://www.learnrxjs.io/learn-rxjs/recipes/swipe-to-refresh) +* [Type Ahead](https://www.learnrxjs.io/learn-rxjs/recipes/type-ahead) + +--- ### Additional Resources -* [map](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-map) - :newspaper: - Official docs -* [map vs flatMap](https://egghead.io/lessons/rxjs-rxjs-map-vs-flatmap) - :video_camera: - Ben Lesh -* [Transformation operator: map and mapTo](https://egghead.io/lessons/rxjs-transformation-operator-map-and-mapto?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +* [map](https://rxjs.dev/api/operators/map) 📰 - Official docs +* [map vs flatMap](https://egghead.io/lessons/rxjs-rxjs-map-vs-flatmap) 🎥 - Ben Lesh +* [Transformation operator: map and mapTo](https://egghead.io/lessons/rxjs-transformation-operator-map-and-mapto?course=rxjs-beyond-the-basics-operators-in-depth) 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/map.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/map.ts) +📁 **Source Code:** [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/map.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/map.ts) \ No newline at end of file diff --git a/operators/transformation/mapto.md b/operators/transformation/mapto.md index d77ae82a..f619c4d4 100644 --- a/operators/transformation/mapto.md +++ b/operators/transformation/mapto.md @@ -4,7 +4,14 @@ ## Map emissions to constant value. -
+{% hint style="info" %} + +New to transformation operators? Check out the article +[Get started transforming streams with map, pluck, and mapTo](../../concepts/get-started-transforming.md)! + +{% endhint %} + + ### Examples @@ -50,19 +57,20 @@ const subscribe = example.subscribe(val => console.log(val)); ### Related Recipes -* [HTTP Polling](../../recipes/http-polling.md) -* [Smart Counter](../../recipes/smartcounter.md) +- [HTTP Polling](../../recipes/http-polling.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Smart Counter](../../recipes/smartcounter.md) +- [Stop Watch](../../recipes/stop-watch.md) ### Additional Resources -* [mapTo](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mapTo) - :newspaper: - Official docs -* [Changing behavior with mapTo](https://egghead.io/lessons/rxjs-changing-behavior-with-mapto?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist -* [Transformation operator: map and mapTo](https://egghead.io/lessons/rxjs-transformation-operator-map-and-mapto?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz +- [mapTo](https://rxjs.dev/api/operators/mapTo) 📰 - Official docs +- [Changing behavior with mapTo](https://egghead.io/lessons/rxjs-changing-behavior-with-mapto?course=step-by-step-async-javascript-with-rxjs) + 🎥 💵 - John Linquist +- [Transformation operator: map and mapTo](https://egghead.io/lessons/rxjs-transformation-operator-map-and-mapto?course=rxjs-beyond-the-basics-operators-in-depth) + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/mapTo.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/mapTo.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mapTo.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mapTo.ts) diff --git a/operators/transformation/mergemap.md b/operators/transformation/mergemap.md index 2007306b..76effc1e 100644 --- a/operators/transformation/mergemap.md +++ b/operators/transformation/mergemap.md @@ -6,13 +6,13 @@ --- -:bulb: flatMap is an alias for mergeMap! +💡 flatMap is an alias for mergeMap! -:bulb: If only one inner subscription should be active at a time, try +💡 If only one inner subscription should be active at a time, try [`switchMap`](switchmap.md)! -:bulb: If the order of emission and subscription of inner observables is -important, try [`concatMap`](concatmap.md)! +💡 If the order of emission and subscription of inner observables is important, +try [`concatMap`](concatmap.md)! --- @@ -37,114 +37,163 @@ manage the completion of the inner subscription, think [`take`](../filtering/take.md) or [`takeUntil`](../filtering/takeuntil.md). You can also limit the number of active inner subscriptions at a time with the `concurrent` parameter, seen in -[example 4](#example-4-mergemap-with-concurrent-value). +[example 5](#example-5-mergemap-with-concurrent-value). + -
### Examples -##### Example 1: mergeMap with observable +##### Example 1: mergeMap simulating save of click locations ( -[StackBlitz](https://stackblitz.com/edit/typescript-f8ghcx?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/mojurubana/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/41awjgda/) ) +[StackBlitz](https://stackblitz.com/edit/rxjs-xfwdnl?file=index.ts&devtoolsheight=60) +) ```js // RxJS v6+ -import { of } from 'rxjs'; +import { fromEvent, of } from 'rxjs'; +import { mergeMap, delay } from 'rxjs/operators'; + +// faking network request for save +const saveLocation = location => { + return of(location).pipe(delay(500)); +}; +// streams +const click$ = fromEvent(document, 'click'); + +click$ + .pipe( + mergeMap((e: MouseEvent) => { + return saveLocation({ + x: e.clientX, + y: e.clientY, + timestamp: Date.now() + }); + }) + ) + // Saved! {x: 98, y: 170, ...} + .subscribe(r => console.log('Saved!', r)); +``` + +##### Example 2: mergeMap with ajax observable + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-wixf2a?file=index.ts&devtoolsheight=60) +) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { ajax } from 'rxjs/ajax'; import { mergeMap } from 'rxjs/operators'; -//emit 'Hello' -const source = of('Hello'); -//map to inner observable and flatten -const example = source.pipe(mergeMap(val => of(`${val} World!`))); -//output: 'Hello World!' -const subscribe = example.subscribe(val => console.log(val)); +// free api url +const API_URL = 'https://jsonplaceholder.typicode.com/todos/1'; + +// streams +const click$ = fromEvent(document, 'click'); + +click$ + .pipe( + /* + * Using mergeMap for example, but generally for GET requests + * you will prefer switchMap. + * Also, if you do not need the parameter like + * below you could use mergeMapTo instead. + * ex. mergeMapTo(ajax.getJSON(API_URL)) + */ + mergeMap(() => ajax.getJSON(API_URL)) + ) + // { userId: 1, id: 1, ...} + .subscribe(console.log); ``` -##### Example 2: mergeMap with promise +##### Example 3: mergeMap with promise (could also use [from](../creation/from.md) to convert to observable) ( [StackBlitz](https://stackblitz.com/edit/typescript-pnnsrq?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/vuhecorana/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/o9kxpvsv/) ) +) ```js // RxJS v6+ import { of } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -//emit 'Hello' -const source = of('Hello'); -//mergeMap also emits result of promise +// helper to create promise const myPromise = val => new Promise(resolve => resolve(`${val} World From Promise!`)); -//map to promise and emit result -const example = source.pipe(mergeMap(val => myPromise(val))); -//output: 'Hello World From Promise' -const subscribe = example.subscribe(val => console.log(val)); + +// emit 'Hello' +const source$ = of('Hello'); + +// map to promise and emit result +source$ + .pipe(mergeMap(val => myPromise(val))) + // output: 'Hello World From Promise' + .subscribe(val => console.log(val)); ``` -##### Example 3: mergeMap with `resultSelector` +##### Example 4: mergeMap with `resultSelector` ( [StackBlitz](https://stackblitz.com/edit/typescript-9p6ws7?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/wajokocage/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/zu9a6vr4/) ) +) ```js // RxJS v6+ import { of } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; -/* - you can also supply a second argument which receives the source value and emitted - value of inner observable or promise -*/ -//emit 'Hello' -const source = of('Hello'); -//mergeMap also emits result of promise +// helper to create promise const myPromise = val => new Promise(resolve => resolve(`${val} World From Promise!`)); -const example = source.pipe( - mergeMap( - val => myPromise(val), - (valueFromSource, valueFromPromise) => { - return `Source: ${valueFromSource}, Promise: ${valueFromPromise}`; - } + +// emit 'Hello' +const source$ = of('Hello'); + +source$ + .pipe( + mergeMap( + val => myPromise(val), + /* + you can also supply a second argument which receives the source value and emitted + value of inner observable or promise + */ + (valueFromSource, valueFromPromise) => { + return `Source: ${valueFromSource}, Promise: ${valueFromPromise}`; + } + ) ) -); -//output: "Source: Hello, Promise: Hello World From Promise!" -const subscribe = example.subscribe(val => console.log(val)); + // output: "Source: Hello, Promise: Hello World From Promise!" + .subscribe(val => console.log(val)); ``` -##### Example 4: mergeMap with concurrent value +##### Example 5: mergeMap with concurrent value ( [StackBlitz](https://stackblitz.com/edit/typescript-r3gcr4?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/qaqucuwise/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/2rmLxpyz/) ) +) ```js // RxJS v6+ import { interval } from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; -//emit value every 1s -const source = interval(1000); - -const example = source.pipe( - mergeMap( - //project - val => interval(5000).pipe(take(2)), - //resultSelector - (oVal, iVal, oIndex, iIndex) => [oIndex, oVal, iIndex, iVal], - //concurrent - 2 +// emit value every 1s +const source$ = interval(1000); + +source$ + .pipe( + mergeMap( + // project + val => interval(5000).pipe(take(2)), + // resultSelector + (oVal, iVal, oIndex, iIndex) => [oIndex, oVal, iIndex, iVal], + // concurrent + 2 + ) ) -); -/* + /* Output: [0, 0, 0, 0] <--1st inner observable [1, 1, 0, 0] <--2nd inner observable @@ -153,27 +202,30 @@ const example = source.pipe( [2, 2, 0, 0] <--3rd inner observable [3, 3, 0, 0] <--4th inner observable */ -const subscribe = example.subscribe(val => console.log(val)); + .subscribe(val => console.log(val)); ``` ### Related Recipes +- [Breakout Game](../../recipes/breakout-game.md) - [HTTP Polling](../../recipes/http-polling.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) ### Additional Resources -- [mergeMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeMap) - :newspaper: - Official docs -- [map vs flatMap](https://egghead.io/lessons/rxjs-rxjs-map-vs-flatmap) - :video_camera: :dollar: - Ben Lesh +- [mergeMap](https://rxjs.dev/api/operators/mergeMap) 📰 - Official docs +- [map vs flatMap](https://egghead.io/lessons/rxjs-rxjs-map-vs-flatmap) 🎥 💵 - + Ben Lesh - [Async requests and responses in RxJS](https://egghead.io/lessons/rxjs-04-reactive-programming-async-requests-and-responses-in-rxjs) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz - [Use RxJS mergeMap to map and merge higher order observables](https://egghead.io/lessons/rxjs-use-rxjs-mergemap-to-map-and-merge-high-order-observables?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz - [Use RxJS mergeMap for fine grain custom behavior](https://egghead.io/lessons/rxjs-use-rxjs-mergemap-for-fine-grain-custom-behavior?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/mergeMap.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/mergeMap.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mergeMap.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mergeMap.ts) diff --git a/operators/transformation/mergescan.md b/operators/transformation/mergescan.md new file mode 100644 index 00000000..6251d369 --- /dev/null +++ b/operators/transformation/mergescan.md @@ -0,0 +1,52 @@ +# mergeScan + +#### signature: `mergeScan(accumulator: (acc, value, index: number) => ObservableInput, seed, concurrent: number = Number.POSITIVE_INFINITY): OperatorFunction` + +## Accumulate value over time via merged observables. + + + +### Examples + +##### Example 1: Accumulate total duration mouse held down over time + +( +[StackBlitz](https://stackblitz.com/edit/typescript-gzaak8?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent, interval } from 'rxjs'; +import { mergeScan, take, takeUntil, map, scan } from 'rxjs/operators'; + +// reference +const durationElem = document.getElementById('duration'); + +// streams +const mouseDown$ = fromEvent(document, 'mousedown'); +const mouseUp$ = fromEvent(document, 'mouseup'); + +// accumulate time mouse held down over time +mouseDown$ + .pipe( + mergeScan((acc, curr) => { + return interval(1000).pipe( + scan((a, _) => ++a, 0), + map((val: any) => val + acc), + takeUntil(mouseUp$) + ); + }, 0) + // output: 1s...2s...3s...4s... + ) + .subscribe(val => (durationElem.innerHTML = `${val}s`)); +``` + +### Additional Resources + +- [pluck](https://rxjs-dev.firebaseapp.com/api/operators/mergeScan) 📰 - + Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mergeScan.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/mergeScan.ts) diff --git a/operators/transformation/partition.md b/operators/transformation/partition.md index bb9d4788..d90c42f6 100644 --- a/operators/transformation/partition.md +++ b/operators/transformation/partition.md @@ -4,7 +4,7 @@ ## Split one observable into two based on provided predicate. -
+ ### Examples @@ -76,12 +76,48 @@ const subscribe = merge( ).subscribe(val => console.log(val)); ``` +##### Example 3: (v6.5+) Partition as a static function + +( +[StackBlitz](https://stackblitz.com/edit/typescript-vmfvp8?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6.5+ +import { merge, of, from, partition } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +const source = from([1, 2, 3, 4, 5, 6]); +//if greater than 3 throw +const example = source.pipe( + map(val => { + if (val > 3) { + throw `${val} greater than 3!`; + } + return { success: val }; + }), + catchError(val => of({ error: val })) +); +// split on success or error +const [success, error] = partition(example, res => res.success); +/* + Output: + "Success! 1" + "Success! 2" + "Success! 3" + "Error! 4 greater than 3!" +*/ +const subscribe = merge( + success.pipe(map(val => `Success! ${val.success}`)), + error.pipe(map(val => `Error! ${val.error}`)) +).subscribe(val => console.log(val)); +``` + ### Additional Resources -- [partition](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-partition) - :newspaper: - Official docs +- [partition](https://rxjs.dev/api/operators/partition) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/partition.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/partition.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/partition.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/partition.ts) diff --git a/operators/transformation/pluck.md b/operators/transformation/pluck.md index 7070bfd5..4421316a 100644 --- a/operators/transformation/pluck.md +++ b/operators/transformation/pluck.md @@ -2,9 +2,16 @@ #### signature: `pluck(properties: ...args): Observable` -## Select properties to emit. +## Select property to emit. + +{% hint style="info" %} + +New to transformation operators? Check out the article +[Get started transforming streams with map, pluck, and mapTo](../../concepts/get-started-transforming.md)! + +{% endhint %} + -
### Examples @@ -20,7 +27,10 @@ import { from } from 'rxjs'; import { pluck } from 'rxjs/operators'; -const source = from([{ name: 'Joe', age: 30 }, { name: 'Sarah', age: 35 }]); +const source = from([ + { name: 'Joe', age: 30 }, + { name: 'Sarah', age: 35 } +]); //grab names const example = source.pipe(pluck('name')); //output: "Joe", "Sarah" @@ -50,12 +60,22 @@ const example = source.pipe(pluck('job', 'title')); const subscribe = example.subscribe(val => console.log(val)); ``` +### Related Recipes + +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) + ### Additional Resources -- [pluck](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-pluck) - :newspaper: - Official docs +- [pluck](https://rxjs.dev/api/operators/pluck) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/pluck.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/pluck.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/pluck.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/pluck.ts) diff --git a/operators/transformation/reduce.md b/operators/transformation/reduce.md index 96fa5310..c6333ec3 100644 --- a/operators/transformation/reduce.md +++ b/operators/transformation/reduce.md @@ -4,13 +4,13 @@ ## Reduces the values from source observable to a single value that's emitted when the source completes. -:bulb: Just like +💡 Just like [`Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce?v=a) -:bulb: If you need the current accumulated value on each emission, try +💡 If you need the current accumulated value on each emission, try [scan](scan.md)! -
+ ### Examples @@ -34,12 +34,12 @@ const subscribe = example.subscribe(val => console.log('Sum:', val)); ### Additional Resources -- [reduce](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-reduce) - :newspaper: - Official docs +- [reduce](https://rxjs.dev/api/operators/reduce) 📰 - Official docs - [Scan() vs reduce() | RxJS TUTORIAL](https://www.youtube.com/watch?v=myEeo2rZc3g) - :video_camera: - Academind + 🎥 - Academind + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/reduce.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/reduce.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/reduce.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/reduce.ts) diff --git a/operators/transformation/scan.md b/operators/transformation/scan.md index ea7b4036..8b192d1d 100644 --- a/operators/transformation/scan.md +++ b/operators/transformation/scan.md @@ -6,12 +6,19 @@ --- -:bulb: You can create [Redux](http://redux.js.org)-like state management with -scan! +💡 You can create [Redux](http://redux.js.org)-like state management with scan! --- -
+### Why use `scan`? + +The key distinction of the scan operator when compared to other reduction operators is its continuous accumulation feature. With each emitted value, the accumulator function is applied, and the accumulated result is emitted instantaneously. You can remember this by the phrase "accumulate and emit on-the-go." + +The scan operator is highly useful in scenarios that require real-time monitoring and processing, such as tallying scores in a game, where you want to display the updated score each time points are added. However, be cautious when using scan for cases where the *only* the final accumulated result is crucial. In those situations, the [`reduce`](reduce.md) operator may be more appropriate, as it emits only the final value after the source completes. + +In summary, the scan operator provides a powerful and flexible means of handling continuous accumulation and emission of values, which can be especially useful in real-time monitoring and processing tasks. + + ### Examples @@ -85,23 +92,65 @@ const scanObs = interval(1000) .subscribe(console.log); ``` +##### Example 4: Accumulating http responses over time + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-scan-accumulate-request-responses?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { interval, of } from 'rxjs'; +import { scan, delay, repeat, mergeMap } from 'rxjs/operators'; + +const fakeRequest = of('response').pipe(delay(2000)); + +// output: +// ['response'], +// ['response','response'], +// ['response','response','response'], +// etc... + +interval(1000) + .pipe( + mergeMap(_ => fakeRequest), + scan < string > ((all, current) => [...all, current], []) + ) + .subscribe(console.log); +``` + ### Related Recipes -- [Smart Counter](../../recipes/smartcounter.md) +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Catch The Dot Game](../../recipes/catch-the-dot-game.md) +- [Click Ninja Game](../../recipes/click-ninja-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) +- [Matrix Digital Rain](../../recipes/matrix-digital-rain.md) +- [Memory Game](../../recipes/memory-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) - [Progress Bar](../../recipes/progressbar.md) +- [Smart Counter](../../recipes/smartcounter.md) +- [Space Invaders Game](../../recipes/space-invaders-game.md) +- [Stop Watch](../../recipes/stop-watch.md) +- [Tank Battle Game](../../recipes/tank-battle-game.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) ### Additional Resources -- [scan](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-scan) - :newspaper: - Official docs +- [scan](https://rxjs.dev/api/operators/scan) 📰 - Official docs - [Aggregating streams with reduce and scan using RxJS](https://egghead.io/lessons/rxjs-aggregating-streams-with-reduce-and-scan-using-rxjs) - :video_camera: - Ben Lesh + 🎥 - Ben Lesh - [Updating data with scan](https://egghead.io/lessons/rxjs-updating-data-with-scan?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist - [Transformation operator: scan](https://egghead.io/lessons/rxjs-transformation-operator-scan?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/scan.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/scan.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/scan.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/scan.ts) diff --git a/operators/transformation/switchmap.md b/operators/transformation/switchmap.md index f94cf5f6..71adb4aa 100644 --- a/operators/transformation/switchmap.md +++ b/operators/transformation/switchmap.md @@ -6,13 +6,13 @@ --- -:bulb: If you would like more than one inner subscription to be maintained, try +💡 If you would like more than one inner subscription to be maintained, try [`mergeMap`](mergemap.md)! -:bulb: This operator is generally considered a safer default to +💡 This operator is generally considered a safer default to [`mergeMap`](mergemap.md)! -:bulb: This operator can cancel in-flight network requests! +💡 This operator can cancel in-flight network requests! --- @@ -23,7 +23,7 @@ cancelling effect. On each emission the previous inner observable (the result of the function you supplied) is cancelled and the new observable is subscribed. You can remember this by the phrase **switch to a new observable**. -This works perfect for scenarios like +This works perfectly for scenarios like [typeaheads](https://angular-2-training-book.rangle.io/handout/http/search_with_switchmap.html) where you are no longer concerned with the response of the previous request when a new input arrives. This also is a safe option in situations where a long lived @@ -38,89 +38,30 @@ every request needs to complete, think writes to a database. `switchMap` could cancel a request if the source emits quickly enough. In these scenarios [mergeMap](mergemap.md) is the correct option. -
-### Examples - -##### Example 1: Restart interval every 5 seconds - -( -[StackBlitz](https://stackblitz.com/edit/typescript-eb62ap?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/birepuveya/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/6pz981gd/) ) - -```js -// RxJS v6+ -import { timer, interval } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -//emit immediately, then every 5s -const source = timer(0, 5000); -//switch to new inner observable when source emits, emit items that are emitted -const example = source.pipe(switchMap(() => interval(500))); -//output: 0,1,2,3,4,5,6,7,8,9...0,1,2,3,4,5,6,7,8 -const subscribe = example.subscribe(val => console.log(val)); -``` +### Examples -##### Example 2: Reset on every click +##### Example 1: Restart interval on every click ( [StackBlitz](https://stackblitz.com/edit/typescript-s4pvix?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/zoruboxogo/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/y11v8aqz/) ) +) ```js // RxJS v6+ import { interval, fromEvent } from 'rxjs'; -import { switchMap, mapTo } from 'rxjs/operators'; - -//emit every click -const source = fromEvent(document, 'click'); -//if another click comes within 3s, message will not be emitted -const example = source.pipe( - switchMap(val => interval(3000).pipe(mapTo('Hello, I made it!'))) -); -//(click)...3s...'Hello I made it!'...(click)...2s(click)... -const subscribe = example.subscribe(val => console.log(val)); -``` - -##### Example 3: Using a `resultSelector` function - -( -[StackBlitz](https://stackblitz.com/edit/typescript-bmibzi?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/qobapubeze/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/nqfu534y/) ) - -```js -// RxJS v6+ -import { timer, interval } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -//emit immediately, then every 5s -const source = timer(0, 5000); -//switch to new inner observable when source emits, invoke project function and emit values -const example = source.pipe( - switchMap( - _ => interval(2000), - (outerValue, innerValue, outerIndex, innerIndex) => ({ - outerValue, - innerValue, - outerIndex, - innerIndex - }) +fromEvent(document, 'click') + .pipe( + // restart counter on every click + switchMap(() => interval(1000)) ) -); -/* - Output: - {outerValue: 0, innerValue: 0, outerIndex: 0, innerIndex: 0} - {outerValue: 0, innerValue: 1, outerIndex: 0, innerIndex: 1} - {outerValue: 1, innerValue: 0, outerIndex: 1, innerIndex: 0} - {outerValue: 1, innerValue: 1, outerIndex: 1, innerIndex: 1} -*/ -const subscribe = example.subscribe(val => console.log(val)); + .subscribe(console.log); ``` -##### Example 4: Countdown timer with switchMap +##### Example 2: Countdown timer with pause and resume ( [StackBlitz](https://stackblitz.com/edit/typescript-ivdebg?file=index.ts) ) @@ -129,12 +70,15 @@ const subscribe = example.subscribe(val => console.log(val)); import { interval, fromEvent, merge, empty } from 'rxjs'; import { switchMap, scan, takeWhile, startWith, mapTo } from 'rxjs/operators'; -const countdownSeconds = 10; -const setHTML = id => val => (document.getElementById(id).innerHTML = val); +const COUNTDOWN_SECONDS = 10; + +// elem refs +const remainingLabel = document.getElementById('remaining'); const pauseButton = document.getElementById('pause'); const resumeButton = document.getElementById('resume'); -const interval$ = interval(1000).pipe(mapTo(-1)); +// streams +const interval$ = interval(1000).pipe(mapTo(-1)); const pause$ = fromEvent(pauseButton, 'click').pipe(mapTo(false)); const resume$ = fromEvent(resumeButton, 'click').pipe(mapTo(true)); @@ -142,46 +86,76 @@ const timer$ = merge(pause$, resume$) .pipe( startWith(true), switchMap(val => (val ? interval$ : empty())), - scan((acc, curr) => (curr ? curr + acc : acc), countdownSeconds), + scan((acc, curr) => (curr ? curr + acc : acc), COUNTDOWN_SECONDS), takeWhile(v => v >= 0) ) - .subscribe(setHTML('remaining')); + .subscribe((val: any) => (remainingLabel.innerHTML = val)); ``` -###### HTML - -```html -

-Time remaining: -

- - +##### Example 3: Using a `resultSelector` function + +( +[StackBlitz](https://stackblitz.com/edit/typescript-bmibzi?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { timer, interval } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +// switch to new inner observable when source emits, emit result of project function +timer(0, 5000) + .pipe( + switchMap( + _ => interval(2000), + (outerValue, innerValue, outerIndex, innerIndex) => ({ + outerValue, + innerValue, + outerIndex, + innerIndex + }) + ) + ) + /* + Output: + {outerValue: 0, innerValue: 0, outerIndex: 0, innerIndex: 0} + {outerValue: 0, innerValue: 1, outerIndex: 0, innerIndex: 1} + {outerValue: 1, innerValue: 0, outerIndex: 1, innerIndex: 0} + {outerValue: 1, innerValue: 1, outerIndex: 1, innerIndex: 1} +*/ + .subscribe(console.log); ``` ### Related Recipes -- [Smart Counter](../../recipes/smartcounter.md) -- [Progress Bar](../../recipes/progressbar.md) +- [Alphabet Invasion Game](../../recipes/alphabet-invasion-game.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Catch The Dot Game](../../recipes/catch-the-dot-game.md) - [HTTP Polling](../../recipes/http-polling.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Progress Bar](../../recipes/progressbar.md) +- [Smart Counter](../../recipes/smartcounter.md) +- [Stop Watch](../../recipes/stop-watch.md) +- [Typeahead](../../recipes/type-ahead.md) ### Additional Resources -- [switchMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-switchMap) - :newspaper: - Official docs +- [switchMap](https://rxjs.dev/api/operators/switchMap) 📰 - Official docs - [Avoiding switchMap-Related Bugs](https://blog.angularindepth.com/switchmap-bugs-b6de69155524) - Nicholas Jamieson - [Starting a stream with switchMap](https://egghead.io/lessons/rxjs-starting-a-stream-with-switchmap?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist - [Use RxJS switchMap to map and flatten higher order observables](https://egghead.io/lessons/rxjs-use-rxjs-switchmap-to-map-and-flatten-higher-order-observables?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz - [Use switchMap as a safe default to flatten observables in RxJS](https://egghead.io/lessons/rxjs-use-switchmap-as-a-safe-default-to-flatten-observables-in-rxjs?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/switchMap.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/switchMap.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/switchMap.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/switchMap.ts) diff --git a/operators/transformation/switchmapto.md b/operators/transformation/switchmapto.md new file mode 100644 index 00000000..bbd07f38 --- /dev/null +++ b/operators/transformation/switchmapto.md @@ -0,0 +1,61 @@ +# switchMapTo + +#### signature: `switchMapTo(innerObservable: Observable, resultSelector: function(outerValue, innerValue, outerIndex, innerIndex): any): Observable` + +## Map to same inner observable, complete previous inner observable. + +--- + +💡 If you need to consider the emitted value from the source, try +[`switchMap`](switchmap.md)! + +--- + + + +### Examples + +##### Example 1: Restart countdown on click, until countdown completes one time + +( [StackBlitz](https://stackblitz.com/edit/typescript-r97ngc?file=index.ts) ) + +```js +// RxJS v6+ +import { interval, fromEvent } from 'rxjs'; +import { + switchMapTo, + scan, + startWith, + takeWhile, + finalize +} from 'rxjs/operators'; + +const COUNTDOWN_TIME = 10; + +// reference +const countdownElem = document.getElementById('countdown'); + +// streams +const click$ = fromEvent(document, 'click'); +const countdown$ = interval(1000).pipe( + scan((acc, _) => --acc, COUNTDOWN_TIME), + startWith(COUNTDOWN_TIME) +); + +click$ + .pipe( + switchMapTo(countdown$), + takeWhile(val => val >= 0), + finalize(() => (countdownElem.innerHTML = "We're done here!")) + ) + .subscribe((val: any) => (countdownElem.innerHTML = val)); +``` + +### Additional Resources + +- [switchMapTo](https://rxjs.dev/api/operators/switchMapTo) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/switchMapTo.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/switchMapTo.ts) diff --git a/operators/transformation/toarray.md b/operators/transformation/toarray.md new file mode 100644 index 00000000..13bfe72e --- /dev/null +++ b/operators/transformation/toarray.md @@ -0,0 +1,41 @@ +# toArray + +#### signature: `toArray(): OperatorFunction` + +## Collects all source emissions and emits them as an array when the source completes. + + + +### Examples + +##### Example 1: get values emitted by interval as an array when interval completes + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-toarray?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { interval } from 'rxjs'; +import { toArray, take } from 'rxjs/operators'; + +interval(100) + .pipe(take(10), toArray()) + .subscribe(console.log); + +// output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +### Related Recipes + +- [Breakout Game](../../recipes/breakout-game.md) +- [Lockscreen](../../recipes/lockscreen.md) + +### Additional Resources + +- [toArray](https://rxjs.dev/api/operators/toArray) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/toArray.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/toArray.ts) diff --git a/operators/transformation/window.md b/operators/transformation/window.md index 8360a570..2bd23d23 100644 --- a/operators/transformation/window.md +++ b/operators/transformation/window.md @@ -41,12 +41,12 @@ const subscribeTwo = example ### Additional Resources -- [window](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-window) - :newspaper: - Official docs +- [window](https://rxjs.dev/api/operators/window) + 📰 - Official docs - [Split an RxJS observable with window](https://egghead.io/lessons/rxjs-split-an-rxjs-observable-with-window?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/window.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/window.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/window.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/window.ts) diff --git a/operators/transformation/windowcount.md b/operators/transformation/windowcount.md index ef8691a1..62035cb8 100644 --- a/operators/transformation/windowcount.md +++ b/operators/transformation/windowcount.md @@ -4,7 +4,7 @@ ## Observable of values from source, emitted each time provided count is fulfilled. -
+ ### Examples @@ -51,10 +51,9 @@ const subscribeTwo = example ### Additional Resources -- [windowCount](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-windowCount) - :newspaper: - Official docs +- [windowCount](https://rxjs.dev/api/operators/windowCount) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowCount.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowCount.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowCount.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowCount.ts) diff --git a/operators/transformation/windowtime.md b/operators/transformation/windowtime.md index 679393b5..1d1e3efa 100644 --- a/operators/transformation/windowtime.md +++ b/operators/transformation/windowtime.md @@ -4,7 +4,7 @@ ## Observable of values collected from source for each provided time span. -
+ ### Examples @@ -49,10 +49,9 @@ const subscribeTwo = example ### Additional Resources -- [windowTime](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-windowTime) - :newspaper: - Official docs +- [windowTime](https://rxjs.dev/api/operators/windowTime) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowTime.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowTime.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowTime.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowTime.ts) diff --git a/operators/transformation/windowtoggle.md b/operators/transformation/windowtoggle.md index add945b0..ebeeda03 100644 --- a/operators/transformation/windowtoggle.md +++ b/operators/transformation/windowtoggle.md @@ -4,7 +4,7 @@ ## Collect and emit observable of values from source between opening and closing emission. -
+ ### Examples @@ -55,12 +55,11 @@ const subscribeTwo = example ### Additional Resources -- [windowToggle](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-windowToggle) - :newspaper: - Official docs +- [windowToggle](https://rxjs.dev/api/operators/windowToggle) 📰 - Official docs - [Split an RxJS observable conditionally with windowToggle](https://egghead.io/lessons/rxjs-split-an-rxjs-observable-conditionally-with-windowtoggle?course=use-higher-order-observables-in-rxjs-effectively) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowToggle.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowToggle.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowToggle.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowToggle.ts) diff --git a/operators/transformation/windowwhen.md b/operators/transformation/windowwhen.md index 5df63ddd..9d5d7dd1 100644 --- a/operators/transformation/windowwhen.md +++ b/operators/transformation/windowwhen.md @@ -4,7 +4,7 @@ ## Close window at provided time frame emitting observable of collected values from source. -
+ ### Examples @@ -53,10 +53,9 @@ const subscribeTwo = example ### Additional Resources -- [windowWhen](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-windowWhen) - :newspaper: - Official docs +- [windowWhen](https://rxjs.dev/api/operators/windowWhen) 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/windowWhen.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/windowWhen.ts) diff --git a/operators/utility/README.md b/operators/utility/README.md index 0d531317..eb7f5444 100644 --- a/operators/utility/README.md +++ b/operators/utility/README.md @@ -5,13 +5,17 @@ provide helpful utilities in your observable toolkit. ## Contents -* [do / tap](do.md) :star: -* [delay](delay.md) :star: -* [delayWhen](delaywhen.md) -* [dematerialize](dematerialize.md) -* [finalize / finally](finalize.md) -* [let](let.md) -* [timeout](timeout.md) -* [toPromise](topromise.md) +- [tap / do](do.md) ⭐ +- [delay](delay.md) ⭐ +- [delayWhen](delaywhen.md) +- [dematerialize](dematerialize.md) +- [finalize / finally](finalize.md) +- [let](let.md) +- [repeat](repeat.md) +- [repeatWhen](repeatwhen.md) +- [timeInterval](timeinterval.md) +- [timeout](timeout.md) +- [timeoutWith](timeoutwith.md) +- [toPromise](topromise.md) -:star: - _commonly used_ +⭐ - _commonly used_ diff --git a/operators/utility/delay.md b/operators/utility/delay.md index 00fc0d30..5fd0d96f 100644 --- a/operators/utility/delay.md +++ b/operators/utility/delay.md @@ -4,16 +4,37 @@ ## Delay emitted values by given time. -
+### Why use `delay`? + +This operator is your go-to when simulating real-world scenarios such as network latency or introducing a pause before a value is emitted. The `delay` operator allows you to hold back values for a specified duration before they're released to subscribers. + +Keep in mind that **`delay` won’t prevent the original observable from emitting values**. It merely postpones the delivery to its subscribers. This is a _gotcha_ as it could look like your data is lagging or not in sync with the source, especially when multiple observables are at play. + + ### Examples -##### Example 1: Delay for increasing durations +##### Example 1: Delay to recognize long press + +( [StackBlitz](https://stackblitz.com/edit/rxjs-bru5fi?devtoolsheight=60) ) + +```js +import { fromEvent, of } from 'rxjs'; +import { mergeMap, delay, takeUntil } from 'rxjs/operators'; + +const mousedown$ = fromEvent(document, 'mousedown'); +const mouseup$ = fromEvent(document, 'mouseup'); + +mousedown$ + .pipe(mergeMap(event => of(event).pipe(delay(700), takeUntil(mouseup$)))) + .subscribe(event => console.log('Long Press!', event)); +``` + +##### Example 2: Delay for increasing durations ( [StackBlitz](https://stackblitz.com/edit/typescript-twjn8r?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/zebatixije/1/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/1kxtzcu6/) ) +) ```js // RxJS v6+ @@ -25,18 +46,9 @@ const example = of(null); //delay output of each by an extra second const message = merge( example.pipe(mapTo('Hello')), - example.pipe( - mapTo('World!'), - delay(1000) - ), - example.pipe( - mapTo('Goodbye'), - delay(2000) - ), - example.pipe( - mapTo('World!'), - delay(3000) - ) + example.pipe(mapTo('World!'), delay(1000)), + example.pipe(mapTo('Goodbye'), delay(2000)), + example.pipe(mapTo('World!'), delay(3000)) ); //output: 'Hello'...'World!'...'Goodbye'...'World!' const subscribe = message.subscribe(val => console.log(val)); @@ -44,16 +56,18 @@ const subscribe = message.subscribe(val => console.log(val)); ### Related Recipes +- [Battleship Game](../../recipes/battleship-game.md) - [Progress Bar](../../recipes/progressbar.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) ### Additional Resources -- [delay](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-delay) - :newspaper: - Official docs +- [delay](https://rxjs.dev/api/operators/delay) 📰 - Official docs - [Transformation operator: delay and delayWhen](https://egghead.io/lessons/rxjs-transformation-operators-delay-and-delaywhen?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/delay.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/delay.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/delay.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/delay.ts) diff --git a/operators/utility/delaywhen.md b/operators/utility/delaywhen.md index 059c855b..2e1b9ec9 100644 --- a/operators/utility/delaywhen.md +++ b/operators/utility/delaywhen.md @@ -4,7 +4,7 @@ ## Delay emitted values determined by provided function. -
+ ### Examples @@ -33,12 +33,11 @@ const subscribe = delayWhenExample.subscribe(val => console.log(val)); ### Additional Resources -- [delayWhen](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-delayWhen) - :newspaper: - Official docs +- [delayWhen](https://rxjs.dev/api/operators/delayWhen) 📰 - Official docs - [Transformation operator: delay and delayWhen](https://egghead.io/lessons/rxjs-transformation-operators-delay-and-delaywhen?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/delayWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/delayWhen.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/delayWhen.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/delayWhen.ts) diff --git a/operators/utility/dematerialize.md b/operators/utility/dematerialize.md index 29b173f0..7bc89d28 100644 --- a/operators/utility/dematerialize.md +++ b/operators/utility/dematerialize.md @@ -4,18 +4,22 @@ ## Turn notification objects into notification values. -
+[![UltimateRxJS][uc-image]][uc-url] + +[uc-image]: + https://ultimatecourses.com/static/banners/banner-rxjs.svg +[uc-url]: https://ultimatecourses.com/courses/rxjs?ref=4 ### Examples -##### Example 1: Converting notifications to values +**Example 1: Converting notifications to values** ( [StackBlitz](https://stackblitz.com/edit/typescript-bxdwbg?file=index.ts&devtoolsheight=100) | [jsBin](http://jsbin.com/vafedocibi/1/edit?js,console) | [jsFiddle](https://jsfiddle.net/btroncone/jw08mouy/) ) -```js +```javascript // RxJS v6+ import { from, Notification } from 'rxjs'; import { dematerialize } from 'rxjs/operators'; @@ -38,10 +42,8 @@ const subscription = source.subscribe({ ### Additional Resources -- [dematerialize](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-dematerialize) - :newspaper: - Official docs - ---- +- [dematerialize](https://rxjs.dev/api/operators/dematerialize) 📰 - Official + docs -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/demterialize.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/dematerialize.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/demterialize.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/dematerialize.ts) diff --git a/operators/utility/do.md b/operators/utility/do.md index d70327a4..e3b3276c 100644 --- a/operators/utility/do.md +++ b/operators/utility/do.md @@ -1,20 +1,30 @@ -# do / tap +# tap / do -#### signature: `do(nextOrObserver: function, error: function, complete: function): Observable` +#### signature: `tap(nextOrObserver: function, error: function, complete: function): Observable` ## Transparently perform actions or side-effects, such as logging. --- -:bulb: If you are using as a pipeable operator, `do` is known as `tap`! +💡 If you are using and old version of RxJS, `tap` used to be known as `do`! --- -
+### Why use `tap`? + +Think of `tap` as a surveillance camera in a shopping mall. It doesn't interfere with the shoppers (values) moving around but merely observes and records their actions. This operator is best for side effects: actions you want to take in response to values in an observable, without affecting the values themselves. + +One of the superpowers of `tap` is its utility in debugging. **When things aren't going as planned with your observable**, instead of tearing apart your chain or inserting numerous logs, simply sprinkle in some `tap` operators. It's like adding checkpoints in a video game, helping you swiftly pinpoint issues without disrupting the main flow. + +However, a word of caution: **remember that `tap` is solely for side effects**. If you find yourself tempted to modify data within a `tap`, it's generally best to resist. That's not its purpose, and you're better off with [`map`](../transformation/map.md) or other transformational operators in these cases. + +Lastly, it's best to ensure that the side effects you introduce via `tap` are not critical to the main logic of your observable chain, keeping them non-intrusive and harmless. + + ### Examples -##### Example 1: Logging with do +##### Example 1: Logging with tap ( [StackBlitz](https://stackblitz.com/edit/typescript-cd2gjp?file=index.ts&devtoolsheight=100) @@ -27,28 +37,81 @@ import { of } from 'rxjs'; import { tap, map } from 'rxjs/operators'; const source = of(1, 2, 3, 4, 5); -//transparently log values from source with 'do' +// transparently log values from source with 'tap' const example = source.pipe( tap(val => console.log(`BEFORE MAP: ${val}`)), map(val => val + 10), tap(val => console.log(`AFTER MAP: ${val}`)) ); -//'do' does not transform values +//'tap' does not transform values //output: 11...12...13...14...15 const subscribe = example.subscribe(val => console.log(val)); ``` +##### Example 2: Using tap with object + +( +[StackBlitz](https://stackblitz.com/edit/typescript-3xykpb?file=index.ts&devtoolsheight=100)) + +```js +// RxJS v6+ +import { of } from 'rxjs'; +import { tap, map } from 'rxjs/operators'; + +const source = of(1, 2, 3, 4, 5); + +// tap also accepts an object map to log next, error, and complete +const example = source + .pipe( + map(val => val + 10), + tap({ + next: val => { + // on next 11, etc. + console.log('on next', val); + }, + error: error => { + console.log('on error', error.message); + }, + complete: () => console.log('on complete') + }) + ) + // output: 11, 12, 13, 14, 15 + .subscribe(val => console.log(val)); +``` + +### Related Recipes + +- [Battleship Game](../../recipes/battleship-game.md) +- [Breakout Game](../../recipes/breakout-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Catch The Dot Game](../../recipes/catch-the-dot-game.md) +- [Click Ninja Game](../../recipes/click-ninja-game.md) +- [Flappy Bird Game](../../recipes/flappy-bird-game.md) +- [Horizontal Scroll Indicator](../../recipes/horizontal-scroll-indicator.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Memory Game](../../recipes/memory-game.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Platform Jumper Game](../../recipes/platform-jumper-game.md) +- [Save Indicator](../../recipes/save-indicator.md) +- [Space Invaders Game](/recipes/space-invaders-game.md) +- [Stop Watch](../../recipes/stop-watch.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) +- [Tank Battle Game](../../recipes/tank-battle-game.md) +- [Tetris Game](../../recipes/tetris-game.md) +- [Type Ahead](../../recipes/type-ahead.md) +- [Uncover Image Game](../../recipes/uncover-image-game.md) + ### Additional Resources -- [do](http://reactivex.io/documentation/operators/do.html) :newspaper: - - Official docs +- [tap](https://rxjs.dev/api/operators/tap) 📰 - Official docs - [Logging a stream with do](https://egghead.io/lessons/rxjs-logging-a-stream-with-do?course=step-by-step-async-javascript-with-rxjs) - :video_camera: :dollar: - John Linquist + 🎥 💵 - John Linquist - [Utility operator: do](https://egghead.io/lessons/rxjs-utility-operator-do?course=rxjs-beyond-the-basics-operators-in-depth) - :video_camera: :dollar: - André Staltz + 🎥 💵 - André Staltz + --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/do.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/do.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/tap.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/tap.ts) diff --git a/operators/utility/finalize.md b/operators/utility/finalize.md index e82e03fc..62ec24b1 100644 --- a/operators/utility/finalize.md +++ b/operators/utility/finalize.md @@ -4,18 +4,42 @@ ## Call a function when observable completes or errors -### [ Examples Coming Soon! ] +### Examples + +##### Example 1: Execute callback function when the observable completes + +( [StackBlitz](https://stackblitz.com/edit/typescript-ohddud) ) + +```js +import { interval } from 'rxjs'; +import { take, finalize } from 'rxjs/operators'; + +//emit value in sequence every 1 second +const source = interval(1000); +//output: 0,1,2,3,4,5.... +const example = source.pipe( + take(5), //take only the first 5 values + finalize(() => console.log('Sequence complete')) // Execute when the observable completes +) +const subscribe = example.subscribe(val => console.log(val)); +``` ### Related Recipes -* [HTTP Polling](../../recipes/http-polling.md) +- [Battleship Game](../../recipes/battleship-game.md) +- [Car Racing Game](../../recipes/car-racing-game.md) +- [Click Ninja Game](../../recipes/click-ninja-game.md) +- [HTTP Polling](../../recipes/http-polling.md) +- [Mine Sweeper Game](../../recipes/mine-sweeper-game.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) +- [Tetris Game](../../recipes/tetris-game.md) ### Additional Resources -* [finalize](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-finalize) - :newspaper: - Official docs +* [finalize](https://rxjs.dev/api/operators/finalize) + 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/finalize.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/finalize.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/finalize.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/finalize.ts) diff --git a/operators/utility/let.md b/operators/utility/let.md index 39ac5997..bd00485a 100644 --- a/operators/utility/let.md +++ b/operators/utility/let.md @@ -6,12 +6,12 @@ --- -:warning: `let` is no longer available, or necessary, with pipeable operators! -(RxJS 5.5+) +⚠ `let` is no longer available, or necessary, with pipeable operators! (RxJS +5.5+) --- -
+ ### Examples @@ -117,9 +117,9 @@ const subscribe = obsArrayPlusYourOperators(addTenThenTwenty) ### Additional Resources - [let](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/let.md) - :newspaper: - Official docs + 📰 - Official docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/let.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/let.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/let.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/let.ts) diff --git a/operators/utility/repeat.md b/operators/utility/repeat.md new file mode 100644 index 00000000..08437451 --- /dev/null +++ b/operators/utility/repeat.md @@ -0,0 +1,50 @@ +# repeat + +#### signature: `repeat(count: number): Observable` + +## Repeats an observable on completion. + +--- + +💡 Like [`retry`](../error_handling/retry.md) but for non error cases! + +--- + + + +### Examples + +##### Example 1: Repeat 3 times + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-repeat-learnrxjs?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { repeat, delay } from 'rxjs/operators'; +import { of } from 'rxjs'; + +const delayedThing = of('delayed value').pipe(delay(2000)); + +delayedThing + .pipe(repeat(3)) + // delayed value...delayed value...delayed value + .subscribe(console.log); +``` + +### Related Recipes + +- [Click Ninja Game](../../recipes/click-ninja-game.md) +- [Lockscreen](../../recipes/lockscreen.md) +- [Space Invaders Game](/recipes/space-invaders-game.md) +- [Swipe To Refresh](/recipes/swipe-to-refresh.md) + +### Additional Resources + +- [repeat](https://rxjs.dev/api/operators/repeat) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/repeat.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/repeat.ts) diff --git a/operators/utility/repeatwhen.md b/operators/utility/repeatwhen.md new file mode 100644 index 00000000..e2b77921 --- /dev/null +++ b/operators/utility/repeatwhen.md @@ -0,0 +1,52 @@ +# repeatWhen + +#### signature: `repeatWhen(notifier: (notifications: Observable) => Observable): Observable` + +## Repeat an observable on completion based on custom criteria. + +--- + +💡 If you just want to repeat a specified number of times, try +[retry](retry.md)! + +--- + + + +### Examples + +##### Example 1: Repeat on interval + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-repeatwhen?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { of, interval } from 'rxjs'; +import { repeatWhen, take } from 'rxjs/operators'; + +const repeatInterval$ = interval(1000).pipe(take(5)); +const source$ = of('hey!').pipe(repeatWhen(_ => repeatInterval$)); + +source$.subscribe(console.log); + +/* +OUTPUT: +hey! +hey! +hey! +hey! +hey! +hey! +*/ +``` + +### Additional Resources + +- [repeatWhen](https://rxjs.dev/api/operators/repeatWhen) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/6.5.2/packages/rxjs/src/internal/operators/repeatWhen.ts](https://github.com/ReactiveX/rxjs/blob/6.5.2/packages/rxjs/src/internal/operators/repeatWhen.ts) diff --git a/operators/utility/timeinterval.md b/operators/utility/timeinterval.md new file mode 100644 index 00000000..9799e568 --- /dev/null +++ b/operators/utility/timeinterval.md @@ -0,0 +1,41 @@ +# timeInterval + +#### signature: `timeInterval(scheduler: *): Observable> | WebSocketSubject | Observable` + +## Convert an Observable that emits items into one that emits indications of the amount of time elapsed between those emissions + + + +### Examples + +##### Example 1: Time between mouse clicks + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-time-interval?file=index.ts&devtoolsheight=50) +) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { timeInterval, tap } from 'rxjs/operators'; + +fromEvent(document, 'mousedown') + .pipe(timeInterval(), tap(console.log)) + .subscribe( + i => + (document.body.innerText = `milliseconds since last click: ${i.interval}`) + ); +``` + +### Related Recipes + +- [Click Ninja Game](../../recipes/click-ninja-game.md) + +### Additional Resources + +- [timeInterval](https://rxjs.dev/api/operators/timeInterval) 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/timeInterval.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/timeInterval.ts) diff --git a/operators/utility/timeout.md b/operators/utility/timeout.md index 593d21a9..7ddec764 100644 --- a/operators/utility/timeout.md +++ b/operators/utility/timeout.md @@ -4,16 +4,14 @@ ## Error if no value is emitted before specified duration -
+ ### Examples ##### Example 1: Timeout after 2.5 seconds ( -[StackBlitz](https://stackblitz.com/edit/typescript-eegqyz?file=index.ts&devtoolsheight=100) -| [jsBin](http://jsbin.com/gonakiniho/edit?js,console) | -[jsFiddle](https://jsfiddle.net/btroncone/nr4e1ofy/1/) ) +[StackBlitz](https://stackblitz.com/edit/typescript-eegqyz?file=index.ts&devtoolsheight=100) ) ```js // RxJS v6+ @@ -35,19 +33,18 @@ of(4000, 3000, 2000) ) ) /* - * "Request timed out after: 4000" - * "Request timed out after: 3000" - * "Request Complete!" - */ + * "Request timed out after: 4000" + * "Request timed out after: 3000" + * "Request Complete!" + */ .subscribe(val => console.log(val)); ``` ### Additional Resources -- [timeout](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/timeout.md) - :newspaper: - Official Docs +- [timeout](https://rxjs.dev/api/operators/timeout) 📰 - Official Docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/timeout.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/timeout.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/timeout.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/timeout.ts) diff --git a/operators/utility/timeoutwith.md b/operators/utility/timeoutwith.md new file mode 100644 index 00000000..e215f05e --- /dev/null +++ b/operators/utility/timeoutwith.md @@ -0,0 +1,50 @@ +# timeoutWith + +#### signature: `timeoutWith(due: number | Date, withObservable: ObservableInput, scheduler: SchedulerLike = async): OperatorFunction` + +## Subscribe to second Observable if no emission occurs in given time span. + + + +### Examples + +##### Example 1: Timeout after 1 second + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-timeoutwith?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { of } from 'rxjs'; +import { timeoutWith, delay, concatMap } from 'rxjs/operators'; + +const fakeRequest = delayTime => of('!response!').pipe(delay(delayTime)); +const requestTimeoutLogger = of('logging request timeout'); +const timeoutThreshold = 1000; + +of(timeoutThreshold + 1, timeoutThreshold - 1, timeoutThreshold + 3) + .pipe( + concatMap(e => + fakeRequest(e).pipe(timeoutWith(timeoutThreshold, requestTimeoutLogger)) + ) + ) + .subscribe(console.log); + +/* + OUTPUT: + logging request timeout + !response! + logging request timeout +*/ +``` + +### Additional Resources + +- [timeoutWith](https://rxjs-dev.firebaseapp.com/api/operators/timeoutWith) 📰 - + Official Docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/timeoutWith.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/timeoutWith.ts) diff --git a/operators/utility/topromise.md b/operators/utility/topromise.md index 1627da92..ac049ec4 100644 --- a/operators/utility/topromise.md +++ b/operators/utility/topromise.md @@ -6,11 +6,11 @@ --- -:warning: `toPromise` has been deprecated! (RxJS 5.5+) +⚠ `toPromise` is not a pipable operator, as it does not return an observable. --- -
+ ### Examples @@ -58,9 +58,9 @@ example().then(val => { ### Additional Resources - [toPromise](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/topromise.md) - :newspaper: - Official Docs + 📰 - Official Docs --- -> :file_folder: Source Code: -> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/toPromise.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/toPromise.ts) +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/toPromise.ts](https://github.com/ReactiveX/rxjs/blob/master/packages/rxjs/src/internal/operators/toPromise.ts) diff --git a/package.json b/package.json index 2b1de74a..e875b561 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,17 @@ "type": "git", "url": "git+https://github.com/btroncone/learn-rxjs.git" }, - "keywords": ["rxjs"], + "keywords": [ + "rxjs" + ], "author": "Brian Troncone ", "contributors": [ { "name": "Huy Le" + }, + { + "name": "Adam Lubek", + "url": "https://github.com/adamlubek" } ], "license": "MIT", diff --git a/recipes/README.md b/recipes/README.md index 91c3b5fc..532e195b 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -2,10 +2,30 @@ Common use-cases and interesting recipes to help learn RxJS. - ### Contents -* [Progress Bar](progressbar.md) -* [Smart Counter](smartcounter.md) -* [Game Loop](gameloop.md) -* [HTTP Polling](http-polling.md) - + +- [Alphabet Invasion Game](alphabet-invasion-game.md) +- [Battleship Game](battleship-game.md) +- [Breakout Game](breakout-game.md) +- [Car Racing Game](car-racing-game.md) +- [Catch The Dot Game](catch-the-dot-game.md) +- [Click Ninja Game](click-ninja-game.md) +- [Flappy Bird Game](flappy-bird-game.md) +- [Game Loop](gameloop.md) +- [Horizontal Scroll Indicator](horizontal-scroll-indicator.md) +- [HTTP Polling](http-polling.md) +- [Lockscreen](lockscreen.md) +- [Matrix Digital Rain](matrix-digital-rain.md) +- [Memory Game](memory-game.md) +- [Mine Sweeper Game](mine-sweeper-game.md) +- [Platform Jumper Game](platform-jumper-game.md) +- [Progress Bar](progressbar.md) +- [Save Indicator](save-indicator.md) +- [Smart Counter](smartcounter.md) +- [Space Invaders Game](space-invaders-game.md) +- [Stop Watch](stop-watch.md) +- [Swipe To Refresh](swipe-to-refresh.md) +- [Tank Battle Game](tank-battle-game.md) +- [Tetris Game](tetris-game.md) +- [Type Ahead](type-ahead.md) +- [Uncover Image Game](uncover-image-game.md) diff --git a/recipes/alphabet-invasion-game.md b/recipes/alphabet-invasion-game.md new file mode 100644 index 00000000..99a82b44 --- /dev/null +++ b/recipes/alphabet-invasion-game.md @@ -0,0 +1,122 @@ +# Alphabet Invasion Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Alphabet Invasion Game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-alphabet-invasion?file=index.ts) +) + +![Alphabet Invasion](https://drive.google.com/uc?export=view&id=1huQHQFCmfdKPbh7ayjzJOOd1leVAY7Pi) + +#### index.ts + +```ts +// RxJS v6+ +import { interval, fromEvent, combineLatest, BehaviorSubject } from 'rxjs'; +import { scan, startWith, map, takeWhile, switchMap } from 'rxjs/operators'; +import { State, Letter, Letters } from './interfaces'; + +const randomLetter = () => + String.fromCharCode( + Math.random() * ('z'.charCodeAt(0) - 'a'.charCodeAt(0)) + 'a'.charCodeAt(0) + ); +const levelChangeThreshold = 20; +const speedAdjust = 50; +const endThreshold = 15; +const gameWidth = 30; + +const intervalSubject = new BehaviorSubject(600); + +const letters$ = intervalSubject.pipe( + switchMap(i => + interval(i).pipe( + scan + (letters => ({ + intrvl: i, + ltrs: [ + { + letter: randomLetter(), + yPos: Math.floor(Math.random() * gameWidth) + }, + ...letters.ltrs + ] + }), + { ltrs: [], intrvl: 0 }) + ) + ) +); + +const keys$ = fromEvent(document, 'keydown').pipe( + startWith({ key: '' }), + map((e: KeyboardEvent) => e.key) +); + +const renderGame = (state: State) => ( + (document.body.innerHTML = `Score: ${state.score}, Level: ${state.level}
`), + state.letters.forEach( + l => + (document.body.innerHTML += ' '.repeat(l.yPos) + l.letter + '
') + ), + (document.body.innerHTML += + '
'.repeat(endThreshold - state.letters.length - 1) + + '-'.repeat(gameWidth)) +); +const renderGameOver = () => (document.body.innerHTML += '
GAME OVER!'); +const noop = () => {}; + +const game$ = combineLatest(keys$, letters$).pipe( + scan<[string, Letters], State> + ((state, [key, letters]) => ( + letters.ltrs[letters.ltrs.length - 1] && + letters.ltrs[letters.ltrs.length - 1].letter === key + ? ((state.score = state.score + 1), letters.ltrs.pop()) + : noop, + state.score > 0 && state.score % levelChangeThreshold === 0 + ? ((letters.ltrs = []), + (state.level = state.level + 1), + (state.score = state.score + 1), + intervalSubject.next(letters.intrvl - speedAdjust)) + : noop, + { score: state.score, letters: letters.ltrs, level: state.level } + ), + { score: 0, letters: [], level: 1 }), + takeWhile(state => state.letters.length < endThreshold) +); + +game$.subscribe(renderGame, noop, renderGameOver); +``` + +#### interfaces.ts + +```js +export interface Letter { + letter: String; + yPos: number; +} +export interface Letters { + ltrs: Letter[]; + intrvl: number; +} +export interface State { + score: number; + letters: Letter[]; + level: number; +} +``` + +### Operators Used + +- [BehaviorSubject](../subjects/behaviorsubject.md) +- [combineLatest](../operators/combination/combinelatest.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeWhile](../operators/filtering/takewhile.md) diff --git a/recipes/battleship-game.md b/recipes/battleship-game.md new file mode 100644 index 00000000..d2ef0911 --- /dev/null +++ b/recipes/battleship-game.md @@ -0,0 +1,569 @@ +# Battleship Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates an RxJS implementation of Battleship Game where you +play against the computer. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-battleships?file=index.ts) ) + +![Battleship Game](https://drive.google.com/uc?export=view&id=1WawRG9DbBILOgmuHTulFXsWbxwC8lrX4) + +#### index.ts + +```js +// RxJS v6+ +import { concat, merge } from 'rxjs'; +import { switchMap, takeWhile, finalize } from 'rxjs/operators'; +import { NUMBER_OF_SHIP_PARTS } from './constants'; +import { displayGameOver, paintBoards$ } from './html-renderer'; +import { shots$, computerScore$, playerScore$, isNotGameOver } from './game'; +import { setup$, emptyBoards$ } from './setup'; +import { Boards } from './interfaces'; + +const game$ = emptyBoards$.pipe( + paintBoards$, + switchMap((boards: Boards) => + concat(setup$(boards), shots$(boards)).pipe( + takeWhile(isNotGameOver), + finalize(displayGameOver(computerScore$)) + ) + ) +); + +merge(game$, computerScore$, playerScore$).subscribe(); +``` + +#### setup.ts + +```js +import { concat, interval, of, fromEvent, pipe, noop } from 'rxjs'; +import { filter, map, scan, take, tap } from 'rxjs/operators'; +import { + GAME_SIZE, + NUMBER_OF_SHIP_PARTS, + EMPTY, + COMPUTER, + PLAYER +} from './constants'; +import { + paintBoards$, + computerScoreContainer, + playerScoreContainer +} from './html-renderer'; +import { random, validClicks$ } from './game'; +import { Boards } from './interfaces'; + +const isThereEnoughSpaceForNextMove = ( + board: number[][], + ship: number, + x: number, + y: number +) => { + const row = [...board[x]]; + row[y] = ship; + const col = board.map(r => r.filter((c, j) => j === y)[0]); + col[x] = ship; + + const shipStartInCol = col.indexOf(ship); + const shipEndInCol = col.lastIndexOf(ship); + const shipStartInRow = row.indexOf(ship); + const shipEndInRow = row.lastIndexOf(ship); + + const checkSpace = (arr, start, end) => { + const startIndex = arr.lastIndexOf( + (e, i) => e !== EMPTY && e !== ship && i < start + ); + const endIndex = arr.findIndex( + (e, i) => e !== EMPTY && e !== ship && i > end + ); + const room = arr.slice(startIndex + 1, endIndex); + return room.length >= ship; + }; + + return shipStartInCol !== shipEndInCol + ? checkSpace(col, shipStartInCol, shipEndInCol) + : shipStartInRow !== shipEndInRow + ? checkSpace(row, shipStartInRow, shipEndInRow) + : true; +}; + +const getTwoValidMoves = (row: number[], ship: number): [number, number] => [ + row.indexOf(ship) - 1, + row.lastIndexOf(ship) + 1 +]; + +const getValidMoves = ( + expectedPlayer: string, + boards: Boards, + ship: number, + [name, x, y] +): any[] => { + const board = boards[expectedPlayer]; + const rowIndex = board.findIndex(r => r.some(c => c === ship)); + if (!isThereEnoughSpaceForNextMove(board, ship, x, y)) { + return []; + } + if (rowIndex >= 0) { + const row = board[rowIndex]; + const colIndex = row.findIndex(e => e === ship); + + const isHorizontal = + row[colIndex - 1] === ship || row[colIndex + 1] === ship; + if (isHorizontal) { + const [left, right] = getTwoValidMoves(row, ship); + return [ + { x: rowIndex, y: left }, + { x: rowIndex, y: right } + ]; + } + + const isVertical = + (board[rowIndex - 1] ? board[rowIndex - 1][colIndex] === ship : false) || + (board[rowIndex + 1] ? board[rowIndex + 1][colIndex] === ship : false); + if (isVertical) { + const [up, down] = getTwoValidMoves( + board.map(r => r.filter((c, j) => j === colIndex)[0]), + ship + ); + return [ + { x: up, y: colIndex }, + { x: down, y: colIndex } + ]; + } + + return [ + { x: rowIndex, y: colIndex - 1 }, + { x: rowIndex, y: colIndex + 1 }, + { x: rowIndex - 1, y: colIndex }, + { x: rowIndex + 1, y: colIndex } + ]; + } + + return [{ x: x, y: y }]; +}; + +const isCellEmpty = (boards: Boards, [name, x, y]): boolean => + boards[name][x][y] === EMPTY; + +const areSpacesAroundCellEmpty = (boards: Boards, [name, x, y]): boolean => + (board => + (board[x - 1] && board[x - 1][y] === EMPTY) || + (board[x + 1] && board[x + 1][y] === EMPTY) || + board[x][y - 1] === EMPTY || + board[x][y + 1] === EMPTY)(boards[name]); + +const canMove = ( + expectedPlayer: string, + boards: Boards, + ship: number, + [name, x, y] +): boolean => { + if (!isCellEmpty(boards, [name, x, y]) || name !== expectedPlayer) { + return false; + } + + const validMoves = getValidMoves(expectedPlayer, boards, ship, [name, x, y]); + const isValidMove = validMoves.some(e => e.x === x && e.y === y); + + return isValidMove; +}; + +const addShips$ = (player: string, boards: Boards) => + pipe( + map((e: string) => e.split(',')), + filter(e => e.length === 3), + map(e => [e[0], parseInt(e[1]), parseInt(e[2])]), + scan( + (a, coords: any) => ( + (a.validMove = + a.shipPartsLeft > 0 + ? canMove(player, boards, a.ship, coords) + : isCellEmpty(boards, coords) && + (a.ship - 1 === 1 || areSpacesAroundCellEmpty(boards, coords))), + a.validMove + ? a.shipPartsLeft > 0 + ? (a.shipPartsLeft -= 1) + : ((a.ship = a.ship - 1), (a.shipPartsLeft = a.ship - 1)) + : noop, + (a.coords = coords), + a + ), + { ship: 5, shipPartsLeft: 5, coords: [], validMove: true } + ), + filter(({ validMove }) => validMove), + map( + ({ ship, coords }) => ( + (boards[player][coords[1]][coords[2]] = ship), boards + ) + ), + paintBoards$, + take(NUMBER_OF_SHIP_PARTS) + ); + +const playerSetup$ = (boards: Boards) => + fromEvent(document, 'click').pipe(validClicks$, addShips$(PLAYER, boards)); + +const computerSetup$ = (boards: Boards) => + interval().pipe( + tap(i => (i % 70 === 0 ? (playerScoreContainer.innerHTML += '.') : noop)), + map(_ => `${COMPUTER}, ${random()}, ${random()}`), + addShips$(COMPUTER, boards) + ); + +const info$ = (container: HTMLElement, text: string) => + of({}).pipe(tap(_ => (container.innerHTML = text))); + +const createBoard = () => + Array(GAME_SIZE) + .fill(EMPTY) + .map(_ => Array(GAME_SIZE).fill(EMPTY)); + +export const emptyBoards$ = of({ + [PLAYER]: createBoard(), + [COMPUTER]: createBoard() +}); + +export const setup$ = (boards: Boards) => + concat( + info$(computerScoreContainer, 'Setup your board!!!'), + playerSetup$(boards), + info$(playerScoreContainer, 'Computer setting up!!!'), + computerSetup$(boards) + ); +``` + +#### game.ts + +```js +import { fromEvent, pipe, noop, Subject, BehaviorSubject, merge } from 'rxjs'; +import { repeatWhen, delay, filter, map, takeWhile, tap } from 'rxjs/operators'; +import { + GAME_SIZE, + EMPTY, + MISS, + HIT, + SHORTEST_SHIP, + LONGEST_SHIP, + PLAYER, + COMPUTER, + NUMBER_OF_SHIP_PARTS +} from './constants'; +import { paintBoards, paintScores } from './html-renderer'; +import { Boards, ComputerMove } from './interfaces'; + +export const random = () => Math.floor(Math.random() * Math.floor(GAME_SIZE)); + +export const validClicks$ = pipe( + map((e: MouseEvent) => e.target['id']), + filter(e => e) +); + +const playerMove = new Subject(); +const computerMove = new BehaviorSubject({ playerBoard: [], hits: {} }); + +const shot = ( + boards: Boards, + player: string, + x: number, + y: number +): [number, number, boolean, number] => + ((boardValue): [number, number, boolean, number] => ( + (boards[player][x][y] = boardValue === EMPTY ? MISS : HIT), + [x, y, boards[player][x][y] === HIT, boardValue] + ))(boards[player][x][y]); + +const isValidMove = (boards: Boards, player, x, y): boolean => + boards[player][x][y] !== HIT && boards[player][x][y] !== MISS; + +const performShot$ = ( + boards: Boards, + player: string, + nextMove: (x, y, wasHit, boardValue) => void +) => + pipe( + tap(([player, x, y]) => + !isValidMove(boards, player, x, y) + ? nextMove(x, y, true, boards[player][x][y]) + : noop + ), + filter(([player, x, y]) => isValidMove(boards, player, x, y)), + map(([_, x, y]) => shot(boards, player, x, y)), + tap( + ([x, y, wasHit, boardValue]) => ( + paintBoards(boards), + nextMove(x, y, wasHit, boardValue), + paintScores(computerScore$, playerScore$) + ) + ) + ); + +const computerHits = ( + playerBoard: number[][], + x: number, + y: number, + wasHit: boolean, + boardValue: number +): ComputerMove => { + if ([EMPTY, HIT, MISS].some(e => e === boardValue)) { + return computerMove.value; + } + if (!computerMove.value.hits[boardValue]) { + computerMove.value.hits[boardValue] = []; + } + computerMove.value.hits[boardValue].push({ x, y }); + computerMove.value.playerBoard = playerBoard; + + return computerMove.value; +}; + +const nextComputerMove = (): [string, number, number] => { + const hits = computerMove.value.hits; + const shipToPursue = Object.keys(hits).find( + e => hits[e].length !== parseInt(e) + ); + if (!shipToPursue) { + return [PLAYER, random(), random()]; + } + + const playerBoard = computerMove.value.playerBoard; + const shipHits = hits[shipToPursue]; + if (shipHits.length === 1) { + const hit = shipHits[0]; + + const shotCandidates = [ + [hit.x, hit.y - 1], + [hit.x, hit.y + 1], + [hit.x - 1, hit.y], + [hit.x + 1, hit.y] + ].filter( + ([x, y]) => + playerBoard[x] && + playerBoard[x][y] !== undefined && + playerBoard[x][y] !== MISS && + playerBoard[x][y] !== HIT + ); + + return [PLAYER, shotCandidates[0][0], shotCandidates[0][1]]; + } + + const getOrderedHits = key => + (orderedHits => [orderedHits[0], orderedHits[orderedHits.length - 1]])( + shipHits.sort((h1, h2) => (h1[key] > h2[key] ? 1 : -1)) + ); + const isHorizontal = shipHits.every(e => e.x === shipHits[0].x); + + if (isHorizontal) { + const [min, max] = getOrderedHits('y'); + return [ + PLAYER, + min.x, + playerBoard[min.x][min.y - 1] !== undefined && + playerBoard[min.x][min.y - 1] !== HIT && + playerBoard[min.x][min.y - 1] !== MISS + ? min.y - 1 + : max.y + 1 + ]; + } + + const [min, max] = getOrderedHits('x'); + return [ + PLAYER, + playerBoard[min.x - 1] !== undefined && + playerBoard[min.x - 1][min.y] !== HIT && + playerBoard[min.x - 1][min.y] !== MISS + ? min.x - 1 + : max.x + 1, + min.y + ]; +}; + +const initialScore = () => ({ + score: 0, + ships: { 5: 5, 4: 4, 3: 3, 2: 2, 1: 1 } +}); +export const playerScore$ = new BehaviorSubject(initialScore()); +export const computerScore$ = new BehaviorSubject(initialScore()); +export const isNotGameOver = _ => + computerScore$.value.score < NUMBER_OF_SHIP_PARTS && + playerScore$.value.score < NUMBER_OF_SHIP_PARTS; + +const scoreChange = (subject: BehaviorSubject, boardValue: number) => + boardValue >= SHORTEST_SHIP && boardValue <= LONGEST_SHIP + ? ((subject.value.ships[boardValue] -= 1), + subject.next({ + score: subject.value.score + 1, + ships: subject.value.ships + })) + : noop; + +const computerShot$ = (boards: Boards) => + computerMove.pipe( + delay(200), + map(_ => nextComputerMove()), + performShot$(boards, PLAYER, (x, y, wasHit, boardValue) => + wasHit + ? (scoreChange(computerScore$, boardValue), + computerMove.next( + computerHits(boards[PLAYER], x, y, wasHit, boardValue) + )) + : playerMove.next() + ) + ); + +const playerShot$ = (boards: Boards) => + fromEvent(document, 'click').pipe( + validClicks$, + map((click: string) => click.split(',')), + filter(([player]) => player === COMPUTER), + performShot$(boards, COMPUTER, (x, y, wasHit, boardValue) => + wasHit + ? scoreChange(playerScore$, boardValue) + : computerMove.next(computerMove.value) + ), + takeWhile(([x, y, wasHit]) => wasHit), + repeatWhen(_ => playerMove) + ); + +export const shots$ = (boards: Boards) => + merge(playerShot$(boards), computerShot$(boards)); +``` + +#### html-renderer.ts + +```js +import { BehaviorSubject, pipe } from "rxjs"; +import { tap } from "rxjs/operators"; +import { + NUMBER_OF_SHIP_PARTS, + EMPTY, + MISS, + HIT, + PLAYER, + COMPUTER +} from "./constants"; +import { Boards } from "./interfaces"; + +const byId = (id: string): HTMLElement => document.getElementById(id); +export const computerScoreContainer = byId("computer_score"); +export const playerScoreContainer = byId("player_score"); + +const playerCells = (cell: number): string | number => + cell !== EMPTY ? (cell === MISS ? "o" : cell === HIT ? "x" : cell) : "_"; +const computerCells = (cell: number): string | number => + cell === HIT || cell === MISS ? (cell === MISS ? "o" : "x") : "_"; + +export const paintBoard = ( + container: HTMLElement, + playerName: string, + board: number[][] +) => ( + (container.innerHTML = ""), + board.forEach((r, i) => + r.forEach( + (c, j) => + (container.innerHTML += ` +
+ ${playerName === PLAYER ? playerCells(c) : computerCells(c)} +
`), + (container.innerHTML += "
") + ) + ), + (container.innerHTML += "

") +); + +export const paintShipsInfo = (scoreSubject: BehaviorSubject) => + Object.keys(scoreSubject.value.ships).reduce( + (a, c) => ((a += `${c} : ${scoreSubject.value.ships[c]} | `), a), + "" + ); + +export const paintScores = ( + computerScore: BehaviorSubject, + playerScore: BehaviorSubject +) => + ((c: HTMLElement, p: HTMLElement) => ( + (c.innerHTML = ""), + (c.innerHTML += "Computer score: " + computerScore.value.score + "
"), + paintShipsInfo(computerScore), + (c.innerHTML += "Ships: " + paintShipsInfo(computerScore)), + (p.innerHTML = ""), + (p.innerHTML += "Player score: " + playerScore.value.score + "
"), + (p.innerHTML += "Ships: " + paintShipsInfo(playerScore)) + ))(computerScoreContainer, playerScoreContainer); + +export const paintBoards = (boards: Boards) => ( + paintBoard(byId("player_board"), PLAYER, boards[PLAYER]), + paintBoard(byId("computer_board"), COMPUTER, boards[COMPUTER]) +); + +export const paintBoards$ = pipe(tap(paintBoards)); + +export const displayGameOver = (computerScore: BehaviorSubject) => () => { + const gameOverText = `GAME OVER, + ${ + computerScore.value.score === NUMBER_OF_SHIP_PARTS + ? "Computer" + : "Player" + } + won`; + playerScoreContainer.innerHTML = gameOverText; + computerScoreContainer.innerHTML = gameOverText; +}; +``` + +#### interfaces.ts + +```js +export interface Boards { + player: [string, number[][]]; + computer: [string, number[][]]; +} + +export interface ComputerMove { + playerBoard: number[]; + hits: {}; +} +``` + +#### constants.ts + +```js +export const GAME_SIZE = 12; +export const NUMBER_OF_SHIP_PARTS = 15; +export const EMPTY = 0; +export const MISS = 8; +export const HIT = 9; +export const SHORTEST_SHIP = 1; +export const LONGEST_SHIP = 5; +export const PLAYER = 'p'; +export const COMPUTER = 'c'; +``` + +### Operators Used + +- [concat](../operators/combination/concat.md) +- [delay](../operators/utility/delay.md) +- [filter](../operators/filtering/filter.md) +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [merge](../operators/combination/merge.md) +- [of](../operators/creation/of.md) +- repeatWhen +- [scan](../operators/transformation/scan.md) +- [switchMap](../operators/transformation/switchmap.md) +- [take](../operators/filtering/take.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) + +### Subjects Used + +- [BehaviorSubject](../subjects/behaviorsubject.md) +- [Subject](../subjects/subject.md) diff --git a/recipes/breakout-game.md b/recipes/breakout-game.md new file mode 100644 index 00000000..c89072e8 --- /dev/null +++ b/recipes/breakout-game.md @@ -0,0 +1,168 @@ +# Breakout Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates an RxJS implementation of Breakout game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-breakout?file=index.ts) ) + +![Breakout Game](https://drive.google.com/uc?export=view&id=1unsdGI5UBZu9ECjFtA4t_hLl4l7CRBSE) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, of, interval, combineLatest, generate, noop } from 'rxjs'; +import { map, mergeMap, pluck, startWith, scan, toArray, takeWhile, tap } from 'rxjs/operators'; +import { gameSize } from './constants'; +import { Player, Ball, GameObject } from './interfaces'; +import { render } from './html-renderer'; + +const createGameObject = (x, y) => ({ x, y }); + +const player$ = combineLatest( + of({ ...createGameObject(gameSize - 2, (gameSize / 2) - 1), score: 0, lives: 3 }), + fromEvent(document, 'keyup').pipe(startWith({ code: '' }), pluck('code')) +).pipe( + map(([player, key]) => ( + key === 'ArrowLeft' + ? player.y -= 1 + : key === 'ArrowRight' + ? player.y += 1 + : noop + , player) + ) +) + +const ball$ = combineLatest( + of({ ...createGameObject(gameSize / 2, (gameSize - 3)), dirX: 1, dirY: 1 }), + interval(150) +).pipe( + map(([ball, _]: [Ball, number]) => ( + ball.dirX *= ball.x > 0 ? 1 : -1, + ball.dirY *= (ball.y > 0 && ball.y < gameSize - 1) ? 1 : -1, + ball.x += 1 * ball.dirX, + ball.y -= 1 * ball.dirY, + ball) + ) +) + +const bricks$ = generate(1, x => x < 8, x => x + 1) + .pipe( + mergeMap(r => generate(r % 2 === 0 ? 1 : 0, x => x < gameSize, x => x + 2) + .pipe(map(c => createGameObject(r, c))) + ), + toArray() + ) + +const processGameCollisions = (_, [player, ball, bricks]: [Player, Ball, GameObject[]]) + : [Player, Ball, GameObject[]] => ( + (collidingBrickIndex => collidingBrickIndex > -1 + ? (bricks.splice(collidingBrickIndex, 1), ball.dirX *= -1, player.score++) + : noop + )(bricks.findIndex(e => e.x === ball.x && e.y === ball.y)), + ball.dirX *= player.x === ball.x && player.y === ball.y ? -1 : 1, + ball.x > player.x ? (player.lives-- , ball.x = (gameSize / 2) - 3) : noop, + [player, ball, bricks] + ) + +combineLatest(player$, ball$, bricks$) + .pipe( + scan<[Player, Ball, GameObject[]], [Player, Ball, GameObject[]]>(processGameCollisions), + tap(render), + takeWhile(([player]: [Player, Ball, GameObject[]]) => player.lives > 0) + ).subscribe() +``` + +#### interfaces.ts + +```js +export interface GameObject { + x: number; + y: number; +} +export interface Player extends GameObject { + score: number; + lives: number; +} +export interface Ball extends GameObject { + dirX: number; + dirY: number; +} +``` + +#### constants.ts + +```js +export const gameSize = 20; +``` + +#### html-renderer.ts + +```js +import { gameSize } from './constants'; +import { Player, Ball, GameObject } from './interfaces'; + +const empty = 0; +const plyer = 1; +const bll = 2; +const brick = 3; + +const createElem = col => { + const elem = document.createElement('div'); + elem.classList.add('board'); + elem.style.display = 'inline-block'; + elem.style.marginLeft = '10px'; + elem.style.height = '6px'; + elem.style.width = '6px'; + elem.style['background-color'] = + col === empty + ? 'white' + : col === plyer + ? 'cornflowerblue' + : col === bll + ? 'gray' + : 'silver'; + elem.style['border-radius'] = col === bll ? '100%' : '0%'; + return elem; +}; + +export const render = ([player, ball, bricks]: [ + Player, + Ball, + GameObject[] +]) => { + const game = Array(gameSize) + .fill(0) + .map(e => Array(gameSize).fill(0)); + game[player.x][player.y] = plyer; + game[ball.x][ball.y] = bll; + bricks.forEach(b => (game[b.x][b.y] = brick)); + + document.body.innerHTML = `Score: ${player.score} Lives: ${player.lives}
`; + game.forEach(r => { + const rowContainer = document.createElement('div'); + r.forEach(c => rowContainer.appendChild(createElem(c))); + document.body.appendChild(rowContainer); + }); +}; +``` + +### Operators Used + +- [combineLatest](../operators/combination/combinelatest.md) +- [fromEvent](../operators/creation/fromevent.md) +- [generate](../operators/creation/generate.md) +- [interval](../operators/creation/interval.md) +- [mergeMap](../operators/transformation/mergemap.md) +- [of](../operators/creation/of.md) +- [pluck](../operators/transformation/pluck.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) +- [toArray](../operators/transformation/toarray.md) diff --git a/recipes/car-racing-game.md b/recipes/car-racing-game.md new file mode 100644 index 00000000..b1917afc --- /dev/null +++ b/recipes/car-racing-game.md @@ -0,0 +1,276 @@ +# Car Racing Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Car Racing game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-car-racing?file=index.html) ) + +![Car Racing](https://drive.google.com/uc?export=view&id=1geB1veKhXmqEeTqy5h23Lyo6dgsRRPjH) + +#### index.ts + +```js +// RxJS v6+ +import { + interval, + fromEvent, + combineLatest, + of, + BehaviorSubject, + noop +} from 'rxjs'; +import { + scan, + tap, + pluck, + startWith, + takeWhile, + finalize, + switchMap +} from 'rxjs/operators'; +import { Car, Road, Player, Game } from './interfaces'; +import { gameHeight, gameWidth, levelDuration } from './constants'; +import { updateState } from './state'; +import { render, renderGameOver } from './html-renderer'; + +const car = (x: number, y: number): Car => ({ x, y, scored: false }); +const randomCar = (): Car => + car(0, Math.floor(Math.random() * Math.floor(gameWidth))); +const gameSpeed$ = new BehaviorSubject(200); + +const road$ = gameSpeed$.pipe( + switchMap(i => + interval(i).pipe( + scan( + (road: Road, _: number): Road => ( + (road.cars = road.cars.filter(c => c.x < gameHeight - 1)), + road.cars[0].x === gameHeight / 2 + ? road.cars.push(randomCar()) + : noop, + road.cars.forEach(c => c.x++), + road + ), + { cars: [randomCar()] } + ) + ) + ) +); + +const keys$ = fromEvent(document, 'keyup').pipe( + startWith({ code: '' }), + pluck('code') +); + +const player$ = keys$.pipe( + scan( + (player: Player, key: string): Player => ( + (player.y += + key === 'ArrowLeft' && player.y > 0 + ? -1 + : key === 'ArrowRight' && player.y < gameWidth - 1 + ? 1 + : 0), + player + ), + { y: 0 } + ) +); + +const state$ = of({ + score: 1, + lives: 3, + level: 1, + duration: levelDuration, + interval: 200 +}); + +const isNotGameOver = ([state]: Game) => state.lives > 0; + +const game$ = combineLatest(state$, road$, player$).pipe( + scan(updateState(gameSpeed$)), + tap(render), + takeWhile(isNotGameOver), + finalize(renderGameOver) +); + +game$.subscribe(); +``` + +#### state.ts + +```js +import { BehaviorSubject, noop } from 'rxjs'; +import { Game } from './interfaces'; +import { gameHeight, gameWidth, levelDuration } from './constants'; + +const handleScoreIncrease = ([state, road, player]: Game) => + !road.cars[0].scored && + road.cars[0].y !== player.y && + road.cars[0].x === gameHeight - 1 + ? ((road.cars[0].scored = true), (state.score += 1)) + : noop; + +const handleCollision = ([state, road, player]: Game) => + road.cars[0].x === gameHeight - 1 && road.cars[0].y === player.y + ? (state.lives -= 1) + : noop; + +const updateSpeed = ([state]: Game, gameSpeed: BehaviorSubject) => ( + (state.duration -= 10), + state.duration < 0 + ? ((state.duration = levelDuration * state.level), + state.level++, + (state.interval -= state.interval > 60 ? 20 : 0), + gameSpeed.next(state.interval)) + : () => {} +); + +export const updateState = (gameSpeed: BehaviorSubject) => ( + _, + game: Game +) => ( + handleScoreIncrease(game), + handleCollision(game), + updateSpeed(game, gameSpeed), + game +); +``` + +#### html-renderer.ts + +```js +import { Game } from './interfaces'; +import { gameHeight, gameWidth, car, player } from './constants'; + +const createElem = (column: number) => + (elem => ( + (elem.style.display = 'inline-block'), + (elem.style.marginLeft = '3px'), + (elem.style.height = '12px'), + (elem.style.width = '6px'), + (elem.style.borderRadius = '40%'), + (elem.style['background-color'] = + column === car ? 'green' : column === player ? 'blue' : 'white'), + elem + ))(document.createElement('div')); + +export const render = ([state, road, playerPosition]: Game) => + (renderFrame => ( + road.cars.forEach(c => (renderFrame[c.x][c.y] = car)), + (document.getElementById( + 'game' + ).innerHTML = `Score: ${state.score} Lives: ${state.lives} Level: ${state.level}`), + (renderFrame[gameHeight - 1][playerPosition.y] = player), + renderFrame.forEach(r => { + const rowContainer = document.createElement('div'); + r.forEach(c => rowContainer.appendChild(createElem(c))); + document.getElementById('game').appendChild(rowContainer); + }) + ))( + Array(gameHeight) + .fill(0) + .map(e => Array(gameWidth).fill(0)) + ); + +export const renderGameOver = () => + (document.getElementById('game').innerHTML += '
GAME OVER!!!'); +``` + +#### interfaces.ts + +```js +export interface Car { + x: number; + y: number; + scored: boolean; +} + +export interface Road { + cars: Car[]; +} + +export interface State { + score: number; + lives: number; + level: number; + duration: number; + interval: number; +} + +export interface Player { + y: number; +} + +export type Game = [State, Road, Player]; +``` + +#### constants.ts + +```js +export const gameHeight = 10; +export const gameWidth = 6; + +export const levelDuration = 500; + +export const car = 1; +export const player = 2; +``` + +### index.html + +```html + + +
+
+
+
+
+
+
+
+
+
+``` + +### Operators Used + +- [BehaviorSubject](../subjects/behaviorsubject.md) +- [combineLatest](../operators/combination/combinelatest.md) +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [of](../operators/creation/of.md) +- [pluck](../operators/transformation/pluck.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/catch-the-dot-game.md b/recipes/catch-the-dot-game.md new file mode 100644 index 00000000..198efabc --- /dev/null +++ b/recipes/catch-the-dot-game.md @@ -0,0 +1,126 @@ +# Catch The Dot Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe shows usage of scan operator for state management in simple game + + + +### Example Code + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-catch-the-dot-game?file=index.ts) +) + +![Catch the dot](https://drive.google.com/uc?export=view&id=1VKje20yXoplC2MPgzxP-OykpvhDuv6el) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, interval } from 'rxjs'; +import { tap, scan, map, switchMap, takeWhile } from 'rxjs/operators'; +import { + dot, + updatedDot, + setTimerText, + resetDotSize, + moveDot +} from './dom-updater'; + +interface State { + score: number; + intrvl: number; +} +const makeInterval = (val: State) => + interval(val.intrvl).pipe( + map(v => 5 - v), + tap(setTimerText) + ); +const gameState: State = { score: 0, intrvl: 500 }; +const nextState = (acc: State) => ({ + score: (acc.score += 1), + intrvl: acc.score % 3 === 0 ? (acc.intrvl -= 50) : acc.intrvl +}); +const isNotGameOver = intervalValue => intervalValue >= 0; + +const game$ = fromEvent(dot, 'mouseover').pipe( + tap(moveDot), + scan < Event, + State > (nextState, gameState), + tap(state => updatedDot(state.score)), + switchMap(makeInterval), + tap(resetDotSize), + takeWhile(isNotGameOver) +); + +game$.subscribe( + n => {}, + e => {}, + () => setTimerText('ouch!') +); +``` + +#### dom-updater.ts + +```js +const random = () => Math.random() * 300; +const elem = id => document.getElementById(id); +const setElementText = (elem, text) => (elem.innerText = text.toString()); +const timer = elem('timer'); +const setDotSize = size => { + dot.style.height = `${size}px`; + dot.style.width = `${size}px`; +}; + +export const dot = elem('dot'); +export const updatedDot = score => { + if (score % 3 === 0) { + dot.style.backgroundColor = + '#' + ((Math.random() * 0xffffff) << 0).toString(16); + } + setElementText(dot, score); +}; +export const setTimerText = text => setElementText(timer, text); +export const moveDot = () => { + setDotSize(5); + dot.style.transform = `translate(${random()}px, ${random()}px)`; +}; +export const resetDotSize = () => setDotSize(30); +``` + +##### html + +``` + + +
+
+``` + +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [scan](../operators/transformation/scan.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/click-ninja-game.md b/recipes/click-ninja-game.md new file mode 100644 index 00000000..4fa5320e --- /dev/null +++ b/recipes/click-ninja-game.md @@ -0,0 +1,101 @@ +# Click Ninja Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe shows usage of time interval operator in a simple game + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-click-ninja?file=index.ts) ) + +![Click Ninja Game](https://drive.google.com/uc?export=view&id=1VT8umN-jtaqBfcKtlCwZ3w805qe3bXWN) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, TimeInterval } from 'rxjs'; +import { timeInterval, takeWhile, scan, tap, repeat, finalize } from 'rxjs/operators'; +import { render, clear } from './html-renderer'; + +interface State { + score: number; + interval: number; + threshold: number; +} + +fromEvent(document, 'mousedown').pipe( + timeInterval(), + scan, State>((state, timeInterval) => ({ + score: state.score + 1, + interval: timeInterval.interval, + threshold: state.threshold - 2 + }), { score: 0, interval: 0, threshold: 300 }), + takeWhile((state: State) => state.interval < state.threshold), + tap((state: State) => render(state.score, Math.floor(state.score / 10))), + finalize(clear), + repeat() +).subscribe(); +``` + +#### html-renderer.ts + +```js +const texts = { + 0: 'click, click', + 1: 'keep clicking', + 2: 'wow', + 3: 'not tired yet?!', + 4: 'click master!', + 5: 'inhuman!!!', + 6: 'ininhuman!!!' +}; + +const text = (score: number, level: number) => `${texts[level]} \n ${score}`; + +export const render = (score: number, level: number) => { + const id = 'level' + level; + const element = document.getElementById(id); + const innerText = text(score, level); + if (element) { + element.innerText = innerText; + } else { + const elem = document.createElement('div'); + elem.id = id; + elem.style.zIndex = `${level}`; + elem.style.position = 'absolute'; + elem.style.height = '150px'; + elem.style.width = '150px'; + elem.style.borderRadius = '10px'; + const position = level * 20; + elem.style.top = position + 'px'; + elem.style.left = position + 'px'; + const col = 100 + position; + elem.style.background = `rgb(0,${col},0)`; + elem.style.color = 'white'; + elem.innerText = innerText; + elem.style.textAlign = 'center'; + elem.style.verticalAlign = 'middle'; + elem.style.lineHeight = '90px'; + document.body.appendChild(elem); + } +}; + +export const clear = () => (document.body.innerText = ''); +``` + +##### html + +``` +
How fast can you click?!
+``` + +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [repeat](../operators/utility/repeat.md) +- [scan](../operators/transformation/scan.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) +- [timeInterval](../operators/utility/timeinterval.md) diff --git a/recipes/flappy-bird-game.md b/recipes/flappy-bird-game.md new file mode 100644 index 00000000..133cd394 --- /dev/null +++ b/recipes/flappy-bird-game.md @@ -0,0 +1,133 @@ +# Flappy Bird Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJs implementation of Flappy Bird like game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-flappy-bird?file=index.ts) ) + +![Flappy Bird](https://drive.google.com/uc?export=view&id=1NcV8nce0NfvqghyBr0gxLoPm_Y4zjzXI) + +#### index.ts + +```js +// RxJS v6+ +import { interval, merge, combineLatest, fromEvent } from 'rxjs'; +import { tap, scan, takeWhile } from 'rxjs/operators'; +import { paint } from './html-renderer'; + +const gamePipe = (x, y) => ({ x, y, checked: false }); +const gameSize = 10; +const createPipes = y => + (random => + Array.from(Array(gameSize).keys()) + .map(e => gamePipe(e, y)) + .filter(e => e.x < random || e.x > random + 2))( + Math.floor(Math.random() * Math.floor(gameSize)) + ); + +const gamePipes$ = interval(500).pipe( + scan < any, + any > + (acc => + (acc.length < 2 ? [...acc, createPipes(gameSize)] : acc) + .filter(c => c.some(e => e.y > 0)) + .map(cols => cols.map(e => gamePipe(e.x, e.y - 1))), + [createPipes(gameSize / 2), createPipes(gameSize)]) +); + +const fly = xPos => (xPos > 0 ? (xPos -= 1) : xPos); +const fall = xPos => (xPos < gameSize - 1 ? (xPos += 1) : gameSize - 1); +const bird$ = merge(interval(300), fromEvent(document, 'keydown')).pipe( + scan < any, + any > + ((xPos, curr) => (curr instanceof KeyboardEvent ? fly(xPos) : fall(xPos)), + gameSize - 1) +); + +const updateGame = (bird, pipes) => + (game => ( + pipes.forEach(col => col.forEach(v => (game[v.x][v.y] = 2))), + (game[bird][0] = 1), + game + ))( + Array(gameSize) + .fill(0) + .map(e => Array(gameSize).fill(0)) + ); + +const valueOnCollisionFor = pipes => ({ + when: predicate => + !pipes[0][0].checked && predicate ? ((pipes[0][0].checked = true), 1) : 0 +}); + +combineLatest(bird$, gamePipes$) + .pipe( + scan < any, + any > + ((state, [bird, pipes]) => ({ + bird: bird, + pipes: pipes, + lives: + state.lives - + valueOnCollisionFor(pipes).when( + pipes.some(c => c.some(c => c.y === 0 && c.x === bird)) + ), + score: + state.score + valueOnCollisionFor(pipes).when(pipes[0][0].y === 0) + }), + { lives: 3, score: 0, bird: 0, pipes: [] }), + tap(state => + paint(updateGame(state.bird, state.pipes), state.lives, state.score) + ), + takeWhile(state => state.lives > 0) + ) + .subscribe(); +``` + +#### html-renderer.ts + +```js +const createElem = col => { + const elem = document.createElement('div'); + elem.classList.add('board'); + elem.style.display = 'inline-block'; + elem.style.marginLeft = '10px'; + elem.style.height = '6px'; + elem.style.width = '6px'; + elem.style['background-color'] = + col === 0 + ? 'white' + : col === 1 + ? 'cornflowerblue' + : col === 2 + ? 'gray' + : 'silver'; + elem.style['border-radius'] = '90%'; + return elem; +}; + +export const paint = (game: number[][], lives, score) => { + document.body.innerHTML = `Lives: ${lives}, Score: ${score}`; + + game.forEach(row => { + const rowContainer = document.createElement('div'); + row.forEach(col => rowContainer.appendChild(createElem(col))); + document.body.appendChild(rowContainer); + }); +}; +``` + +### Operators Used + +- [combineLatest](../operators/combination/combinelatest.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [merge](../operators/combination/merge.md) +- [scan](../operators/transformation/scan.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/gameloop.md b/recipes/gameloop.md index e9acab39..a6fee80d 100644 --- a/recipes/gameloop.md +++ b/recipes/gameloop.md @@ -9,7 +9,7 @@ a stream of frames and their deltaTimes since the previous frames. Combined with this is a stream of user inputs, and the current gameState, which we can use to update our objects, and render to to the screen on each frame emission. -
+ ### Example Code @@ -17,6 +17,8 @@ update our objects, and render to to the screen on each frame emission. [StackBlitz](https://stackblitz.com/edit/rxjs-5-based-game-loop-jlhyfx?file=index.ts&devtoolsheight=50) ) +![Game Loop](https://drive.google.com/uc?export=view&id=1d4j8fx_b_NTT89VhYAqaqaff46oZaPms) + ```js import { BehaviorSubject, Observable, of, fromEvent } from 'rxjs'; import { buffer, bufferCount, expand, filter, map, share, tap, withLatestFrom } from 'rxjs/operators'; @@ -88,7 +90,7 @@ const update = (deltaTime: number, state: any, inputState: any): any => { // Apply Velocity Movements obj.x = obj.x += obj.velocity.x*deltaTime; - obj.y = obj.y += obj.velocity.y*deltaTime; + obj.y = obj.y += obj.velocity.y*deltaTime; // Check if we exceeded our boundaries const didHit = runBoundaryCheck(obj, boundaries); @@ -125,7 +127,7 @@ const render = (state: any) => { state['objects'].forEach((obj) => { ctx.fillStyle = obj.color; ctx.fillRect(obj.x, obj.y, obj.width, obj.height); - }); + }); }; @@ -251,7 +253,8 @@ frames$

- Each time a block hits a wall, it gets faster. You can hit SPACE to pause the boxes. They will change colors to show they are paused. + Each time a block hits a wall, it gets faster. You can hit SPACE to pause the + boxes. They will change colors to show they are paused.

``` diff --git a/recipes/horizontal-scroll-indicator.md b/recipes/horizontal-scroll-indicator.md new file mode 100644 index 00000000..d9d57146 --- /dev/null +++ b/recipes/horizontal-scroll-indicator.md @@ -0,0 +1,61 @@ +# Horizontal scroll indicator + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates the creation of a horizontal scroll indicator. + + + +### Example Code + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-horizontal-scroll-indicator?file=index.ts) +) + +```js +// RxJS v6+ +import { fromEvent } from 'rxjs'; +import { throttleTime, tap } from 'rxjs/operators'; + +const scrollIndication = document.getElementById('indication'); +const getScrollWidth = () => { + const doc = document.documentElement; + // https://www.w3schools.com/howto/howto_js_scroll_indicator.asp + const winScroll = doc.scrollTop; + const height = doc.scrollHeight - doc.clientHeight; + + return (winScroll / height) * 100; +}; +const setScroll = _ => (scrollIndication.style.width = getScrollWidth() + '%'); + +fromEvent(document, 'scroll') + .pipe(throttleTime(20), tap(setScroll)) + .subscribe(); +``` + +##### html + +```html + + +
 
+Scroll down!!! +
Boom!
+``` + +### Operators Used + +- [fromEvent](../operators/creation/fromevent.md) +- [tap](../operators/utility/do.md) +- [throttleTime](../operators/filtering/throttletime.md) diff --git a/recipes/http-polling.md b/recipes/http-polling.md index 6b5d4440..12480206 100644 --- a/recipes/http-polling.md +++ b/recipes/http-polling.md @@ -1,5 +1,11 @@ # HTTP Polling + + +### Examples + +##### Example 1 + _By [@barryrowe](https://twitter.com/barryrowe)_ This recipe demonstrates one way you can achieve polling an HTTP endpoint on an @@ -7,14 +13,12 @@ interval. This is a common task in web applications, and one that RxJS tends to handle really well as the continuous series of HTTP requests and responses is easy to reason about as a stream of data. -
- -### Example Code - ( [StackBlitz](https://stackblitz.com/edit/rxjs-http-poll-recipe-jc5cj7?file=index.ts&devtoolsheight=50) ) +![HTTP Polling](https://drive.google.com/uc?export=view&id=1HwHApLDoxO9Zc5DAG3XtgJBl83CpXmjU) + ```js // Import stylesheets import './style.css'; @@ -178,3 +182,38 @@ fromEvent(startButton, 'click') - [mergeMap](../operators/transformation/mergemap.md) - [switchMap](../operators/transformation/switchmap.md) - [timer](../operators/creation/timer.md) + +##### Example 2: Simple http polling + +_By [@adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates polling an HTTP endpoint using repeat. It waits for 3 +seconds following the response to poll again. Code below is simplifed to +demonstrate bare bones of solution but link below contains verbose logging and +error handling. + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-http-polling?file=index.ts&devtoolsheight=80) +) + +```js +// RxJS v6+ +import { of } from 'rxjs'; +import { delay, tap, mergeMap, repeat } from 'rxjs/operators'; + +const fakeDelayedRequest = () => of(new Date()).pipe(delay(1000)); + +const display = response => { + document.open(); + document.write(response); +}; + +const poll = of({}).pipe( + mergeMap(_ => fakeDelayedRequest()), + tap(display), + delay(3000), + repeat() +); + +poll.subscribe(); +``` diff --git a/recipes/lockscreen.md b/recipes/lockscreen.md new file mode 100644 index 00000000..394e1439 --- /dev/null +++ b/recipes/lockscreen.md @@ -0,0 +1,239 @@ +# Lockscreen + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of lockscreen functionality (known +for example from smartphones). + + + +### Example Code + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-lockscreen?file=index.ts&devtoolsheight=30) +) + +![Lockscreen](https://drive.google.com/uc?export=view&id=1EknMWCVag08IuecwP4UppUR7fKn3qd-2) + +#### index.ts + +```js +/* + Use mouse to 'swipe' across the lock pad (hold mouse button and swipe :) ). + Pad will turn green if password is correct or red if password is incorrect. + You can set password to whatever sequence you like. +*/ +// RxJS v6+ +import { from, fromEvent, Subject, merge, pipe } from 'rxjs'; +import { + switchMap, + takeUntil, + repeat, + tap, + map, + throttleTime, + distinctUntilChanged, + filter, + toArray, + sequenceEqual, + pluck +} from 'rxjs/operators'; +import { + displaySelectedNumbersSoFar, + markTouchedPad, + pads, + resetPasswordPad, + setResult +} from './dom-updater'; + +const sub = new Subject(); +const expectedPasswordUpdate$ = fromEvent( + document.getElementById('expectedPassword'), + 'keyup' +).pipe( + map((e: any) => e.target.value), + tap(pass => sub.next(pass.split('').map(e => parseInt(e)))) +); +let expectedPassword = [1, 2, 5, 2]; +const expectedPassword$ = sub.pipe(tap((v: any) => (expectedPassword = v))); + +const takeMouseSwipe = pipe( + // take mouse moves + switchMap(_ => fromEvent(document, 'mousemove')), + // once mouse is up, we end swipe + takeUntil(fromEvent(document, 'mouseup')), + throttleTime(50) +); +const checkIfPasswordMatch = password => + from(password).pipe(sequenceEqual(from(expectedPassword))); +const getXYCoordsOfMousePosition = ({ clientX, clientY }: MouseEvent) => ({ + x: clientX, + y: clientY +}); +const findSelectedPad = v => + pads.find( + r => v.x > r.left && v.x < r.right && v.y > r.top && v.y < r.bottom + ); +const getIdOfSelectedPad = pipe( + filter(v => !!v), + pluck('id'), + distinctUntilChanged() +); + +const actualPassword$ = fromEvent(document, 'mousedown').pipe( + // new stream so reset password pad and take swipe until mouse up + tap(resetPasswordPad), + takeMouseSwipe, + // as we swipe, we mark pads as touched and display selected numbers + map(getXYCoordsOfMousePosition), + map(findSelectedPad), + getIdOfSelectedPad, + tap(markTouchedPad), + tap(displaySelectedNumbersSoFar), + // we need an array of numbers from current swipe which we can pass to checkIfPasswordMatch + toArray(), + // on mouse up (swipe end), switchMap to new stream to check if password match + switchMap(checkIfPasswordMatch), + tap(setResult), + // takeUntil inside takeMouseSwipe terminated stream so we repeat from beginning (mousedown) + repeat() +); + +merge(expectedPassword$, expectedPasswordUpdate$, actualPassword$).subscribe(); +``` + +#### dom-updater.ts + +```js +const createPadObject = (id, rectange) => ({ + id: id, + left: rectange.left, + right: rectange.right, + top: rectange.top, + bottom: rectange.bottom +}); + +const setResultText = text => + (document.getElementById('result').innerText = text); + +const setPasswordPads = color => + Array.from(document.querySelectorAll('.cell')).forEach( + (v: HTMLElement) => (v.style.background = color) + ); + +const getPad = id => document.getElementById(`c${id}`); + +export const pads = Array.from({ length: 9 }, (_, n) => n + 1).map(v => + createPadObject(v, getPad(v).getBoundingClientRect()) +); + +export const markTouchedPad = v => { + const pad = getPad(v); + pad.style.background = 'lightgrey'; + if (!pad.animate) return; //animate does not work in IE + const animation: any = [ + { transform: 'scale(0.9)' }, + { transform: 'scale(1)' } + ]; + const animationOptions = { + duration: 300, + iterations: 1 + }; + pad.animate(animation, animationOptions); + document.getSelection().removeAllRanges(); +}; + +export const setResult = result => { + setPasswordPads(result ? 'MediumSeaGreen' : 'IndianRed'); + setResultText('Password ' + (result ? 'matches :)' : 'does not match :(')); +}; + +export const displaySelectedNumbersSoFar = v => + (document.getElementById('result').textContent += v); + +export const resetPasswordPad = () => { + setResultText(''); + setPasswordPads('gray'); +}; +``` + +##### html + +```html + + +Expected Password: + +
+Password: +
+
+
1
+
2
+
3
+
+
+
4
+
5
+
6
+
+
+
7
+
8
+
9
+
+
+ +
+``` + +### Operators Used + +- [distinctUntilChanged](../operators/filtering/distinctuntilchanged.md) +- [filter](../operators/filtering/filter.md) +- [from](../operators/creation/from.md) +- [fromEvent](../operators/creation/fromevent.md) +- [map](../operators/transformation/map.md) +- [merge](../operators/combination/merge.md) +- [pluck](../operators/transformation/pluck.md) +- [repeat](../operators/utility/repeat.md) +- [sequenceEqual](../operators/conditional/sequenceequal.md) +- [Subject](../subjects/subject.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeUntil](../operators/filtering/takeuntil.md) +- [tap](../operators/utility/do.md) +- [throttleTime](../operators/filtering/throttletime.md) +- [toArray](../operators/transformation/toarray.md) diff --git a/recipes/matrix-digital-rain.md b/recipes/matrix-digital-rain.md new file mode 100644 index 00000000..8c5b1489 --- /dev/null +++ b/recipes/matrix-digital-rain.md @@ -0,0 +1,84 @@ +# Matrix Digital Rain + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Matrix Digital Rain. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-matrix?file=index.ts) ) + +![Matrix Digital Rain](https://drive.google.com/uc?export=view&id=1ZEyGvICTosGA58SVWq8kBK8mBWyEpk9T) + +#### index.ts + +```js +// RxJS v6+ +import { interval } from 'rxjs'; +import { scan } from 'rxjs/operators'; +import { render } from './html-renderer'; +import { markForRemoval, updateDrops, updateMatrix } from './matrix'; + +interval(300) + .pipe( + scan(matrix => ( + markForRemoval(matrix), + updateDrops(matrix), + updateMatrix(matrix) + ), []) + ).subscribe(render); +``` + +#### matrix.ts + +```js +const drop = (x: number, y: number) => ({ x, y, d: [], remove: false }); +const random = (max: number) => Math.floor(Math.random() * Math.floor(max)); +const ranodmChar = () => String.fromCharCode(random(128)); + +export const markForRemoval = matrix => + matrix.forEach( + drop => (drop.remove = drop.remove ? true : drop.d.length > 20) + ); +export const updateDrops = matrix => + matrix.forEach( + drop => + (drop.d = drop.remove + ? drop.d.slice(1).map(e => ranodmChar()) + : [ranodmChar(), ...drop.d.map(e => ranodmChar())]) + ); +export const updateMatrix = matrix => [ + ...matrix, + drop(random(window.innerHeight) / 4, random(window.innerWidth)) +]; +``` + +#### html-renderer.ts + +```js +const createElem = drop => { + const elem = document.createElement('div'); + elem.style.position = 'absolute'; + elem.style.marginTop = drop.x + 'px'; + elem.style.marginLeft = drop.y + 'px'; + elem.style.fontSize = '12px'; + elem.innerHTML = drop.d.reduce((acc, c) => (acc += '
' + c), ''); + elem.style['color'] = `rgb(21, ${100 + drop.d.length * 10}, 21)`; + return elem; +}; + +export const render = matrix => { + document.body.innerHTML = ''; + const container = document.createElement('div'); + container.style.position = 'relative'; + matrix.forEach(m => container.appendChild(createElem(m))); + document.body.appendChild(container); +}; +``` + +### Operators Used + +- [interval](../operators/creation/interval.md) +- [scan](../operators/transformation/scan.md) diff --git a/recipes/memory-game.md b/recipes/memory-game.md new file mode 100644 index 00000000..aac19fb7 --- /dev/null +++ b/recipes/memory-game.md @@ -0,0 +1,172 @@ +# Memory Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates an RxJS game to train your memory. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-memory-game?file=index.ts) ) + +![Memory Game](https://drive.google.com/uc?export=view&id=1IsizNEcko-aKcyb54ZuMis-gHs9UR9Ei) + +#### index.ts + +```js +// RxJS v6+ +import { EMPTY, from, fromEvent, generate, interval, merge, noop } from 'rxjs'; +import { + map, + pluck, + scan, + sequenceEqual, + switchMap, + take, + tap +} from 'rxjs/operators'; + +const random = (): number => Math.floor(Math.random() * Math.floor(8)); +const setInfo = (text: string) => + (document.getElementById('info').innerHTML = text); +const displayLevelChange = () => + document + .querySelectorAll('.child') + .forEach((c: HTMLElement) => (c.style.background = 'gray')); + +const checkIfGameOver$ = (randomSequence: number[]) => ( + userSequence: number[] +) => + from(userSequence).pipe( + sequenceEqual(from(randomSequence)), + tap(match => + !match && userSequence.length === randomSequence.length + ? setInfo('GAME OVER!') + : noop + ) + ); + +const takePlayerInput$ = (randomSequence: number[]) => _ => + fromEvent(document, 'click').pipe( + take(randomSequence.length), + scan( + (acc: number[], curr: MouseEvent) => [ + ...acc, + parseInt(curr.target['id']) + ], + [] + ), + switchMap(checkIfGameOver$(randomSequence)), + switchMap(result => + result + ? (displayLevelChange(), memoryGame$(randomSequence.length + 1)) + : EMPTY + ) + ); + +const showSequenceToMemorize$ = (memorySize: number) => ( + randomSequence: number[] +) => + interval(1000).pipe( + tap(i => + setInfo(i === memorySize - 1 ? `YOUR TURN` : `${memorySize - i} elements`) + ), + take(randomSequence.length), + map(index => randomSequence[index]), + tap(value => document.getElementById(`${value}`).click()), + switchMap(takePlayerInput$(randomSequence)) + ); + +const memoryGame$ = memorySize => + generate( + 1, + x => x <= memorySize, + x => x + 1 + ).pipe( + scan((acc: number[], _: number): number[] => [...acc, random() + 1], []), + switchMap(showSequenceToMemorize$(memorySize)) + ); + +const elementClick$ = (event: string, color: string) => + fromEvent(document.querySelectorAll('.child'), event).pipe( + pluck('srcElement'), + tap((e: HTMLElement) => (e.style.background = color)) + ); + +const clicks$ = merge( + elementClick$('click', 'lightgray'), + elementClick$('transitionend', 'white') +); + +const game$ = merge(clicks$, memoryGame$(2)); + +game$.subscribe(); +``` + +#### index.html + +```html + + +
Train Your Memory!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+``` + +### Operators Used + +- [empty](../operators/creation/empty.md) +- [from](../operators/creation/from.md) +- [fromEvent](../operators/creation/fromevent.md) +- [generate](../operators/creation/generate.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [merge](../operators/combination/merge.md) +- noop +- [pluck](../operators/transformation/pluck.md) +- [scan](../operators/transformation/scan.md) +- [sequenceEqual](../operators/conditional/sequenceequal.md) +- [switchMap](../operators/transformation/switchmap.md) +- [take](../operators/filtering/take.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/mine-sweeper-game.md b/recipes/mine-sweeper-game.md new file mode 100644 index 00000000..181ba827 --- /dev/null +++ b/recipes/mine-sweeper-game.md @@ -0,0 +1,150 @@ +# Mine Sweeper Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Mine Sweeper Game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-minesweeper?file=index.ts) ) + +![Mine Sweeper](https://drive.google.com/uc?export=view&id=18_2_QWnk5ImRT_dGaglKMvtWKa4xwC6B) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, of } from 'rxjs'; +import { + map, + tap, + filter, + pluck, + switchMap, + takeWhile, + finalize +} from 'rxjs/operators'; +import { renderMinefield, renderScore, renderGameOver } from './html-renderer'; +import { size, mine } from './constants'; +import { addMines, addMarks } from './mines'; + +const mines$ = of( + Array(size) + .fill(0) + .map(e => Array(size).fill(0)) +).pipe(map(addMines), map(addMarks), tap(renderMinefield)); + +const click$ = mines => + fromEvent(document, 'click').pipe( + map(({ clientX, clientY }: MouseEvent) => + document.elementFromPoint(clientX, clientY) + ), + filter(elem => elem.id !== ''), + tap(elem => + (val => ( + renderScore(val === mine || elem.innerHTML !== '_' ? 0 : val), + (elem.innerHTML = val) + ))(mines[elem.id[0]][elem.id[1]]) + ), + pluck('id'), + takeWhile(([x, y]) => mines[x][y] !== mine), + finalize(renderGameOver) + ); + +mines$.pipe(switchMap(click$)).subscribe(); +``` + +#### mines.ts + +```js +import { size, mine } from './constants'; + +const randomNumber = () => Math.floor(Math.random() * Math.floor(size)); + +export const addMines = arr => { + for (let i = 0; i < size / 2; i++) { + arr[randomNumber()][randomNumber()] = mine; + } + + return arr; +}; + +const mark = (arr, x, y) => + arr[x] !== undefined && arr[x][y] !== undefined + ? (arr[x][y] += arr[x][y] === mine ? 0 : 1) + : () => {}; + +export const addMarks = arr => { + for (let ri = 0; ri < size; ri++) { + for (let ci = 0; ci < size; ci++) { + if (arr[ri][ci] === mine) { + mark(arr, ri - 1, ci + 1); + mark(arr, ri - 1, ci); + mark(arr, ri - 1, ci - 1); + mark(arr, ri, ci + 1); + mark(arr, ri, ci - 1); + mark(arr, ri + 1, ci + 1); + mark(arr, ri + 1, ci); + mark(arr, ri + 1, ci - 1); + } + } + } + return arr; +}; +``` + +#### constants.ts + +```js +export const mine = 9; +export const size = 10; +``` + +#### html-renderer.ts + +```js +export const renderMinefield = arr => + arr.forEach((r, ri) => + (elem => + r.forEach( + (c, ci) => + (col => ( + (col.innerText = '_'), + (col.id = `${ri}${ci}`), + elem.appendChild(document.createTextNode('\u00A0\u00A0')), + elem.appendChild(col) + ))(document.createElement('span')), + document.body.appendChild(elem) + ))(document.createElement('div')) + ); + +export const renderScore = val => + (scoreElem => (scoreElem.innerText = parseInt(scoreElem.innerText) + val))( + document.getElementById('score') + ); + +export const renderGameOver = () => + (document.body.innerHTML += '
GAME OVER'); + +const addElem = decorator => + (elem => (decorator(elem), document.body.appendChild(elem)))( + document.createElement('span') + ); + +addElem(elem => (elem.innerText = 'Score: ')); +addElem(elem => ((elem.id = 'score'), (elem.innerText = '0'))); +``` + +### Operators Used + +- [filter](../operators/filtering/filter.md) +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [map](../operators/transformation/map.md) +- [of](../operators/creation/of.md) +- [pluck](../operators/transformation/pluck.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/platform-jumper-game.md b/recipes/platform-jumper-game.md new file mode 100644 index 00000000..32e5041a --- /dev/null +++ b/recipes/platform-jumper-game.md @@ -0,0 +1,232 @@ +# Platform Jumper Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Platform Jumper game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-platform-jumper?file=index.ts) ) + +![Platform Jumper](https://drive.google.com/uc?export=view&id=1poxaFwTtypxoTzOU13KcvE5fIcnzXog-) + +#### index.ts + +```js +// RxJS v6+ +import { interval, of, fromEvent, combineLatest } from 'rxjs'; +import { scan, tap, startWith, pluck, switchMap, takeWhile } from 'rxjs/operators'; +import { gameSize } from './constants'; +import { render } from './html-renderer'; +import { Player, Platform } from './interfaces'; +import { updatePlayer, updatePlatforms, initialPlatforms, initialPlayer, handleCollisions, handleKeypresses } from './game'; + +const gameSpeed = 500; + +const platforms$ = interval(gameSpeed) + .pipe( + scan(updatePlatforms, initialPlatforms) + ); + +const keys$ = (initialPlayer: Player) => fromEvent(document, 'keydown') + .pipe( + startWith({ key: '' }), + pluck('key'), + scan( + (plyr: Player, key: string) => handleKeypresses(plyr, key), + initialPlayer + ) + ); + +const player$ = of(initialPlayer()) + .pipe( + switchMap(p => combineLatest(interval(gameSpeed / 4), keys$(p)) + .pipe( + scan<[number, Player], Player>((_, [__, player]) => updatePlayer(player)) + )), + ); + +combineLatest(player$, platforms$) + .pipe( + scan<[Player, Platform[]], [Player, Platform[]]>( + (_, [player, platforms]) => handleCollisions([player, platforms])), + tap(render), + takeWhile(([player, platforms]) => player.lives > 0) + ) + .subscribe() +``` + +#### game.ts + +```js +import { Player, Platform } from './interfaces'; +import { gameSize } from './constants'; + +const newPlatform = (x, y): Platform => ({ x, y, scored: false }); +const newPlayer = (x, y, jumpValue, score, lives): Player => ({ + x, + y, + jumpValue, + canJump: false, + score: score, + lives: lives +}); +const startingY = 4; +export const initialPlayer = (): Player => newPlayer(0, startingY, 0, 0, 3); +export const initialPlatforms = [newPlatform(gameSize / 2, startingY)]; + +const random = y => { + let min = Math.ceil(y - 4); + let max = Math.floor(y + 4); + min = min < 0 ? 0 : min; + max = max > gameSize - 1 ? gameSize - 1 : max; + + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +export const updatePlatforms = (platforms: Platform[]): Platform[] => ( + platforms[platforms.length - 1].x > gameSize / 5 + ? platforms.push(newPlatform(1, random(platforms[platforms.length - 1].y))) + : () => {}, + platforms.filter(e => e.x < gameSize - 1).map(e => newPlatform(e.x + 1, e.y)) +); + +export const handleKeypresses = (player: Player, key: string) => + key === 'ArrowRight' + ? newPlayer( + player.x, + player.y + (player.y < gameSize - 1 ? 1 : 0), + player.jumpValue, + player.score, + player.lives + ) + : key === 'ArrowLeft' + ? newPlayer( + player.x, + player.y - (player.y > 0 ? 1 : 0), + player.jumpValue, + player.score, + player.lives + ) + : key === 'ArrowUp' + ? newPlayer( + player.x, + player.y, + player.x === gameSize - 1 || player.canJump ? 6 : 0, + player.score, + player.lives + ) + : player; + +export const updatePlayer = (player: Player): Player => ( + (player.jumpValue -= player.jumpValue > 0 ? 1 : 0), + (player.x -= player.x - 3 > 0 ? player.jumpValue : 0), + (player.x += player.x < gameSize - 1 ? 1 : 0), + player.x === gameSize - 1 ? ((player.lives -= 1), (player.x = 1)) : () => {}, + player +); + +const handleCollidingPlatform = ( + collidingPlatform: Platform, + player: Player +) => { + if (player.canJump) { + return; + } + + if (!collidingPlatform) { + player.canJump = false; + return; + } + + if (!collidingPlatform.scored) { + player.score += 1; + } + collidingPlatform.scored = true; + + player.canJump = true; +}; + +export const handleCollisions = ([player, platforms]: [Player, Platform[]]): [ + Player, + Platform[] +] => ( + handleCollidingPlatform( + platforms.find(p => p.x - 1 === player.x && p.y === player.y), + player + ), + (player.x = player.canJump + ? (collidingPlatforms => + collidingPlatforms.length + ? (platform => platform.x - 1)( + collidingPlatforms[collidingPlatforms.length - 1] + ) + : player.x)( + platforms.filter(p => p.y === player.y && p.x >= player.x) + ) + : player.x), + [player, platforms] +); +``` + +#### interfaces.ts + +```js +export interface Player { + x: number; + y: number; + jumpValue: number; + canJump: boolean; + score: number; + lives: number; +} + +export interface Platform { + x: number; + y: number; + scored: boolean; +} +``` + +#### constants.ts + +```js +export const gameSize = 20; +``` + +#### html-renderer.ts + +```js +import { gameSize } from './constants'; +import { Player, Platform } from './interfaces'; + +export const render = ([player, platforms]: [Player, Platform[]]) => { + document.body.innerHTML = `Lives: ${player.lives} Score: ${player.score}
`; + + const game = Array(gameSize) + .fill(0) + .map(_ => Array(gameSize).fill(0)); + game[player.x][player.y] = '*'; + platforms.forEach(p => (game[p.x][p.y] = '_')); + + game.forEach(r => { + r.forEach(c => (document.body.innerHTML += c === 0 ? '...' : c)); + document.body.innerHTML += '
'; + }); +}; +``` + +### Operators Used + +- [combineLatest](../operators/combination/combinelatest.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [of](../operators/creation/of.md) +- [pluck](../operators/transformation/pluck.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/progressbar.md b/recipes/progressbar.md index 9c360022..9ab82697 100644 --- a/recipes/progressbar.md +++ b/recipes/progressbar.md @@ -6,7 +6,7 @@ This recipe demonstrates the creation of an animated progress bar, simulating the management of multiple requests, and updating overall progress as each completes. -
+ ### Example Code @@ -14,6 +14,8 @@ completes. [StackBlitz](https://stackblitz.com/edit/rxjs-5-progress-bar-wxdxwe?file=index.ts&devtoolsheight=50) ) +![Progress Bar](https://drive.google.com/uc?export=view&id=18wsoRuVkjiQmhTDk8CgZo3BHTUTEyMqT) + ```js import './style.css'; @@ -57,7 +59,7 @@ const displayData = data => { updateContent(`
${data}
`); }; -// simulate 5 seperate requests that complete at variable length +// simulate 5 separate requests that complete at variable length const observables: Array> = [ requestOne, requestTwo, @@ -70,10 +72,7 @@ const array$ = from(observables); const requests$ = array$.pipe(concatAll()); const clicks$ = fromEvent(loadButton, 'click'); -const progress$ = clicks$.pipe( - switchMapTo(requests$), - share() -); +const progress$ = clicks$.pipe(switchMapTo(requests$), share()); const count$ = array$.pipe(count()); @@ -95,12 +94,10 @@ progress$.subscribe(displayData); -
- -
+
``` _Thanks to [@johnlinquist](https://twitter.com/johnlindquist) for the additional @@ -108,7 +105,7 @@ help with example!_ ### Operators Used -- [concatAll](../operators/transformation/concatall.md) +- [concatAll](../operators/combination/concatall.md) - [delay](../operators/utility/delay.md) - [fromEvent](../operators/creation/fromevent.md) - [from](../operators/creation/from.md) diff --git a/recipes/save-indicator.md b/recipes/save-indicator.md new file mode 100644 index 00000000..92c5f48f --- /dev/null +++ b/recipes/save-indicator.md @@ -0,0 +1,88 @@ +# Save Indicator + +This recipe demonstrates the creation of a google docs-esque save indicator with +RxJS. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-3txbvy?file=index.ts) ) + +![Save Indicator](https://drive.google.com/uc?export=view&id=1sYFqLoKlT0EPHxSDMSX7pT14RDjybU0Q) + +```js +import { fromEvent, of, merge, empty, concat, defer } from 'rxjs'; +import { + delay, + map, + mergeMap, + tap, + debounceTime, + distinctUntilChanged, + mapTo, + filter, + share, + switchAll +} from 'rxjs/operators'; +import { format } from 'date-fns'; + +// track in progress saves +let savesInProgress = 0; + +// references +const input = document.getElementById('note-input'); +const saveIndicator = document.querySelector('.save-indicator'); + +// streams +const keyup$ = fromEvent(input, 'keyup'); + +// fake save request +const saveChanges = value => { + return of(value).pipe(delay(1500)); +}; + +/** + * Trigger a save when the user stops typing for 200ms + * After new data has been successfully saved, so a saved + * and last updated indicator. + */ +const inputToSave$ = keyup$.pipe( + debounceTime(200), + map(e => e.target.value), + distinctUntilChanged(), + share() +); + +const savesInProgress$ = inputToSave$.pipe( + mapTo(of('Saving')), + tap(_ => savesInProgress++) +); + +const savesCompleted$ = inputToSave$.pipe( + mergeMap(saveChanges), + tap(_ => savesInProgress--), + // ignore if additional saves are in progress + filter(_ => !savesInProgress), + mapTo( + concat( + // display saved for 2s + of('Saved!'), + empty().pipe(delay(2000)), + // then last updated time, defer for proper time + defer(() => of(`Last updated: ${format(Date.now(), 'MM/DD/YYYY hh:mm')}`)) + ) + ) +); + +merge(savesInProgress$, savesCompleted$) + .pipe( + /* + If new save comes in when our completion observable is running, we want to switch to it for a status update. + */ + switchAll() + ) + .subscribe(status => { + saveIndicator.innerHTML = status; + }); +``` diff --git a/recipes/smartcounter.md b/recipes/smartcounter.md index c6027972..2899d459 100644 --- a/recipes/smartcounter.md +++ b/recipes/smartcounter.md @@ -8,54 +8,75 @@ a popular library that accomplishes this is [Hubspot](https://github.com/HubSpot). Let's see how we can accomplish something similar with just a few lines of RxJS. -
+ #### Vanilla JS -( [JSBin](http://jsbin.com/jojucaqiki/1/edit?js,output) | -[JSFiddle](https://jsfiddle.net/btroncone/au4sqvxu/) ) +( [StackBlitz](https://stackblitz.com/edit/rxjs-m2zsud) ) + +![Smart Counter](https://drive.google.com/uc?export=view&id=1cG8huWWfsqSe8jLed_NmLrENnF0QyhfZ) ```js +import { timer, fromEvent, merge } from 'rxjs'; +import { + switchMap, + startWith, + scan, + takeWhile, + takeUntil, + filter, + mapTo, + map, + tap, + pluck +} from 'rxjs/operators'; + +let currentNumber = 0; + +// elems +const input: any = document.getElementById('range'); + // utility functions const takeUntilFunc = (endRange, currentNumber) => { return endRange > currentNumber ? val => val <= endRange : val => val >= endRange; }; - const positiveOrNegative = (endRange, currentNumber) => { return endRange > currentNumber ? 1 : -1; }; - const updateHTML = id => val => (document.getElementById(id).innerHTML = val); -// display -const input = document.getElementById('range'); -const updateButton = document.getElementById('update'); -const subscription = (function(currentNumber) { - return fromEvent(updateButton, 'click').pipe( - map(_ => parseInt(input.value)), +// streams +const enter$ = fromEvent(input, 'keyup').pipe( + pluck('code'), + filter(code => code === 'Enter') +); + +enter$ + .pipe( + map(() => parseInt(input.value)), switchMap(endRange => { return timer(0, 20).pipe( mapTo(positiveOrNegative(endRange, currentNumber)), startWith(currentNumber), scan((acc, curr) => acc + curr), - takeWhile(takeUntilFunc(endRange, currentNumber)); - ) + takeWhile(takeUntilFunc(endRange, currentNumber)) + ); }), tap(v => (currentNumber = v)), startWith(currentNumber) ) .subscribe(updateHTML('display')); -})(0); ``` ###### HTML ```html - - -

0

+
+ +

+
``` We can easily take our vanilla smart counter and wrap it in any popular @@ -128,11 +149,12 @@ export class NumberTrackerComponent implements OnDestroy { ```html

- -

@@ -141,10 +163,10 @@ export class NumberTrackerComponent implements OnDestroy { ### Operators Used -* [fromEvent](../operators/creation/fromevent.md) -* [map](../operators/transformation/map.md) -* [mapTo](../operators/transformation/mapto.md) -* [scan](../operators/transformation/scan.md) -* [startWith](../operators/combination/startwith.md) -* [switchMap](../operators/transformation/switchmap.md) -* [takeWhile](../operators/filtering/takewhile.md) +- [fromEvent](../operators/creation/fromevent.md) +- [map](../operators/transformation/map.md) +- [mapTo](../operators/transformation/mapto.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [switchMap](../operators/transformation/switchmap.md) +- [takeWhile](../operators/filtering/takewhile.md) diff --git a/recipes/space-invaders-game.md b/recipes/space-invaders-game.md new file mode 100644 index 00000000..f74b0226 --- /dev/null +++ b/recipes/space-invaders-game.md @@ -0,0 +1,280 @@ +# Space Invaders Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Space Invaders Game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-space-invaders?file=index.ts) ) + +![Space +Invaders](https://drive.google.com/uc?export=view&id=1s3JUvMEKVDMroou92mCtGHr9-qu89KWL) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, interval } from 'rxjs'; +import { + map, + scan, + tap, + startWith, + withLatestFrom, + takeUntil, + repeat +} from 'rxjs/operators'; +import { gameUpdate, initialState } from './game'; +import { State, Input } from './interfaces'; +import { paint } from './html-renderer'; + +const spaceInvaders$ = interval(100).pipe( + withLatestFrom( + fromEvent(document, 'keydown').pipe( + startWith({ code: '' }), + takeUntil(fromEvent(document, 'keyup')), + repeat() + ) + ), + map(([intrvl, event]: [number, KeyboardEvent]): Input => ({ + dlta: intrvl, + key: event.code + })), + scan(gameUpdate, initialState), + tap(e => paint(e.game, e.playerLives, e.score, e.isGameOver)) +); + +spaceInvaders$.subscribe(); +``` + +#### game.ts + +```js +import { State, Input } from './interfaces'; +import { empty, player, invader, shot, noOfInvadersRows } from './constants'; + +const gameObject = (x, y) => ({ x: x, y: y }); +const gameSize = 20; +const clearGame = () => + Array(gameSize) + .fill(empty) + .map(e => Array(gameSize).fill(empty)); + +const createInvaders = () => + Array.from(Array(noOfInvadersRows).keys()).reduce( + (invds, row) => [...invds, ...createRowOfInvaders(row)], + [] + ); +const createRowOfInvaders = row => + Array.from(Array(gameSize / 2).keys()) + .filter(e => (row % 2 === 0 ? e % 2 === 0 : e % 2 !== 0)) + .map(e => gameObject(row, e + 4)); + +const invadersDirection = (state: State): number => + state.invaders.length && state.invaders[0].y <= 0 + ? 1 + : state.invaders.length && + state.invaders[state.invaders.length - 1].y >= gameSize - 1 + ? -1 + : state.invadersDirY; + +const drawGame = (state: State): number[][] => ( + keepShipWithinGame(state), + (state.game = clearGame()), + (state.game[state.game.length - 1][state.shipY] = player), + state.invaders.forEach(i => (state.game[i.x][i.y] = invader)), + state.invadersShoots.forEach(s => (state.game[s.x][s.y] = shot)), + state.shoots.forEach(s => (state.game[s.x][s.y] = shot)), + state.game +); + +const addInvaderShoot = state => + (randomInvader => gameObject(randomInvader.x, randomInvader.y))( + state.invaders[Math.floor(Math.random() * state.invaders.length)] + ); + +const collision = (e1, e2) => e1.x === e2.x && e1.y === e2.y; +const filterOutCollisions = (c1: any[], c2: any[]): any[] => + c1.filter(e1 => !c2.find(e2 => collision(e1, e2))); +const updateScore = (state: State): number => + state.shoots.find(s => state.invaders.find(i => collision(s, i))) + ? state.score + 1 + : state.score; + +const updateState = (state: State): State => ({ + delta: state.delta, + game: drawGame(state), + shipY: state.shipY, + playerLives: state.invadersShoots.some( + e => e.x === gameSize - 1 && e.y === state.shipY + ) + ? state.playerLives - 1 + : state.playerLives, + isGameOver: state.playerLives <= 0, + score: updateScore(state), + invadersDirY: invadersDirection(state), + invaders: !state.invaders.length + ? createInvaders() + : filterOutCollisions(state.invaders, state.shoots).map(i => + state.delta % 10 === 0 + ? gameObject( + i.x + (state.delta % (state.shootFrequency + 10) === 0 ? 1 : 0), + i.y + state.invadersDirY + ) + : i + ), + invadersShoots: + ((state.invadersShoots = + state.delta % state.shootFrequency === 0 + ? [...state.invadersShoots, addInvaderShoot(state)] + : state.invadersShoots), + state.invadersShoots + .filter(e => e.x < gameSize - 1) + .map(e => gameObject(e.x + 1, e.y))), + shoots: filterOutCollisions(state.shoots, state.invaders) + .filter(e => e.x > 0) + .map(e => gameObject(e.x - 1, e.y)), + shootFrequency: !state.invaders.length + ? state.shootFrequency - 5 + : state.shootFrequency +}); + +const keepShipWithinGame = (state: State): number => ( + (state.shipY = state.shipY < 0 ? 0 : state.shipY), + (state.shipY = state.shipY >= gameSize - 1 ? gameSize - 1 : state.shipY) +); + +const updateShipY = (state: State, input: Input): number => + input.key !== 'ArrowLeft' && input.key !== 'ArrowRight' + ? state.shipY + : (state.shipY -= input.key === 'ArrowLeft' ? 1 : -1); + +const addShots = (state: State, input: Input) => + (state.shoots = + input.key === 'Space' + ? [...state.shoots, gameObject(gameSize - 2, state.shipY)] + : state.shoots); + +const isGameOver = (state: State): boolean => + state.playerLives <= 0 || + (state.invaders.length && + state.invaders[state.invaders.length - 1].x >= gameSize - 1); + +export const initialState: State = { + delta: 0, + game: clearGame(), + shipY: 10, + playerLives: 3, + isGameOver: false, + score: 0, + invadersDirY: 1, + invaders: createInvaders(), + invadersShoots: [], + shoots: [], + shootFrequency: 20 +}; + +const processInput = (state: State, input: Input) => ( + updateShipY(state, input), addShots(state, input) +); +const whileNotGameOver = (state: State, input: Input) => + (state.delta = isGameOver(state) ? undefined : input.dlta); + +export const gameUpdate = (state: State, input: Input): State => ( + whileNotGameOver(state, input), processInput(state, input), updateState(state) +); +``` + +#### constants.ts + +```js +export const empty = 0; +export const player = 1; +export const invader = 2; +export const shot = 3; +export const noOfInvadersRows = 6; +``` + +#### interfaces.ts + +```js +export interface State { + delta: number; + game: number[][]; + shipY: number; + playerLives: number; + isGameOver: boolean; + score: number; + invadersDirY: number; + invaders: any[]; + invadersShoots: any[]; + shoots: any[]; + shootFrequency: number; +} + +export interface Input { + dlta: number; + key: string; +} +``` + +#### html-renderer.ts + +```js +import { empty, player, invader, shot } from './constants'; + +const createElem = col => { + const elem = document.createElement('div'); + elem.classList.add('board'); + elem.style.display = 'inline-block'; + elem.style.marginLeft = '10px'; + elem.style.height = '6px'; + elem.style.width = '6px'; + elem.style['background-color'] = + col === empty + ? 'white' + : col === player + ? 'cornflowerblue' + : col === invader + ? 'gray' + : 'silver'; + elem.style['border-radius'] = '90%'; + return elem; +}; + +export const paint = ( + game: number[][], + playerLives: number, + score: number, + isGameOver: boolean +) => { + document.body.innerHTML = ''; + document.body.innerHTML += `Score: ${score} Lives: ${playerLives}`; + + if (isGameOver) { + document.body.innerHTML += ' GAME OVER!'; + return; + } + + game.forEach(row => { + const rowContainer = document.createElement('div'); + row.forEach(col => rowContainer.appendChild(createElem(col))); + document.body.appendChild(rowContainer); + }); +}; +``` + +### Operators Used + +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [repeat](../operators/utility/repeat.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [takeUntil](../operators/filtering/takeuntil.md) +- [tap](../operators/utility/do.md) +- [withLatestFrom](../operators/combination/withlatestfrom.md) diff --git a/recipes/stop-watch.md b/recipes/stop-watch.md new file mode 100644 index 00000000..42cb7ae1 --- /dev/null +++ b/recipes/stop-watch.md @@ -0,0 +1,145 @@ +# Stop Watch + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Stop Watch, inspired by +[RxJS Advanced Patterns – Operate Heavily Dynamic UI’s](https://www.youtube.com/watch?v=XKfhGntZROQ) +talk by [@Michael_Hladky](https://twitter.com/michael_hladky) + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-stop-watch?file=index.ts) ) + +![Stop Watch](https://drive.google.com/uc?export=view&id=14kiNGgR8laJq4a2gEvInNyTPXLVbWH5l) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, interval, merge, noop, NEVER } from 'rxjs'; +import { map, mapTo, scan, startWith, switchMap, tap } from 'rxjs/operators'; + +interface State { + count: boolean; + countup: boolean; + speed: number; + value: number; + increase: number; +} + +const getElem = (id: string): HTMLElement => document.getElementById(id); +const getVal = (id: string): number => parseInt(getElem(id)['value']); +const fromClick = (id: string) => fromEvent(getElem(id), 'click'); +const fromClickAndMapTo = (id: string, obj: {}) => + fromClick(id).pipe(mapTo(obj)); +const fromClickAndMap = (id: string, fn: _ => {}) => + fromClick(id).pipe(map(fn)); +const setValue = (val: number) => + (getElem('counter').innerText = val.toString()); + +const events$ = merge( + fromClickAndMapTo('start', { count: true }), + fromClickAndMapTo('pause', { count: false }), + fromClickAndMapTo('reset', { value: 0 }), + fromClickAndMapTo('countup', { countup: true }), + fromClickAndMapTo('countdown', { countup: false }), + fromClickAndMap('setto', _ => ({ value: getVal('value') })), + fromClickAndMap('setspeed', _ => ({ speed: getVal('speed') })), + fromClickAndMap('setincrease', _ => ({ increase: getVal('increase') })) +); + +const stopWatch$ = events$.pipe( + startWith({ + count: false, + speed: 1000, + value: 0, + countup: true, + increase: 1 + }), + scan((state: State, curr): State => ({ ...state, ...curr }), {}), + tap((state: State) => setValue(state.value)), + switchMap((state: State) => + state.count + ? interval(state.speed).pipe( + tap( + _ => + (state.value += state.countup ? state.increase : -state.increase) + ), + tap(_ => setValue(state.value)) + ) + : NEVER + ) +); + +stopWatch$.subscribe(); +``` + +#### index.html + +```html + + +
0
+
+
+ Setup + + + +
+
+ Count + + +
+
+ Set to + +
+ +
+
+ Speed + +
+ +
+
+ Increase + +
+ +
+
+``` + +### Operators Used + +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [mapTo](../operators/transformation/mapto.md) +- [merge](../operators/transformation/map.md) +- NEVER +- noop +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [switchMap](../operators/transformation/switchmap.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/swipe-to-refresh.md b/recipes/swipe-to-refresh.md new file mode 100644 index 00000000..0933f982 --- /dev/null +++ b/recipes/swipe-to-refresh.md @@ -0,0 +1,118 @@ +# Swipe To Refresh + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of swipe to refresh functionality. +Inspired by [@BenLesh](https://twitter.com/BenLesh) RxJs talks. + + + +### Example Code + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-refresh?file=index.ts&devtoolsheight=40) +) + +![Swipe to Refresh](https://drive.google.com/uc?export=view&id=1BLA2TcAhjwtodkcnsJ8e91ckrvurqkEv) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, iif, of, pipe } from 'rxjs'; +import { + finalize, + mergeMap, + takeUntil, + takeWhile, + repeat, + map, + tap, + exhaustMap, + delay +} from 'rxjs/operators'; + +const setRefreshPos = y => + (document.getElementById('refresh').style.top = `${y}px`); +const resetRefresh = () => setRefreshPos(10); +const setData = data => (document.getElementById('data').innerText = data); + +const fakeRequest = () => + of(new Date().toUTCString()).pipe( + tap(_ => console.log('request')), + delay(1000) + ); + +const takeUntilMouseUpOrRefresh$ = pipe( + takeUntil(fromEvent(document, 'mouseup')), + takeWhile(y => y < 110) +); +const moveDot = y => of(y).pipe(tap(setRefreshPos)); +const refresh$ = of({}).pipe( + tap(resetRefresh), + tap(e => setData('...refreshing...')), + exhaustMap(_ => fakeRequest()), + tap(setData) +); + +fromEvent(document, 'mousedown') + .pipe( + mergeMap(_ => fromEvent(document, 'mousemove')), + map((e: MouseEvent) => e.clientY), + takeUntilMouseUpOrRefresh$, + finalize(resetRefresh), + exhaustMap(y => iif(() => y < 100, moveDot(y), refresh$)), + finalize(() => console.log('end')), + repeat() + ) + .subscribe(); +``` + +##### html + +``` + + +
+
+
Swipe gray dot down to get latest date/time
+``` + +### Operators Used + +- [delay](../operators/utility/delay.md) +- [exhaustMap](../operators/transformation/exhaustmap.md) +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [iif](../operators/conditional/iif.md) +- [map](../operators/transformation/map.md) +- [mergeMap](../operators/transformation/mergemap.md) +- [of](../operators/creation/of.md) +- [repeat](../operators/utility/repeat.md) +- [takeUntil](../operators/filtering/takeuntil.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/tank-battle-game.md b/recipes/tank-battle-game.md new file mode 100644 index 00000000..2b018ac0 --- /dev/null +++ b/recipes/tank-battle-game.md @@ -0,0 +1,252 @@ +# Tank Battle Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Tank Battle like game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-tank-battle?file=index.ts) ) + +![Tank Battle](https://drive.google.com/uc?export=view&id=1-QeeMZCtd9rp60m9C9yphMW5vs0jQxYW) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, combineLatest, interval } from 'rxjs'; +import { scan, tap, startWith } from 'rxjs/operators'; +import { gameSize, down, up, right, left, p1Color, p2Color } from './constants'; +import { State } from './interfaces'; +import { + updatePlayer, + addShots, + updateShots, + checkCollisions, + initialState +} from './game'; +import { paint } from './html-renderer'; + +combineLatest( + interval(100), + fromEvent(document, 'keydown').pipe(startWith({ key: '' })) +) + .pipe( + scan < [number, KeyboardEvent], + State > + ((state, [_, event]) => ( + updatePlayer(state.players[0], event.key, 'w', 's', 'a', 'd'), + updatePlayer(state.players[1], event.key, 'i', 'k', 'j', 'l'), + addShots(state, event.key), + (state.shots = updateShots(state.shots)), + checkCollisions(state), + state + ), + initialState), + tap(paint) + ) + .subscribe(); +``` + +#### game.ts + +```js +import { + gameSize, + down, + up, + right, + left, + p1Color, + p2Color, + p1Shot, + p2Shot +} from './constants'; +import { GameObject, State } from './interfaces'; + +const gameObject = (x, y, g, c): GameObject => ({ x, y, g, s: 0, c: c }); +const noop = (): void => {}; + +export const updatePlayer = ( + p: GameObject, + key: string, + u, + d, + l, + r +): GameObject => ( + key === d + ? ((p.x += p.x < gameSize - 1 ? 1 : 0), (p.g = down)) + : key === u + ? ((p.x += p.x > 0 ? -1 : 0), (p.g = up)) + : noop, + key === r + ? ((p.y += p.y < gameSize - 1 ? 1 : 0), (p.g = right)) + : key === l + ? ((p.y += p.y > 0 ? -1 : 0), (p.g = left)) + : noop, + p +); + +export const addShot = (player: GameObject): GameObject => ({ + x: player.x, + y: player.y, + g: player.g +}); + +export const addShots = (state: State, key: string): void => + state.shots.push( + key === p1Shot + ? addShot(state.players[0]) + : key === p2Shot + ? addShot(state.players[1]) + : [] + ); + +export const updateShots = (shots: GameObject[]): GameObject[] => + shots + .filter(s => s.x > 0 && s.x < gameSize - 1 && s.y > 0 && s.y < gameSize) + .map( + s => ( + s.g === down + ? (s.x += 1) + : s.g === up + ? (s.x += -1) + : s.g === right + ? (s.y += 1) + : s.g === left + ? (s.y += -1) + : () => {}, + s + ) + ); + +export const initialState: State = { + players: [ + gameObject(1, 1, right, p1Color), + gameObject(gameSize - 2, gameSize - 2, left, p2Color) + ], + shots: [] +}; + +export const checkCollisions = (state: State): void => + state.players.forEach((p, i) => { + const collidingShotIndex = state.shots.findIndex( + s => s.x === p.x && s.y === p.y + ); + if (collidingShotIndex > -1) { + if (i === 0) { + state.players[1].s += 1; + } else { + state.players[0].s += 1; + } + state.shots.splice(collidingShotIndex, 1); + } + }); +``` + +#### interfaces.ts + +```js +export interface GameObject { + x: number; + y: number; + g: string; + s: number; + c: string; +} + +export interface State { + players: GameObject[]; + shots: GameObject[]; +} +``` + +#### constants.ts + +```js +export const gameSize = 20; +export const up = '^'; +export const down = 'v'; +export const left = '<'; +export const right = '>'; +export const empty = 0; +export const p1Color = 'DarkViolet'; +export const p2Color = 'CornflowerBlue'; +export const p1Shot = 'c'; +export const p2Shot = 'n'; +``` + +#### html-renderer.ts + +```js +import { gameSize, empty, p1Color, p2Color } from './constants'; +import { State, GameObject } from './interfaces'; + +const createElem = (gameObject: GameObject) => { + const elem = document.createElement('div'); + elem.style.display = 'inline-block'; + elem.style.marginLeft = '10px'; + elem.style.height = '6px'; + elem.style.width = '6px'; + elem.style.color = gameObject.c; + elem.innerText = gameObject === empty ? ' ' : gameObject.g; + + return elem; +}; + +const paintPlayerScore = (score: number, color: string) => { + const scoreElem = document.createElement('span'); + scoreElem.innerHTML = `P1: ${score} `; + scoreElem.style.color = color; + document.body.appendChild(scoreElem); +}; + +const paintScores = (state: State) => { + document.body.innerHTML = 'Scores: '; + paintPlayerScore(state.players[0].s, p1Color); + paintPlayerScore(state.players[1].s, p2Color); +}; + +const painInfo = () => { + document.body.innerHTML += 'This game requires 2 players :)'; + document.body.innerHTML += '
'; + document.body.innerHTML += 'Player 1 controls: wsad, fire: c'; + document.body.innerHTML += '
'; + document.body.innerHTML += 'Player 2 controls: ikjl, fire: n'; +}; + +const emptyGame = () => + Array(gameSize) + .fill(empty) + .map(_ => Array(gameSize).fill(empty)); +const paintGame = (state: State) => { + const game = emptyGame(); + state.players.forEach(p => (game[p.x][p.y] = { g: p.g, c: p.c })); + state.shots.forEach(s => (game[s.x][s.y] = { g: '*', c: 'black' })); + + game.forEach(row => { + const rowContainer = document.createElement('div'); + row.forEach(col => rowContainer.appendChild(createElem(col))); + document.body.appendChild(rowContainer); + }); +}; + +export const paint = (state: State) => { + paintScores(state); + document.body.innerHTML += '
'; + paintGame(state); + painInfo(); +}; +``` + +### Operators Used + +- [combineLatest](../operators/combination/combinelatest.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/tetris-game.md b/recipes/tetris-game.md new file mode 100644 index 00000000..ceabe9c3 --- /dev/null +++ b/recipes/tetris-game.md @@ -0,0 +1,351 @@ +# Tetris Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Tetris game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-tetris?file=index.ts) ) + +![Tetris Game](https://drive.google.com/uc?export=view&id=1BjyB43WSHEU9rDbNN6uFeoYxIoEzJTZp) + +#### index.ts + +```js +// RxJS v6+ +import { fromEvent, of, interval, combineLatest } from 'rxjs'; +import { + finalize, + map, + pluck, + scan, + startWith, + takeWhile, + tap +} from 'rxjs/operators'; +import { score, randomBrick, clearGame, initialState } from './game'; +import { render, renderGameOver } from './html-renderer'; +import { handleKeyPress, resetKey } from './keyboard'; +import { collide } from './collision'; +import { rotate } from './rotation'; +import { BRICK } from './constants'; +import { State, Brick, Key } from './interfaces'; + +const player$ = combineLatest( + of(randomBrick()), + of({ code: '' }), + fromEvent(document, 'keyup').pipe( + startWith({ code: undefined }), + pluck('code') + ) +).pipe( + map( + ([brick, key, keyCode]: [Brick, Key, string]) => ( + (key.code = keyCode), [brick, key] + ) + ) +); + +const state$ = interval(1000).pipe( + scan < number, + State > ((state, _) => (state.x++, state), initialState) +); + +const game$ = combineLatest(state$, player$).pipe( + scan < [State, [Brick, Key]], + [State, [Brick, Key]] > + (([state, [brick, key]]) => ( + (state = handleKeyPress(state, brick, key)), + (([newState, rotatedBrick]: [State, Brick]) => ( + (state = newState), (brick = rotatedBrick) + ))(rotate(state, brick, key)), + (([newState, collidedBrick]: [State, Brick]) => ( + (state = newState), (brick = collidedBrick) + ))(collide(state, brick)), + (state = score(state)), + resetKey(key), + [state, [brick, key]] + )), + tap(([state, [brick, key]]) => render(state, brick)), + takeWhile(([state, [brick, key]]) => !state.game[1].some(c => c === BRICK)), + finalize(renderGameOver) +); + +game$.subscribe(); +``` + +#### game.ts + +```js +import { GAME_SIZE, EMPTY, BRICK } from './constants'; +import { State } from './interfaces'; + +const bricks = [ + [ + [0, 0, 0], + [1, 1, 1], + [0, 0, 0] + ], + [ + [1, 1, 1], + [0, 1, 0], + [0, 1, 0] + ], + [ + [0, 1, 1], + [0, 1, 0], + [0, 1, 0] + ], + [ + [1, 1, 0], + [0, 1, 0], + [0, 1, 0] + ], + [ + [1, 1, 0], + [1, 1, 0], + [0, 0, 0] + ] +]; + +export const clearGame = () => + Array(GAME_SIZE) + .fill(EMPTY) + .map(e => Array(GAME_SIZE).fill(EMPTY)); +export const updatePosition = (position: number, column: number) => + position === 0 ? column : position; +export const validGame = (game: number[][]) => + game.map(r => r.filter((_, i) => i < GAME_SIZE)); +export const validBrick = (brick: number[][]) => + brick.filter(e => e.some(b => b === BRICK)); +export const randomBrick = () => + bricks[Math.floor(Math.random() * bricks.length)]; + +export const score = (state: State): State => + (scoreIndex => + scoreIndex > -1 + ? ((state.score += 1), + state.game.splice(scoreIndex, 1), + (state.game = [Array(GAME_SIZE).fill(EMPTY), ...state.game]), + state) + : state)(state.game.findIndex(e => e.every(e => e === BRICK))); + +export const initialState = { + game: clearGame(), + x: 0, + y: 0, + score: 0 +}; +``` + +#### collision.ts + +```js +import { GAME_SIZE, BRICK, EMPTY } from './constants'; +import { validBrick, validGame, updatePosition, randomBrick } from './game'; +import { State, Brick } from './interfaces'; + +const isGoingToLevelWithExistingBricks = ( + state: State, + brick: Brick +): boolean => { + const gameHeight = state.game.findIndex(r => r.some(c => c === BRICK)); + const brickBottomX = state.x + brick.length - 1; + return gameHeight > -1 && brickBottomX + 1 > gameHeight; +}; + +const areAnyBricksColliding = (state: State, brick: Brick): boolean => + validBrick(brick).some((r, i) => + r.some((c, j) => + c === EMPTY + ? false + : ((x, y) => state.game[x][y] === c)(i + state.x, j + state.y) + ) + ); + +const collideBrick = ( + state: State, + brick: Brick, + isGoingToCollide: boolean +): State => { + const xOffset = isGoingToCollide ? 1 : 0; + validBrick(brick).forEach((r, i) => { + r.forEach( + (c, j) => + (state.game[i + state.x - xOffset][j + state.y] = updatePosition( + state.game[i + state.x - xOffset][j + state.y], + c + )) + ); + }); + state.game = validGame(state.game); + state.x = 0; + state.y = GAME_SIZE / 2 - 1; + return state; +}; + +export const collide = (state: State, brick: Brick): [State, Brick] => { + const isGoingToCollide = + isGoingToLevelWithExistingBricks(state, brick) && + areAnyBricksColliding(state, brick); + + const isOnBottom = state.x + validBrick(brick).length > GAME_SIZE - 1; + + if (isGoingToCollide || isOnBottom) { + state = collideBrick(state, brick, isGoingToCollide); + brick = randomBrick(); + } + + return [state, brick]; +}; +``` + +#### rotation.ts + +```js +import { GAME_SIZE, BRICK_SIZE, EMPTY } from './constants'; +import { State, Brick, Key } from './interfaces'; + +const rightOffsetAfterRotation = ( + state: State, + brick: Brick, + rotatedBrick: Brick +) => + state.y + rotatedBrick.length === GAME_SIZE + 1 && + brick.every(e => e[2] === EMPTY) + ? 1 + : 0; + +const leftOffsetAfterRotation = (game: State) => (game.y < 0 ? 1 : 0); +const emptyBrick = (): Brick => + Array(BRICK_SIZE) + .fill(EMPTY) + .map(e => Array(BRICK_SIZE).fill(EMPTY)); + +const rotateBrick = ( + state: State, + brick: Brick, + rotatedBrick: Brick +): [State, Brick] => ( + brick.forEach((r, i) => + r.forEach((c, j) => (rotatedBrick[j][brick[0].length - 1 - i] = c)) + ), + (state.y -= rightOffsetAfterRotation(state, brick, rotatedBrick)), + (state.y += leftOffsetAfterRotation(state)), + [state, rotatedBrick] +); + +export const rotate = (state: State, brick: Brick, key: Key): [State, Brick] => + key.code === 'ArrowUp' + ? rotateBrick(state, brick, emptyBrick()) + : [state, brick]; +``` + +#### keyboard.ts + +```js +import { GAME_SIZE } from './constants'; +import { State, Brick, Key } from './interfaces'; + +const xOffset = (brick: Brick, columnIndex: number) => + brick.every(e => e[columnIndex] === 0) ? 1 : 0; + +export const handleKeyPress = (state: State, brick: Brick, key: Key): State => ( + (state.x += key.code === 'ArrowDown' ? 1 : 0), + (state.y += + key.code === 'ArrowLeft' && state.y > 0 - xOffset(brick, 0) + ? -1 + : key.code === 'ArrowRight' && state.y < GAME_SIZE - 3 + xOffset(brick, 2) + ? 1 + : 0), + state +); + +export const resetKey = key => (key.code = undefined); +``` + +#### html-renderer.ts + +```js +import { BRICK } from './constants'; +import { State, Brick } from './interfaces'; +import { updatePosition, validGame, validBrick, clearGame } from './game'; + +const createElem = (column: number): HTMLElement => + (elem => ( + (elem.style.display = 'inline-block'), + (elem.style.marginLeft = '3px'), + (elem.style.height = '6px'), + (elem.style.width = '6px'), + (elem.style['background-color'] = column === BRICK ? 'green' : 'aliceblue'), + elem + ))(document.createElement('div')); + +export const render = (state: State, brick: Brick): void => { + const gameFrame = clearGame(); + + state.game.forEach((r, i) => r.forEach((c, j) => (gameFrame[i][j] = c))); + validBrick(brick).forEach((r, i) => + r.forEach( + (c, j) => + (gameFrame[i + state.x][j + state.y] = updatePosition( + gameFrame[i + state.x][j + state.y], + c + )) + ) + ); + + document.body.innerHTML = `score: ${state.score}
`; + validGame(gameFrame).forEach(r => { + const rowContainer = document.createElement('div'); + r.forEach(c => rowContainer.appendChild(createElem(c))); + document.body.appendChild(rowContainer); + }); +}; + +export const renderGameOver = () => + (document.body.innerHTML += '
GAME OVER!'); +``` + +#### interfaces.ts + +```js +export interface State { + game: number[][]; + x: number; + y: number; + score: number; +} + +export interface Key { + code: string; +} + +export type Brick = number[][]; +``` + +#### constants.ts + +```js +export const GAME_SIZE = 10; +export const BRICK_SIZE = 3; +export const EMPTY = 0; +export const BRICK = 1; +``` + +### Operators Used + +- [combineLatest](../operators/combination/combinelatest.md) +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [map](../operators/transformation/map.md) +- [of](../operators/creation/of.md) +- [pluck](../operators/transformation/pluck.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/type-ahead.md b/recipes/type-ahead.md new file mode 100644 index 00000000..c9e0d627 --- /dev/null +++ b/recipes/type-ahead.md @@ -0,0 +1,72 @@ +# Type Ahead + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates the creation of type ahead client side code. + + + +### Example Code + +( +[StackBlitz](https://stackblitz.com/edit/rxjs-type-ahead?file=index.ts&devtoolsheight=50) +) + +![Typeahead](https://drive.google.com/uc?export=view&id=1TdDA78dkiy5lC8A3Rz28oDq9SuaxsS45) + +```js +// RxJS v6+ +import { fromEvent, of } from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, + map, + switchMap, + tap +} from 'rxjs/operators'; + +const getContinents = keys => + [ + 'africa', + 'antarctica', + 'asia', + 'australia', + 'europe', + 'north america', + 'south america' + ].filter(e => e.indexOf(keys.toLowerCase()) > -1); + +const fakeContinentsRequest = keys => + of(getContinents(keys)).pipe( + tap(_ => console.log(`API CALL at ${new Date()}`)) + ); + +fromEvent(document.getElementById('type-ahead'), 'keyup') + .pipe( + debounceTime(200), + map((e: any) => e.target.value), + distinctUntilChanged(), + switchMap(fakeContinentsRequest), + tap(c => (document.getElementById('output').innerText = c.join('\n'))) + ) + .subscribe(); +``` + +##### html + +```html +Get continents + +
+
+``` + +### Operators Used + +- [debounceTime](../operators/filtering/debouncetime.md) +- [distinctUntilChanged](../operators/filtering/distinctuntilchanged.md) +- [fromEvent](../operators/creation/fromevent.md) +- [map](../operators/transformation/map.md) +- [of](../operators/creation/of.md) +- [switchMap](../operators/transformation/switchmap.md) +- [tap](../operators/utility/do.md) diff --git a/recipes/uncover-image-game.md b/recipes/uncover-image-game.md new file mode 100644 index 00000000..dc82957e --- /dev/null +++ b/recipes/uncover-image-game.md @@ -0,0 +1,318 @@ +# Uncover Image Game + +_By [adamlubek](https://github.com/adamlubek)_ + +This recipe demonstrates RxJS implementation of Uncover Image Game. + + + +### Example Code + +( [StackBlitz](https://stackblitz.com/edit/rxjs-uncover-image?file=index.ts) ) + +#### index.ts + +```js +// RxJS v6+ +import { interval } from 'rxjs'; +import { finalize, scan, takeWhile, tap, withLatestFrom } from 'rxjs/operators'; +import { keyboardEvents$ } from './keyboard'; +import { initialGame, updateGame, isGameOn } from './game'; +import { paintGame, paintGameOver } from './html-renderer'; + +interval(15) + .pipe( + withLatestFrom(keyboardEvents$), + scan(updateGame, initialGame), + tap(paintGame), + takeWhile(isGameOn), + finalize(paintGameOver) + ) + .subscribe(); +``` + +#### game.ts + +```js +import { noop } from 'rxjs'; +import { Enemy, State, Move } from './interfaces'; +import { size } from './constants'; +import { newPlayerFrom, movePlayer } from './player'; +import { newEnemiesFrom } from './enemy'; + +const intersect = (state: State): State => ( + state.moves.some((m: Move) => + state.enemies.some(e => m.x === e.x && m.y === e.y) + ) + ? ((state.player.lives -= 1), + (state.player.x = 0), + (state.player.y = 0), + (state.moves = [])) + : noop, + state +); + +const initialGame: State = { + player: { x: 0, y: 0, lives: 3 }, + enemies: [ + { x: 10, y: 10, moveDuration: 0, dirX: 1, dirY: 1 }, + { x: 50, y: 50, moveDuration: 0, dirX: -1, dirY: 1 } + ], + key: '', + moves: [], + corners: [] +}; + +const updateGame = (state: State, [_, key]: [number, string]): State => ( + (state.enemies = newEnemiesFrom(state)), + (state.player = newPlayerFrom(state, key)), + (state = intersect(state)), + (state = movePlayer(state, key)), + (state.key = key), + state +); + +const isGameOn = (state: State): boolean => state.player.lives > 0; + +export { initialGame, updateGame, isGameOn }; +``` + +#### player.ts + +```js +import { noop } from 'rxjs'; +import { Player, State, Move } from './interfaces'; +import { size } from './constants'; +import { keyToDirection } from './keyboard'; + +const up = 'ArrowUp'; +const down = 'ArrowDown'; +const left = 'ArrowLeft'; +const right = 'ArrowRight'; + +const newPlayerFrom = (state: State, key: string): Player => ( + (state.player.x += keyToDirection(key, down, up)), + (state.player.y += keyToDirection(key, right, left)), + state.player.x < 0 ? (state.player.x = 0) : noop, + state.player.x > size ? (state.player.x = size) : noop, + state.player.y < 0 ? (state.player.y = 0) : noop, + state.player.y > size ? (state.player.y = size) : noop, + state.player +); + +const getEnclosedArea = (state: State): Move[] => + state.moves.length <= 1 + ? [] + : [ + ...state.moves + .slice( + state.moves.findIndex( + e => e.x === state.player.x && e.y === state.player.y + ) + ) + .filter(e => e.dirChange), + state.moves.pop() + ]; + +const movePlayer = (state: State, key: string): State => ( + state.moves.some(e => e.x === state.player.x && e.y === state.player.y) + ? ((state.corners = getEnclosedArea(state)), (state.moves = [])) + : state.moves.push({ + x: state.player.x, + y: state.player.y, + dirChange: state.key !== key + }), + state +); + +export { newPlayerFrom, movePlayer }; +``` + +#### enemy.ts + +```js +import { noop } from 'rxjs'; +import { Enemy, State } from './interfaces'; +import { size } from './constants'; + +const newEnemiesFrom = (state: State): Enemy[] => ( + state.enemies.forEach( + e => ( + e.x <= 0 || e.x > size ? (e.dirX *= -1) : noop, + e.y <= 0 || e.y > size ? (e.dirY *= -1) : noop, + (e.x += e.dirX), + (e.y += e.dirY), + (e.moveDuration += 1), + e.moveDuration > 100 + ? ((e.dirX = Math.random() > 0.5 ? 1 : -1), + (e.dirY = Math.random() > 0.5 ? 1 : -1), + (e.moveDuration = 0)) + : noop + ) + ), + state.enemies +); + +export { newEnemiesFrom }; +``` + +#### keyboard.ts + +```js +import { fromEvent } from 'rxjs'; +import { pluck, startWith } from 'rxjs/operators'; + +const positionChangeUnit = 2; + +export const keyboardEvents$ = fromEvent(document, 'keydown').pipe( + pluck < KeyboardEvent, + string > 'code', + startWith('') +); + +export const keyToDirection = ( + key: string, + key1: string, + key2: string +): number => + key === key1 ? positionChangeUnit : key === key2 ? -positionChangeUnit : 0; +``` + +#### interfaces.ts + +```js +interface GameObject { + x: number; + y: number; +} + +interface Player extends GameObject { + lives: number; +} + +interface Enemy extends GameObject { + moveDuration: number; + dirX: number; + dirY: number; +} + +interface Move extends GameObject { + dirChange: boolean; +} + +interface State { + player: Player; + enemies: Enemy[]; + key: string; + moves: Move[]; + corners: Move[]; +} + +export { GameObject, Player, Enemy, State }; +``` + +#### constants.ts + +```js +const size = 200; + +export { size }; +``` + +#### html-renderer.ts + +```js +import { State, GameObject } from './interfaces'; + +const clearPlayerPath = _ => + document + .querySelectorAll('circle') + .forEach(e => document.querySelector('#svg_container').removeChild(e)); + +const addCircleColored = (color: string) => (e: GameObject) => { + const circle = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'circle' + ); + circle.setAttribute('cx', e.y); + circle.setAttribute('cy', e.x); + circle.setAttribute('r', '2'); + circle.setAttribute('stroke', color); + circle.setAttribute('strokeWidth', '1'); + document.querySelector('#svg_container').appendChild(circle); +}; + +const addPlayerPath = (state: State) => + state.moves.forEach(addCircleColored('gray')); +const addEnemy = (state: State) => + state.enemies.forEach(addCircleColored('red')); + +const addHoles = (state: State) => { + if (!state.corners.length) { + return; + } + + const createPathFromCorners = (a, c) => + (a += `${a.endsWith('Z') ? 'M' : 'L'} ${c.y} ${c.x} ${ + c.dirChange ? '' : 'Z' + }`); + const newPath = + `M${state.corners[0].y} ${state.corners[0].x}` + + state.corners.reduce(createPathFromCorners, ''); + const maskPath = document.querySelector('#mask_path'); + + const currentPath = maskPath.getAttribute('d'); + const path = newPath + ' ' + currentPath; + + maskPath.setAttribute('d', path); +}; + +const paintInfo = (text: string) => + (document.querySelector('#info').innerHTML = text); +const paintLives = (state: State) => paintInfo(`lives: ${state.player.lives}`); + +const updateSvgPath = (state: State) => + [clearPlayerPath, addPlayerPath, addEnemy, addHoles, paintLives].forEach(fn => + fn(state) + ); + +const paintGame = updateSvgPath; +const paintGameOver = () => paintInfo('Game Over !!!'); + +export { paintGame, paintGameOver }; +``` + +#### index.html + +```html +
+ + + + + + + + + RxJs + + +
Use arrows to uncover image!!!
+``` + +### Operators Used + +- [finalize](../operators/utility/finalize.md) +- [fromEvent](../operators/creation/fromevent.md) +- [interval](../operators/creation/interval.md) +- [pluck](../operators/transformation/pluck.md) +- [scan](../operators/transformation/scan.md) +- [startWith](../operators/combination/startwith.md) +- [takeWhile](../operators/filtering/takewhile.md) +- [tap](../operators/utility/do.md) +- [withLatestFrom](../operators/combination/withlatestfrom.md) diff --git a/styles/website.css b/styles/website.css index 9369d46f..9163b141 100644 --- a/styles/website.css +++ b/styles/website.css @@ -1,4 +1,37 @@ +@import url('https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono&display=swap'); + .ua-ad { text-align: center; padding: 10px 0; } + +code[class*='language-'], +pre[class*='language-'] { + text-shadow: none !important; + border-radius: 4px; + margin-bottom: 44px; +} + +.book { + font-family: 'Roboto', sans-serif !important; +} + +.book .markdown-section pre > code { + font-family: 'Roboto Mono', Consolas, Monaco, andale mono, ubuntu mono, + monospace !important; + font-size: 0.9em !important; +} + +.markdown-section code { + font-family: 'Roboto Mono', Consolas, Monaco, andale mono, ubuntu mono, + monospace !important; +} + +.markdown-section h5 { + font-size: 0.9em !important; + text-transform: uppercase !important; +} + +.font-settings { + display: none; +} diff --git a/subjects/README.md b/subjects/README.md new file mode 100644 index 00000000..4a545734 --- /dev/null +++ b/subjects/README.md @@ -0,0 +1,124 @@ +# Subjects + +A Subject is a special type of Observable which shares a single execution path among observers. + +You can think of this as a single speaker talking at a microphone in a room full of people. Their message (the subject) is being delivered to many (multicast) people (the observers) at once. This is the basis of **multicasting**. Typical observables would be comparable to a 1 on 1 conversation. + +There are 4 variants of subjects: + +- **Subject** - No initial value or replay behavior. +- **AsyncSubject** - Emits latest value to observers upon completion. +- **BehaviorSubject** - Requires an initial value and emits its current value (last emitted item) to new subscribers. +- **ReplaySubject** - Emits specified number of last emitted values (a replay) to new subscribers. + +## Contents + +* [AsyncSubject](asyncsubject.md) +* [BehaviorSubject](behaviorsubject.md) +* [ReplaySubject](replaysubject.md) +* [Subject](subject.md) + +## Subjects comparison + +( [Stackblitz](https://stackblitz.com/edit/rxjs-subjects-comparison?file=index.ts&devtoolsheight=100) ) + +```js +/* + s1 n(r) n(x) s2 n(j) c n(s) +Subject + s1 ^-----r------x--------------j------|---------- + s2 ---------------------^------j------|---------- +AsyncSubject + s1 ^----------------------------------j|--------- + s2 ---------------------^-------------j|--------- +BehaviorSubject + s1 ^a----r------x--------------j------|---------- + s2 ---------------------^x-----j------|---------- +ReplaySubject + s1 ^-----r------x--------------j------|---------- + s2 ---------------------^r-x---j------|---------- +*/ + +// RxJS v6+ +import { Subject, AsyncSubject, BehaviorSubject, ReplaySubject } from 'rxjs'; + +const subject = new Subject(); +const asyncSubject = new AsyncSubject(); +const behaviorSubject = new BehaviorSubject('a'); +const replaySubject = new ReplaySubject(2); + +const subjects = [subject, asyncSubject, behaviorSubject, replaySubject]; +const log = subjectType => e => console.log(`${subjectType}: ${e}`); + +console.log('SUBSCRIBE 1'); +subject.subscribe(log('s1 subject')); +asyncSubject.subscribe(log('s1 asyncSubject')); +behaviorSubject.subscribe(log('s1 behaviorSubject')); +replaySubject.subscribe(log('s1 replaySubject')); + +console.log('\nNEXT(r)'); +subjects.forEach(o => o.next('r')); + +console.log('\nNEXT(x)'); +subjects.forEach(o => o.next('x')); + +console.log('\nSUBSCRIBE 2'); +subject.subscribe(log('s2 subject')); +asyncSubject.subscribe(log('s2 asyncSubject')); +behaviorSubject.subscribe(log('s2 behaviorSubject')); +replaySubject.subscribe(log('s2 replaySubject')); + +console.log('\nNEXT(j)'); +subjects.forEach(o => o.next('j')); + +console.log('\nCOMPLETE'); +subjects.forEach(o => o.complete()); + +console.log('\nNEXT(s)'); +subjects.forEach(o => o.next('s')); + +/* +OUTPUT: + +SUBSCRIBE 1 +s1 behaviorSubject: a + +NEXT(r) +s1 subject: r +s1 behaviorSubject: r +s1 replaySubject: r + +NEXT(x) +s1 subject: x +s1 behaviorSubject: x +s1 replaySubject: x + +SUBSCRIBE 2 +s2 behaviorSubject: x +s2 replaySubject: r +s2 replaySubject: x + +NEXT(j) +s1 subject: j +s2 subject: j +s1 behaviorSubject: j +s2 behaviorSubject: j +s1 replaySubject: j +s2 replaySubject: j + +COMPLETE +s1 asyncSubject: j +s2 asyncSubject: j + +NEXT(s) +*/ +``` + +### Additional Resources + +* [Official Overview](http://reactivex.io/rxjs/manual/overview.html#subject) + 📰 +* [Official Documentation](http://reactivex.io/documentation/subject.html) + 📰 +* [On The Subject Of Subjects (in RxJS)](https://medium.com/@benlesh/on-the-subject-of-subjects-in-rxjs-2b08b7198b93) + 📰 - Ben Lesh diff --git a/subjects/asyncsubject.md b/subjects/asyncsubject.md new file mode 100644 index 00000000..c9937e24 --- /dev/null +++ b/subjects/asyncsubject.md @@ -0,0 +1,39 @@ +# AsyncSubject + +## Emits its last value on completion + + + +### Examples + +##### Example 1: simple AsyncSubject + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-asyncsubject?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { AsyncSubject } from 'rxjs'; + +const sub = new AsyncSubject(); + +sub.subscribe(console.log); + +sub.next(123); //nothing logged + +sub.subscribe(console.log); + +sub.next(456); //nothing logged +sub.complete(); //456, 456 logged by both subscribers +``` + +### Additional Resources + +- [AsyncSubject](https://rxjs-dev.firebaseapp.com/api/index/class/AsyncSubject) + 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/AsyncSubject.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/AsyncSubject.ts) diff --git a/subjects/behaviorsubject.md b/subjects/behaviorsubject.md new file mode 100644 index 00000000..5d3f0e7a --- /dev/null +++ b/subjects/behaviorsubject.md @@ -0,0 +1,118 @@ +# BehaviorSubject + +## Requires an initial value and emits the current value to new subscribers + +--- + +💡 If you want the last emitted value(s) on subscription, but do not need to +supply a seed value, check out [ReplaySubject](replaysubject.md) instead! + +--- + +### Why use `BehaviorSubject`? + +This specialized subject is ideal when you want to maintain and provide a "current value" to subscribers. Think of it as a scoreboard in a basketball game. Even if you join watching in the middle of the game, you'll still see the current score. Similarly, when a new observer subscribes to a `BehaviorSubject`, it immediately receives the current value (or the last value that was emitted). + +It's important to remember that a **`BehaviorSubject` requires an initial value upon instantiation**. This is where it differs from a regular [`Subject`](subject.md) which doesn't have an initial value. Picture a newly installed scoreboard – with `BehaviorSubject`, you set a starting score, say 0-0. With a regular [`Subject`](subject.md), the board remains blank until a point is scored. Subscribers (early or late) of a normal [`Subject`](subject.md) will not receive + emissions until the `Subject` emits a value. + +Contrasting with [`ReplaySubject`](replaysubject.md), while both provide historical values, [`ReplaySubject`](replaysubject.md) can relay multiple previous values, not just the last one. If the basketball scoreboard could show the last five scores in the match sequence, that'd be akin to [`ReplaySubject`](replaysubject.md). [`ReplaySubject`](replaysubject.md) also does not receive an initial seed value. + +In conclusion, if you need to ensure subscribers always get the latest value upon subscription, or you have an initial seed value, `BehaviorSubject` is your pick. If you need more historical emissions, consider [`ReplaySubject`](replaysubject.md). And if you don't need any history at all, a simple [`Subject`](subject.md) might be what you're looking for. + + + +### Examples + +##### Example 1: Simple BehaviorSubject + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-behaviorsubject-simpleexample?file=index.ts?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { BehaviorSubject } from 'rxjs'; + +const subject = new BehaviorSubject(123); + +// two new subscribers will get initial value => output: 123, 123 +subject.subscribe(console.log); +subject.subscribe(console.log); + +// two subscribers will get new value => output: 456, 456 +subject.next(456); + +// new subscriber will get latest value (456) => output: 456 +subject.subscribe(console.log); + +// all three subscribers will get new value => output: 789, 789, 789 +subject.next(789); + +// output: 123, 123, 456, 456, 456, 789, 789, 789 +``` + +##### Example 2: BehaviorSubject with new subscribers created on mouse clicks + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-behaviorsubject-mouseclicks?file=index.ts) +) + +```js +// RxJS v6+ +import { BehaviorSubject, fromEvent, interval, merge } from 'rxjs'; +import { map, tap, mergeMap } from 'rxjs/operators'; + +const setElementText = (elemId, text) => + (document.getElementById(elemId).innerText = text.toString()); +const addHtmlElement = coords => + (document.body.innerHTML += ` +
+
`); + +const subject = new BehaviorSubject(0); + +const click$ = fromEvent(document, 'click').pipe( + map((e: MouseEvent) => ({ + x: e.clientX, + y: e.clientY, + id: Math.random() + })), + tap(addHtmlElement), + mergeMap(coords => subject.pipe(tap(v => setElementText(coords.id, v)))) +); + +const interval$ = interval(1000).pipe( + tap(v => subject.next(v)), + tap(v => setElementText('intervalValue', v)) +); + +merge(click$, interval$).subscribe(); +``` + +### Related Recipes + +- [Alphabet Invasion Game](../recipes/alphabet-invasion-game.md) +- [Battleship Game](../recipes/battleship-game.md) +- [Car Racing Game](../recipes/car-racing-game.md) + +### Additional Resources + +- [BehaviorSubject](https://rxjs-dev.firebaseapp.com/api/index/class/BehaviorSubject) + 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/BehaviorSubject.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/BehaviorSubject.ts) diff --git a/subjects/replaysubject.md b/subjects/replaysubject.md new file mode 100644 index 00000000..12baccb8 --- /dev/null +++ b/subjects/replaysubject.md @@ -0,0 +1,56 @@ +# ReplaySubject + +## "Replays" or emits old values to new subscribers + +### Why use `ReplaySubject`? + +This subject variation shines when you want subscribers to have access to previous values emitted, even if they subscribe after those values have been sent. Think of it as a Tivo or DVR for observables. Missed the start of the show? No worries! With a `ReplaySubject`, you can still catch up on what you missed. + +For a real-world example, consider a chat application. When a user joins a chatroom, they might want to see the last few messages, not just new ones. A `ReplaySubject` can hold onto a specified number of the most recent messages and display them to the user when they join. + +Now, how does it compare with `BehaviorSubject` or a plain `Subject`? + +- **[`BehaviorSubject`](behaviorsubject.md)** always requires an initial value and only stores the most recent value. Subscribers will get that latest value upon subscription. It's like walking into a movie and only catching the most recent scene. + +- **[`Subject`](subject.md)**, on the other hand, doesn't give new subscribers any previously emitted values. It’s the traditional live broadcast. If you're late, you miss out. + +- **`ReplaySubject`** can remember more than just the last scene. Depending on how it's configured, it might replay the entire movie or just the last few scenes. + +However, be cautious. `ReplaySubject` can potentially use more memory since it's storing multiple values. Ensure that you specify a limited buffer size if you're concerned about memory consumption. + +In conclusion, if you want the ability to replay a series of previous emissions to new subscribers, `ReplaySubject` is your go-to. If you only care about the most recent emission (with an initial value), opt for `BehaviorSubject`. And if you want a basic, no-frills broadcasting mechanism where late subscribers miss prior emissions, stick with a plain `Subject`. + + + +### Examples + +##### Example 1: simple ReplaySubject + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-replaysubject-simple-example?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { ReplaySubject } from 'rxjs'; + +const sub = new ReplaySubject(3); + +sub.next(1); +sub.next(2); +sub.subscribe(console.log); // OUTPUT => 1,2 +sub.next(3); // OUTPUT => 3 +sub.next(4); // OUTPUT => 4 +sub.subscribe(console.log); // OUTPUT => 2,3,4 (log of last 3 values from new subscriber) +sub.next(5); // OUTPUT => 5,5 (log from both subscribers) +``` + +### Additional Resources + +- [ReplaySubject](https://rxjs-dev.firebaseapp.com/api/index/class/ReplaySubject) + 📰 - Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/ReplaySubject.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/ReplaySubject.ts) diff --git a/subjects/subject.md b/subjects/subject.md new file mode 100644 index 00000000..49f6aceb --- /dev/null +++ b/subjects/subject.md @@ -0,0 +1,61 @@ +# Subject + +## A special type of Observable which shares a single execution path among observers + +### Why use `Subject`? + +`Subject` in RxJS acts as a bridge between the observer and the observable world. Imagine it as a microphone on a stage: you can shout into it (emit values) and those sitting in the audience (observers) can hear it loud and clear. The uniqueness of a `Subject` is its capability to multicast, i.e., it can emit values to multiple listeners. + +A real-world example of this is like a radio show: one broadcast can be listened to by multiple listeners at once. + +However, there's an array of "microphones" in the RxJS universe, and here's where the other "specialized" Subject types come in: + +- **[`BehaviorSubject`](behaviorsubject.md)**: Imagine going to a movie late and asking your friend, "Hey, what just happened?" and they fill you in. `BehaviorSubject` is similar: when you subscribe, it will give you the latest value that was emitted before you tuned in, and then you continue getting updates. So, if you need that "previous context," this is your pick. `BehaviorSubject` also require a seed value. + +- **[`ReplaySubject`](replaysubject.md)**: Think of this as a DVR for your TV. It records, say, the last 5 shows, and you can replay those whenever you switch on your TV. `ReplaySubject` can keep a buffer of emitted values, and when you subscribe, it will "replay" those values for you, ensuring you don't miss out on what was broadcasted earlier. + +In contrast, a simple `Subject` doesn't offer these playback features. If you join late, you've missed it, akin to a live concert. You only hear what's played after you've arrived. + +In summary, choose `Subject` for standard multicasting needs. Opt for `BehaviorSubject` when the latest value or seed value is critical, and `ReplaySubject` when you want to ensure a history of values is available for late subscribers. + + + +### Examples + +##### Example 1: simple Subject + +( +[Stackblitz](https://stackblitz.com/edit/rxjs-subject-simple-example-j33czp?file=index.ts&devtoolsheight=100) +) + +```js +// RxJS v6+ +import { Subject } from 'rxjs'; + +const sub = new Subject(); + +sub.next(1); +sub.subscribe(x => { + console.log('Subscriber A', x); +}); +sub.next(2); // OUTPUT => Subscriber A 2 +sub.subscribe(x => { + console.log('Subscriber B', x); +}); +sub.next(3); // OUTPUT => Subscriber A 3, Subscriber B 3 (logged from both subscribers) +``` + +### Related Recipes + +- [Battleship Game](../recipes/battleship-game.md) +- [Lockscreen](../recipes/lockscreen.md) + +### Additional Resources + +- [Subject](https://rxjs-dev.firebaseapp.com/api/index/class/Subject) 📰 - + Official docs + +--- + +> 📁 Source Code: +> [https://github.com/ReactiveX/rxjs/blob/master/src/internal/Subject.ts](https://github.com/ReactiveX/rxjs/blob/master/src/internal/Subject.ts)