Adrien Lemaire
Adrien Lemaire

Reputation: 1894

Sorting a collection after transform

I have a collection that uses transform to instantiate the documents from classes. Those instances then set new attributes, data fetched from 3rd part apis and made reactive.

Now, I need to sort those objects based on a method that retrieve the reactive data. But I cannot do a collection find.sort, or use collection-hooks, because it operates on the document before transforming it, hence the method is not available.

Therefore, it seems to me that the only way to sort that collection based on that data which is not in mongo, is to override the UI.each element and adding sorting there. But I'm quite new with Meteor, and do not really know how UI.each works and how to override it to implement that sorting method.

Below a simplified example from my code:

model

class @BaseCrypto

  constructor: (@address) ->
    @keys =
      balance: "Processing..."
    @deps = {}

  ensureDeps: (key) ->
    if not @deps[key]
      @deps[key] = new Deps.Dependency()
      @set_balance()

  get_balance: ->
    """Retrieve value set from @set_balance()"""
    @ensureDeps "balance"
    @deps.balance.depend()
    return @keys.balance

  set_balance: (url, lambda_balance) ->
    cls = this
    Meteor.call "call_url", url, (err, result) ->
      if err
        throw new Meteor.Error err.error, err.reason
      else
        cls.keys.balance = lambda_balance result
        cls.deps.balance.changed()

collection

@Addresses = new Meteor.Collection "addresses",
  transform: (doc) ->
      doc = BaseCrypto doc.address
      doc.set_balance url, lambda_balance
    return doc

helper

Template.coinsManager.helpers
  donationAddresses: ->
      Addresses.find {}

template

template(name="coinsManager")

    div
        div.addresses
            {{#each donationAddresses}}
            {{> addressItem}}
            {{/each}}

How can I get {{#each}} to sort my addresses depending on their method get_balance() ?

Edit

We can do a fetch() on the collection query in the template to retrieve transformed elements. How do you use observe() in this case ? Because in this case, reactivity is lost and addresses not updated.

before:

no sorting

donationAddresses: ->
  coinsManager = Meteor.users.findOne
    "emails.address": "[email protected]"
  if coinsManager
    Addresses.find
      userId: coinsManager._id

after: sorting

donationAddresses: ->
  coinsManager = Meteor.users.findOne
    "emails.address": "[email protected]"
  if coinsManager
    addresses = Addresses.find
      userId: coinsManager._id
    addresses = addresses.fetch().sort (a, b) ->
      a = a.get_balance()
      b = b.get_balance()
      if not _.isNumber a
        a = -1
      if not _.isNumber b
        b = -1
      b - a
    return addresses

Upvotes: 2

Views: 942

Answers (3)

Dan Dascalescu
Dan Dascalescu

Reputation: 151958

Minimongo doesn't support sorting on virtual fields., so Addresses.find({...}, {sort: {balanceVirtualField: 1}} wont' work.

Can you fetch() the collection's find() result and sort the array? To preserve reactivity, you can observe() it and recreate the array. Slow but might be a stop-gap measure until Meteor implements that feature.

Upvotes: 1

nathan-m
nathan-m

Reputation: 8865

The following code will sync a source collection (myCollection) in to a local collection (myLocalCollection), and apply transforms on new / updated documents.

myCollection = new Meteor.Collection('myCollection')
myLocalCollection = new Meteor.Collection(null)

_myTransform = (doc)->
  doc.awesome = (doc.someProp > 3)
  return

_syncWithTransform = (destination, xform)->
  return {
    added: (doc)->
      destination.insert(xform(doc))
      return

    changed: (doc)->
      destination.update(id, xform(doc))
      return

    removed: (doc)->
      destination.remove(doc._id)
      return
  }

myCollection.find().observe(_syncWithTransform(myLocalCollection, _myTransform))

If you only want to perform your transform task when specific fields change, you can create two transform functions, and use observeChanges to check if a specific field was updated-

myCollection = new Meteor.Collection('myCollection')
myLocalCollection = new Meteor.Collection(null)

_transformNew = (doc)->
  doc.awesome = (doc.someProp > 3)
  return

_transformUpdate = ($modifier)->
  if $modifier.$set?.someProp?
    $modifier.$set.awesome = ($modifier.$set.someProp > 3)
  return $modifier

_syncWithTransform = (destination, xnew, xmod)->
  return {
    added: (id, fields)->
      fields._id = id
      destination.insert(xnew(fields))
      return

    changed: (id, fields)->
      $modifier = {}
      for key, value of fields
        if value == undefined
          unless $modifier.$unset?
            $modifier.$unset = {}
          $modifier.$unset[key] = true
        else
          unless $modifier.$set?
            $modifier.$set = {}
          $modifier.$set[key] = value
      destination.update(id, xmod($modifier))
      return

    removed: (id)->
      destination.remove(id)
      return
  }

myCollection.find().observeChanges(_syncWithTransform(myLocalCollection, _transformNew, _transformUpdate()))

Once you have the separate collection filled with the transformed documents - you can do regular reactive & sorted queries, eg. myLocalCollection.find({},{sort:['a','b','c']})

Upvotes: 1

Adrien Lemaire
Adrien Lemaire

Reputation: 1894

I finally got it working!

success

And here is the code:

code

So basically,

  • I moved the initialization of the collection cursor from the template to the router, and I return a fetched collection (array)
  • I retrieve that collection in my template helper, and perform a sort on the array.
  • I pass the sorted array to UI.each.

Not sure how I managed to handle the reactivity without observe or observeHandler... but it works. Probably the magic is handled from Iron router to keep the data reactive :)

Upvotes: 0

Related Questions