├── .gitignore ├── API.md ├── HISTORY.md ├── README.md ├── examples ├── deferStop │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ └── client │ │ └── main.coffee ├── slide-menu │ ├── .gitignore │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── README.md │ └── client │ │ ├── main.coffee │ │ └── main.html └── tutorial │ ├── .gitignore │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ └── client │ ├── main.coffee │ ├── main.css │ └── main.html ├── lib └── stream.coffee └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .versions -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Tracker.stream API 2 | 3 | Create a stream with an optional 4 | 5 | ```coffee 6 | Tracker.stream = (initialValue=undefined) -> 7 | ``` 8 | 9 | A curried function for merging streams into one 10 | 11 | ```coffee 12 | Tracker.mergeStreams = (streams...) -> 13 | ``` 14 | 15 | Create a stream from an event on an element 16 | 17 | ```coffee 18 | Tracker.eventStream = (eventName, element) -> 19 | ``` 20 | 21 | A Blaze prototype function for creating event streams that automatically stop onDestroyed 22 | 23 | ```coffee 24 | Blaze.TemplateInstance.prototype.eventStream = (eventName, elementSelector, global=false) -> 25 | ``` 26 | 27 | Create a Blaze prototype for creating streams that automatically stop onDestroyed 28 | 29 | ```coffee 30 | Blaze.TemplateInstance.prototype.stream = (initialValue=undefined) -> 31 | ``` 32 | 33 | Add a value to a stream 34 | 35 | ```coffee 36 | Tracker.stream::set = (x) -> 37 | ``` 38 | 39 | Get a value from a stream 40 | 41 | ```coffee 42 | Tracker.stream::get = () -> 43 | ``` 44 | 45 | Stop a stream, i.e. its subscription, and its subscribers. 46 | 47 | ```coffee 48 | Tracker.stream::stop = () -> 49 | ``` 50 | 51 | Map the stream across a function 52 | 53 | ```coffee 54 | Tracker.stream::map = (func) -> 55 | ``` 56 | 57 | Create a dupelicate stream 58 | 59 | ```coffee 60 | Tracker.stream::copy = () -> 61 | ``` 62 | 63 | Filter the stream based on a function 64 | 65 | ```coffee 66 | Tracker.stream::filter = (func) -> 67 | ``` 68 | 69 | Reduce a stream 70 | 71 | ```coffee 72 | Tracker.stream::reduce = (initialValue, func) -> 73 | ``` 74 | 75 | Filter consecutive duplicate values 76 | 77 | ```coffee 78 | Tracker.stream::dedupe = (func) -> 79 | ``` 80 | 81 | The most recent value of a stream with a minimum amount of 82 | time since the last value 83 | 84 | ```coffee 85 | Tracker.stream::debounce = (ms) -> 86 | ``` 87 | 88 | Merge with another stream into a new stream 89 | 90 | ```coffee 91 | Tracker.stream::merge = (anotherStream) -> 92 | ``` 93 | 94 | Pipe to a new stream only when another stream is falsy value. 95 | 96 | ```coffee 97 | Tracker.stream::unless = (anotherStream) -> 98 | ``` 99 | 100 | Stop on the next event from anotherStream. The following stop events stop after a `Meteor.defer` ensuring that any dependancies will update. 101 | 102 | ```coffee 103 | Tracker.stream::stopWhen = (anotherStream, func) -> 104 | ``` 105 | 106 | Stop stream after some time 107 | 108 | ```coffee 109 | Tracker.stream::stopAfterMs = (ms, func) -> 110 | ``` 111 | 112 | Stop stream after N values 113 | 114 | ```coffee 115 | Tracker.stream::stopAfterN = (number, func) -> 116 | ``` 117 | 118 | Alias for stream.copy().stopWhen 119 | 120 | ```coffee 121 | Tracker.stream::takeUntil = (anotherStream, func) -> 122 | ``` 123 | 124 | Alias for stream.copy().stopAfterMs 125 | 126 | ```coffee 127 | Tracker.stream::takeForMs = (ms, func) -> 128 | ``` 129 | 130 | Alias for stream.copy().stopAfterN 131 | 132 | ```coffee 133 | Tracker.stream::takeN = (number, func) -> 134 | ``` 135 | 136 | Some other aliases 137 | ```coffee 138 | Tracker.stream::push = Tracker.stream::set 139 | Tracker.stream::read = Tracker.stream::get 140 | Tracker.stream::completed = Tracker.stream::stop 141 | Tracker.stream::forEach = Tracker.stream::map 142 | Tracker.stream::throttle = Tracker.stream::debounce 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | 3 | Updated the API to be more consistent. More explicit about methods that stop the current stream by prefixing those methods with 'stop' whereas methods prefixed with take merely copy the stream and then call the associated stop method. The crucial non-backwards compatibility is the `takeUntil` method which does not stop the current stream. Now you must use `stopWhen` to stop the current stream. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tracker Streams 2 | 3 | This package uses Tracker to build "observable streams" for Meteor. [Check out the live demo](http://tracker-streams.meteor.com). Also checkout the [swipe menu drawer demo](http://tracker-streams-menu.meteor.com). 4 | 5 | If you haven't heard about observable streams, then [check out this talk](https://www.youtube.com/watch?v=XRYN2xt11Ek). 6 | For a more hands on introduction, check out [this interactive tutorial](http://jhusain.github.io/learnrx/). 7 | 8 | ## Getting Started 9 | 10 | meteor add ccorcos:tracker-streams 11 | 12 | You can create your own stream using 13 | 14 | ```coffee 15 | numbers = Tracker.stream() 16 | ``` 17 | 18 | You can create new streams using `map`, `forEach`, `filter`, etc. 19 | 20 | ```coffee 21 | times10 = myStream.map (x) -> x*10 22 | times10.forEach (x) -> console.log x 23 | ``` 24 | 25 | Now you push to the stream by setting the stream's reactive variable. 26 | 27 | ```coffee 28 | numbers.value.set(10) 29 | # log: 100 30 | numbers.value.set(2) 31 | # log: 20 32 | ``` 33 | 34 | Tracker is pretty amazing and you could say that this isn't that useful. 35 | But what about when we create streams from events? 36 | 37 | Suppose we want to make an element draggable. We can do this by creating 38 | an event stream for mousedown and mouseup events. Then after mousedown, 39 | we can create a mousemove stream which completes on the next mouseup event 40 | using `.takeUntil`. Check it out: 41 | 42 | ```coffee 43 | Template.drag.rendered = -> 44 | # create the mouseDown and mouseUp streams that will 45 | # be automatically completed on Template.destroyed. 46 | mouseDown = @eventStream("mousedown", ".draggable") 47 | mouseUp = @eventStream("mouseup", ".draggable") 48 | 49 | self = this 50 | mouseDown.map (e) -> 51 | # on each mousedown, get the initial position and the offset of the click 52 | $elem = $(e.target) 53 | initPos = $elem.position() 54 | initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX} 55 | # create a new event stream to listen to mousemove until mouseUp 56 | self.eventStream("mousemove", "*") 57 | .stopWhen(mouseUp) 58 | .forEach (e) -> 59 | # update the position of the element 60 | pos = {top: e.pageY, left: e.pageX} 61 | $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left}) 62 | ``` 63 | 64 | Pretty cool right? Imagine of all the state you could have to manage if you 65 | used events as opposed to streams. To help with your imagination, [here's some 66 | code I'm not terribly proud of](https://github.com/ccorcos/meteor-swipe/blob/3f1efdff1f1e1280d46f2715496df0f21a353cb8/swipe/swipe.coffee#L332). 67 | 68 | So what else can you so with `Tracker.eventStreams`? As you can see, it helps 69 | eliminate state from your templates... 70 | 71 | Check out the following example of typeahead suggestions. After creating 72 | an eventStream listening to keyup on the input, we implement everything 73 | else right in the template helper! Because we're using Tracker, the 74 | helper reactively updates just as you'd expect. 75 | 76 | Now for the sake of the demo, we also throttle the the input. This would 77 | be very useful if you need to subscribe for results before displaying 78 | them. This way, you aren't blasting your server on every keyup. 79 | 80 | We also do something pretty unconventional with observable streams -- we 81 | return the value of the searchStream to the helper. However unconventional, 82 | it works like a charm! 83 | 84 | ```coffee 85 | Template.typeahead.created = -> 86 | @keyUp = @eventStream("keyup", ".typeahead") 87 | 88 | Template.typeahead.helpers 89 | matches: () -> 90 | t = Template.instance() 91 | 92 | searchStream = t.keyUp 93 | .map (e) -> e.which 94 | # filter for the relevant keys: http://css-tricks.com/snippets/javascript/javascript-keycodes/ 95 | .filter (key) -> 96 | _.contains(_.union([8, 32], [46..90], [186..192], [219..222]), key) 97 | # throttle the stream to every 1.5 seconds 98 | .debounce(1500) 99 | .map (key) -> 100 | text = t.find('.typeahead').value 101 | # Meteor.subscribe("typeahead", text) 102 | if text.length > 0 103 | return People.find({name:{$regex: ".*#{text}.*"}}) 104 | else 105 | return [] 106 | 107 | # return the latest value in the searchStream 108 | searchStream.get() 109 | ``` 110 | 111 | As you can see, we once again eliminated a lot of state from out template. 112 | We didn't have to keep track of the search text in a reactive variable, and 113 | we didn't have to throttle the results in the template logic. In a large template 114 | it could become a hassle to trace how the internal state changes with respect 115 | to events ([again, some code I'm not very proud of](https://github.com/ccorcos/meteor-swipe/blob/3f1efdff1f1e1280d46f2715496df0f21a353cb8/swipe/swipe.coffee#L325)). 116 | With observable streams, we can eliminate state with more declarative asynchronous 117 | code. [Check out these examples in action](http://tracker-streams.meteor.com). 118 | 119 | ## Implementation Details 120 | 121 | Observable streams are often thought of as asynchronous arrays. 122 | But in the end, we never deal with arrays, just one element at a time. 123 | `Tracker.stream` accomplished all of this with a reactive variable and 124 | subscriptions are created with `Tracker.autorun`. 125 | The "completion" of a stream results in stopping the `Tracker.autorun` 126 | computation for a stream's subscription and all of its subscribers. 127 | 128 | ## Performance Advice 129 | 130 | I noticed while building the swipe menu that it takes a lot of CPU to bind to an event. 131 | So while it may seem intuitive that we should only bind to events when we need to and unbind 132 | them as soon as we don't need them, it costs a lot to do that sometimes. Since the template 133 | will take care of memory leaks, it's often best just create the event streams from the get go. 134 | For example: 135 | 136 | ```coffee 137 | Template.drag.rendered = -> 138 | # create the mouseDown and mouseUp streams that will 139 | # be automatically completed on Template.destroyed. 140 | mouseDown = @eventStream("mousedown", ".draggable") 141 | mouseUp = @eventStream("mouseup", ".draggable") 142 | mouseMove = @eventStream("mousemove", "*") 143 | 144 | self = this 145 | mouseDown.map (e) -> 146 | # on each mousedown, get the initial position and the offset of the click 147 | $elem = $(e.target) 148 | initPos = $elem.position() 149 | initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX} 150 | # create a new event stream to listen to mousemove until mouseUp 151 | mouseMove 152 | .takeUntil(mouseUp) 153 | .forEach (e) -> 154 | # update the position of the element 155 | pos = {top: e.pageY, left: e.pageX} 156 | $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left}) 157 | ``` 158 | 159 | Thus we are not constantly binding and unbinding events. You can see this the performance profiler 160 | or your browser's devtools. Also notice we are using `takeUntil` instead of `stopWhen`. 161 | 162 | ## To Do 163 | - error propagation 164 | 165 | I haven't come up with a good example to use this yet, but when I do 166 | I will implement them. 167 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1visgsu4m4b0w5ljbww 8 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.4 2 | autopublish@1.0.2 3 | autoupdate@1.1.5 4 | base64@1.0.2 5 | binary-heap@1.0.2 6 | blaze@2.0.4 7 | blaze-tools@1.0.2 8 | boilerplate-generator@1.0.2 9 | callback-hook@1.0.2 10 | check@1.0.4 11 | coffeescript@1.0.5 12 | ddp@1.0.14 13 | deps@1.0.6 14 | ejson@1.0.5 15 | fastclick@1.0.2 16 | follower-livedata@1.0.3 17 | geojson-utils@1.0.2 18 | html-tools@1.0.3 19 | htmljs@1.0.3 20 | http@1.0.10 21 | id-map@1.0.2 22 | insecure@1.0.2 23 | jquery@1.11.3 24 | json@1.0.2 25 | launch-screen@1.0.1 26 | livedata@1.0.12 27 | logging@1.0.6 28 | meteor@1.1.4 29 | meteor-platform@1.2.1 30 | minifiers@1.1.3 31 | minimongo@1.0.6 32 | mobile-status-bar@1.0.2 33 | mongo@1.0.11 34 | observe-sequence@1.0.4 35 | ordered-dict@1.0.2 36 | random@1.0.2 37 | reactive-dict@1.0.5 38 | reactive-var@1.0.4 39 | reload@1.1.2 40 | retry@1.0.2 41 | routepolicy@1.0.4 42 | session@1.0.5 43 | spacebars@1.0.5 44 | spacebars-compiler@1.0.4 45 | templating@1.0.11 46 | tracker@1.0.5 47 | ui@1.0.5 48 | underscore@1.0.2 49 | url@1.0.3 50 | webapp@1.1.6 51 | webapp-hashing@1.0.2 52 | -------------------------------------------------------------------------------- /examples/deferStop/client/main.coffee: -------------------------------------------------------------------------------- 1 | Session.setDefault('a', 0) 2 | Session.setDefault('b', 1) 3 | Session.setDefault('c', false) 4 | 5 | c1 = Tracker.autorun -> 6 | console.log "c1" 7 | Session.set('b', Session.get('a')) 8 | 9 | Tracker.autorun (c2) -> 10 | console.log "c2" 11 | if Session.get('c') 12 | console.log "stop" 13 | Session.set('a', 2) 14 | Meteor.defer -> 15 | c1.stop() 16 | c2.stop() 17 | 18 | console.log Session.get('a'), Session.get('b'), Session.get('c') 19 | Session.set('c', true) 20 | console.log Session.get('a'), Session.get('b'), Session.get('c') -------------------------------------------------------------------------------- /examples/slide-menu/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | packages/ -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1psby99elwm1h1bsgphe 8 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | reactive-var 12 | ccorcos:tracker-streams 13 | ccorcos:reactive-css 14 | velocityjs:velocityjs 15 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:template-extension@3.3.0 2 | application-configuration@1.0.4 3 | autopublish@1.0.2 4 | autoupdate@1.1.5 5 | base64@1.0.2 6 | binary-heap@1.0.2 7 | blaze@2.0.4 8 | blaze-tools@1.0.2 9 | boilerplate-generator@1.0.2 10 | callback-hook@1.0.2 11 | ccorcos:reactive-css@1.0.2 12 | ccorcos:tracker-streams@2.0.0 13 | check@1.0.4 14 | coffeescript@1.0.5 15 | ddp@1.0.14 16 | deps@1.0.6 17 | ejson@1.0.5 18 | fastclick@1.0.2 19 | follower-livedata@1.0.3 20 | geojson-utils@1.0.2 21 | html-tools@1.0.3 22 | htmljs@1.0.3 23 | http@1.0.10 24 | id-map@1.0.2 25 | insecure@1.0.2 26 | jquery@1.11.3 27 | json@1.0.2 28 | launch-screen@1.0.1 29 | livedata@1.0.12 30 | logging@1.0.6 31 | meteor@1.1.4 32 | meteor-platform@1.2.1 33 | minifiers@1.1.3 34 | minimongo@1.0.6 35 | mobile-status-bar@1.0.2 36 | mongo@1.0.11 37 | observe-sequence@1.0.4 38 | ordered-dict@1.0.2 39 | random@1.0.2 40 | reactive-dict@1.0.5 41 | reactive-var@1.0.4 42 | reload@1.1.2 43 | retry@1.0.2 44 | routepolicy@1.0.4 45 | session@1.0.5 46 | spacebars@1.0.5 47 | spacebars-compiler@1.0.4 48 | templating@1.0.11 49 | tracker@1.0.5 50 | ui@1.0.5 51 | underscore@1.0.2 52 | url@1.0.3 53 | velocityjs:velocityjs@1.2.1 54 | webapp@1.1.6 55 | webapp-hashing@1.0.2 56 | -------------------------------------------------------------------------------- /examples/slide-menu/README.md: -------------------------------------------------------------------------------- 1 | why does this perform better on mobile than on desktop? 2 | 3 | unit tests for streams 4 | 5 | make sure we have some universal conventions. does a function create a new stream, does it close when its done? 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/slide-menu/client/main.coffee: -------------------------------------------------------------------------------- 1 | navHeight = 50 2 | menuWidth = 200 3 | css('*:not(input):not(textarea)').userSelect('none').boxSizing('border-box') 4 | css('.menu').transform("translateX(-#{menuWidth}px)") 5 | css('html body .page').fp().margin(0) 6 | 7 | css 8 | '.menu': 9 | zIndex: 1 10 | position: 'absolute' 11 | top: 0 12 | bottom: 0 13 | rightpc: 100 14 | widthpx: menuWidth 15 | left: 0 16 | color: 'white' 17 | backgroundColor: 'blue' 18 | '.handle': 19 | position: 'absolute' 20 | top: 0 21 | leftpc: 100 22 | heightpx: navHeight 23 | widthpx: navHeight 24 | lineHeightpx: navHeight 25 | textAlign: 'center' 26 | backgroundColor: 'red' 27 | '.item': 28 | widthpc: 100 29 | textAlign: 'center' 30 | heightpx: navHeight 31 | borderBottom: '1px solid white' 32 | lineHeightpx: navHeight 33 | '.content': 34 | paddingpx: 10 35 | position: 'absolute' 36 | toppx: navHeight 37 | bottom: 0 38 | left: 0 39 | right: 0 40 | '.nav': 41 | position: 'absolute' 42 | top: 0 43 | left: 0 44 | right: 0 45 | heightpx: navHeight 46 | color: 'white' 47 | backgroundColor: 'blue' 48 | 49 | strangle = (x, maxMin) -> 50 | x = Math.max(x, maxMin[0]) 51 | x = Math.min(x , maxMin[1]) 52 | return x 53 | 54 | Template.menu.rendered = -> 55 | self = this 56 | 57 | # start stream of x position values 58 | toushStart = @eventStream("touchstart", ".handle") 59 | .map (e) -> e.originalEvent.touches[0].pageX 60 | mouseDown = @eventStream("mousedown", ".handle") 61 | .map (e) -> e.pageX 62 | startStream = Tracker.mergeStreams(toushStart, mouseDown) 63 | 64 | # cancel on a variety of annoying events 65 | touchEnd = self.eventStream("touchend", ".page", true) 66 | touchCancel = self.eventStream("touchcancel", ".page", true) 67 | touchLeave = self.eventStream("touchleave", ".page", true) 68 | mouseUp = self.eventStream("mouseup", ".page", true) 69 | mouseOut = self.eventStream("mouseout", ".page", true) 70 | mouseOffPage = mouseOut 71 | .filter (e) -> (e.relatedTarget or e.toElement) is undefined 72 | endStream = Tracker.mergeStreams(mouseUp, mouseOffPage, touchEnd, touchCancel, touchLeave) 73 | 74 | # create a move stream on demand returning the x position values 75 | mouseMove = self.eventStream("mousemove", ".page", true) 76 | .map (e) -> e.pageX 77 | touchMove = self.eventStream("touchmove", ".page", true) 78 | .map (e) -> e.originalEvent.touches[0].pageX 79 | moveStream = Tracker.mergeStreams(mouseMove, touchMove) 80 | 81 | # create an animation stream to block the start stream from interrupting an animation 82 | animatingStream = @stream(false) 83 | 84 | # get the jquery object we're going to drag 85 | $menu = $(@find('.menu')) 86 | 87 | startStream 88 | .unless(animatingStream) 89 | .map (x) -> 90 | 91 | initLeft = $menu.position().left 92 | offset = initLeft - x 93 | lastLeft = initLeft 94 | velocity = 0 95 | 96 | # toggle menu position 97 | toggle = -> 98 | if lastLeft > -menuWidth/2 99 | # close it 100 | $menu.velocity({translateX: [-menuWidth, 0], translateZ: [0, 0]}, {duration: 400, easing: 'ease-in-out', complete: -> animatingStream.set(false)}) 101 | else 102 | # open it 103 | $menu.velocity({translateX: [0, -menuWidth], translateZ: [0, 0]}, {duration: 400, easing: 'ease-in-out', complete: -> animatingStream.set(false)}) 104 | 105 | # resolve menu position 106 | resolve = -> 107 | animatingStream.set(true) 108 | # wait for animation to finish 109 | if initLeft is lastLeft and velocity is 0 110 | toggle() 111 | return 112 | 113 | momentum = velocity*3 114 | if lastLeft + momentum > -menuWidth/2 115 | momentum = Math.abs(momentum) 116 | duration = Math.min(-lastLeft/momentum*100, 400) 117 | $menu.velocity({translateX: 0, translateZ: 0}, {duration: duration, easing: 'ease-out', complete: -> animatingStream.set(false)}) 118 | else 119 | momentum = Math.abs(momentum) 120 | duration = Math.min((200-lastLeft)/momentum*100, 400) 121 | $menu.velocity({translateX: -menuWidth, translateZ: 0}, {duration: duration, easing: 'ease-out', complete: -> animatingStream.set(false)}) 122 | 123 | moveStream 124 | .takeUntil(endStream, resolve) 125 | .forEach (x) -> 126 | # wait for animation to finish 127 | left = strangle(x + offset, [-menuWidth, 0]) 128 | velocity = left - lastLeft 129 | lastLeft = left 130 | $menu.velocity({translateX: left, translateZ: 0}, {duration: 0}) 131 | 132 | 133 | -------------------------------------------------------------------------------- /examples/slide-menu/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{>main}} 7 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /examples/tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | packages/ -------------------------------------------------------------------------------- /examples/tutorial/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/tutorial/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/tutorial/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1psby99elwm1h1bsgphe 8 | -------------------------------------------------------------------------------- /examples/tutorial/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | reactive-var 12 | ccorcos:tracker-streams 13 | anti:fake 14 | -------------------------------------------------------------------------------- /examples/tutorial/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/tutorial/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/tutorial/.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:template-extension@3.3.0 2 | anti:fake@0.4.1 3 | application-configuration@1.0.4 4 | autopublish@1.0.2 5 | autoupdate@1.1.5 6 | base64@1.0.2 7 | binary-heap@1.0.2 8 | blaze@2.0.4 9 | blaze-tools@1.0.2 10 | boilerplate-generator@1.0.2 11 | callback-hook@1.0.2 12 | ccorcos:tracker-streams@2.0.0 13 | check@1.0.4 14 | coffeescript@1.0.5 15 | ddp@1.0.14 16 | deps@1.0.6 17 | ejson@1.0.5 18 | fastclick@1.0.2 19 | follower-livedata@1.0.3 20 | geojson-utils@1.0.2 21 | html-tools@1.0.3 22 | htmljs@1.0.3 23 | http@1.0.10 24 | id-map@1.0.2 25 | insecure@1.0.2 26 | jquery@1.11.3 27 | json@1.0.2 28 | launch-screen@1.0.1 29 | livedata@1.0.12 30 | logging@1.0.6 31 | meteor@1.1.4 32 | meteor-platform@1.2.1 33 | minifiers@1.1.3 34 | minimongo@1.0.6 35 | mobile-status-bar@1.0.2 36 | mongo@1.0.11 37 | observe-sequence@1.0.4 38 | ordered-dict@1.0.2 39 | random@1.0.2 40 | reactive-dict@1.0.5 41 | reactive-var@1.0.4 42 | reload@1.1.2 43 | retry@1.0.2 44 | routepolicy@1.0.4 45 | session@1.0.5 46 | spacebars@1.0.5 47 | spacebars-compiler@1.0.4 48 | templating@1.0.11 49 | tracker@1.0.5 50 | ui@1.0.5 51 | underscore@1.0.2 52 | url@1.0.3 53 | webapp@1.1.6 54 | webapp-hashing@1.0.2 55 | -------------------------------------------------------------------------------- /examples/tutorial/client/main.coffee: -------------------------------------------------------------------------------- 1 | # Toggle between examples 2 | Session.setDefault('example', 'drag') 3 | Template.registerHelper 'sessionVarEq', (string, value) -> Session.equals(string, value) 4 | Template.main.events 5 | 'click .switch': (e,t) -> 6 | state = Session.get('example') 7 | if state is 'drag' 8 | Session.set('example', 'typeahead') 9 | else 10 | Session.set('example', 'drag') 11 | 12 | 13 | # Drag a DOM element using a stream. It would probably be better to 14 | # user translate3d to use the GPU or VelocityJS but for the sake of 15 | # exmaple, we're using abosolute positioning. 16 | Template.drag.rendered = -> 17 | mouseDown = @eventStream("mousedown", ".draggable") 18 | mouseUp = @eventStream("mouseup", ".draggable") 19 | self = this 20 | 21 | mouseDown.map (e) -> 22 | $elem = $(e.target) 23 | initPos = $elem.position() 24 | initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX} 25 | self.eventStream("mousemove", "*") 26 | .stopWhen(mouseUp) 27 | .forEach (e) -> 28 | pos = {top: e.pageY, left: e.pageX} 29 | $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left}) 30 | 31 | 32 | 33 | # Some random people to search for... 34 | People = new Mongo.Collection(null) 35 | for i in [0...1000] 36 | People.insert({name: Fake.word()}) 37 | 38 | # In the typeahead example, we are reactively updating a list based on 39 | # what is typed. The beauty of this is that we dont have to maintain any 40 | # state outside of the template helper 41 | Template.typeahead.created = -> 42 | @keyUp = @eventStream("keyup", ".typeahead") 43 | 44 | Template.typeahead.helpers 45 | matches: () -> 46 | t = Template.instance() 47 | 48 | searchStream = t.keyUp 49 | .map (e) -> e.which 50 | # filter for the relevant keys: http://css-tricks.com/snippets/javascript/javascript-keycodes/ 51 | .filter (key) -> 52 | _.contains(_.union([8, 32], [46..90], [186..192], [219..222]), key) 53 | .debounce(1500) 54 | .map (key) -> 55 | text = t.find('.typeahead').value 56 | if text.length > 0 57 | return People.find({name:{$regex: ".*#{text}.*"}}) 58 | else 59 | return [] 60 | 61 | searchStream.value.get() 62 | -------------------------------------------------------------------------------- /examples/tutorial/client/main.css: -------------------------------------------------------------------------------- 1 | .draggable { 2 | border: 1px solid black; 3 | width: 100px; 4 | height: 100px; 5 | line-height: 100px; 6 | background-color: white; 7 | position: absolute; 8 | text-align: center; 9 | top: 100px; 10 | left: 40px; 11 | padding: 5px; 12 | margin: 5px; 13 | -webkit-user-select: none; /* Chrome all / Safari all */ 14 | -moz-user-select: none; /* Firefox all */ 15 | -ms-user-select: none; /* IE 10+ */ 16 | 17 | /* No support for these yet, use at own risk */ 18 | -o-user-select: none; 19 | user-select: none; 20 | } 21 | 22 | html, body { 23 | height: 100%; 24 | width: 100%; 25 | margin: 0; 26 | } 27 | 28 | .everything { 29 | height: calc(100% - 90px); 30 | width: 100%; 31 | } 32 | 33 | .switch { 34 | width: 100%; 35 | text-align: center; 36 | height: 70px; 37 | background-color: black; 38 | color: white; 39 | line-height: 70px; 40 | } 41 | 42 | input { 43 | font-size: 16px; 44 | width: 80%; 45 | margin-left: 10%; 46 | margin-top: 20px; 47 | } -------------------------------------------------------------------------------- /examples/tutorial/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | {{>main}} 3 | 4 | 5 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /lib/stream.coffee: -------------------------------------------------------------------------------- 1 | # Some helper functions 2 | debug = (args...) -> return #console.log.apply(console, args) 3 | delay = (ms, func) -> Meteor.setTimeout(func, ms) 4 | 5 | Tracker.stream = (initialValue=undefined) -> 6 | if not (this instanceof Tracker.stream) then return new Tracker.stream(initialValue) 7 | # Keep track of any autorun "subscribers" so we can stop them when complete 8 | @subscribers = [] 9 | # keep track of the autorun this stream subscribes to so we can stop it as well 10 | @subscription = undefined 11 | # second arguement is an equals function. we want repeat values. 12 | @value = new ReactiveVar(initialValue, -> false) 13 | @error = new ReactiveVar(undefined, -> false) 14 | return this 15 | 16 | # A curried function for merging streams into one 17 | Tracker.mergeStreams = (streams...) -> 18 | mergedStream = new Tracker.stream() 19 | 20 | subs = [] 21 | for stream in streams 22 | do (stream) -> 23 | stream.subscribers.push(mergedStream) 24 | sub = Tracker.autorun -> 25 | value = stream.get() 26 | if value isnt undefined 27 | mergedStream.set(value) 28 | subs.push(sub) 29 | 30 | s = stop: -> 31 | for sub in subs 32 | sub.stop() 33 | 34 | mergedStream.subscription = s 35 | return mergedStream 36 | 37 | # Add a value to a stream 38 | Tracker.stream::set = (x) -> 39 | @value.set(x) 40 | 41 | # Get a value from a stream 42 | Tracker.stream::get = () -> 43 | @value.get() 44 | 45 | # Stop a stream, i.e. its subscription, and its subscribers. 46 | Tracker.stream::stop = () -> 47 | # stop this stream. 48 | @subscription?.stop() 49 | # stop the subscribers 50 | for subscriber in @subscribers 51 | subscriber.stop() 52 | 53 | # Create a stream from an event on an element 54 | Tracker.eventStream = (eventName, element) -> 55 | if not (this instanceof Tracker.eventStream) then return new Tracker.eventStream(eventName, element) 56 | stream = new Tracker.stream() 57 | debug "New event stream #{eventName} #{element}" 58 | # create stream from an event using jquery 59 | element.bind eventName, (e) -> 60 | debug "event:", eventName 61 | stream.set(e) 62 | 63 | # set the subscription to unbind the event on stop 64 | stream.subscription = 65 | stop: -> 66 | debug "Stop event stream #{eventName} #{element}" 67 | element.unbind(eventName) 68 | 69 | return stream 70 | 71 | # Map the stream across a function 72 | Tracker.stream::map = (func) -> 73 | self = this 74 | nextStream = new Tracker.stream() 75 | @subscribers.push(nextStream) 76 | nextStream.subscription = Tracker.autorun -> 77 | value = self.get() 78 | if value isnt undefined 79 | nextStream.set(func(value)) 80 | return nextStream 81 | 82 | # Create a dupelicate stream 83 | Tracker.stream::copy = () -> 84 | @map((x)-> x) 85 | 86 | # Filter the stream based on a function 87 | Tracker.stream::filter = (func) -> 88 | self = this 89 | nextStream = new Tracker.stream() 90 | @subscribers.push(nextStream) 91 | nextStream.subscription = Tracker.autorun -> 92 | value = self.get() 93 | if value isnt undefined and func(value) 94 | nextStream.set(value) 95 | return nextStream 96 | 97 | # Reduce a stream 98 | Tracker.stream::reduce = (initialValue, func) -> 99 | self = this 100 | nextStream = new Tracker.stream(initialValue) 101 | @subscribers.push(nextStream) 102 | lastValue = initialValue 103 | nextStream.subscription = Tracker.autorun -> 104 | value = self.get() 105 | if value isnt undefined 106 | nextStream.set(func(value, lastValue)) 107 | lastValue = value 108 | return nextStream 109 | 110 | # Filter consecutive duplicate values 111 | Tracker.stream::dedupe = (func) -> 112 | lastValue = undefined 113 | @filter (value) -> 114 | notDupe = (lastValue isnt value) 115 | lastValue = value 116 | return notDupe 117 | 118 | # The most recent value of a stream with a minimum amount of 119 | # time since the last value 120 | Tracker.stream::debounce = (ms) -> 121 | self = this 122 | nextStream = new Tracker.stream() 123 | @subscribers.push(nextStream) 124 | 125 | waiting = false 126 | waitingValue = undefined 127 | wait = -> 128 | waiting = true 129 | waitingValue = undefined 130 | debug "start waiting" 131 | delay ms, -> 132 | if waitingValue isnt undefined 133 | nextStream.set(waitingValue) 134 | debug "wait again" 135 | wait() 136 | else 137 | waiting = false 138 | debug "done waiting" 139 | 140 | nextStream.subscription = Tracker.autorun -> 141 | value = self.get() 142 | if value isnt undefined 143 | if waiting 144 | debug "queue value" 145 | waitingValue = value 146 | else 147 | waitingValue = undefined 148 | debug "set value" 149 | nextStream.set(value) 150 | wait() 151 | 152 | return nextStream 153 | 154 | # Merge with another stream into a new stream 155 | Tracker.stream::merge = (anotherStream) -> 156 | self = this 157 | nextStream = new Tracker.stream() 158 | 159 | @subscribers.push(nextStream) 160 | sub1 = Tracker.autorun -> 161 | value = self.get() 162 | if value isnt undefined 163 | nextStream.set(value) 164 | 165 | anotherStream.subscribers.push(nextStream) 166 | sub2 = Tracker.autorun -> 167 | value = anotherStream.get() 168 | if value isnt undefined 169 | nextStream.set(value) 170 | 171 | sub = stop: -> 172 | sub1.stop() 173 | sub2.stop() 174 | 175 | nextStream.subscription = sub 176 | return nextStream 177 | 178 | # Pipe to a new stream only when another stream is falsy value 179 | Tracker.stream::unless = (anotherStream) -> 180 | self = this 181 | nextStream = new Tracker.stream() 182 | @subscribers.push(nextStream) 183 | blockStream = false 184 | sub1 = Tracker.autorun -> 185 | otherValue = anotherStream.get() 186 | if otherValue 187 | blockStream = true 188 | else 189 | blockStream = false 190 | 191 | sub2 = Tracker.autorun -> 192 | value = self.get() 193 | if value isnt undefined and not blockStream 194 | nextStream.set(value) 195 | 196 | nextStream.subscription 197 | return nextStream 198 | 199 | # Stop on the next event from anotherStream. 200 | Tracker.stream::stopWhen = (anotherStream, func) -> 201 | self = this 202 | first = true 203 | Tracker.autorun (c) -> 204 | value = anotherStream.get() 205 | if value isnt undefined 206 | # there may already be a value in another stream, so make sure 207 | # not to immediately stop the stream. 208 | unless first 209 | val = self.get() 210 | if func then func(val) 211 | # Meteor.defer -> 212 | self.stop() 213 | c.stop() 214 | first = false 215 | return this 216 | 217 | # Stop stream after some time 218 | Tracker.stream::stopAfterMs = (ms, func) -> 219 | self = this 220 | delay ms, -> 221 | value = self.get() 222 | if func then func(value) 223 | Meteor.defer -> 224 | self.stop() 225 | return this 226 | 227 | # Stop stream after N values 228 | Tracker.stream::stopAfterN = (number, func) -> 229 | self = this 230 | count = 0 231 | Tracker.autorun (c) -> 232 | value = self.get() 233 | if value isnt undefined 234 | count++ 235 | if count >= number 236 | if func then func(value) 237 | Meteor.defer -> 238 | self.stop() 239 | c.stop() 240 | 241 | return this 242 | 243 | # Alias for stream.copy().stopWhen 244 | Tracker.stream::takeUntil = (anotherStream, func) -> 245 | @copy().stopWhen(anotherStream, func) 246 | 247 | # Alias for stream.copy().stopAfterMs 248 | Tracker.stream::takeForMs = (ms, func) -> 249 | @copy().stopAfterMs(ms, func) 250 | 251 | # Alias for stream.copy().stopAfterN 252 | Tracker.stream::takeN = (number, func) -> 253 | @copy().stopAfterN(number, func) 254 | 255 | # Aliases 256 | Tracker.stream::push = Tracker.stream::set 257 | Tracker.stream::read = Tracker.stream::get 258 | Tracker.stream::completed = Tracker.stream::stop 259 | 260 | Tracker.stream::forEach = Tracker.stream::map 261 | Tracker.stream::throttle = Tracker.stream::debounce 262 | 263 | # Create a Blaze prototype for creating event streams that automatically stop onDestroyed 264 | Blaze.TemplateInstance.prototype.eventStream = (eventName, elementSelector, global=false) -> 265 | unless @eventStreams isnt undefined 266 | @eventStreams = [] 267 | 268 | # if you set global, the event stream still closes onDestroyed 269 | # but this way you can do things lik `this.eventStream("mousemove", "*")` 270 | # and it selectes everything. 271 | if global 272 | element = $(elementSelector) 273 | stream = Tracker.eventStream(eventName, element) 274 | @eventStreams.push(stream) 275 | return stream 276 | 277 | if @view.isRendered 278 | # if the view is already rendered, we can use TemplateInstance.$ to 279 | # find all elements within the template and create an event stream 280 | element = @$(elementSelector) 281 | stream = Tracker.eventStream(eventName, element) 282 | @eventStreams.push(stream) 283 | return stream 284 | else 285 | # if the view hasnt been rendered yet (e.g. Template.created) 286 | # we can register events with Template.events. We don't have 287 | # to worry about unsubscribing -- Meteor does that :) 288 | stream = new Tracker.stream() 289 | evtMap = {} 290 | evtMap["#{eventName} #{elementSelector}"] = (e,t) -> 291 | stream.set(e) 292 | @view.template.events(evtMap) 293 | @eventStreams.push(stream) 294 | return stream 295 | 296 | # Create a Blaze prototype for creating streams that automatically stop onDestroyed 297 | Blaze.TemplateInstance.prototype.stream = (initialValue=undefined) -> 298 | unless @eventStreams isnt undefined 299 | @eventStreams = [] 300 | 301 | stream = new Tracker.stream(initialValue) 302 | @eventStreams.push(stream) 303 | return stream 304 | 305 | # Clean up all the streams when the Template dies thanks to 306 | # the template-extentions package 307 | Template.onDestroyed -> 308 | _.map(@eventStreams, (stream) -> stream.stop()) -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ccorcos:tracker-streams', 3 | summary: 'Observable streams using Tracker.', 4 | version: '2.0.0', 5 | git: 'https://github.com/ccorcos/meteor-tracker-streams' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('METEOR@1'); 10 | 11 | api.use([ 12 | 'tracker', 13 | 'reactive-var', 14 | 'coffeescript', 15 | 'templating', 16 | 'underscore', 17 | 'aldeed:template-extension@3.3.0' 18 | ], 'client'); 19 | 20 | api.addFiles([ 21 | 'lib/stream.coffee', 22 | ], 'client'); 23 | 24 | }); --------------------------------------------------------------------------------