Update Query Param with Iron Router - meteor

I'm trying to figure out the least brittle way to update a query param in Iron Router.
Flow-Router has FlowRouter.setParams({step: 2}) which is ideal.
Currently i'm using this but I wanted to check if there's a better way (especially since the IR API changes so frequently)
var currentId = Router.current().params.id;
var newStep = '2';
Router.go('checkout', {id: currentId}, {query: 'step=' + newStep});

This is the correct way, but you can use the object syntax for the query just like in FlowRouter.
Router.go('checkout', {
id: currentId
}, {
query: {
step: 2
}
});

Related

GraphQL Syntax for mapping array on mutation field

I'm looking for advice on syntax. In my query, I need my backend (Graphlite, Symfony) to get the child items. The way I get the regular item is as below, and works correctly. I'm not well versed enough in GraphQL / Apollo / VueJS / GraphQLite to know which technology owns the "{id: $itemId}" syntax, nor do I know what it is called, so I can't find any info about it.
insertItem: gql` mutation insertItem(
$itemId: Uuid!,
$childItemIds: [Uuid!]!
) {
insertItem(
item: {id: $itemId},
childItems: { id: { in : { $childItemIds} }
){
// ... stuff
}
}`
So, given that the {id: $itemId} correctly works for getting an item, I assume there is some graphQL syntax that would work to apply the childItemIds to get the childItems. Is there a name for this type of mapping? What would the syntax be?
As per discussion with #UjinT34, since this couldn't be done inline with GraphQL, I created an array of ItemInput before calling the mutation:
for (var i = 0, len = item.childItems.length; i < len; i++) {
childItems.push({ id : item.childItems[i].id } )
}
The original query then became:
insertItem:`` gql` mutation insertItem(
$itemId: Uuid!,
$childItemIds: [ItemInput!]!
) {
insertItem(
item: {id: $itemId},
childItems: $childItemIds
){
// ... stuff
}
}
Once I understood that the {id: $item} wasn't a magic GQL/GraphQLite syntax and was simply an inline javascript object being created, the solution became fairly simple.

dynamic query object for iron-router route

I have a current route as follows:
Router.route('/:arg', function(){
this.render('tmpResults', {
data: function(){
return hcDrefs.find({name: {$regex: this.params.query.q}})
}
});
}, {name: 'showSome'});
As you can see, the query is hard coded to really only take a single input (see block below). What I want to do is execute this route where the query object would need to be of a variable form...sometimes I only need a simple query, other times, the query may contain an AND operation. I currently call this route with:
Router.go('showSome', {arg: 1},
{query: '?q=' + e.target.value});
}
...what I want to know is if I can pass some kind of query object to the route somehow, but I haven't seem to hit on a syntax that work....not even sure IF it can work. I have tried some brute force stuff like defining a query object:
query_object = {name: {$regex: pattern}}
and attempting to get it to the router somehow like:
Router.go('showSome', {arg: 1},
{query: query_object});
}
//----------------------------------------------------------
Router.route('/:arg', function(){
this.render('tmpResults', {
data: function(){
return hcDrefs.find(this.params.query.q}})
}
});
}, {name: 'showSome'});
but that seems to be a no go.
What would be a good way to set the data context of a route where the query to yield the data context could be of a variable form? I would think a global object would probably work, but I am curious is there is a more strategic way passing through the router. I am still digging through the iron router docs for something I can hook onto.
There is no client-side POST, so the data you can pass to a client-side route is limited by what you can put into the URL (similar to a GET request). So, no, I don't think there is a more elegant solutions.
I think you have two options:
Stringifying your object into the URL:
Router.go('showSome', {arg: 1},
{query: JSON.stringify(query_object)});
//----------------------------------------------------------
Router.route('/:arg', function(){
this.render('tmpResults', {
data: function(){
return hcDrefs.find(JSON.parse(this.params.query)}})
}
});
}, {name: 'showSome'});
2) use a global or session variable:
Session.set('query', query_object);
Router.go('showSome', {arg: 1});
//----------------------------------------------------------
Router.route('/:arg', function(){
this.render('tmpResults', {
data: function(){
return hcDrefs.find(Session.get('query'))}})
}
});
}, {name: 'showSome'});

Best practice for meteor dynamic subscription

Is it possible to do something like "filtered subscription" in Meteor: for example if you have a filter on month june and switching to july fetches the new data and subscribes to it?
i tried something like:
Meteor.publish("report", function (query, opt) {
return Report.find({ 'timestamp' : { $gte : query.from, $lt: query.to }}, options);
}
on client with iron router:
HomeController=RouteController.extend({
template:"home",
waitOn:function(){
var dates = getDates();
return Meteor.subscribe("report", dates);
},
fastRender: true
});
but it does not work.
Is there a better method to dynamically subscribe? Or does it just help to navigate with url pattern?
thanks
Is there a better method to dynamically subscribe?
There is an alternative method using template subscriptions, example below. I don't think it's better, just different.
Or does it just help to navigate with url pattern?
If you want to handle the subscriptions in the Router, then storing the subscription query params in the URL does help and has some added benefits in my opinion. But it depends on your desired app behavior.
Using Template Subscriptions approach :
This Meteor Pad example will subscribe to a range of data based on a select :
http://meteorpad.com/pad/26dd8YQevBbA5uNGA/Dynamic%20Subscription
Using Iron Router approach :
This route example will subscribe based on the URL . "items/0/10" will subscribe to the itemData with a range of zero to 10.
Router.route('Items', {
name:'Items',
path:'items/:low/:high',
subscriptions : function(){
var low = parseInt(this.params.low);
var high = parseInt(this.params.high);
return [
Meteor.subscribe("itemData",low,high),
];
},
action: function () {
if (this.ready()) {
this.render();
} else {
this.render('Loading');
}
}
});
I think either approach is fine and depends on your interface. Using the URL is nice because you can provide links directly to the range of data, use forward and back buttons in browser, good for paging lists of data.
The template subscriptions approach might be appropriate to change the data on a graph.
The specific issue you are having might be due to the fact that your getDates() is not reactive, so the subscription is only run once when the route waitOn is first run.

Using Iron Router to waitOn subscription that's dependent on data from a doc that will come from another subscription

I'm having trouble configuring the waitOn portion of a route where one of the subscription's parameters is determined by the value from a doc that comes from a different subscription.
The collections in play are Candidates and Interviews. An interview will have one and only one candidate. Here's some sample data:
candidate = {
_id: 1
firstName: 'Some',
lastName: 'Developer'
//other props
};
interview = {
_id: 1,
candidateId: 1
//other props
};
The route is configured as follows.
this.route('conductInterview', {
path: '/interviews/:_id/conduct', //:_id is the interviewId
waitOn: function () {
return [
Meteor.subscribe('allUsers'),
Meteor.subscribe('singleInterview', this.params._id),
// don't know the candidateId to lookup because it's stored
// in the interview doc
Meteor.subscribe('singleCandidate', ???),
Meteor.subscribe('questions'),
Meteor.subscribe('allUsers')
];
},
data: function () {
var interview = Interviews.findOne(this.params._id);
return {
interview: interview,
candidate: Candidates.findOne(interview.candidateId);
};
}
});
The problem is that I don't have a candidateId to pass to the singleCandidate subscription in the waitOn method because it's stored in the interview doc.
I've thought of two possible solutions, but I don't really like either of them. The first is to change the route to something like /interviews/:_id/:candidateId/conduct. The second is to denormalize the data and store the candidate's info in the interview doc.
Are there any other options to accomplish this besides those two?
You may get some ideas by reading this post on reactive joins. Because you need to fetch the candidate as part of the route's data, it seems like the easiest way is just to publish both the interview and the candidate at the same time:
Meteor.publish('interviewAndCandidate', function(interviewId) {
check(interviewId, String);
var interviewCursor = Interviews.find(interviewId);
var candidateId = interviewCursor.fetch()[0].candidateId;
return [interviewCursor, Candidates.find(candidateId);];
});
However, this join is not reactive. If a different candidate gets assigned to the interview, the client will not be updated. I suspect that isn't a problem in this case though.
You can change your publish function singleCandidate to take interviewId as paramater instead of candidateId and pass this.params._id
I had similar problem I managed to solve it via callback in subscribe
http://docs.meteor.com/#/basic/Meteor-subscribe
For example you have user data with city ids, and you need to get city objects
waitOn: ->
router = #
[
Meteor.subscribe("currentUserData", () ->
user = Meteor.user()
return unless user
cityIds = user.cityIds
router.wait( Meteor.subscribe("cities", cityIds)) if cityIds
)
]

Meteor minimongo dynamic cursor

In my client UI I have a form with differents search criterias, and I'd like to reactively update the results list. The search query is transformed into a classical minimongo selector, saved in a Session variable, and then I have observers to do things with the results:
// Think of a AirBnb-like application
// The session variable `search-query` is updated via a form
// example: Session.set('search-query', {price: {$lt: 100}});
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
resultsCursor = Offers.find(Session.get('search-query'));
// I want to add and remove pins on a map
resultCursor.observe({
added: Map.addPin,
removed: Map.removePin
});
Deps.autorun(function() {
// I want to modify the cursor selector and keep the observers
// so that I would only have the diff between the old search and
// the new one
// This `modifySelector` method doesn't exist
resultsCursor.modifySelector(Session.get('search-query'));
});
How could I implement this modifySelector method on the cursor object?
Basically I think this method needs to update the compiled version of the cursor, ie the selector_f attribute, and then rerun observers (without losing the cache of the previous results). Or is there any better solution?
Edit: Some of you have misunderstood what I'm trying to do. Let me provide a complete example:
Offers = new Meteor.Collection('offers');
if (Meteor.isServer && Offers.find().count() === 0) {
for (var i = 1; i < 4; i++) {
// Inserting documents {price: 1}, {price: 2} and {price: 3}
Offers.insert({price:i})
}
}
if (Meteor.isClient) {
Session.setDefault('search-query', {price:1});
resultsCursor = Offers.find(Session.get('search-query'));
resultsCursor.observe({
added: function (doc) {
// First, this added observer is fired once with the document
// matching the default query {price: 1}
console.log('added:', doc);
}
});
setTimeout(function() {
console.log('new search query');
// Then one second later, I'd like to have my "added observer" fired
// twice with docs {price: 2} and {price: 3}.
Session.set('search-query', {});
}, 1000);
}
This doesn't solve the problem in the way you seem to be wanting to, but I think the result is still the same. If this is a solution you explicitly don't want, let me know and I can remove the answer. I just didn't want to put code in a comment.
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
Template.map.pins = function() {
return Offers.find(Session.get('search-query'));
}
Template.map.placepins = function(pins) {
// use d3 or whatever to clear the map and then place all pins on the map
}
Assuming your template is something like this:
<template name="map">
{{placepins pins}}
</template>
One solution is to manually diff the old and the new cursors:
# Every time the query change, do a diff to add, move and remove pins on the screen
# Assuming that the pins order are always the same, this use a single loop of complexity
# o(n) rather than the naive loop in loop of complexity o(n^2)
Deps.autorun =>
old_pins = #pins
new_pins = []
position = 0
old_pin = undefined # This variable needs to be in the Deps.autorun scope
# This is a simple algo to implement a kind of "reactive cursor"
# Sorting is done on the server, it's important to keep the order
collection.find(Session.get('search-query'), sort: [['mark', 'desc']]).forEach (product) =>
if not old_pin?
old_pin = old_pins.shift()
while old_pin?.mark > product.mark
#removePin(old_pin)
old_pin = old_pins.shift()
if old_pin?._id == product._id
#movePin(old_pin, position++)
new_pins.push(old_pin)
old_pin = old_pins.shift()
else
newPin = #render(product, position++)
new_pins.push(newPin)
# Finish the job
if old_pin?
#removePin(old_pin)
for old_pin in old_pins
#removePin(old_pin)
#pins = new_pins
But it's a bit hacky and not so efficient. Moreover the diff logic is already implemented in minimongo so it's better to reuse it.
Perhaps an acceptable solution would be to keep track of old pins in a local collection? Something like this:
Session.setDefault('search-query', {});
var Offers = new Meteor.Collection('offers');
var OldOffers = new Meteor.Collection(null);
var addNewPin = function(offer) {
// Add a pin only if it's a new offer, and then mark it as an old offer
if (!OldOffers.findOne({_id: offer._id})) {
Map.addPin(offer);
OldOffers.insert(offer);
}
};
var removePinsExcept = function(ids) {
// Clean out the pins that no longer exist in the updated query,
// and remove them from the OldOffers collection
OldOffers.find({_id: {$nin: ids}}).forEach(function(offer) {
Map.removePin(offer);
OldOffers.remove({_id: offer._id});
});
};
Deps.autorun(function() {
var offers = Offers.find(Session.get('search-query'));
removePinsExcept(offers.map(function(offer) {
return offer._id;
}));
offers.observe({
added: addNewPin,
removed: Map.removePin
});
});
I'm not sure how much faster this is than your array answer, though I think it's much more readable. The thing you need to consider is whether diffing the results as the query changes is really much faster than removing all the pins and redrawing them each time. I would suspect that this might be a case of premature optimization. How often do you expect a user to change the search query, such that there will be a significant amount of overlap between the results of the old and new queries?
I have the same problem in my own hobby Meteor project.
There is filter session var where selector is storing. Triggering any checkbox or button changes filter and all UI rerender.
That solution have some cons and the main - you can't share app state with other users.
So i realized that better way is storing app state in URL.
May be it is also better in your case?
Clicking button now change URL and UI rendering based on it. I realize it with FlowRouter.
Helpful reading: Keeping App State on the URL

Resources