In Meteor, using iron-router, I'm trying to implement a route mapping which has a dynamic segment and a fallback route if the dynamic segment does not match any items in a collection. For example, say I have a URL like so:
http://foobar.com/harold
I would like to first check if harold matches any IDs in a Posts collection. If there is a match then it should take me to a postPage template.
If there are no matches, then the router should render harold matches any items.
I've searched through all the iron-router documentation, but can't seem to figure out the right approach. I wonder if there is something like this.next() which cancels the current route mapping and goes to the next route mapping. Here's my attempt to try to do it:
Router.map(function () {
this.route('postPage', {
// matches: '/MFLS6aKqqRZ2JGz9q'
// matches: '/81zBqGE85aAfjk1js'
path: '/:postId',
before: function () {
//check if segment matches a post ID
post = Posts.findOne(this.params.postId);
if (!post) {
//If no matches, then go to next route
//Something like this.next()?
}
},
data: function() {
return Posts.findOne(this.params.postId);
}
});
this.route('profilePage', {
// matches: '/harold'
// matches: '/brendan'
path: '/:username',
data: function() {
return Profiles.findOne(this.params.username);
}
});
});
What you're describing is really just one route: /:input where input can be anything. As #David Weldon mentioned, this isn't a good idea; it basically subverts the point of using a router, and causes a lot of extra code and database queries to run every time the URL changes.
That said, if you really want to do it that way, you just need to collapse all your code into one route:
Router.map(function () {
this.route('theOneRouteToRuleThemAll', {
path: '/:input',
data: function() {
if (Profiles.findOne({username: this.params.input}) != null)
return Profiles.findOne({username: this.params.input});
else if (Posts.findOne({postId: this.params.input} != null)
return Posts.findOne({postId: this.params.input};
else
return null; // No user or post found; could handle with notFoundTemplate
}
});
});
Related
I have created an app with Vue with some different views/components. The views/components have to get run through in an specific order (MainPage -> Step1 -> Step2 -> Step3 -> FinalPage --> MainPage...).
As I am learning Vue I managed the navigation with v-if directives which worked well, as there are no URLs, so I was able to implement the logic in each view/component like
//pseudocode in each view/component
checkConditionsToMoveOn ? moveToNextPae : showErrorWithModalAndStayOnPage;
Getting more professional I implemented vue-router, which somehow broke my app as I am able now to trick the logic to pass a step with just changing the URL manually.
I can think of implementing some navigation guards, but then I have to move the conditions to my router/index.js file, which I think is not really pretty.
Is there any best practice to solve this problem?
I'd appreciate a hint, thanks.
Using Vuex, track the current state of step progress:
state: {
step: 1
}
When the user visits any step, you'll check this state. And if the requested step route doesn't match this, you'll redirect to it. To implement this, define the route with route params:
// import store from '#/store';
{
path: '/step/:sid',
name: 'step',
component: Step,
...
Now you can direct your app to routes like http://localhost:8080/step/5. The variable to.params.sid would match the 5 in that route. So, in the beforeEnter guard, check that the param matches the store's step.
...
beforeEnter: (to, from, next) => {
const nextStep = parseInt(to.params.sid); // convert string param to number
if (nextStep === store.state.step) { // is this the current allowed step?
next(); // if so, it's ok
} else { // if not, direct to that step
next({ name: 'step', params: { sid: store.state.step }})
}
}
},
Whenever a step is completed, you'll increment the store step and then direct the router to the next step.
Extra:
You could also change the if condition to allow for visiting previous steps:
if (nextStep > 0 && nextStep <= store.state.step) {
I had similar issue.
I think best solution is put steps into single route. It is easy to handle and hard to trick the logic.
But, if you must split steps into routes for some reasons (each step is so big to put in single route or ...), you can use step counter as global (above router-view).
I suggest two way to set global value.
Set value in App.vue (contains router-view) as data. Serve value as props to every routes. Each route checks step is valid and change url to valid step using $router.replace to prevent backward navigation. If step is valid for this url, process the logic and emit new step number before change url.
App.vue
<template>
<router-view :step="fooStep" #changeStep=changStep />
</template>
<script>
export default {
data () {
return {
fooStep: 0
}
},
methods: {
changeStep (value) {
this.step = value
}
}
}
SomeComponent.vue
<script>
export default {
name: 'Step3', /* optional */
props: ['step'],
mounted () {
/* some logic to check step is valid */
if (this.step !== 3) {
this.$router.replace(this.step > 0 ? '/step' + this.step : '/')
}
},
methods: {
submit () {
...
/* after submit data, send signal to App.vue to change step value */
this.$emit('changeStep', 4)
/* change url */
this.$router.replace('step4')
}
}
}
</script>
Second way is using Vuex the vue store. Vuex is good way to manage global value of vue app. My example is too brief that violate vuex rule. Find more information about Vuex. It is too long to write here.
SomeComponent.vue
<script>
export default {
name: 'Step3', /* optional */
mounted () {
/* some logic to check step is valid */
const step = this.$store.state.step
if (step !== 3) {
this.$router.replace(step > 0 ? '/step' + step : '/')
}
},
methods: {
submit () {
...
/* after submit data, store new step value in vuex */
/* change the value directly is not good */
this.$store.state.step = 4
/* change url */
this.$router.replace('step4')
}
}
}
</script>
Not sure what the issue is but my Navigo router is duplicating routes.
The Router:
this.Navigo.hooks({
before: (done, params) => {
// some tomfoolery
done();
}
});
this.Navigo.on({
'/:region/travel': (params) => {
// import Travel module
// some nonsense
},
'/:region/travel/car': (params) => {
// import TravelCar module
// some nonsense
}
)};
this.Navigo.resolve();
The Problem
this.Navigo.navigate('/london/travel/car');
Navigating to /london/travel/car is also triggering the route for /london/travel and thus causing all kinds of twaddle.
Is this standard behaviour? If not, what could be wrong?
I could rewrite the routes so they don't collide e.g. /london/travel-by-car, but I really don't want to if I can avoid it.
UPDATE 1:
I tried switching the order of routes but makes no difference. I did this by declaring the longest travel routes first, /:region/travel/car, and the smallest, /:region/travel, last.
UPDATE 2:
The more I look into this, the more I'm convinced this cannot be achieved with Navigo. Navigo do not support nested routes. If somebody could confirm that my routes are in fact 'nested', I will use an alternative routing library that does support them.
My code is a little different, but works the way you expect:
var router = new Navigo("/");
var render = (content) => (document.querySelector("#app").innerHTML = content);
router
.on('/:id', ({ data }) => {
setuserId(data.id)
if (verifiedUser) {
console.log("User verified");
} else {
console.log("User NOT verified");
}
rendertemplate(userDataURL(), "#landing-template", "#app")
})
.on('/:id/q', ({ data }) => {
// Example - flaging a send with 's' from 'SMS', perhaps a diff flow?
setuserId(data.id)
rendertemplate(userDataURL(), "#landing-template", "#app")
console.log("Source was a QRcode");
})
.on('/:id/q/t', ({ data }) => {
// Example - flaging a send with 's' from 'SMS', perhaps a diff flow?
setuserId(data.id)
rendertemplate(userDataURL(), "#landing-template", "#app")
console.log("Source was a QRcode in a Train");
})
This will give me a single discreet ".. verified"/"Source was a QRcode"/"Source was a QRcode in a Train" console.log response.
B
I'm creating a set of routes, for example
/ - should render home page template
/items - should items page template
/items/weeARXpqqTFQRg275 - should return item from MongoDB with given _id
This is example of what I'm trying to achieve
Router.route('items/:_id', function () {
var item = return myItems.find(:_id);
this.render(item);
});
[update - solved]
solved this by using Router.map on server side instead of Router.route
Router.map(function () {
this.route('post', {
path: 'items/:_id',
where: 'server',
action: function(){
var id = this.params._id;
var json = myItems.findOne({_id: id});
this.response.setHeader('Content-Type', 'application/json');
this.response.end(JSON.stringify(json, null, 2));
}
});
});
There are several problems with your code.
First, it seems you want to get the _id parameter from the url and don't know how. It's stored on this.params, so it's this.params._id.
Second, the first parameter you should send to find is a MongoDB query that in your case would be { _id: this.params._id }.
Third, that's not how you render something in Iron Router. The string parameter on the render method is the name of the template you want to render, not the item.
Assuming that myItems is a valid collection and your template is called showItem, your code should be something like:
Router.route('items/:_id', {
name: 'showItem',
data: function () {
return myItems.find({ _id: this.params._id });
}
});
Try something like this:
Router.map(function () {
this.route('items/:myItemId', {
data: function(){
return myItems.findOne({_id: this.params.myItemId});
}
});
});
good luck!
I'm trying to make a publishment statement to publish
ONLY the author(OP)'s profile avatar. I am thinking of grabbing the _id of the page. And from that page, I will grab the userId which is the author's _id and try to show the profile.
However, I have been very unsuccessful, and currently, I am using the following. Publishing EVERY user's profile avatar.
Publications.js
//Need to filter this to show only OP.
Meteor.publish("userPostAvatar", function() {
return Meteor.users.find( {} ,
{
fields: {'profile.avatar': 1}
})
});
Meteor.publish('singlePost', function(id) {
check(id, String);
return Posts.find(id);
});
Router.js
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('userStatus'),
Meteor.subscribe('userPostAvatar')
];
},
data: function() {
return Posts.findOne({_id:this.params._id});
}
});
You can do a simple join in the userPostAvatar publish function like this:
Meteor.publish('userPostAvatar', function(postId) {
check(postId, String);
var post = Posts.findOne(postId);
return Meteor.users.find(post.authorId, {fields: {profile: 1}});
});
This assumes posts have an authorId field - adjust as needed for your use case. Note three important things:
You will need to subscribe with this.params._id just as you did for singlePost.
The join is non-reactive. If the author changes, the avatar will not be republished. Given the general nature of posts I assume this isn't a problem.
I didn't publish the nested field profile.avatar on purpose because doing so can cause weird behavior on the client. See this question for more details.
I believe you can achieve this within the iron:router data context, by finding the post, associated author (whatever the field is), and then the subsequent user avatar. You can return an object to the iron:router data context. Then you can access post and avatar in the template as variables (so you might need to adjust the template output a little).
Publications.js
Meteor.publish("userPostAvatar", function() {
return Meteor.users.findOne( {} ,
{
fields: {'profile.avatar': 1}
})
});
Meteor.publish('singlePost', function(id) {
check(id, String);
return Posts.find(id);
});
Router.js
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('userStatus'),
Meteor.subscribe('userPostAvatar')
];
},
data: function() {
var post = Posts.findOne({_id: this.params._id});
var avatar = Users.findOne(post.authorId).profile.avatar;
return {
post: post,
avatar: avatar
};
}
});
Two problems with this method are that you could achieve the same thing with template helpers, and the user publication hasn't been limited to one user (I'm unsure how to do this unless we know the authorId within the waitOn, although maybe you could try moving the logic to there instead of the data context as my example shows).
If the data function returns a falsy value like null, the NotFound template will be rendered in my application. This works fine, but now I want also to render the NotFound template, if the route does not exist.
For instance:
this.route('settingsOverviewPage', {
path: '/settings',
data: function() { return Users.findOne(Meteor.userId()); },
waitOn: function() {
if (Meteor.userId()) {
return Meteor.subscribe('ownUser', Meteor.userId());
}
return null;
}
});
If I use this route: /settings12345 for instance, the browser reloads, but it renders the last route.
Any help would be greatly appreciated.
You have to define a "catch-all" route like this :
this.route("notFound",{
path:"*",
template:"notFoundTemplate"
});
It is important that you define this route as the LAST one, otherwise it will catch valid URLs.