Code Monkey home page Code Monkey logo

meteor-tracker-streams's Introduction

Tracker Streams

This package uses Tracker to build "observable streams" for Meteor. Check out the live demo. Also checkout the swipe menu drawer demo.

If you haven't heard about observable streams, then check out this talk. For a more hands-on introduction, check out this interactive tutorial.

Getting Started

meteor add ccorcos:tracker-streams

You can create you're own stream using

numbers = Tracker.stream()

You can create new streams using map, forEach, filter, etc.

times10 = myStream.map (x) -> x*10
times10.forEach (x) -> console.log x

Now you push to the stream by setting the stream's reactive variable.

numbers.value.set(10)
# log: 100
numbers.value.set(2)
# log: 20

Tracker is pretty amazing and you could say that this isn't that useful. But what about when we create streams from events?

Suppose we want to make an element draggable. We can do this by creating an event stream for mousedown and mouseup events. Then after mousedown, we can create a mousemove stream which completes on the next mouseup event using .takeUntil. Check it out:

Template.drag.rendered = ->
  # create the mouseDown and mouseUp streams that will 
  # be automatically completed on Template.destroyed.
  mouseDown = @eventStream("mousedown", ".draggable")
  mouseUp   = @eventStream("mouseup", ".draggable")
  
  self = this
  mouseDown.map (e) ->
    # on each mousedown, get the initial position and the offset of the click
    $elem = $(e.target)
    initPos = $elem.position()
    initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX}
    # create a new event stream to listen to mousemove until mouseUp
    self.eventStream("mousemove", "*")
      .stopWhen(mouseUp)
      .forEach (e) ->
        # update the position of the element
        pos = {top: e.pageY, left: e.pageX}
        $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left})

Pretty cool right? Imagine of all the state you could have to manage if you used events as opposed to streams. To help with your imagination, here's some code I'm not terribly proud of.

So what else can you so with Tracker.eventStreams? As you can see, it helps eliminate state from your templates...

Check out the following example of typeahead suggestions. After creating an eventStream listening to keyup on the input, we implement everything else right in the template helper! Because we're using Tracker, the helper reactively updates just as you'd expect.

Now for the sake of the demo, we also throttle the the input. This would be very useful if you need to subscribe for results before displaying them. This way, you aren't blasting your server on every keyup.

We also do something pretty unconventional with observable streams -- we return the value of the searchStream to the helper. However unconventional, it works like a charm!

Template.typeahead.created =  ->
  @keyUp = @eventStream("keyup", ".typeahead")

Template.typeahead.helpers
  matches: () ->
    t = Template.instance()
    
    searchStream = t.keyUp
      .map (e) -> e.which
      # filter for the relevant keys: http://css-tricks.com/snippets/javascript/javascript-keycodes/
      .filter (key) -> 
        _.contains(_.union([8, 32], [46..90], [186..192], [219..222]), key)
      # throttle the stream to every 1.5 seconds
      .debounce(1500)
      .map (key) ->
        text = t.find('.typeahead').value
        # Meteor.subscribe("typeahead", text)
        if text.length > 0
          return People.find({name:{$regex: ".*#{text}.*"}})
        else 
          return []
    
    # return the latest value in the searchStream
    searchStream.get()

As you can see, we once again eliminated a lot of state from out template. We didn't have to keep track of the search text in a reactive variable, and we didn't have to throttle the results in the template logic. In a large template it could become a hassle to trace how the internal state changes with respect to events (again, some code I'm not very proud of). With observable streams, we can eliminate state with more declarative asynchronous code. Check out these examples in action.

Implementation Details

Observable streams are often thought of as asynchronous arrays. But in the end, we never deal with arrays, just one element at a time. Tracker.stream accomplished all of this with a reactive variable and subscriptions are created with Tracker.autorun. The "completion" of a stream results in stopping the Tracker.autorun computation for a stream's subscription and all of its subscribers.

Performance Advice

I noticed while building the swipe menu that it takes a lot of CPU to bind to an event. So while it may seem intuitive that we should only bind to events when we need to and unbind them as soon as we don't need them, it costs a lot to do that sometimes. Since the template will take care of memory leaks, its often best just create the event streams from the get-go. For example:

Template.drag.rendered = ->
  # create the mouseDown and mouseUp streams that will 
  # be automatically completed on Template.destroyed.
  mouseDown = @eventStream("mousedown", ".draggable")
  mouseUp   = @eventStream("mouseup", ".draggable")
  mouseMove = @eventStream("mousemove", "*")

  self = this
  mouseDown.map (e) ->
    # on each mousedown, get the initial position and the offset of the click
    $elem = $(e.target)
    initPos = $elem.position()
    initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX}
    # create a new event stream to listen to mousemove until mouseUp
    mouseMove
      .takeUntil(mouseUp)
      .forEach (e) ->
        # update the position of the element
        pos = {top: e.pageY, left: e.pageX}
        $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left})

Thus we are not constantly binding and unbinding events. You can see this the performance profiler or your browser's devtools. Also notice we are using takeUntil instead of stopWhen.

To Do

  • error propagation

    I haven't come up with a good example to use this yet, but when I do I will implement them.

meteor-tracker-streams's People

Contributors

ccorcos avatar

Stargazers

Sean Keeley avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.