How to determine the Ember route name from a URL? - ember.js

I need determine the route name from a URL string.
This is normally work that the Router does internally, but because of {insert long story here} I need to do it manually. I am given a piece of data from the API that looks like 'gallery/123' and I need to know the route name is 'gallery.post'. So that I can do route.replaceWith(determinedRouteName);
Need to turn 'gallery/123' into 'gallery.post'
Need to turn 'stuff/99/comments' into 'stuff.post.comments'
Etc

Ember relies internally to the router-recognizer micro lib. I think that your best option is to use it as well.
Unfortunately, accessing the router instance is currently requiring to rely on the private -routing service. (Note: here is a pending RFC about offering a public router service).
Injecting this private service:
import Ember from 'ember';
export default Ember.Route.extend({
router: Ember.inject.service('-routing'),
...
});
The recognize function returns the list of all handlers that you can join if you want to build the complete string or anything else.
Ex:
this.router.recognize("stuffs/99/comments");
> [{handler: "application", ...},
handler: "stuffs", ...},
handler: "stuff", ...},
handler: "comments", ...},
handler: "comments.index, ...}]
Hope it helps

Are you going to always only have one ID in the route? Or will the first ID always be the 'post' ID at least? If so you this isn't really an Ember issue it would just be a JavaScript find/replace that always follows the same format assuming the API data always come back in the same format.
Assuming the API always provides this format:
"something/:post_id/optionally_something_else"
You could take the string that the API provided and run it through a couple replace methods:
var apiString = "stuff/99/comments";
apiString = apiString.replace(/[0-9]+/, "post").replace(/\//g, "."); // => "stuff.post.comments"
// Also works with:
var apiString = "gallery/123";
apiString = apiString.replace(/[0-9]+/, "post").replace(/\//g, "."); // => "gallery.post"
DISCLAIMER: This is pretty icky and will break if the string doesn't follow this format. It will also only covert the first set of digits to "post". But as usual, use it if you must :P

Related

How to maintain unknown/wildcard queryParams through a transition?

I have a route (route-a) that transitions to another route (route-b) and I am trying to find a way for the destination URL to maintain the all query parameters, even if route-b does not know about them in advance.
For example, if a user visits https://example.com/route-a/?var1=x&var2=y, and the transition to route-b happens like this:
afterModel(model, transition) {
this.transitionTo('route-b', model, {queryParams: transition.to.queryParams}) // transition route-a to route-b
}
...the ultimate URL will be https://example.com/route-b/ — without the query params.
Now, I realize the "Ember way" is to define the queryParams on route-b's controller in advance, but in this particular use-case, I do not know the queryParams in advance. Route B consumes any and all query params provided to it, which means they would be impossible to enumerate in advance.
How can I transition to a new route without dropping query parameters that are not specifically enumerated on the destination route's controller?
Is there a way to handle unknown queryParams, or is there the notion of a wildcard for queryParams (similar to *path routes)?
Update: I'm not marking this as the answer, because as jelhan notes below, using a computed property for this key is explicitly identified as a no-no in the docs. But it worked for our use-case, and it might for others, though I'm guessing it may break down if you have additional queryParams in other routes that might conflict when Ember attempts to combine them.
Previous answer:
My solution here ended up using Ember's computed method to auto-generate the Array of query params by parsing the URL.
queryParams: computed("router.location", function () {
let qp = this.get("router.location").getURL().split("?")[1];
if (qp) {
let qpAsObj = JSON.parse(
'{"' +
decodeURI(qp)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
);
return Object.keys(qpAsObj)
}
})
If you don't want to subsequently maintain those query params on the page/model the next time a user re-visits that page ("sticky query params"), you will also need to remove the queryParams on the route:
resetController(controller) {
// unset all queryParams when leaving the route
controller.queryParams.forEach(v => {
controller.set(v, null)
})
}
This solution is... not ideal, but it works and we have tests written to ensure that we will catch any errors if it breaks going forward.

Lookup route for URL in Ember?

Does Ember expose an API I can use to manually look up a route (as a dot separated path) from a URL?
For example:
Ember.routeForURL('/foo/bar')
There is no public API for that as far as I know. What you can do is use router's recognizer.
let owner = Ember.getOwner(this);
let router = owner.lookup('router:main');
let handlers = router.router.recognizer.recognize('/foo/bar')
"handlers" will contain an array of objects where handler property will be something like this ["application", "foo", "foo.bar"]
And then you can probably use the last handler to do
owner.lookup('route:' + lastHandler)

How can I create an Express route that handles URLs that look like this?

app.get('/:service[SOMETHING GOES HERE]', function(req, res, next){
console.log('Service is:', req.params.service);
});
This needs to catch URLs that can look like any one of:
/foo
/foo/bar
/foo/bar/baz
The call back isn't concerned with anything that comes after foo, but ideally should be able to access foo as a parameter called service without having to manually parse the path.
I've been using this to test and still haven't found anything that does exactly that. Closest so far is /:service*.
Edit: No it's not a duplicate of the one where the answer is /:service/* because that doesn't cover /foo.
Using /:service* in actual Express routes does exactly what you want:
/foo maps to { '0': '', service: 'foo' }
/foo/bar maps to { '0': '/bar', service: 'foo' }
/foo/bar/blah maps to { '0': '/bar/blah', service: 'foo' }
The Express Route Tester, for some reason, maps these URL's differently for that these kinds of patterns (it might be configured differently than Express).
You can use the app.use() function for that. Read the doc about path handling for more info. Your code once modified will be:
app.use('/foo', function(req, res, next){
console.log('Service is:', req.params.service);
});
The downside is that you are not going to recover foo as the service parameter.

Ember makes unwanted call to backend in model hook

I want to be able to retrieve a certain conversation when its id is entered in the URL. If the conversation does not exist, I want to display an alert message with a record not found.
here is my model hook :
model: function(params){
return this.store.filter('conversation', { status : params.status}, function(rec){
if(params.status == 'all'){
return ((rec.get('status') === 'opened' || rec.get('status') === 'closed'));
}
else{
return (rec.get('status') === params.status); <--- Problem is here
}
});
}
For example, if I want to access a certain conversation directly, I could do :
dev.rails.local:3000/conversations/email.l#email.com#/convid
The problem is when I enter a conversation id which doesn't exist (like asdfasdf), ember makes call to an inexisting backend route.
It makes a call to GET conversation/asdfasdf. I'm about sure that it is only due to the record not existing. I have nested resources in my router so I'm also about sure that it tries to retrieve the conversation with a non existing id.
Basically, I want to verify the existence of the conversation before returning something from my hook. Keep in mind that my model hook is pretty much set and won't change, except for adding a validation on the existence of the conversation with the id in the url. The reason behind this is that the project is almost complete and everything is based on this hook.
Here is my router (some people are going to tell me you can't use nested resources, but I'm doing it and it is gonna stay like that so I have to work with it because I'm working on a project and I have to integrate ember in this section only and I have to use this setup) :
App.Router.map(function(){
// Routing list to raw namespace path
this.resource('conversations', { path : '/' }, function() {
this.resource('conversation', { path : '/:conversation_id'});
});
});
This also happens when I dont specify any id and I use the hashtag in my url like this :
dev.rails.local:3000/conversations/email.l#email.com#/ would make a call to conversation/
I know it is because of my nested resource. How can I do it?
By passing a query to filter (your { status : params.status}) you are asking Ember Data to do a server query. Try removing it.
From the docs at http://emberjs.com/api/data/classes/DS.Store.html#method_filter:
Optionally you can pass a query, which is the equivalent of calling find with that same query, to fetch additional records from the server. The results returned by the server could then appear in the filter if they match the filter function.
So, remove the query:
model: function(params){
return this.store.filter('conversation', function(rec) {
if (params.status == 'all') {
return rec.get('status') === 'opened' || rec.get('status') === 'closed';
} else {
return rec.get('status') === params.status;
}
});
}
Ok so here is what I did. I removed my nested resource because I realised I wasn't using it for any good reason other than redirecting my url. I decided to manually redirect my url using javascript window.location.
This removed the unwanted call (which was caused by the nested resource).
Thanks to torazaburo, you opened my eyes on many things.

What's the standard pattern for ember-data validations? (invalid state, becameInvalid...)

I've kinda been struggling with this for some time; let's see if somebody can help me out.
Although it's not explicitly said in the Readme, ember-data provides somewhat validations support. You can see that on some parts of the code and documentation:
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/states.js#L411
https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/states.js#L529
The REST adapter doesn't add validations support on itself, but I found out that if I add something like this in the ajax calls, I can put the model on a "invalid" state with the errors object that came from the server side:
error: function(xhr){
var data = Ember.$.parseJSON(xhr.responseText);
store.recordWasInvalid(record, data.errors);
}
So I can easily to the following:
var transaction = App.store.transaction();
var record = transaction.createRecord(App.Post);
record.set('someProperty', 'invalid value');
transaction.commit()
// This makes the validation fail
record.set('someProperty', 'a valid value');
transaction.commit();
// This doesn't trigger the commit again.
The thing is: As you see, transactions don't try to recommit. This is explained here and here.
So the thing is: If I can't reuse a commit, how should I handle this? I kinda suspect that has something to do to the fact I'm asyncronously putting the model to the invalid state - by reading the documentation, it seems like is something meant for client-side validations. In this case, how should I use them?
I have a pending pull request that should fix this
https://github.com/emberjs/data/pull/539
I tried Javier's answer, but I get "Invalid Path" when doing any record.set(...) with the record in invalid state. What I found worked was:
// with the record in invalid state
record.send('becameValid');
record.set('someProperty', 'a valid value');
App.store.commit();
Alternatively, it seems that if I call record.get(...) first then subsequent record.set(...) calls work. This is probably a bug. But the above work-around will work in general for being able to re-commit the same record even without changing any properties. (Of course, if the properties are still invalid it will just fail again.)
this may seem to be an overly simple answer, but why not create a new transaction and add the pre-existing record to it? i'm also trying to figure out an error handling approach.
also you should probably consider writing this at the store level rather than the adapter level for the sake of re-use.
For some unknown reason, the record becomes part of the store default transaction. This code works for me:
var transaction = App.store.transaction();
var record = transaction.createRecord(App.Post);
record.set('someProperty', 'invalid value');
transaction.commit()
record.set('someProperty', 'a valid value');
App.store.commit(); // The record is created in backend
The problem is that after the first failure, you must always use the App.store.commit() with the problems it has.
Give a look at this gist. Its the pattern that i use in my projects.
https://gist.github.com/danielgatis/5550982
#josepjaume
Take a look at https://github.com/esbanarango/ember-model-validator.
Example:
import Model, { attr } from '#ember-data/model';
import { modelValidator } from 'ember-model-validator';
#modelValidator
export default class MyModel extends Model {
#attr('string') fullName;
#attr('string') fruit;
#attr('string') favoriteColor;
validations = {
fullName: {
presence: true
},
fruit: {
presence: true
},
favoriteColor: {
color: true
}
};
}