Using meteor for a test project. Can't figure out how to pass an ID and a search parameter when playing with the sample todo app they have.
For the moment, I have in my iron router:
this.route('team', {
path: '/team/:_id',
onBeforeAction: function() {
this.todosHandle = Meteor.subscribe('todos', this.params._id);
// Then filter mongoDB to search for the text
}});
The thing is, I also want to pass an optional search parameter to search for todos. So something like path: '/team/:_id(/search/:search)?'
Any ideas how to do this?
From your explanation, it sounds like you would like to carefully control which documents are actually published to the client, rather than publishing all of them and narrowing down your result set on the client. In this case, I would suggest first defining a publication on the server like so:
Meteor.publish('todosByTeamIdAndSearch', function(todoTeamId, searchParameter) {
var todosCursor = null;
// Check for teamId and searchParameter existence and set
// todosCursor accordingly. If neither exist, return an empty
// cursor, while returning a subset of documents depending on
// parameter existence.
todosCursor = Todos.find({teamId: todoTeamId, ...}); // pass parameters accordingly
return todosCursor;
});
To read more about defining more granular publications, check this out.
With a publication like the one above defined, you can then setup your route like so:
Router.route('/team/:_id/search/:search', {
name: 'team',
waitOn: function() {
return Meteor.subscribe('todosByTeamIdAndSearch', this.params._id, this.params.search);
},
data: function() {
if(this.ready()) {
// Access your Todos collection like you normally would
var todos = Todos.find({});
}
}
});
As you can see from the example route definition, you can define the path for the route exactly as you would like to see it directly in the call to the Router.route() function and access the parameters directly passed in like in the waitOn route option. Since the publication has been defined like I suggested, you can simply pass those route parameters right to the Meteor.subscribe() function. Then, in the data route option, once you have checked that your subscription is ready, you can access the Todos collection like normal with no further narrowing of the result set if you do not need to do so.
In order to learn more about how to configure your routes, check these two links out: Iron Router Route Parameters and Iron Router Route Options
On the client, you would just use Meteor.subscribe('todos'); in top-level code. 'todos' here doesn't refer to the Collection, it's an arbitrary string. Subscriptions don't care about what route you're on.
On the server, you would have a publish function like this:
Meteor.publish('todos', function() {
if (!Meteor.userId()) return;
// return all todos (you could pass whatever query params)
return Todos({});
});
Then, on your route definition:
Router.route('team', {
path: '/team/:_id',
data: function() {
if (this.params.query) { //if there's a query string
return Todos.find(/* according to the query string */).fetch();
else {
// return all the user's todos
return Todos.find({ uid: this.params._id }).fetch();
}
}
});
Related
I am trying to understand what a basic implementation of loading a routerLink while also pulling in saved url params would look like. Normally, the way I handle routing in my app is via a subscribing to an observable, that looks like this:
private onFilterProcessed(value: any, type: string, body, page)
{
if (type === 'zip')
{
this.processType('addresses.zip', value);
} if (type === 'location')
{
this.processType('addresses.city', value);
this.filtersService.getByCategory(
page, this.pagesize, this.body)
.subscribe(resRecordsData => {
let data = resRecordsData.data;
},
responseRecordsError => this.errorMsg = responseRecordsError);
}
This allows me to pass in some filter parameters to the api call as part of the body in a POST request. This will return results where the user's filter selections are passed in before returning the data.
This all works as expected. When a user goes "back" to a previously loaded component, their previous filter selections will be passed into the api call, so the "page" looks just like it did when they were last on that page/component.
However, I also have a couple of sections in my app where I'm loading components via routerLink instead. They initially looked like this:
<a routerLink="/customer-history" routerLinkActive="selected">Customer History</a>
The thing is, now that I have filter params in the url, this alone won't work, because each time I click on these particular links, it will wipe out the url and re-load it with just the page identifier "customer-history" -- because that's what I'm currently telling it to do.
If, for instance, the filters had been used by the user to filter results based on a city, the url would look like this:
http://localhost:4200/customer-history;locations=true;locations_place=Seattle
So the problem is, if they were to click away, and then RETURN to that page/component via the routerLink link, instead of getting the filtered data for that page, it will instead load this:
http://localhost:4200/customer-history
So my question is about how to pass those url params in as part of the routerLink. I assume it would look something like this, with square brackets for binding:
<a [routerLink]="['/customer-history', getParams()]" routerLinkActive="selected">Customer History</a>
What I'm not clear on is how I get those specific url params (just the filter params, not the component/page name) and pass them in via binding like this.
I know Angular makes available activatedRoute.snapshot, which I could get like this, to pass into getParams():
getParams()
{
return this.activatedRoute.snapshot;
}
But this will return the full url, not just the filter params part, which is what I need. So how would I get the part of the url I need, and pass it in here to append to "customer-history" in the url?
What would that look like in a basic implementation?
A way to resolve this, instead of using routerLink in the template, is to pass a function that resolves the correct page/component while subscribing to and navigating to that page and all relevant url params.
To do this I do this in the view:
<a (click)="goToCustomerHistory()">Customer History</a>
And that function in the component looks like this:
goToCustomerHistory()
{
this.route.params.subscribe(
(params: any) => {
this.page = params['page'];
this.locations = params['locations'];
this.locations_place = params['locations_place'];
}
);
this.router.navigate(
['/customer-history', {
page: this.page,
locations = this.locations;
locations_place = this.locations_place;
}]);
}
And of course, you also need to import Router and ActivatedRoute and inject in the constructor:
constructor(private router: Router,
private route: ActivatedRoute){}
I am trying to find the account associated with the current user in an ember project. I am able to get the users id and pass it to a handlebars script via {{account.user.id}}. All my attempts to find an account with this user ID in my model hook, however, have been unsuccessful.
My current model hook in routes/my-account.js:
model (params) {
let accountID = this.store.query('account', { filter: { user: { id:currentUser} } });
console.log(accountID.id);
return this.get('store').findRecord('account', accountID);
}
accountID is returning as an ember class, but I cannot seem to parse any data from it. How would I go about getting the ID from the returned ember class in order to pass it to my get request?
To get and set properties from and to Ember objects, you have to use get and set, e.g.:
console.log(account.get('id'));
More to the point, though, your .query will (or should, at least) return an array of account models matching the filter. It will be wrapped in a promise—because it's an asynchronous network call—so you'll need to .then it. And you probably just want to grab the first account:
model() {
return this.store.query('account', { filter: { user: { id: currentUser } } })
.then(function(accounts) {
return accounts.get('firstObject');
});
}
If you have a proper {json:api}, you can just get the user, and then get its account relationship from e.g. /api/users/:id/account. Your model hook would look something like:
model() {
return this.store.findRecord('user', currentUser)
.then(function(user) {
return user.get('account');
});
}
EDIT: Here is the github repo. And you can test the site here.
On the homepage, just open the browser console and you will notice that WaitOn and data are being run twice. When there is no WaitOn, then the data just runs once.
I have setup my pages by extending RouteController and further extending these controllers. For example:
ProfileController = RouteController.extend({
layoutTemplate: 'UserProfileLayout',
yieldTemplates: {
'navBarMain': {to: 'navBarMain'},
'userNav': {to: 'topUserNav'},
'profileNav': {to: 'sideProfileNav'}
},
// Authentication
onBeforeAction: function() {
if(_.isNull(Meteor.user())){
Router.go(Router.path('login'));
} else {
this.next();
}
}
});
ProfileVerificationsController = ProfileController.extend({
waitOn: function() {
console.log("from controller waitOn");
return Meteor.subscribe('userProfileVerification');
},
data: function() {
// If current user has verified email
console.log("from controller data start");
var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';
var data = {
verifiedEmail: verifiedEmail,
verifiedPhoneNumber: verifiedPhoneNumber
};
console.log("from controller data end");
return data;
}
});
On observing the console in the client, it seems the hooks are being run 2-3 times. And I also get an error on one of the times because the data is not available. The following is the console on just requesting the page once:
from controller waitOn
profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:44 from controller data start
debug.js:41 Exception from Tracker recompute function: Cannot read property 'phoneNumber' of undefined
TypeError: Cannot read property 'phoneNumber' of undefined
at ProfileController.extend.data (http://localhost:3000/lib/router/profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:46:62)
at bindData [as _data] (http://localhost:3000/packages/iron_controller.js?b02790701804563eafedb2e68c602154983ade06:226:50)
at DynamicTemplate.data (http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:219:50)
at http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:252:25
at null.<anonymous> (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2445:26)
at http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1808:16
at Object.Blaze._withCurrentView (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2043:12)
at viewAutorun (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1807:18)
at Tracker.Computation._compute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:296:36)
at Tracker.Computation._recompute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:310:14)
from controller data start
from controller data end
from controller waitOn
from controller data start
from controller data end
Have I not used the controllers properly?
Without being able to see the rest of the code that you have defined that uses these route controllers (such as templates or route definitions), I cannot accurately speak to the reason for the data function being called multiple times. I suspect that you may be using the ProfileVerificationsController with multiple routes, in which case the data definition for this controller would be executed multiple times, one for each route that uses the controller. Since the data definition is reactive, as you browse through your application and data changes, this might be resulting in the code defined to be rerun.
As for your controller definitions, I would suggest making a few modifications to make the code more robust and bulletproof. First, the ProfileController definition:
ProfileController = RouteController.extend({
layoutTemplate: 'UserProfileLayout',
yieldRegions: {
'navBarMain': {to: 'navBarMain'},
'userNav': {to: 'topUserNav'},
'profileNav': {to: 'sideProfileNav'}
},
onBeforeAction: function() {
if(!Meteor.user()) {
Router.go(Router.path('login'));
this.redirect('login'); // Could do this as well
this.render('login'); // And possibly this is necessary
} else {
this.next();
}
}
});
Notice the first thing that I changed, yieldTemplates to yieldRegions. This typo would prevent the regions from your templates using this route controller to be properly filled with the desired subtemplates. Second, in the onBeforeAction definition, I would suggest checking not only whether or not the Meteor.user() object is null using Underscore, but also checking for whether or not it is undefined as well. The modification that I made will allow you to check both states of the Meteor.user() object. Finally, not so much a typo correction as an alternative suggestion for directing the user to the login route, you could use the this.redirect() and this.render() functions instead of the Router.go() function. For additional information on all available options that can be defined for a route/route controller, check this out.
Now for the ProfileVerificationsController definition:
ProfileVerificationsController = ProfileController.extend({
waitOn: function() {
return Meteor.subscribe('userProfileVerification');
},
data: function() {
if(this.ready()) {
var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';
var data = {
verifiedEmail: verifiedEmail,
verifiedPhoneNumber: verifiedPhoneNumber
};
return data;
}
}
});
Notice the one thing that I changed, which is to wrap all of your code defined in the data option for your controller with a if(this.ready()){}. This is critical when using the waitOn option because the waitOn option adds one or more subscription handles to a wait list for the route and the this.ready() check returns true only when all of the handles in the wait list are ready. Making sure to use this check will prevent any cases of data unexpectedly not being loaded yet when you are building up your data context for the route. For additional information on defining subscriptions for your routes/route controllers, check this out.
As a final suggestion, for your onBeforeAction option definition in your ProfileController, I would suggest moving this out into its own global hook like so:
Router.onBeforeAction(function() {
if(!Meteor.user()) {
Router.go(Router.path('login'));
} else {
this.next();
}
});
Defining this check in the global hook ensures that you don't have to worry about adding your ProfileController to all of your routes just to make sure that this check is run for all of them. The check will be run for every route every time that one is accessed. Just a suggestion, though, as you may have reasons for not doing this. I just wanted to suggest it since I make sure to do it for every Meteor app that I develop for additional security.
I have a route that creates a new record like so:
App.ServicesNewRoute = Ember.Route.extend({
model : function() {
return this.store.createRecord('service');
},
setupController: function(controller, model) {
controller.set('model', model);
},
});
Then I bind that model's properties to the route's template using {{input type="text" value=model.serviceId ... }} which works great, the model gets populated as I fill up the form.
Then I save record:
App.ServicesNewController = Ember.ObjectController.extend({
actions : {
saveService : function() {
this.get('model').save(); // => POST to '/services'
}
}
});
Which works too.
Then I click the save button again, now the save method does a PUT as expected since the model has an id set (id: 102):
But then when I look at the PUT request in Dev Tools, I see that the id attribute was not serialized:
As a result, a new instance is created in the backend instead of updating the existing one.
Please ignore the serviceId property, it is just a regular string property unrelated to the record id which should be named just id.
I don't know why the id is not being serialized... I cannot define an id property on the model of course since Ember Data will not allow it, it is implicit. So I don't know what I am missing...
Any help is greatly appreciated!
The base JSONSerializer in Ember-Data only includes id in the payload when creating records. See DS.JSONAdapter.serialize docs.
The URL the RestAdapter generates for PUTting the update includes the ID in the path. In your case I believe it would be: PUT '/services/102'.
You can either extract it from the path in your backend service. Or you should be able to override the behavior of your serializer to add the id like this:
App.ServiceSerializer = DS.JSONSerializer.extend({
serialize: function(record, options) {
var json = this._super.apply(this, arguments); // Get default serialization
json.id = record.id; // tack on the id
return json;
}
});
There's plenty of additional info on serialization customization in the docs.
Hope that helps!
Initially I used ronco's answer and it worked well.
But when I looked at ember data's source code I noticed that this option is supported natively. You just need to pass the includeId option to the serializer.
Example code:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serialize: function(record, options) {
options = options ? options : {}; // handle the case where options is undefined
options.includeId = true;
return this._super.apply(this, [record, options]); // Call the parent serializer
}
});
This will also handle custom primary key definitions nicely.
Well, as far as I know it's a sync issue. After first request you do the post request and then, it has been saved in the server, when you click next time the store haven't got enough time to refresh itself. I've got similar issue when I've created something and immediately after that (without any transition or actions) I've tried to delete it - the error appears, in your case there's a little bit another story but with the same source. I think the solution is to refresh state after promise resolving.
In my application I want to read the parameters user is entering and then I want to use that parameter. http://responsive.beta.postify.com/X I want to read that X value. But first how do I ensure that the router expects a parameter?
My router is like this
Cards.Router.map(function ()
{
this.resource('cards', {path: '/'}, function ()
{
// additional child routes
this.resource('selectImage');
this.resource('message');
this.resource('recipient');
this.resource('orderStatus');
this.resource('thankyou');
this.resource('accountInfo');
this.resource('recentOrders');
this.resource('howTo');
this.resource('faq');
});
});
I want that parameter whenever the app loads. That is going to be my clientID which I would be using to fetch data from server depending upon the client.
Any thoughts on it?
When I do something like this
Cards.Router.map(function ()
{
this.resource('cards', {path: ':clientID'}, function ()
{
// additional child routes
this.resource('selectImage');
this.resource('message');
this.resource('recipient');
this.resource('orderStatus');
this.resource('thankyou');
this.resource('accountInfo');
this.resource('recentOrders');
this.resource('howTo');
this.resource('faq');
});
});
and in my browser if I put like this http://responsive.beta.postify.com/#/26 then its working but if I do like http://responsive.beta.postify.com/26 then it is not working.
To answer your question directly, to use a parameter in a route you would do something like this:
this.resource('cards', { path: '/:user_id' });
Then in your route
App.CardsRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('post', params.user_id);
}
});
This is how you can get a parameter in a certain route. Now as far as your application goes, using the code I posted above should get you that parameter as long as they access the root ('/') of your application on first load and have the user_id in the url.
I would suggest a different strategy maybe for getting the client_id and storing it for later user in your application. For example, in my application I have an Ember.Application.initializer({}) where I store the client_id. All depends on your server configuration and how your app is built, but I would definitely try and get the client_id a different way if you can!
Good luck.