I want to take a list of item names from a collection as a simple array to use for things like autocompleting user input and checking for duplicates. I would like this list to be reactive so that changes in the data will be reflected in the array. I have tried the following based on the Meteor documentation:
setReactiveArray = (objName, Collection, field) ->
update = ->
context = new Meteor.deps.Context()
context.on_invalidate update
context.run ->
list = Collection.find({},{field: 1}).fetch()
myapp[objName] = _(list).pluck field
update()
Meteor.startup ->
if not app.items?
setReactiveArray('items', Items, 'name')
#set autocomplete using the array
Template.myForm.set_typeahead = ->
Meteor.defer ->
$('[name="item"]').typeahead {source: app.items}
This code seems to work, but it kills my app's load time (takes 5-10 seconds to load on dev/localhost vs. ~1 second without this code). Am I doing something wrong? Is there a better way to accomplish this?
You should be able to use Items.find({},{name: 1}).fetch()
, which will return an array of items and is reactive, so it will re-run its enclosing function whenever the query results change, as long as it's called in a reactive context.
For the Template.myForm.set_typeahead
helper, you might want to call that query inside the helper itself, store the result in a local variable, and then call Meteor.defer
with a function that references that variable. Otherwise I'm not sure that the query will be inside a reactive context when it's called.
Edit: I have updated the code below both because it was fragile, and to put it in context so it's easier to test. I have also added a caution - in most cases, you will want to use @zorlak's or @englandpost's methods (see below).
First of all, kudos to @zorlak for digging up my old question that nobody answered. I have since solved this with a couple of insights gleaned from @David Wihl and will post my own solution. I will hold off on selecting the correct answer until others have a chance to weigh in.
@zorlak's answer solves the autocomplete issue for a single field, but as stated in the question, I was looking for an array that would update reactively, and the autocomplete was just one example of what it would be used for. The advantage of having this array is that it can be used anywhere (not just in template helpers) and that it can be used multiple times in the code without having to re-execute the query (and the _.pluck()
that reduces the query to an array). In my case, this array ends up in multiple autocomplete fields as well as validation and other places. It's possible that the advantages I'm putting forth are not significant in most Meteor apps (please leave comments), but this seems like an advantage to me.
To make the array reactive, simply build it inside a Meteor.autorun()
callback - it will re-execute any time the target collection changes (but only then, avoiding repetitive queries). This was the key insight I was looking for. Also, using the Template.rendered()
callback is cleaner and less of a hack than the set_typeahead
template helper I used in the question. The code below uses underscore.js's _.pluck()
to extract the array from the collection and uses Twitter bootstrap's $.typeahead()
to create the autocomplete.
Updated code: I have edited the code so you can try this with a stock meteor create
d test environment. Your html will need a line <input id="typeahead" />
in the 'hello' template. @Items
has the @
sign to make Items
available as a global on the console (Meteor 0.6.0 added file-level variable scoping). That way you can enter new items in the console, such as Items.insert({name: "joe"})
, but the @
is not necessary for the code to work. The other necessary change for standalone use is that the typeahead function now sets the source to a function (->
) so that it will query items
when activated instead of being set at rendering, which allows it to take advantage of changes to items
.
@Items = new Meteor.Collection("items")
items = {}
if Meteor.isClient
Meteor.startup ->
Meteor.autorun ->
items = _(Items.find().fetch()).pluck "name"
console.log items #first result will be empty - see caution below
Template.hello.rendered = ->
$('#typeahead').typeahead {source: -> _(Items.find().fetch()).pluck "name"}
Caution! The array we created is not itself a reactive data source. The reason that the typeahead source:
needed to be set to a function ->
that returned items
is that when Meteor first starts, the code runs before Minimongo has gotten its data from the server, and items
is set to an empty array. Minimongo then receives its data, and items
is updated You can see this process if you run the above code with the console open: console.log items
will log twice if you have any data stored.
Template.x.rendered()
calls don't don't set a reactivity context and so won't retrigger due to changes in reactive elements (to check this, pause your code in the debugger and examine Deps.currentComputation
-- if it's null
, you are not in a reactive context and changes to reactive elements will be ignored). But you might be surprised to learn that your templates and helpers will also not react to items
changing -- a template using #each
to iterate over items
will render empty and never rerender. You could make it act as a reactive source (the simplest way being to store the result with Session.set()
, or you can do it yourself), but unless you are doing a very expensive calculation that should be run as seldom as possible, you are better off using @zorlak's or @englandpost's methods. It may seem expensive to have your app querying the database repetitively, but Minimongo is caching the data locally, avoiding the network, so it will be quite fast. Thus in most situations, it's better just to use
Template.hello.rendered = ->
$('#typeahead').typeahead {source: -> _(Items.find().fetch()).pluck "name"}
unless you find that your app is really bogging down.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With