RestAdaptor and API Structure - ember.js

I'm a newbie to Ember Data and all I've done to date is FIXTURE data but today I'm trying to graduate to the real deal and am realising that I don't know enough about how to connect the model and the API's call signature.
Specifically I'd like to be able to call an endpoint GET /activities/[:user_id]/[date]. This would load an array of "Activity" objects but only those for a given date. I know that I can offset the API's directory with:
DS.RESTAdapter.reopen({
namespace: 'api'
});
In my case the api prefix is appropriate. I think I should also be able to get the date component solved by setting up a route something like this:
this.resource('activities', { path: '/activities' }, function() {
this.route('by_date', {path: '/:target_date'});
});
The above is just an educated guess because I'm completely at a loss on how to get the user_id component into the URL signature. Can anyone help? Are there any good tutorials or examples of basic Ember Data use cases?
Also, because I know I'm going to run into this next ... how does one add parameters to the url string (aka, GET /foobar?something=value) versus parameters to the URL itself (like above)?
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
UPDATE
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
I've implemented the suggestions from #intuitivepixel but am still having some problems ...
First off I tried to hard code the values for userId and dateBy:
Router:
this.resource('activities', { path: '/activities' }, function() {
this.route('by_date', {path: '/:user_id/:by_date'});
});
Route:
App.ActivitiesByDateRoute = Ember.Route.extend({
serialize: function(activity) {
return {
userId: 1,
dateBy: "2013-07-01"
};
}
});
Sadly that did not work. I think I understand why -- although I don't have a quick way to fix this -- but more disturbing for me was that when I manually put in the parameters into the URL: http://restful.service.com/api/activities/1/2013-07-01. The results are quite surprising to me:
Initially the debugging messages suggest a success:
This however, is not correct as no network requests are actually made
If you reload the browser, it will now go out and get the Activities but to my surprise it also goes out to find the specified user. Hmmm. That's ok, the user 1 is pulled back successfully.
The Activity, however, is just a GET /activities call which fails because this endpoint needs the user and date qualifier to work. Why weren't these included in the request?

I know that I can offset the API's directory with:
You can also set a different URL of our API if it's the case:
DS.RESTAdapter.reopen({
url: 'http://myapihost.com',
namespace: 'api'
});
This would produce a URL like http://myapihost.com/api/
The above is just an educated guess because I'm completely at a loss on how to get the user_id component into the URL signature.
Following your example and adding the user_id dynamic segment, let's say you have this router map definition:
this.resource('activities', { path: '/activities' }, function() {
this.route('byDate', {path: '/:user_id/:target_date'});
});
and this is your {{linkTo}} helper:
{{#each activity in model}}
{{#linkTo 'activities.byDate' activity}}
{{/each}}
then to build your url out of multiple dynamic segments you could hook into your route's serialize function and returning a hash composed out of the dynamic segments your URL needs:
App.ActivitiesByDateRoute = Ember.Route.extend({
serialize: function(activity) {
return {
user_id: activity.get('userId'),
target_date: activity.get('targetDate')
}
}
});
The code above will generate a URL like /activities/[userId]/[targetDate] when the {{linkTo}} link is clicked. The example assumes that the properties you need to build your URL are available in your Activity model.
Also, because I know I'm going to run into this next ... how does one add parameters to the url string (aka, GET /foobar?something=value) versus parameters to the URL itself (like above)?
This kind of URL queries are not yet supported by the ember framework, but the are good workarounds/projects that try to deal with that missing feature like: https://github.com/alexspeller/ember-query, and if this PR get's merged some day then you will have them also soon in ember, but for the time beeing you could use the above mentioned library to have support for custom queries. For the current status whether the lib get's merged or not have look here: http://discuss.emberjs.com/t/query-string-support-in-ember-router there is a discussion going on.
Hope it helps.

Related

Is it possible to make a redirect rule in the routerjs file to create a vanity route

To speak specifically, I have a URL to one page on a website but the URL is somewhat long and ugly.
It might look a little bit like this
mywebsite.com/all-products/hats/golf-caps
however, my marketing team feels this URL is too long and wants me to add a "prettified" url like this:
mywebsite.com/golf-caps
But all it would really do is redirect to the original route.
Is there a way to do this in ember.js? Or is this a bad practice? Ideally, I would only need to modify the router file rather than create a completely new route in my application just to perform a redirect.
If you have control over the server then I would handle this there. It's nice to be able to send the correct 308 permanent redirect header so search indexing lines up and most backend routing solutions seem to have concept this built in.
As far as I'm aware of you need to create a new route with a beforeModel() hook containing the redirect. But I don't think that this adds a lot of complexity. It's basically ember generate route golf-caps and edit the created app/routes/golf-caps.js to contain:
export default class GolfCapsRoute extends Route {
beforeModel() {
this.transitionTo('all-products/hats/golf-caps');
}
}
You may want to delete the route's template app/templates/golf-caps.hbs as that won't be rendered in any case.
If you have more than one redirect, you may want to consider adding a wild-card route that catches all requests. This could do the redirects based on a map for multiple paths and render a not-found page if there isn't any redirect registered for the path.
Ember has resetNamespace (guides).
this.route('all-products', { resetNamespace: true, path: '/' }, function() {
this.route('categories', { resetNamespace: true, path: ':category_id' }, function() {
this.route('subcategories', { path: ':subcategory_id' });
});
});
I'm not sure how resetNamespace will work two levels deep like that though. Especially since you have something meaningful in the second level, i.e.,hats`. Your ember router needs that to know which 'category' to request from the server.
For example golf-caps 'subcategory' could be in two 'categories'. If someone navigated directly to mysite.com/golf-caps your server would have no way to know which golf-caps to show, the ones from hats or the ones from accessories.
The only way you'd be able to collapse them down is if all your subcategories were truly unique. In which case, then yes, do just get rid of the category level.
Assuming you still need the category, I think you'll just be able to get rid of all-products in your URL:
this.route('all-products', { resetNamespace: true, path: '/' }, function() {
this.route('categories', { path: ':category_id' }, function() {
this.route('subcategories', { path: ':subcategory_id' });
});
});

Dynamic segments other than id in Ember.js

I'm trying to wrap my head around dynamic segments and I want to be able to use a slug or other property instead of the id. When I can get things working it feels like a fluke. (I'm using ember 2.7+)
I've been looking at the following sources/threads for ideas:
https://guides.emberjs.com/v1.10.0/cookbook/ember_data/linking_to_slugs (1.10)
http://discuss.emberjs.com/t/slugs-for-replacing-id-path-for-dynamic-segments
I plan on using ember-data, but I want to ensure I'm in control - and so I don't want to use the :post_slug / underscore style that has some built in magic that I want to avoid.
Here is an ember-twiddle
Here are step-by-step commits in a github repo
My thought process
1. Conceptually, lets say I need a list of cats - so I need to describe the model for what a 'cat' is.
models/cat.js
import Model from "ember-data/model";
import attr from "ember-data/attr";
export default Model.extend({
name: attr('string'),
slug: attr('string')
});
2. define where the dynamic segment will be in the url. I'm going to use catId to prove a point instead of cat_id or :id like most of the tutorials I've seen. For this example, I'm also writing an actual app structure instead of the smallest router possible - to test the edges of this. + what if I needed something like this later: animals.com/cats/:catId/best-friend/:dogId
router.js
Router.map(function() {
this.route('index', { path: '/' });
this.route('cats', { path: '/cats' }, function() {
this.route('index', { path: '/' }); // list of cats
this.route('cat', { path: '/:catId' }); // cat spotlight
});
});
3. pull in the catData into the store ~ in the /cats route
routes/cats.js
import Ember from 'ember';
const catData = [
{
id: 1,
name: 'Dolly',
slug: 'dolly'
},
{
id: 2,
name: 'kitty cat',
slug: 'kitty-cat'
},
{
id: 3,
name: 'Cleopatra',
slug: 'cleo'
}
];
export default Ember.Route.extend({
model() {
return catData;
// return this.get('store').findAll('cat'); // with mirage or live api
}
});
Update from comments:
I do not believe that you can use queryRecord with your test data.
Ember data plays dumb with query and queryRecord; it doesn't assume
anything about your data and just passes the call on to your server.
~ #xcskier56
So this kinda blows my twiddle as is. The git repo example is Mirage.
4. create the templates... + set up the 'cat' route. The records are in the store... right? so I should be able to 'peek' at them based on id. The docs use params - but -
Ember will extract the value of the dynamic segment from the URL for
you - and pass them as a hash to the model hook as the first argument
: ~ and so the params object name isn't special and could really be anything you wanted... and is just replaced with a hash - so to that point / I'm using 'passedInThing' just to assert control over the confusing conventions (many tutorials use param instead of params)
routes/cats/cat.js
model( passedInThing ) {
return this.store.peekRecord('cat', passedInThing.catId );
} // not going to happen - but in theory...
model( passedInThing ) {
return this.store.queryRecord('cat', { slug: passedInThing.catId } );
}
5. At this point, I should be able to navigate to the url /cats/2 - and the 2 should get passed through the model hook - to the query. "Go get a 'cat' with an id of 2" --- right??? ((the twiddle example uses a hard-coded set of catData - but in my other attempts I'm using mirage with a combination of fixtures and dynamic slugs: https://github.com/sheriffderek/dynamic-segments-tests/commits/queryRecord
6. Typing in the segment works - but for link-to helpers I need to pass in the explicit cat.id
{{#link-to 'cats.cat' cat.id}}
<span>{{cat.name}}</span>
{{/link-to}}
7. I can get all that working - but I don't want an ID in the URL. I want cats/cleo with the 'slug' ~ in theory I can just switch catId for catSlug and cat.id to cat.slug etc - but that is not the case. I've seen many tutorials outlining this but they are outdated. I've tried passing in { slug: params.slug } and every combo of find query and peek etc. Sometimes the url will work but the model wont render - or the opposite.
8. This seems like 101 stuff. Can anyone explain this to me? Oh wise emberinos - come to my aid!
UPDATES
A nice video showing how to use serialize() in this case
There is supposedly an example coming to the Ember tutorial, but I haven't seen it land yet.
I've struggled with this same issue, and AFIK there is not a way to handle this without querying the server. While this is old and the code is a little out dated the logic still stands. http://discuss.emberjs.com/t/use-slug-for-page-routes-id-for-store-find/6443/12
What I have done is to use store.queryRecord() and then my server is able to return a record fetched via a slug.
This is what the route would look like:
model: function (params) {
return this.store.queryRecord('cat', {slug: params.catSlug})
}
This will enable you to not expose the ID in the url, but it will issue a query to the store every single time that the model gets hit. There seems to be some discussion of caching query and queryRecord in ember-data, but nothing working yet.
https://github.com/emberjs/data/issues/1966
Other helpful resources:
How to cache query result in ember data
Cache a record when using Query Params in the call? Ember-data

Getting different url requests to API depending on how I've gotten to the page

I'm getting some curious behaviour that I can't figure out the reason for.
This is my router:
App.Router.map(function() {
this.resource('mapPieceSets', { path: '/map-pieces' }, function () {
this.resource('mapPieceSet', { path: '/:mapPieceSet_id' }, function () {
this.resource('mapPiece', { path: '/:mapPiece_id' });
});
});
});
I reload the app from the home page #/ then navigate down to the mapPiece route, I get these URLs requested:
[Domain]/api/mapPieceSets/
[Domain]/api/mapPieces/1/
[Domain]/api/mapPieces/2/
And it all works fine (mapPieceSets returns a list of mapPieceSet which have a list of mapPiece against them)
However, if I reload the whilst on a mapPiece routed page, then I get this URL:
[Domain]/api/mapPieceSets/
[Domain]/api/mapPieceSets/?mapPieceSet_id=1
[Domain]/api/mapPieces/?mapPiece_id=1
So switching from /:id= to ?id=, which isn't working on my end points (that's a side issue which I need to resolve), but just wondering why the URLs changed what they're requesting, and why we get a request to mapPieceSets/?mapPieceSet_id=1 when the whole of that object is returned within the response from mapPieceSets/
(If you need any other snippets of code from my app, let me know and I can put them in)
This is a fairly common confusion. When you're in your app navigating around you're often using a link-to which is then telling ember to use the specified model when visiting the route. When you're refreshing the page, Ember has to divine the models using the url /apimapPieceSets/3/2. At that point it will go to each route MapPieceSetsRoute, MapPieceSetRoute, and MapPieceRoute and hit each model hook passing in any associated params. So what you need to tell Ember how to do, is how to load a mapPieceSet, and mapPiece properly. You'll need to setup a model hook for both of those.
App.MapPieceSetsRoute = Em.Route.extend({
// I don't know if you're using Ember Data, but I'm going to assume you are
model: function(params){
return this.store.find('mapPieceSet', params.mapPieceSet_id);
}
});
From what you said, it sounds like the model is already available client side from the mapPieceSets. In that case, you can use the modelFor method to get a parent's route's model and get your model.
App.MapPieceSetsRoute = Em.Route.extend({
// I don't know if you're using Ember Data, but I'm going to assume you are
model: function(params){
return this.modelFor('mapPieceSets').get('properyWithMapPieces').findBy('id', params.mapPieceSet_id);
}
});

From Ember Pre1 to Pre4: Multiple dynamic segments per route? Update: What is the allowed syntax for dynamic segments?

I am currently trying to migrate my Ember based on pre1 to the current release pre4. In my pre1-code, i defined a route as follows:
formCreated : Ember.Route.extend({
route : '/genre=:genre/sorting=:sorting/location=:location/range=:range/time=:date/:timeFrame',
....
})
This Route worked fine for me, but now i am struggling to mimic this behaviour with pre4. This is my approach:
App.Router.map(function() {
this.route("/");
this.route("formCreated", { path: "/genre=:genre/sorting=:sorting/location=:location/range=:range/time=:date/:timeFrame" });
});
App.FormCreatedRoute = Ember.Route.extend({
serialize: function(context, params){
// here i am returning a hash containing all the dynamic segments
}
});
What is going wrong?
When the App enters the state, the URL does not get updated properly. I am seeing this result:
/genre=:genre/sorting=:sorting/location=:location/range=:range/time=:date/6:00-19:00
So most of my dynamic segments do not get updated. I made sure that my custom serialize method is returning an appropriate hash object, where one property for each dynamic segment is set.
Are multiple dynamic segments per route still possible with pre4 or do i have to switch to some route nesting approach or something like that?
UPDATE: Root cause found:
I just discovered that the error happened because of the syntax i used for the route. I changed it to the following(replaced the "=" with "/"):
this.route("formCreated", { path: "/genre/:genre/sorting/:sorting/location/:location/range/:range/time/:date/:timeFrame" });
Is there any documentation on how the path may be structured? It seems that syntax has changed since ember-pre1. I would like to have a user friendly URL and those numerous Slashes make it difficult to read. Or is the rule, that a segment always has to start with ":/"?
You will need to use resource nesting, like described here and here
App.Router.map(function() {
this.route('/');
this.resource('genre', { path: '/genre/:genre_id' }, function(params) {
this.resource('sorting', { path: '/sorting/:sorting_id' }, function(params) {
...
});
});
});

add/delete items from Ember Data backed ArrayController

I'm using an ArrayController in my application that is fed from a Ember Data REST call via the application's Router:
postsController.connectOutlet('comment', App.Comment.find({post_id: post_id}));
For the Post UI, I have the ability to add/remove Comments. When I do this, I'd like to be able to update the contentArray of the postsController by deleting or adding the same element to give the user visual feedback, but Ember Data is no fun:
Uncaught Error: The result of a server query (on App.Comment) is immutable.
Per sly7_7's comment below, I just noticed that the result is indeed DS.RecordArray when there is no query (App.Comment.find()), but in the case where there is a query (App.Comment.find({post_id: post_id}), a DS.AdapterPopulatedRecordArray is returned.
Do I have to .observes('contentArray') and create a mutable copy? Or is there a better way of doing this?
Here is what I ended up implementing to solve this. As proposed in the question, the only solution I know about is to create a mutable copy of the content that I maintain through add and deletes:
contentChanged: function() {
var mutableComments = [];
this.get('content').forEach(function(comment) {
mutableComments.pushObject(comment);
});
this.set('currentComments', mutableComments);
}.observes('content', 'content.isLoaded'),
addComment: function(comment) {
var i;
var currentComments = this.get('currentComments');
for (i = 0; i < this.get('currentComments.length'); i++) {
if (currentComments[i].get('date') < comment.get('date')) {
this.get('currentComments').insertAt(i, comment);
return;
}
}
// fell through --> add it to the end.
this.get('currentComments').pushObject(comment);
},
removeComment: function(comment) {
this.get('currentComments').forEach(function(item, i, currentComments) {
if (item.get('id') == comment.get('id')) {
currentComments.removeAt(i, 1);
}
});
}
Then in the template, bind to the this computed property:
{{#each comment in currentComments}}
...
{{/each}}
I'm not satisfied with this solution - if there is a better way to do it, I'd love to hear about it.
A comment will be too long...
I don't know how do you try to add a record, but you can try to do this: App.Comment.createRecord({}). If all goes right, it will update automatically your controller content. (I think the result of App.Comment.find() works as a 'live' array, and when creating a record, it's automatically updated)
Here is how we do this in our app:
App.ProjectsRoute = Ember.Route.extend({
route: 'projects',
collection: Ember.Route.extend({
route: '/',
connectOutlets: function (router) {
router.get('applicationController').connectOutlet({
name: 'projects',
context: App.Project.find()
});
}
})
and then, the handler of creating a project (in the router):
createProject: function (router) {
App.Project.createRecord({
name: 'new project name'.loc()
});
router.get('store').commit();
},
Just for the record: as of today (using Ember Data 1.0.0-beta), the library takes this situation into account. When a record in an array gets deleted, the array will be updated.
If you try to delete an element on that array manually, for example by using .removeObject(object_you_just_deleted) on the model of the containing controller (which is an ArrayController, hence its model an array of records), you'll get an error like:
"The result of a server query (on XXXXX - the model you try to update manually) is immutable".
So there is no need anymore to code by hand the deletion of the record from the array to which it belonged. Which is great news because I felt like using ED and working it around all the time... :)
Foreword
I had a similar problem and found a little tricky solution. Running through the Ember-Data source code and API docs cleared for me the fact that AdapterPopulatedRecordArray returns from the queried find requests. Thats what manual says:
AdapterPopulatedRecordArray represents an ordered list of records whose order and membership is determined by the adapter. For example, a query sent to the adapter may trigger a search on the server, whose results would be loaded into an instance of the AdapterPopulatedRecordArray.
So the good reason for immutability is that this data is controlled by the server. But what if I dont need that? For example I have a Tasklist model with a number of Tasks and I find them in a TasklistController in a way like
this.get('store').find('task',{tasklist_id: this.get('model').get('id')})
And also I have a big-red-button "Add Task" which must create and save a new record but I dont want to make a new find request to server in order to redraw my template and show the new task. Good practice for me will be something like
var task = this.store.createRecord('task', {
id: Utils.generateGUID(),
name: 'Lorem ipsum'
});
this.get('tasks').pushObject(task);
In that case I got announced error. But hey, I want to drink-and-drive!
Solution
DS.AdapterPopulatedRecordArray.reopen({
replace: DS.RecordArray.replace
})
So that's it. A little "on my own" ember flexibility hack.