├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── custom ├── main.js ├── scripts.js ├── startDemo.js └── styles.css ├── docs ├── features.md ├── images │ ├── logitech.png │ ├── material-canvas.png │ ├── material-printing.png │ ├── ribbon-canvas.png │ └── ribbon-printing.png ├── index.html ├── logitech.md ├── printing.md └── shortcuts.md ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json └── pictures ├── 2requests.png ├── author.png ├── demo-2.gif ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── fetch-cancel.png ├── iterator-pattern.png ├── observer-pattern.png ├── palpatin-50.png ├── pipe-3.gif ├── rx-stachka-table-1-bg-2.png ├── rx-stachka-table-1-bg-3.png ├── rx-stachka-table-1.png ├── rx-stachka-table-2.png ├── rx-stachka-table-3.png ├── rx-stachka-table-4.png ├── rx-stachka-table-5.png ├── rx-stachka-table-6.png ├── rxJs.png ├── rxjs-clicks-1.png ├── rxjs-clicks-2-2.png ├── rxjs-clicks-2.png ├── rxjs-clicks-3.png ├── rxjs-clicks-4.png ├── rxjs-logo.png ├── rxjs-qr.png ├── rxmarbles-combine-latest.png ├── rxmarbles-filter.png ├── rxmarbles-switch-map.png ├── table-1.png ├── table-2.png └── table-3.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | [{bower.json,package.json,.travis.yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | prepared 4 | archive.zip 5 | .DS_Store 6 | .publish 7 | custom-scripts.js 8 | rxjs.todo 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright © 2010–2015 Vadim Makeev, http://pepelsbey.net/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | --- 12 | 13 | # Лицензия MIT 14 | 15 | Copyright © 2010–2015 Вадим Макеев, http://pepelsbey.net/ 16 | 17 | Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации (в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно использовать Программное Обеспечение без ограничений, включая неограниченное право на использование, копирование, изменение, добавление, публикацию, распространение, сублицензирование и/или продажу копий Программного Обеспечения, также как и лицам, которым предоставляется данное Программное Обеспечение, при соблюдении следующих условий: 18 | 19 | Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части данного Программного Обеспечения. 20 | 21 | ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ, ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxJS: введение 2 | 3 | ## Видео 4 | 5 | [MoscowJS 42 – RxJS по косточкам](https://www.youtube.com/watch?v=3rEDHnqn-Cw) 6 | 7 | ## **Важно: пожалуйста, оцените выступлениe** 8 | 9 | Анкета на 5 минут: [https://goo.gl/forms/zxr41nHXqcHxaict2](https://goo.gl/forms/zxr41nHXqcHxaict2) 10 | 11 | ## Cсылки 12 | 13 | ### Observable 14 | - Введение в документацию [http://reactivex.io/rxjs/manual/overview.html](http://reactivex.io/rxjs/manual/overview.html) 15 | - Справочник операторов [https://www.learnrxjs.io/](https://www.learnrxjs.io/) 16 | - Понимание паттернов, из которых состоит Observable [http://anasfirdousi.com/understanding-observable-patterns-behind-observables-rxjs-rx.html](http://anasfirdousi.com/understanding-observable-patterns-behind-observables-rxjs-rx.html) 17 | 18 | ### Subjects и multicast 19 | - Типы subject-ов [https://medium.com/@poudanen/%D0%BF%D0%BE%D0%BD%D0%B8%D0%BC%D0%B0%D0%BD%D0%B8%D0%B5-rxjava-subject-publish-replay-behavior-%D0%B8-async-subject-35ad50cd1064](https://medium.com/@poudanen/%D0%BF%D0%BE%D0%BD%D0%B8%D0%BC%D0%B0%D0%BD%D0%B8%D0%B5-rxjava-subject-publish-replay-behavior-%D0%B8-async-subject-35ad50cd1064) 20 | - Hot vs Cold Observables [https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) 21 | - RxJS: Understanding the publish and share Operators [https://blog.angularindepth.com/rxjs-understanding-the-publish-and-share-operators-16ea2f446635](https://blog.angularindepth.com/rxjs-understanding-the-publish-and-share-operators-16ea2f446635) 22 | - Документация multicast оператора [http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-multicast](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-multicast) 23 | 24 | ### Egghead 25 | 26 | - Use Higher Order Observables in RxJS Effectively [https://egghead.io/courses/use-higher-order-observables-in-rxjs-effectively](https://egghead.io/courses/use-higher-order-observables-in-rxjs-effectively) 27 | - Save time avoiding common mistakes using RxJS [https://egghead.io/courses/save-time-avoiding-common-mistakes-using-rxjs](https://egghead.io/courses/save-time-avoiding-common-mistakes-using-rxjs) 28 | - RxJS Subjects and Multicasting Operators [https://egghead.io/courses/rxjs-subjects-and-multicasting-operators](https://egghead.io/courses/rxjs-subjects-and-multicasting-operators) 29 | 30 | ### Не попали в категории 31 | - Rx Visualizer – песочница по визуализации Observable [https://rxviz.com/](https://rxviz.com/) 32 | - RxJS: Don’t Unsubscribe [https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87](https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87) 33 | -------------------------------------------------------------------------------- /custom/main.js: -------------------------------------------------------------------------------- 1 | import {of} from 'rxjs'; 2 | import {delay, switchMap, takeUntil} from 'rxjs/operators'; 3 | import {startDemo} from './startDemo'; 4 | 5 | startDemo(); 6 | 7 | let START = new Date(); 8 | let isRunning = false; 9 | const STEP = 50; 10 | const TICK_MS = 500; 11 | const MAX_TIME_MS = 6000; 12 | const COLOR_SCHEME = [ 13 | '#FB000D', 14 | '#009D91', 15 | '#8AB32D' 16 | ]; 17 | 18 | const timelineTemplate = document.getElementById('timeline-template'); 19 | 20 | function createElementWithClass(className) { 21 | const element = document.createElement('div'); 22 | 23 | element.classList.add(className); 24 | 25 | return element; 26 | } 27 | 28 | function createTimelineElement() { 29 | const element = timelineTemplate.content.cloneNode(true); 30 | return element.querySelector('.timeline'); 31 | } 32 | 33 | function createEntryElement() { 34 | return createElementWithClass('entry'); 35 | } 36 | 37 | function createCompleteElement() { 38 | return createElementWithClass('entry-complete'); 39 | } 40 | 41 | function getTimeDiff() { 42 | return (new Date() - START) / TICK_MS; 43 | } 44 | 45 | function renderNext(value, timelineElement) { 46 | const newElement = createEntryElement(); 47 | 48 | newElement.textContent = value; 49 | 50 | const offset = getTimeDiff() * STEP; 51 | 52 | newElement.style.left = offset + 'px'; 53 | 54 | const last = timelineElement.lastElementChild; 55 | 56 | if (last) { 57 | const lastOffset = parseInt(last.style.left, 10); 58 | 59 | if (offset - lastOffset < 5) { 60 | newElement.classList.add('shift'); 61 | } 62 | } 63 | 64 | timelineElement.appendChild(newElement); 65 | } 66 | 67 | function renderComplete(timelineElement) { 68 | timelineElement.style.width = getTimeDiff() * STEP + 'px'; 69 | timelineElement.style.transitionDuration = '0s'; 70 | } 71 | 72 | function renderCompleteElement(timelineElement) { 73 | const newElement = createCompleteElement(); 74 | 75 | let offset = getTimeDiff() * STEP; 76 | 77 | const last = timelineElement.lastElementChild; 78 | 79 | if (last) { 80 | const lastOffset = parseInt(last.style.left, 10); 81 | 82 | if (offset - lastOffset < last.offsetWidth) { 83 | // offset += last.offsetWidth; 84 | newElement.classList.add('close-to-entry') 85 | } 86 | } 87 | 88 | newElement.style.left = offset + 'px'; 89 | 90 | timelineElement.appendChild(newElement); 91 | } 92 | 93 | function setTimeline(parentElement) { 94 | parentElement.appendChild(createTimelineElement()); 95 | 96 | const timelineElement = parentElement.lastElementChild; 97 | const index = (parentElement.childNodes.length - 1) % COLOR_SCHEME.length; 98 | 99 | // timelineElement.style.transitionDuration = MAX_TIME_MS + 'ms'; 100 | timelineElement.style.color = COLOR_SCHEME[index]; 101 | 102 | setTimeout(() => { 103 | timelineElement.style.width = '0px'; 104 | }, 0); 105 | 106 | setTimeout(() => { 107 | timelineElement.style.width = (((MAX_TIME_MS) / TICK_MS) * STEP) + 'px'; 108 | }, 100); 109 | 110 | 111 | parentElement.appendChild(timelineElement); 112 | 113 | return timelineElement; 114 | } 115 | 116 | function startStream(parentElement, input$, delayTime = 0) { 117 | const timelineElement = setTimeline(parentElement); 118 | 119 | const subscription = of(null) 120 | .pipe( 121 | delay(delayTime), 122 | switchMap(() => input$), 123 | takeUntil( 124 | of(null) 125 | .pipe( 126 | delay(MAX_TIME_MS) 127 | ) 128 | ) 129 | ) 130 | .subscribe({ 131 | next: (value) => { 132 | renderNext(value, timelineElement); 133 | }, 134 | complete: () => { 135 | renderCompleteElement(timelineElement); 136 | } 137 | }); 138 | 139 | const unsubscribe = subscription.unsubscribe; 140 | 141 | subscription.unsubscribe = function (...args) { 142 | renderComplete(timelineElement); 143 | 144 | unsubscribe.apply(this, args); 145 | }; 146 | 147 | return subscription; 148 | } 149 | 150 | function setSubjectTick1(subject) { 151 | subject.next(0); 152 | setTimeout(() => subject.next(1), 1000); 153 | setTimeout(() => subject.next(2), 2000); 154 | setTimeout(() => subject.next(3), 3000); 155 | setTimeout(() => subject.next(4), 4000); 156 | setTimeout(() => subject.complete(), 5000); 157 | } 158 | 159 | function run(stepFn, btnElement) { 160 | if (isRunning) { 161 | return; 162 | } 163 | 164 | START = new Date(); 165 | isRunning = true; 166 | btnElement.disabled = true; 167 | stepFn(); 168 | 169 | setTimeout(() => { 170 | isRunning = false; 171 | btnElement.disabled = false; 172 | }, MAX_TIME_MS); 173 | } 174 | 175 | function logToOutput(outputElement, value) { 176 | const logElement = document.createElement('div'); 177 | 178 | logElement.textContent = value || '\xa0'; 179 | 180 | outputElement.insertBefore(logElement, outputElement.firstChild); 181 | } 182 | 183 | function logAllToOutput(outputElement, items) { 184 | outputElement.innerHTML = ''; 185 | 186 | items.forEach((item) => { 187 | logToOutput(outputElement, item); 188 | }); 189 | } 190 | 191 | function fetchData(url) { 192 | if (!url) { 193 | return of([]); 194 | } 195 | 196 | const times = 1 + Math.floor(Math.random() * 7); 197 | const result = new Array(times) 198 | .fill(null) 199 | .map(() => getRandomWord(url)); 200 | 201 | return of(result) 202 | .pipe( 203 | delay(300 + Math.floor(Math.random() * 1000)) 204 | ); 205 | } 206 | 207 | function getRandomWord(source) { 208 | return source + '_' + getRandomChar() + getRandomChar() + getRandomChar(); 209 | } 210 | 211 | function getRandomChar() { 212 | const num = 97 + Math.floor(26 * Math.random()); 213 | 214 | return String.fromCharCode(num); 215 | } 216 | 217 | window.run = run; 218 | window.startStream = startStream; 219 | window.logToOutput = logToOutput; 220 | window.logAllToOutput = logAllToOutput; 221 | window.fetchData = fetchData; 222 | window.TICK_MS = TICK_MS; 223 | window.setSubjectTick1 = setSubjectTick1; 224 | 225 | -------------------------------------------------------------------------------- /custom/scripts.js: -------------------------------------------------------------------------------- 1 | let START = new Date(); 2 | let isRunning = false; 3 | const STEP = 50; 4 | const TICK_MS = 500; 5 | const MAX_TIME_MS = 6000; 6 | const COLOR_SCHEME = [ 7 | '#FB000D', 8 | '#009D91', 9 | '#8AB32D' 10 | ]; 11 | 12 | const timelineTemplate = document.getElementById('timeline-template'); 13 | 14 | function createElementWithClass(className) { 15 | const element = document.createElement('div'); 16 | 17 | element.classList.add(className); 18 | 19 | return element; 20 | } 21 | 22 | function createTimelineElement() { 23 | const element = timelineTemplate.content.cloneNode(true); 24 | return element.querySelector('.timeline'); 25 | } 26 | 27 | function createEntryElement() { 28 | return createElementWithClass('entry'); 29 | } 30 | 31 | function createCompleteElement() { 32 | return createElementWithClass('entry-complete'); 33 | } 34 | 35 | function getTimeDiff() { 36 | return (new Date() - START) / TICK_MS; 37 | } 38 | 39 | function renderNext(value, timelineElement) { 40 | const newElement = createEntryElement(); 41 | 42 | newElement.textContent = value; 43 | 44 | const offset = getTimeDiff() * STEP; 45 | 46 | newElement.style.left = offset + 'px'; 47 | 48 | const last = timelineElement.lastElementChild; 49 | 50 | if (last) { 51 | const lastOffset = parseInt(last.style.left, 10); 52 | 53 | if (offset - lastOffset < 5) { 54 | newElement.classList.add('shift'); 55 | } 56 | } 57 | 58 | timelineElement.appendChild(newElement); 59 | } 60 | 61 | function renderComplete(timelineElement) { 62 | timelineElement.style.width = getTimeDiff() * STEP + 'px'; 63 | timelineElement.style.transitionDuration = '0s'; 64 | } 65 | 66 | function renderCompleteElement(timelineElement) { 67 | const newElement = createCompleteElement(); 68 | 69 | let offset = getTimeDiff() * STEP; 70 | 71 | const last = timelineElement.lastElementChild; 72 | 73 | if (last) { 74 | const lastOffset = parseInt(last.style.left, 10); 75 | 76 | if (offset - lastOffset < last.offsetWidth) { 77 | // offset += last.offsetWidth; 78 | newElement.classList.add('close-to-entry') 79 | } 80 | } 81 | 82 | newElement.style.left = offset + 'px'; 83 | 84 | timelineElement.appendChild(newElement); 85 | } 86 | 87 | function setTimeline(parentElement) { 88 | parentElement.appendChild(createTimelineElement()); 89 | 90 | const timelineElement = parentElement.lastElementChild; 91 | const index = (parentElement.childNodes.length - 1) % COLOR_SCHEME.length; 92 | 93 | // timelineElement.style.transitionDuration = MAX_TIME_MS + 'ms'; 94 | timelineElement.style.color = COLOR_SCHEME[index]; 95 | 96 | setTimeout(() => { 97 | timelineElement.style.width = '0px'; 98 | }, 0); 99 | 100 | setTimeout(() => { 101 | timelineElement.style.width = (((MAX_TIME_MS) / TICK_MS) * STEP) + 'px'; 102 | }, 100); 103 | 104 | 105 | parentElement.appendChild(timelineElement); 106 | 107 | return timelineElement; 108 | } 109 | 110 | function startStream(parentElement, input$, delay = 0) { 111 | const timelineElement = setTimeline(parentElement); 112 | 113 | const subscription = Rx.Observable.of(null) 114 | .delay(delay) 115 | .switchMap(() => input$) 116 | .takeUntil(Rx.Observable.of(null).delay(MAX_TIME_MS)) 117 | .subscribe({ 118 | next: (value) => { 119 | renderNext(value, timelineElement); 120 | }, 121 | complete: () => { 122 | renderCompleteElement(timelineElement); 123 | } 124 | }); 125 | 126 | const unsubscribe = subscription.unsubscribe; 127 | 128 | subscription.unsubscribe = function (...args) { 129 | renderComplete(timelineElement); 130 | 131 | unsubscribe.apply(this, args); 132 | }; 133 | 134 | return subscription; 135 | } 136 | 137 | function setSubjectTick1(subject) { 138 | subject.next(0); 139 | setTimeout(() => subject.next(1), 1000); 140 | setTimeout(() => subject.next(2), 2000); 141 | setTimeout(() => subject.next(3), 3000); 142 | setTimeout(() => subject.next(4), 4000); 143 | setTimeout(() => subject.complete(), 5000); 144 | } 145 | 146 | function run(stepFn, btnElement) { 147 | if (isRunning) { 148 | return; 149 | } 150 | 151 | START = new Date(); 152 | isRunning = true; 153 | btnElement.disabled = true; 154 | stepFn(); 155 | 156 | setTimeout(() => { 157 | isRunning = false; 158 | btnElement.disabled = false; 159 | }, MAX_TIME_MS); 160 | } 161 | 162 | function logToOutput(outputElement, value) { 163 | const logElement = document.createElement('div'); 164 | 165 | logElement.textContent = value || '\xa0'; 166 | 167 | outputElement.insertBefore(logElement, outputElement.firstChild); 168 | } 169 | 170 | function logAllToOutput(outputElement, items) { 171 | outputElement.innerHTML = ''; 172 | 173 | items.forEach((item) => { 174 | logToOutput(outputElement, item); 175 | }); 176 | } 177 | 178 | function fetchData(url) { 179 | if (!url) { 180 | return Rx.Observable.of([]); 181 | } 182 | 183 | const times = 1 + Math.floor(Math.random() * 7); 184 | const result = new Array(times) 185 | .fill(null) 186 | .map(() => getRandomWord(url)); 187 | 188 | return Rx.Observable.of(result).delay(300 + Math.floor(Math.random() * 1000)); 189 | } 190 | 191 | function getRandomWord(source) { 192 | return source + '_' + getRandomChar() + getRandomChar() + getRandomChar(); 193 | } 194 | 195 | function getRandomChar() { 196 | const num = 97 + Math.floor(26 * Math.random()); 197 | 198 | return String.fromCharCode(num); 199 | } 200 | -------------------------------------------------------------------------------- /custom/startDemo.js: -------------------------------------------------------------------------------- 1 | import {fromEvent} from 'rxjs'; 2 | import {map, filter} from 'rxjs/operators'; 3 | 4 | 5 | export function startDemo() { 6 | fromEvent(document, 'keyup') 7 | .pipe( 8 | map(({code}) => code), 9 | filter(code => code === 'KeyA') 10 | ) 11 | .subscribe(res => { 12 | const btnElt = document.querySelector('.shower.full .slide.active .btn-play'); 13 | 14 | if (btnElt) { 15 | btnElt.click(); 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /custom/styles.css: -------------------------------------------------------------------------------- 1 | .rxjs-creation, 2 | .rxjs-operator, 3 | .rxjs-pipe, 4 | .rxjs-subscribe { 5 | font-weight: 700; 6 | } 7 | 8 | .rxjs-creation { 9 | color: #c62828; 10 | } 11 | 12 | .rxjs-pipe { 13 | /*color: #283593;*/ 14 | color: #00695c; 15 | } 16 | 17 | .rxjs-operator { 18 | color: #4f2c85; 19 | } 20 | 21 | .rxjs-subscribe { 22 | color: #00695c; 23 | } 24 | 25 | .stachka .slide::before { 26 | display: none; 27 | background: none; 28 | } 29 | 30 | .stachka .slide .slide-header::after { 31 | border-bottom: none; 32 | height: 4px; 33 | background: linear-gradient(90deg, #4f2c85, #ec0d8f); 34 | } 35 | 36 | .stachka .slide .cover-title { 37 | background: linear-gradient(90deg, #8c55e1, #ec0d8f); 38 | padding: 30px 50px; 39 | bottom: 60px; 40 | } 41 | 42 | .stachka .slide .cover-title h2 { 43 | margin: 0; 44 | line-height: 1; 45 | font-size: 60px; 46 | } 47 | 48 | .stachka .slide .cover-title::after { 49 | background: none; 50 | } 51 | 52 | .stachka .progress { 53 | background: linear-gradient(90deg, #4f2c85, #ec0d8f); 54 | height: 8px; 55 | border: none; 56 | clip: unset; 57 | padding: 0; 58 | left: 0; 59 | } 60 | 61 | .stachka .img-boxed { 62 | box-shadow: 0 0 0 1px #aaa; 63 | } 64 | 65 | .stachka .img-centered { 66 | display: block; 67 | margin: 0 auto; 68 | } 69 | 70 | /*.stachka .slide ul,*/ 71 | .stachka .slide p { 72 | font-size: 28px; 73 | line-height: 1.5; 74 | margin: 1em 0; 75 | } 76 | 77 | .stachka .slide pre code { 78 | /*font-size: 18px;*/ 79 | font-size: 20px; 80 | line-height: 1.8; 81 | } 82 | 83 | .stachka .slide pre.pre-centered-big code { 84 | font-size: 26px; 85 | } 86 | 87 | .pre-centered-big { 88 | display: inline-block; 89 | margin-top: 25px; 90 | } 91 | 92 | .stachka .slide .text-big { 93 | font-size: 30px; 94 | } 95 | 96 | /*.stachka .slide li {*/ 97 | /*line-height: 1.5;*/ 98 | /*margin: 10px 0;*/ 99 | /*}*/ 100 | 101 | .stachka .shout.shout-small { 102 | font-size: 60px; 103 | } 104 | 105 | .shower { 106 | font-size: 20px; 107 | } 108 | 109 | .slide .shout { 110 | font-size: 80px; 111 | } 112 | 113 | .slide-description small { 114 | margin-top: 10px; 115 | display: block; 116 | font-size: 14px; 117 | } 118 | 119 | .slide .slide-header { 120 | margin-bottom: 30px; 121 | } 122 | 123 | .slide-description { 124 | min-height: 70px; 125 | position: relative; 126 | padding-right: 35px; 127 | } 128 | 129 | .slide-description.big { 130 | min-height: 175px; 131 | } 132 | 133 | .intro { 134 | text-align: center; 135 | } 136 | 137 | .playground { 138 | padding: 10px 0; 139 | min-height: 54px; 140 | margin-bottom: 25px; 141 | } 142 | 143 | .playground.playground-two-streams { 144 | min-height: 98px; 145 | } 146 | 147 | .playground.playground-three-streams { 148 | min-height: 144px; 149 | } 150 | 151 | .playground-clock .entry { 152 | animation: clock ease-in-out .15s; 153 | animation-iteration-count: 21; 154 | transform-origin: center center; 155 | } 156 | 157 | @keyframes clock { 158 | 0% { 159 | padding-left: 0; 160 | transform: rotate(-5deg); 161 | } 162 | 163 | 50% { 164 | padding-left: 2px; 165 | transform: rotate(5deg); 166 | } 167 | } 168 | 169 | .text-hot { 170 | color: #C0504D; 171 | } 172 | 173 | .text-cold { 174 | color: #4F81BD; 175 | } 176 | 177 | .text-muted { 178 | color: #777; 179 | } 180 | 181 | .text-center { 182 | text-align: center; 183 | } 184 | 185 | .relative-container { 186 | position: relative; 187 | } 188 | 189 | .relative-image { 190 | position: absolute; 191 | box-shadow: 0 0 0 2px #434343; 192 | } 193 | 194 | .code-special { 195 | position: absolute; 196 | top: 0; 197 | right: 0; 198 | font-size: 11px; 199 | padding: 10px; 200 | padding-left: 35px; 201 | padding-bottom: 0; 202 | border: 1px solid #787878; 203 | opacity: .7; 204 | } 205 | 206 | .btn { 207 | font-size: 20px; 208 | padding: .3em .7em; 209 | background: #ffcf0f; 210 | color: #000000; 211 | border: none; 212 | cursor: pointer; 213 | transition: background .2s, box-shadow .2s; 214 | } 215 | 216 | .btn:hover { 217 | background: #f5c400; 218 | } 219 | 220 | .btn:active { 221 | background: #dbaf00; 222 | box-shadow: inset 0 0 .1em rgba(0, 0, 0, 0.5); 223 | } 224 | 225 | .btn:focus { 226 | outline: none; 227 | } 228 | 229 | .btn[disabled] { 230 | background: #c29b00; 231 | opacity: .7; 232 | cursor: default; 233 | } 234 | 235 | .btn.hidden { 236 | display: none; 237 | } 238 | 239 | .btn-play { 240 | position: absolute; 241 | right: 0; 242 | top: 0; 243 | } 244 | 245 | .timeline { 246 | color: red; 247 | margin: 15px 0; 248 | height: 4px; 249 | background-color: currentColor; 250 | position: relative; 251 | z-index: 0; 252 | transition: width linear 6s; 253 | width: 0; 254 | } 255 | 256 | .hidden { 257 | display: none !important; 258 | } 259 | 260 | .button { 261 | margin: 20px 0; 262 | display: inline-block; 263 | padding: 8px 16px; 264 | min-width: 120px; 265 | color: #009D91; 266 | border: 4px solid; 267 | font-size: 20px; 268 | font-weight: bold; 269 | text-transform: uppercase; 270 | border-radius: 8px; 271 | } 272 | 273 | .button:active { 274 | color: #FB000D; 275 | } 276 | 277 | .button:focus { 278 | outline: none; 279 | } 280 | 281 | .button .button-text, 282 | .button:active .button-text-active { 283 | display: inline; 284 | } 285 | 286 | .button .button-text-active, 287 | .button:active .button-text { 288 | display: none; 289 | } 290 | 291 | .timeline + .timeline { 292 | margin-top: 40px; 293 | } 294 | 295 | .entry { 296 | background-color: #ffffff; 297 | position: absolute; 298 | top: -14px; 299 | margin-left: -15px; 300 | left: 0; 301 | display: inline-block; 302 | vertical-align: top; 303 | box-sizing: border-box; 304 | width: 30px; 305 | height: 30px; 306 | line-height: 30px; 307 | /*font-size: 13px;*/ 308 | font-size: 16px; 309 | font-family: PT Mono, monospace, monospace; 310 | box-sizing: border-box; 311 | box-shadow: 0 0 0 2px currentColor; 312 | text-align: center; 313 | border-radius: 50%; 314 | z-index: 1; 315 | } 316 | 317 | .entry.shift { 318 | top: -6px; 319 | z-index: 2; 320 | background: rgba(255, 255, 255, 0.6); 321 | } 322 | 323 | .entry-complete { 324 | width: 0; 325 | height: 40px; 326 | box-shadow: 0 0 0 2px currentColor; 327 | position: absolute; 328 | top: -19px; 329 | z-index: 0; 330 | } 331 | 332 | .entry-complete.close-to-entry { 333 | 334 | } 335 | 336 | .entry-start { 337 | width: 0; 338 | height: 0; 339 | border: 4px solid transparent; 340 | border-left: 8px solid red; 341 | border-right: 0; 342 | position: absolute; 343 | } 344 | 345 | .slide-search { 346 | display: flex; 347 | } 348 | 349 | .search-code { 350 | padding-right: 10px; 351 | flex: 1 0 auto; 352 | font-size: 22px; 353 | } 354 | 355 | .search-playground { 356 | width: 200px; 357 | flex: 0 0 200px; 358 | border: 2px solid #4f2c85; 359 | } 360 | 361 | .search-input { 362 | display: block; 363 | width: 100%; 364 | border: none; 365 | border-bottom: 2px solid #4f2c85; 366 | font-size: inherit; 367 | line-height: 1; 368 | padding: 10px; 369 | height: 52px; 370 | } 371 | 372 | .search-input:focus { 373 | outline: none; 374 | box-shadow: inset 0 0 0 1px #ec0d8f; 375 | } 376 | 377 | .search-output { 378 | overflow-x: hidden; 379 | overflow-y: auto; 380 | padding: 10px; 381 | /*min-height: 54px;*/ 382 | /*max-height: 400px;*/ 383 | height: 380px; 384 | } 385 | 386 | .search-output.search-loading { 387 | background: url('../pictures/rxjs-logo.png') center center no-repeat; 388 | background-size: 140px auto; 389 | animation: 3s blink linear infinite; 390 | } 391 | 392 | @keyframes blink { 393 | 0% { 394 | opacity: .2; 395 | } 396 | 397 | 50% { 398 | opacity: .7; 399 | } 400 | 401 | 100% { 402 | opacity: .2; 403 | } 404 | } 405 | 406 | .search-output div { 407 | white-space: nowrap; 408 | text-overflow: ellipsis; 409 | overflow: hidden; 410 | } 411 | 412 | .emoji { 413 | display: inline-block; 414 | vertical-align: middle; 415 | font-size: 50px; 416 | } 417 | 418 | .nowrap { 419 | white-space: nowrap; 420 | } 421 | 422 | .excel { 423 | font-size: 32px; 424 | } 425 | 426 | .excel code { 427 | background: none; 428 | } 429 | 430 | .excel + .excel { 431 | border-top: 2px solid #434343; 432 | padding-top: 1.2em; 433 | } 434 | 435 | .push-pull-example.next { 436 | opacity: 0; 437 | transform: translateY(100px); 438 | transition: all .15s; 439 | } 440 | 441 | .push-pull-example.next.active { 442 | transform: translateY(0); 443 | opacity: 1; 444 | } 445 | 446 | .notify-clearfix { 447 | overflow: hidden; 448 | } 449 | 450 | .notify-wrap { 451 | position: absolute; 452 | padding: 0 0 20px 10px; 453 | left: 0; 454 | bottom: 0; 455 | } 456 | 457 | .notify-baloon { 458 | width: 320px; 459 | color: #fff; 460 | background: rgba(54, 56, 59, .96); 461 | box-shadow: 0 2px 3px rgba(0, 0, 0, .2); 462 | border-radius: 4px; 463 | font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica Neue, sans-serif; 464 | line-height: 1.154; 465 | font-weight: 400; 466 | font-size: 12.5px; 467 | } 468 | 469 | .notify-head { 470 | display: block; 471 | padding: 10px 12px 0; 472 | } 473 | 474 | .notify-wrap a { 475 | color: #bfe2fe; 476 | font-weight: 700; 477 | -webkit-font-smoothing: antialiased; 478 | -moz-osx-font-smoothing: grayscale; 479 | } 480 | 481 | .notify-close { 482 | float: right; 483 | display: block; 484 | padding: 10px; 485 | margin: -10px -12px -11px 12px; 486 | opacity: .65; 487 | outline: none; 488 | } 489 | 490 | .notify-title { 491 | font-size: 12.5px; 492 | color: inherit; 493 | padding: 0; 494 | margin: 0; 495 | font-weight: 700; 496 | } 497 | 498 | .notify-body { 499 | padding: 12px; 500 | } 501 | 502 | .notify-image-wrap { 503 | float: left; 504 | overflow: hidden; 505 | width: 50px; 506 | height: 50px; 507 | max-height: 50px; 508 | margin-right: 12px 509 | } 510 | 511 | .notify-image { 512 | width: 50px; 513 | height: 50px; 514 | border: 0; 515 | color: transparent 516 | } 517 | 518 | .notify-baloon-msg { 519 | overflow: hidden; 520 | line-height: 16px; 521 | } 522 | 523 | .modal-container { 524 | width: 480px; 525 | box-shadow: 0 2px 10px rgba(0, 0, 0, .35); 526 | font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica Neue, sans-serif; 527 | line-height: 1.154; 528 | font-weight: 400; 529 | font-size: 12.5px; 530 | background: #f7f7f7; 531 | border-radius: 4px; 532 | position: absolute; 533 | right: 20px; 534 | bottom: 20px; 535 | z-index: 20; 536 | } 537 | 538 | .modal-title-wrap { 539 | position: relative; 540 | padding: 0; 541 | background-color: #5b88bd; 542 | color: #fff; 543 | overflow: hidden; 544 | } 545 | 546 | .modal-close { 547 | float: right; 548 | padding: 17px 25px 21px 12px; 549 | width: 12px; 550 | height: 12px; 551 | opacity: 0.75; 552 | filter: alpha(opacity=75); 553 | cursor: pointer; 554 | outline: none 555 | } 556 | 557 | .modal-title { 558 | padding-left: 25px; 559 | font-size: 14px; 560 | color: #fff; 561 | height: 48px; 562 | line-height: 48px; 563 | overflow: hidden; 564 | text-overflow: ellipsis; 565 | white-space: nowrap 566 | } 567 | 568 | .modal-body { 569 | padding: 16px 25px; 570 | } 571 | 572 | .modal-user-info { 573 | overflow: hidden; 574 | } 575 | 576 | .modal-image-wrap { 577 | width: 50px; 578 | height: 50px; 579 | overflow: hidden; 580 | border-radius: 50%; 581 | float: left; 582 | } 583 | 584 | .modal-image { 585 | width: 50px; 586 | height: 50px; 587 | } 588 | 589 | .modal-user-text { 590 | padding-left: 13px; 591 | overflow: hidden; 592 | } 593 | 594 | .slide .modal-link { 595 | color: #42648b; 596 | text-decoration: underline; 597 | background: none; 598 | display: inline-block; 599 | line-height: 1.27em; 600 | padding: 6px 0 5px; 601 | font-weight: 700; 602 | -webkit-font-smoothing: antialiased; 603 | -moz-osx-font-smoothing: grayscale 604 | } 605 | 606 | .modal-user-status { 607 | display: block; 608 | color: #939393; 609 | margin: 0; 610 | padding: 0; 611 | } 612 | 613 | .modal-msg { 614 | background: #fff; 615 | color: #000; 616 | border: 1px solid #c0cad5; 617 | height: 80px; 618 | vertical-align: top; 619 | margin: 15px 0; 620 | overflow: hidden; 621 | outline: 0; 622 | cursor: text 623 | } 624 | 625 | .modal-controls { 626 | overflow: hidden; 627 | } 628 | 629 | .modal-btn { 630 | float: right; 631 | padding: 7px 16px 8px; 632 | margin: 0; 633 | font-size: 13px; 634 | display: inline-block; 635 | cursor: pointer; 636 | white-space: nowrap; 637 | outline: none; 638 | font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica Neue, sans-serif; 639 | vertical-align: top; 640 | line-height: 15px; 641 | text-align: center; 642 | text-decoration: none; 643 | background: none; 644 | background-color: #5181b8; 645 | color: #fff; 646 | border: 0; 647 | border-radius: 4px; 648 | box-sizing: border-box 649 | } 650 | 651 | .stachka.shower.full .slide .next.highlight { 652 | visibility: visible; 653 | } 654 | 655 | .stachka.shower.full .slide .next:not(.active) .next.highlight { 656 | visibility: hidden; 657 | } 658 | 659 | .stachka.shower.full .slide .next.highlight:not(.active) { 660 | background: none; 661 | } 662 | 663 | .stachka.shower.full .slide .next.invisible-next { 664 | display: none; 665 | } 666 | 667 | .stachka.shower.full .slide .next.invisible-next.active { 668 | display: block; 669 | } 670 | 671 | .invisible-next.active + .after-invisible-next { 672 | display: none; 673 | } 674 | 675 | .margin-0, 676 | .slide .margin-0 { 677 | margin: 0; 678 | } 679 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | All theme’s features are demonstrated in the [index.html](index.html) file. Use it as a reference while building your presentation. More detailed features overview follows below. 4 | 5 | - [Anatomy](#anatomy) 6 | - [Common](#common) 7 | - [Language](#language) 8 | - [Canvas](#canvas) 9 | - [Title](#title) 10 | - [Badge](#badge) 11 | - [Progress](#progress) 12 | - [Slide](#slide) 13 | - [Number](#number) 14 | - [Types](#types) 15 | - [White](#white) 16 | - [Black](#black) 17 | - [Grid](#grid) 18 | - [Content](#content) 19 | - [Header](#header) 20 | - [Paragraphs](#paragraphs) 21 | - [Inline](#inline) 22 | - [Quotes](#quotes) 23 | - [Lists](#lists) 24 | - [Columns](#columns) 25 | - [Tables](#tables) 26 | - [Code](#code) 27 | - [Elements](#elements) 28 | - [Cover](#cover) 29 | - [Shout](#shout) 30 | - [Place](#place) 31 | - [Notes](#notes) 32 | 33 | ## Anatomy 34 | 35 | Theme package consists of the following folders and files: 36 | 37 | 2. `fonts` folder with fonts in WOFF format. 38 | 3. `images` folder with decoration images. 39 | 4. `pictures` folder with sample pictures. 40 | 5. `styles` folder with built styles in 16×10 and 4×3 ratios. 41 | 6. `index.html` file with demonstration of all features. 42 | 43 | In addition to files theme’s repository contains source files: 44 | 45 | 1. `source` folder with font source files in TTF and design in [Sketch](http://bohemiancoding.com/sketch/). 46 | 2. `styles` folder also contains source styles in SCSS format. 47 | 48 | ## Common 49 | 50 | ### Language 51 | 52 | The main presentation language is set on the root element of the document, please note it and set the right one: 53 | 54 | 55 | 56 | 57 | Appropriate typography traditions are used based on this value. `lang` attribute could also be set on separate slides or elements. 58 | 59 | ### Canvas 60 | 61 | The root presentation element has the main `shower` class and additional mode class: `list` for the list and `full` for the full screen. `list` mode is usually set by default, but if there’s no one, it’ll be set to `list` anyway and slides will be opened in the list mode. If `full` is set instead of `list` then slides will be opened in the full screen mode. 62 | 63 | List: 64 | 65 | 66 | 67 | Full: 68 | 69 | 70 | 71 | Theme’s architecture is based on agreement that all presentation elements are nested in `shower` element and mode classes are hiding or showing needed elements depending on current mode. 72 | 73 | ### Caption 74 | 75 | Presentation title is marked with the `caption` element, which has following elements provided: `

` for the header, `

` for the description and also links. 76 | 77 |

78 |

Presentation Title

79 |

Yours Truly, Famous Inc.

80 |
81 | 82 | Caption is visible only in the list mode. Don’t forget to also specify presentation title in document’s `` element. 83 | 84 | ### Badge 85 | 86 | Badge with “Fork me on GitHub” link (or any other call to action) is marked with `badge` element. 87 | 88 | <footer class="badge"> 89 | <a href="…">Fork me on Github</a> 90 | </footer> 91 | 92 | Badge is visible only in the list mode. 93 | 94 | ### Progress 95 | 96 | Progress bar shows how much is left until presentation end and marked with `progress` element visible only in full screen mode: 97 | 98 | <div class="progress"></div> 99 | 100 | To remove it from presentation just remove this element from document. There’s no way to hide it for specific slides. 101 | 102 | ## Slide 103 | 104 | Slides are marked with `slide` class. Please don’t nest slides and don’t forget closing tags, things could go wrong. 105 | 106 | <section class="slide"> 107 | … 108 | </section> 109 | <section class="slide"> 110 | … 111 | </section> 112 | 113 | There are two slide ratios supported: 16×10 and 4×3. To enable needed one include appropriate file `screen-4x3.css` or `screen-16x10.css`. Wide screen 16×10 format is included by default. 114 | 115 | Slide width is 1024 px for the both ratios, for 16×10 height is 640 px, for 4×3 it’s 768. Bare in mind these sizes while preparing presentation pictures. In list mode slides are scaled down 2 or 4 times and in full screen mode they are scaled dynamically based on window size. 116 | 117 | ### Number 118 | 119 | Slide numbers help audience to remember slides for questions and open needed slide by changing number in address field. Numbers are generated automatically using CSS counters and could be turned off for specific slides. 120 | 121 | You can hide number manually: 122 | 123 | <section class="slide" id="off"> 124 | <style> 125 | #off::after { 126 | visibility: hidden; 127 | } 128 | </style> 129 | </section> 130 | 131 | Slide type `clear` could also hide slide number. 132 | 133 | ### Types 134 | 135 | Types are changing slide’s look. You can set type by adding class to the main `slide`. There are few built-in types available in the theme, you could also describe custom types for each presention or add it to your theme. 136 | 137 | #### White 138 | 139 | White type sets white background. 140 | 141 | <section class="slide white"> 142 | 143 | #### Black 144 | 145 | Black type sets black background. 146 | 147 | <section class="slide black"> 148 | 149 | Please note that black slide type doesn’t change text color. 150 | 151 | #### Clear 152 | 153 | Clear type turns off slide number. Use it when you need a pure slide. May be mixed with `white` or `black` type. 154 | 155 | <section class="slide clear"> 156 | 157 | #### Grid 158 | 159 | Grid set a background with two guide types: main magenta guides and additional cyan guides, setting margins, rows and columns. 160 | 161 | <section class="slide grid"> 162 | … 163 | </section> 164 | 165 | All theme elements are aligned by this grid and it’s recommended to follow it while changing or extending a theme. 166 | 167 | ### Content 168 | 169 | Simple content: headers, paragraphs, lists. 170 | 171 | #### Header 172 | 173 | Slide header is marked with `<h2>` element: 174 | 175 | <section class="slide"> 176 | <h2>Slide Header</h2> 177 | </section> 178 | 179 | We haven’t introduced next heading levels to not provoke slides complexity. 180 | 181 | #### Paragraphs 182 | 183 | Paragraphs are marked with `<p>` element. You could also make a note, less important part of a slide, by adding a `note` class to a paragraph: 184 | 185 | <section class="slide"> 186 | <p>Text</p> 187 | <p class="note">Note</p> 188 | </section> 189 | 190 | #### Inline 191 | 192 | There are following inline elements styled in the theme: 193 | 194 | - `<a>` is underlined; 195 | - `<strong>` and `<b>` are bold; 196 | - `<em>` and `<i>` are italic; 197 | - `<code>`, `<samp>`, and `<kbd>` are monospaced; 198 | - `<sup>` and `<sub>` make superscript and subscript indexes; 199 | - `<mark>` highlights text with background color. 200 | 201 | #### Quotes 202 | 203 | Quotes are marked with `<blockquote>` element which contains one or more paragraphs: 204 | 205 | <blockquote> 206 | <p>Flannel bicycle rights locavore selfies.</p> 207 | </blockquote> 208 | 209 | To add quote’s author wrap a quote to a `<figure>` element and put a caption in the `<figcaption>` right after: 210 | 211 | <figure> 212 | <blockquote> 213 | <p>Post-ironic fashion axe flexitarian</p> 214 | </blockquote> 215 | <figcaption>Yours Truly</figcaption> 216 | </figure> 217 | 218 | #### Lists 219 | 220 | For creating list you must use `ul` (`ol` for numerical list). 221 | 222 | <ol> 223 | <li>Literally viral vegan</li> 224 | <li>Wes Anderson chillwave Marfa 225 | <ul> 226 | <li>Retro meh brunch aesthetic</li> 227 | <li>Messenger bag retro cred</li> 228 | </ul> 229 | </li> 230 | </ol> 231 | 232 | You can also create list with inner navigation by adding `next` class to each elements following after element from each you want start navigation: 233 | 234 | <h2>Inner navigation</h2> 235 | <ol> 236 | <li>I'll be seen right away.</li> 237 | <li>Just navigate to next slide and you'll see others.</li> 238 | <li class="next">Hey! It's all okay ?</li> 239 | <li class="next"> ... </li> 240 | </ol> 241 | 242 | And even so: 243 | 244 | <h2>Benefits</h2> 245 | <ol> 246 | <li class="next">The most important advantage</li> 247 | <li class="next">Less important advantage</li> 248 | </ol> 249 | <h2 class="next">Disadvantages</h2> 250 | <ol class="next"> 251 | <li class="next">There's nothing here</li> 252 | <li class="next"> ... </li> 253 | </ol> 254 | 255 | #### Columns 256 | 257 | If you want to form text in two or three columns use `double` or `triple` class 258 | 259 | <p class="double"> 260 | Echo Park 8-bit sustainable umami deep v Kickstarter. 261 | </p> 262 | 263 | Also work with lists: 264 | 265 | <ul class="triple"> 266 | <li>Occupy locavore blog</li> 267 | <li>Mustache you haven’t heard of</li> 268 | <li>Something else</li> 269 | </ul> 270 | 271 | #### Tables 272 | 273 | Create table by using usual `table`, `tr`, `th` 274 | 275 | <table> 276 | <tr> 277 | <th scope="col">Gentrify</th> 278 | <th>Twee</th> 279 | </tr> 280 | <tr> 281 | <th scope="row">Messenger</th> 282 | <td>Mixtape</td> 283 | </tr> 284 | </table> 285 | 286 | Class `striped` stylizes your table: even rows will turn gray background 287 | 288 | <table class="striped"> 289 | 290 | #### Code 291 | 292 | `Code` tag define your program code 293 | 294 | <pre><code>function action() { 295 | // TODO 296 | return true; 297 | }</code></pre> 298 | 299 | If you want to add lines numbers use next construction: 300 | 301 | <pre> 302 | <code>function action() {</code> 303 | <code> // TODO</code> 304 | <code> return true;</code> 305 | <code>}<code> 306 | </pre> 307 | 308 | When neccessary emphasize that code is commented, you need to use span element with `comment` class; 309 | If you want to color part of code, wrap this part with `mark` to add yellow background and `mark` with `important` class to add red background; 310 | 311 | <pre><code>function <mark>action()</mark> { 312 | <span class="comment">// TODO<span> 313 | return <mark class="important">true</mark>; 314 | }</code></pre> 315 | 316 | ### Elements 317 | 318 | … 319 | 320 | #### Cover 321 | 322 | `Cover` class on img attribute indicates that picture will be background for slide 323 | 324 | <section class="slide"> 325 | <img class="cover" src="picture.png"> 326 | </section> 327 | 328 | To stretch the picture in width or height, you need to set a `width` or `height` class respectively; 329 | 330 | Use both classes `width height`, if you want to stretch the picture in width and height 331 | 332 | <img class="cover width" src="picture.png"> 333 | <img class="cover height" src="picture.png"> 334 | <img class="cover width height" src="picture.png"> 335 | 336 | Shortcut for `width`, `height`: 337 | 338 | <img class="cover w" src="picture.png"> 339 | <img class="cover h" src="picture.png"> 340 | 341 | To insert an image description, links to the author's site or other information use `figure` tag with `figcaption` 342 | 343 | <figure> 344 | <img class="cover" src="picture.png"> 345 | <figcaption class="white"> 346 | © Yours Truly 347 | </figcaption> 348 | </figure> 349 | 350 | #### Shout 351 | 352 | There are slides, which need to be described in only a few words. Usually they display a call for action, define common themes, link to project or something else. To stylize this text, use the `shout` class. 353 | 354 | <section class="slide"> 355 | <h2 class="shout">Shout</h2> 356 | </section> 357 | 358 | Add `grow` class to animate text from small to big size 359 | 360 | <section class="slide"> 361 | <h2 class="shout grow">Growing Shout</h2> 362 | </section> 363 | 364 | Or, on the contrary, for animate text size from big to small add `shrink` class. 365 | 366 | <section class="slide"> 367 | <h2 class="shout shrink">Shrinking Shout</h2> 368 | </section> 369 | 370 | #### Place 371 | 372 | Use `place` class on img attribute give same effect as `cover` class - set background image 373 | 374 | <section class="slide"> 375 | <img class="place" src="picture.png"> 376 | </section> 377 | 378 | If you want collocate picture at a certain side, you need to use `top` / `right` / `bottom` / `left` class as shown below 379 | 380 | <img class="place top" src="picture.png"> 381 | <img class="place right" src="picture.png"> 382 | <img class="place bottom" src="picture.png"> 383 | <img class="place left" src="picture.png"> 384 | 385 | You can also combine classes for location in corners: 386 | 387 | <img class="place top left" src="picture.png"> 388 | <img class="place top right" src="picture.png"> 389 | <img class="place bottom left" src="picture.png"> 390 | <img class="place bottom right" src="picture.png"> 391 | 392 | #### Notes 393 | 394 | When neccessary to add some notes for slide, you may use `footer` class, that hide your notes at all time and show them when you hover to slide: 395 | 396 | <section class="slide"> 397 | <p>Retro meh brunch aesthetic.</p> 398 | <footer class="footer"> 399 | <p>Cosby sweater Shoreditch.</p> 400 | </footer> 401 | </section> 402 | 403 | -------------------------------------------------------------------------------- /docs/images/logitech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/docs/images/logitech.png -------------------------------------------------------------------------------- /docs/images/material-canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/docs/images/material-canvas.png -------------------------------------------------------------------------------- /docs/images/material-printing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/docs/images/material-printing.png -------------------------------------------------------------------------------- /docs/images/ribbon-canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/docs/images/ribbon-canvas.png -------------------------------------------------------------------------------- /docs/images/ribbon-printing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/docs/images/ribbon-printing.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /docs/logitech.md: -------------------------------------------------------------------------------- 1 | # Logitech Spotlight 2 | 3 | There’s a brand new [Logitech Spotlight](http://www.logitech.com/en-us/product/spotlight-presentation-remote) presentation remote, which is already fully compatible with Shower: you can switch between slides, hover and click your presentation. But you can make the most of it by enabling two additional shortcuts in Logitech Presentation application, available for macOS and Windows. 4 | 5 | 1. [Download](http://support.logitech.com/en_us/software/logi-presentation) and install an app. 6 | 2. Go to the main menu and locate additional shortcuts options. 7 | 3. Change _Hold Next Button_ to `Cmd Enter` for macOS or `Shift F5` for Windows (just press it). 8 | 4. Change _Hold Back Button_ to `Esc` (just press it). 9 | 10 | <img src="images/logitech.png" width="300" alt="Logitech Presenter application"> 11 | 12 | You might have to enable accessibility access on macOS in order to use this features. App will ask you once it’s needed. 13 | 14 | Now when all is set you can: 15 | 16 | - Start your presentation by holding next button: from the first slide (if none selected) or from the current slide (if there’s one). 17 | - Exit full screen mode by holding back button. It might be useful when you need to find a certain slide and navigate the list. 18 | -------------------------------------------------------------------------------- /docs/printing.md: -------------------------------------------------------------------------------- 1 | # Printing 2 | 3 | You can export your presentation to PDF by sending it to print in list mode in Chrome and Opera desktop browsers. 4 | 5 | 1. Press `Cmd P` or `Ctrl P` 6 | 2. Select PDF in targets list 7 | 3. Save resulted file 8 | 9 | ![Printing dialog](images/ribbon-printing.png) 10 | 11 | ## Other options 12 | 13 | You could also try [Prince](http://princexml.com), [wkhtmltopdf](http://code.google.com/p/wkhtmltopdf) and such console utilities for printing, although not complete modern CSS features support and complicated configuration make them less convenient. 14 | -------------------------------------------------------------------------------- /docs/shortcuts.md: -------------------------------------------------------------------------------- 1 | # Keyboard Shortcuts 2 | 3 | You can navigate between slides, start and stop presentation using keyboard shortcuts. 4 | 5 | - [Start and Stop](#start-and-stop) 6 | - [Forward](#forward) 7 | - [Backward](#backward) 8 | - [First and Last](#first-and-last) 9 | 10 | ## Start and Stop 11 | 12 | - Start from the current slide: `Cmd Enter`, `Shift F5`, `Cmd Option P` 13 | - Start from the first slide `Cmd Shift Enter` 14 | - Stop: `Esc` key 15 | 16 | ## Forward 17 | 18 | - `Right` and `Down` arrows 19 | - `Page Down` key or `Fn Down` on Mac 20 | - `N` for “next”, `J` or `L` keys 21 | - `Enter` key 22 | - `Space` key during presentation 23 | 24 | ## Backward 25 | 26 | - `Left` and `Up` arrows 27 | - `Page Up` key or `Fn Up` on Mac 28 | - `P` for “previous”, `K` or `H` keys, like in vim 29 | - `Shift Enter` key 30 | - `Shift Space` key during presentation 31 | 32 | ## First and Last 33 | 34 | - First slide: `Home` key or `Fn Left` on Mac 35 | - Last slide: `End` key or `Fn Right` on Mac 36 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const fs = require('fs'); 3 | const gulp = require('gulp'); 4 | const merge = require('merge-stream'); 5 | const rename = require('gulp-rename'); 6 | const replace = require('gulp-replace'); 7 | const rsync = require('gulp-rsync'); 8 | const sequence = require('run-sequence'); 9 | const zip = require('gulp-zip'); 10 | const pages = require('gulp-gh-pages'); 11 | const browserSync = require('browser-sync').create(); 12 | const rollup = require('gulp-better-rollup'); 13 | const resolve = require('rollup-plugin-node-resolve'); 14 | 15 | gulp.task('scripts', () => { 16 | gulp.src([ 17 | './custom/main.js' 18 | ]) 19 | .pipe(rollup({ 20 | plugins: [ 21 | resolve({ 22 | customResolveOptions: { 23 | moduleDirectory: 'node_modules' 24 | } 25 | }), 26 | ], 27 | external: ['rxjs'] 28 | }, 29 | 'iife' 30 | )) 31 | .pipe(rename('custom-scripts.js')) 32 | .pipe(gulp.dest('./')); 33 | }); 34 | 35 | gulp.task('prepare', () => { 36 | 37 | const shower = gulp.src([ 38 | '**', 39 | '!docs{,/**}', 40 | '!node_modules{,/**}', 41 | '!prepared{,/**}', 42 | '!utils{,/**}', 43 | '!CONTRIBUTING.md', 44 | '!LICENSE.md', 45 | '!README.md', 46 | '!gulpfile.js', 47 | '!package.json', 48 | '!package-lock.json' 49 | ]) 50 | .pipe(replace( 51 | /(<link.*href=")(node_modules\/tinkoff-shower)([^\/]*)\/(.*\.(css|png)">)/g, 52 | '$1shower/themes/tinkoff$3/$4', {skipBinary: true} 53 | )) 54 | .pipe(replace( 55 | /(<link rel="stylesheet" href=")(node_modules\/shower-)([^\/]*)\/(.*\.css">)/g, 56 | '$1shower/themes/$3/$4', {skipBinary: true} 57 | )) 58 | .pipe(replace( 59 | /(<script src=")(node_modules\/shower-core\/)(shower.min.js"><\/script>)/g, 60 | '$1shower/$3', {skipBinary: true} 61 | )) 62 | .pipe(replace( 63 | /(<script src=")(node_modules\/rxjs\/bundles\/)(rxjs.umd.min.js"><\/script>)/g, 64 | '$1dist/$3', {skipBinary: true} 65 | )); 66 | 67 | 68 | const core = gulp.src([ 69 | 'shower.min.js' 70 | ], { 71 | cwd: 'node_modules/shower-core' 72 | }) 73 | .pipe(rename((path) => { 74 | path.dirname = 'shower/' + path.dirname; 75 | })); 76 | 77 | const rx = gulp.src([ 78 | 'rxjs.umd.min.js' 79 | ], { 80 | cwd: 'node_modules/rxjs/bundles' 81 | }) 82 | .pipe(rename((path) => { 83 | path.dirname = 'dist/' + path.dirname; 84 | })); 85 | 86 | const material = gulp.src([ 87 | '**', '!package.json' 88 | ], { 89 | cwd: 'node_modules/shower-material' 90 | }) 91 | .pipe(rename((path) => { 92 | path.dirname = 'shower/themes/material/' + path.dirname; 93 | })) 94 | 95 | const ribbon = gulp.src([ 96 | '**', '!package.json' 97 | ], { 98 | cwd: 'node_modules/shower-ribbon' 99 | }) 100 | .pipe(rename((path) => { 101 | path.dirname = 'shower/themes/ribbon/' + path.dirname; 102 | })); 103 | 104 | const tinkoff = gulp.src([ 105 | '**', '!package.json' 106 | ], { 107 | cwd: 'node_modules/tinkoff-shower' 108 | }) 109 | .pipe(rename((path) => { 110 | path.dirname = 'shower/themes/tinkoff/' + path.dirname; 111 | })); 112 | 113 | const themes = merge(tinkoff) 114 | .pipe(replace( 115 | /(<script src=")(\/shower-core\/)(shower.min.js"><\/script>)/, 116 | '$1../../$3', {skipBinary: true} 117 | )); 118 | 119 | return merge(shower, rx, core, themes) 120 | .pipe(gulp.dest('prepared')); 121 | 122 | }); 123 | 124 | gulp.task('zip', () => { 125 | return gulp.src('prepared/**') 126 | .pipe(zip('archive.zip')) 127 | .pipe(gulp.dest('.')); 128 | }); 129 | 130 | gulp.task('upload', () => { 131 | return gulp.src('prepared/**') 132 | .pipe(pages()) 133 | }); 134 | 135 | gulp.task('archive', (callback) => { 136 | sequence( 137 | 'prepare', 138 | 'zip', 139 | 'clean', callback 140 | ) 141 | }); 142 | 143 | gulp.task('publish', (callback) => { 144 | sequence( 145 | 'prepare', 146 | 'upload', 147 | 'clean', callback 148 | ) 149 | }); 150 | 151 | gulp.task('clean', () => { 152 | return del('prepared/**'); 153 | }); 154 | 155 | gulp.task('serve', ['scripts'], () => { 156 | browserSync.init({ 157 | ui: false, 158 | notify: false, 159 | port: 3000, 160 | server: { 161 | baseDir: '.' 162 | } 163 | }); 164 | 165 | gulp.watch('index.html').on('change', () => { 166 | browserSync.reload(); 167 | }); 168 | gulp.watch('custom/*.js', ['scripts']); 169 | }); 170 | 171 | gulp.task('default', ['serve']); 172 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <title>RxJS: введение 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 |
22 |

RxJS: введение

23 |
24 | 25 |
26 |
27 | RxJS 28 |
29 |
30 |

RxJS
введение

31 |
32 |
33 | 34 |
35 |

Привет

36 |
37 |
38 | 39 |
40 |
41 |
42 |

Андрей Алексеев

43 |

44 | 👨‍💻 45 | Tinkoff business 48 |

49 |

50 | 👨‍🏫 51 | Tinkoff Fintech School

54 |
55 |
56 |
57 | 58 |
59 |

О чем поговорим

60 |
61 |
    62 |
  • Реактивный подход
  • 63 |
  • Observable
  • 64 |
  • Операторы Rx
  • 65 |
  • Демо
  • 66 |
  • Subjects
  • 67 |
68 |
69 |
70 | 71 |
72 |

Реактивный подход

73 |
74 | A1 = 1; B1 = 3; C1 = A1 + B1 = 4 75 | 76 |
77 |
78 |

79 | A1 = 1; B1 = 3; 80 |
81 | 82 | C1 = A1 + B1; 83 | 84 |

85 |
86 |
87 |
88 |
89 | 90 |
91 |

Реактивный подход

92 |
93 | A1 = 1; B1 = 3; C1 = A1 + B1 = 4 94 | 95 |
96 |
97 |

98 | A1 = 1; B1 = 3; 99 |
100 | C1 = A1 + B1; 101 |
102 | 103 | C1 ~ 4; 104 | 105 |

106 |
107 |
108 |
109 |
110 | 111 |
112 |

Реактивный подход

113 |
114 | A1 = 2; B1 = 3; C1 = A1 + B1 = 4 115 | 116 |
117 |
118 |

119 | A1 = 1; B1 = 3; 120 |
121 | C1 = A1 + B1; 122 |
123 | C1 ~ 4; 124 |

125 |

126 | 127 | A1 = 2; 128 | 129 |

130 |
131 |
132 |
133 |
134 | 135 |
136 |

Реактивный подход

137 |
138 | A1 = 2; B1 = 3; C1 = A1 + B1 = 5 139 |
140 |
141 |

142 | A1 = 1; B1 = 3; 143 |
144 | C1 = A1 + B1; 145 |
146 | C1 ~ 4; 147 |

148 |

149 | A1 = 2; 150 |
151 | 152 | C1 ~ 5; 153 | 154 |

155 |
156 |
157 |
158 |
159 | 160 |
161 |

Реактивный подход

162 |
163 |
164 |
165 |

166 | Push-стратегия 167 |

168 |

169 | A1 = 1; B1 = 3; 170 |
171 | C1 = A1 + B1; 172 |
173 | C1 ~ 4; 174 |

175 |

176 | A1 = 2; 177 |
178 | C1 ~ 5; 179 |

180 |
181 |
182 |
183 |
184 |
185 |
186 | 187 |
188 |

Императивный подход

189 |
190 |
191 |
192 |

193 | Push-стратегия 194 |

195 |

196 | A1 = 1; B1 = 3; 197 |
198 | C1 = A1 + B1; 199 |
200 | C1 ~ 4; 201 |

202 |

203 | A1 = 2; 204 |
205 | C1 ~ 5; 206 |

207 |
208 |
209 |

210 | Pull-статегия 211 |

212 |

213 | A1 = 1; B1 = 3; 214 |
215 | C1 = A1 + B1; 216 |
217 | C1 ~ 4; 218 |

219 |

220 | A1 = 2; 221 |
222 | 223 | C1 ~ 4; 224 | 225 |

226 |
227 |
228 |
229 |
230 | 231 |
232 |

233 | Push / Pull 234 |

235 | 262 | 263 | 297 |
298 | 299 |
300 |

Push value

301 |
302 | Единственное значение 303 |
304 |
305 | 306 |
307 |

Promise — асинхронное значение

308 |
309 | Асинхронное значение — Promise 311 |
312 |
313 | 314 |
315 |

Множественное значение

316 |
317 | Множественное значение 319 |
320 |
321 | 322 |
323 |

Iterable

324 |
325 | Множественное значение - Iterable 327 |
328 |
329 | 330 |
331 |

И множественное, и асинхронное?

332 |
333 | Асинхронное и множественное значение 335 |
336 |
337 | 338 |
339 |

Observable

340 |
341 | Observable 343 |
344 |
345 | 346 |
347 |

348 | Поток событий 349 |

350 |
351 | 352 |
353 |

Поток событий

354 |
355 | Поток кликов 356 |
357 |
358 | 359 |
360 |

Поток событий

361 |
362 | Поток кликов 363 |
364 |
365 | 366 |
367 |

Поток событий

368 |
369 | Поток кликов 370 |
371 |
372 | 373 |
374 |

Поток событий

375 |
376 | Поток кликов 377 |
378 |
379 | 380 |
381 |

Поток событий

382 |
383 | Поток кликов 384 |
385 |
386 | 387 |
388 |

Поток событий

389 |
390 |
 391 |             from([{x: 11, y: 99}, {x: 91, y: 151}, {x: 152, y: 106}]).pipe(
 394 |                 map((value) => { ... }),
 395 |                 filter((value) => { ... }),
 396 |                 reduce((acc, curr) => { ... })
 397 |             
 402 |         
403 |
404 |
405 | 406 |
407 |

408 | Поток событий 409 |
~
410 | массив, 411 | распределенный 412 | во времени 413 |

414 |

415 |
416 | 417 |
418 |

419 | Observable 420 |
421 | Observer pattern + Iterator pattern 422 |

423 |
424 | 425 |
426 |

Observer pattern

427 |
428 | 429 |
 430 |                 class Subject {
 431 |                 
 436 |                     add(observer) {...}
 437 |                     remove(observer) {...}
 438 |                 
 445 |                 }
 446 |             
447 |
448 |
449 |
450 | 451 |
452 |

Observer pattern

453 |
454 |
455 | Observer 456 |
457 |
458 | 459 |
460 |

Observer pattern

461 |
462 |

Субъект изменяет значения. 463 |
464 | Также называется Observable, наблюдаемый объект.

465 |

Наблюдатель (Observer) реагирует на изменения.

466 |
467 |
468 | 469 |
470 |

Iterator pattern

471 |
472 | 473 |
 474 |                 class Iterator {
 475 |                 
 479 |                 
 485 |             
486 |
487 |
488 |
489 | 490 |
491 |

Iterator pattern

492 |
493 |
    494 |
  • последовательный обход составных объектов
  • 495 |
  • не раскрывается внутреннее представление
  • 496 |
497 | Observer 498 |
499 |
500 | 501 |
502 |

503 | Observable 504 |

505 |
506 | 507 |
508 |

Observable

509 |
510 |
511 | 512 |
513 | 514 |
 515 |             const stream$ = Observable.create((observer) => {
 516 |                     observer.next(1);
 517 |                     setTimeout(() => { observer.next(2);},    1000);
 518 |                     setTimeout(() => { observer.next(3);},    2000);
 519 |                     setTimeout(() => { observer.complete();}, 3000);
 520 |                 });
 521 |                 const subscribtion = stream$.subscribe({
 522 |                     next: (value) => renderNext(value),
 523 |                     error: (error) => renderError(error),
 524 |                     complete: () => renderComplete()
 525 |                 });
 526 |         
527 |
528 | 549 |
550 | 551 |
552 |

Observable

553 |
554 |

555 | Observable 556 | – наблюдаемый объект, доставляет изменения в подписку. 557 |

558 |
559 | 560 |
 561 |             const stream$ = Observable.create((observer) => {...});
 562 |              
 563 |             const subscribtion = stream$.subscribe(...);
 565 |         
566 |
567 |
568 | 569 |
570 |

Observable

571 |
572 |

573 | Observer 574 | – объект-наблюдатель, 575 |
576 | опциональные обработчики next/error/complete 577 |

578 |
579 | 580 |
 581 |             const subscribtion = stream$.subscribe(
 582 |                 {
 583 |                     next: (value) => renderNext(value),
 584 |                     error: (error) => renderError(error),
 585 |                     complete: () => renderComplete()
 586 |                 }
 587 |             );
 588 |             
 592 |         
593 |
594 |
595 | 596 |
597 |

Observable

598 |
599 |

600 | Функция создания 601 | определяет,
как наблюдатель 602 | будет получать значения. 603 |

604 |
605 | 606 |
 607 |             const stream$ = Observable.create((observer) => {
 608 |                 observer.next(1);
 609 |                 setTimeout(() => { observer.next(2);},    1000);
 610 |                 setTimeout(() => { observer.next(3);},    2000);
 611 |                 setTimeout(() => { observer.complete();}, 3000);
 612 |             });
 613 |             
614 | stream$.subscribe(renderNext); 615 |
616 |
617 | 625 |
626 | 627 |
628 |

Observable

629 | 630 |
 631 |             const stream$ = Observable.create((observer) => {
 632 |                 observer.next(1);
 633 |                 setTimeout(() => { observer.next(2);},    1000);
 634 |                 setTimeout(() => { observer.next(3);},    2000);
 635 |                 setTimeout(() => { observer.complete();}, 3000);
 636 |             });
 637 |             
638 | const promise = new Promise((resolve, reject) => { 639 | resolve(1); 640 | }); 641 |
642 |
643 |
644 | 645 |
646 |

Observable

647 |
648 |

649 | Subscribtion 650 | – подписка.
Старт получения значений. 651 |

652 |
653 | 654 |
 655 |             const stream$ = Observable.create((observer) => {...});
 656 |              
 657 |             const subscribtion = stream$.subscribe(...);
 658 |             subscribtion.unsubscribe();
 659 |         
660 |
661 |
662 | 663 |
664 |

Observable

665 |
666 |

667 | unsubscribe – завершение получения значений. 668 |

669 |
670 | 671 |
 672 |             const stream$ = Observable.create((observer) => {
 673 |                 const interval = setInterval(() => observer.next(1), 1000);
 674 |             
 680 |             });
 681 |              
 682 |             const subscribtion = stream$.subscribe(...);
 683 |             subscribtion.unsubscribe();
 684 |         
685 |
687 | 688 |
689 |

Observable

690 |
691 |

692 | Нет подписки — нет потока 693 |

694 |
695 | 696 |
 697 |             const stream$ = Observable.create((observer) => {...});
 698 |             
 702 |         
703 |
704 |
705 | 706 |
707 |

Observable

708 |
709 |

710 | Каждый observer создаст свой поток значений 711 |
712 | Подписки не зависят друг от друга 713 |

714 |
715 | 716 |
 717 |             const stream$ = Observable.create((observer) => {...});
 718 |             
 722 |             const subscribtionB = stream$.subscribe(observerBC);
 723 |             const subscribtionC = stream$.subscribe(observerBC);
 724 |         
725 |
726 |
727 | 728 |
729 |

Observable

730 | 740 | 741 |
 742 |             
 750 |             
 757 |         
758 |
759 |
760 | 761 | 762 |
763 |

764 | Ок, что дальше? 765 |

766 |
767 | 768 | 769 |
770 |

Дальше — 120+ операторов. 120, Карл!

771 |
772 |
    773 |
  • cоздание
  • 774 |
  • преобразование одного потока
  • 775 |
  • комбинирование потоков
  • 776 |
  • Observable высшего порядка
  • 777 |
778 |
779 |
780 | 781 |
782 |

Cоздание Observable

783 |
784 |
 785 |             import {of} from 'rxjs';
 786 |             of('foo');
 787 |             // ---foo-|
 788 |             
 794 |             
 800 |         
801 | 802 |
803 |
804 | 805 |
806 |

Cоздание Observable

807 |
808 |
 809 |             import {from} from 'rxjs';
 810 |             const promise = new Promise((resolve) => { resolve('foo')});
 811 |             from(promise);
 812 |             // ----------------foo-|
 813 |             
 820 |         
821 | 822 |
823 |
824 | 825 |
826 |

Операторы преобразования

827 |
828 |
829 | 830 |
831 | 832 |
 833 |             import {take, filter} from 'rxjs/operators';
 835 |             timer(0, 500).pipe(
 836 |                 take(12)
 837 |             ).subscribe(renderNext);
 838 |             
 845 |         
846 |
847 | 867 |
868 | 869 |
870 |

Pipe

871 |
872 |
 873 |             .pipe(
 874 |         
875 | pipe в реальном мире 877 |
 878 |             ).subscribe(...)
 879 |         
880 |
881 |
882 | 883 |
884 |

Rx Marble Diagram

885 |
886 | Rx Marble Diagram: filter 888 |
 889 |             ---2--30--22--5--60--1-----|
 890 |             filter(x => x > 10)         
 891 |             ------30--22-----60--------|
 892 |         
893 |
894 |
895 | 896 |
897 |

Операторы преобразования

898 |
899 |
900 | 901 |
 902 |             timer(0, 250).pipe(
 903 |                 take(40),
 904 |                 filter(() => Math.random() < 0.3),
 905 |                 debounceTime(500)
 906 |             ).subscribe(renderNext);
 907 |         
908 |
909 | 930 |
931 | 932 |
933 |

Комбинирование: merge

934 |
935 |
936 | 937 |
 938 |             const streamA$ = timer(0, 1500).pipe(take(4));
 940 |             const streamB$ = timer(800, 1000).pipe(
 942 |                 take(6),
 943 |                 map((i) => 'abcdef'[i])
 944 |             );
 945 |             merge(streamA$, streamB$)
 946 |                 .subscribe(renderNext);
 947 |         
948 |
949 | 972 |
973 | 974 |
975 |

Комбинирование: combineLatest

976 |
977 |
978 | 979 |
 980 |             const streamA$ = timer(0, 1500).pipe(take(4));
 982 |             const streamB$ = timer(800, 1000).pipe(
 984 |                 take(6),
 985 |                 map((i) => 'abcdef'[i])
 986 |             );
 987 |             combineLatest(streamA$, streamB$).pipe(
 989 |                 map(([a, b]) => a + b)
 990 |             ).subscribe(renderNext);
 991 |         
992 |
993 | 1019 |
1020 | 1021 |
1022 |

Операторы высшего порядка: mergeMap

1023 |
1024 |
1025 | 1026 |
1027 |             timer(0, 1500).pipe(
1028 |                 take(3),
1029 |                 map((i) => 'abcd'[i]),
1030 |                 mergeMap(letter => {
1031 |                     return timer(0, 450).pipe(
1033 |                         take(6),
1034 |                         map(digit => letter + digit));
1035 |                 })
1036 |             ).subscribe(renderNext);
1037 |         
1038 |
1039 | 1065 |
1066 | 1067 |
1068 |

Операторы высшего порядка: switchMap

1069 |
1070 |
1071 | 1072 |
1073 | 1074 |
1075 |             timer(0, 1500).pipe(
1076 |                 take(3),
1077 |                 map((i) => 'abcd'[i]),
1078 |                 switchMap(letter => {
1079 |                     return timer(0, 450).pipe(
1081 |                         take(6),
1082 |                         map(digit => letter + digit));
1083 |                 })
1084 |             ).subscribe(renderNext);
1085 |         
1086 |
1087 | 1113 |
1114 | 1115 |
1116 |

Rx Marble Diagram

1117 |
1118 | Rx Marble Diagram: switchMap 1123 |
1124 |
1125 | 1126 | 1127 |
1128 |

1129 | SwitchMap 1130 |

1131 |
1132 | 1133 |
1134 |

Promise <--> Observable

1135 |
1136 |
1137 | 1138 |
1139 |             const getPromise = (value) => new Promise((resolve) => {
1140 |                 setTimeout(() => {
1141 |                     resolve(value);
1142 |                 }, 1500);
1143 |             });
1144 |              
1145 |             from(getPromise('⏰'))
1146 |                 .subscribe(renderNext);
1147 |         
1148 |
1149 | 1165 |
1166 | 1167 |
1168 |

SwitchMap + Promise

1169 |
1170 |
1171 | 1172 |
1173 | 1174 |
1175 |             const streamA$ = Observable.create((observer) => {
1176 |                 observer.next(1);
1177 |                 setTimeout(() => { observer.next(2);}, 2000);
1178 |                 setTimeout(() => { observer.next(3);}, 3000);
1179 |                 setTimeout(() => { observer.complete()}, 4000);
1180 |             })
1181 |             const streamB$ = streamA$.pipe(
1182 |                 switchMap((value) => getPromise(value))
1183 |             ).subscribe(renderNext);
1184 |         
1185 |
1186 | 1219 |
1220 | 1221 | 1222 |
1223 |

SwitchMap и отписки

1224 |
1225 |

SwitchMap автоматически отписывает внутренний поток — вызывает subscription.unsubscribe() 1227 |

1228 |

Можем написать отменяемые запросы:

1229 |
    1230 | 1231 | 1232 | 1233 |
1234 |
1235 |
1236 | 1237 |
1238 |

Пример: отменяемый fetch

1239 |
1240 |
1241 |             const request$ = Observable.create((observer) => {
1242 |             
1246 |             
1250 |             
1256 |             
1262 |             });
1263 |         
1264 |
1265 |
1266 | 1267 |
1268 |

SwitchMap и запросы

1269 |
1270 |
1271 | Отмена fetch-запросов 1272 |
1273 |
1274 | 1275 |
1276 |

1277 | ДЕМО 1278 |

1279 |
1280 | 1281 |
1282 |

Демо — поиск

1283 |
1284 | демо 1285 |
1286 |
1287 | 1288 |
1289 |

Демо: поток ввода

1290 | 1306 | 1307 | 1336 |
1337 | 1338 |
1339 |

Демо: дебаунс

1340 | 1355 | 1356 | 1387 |
1388 | 1389 |
1390 |

Демо: фильтрация совпадений

1391 | 1407 | 1408 | 1440 |
1441 | 1442 |
1443 |

Демо: запрос данных

1444 | 1461 | 1462 | 1495 |
1496 | 1497 |
1498 |

Демо: лоадер

1499 | 1518 | 1519 | 1560 |
1561 | 1562 |
1563 |

Демо: результаты

1564 | 1578 |
1579 | 1580 |
1581 |

Демо: результаты

1582 |
1583 |

Работаем с потоками,
1584 | а не с промежуточными состояниями

1585 |
    1586 |
  • видна вся последовательность обработки
  • 1587 |
  • минимум сайд эффектов
  • 1588 |
  • самодостаточный компонент
  • 1589 |
1590 |
1591 |
1592 | 1593 |
1594 |

1595 | SUBJECTS 1596 |

1597 |
1598 | 1599 |
1600 |

1 подписка - 1 поток значений

1601 |
1602 |
1603 | 1604 |
1605 | 1606 |
1607 |             const stream$ = timer(0, 500).pipe(
1608 |                     take(5),
1609 |                     map(() => random()));
1610 |                 stream$.subscribe(/* ... */);
1611 |                 stream$.subscribe(/* ... */);
1612 |                 
1617 |         
1618 |
1619 | 1635 |
1636 | 1637 |
1638 |

Subject

1639 |
1640 |

Объединение Observable и Observer.
Передает значение всем подписчикам

1641 |
1642 |
1643 | 1644 |
1645 |                 const subject = new Subject();
1646 |                  
1647 |                 subject.next(0);
1648 |                 setTimeout(() => subject.next(1), 1000);
1649 |                 setTimeout(() => subject.next(2), 2000);
1650 |                 setTimeout(() => subject.next(3), 3000);
1651 |                 setTimeout(() => subject.next(4), 4000);
1652 |                 setTimeout(() => subject.complete(), 5000);
1653 |             
1654 |
1655 |
1656 |
1657 | 1658 |
1659 |

Subject

1660 |
1661 |
1662 | 1663 |
1664 |
1665 | 1666 |
1667 |                 const subject = new Subject();
1668 |                 setSubjectTick(subject);
1669 |                  
1670 |                 subject.subscribe();
1671 |                 setTimeout(() => subject.subscribe(/* ... */), 1500);
1672 |                 setTimeout(() => subject.subscribe(/* ... */), 2500);
1673 |         
1674 |
1675 |
1676 | 1689 |
1690 | 1691 | 1692 | 1693 | 1694 | 1695 | 1696 | 1697 | 1698 | 1699 | 1700 | 1701 |
1702 |

Пример - автоотписка

1703 |
1704 |
1705 |             const subscription = streamA$.pipe(
1706 |                 map(...),
1707 |                 switchMap(...)
1708 |             ).subscribe(...);
1709 |             
1721 |         
1722 |
1723 |
1724 | 1725 |
1726 |

Пример - автоотписка

1727 |
1728 |
1729 |             const destroy$ = new Subject();
1730 |             
1738 |             
1743 |         
1744 |
1745 |
1746 | 1747 |
1748 |

Пример - авторизация

1749 |
1750 |
1751 |             class StateService {
1752 |               private authSubject = new BehaviorSubject<boolean>(false);
1753 |             
1759 |             }
1760 |         
1761 |
1762 |
1763 | 1764 |
1765 |

Пример - авторизация

1766 |
1767 |
1768 |             class StateService {
1769 |               private authSubject = new BehaviorSubject<boolean>(false);
1770 |               setAuthState(state: boolean) {...}
1771 |             
1777 |             }
1778 |         
1779 |
1780 |
1781 | 1782 |
1783 |

Пример - авторизация

1784 |
1785 |
1786 |             class StateService {
1787 |               private authSubject = new BehaviorSubject<boolean>(false);
1788 |               setAuthState(state: boolean) {...}
1789 |               getStateChange(): Observable<boolean> {...}
1790 |              
1791 |             
1796 |             }
1797 |         
1798 |
1799 |
1800 | 1801 |
1802 |

Пример - кеширование

1803 |
1804 |
1805 |             let cachingSubject;
1806 |              
1807 |             getResource() {
1808 |             
1821 |             
1825 |             }
1826 |         
1827 |
1828 |
1829 | 1830 |
1831 |

1832 | ИТОГИ 1833 |

1834 |
1835 | 1836 |
1837 |

Итоги

1838 |
1839 |
    1840 |
  • Push / pull
  • 1841 | 1842 | 1843 | 1844 | 1845 | 1846 | 1847 | 1848 |
1849 |
1850 |
1851 | 1852 |
1853 |

Спасибо!

1854 |
1855 |
1856 | https://github.com/aalexeev239/rxjs-intro 1857 |
1858 |

github.com/aalexeev239/rxjs-intro 1860 |

1861 |
1862 |
1863 | 1864 | 1867 | 1868 |
1869 | 1870 | 1871 | 1872 | 1873 | 1874 | 1875 | 1876 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-intro-talk", 3 | "description": "RxJS intro", 4 | "version": "0.0.1", 5 | "author": { 6 | "name": "Andrey Alexeev", 7 | "url": "http://aalexeev.ru" 8 | }, 9 | "dependencies": { 10 | "rxjs": "^6.2.1", 11 | "shower-core": "^2.1.0", 12 | "shower-material": "^1.1.0", 13 | "tinkoff-shower": "git+git@github.com:lekzd/tinkoff-shower.git" 14 | }, 15 | "devDependencies": { 16 | "browser-sync": "^2.18.12", 17 | "del": "^3.0.0", 18 | "fs": "0.0.2", 19 | "gulp": "^3.9.1", 20 | "gulp-better-rollup": "^3.3.0", 21 | "gulp-gh-pages": "^0.5.4", 22 | "gulp-rename": "^1.2.2", 23 | "gulp-replace": "^0.6.1", 24 | "gulp-rsync": "0.0.8", 25 | "gulp-zip": "^4.0.0", 26 | "merge-stream": "^1.0.0", 27 | "path-exists-cli": "^1.0.0", 28 | "rollup-plugin-node-resolve": "^3.3.0", 29 | "run-sequence": "^2.1.0" 30 | }, 31 | "scripts": { 32 | "start": "gulp", 33 | "prepare": "gulp prepare", 34 | "archive": "gulp archive", 35 | "publish": "gulp publish", 36 | "test": "npm run prepare && ls prepared && npm run archive && path-exists archive.zip" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pictures/2requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/2requests.png -------------------------------------------------------------------------------- /pictures/author.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/author.png -------------------------------------------------------------------------------- /pictures/demo-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/demo-2.gif -------------------------------------------------------------------------------- /pictures/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/favicon-16x16.png -------------------------------------------------------------------------------- /pictures/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/favicon-32x32.png -------------------------------------------------------------------------------- /pictures/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/favicon-96x96.png -------------------------------------------------------------------------------- /pictures/fetch-cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/fetch-cancel.png -------------------------------------------------------------------------------- /pictures/iterator-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/iterator-pattern.png -------------------------------------------------------------------------------- /pictures/observer-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/observer-pattern.png -------------------------------------------------------------------------------- /pictures/palpatin-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/palpatin-50.png -------------------------------------------------------------------------------- /pictures/pipe-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/pipe-3.gif -------------------------------------------------------------------------------- /pictures/rx-stachka-table-1-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-1-bg-2.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-1-bg-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-1-bg-3.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-1.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-2.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-3.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-4.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-5.png -------------------------------------------------------------------------------- /pictures/rx-stachka-table-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rx-stachka-table-6.png -------------------------------------------------------------------------------- /pictures/rxJs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxJs.png -------------------------------------------------------------------------------- /pictures/rxjs-clicks-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-clicks-1.png -------------------------------------------------------------------------------- /pictures/rxjs-clicks-2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-clicks-2-2.png -------------------------------------------------------------------------------- /pictures/rxjs-clicks-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-clicks-2.png -------------------------------------------------------------------------------- /pictures/rxjs-clicks-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-clicks-3.png -------------------------------------------------------------------------------- /pictures/rxjs-clicks-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-clicks-4.png -------------------------------------------------------------------------------- /pictures/rxjs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-logo.png -------------------------------------------------------------------------------- /pictures/rxjs-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxjs-qr.png -------------------------------------------------------------------------------- /pictures/rxmarbles-combine-latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxmarbles-combine-latest.png -------------------------------------------------------------------------------- /pictures/rxmarbles-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxmarbles-filter.png -------------------------------------------------------------------------------- /pictures/rxmarbles-switch-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/rxmarbles-switch-map.png -------------------------------------------------------------------------------- /pictures/table-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/table-1.png -------------------------------------------------------------------------------- /pictures/table-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/table-2.png -------------------------------------------------------------------------------- /pictures/table-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aalexeev239/rxjs-intro/edc61370aabb0491f69e817e3f74c133560cd3a5/pictures/table-3.png --------------------------------------------------------------------------------