I am still very new to the world of Ember, and I'm still trying to understand EmberJS and Ember Data (latest version). In my previous (non-Ember) Node app, I included a library that handled all my REST calls to where my data was stored. It set up the connection to the server and handled all the error handling and parsing into a nice and tidy JSON object, and even handled multiple calls to the server in case the response was too big for one call. I can fetch individual records, but if I wanted to fetch a bunch of records, all I had to do was initialize the library object ('myObj') and call myObj.fetchAll(config) to initiate the fetch. Then I just have to wait on several events.
Example
myObj.on('record', function() { // Each record is an event }
myObj.on('error', function () { ...}
myObj.on('end', function () { // After the last record is retrieved }}
I would very much still like to use this library in Ember, but I have no idea how to go about setting it up. I haven't been able to find any examples of creating my own Adapter (is that the right terminology) that would allow me to do this.
Is this something I can do with Ember, or is it not recommended?
I would strongly suggest you use ember-data before attempting something non-standard as you're learning. Virtually all the documentation, and help will specifically be about ember-data. This is a good starting point: http://guides.emberjs.com/v1.13.0/models/
It's perfectly possible to use your own models and use a custom rest interface. You initiate your myObj.fetchAll(config) call on the router. If it's waiting for an event, return a promise and resolve it when the event returns. I don't know anything about your library but it would look something like:
export default Ember.Route.extend({
model() {
return Ember.RSVP.Promise(function(resolve){
var records = [];
myObj.on("record", (record) => {
records.pushObject(record);
});
myObj.on("end", () => {
resolve(records);
});
myObj.fetchAll(ENV.config);
});
}
});
In imperfect contrast, this is how you glue things together from your adapter to your template normally in ember:
Configuring a REST endpoint:
export default DS.RESTAdapter.extend({
host: 'https://api.example.com'
});
Defining a model:
export default Model.extend({
name: attr('string')
});
Fetching data in your route:
export default Ember.Route.extend({
model() {
return this.store.findAll('person');
}
});
Rendering the data:
{{#each model as |person|}}
{{person.name}}
{{/each}}
It's all pretty straight forward if you stick to the default way of doing things.
Related
as the title says, I have some problems understanding how to use an adapter in an ember-engines.
I am currently running my application with ember#2.15, ember-data#2.15 and ember-engines#0.5.14.
I already used an adapter in my main application, but if I tried to reproduce a basic adapter in my application, the page is loading indefinitely.
In my route, I call my adapter with the findAll method:
model()
{
"use strict";
console.log('In my route');
return this.get('store').findAll('my-adapter-name', {reload: true});
}
And in my adapter, I respect the syntax that I used in my in-app adapter:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Adapter.extend({
findAll: function (store, type, sinceToken, snapshotRecordArray)
{
console.log("In my adapter");
return new Ember.RSVP.Promise(function (resolve, reject)
{
// I accede to my websocket service here, nothing to do with my question
});
},
/* *
* My other function, like findRecord, ect...
*/
});
As you can see, I put some console.log in my code, and I can access the message in my route, which is in my engine, but I can't access the message in my adapter, which is also in my engine.
I tried to put a console.log in an adapter in my application, and the message is properly shown, so I am sure this is because I can't access to the adapter in my engine, so if anybody has an idea to how we should configure our adapter in our ember-engines, this would be very appreciated.
For your information, this is an in-repo engines.
Just found it, this is a bit tricky, but your models (and adapters) should be in the myApp/lib/myEngine/app/models/, and not in myApp/lib/myEngine/addon/models.
I don't know if this is intended this way, but this is the only way I found to add model in your in-repo ember-engines.
EDIT
This will do the trick for the serializers and the transform.
The documentation for emberjs clearly states that you should not use controllers, however sometimes you need to pass data into a component that is not the model for the corresponding route. For instance in an application I am working on I want to retrieve a list of records from the store and display them in a component so the user can select them as an attribute of the model for that route.
The advice I have received on this is to either create a controller and use it to retrieve the list in question or to add the list of records as an attribute of the model for that route, but since the former is inadvisable and the latter only makes sense if the item in question is a logical part of the model's schema (and therefore should probably be in there anyway) I am left feeling confused about how this apparently simple thing ought to be done. Can anyone help?
You can use Ember.RSVP.hash in your routes model hook. When the promise resolves, the results get passed as the second param in setupController.
// This would be in a route file like app/blogs/edit/route.js
model: function() {
return Ember.RSVP.hash({
blog: this.store.findRecord('blog', 1),
categories: this.store.findAll('category'),
});
},
setupController: function(controller, models) {
this._super(controller, models);
controller.set('model', models.blog);
controller.set('categories', models.categories);
},
OR
If you wanted all the data logic to exist in the component you can inject the data store service. This goes against the DDAU mantra (data down, actions up) but IMO it's a clean, modular solution. Useful if the extra content isn't visible immediately ie: components that open modal windows.
// This would live within the actual component
store: Ember.inject.service(),
loadCategories: function() {
this.get('store').findAll('category').then((categories) => {
this.set('categories', categories);
});
}.on('init'),
However, I would advise against this if the data (categories in this example) were immediately visible in the layout. Ember won't wait for these requests to complete before rendering so you would see blank spaces/whatever with the actual values loading in a half second later.
just be aware that components don't know anything about outside them self. The way I would solve the problem is by creating a bridge between controller and component by passing the property that you want to access to your component.
<script type="text/x-handlebars" data-template-name="sample-com">
{{sample-com
sampleRequests=sampleRequests
}}
</script>
App.MainController = Ember.Controller.extend({
//bridged properties that the controller must communicate between components/view
sampleRequests: 'hello world'
});
App.SampleComComponent = Ember.Component.extend({
sampleRequests: null
});
if there is a better way please feel free to suggest.
Hey I need to modify some records which I get from the DataStore. If I add the following code in my router I can see that the requests get passed to my template, but I can't modify each request of the collection because the collection is empty.
model() {
return this.store.findAll('user').then(function(users) {
console.log(users.get('length')); // 0
return users;
});
}
I thought that the promise gets resolved when all the records have been fetched from the server but this doesn't seem to be the case. Or did I completely miss something.
I also tried to modify the model in the afterModel callback with the same result.
I'm using Ember 1.13.0 (with Ember-CLI), Ember-Data 1.13.4 and ember-cli-mirage for Mocking my HTTP Requests.
UPDATE:
I managed to create a workaround for this issue. In my controller, I created a new property which listens for model.#each and then I was able to modify model and pass it to the view.
export default Ember.Controller.extend({
users: function() {
return this.get('model.users').filter(function(user) {
// The Promise is resolved twice
// The first time with an empty model and the second time with
// the actual data. So I filter the empty model.
return user.get('id');
}).map(function(user) {
// do fancy stuff with our user
return user
});
}.property('model.#each')
});
Ember Data 1.13
So after spending some time on this topic i found the solution to this issue. It's basically the way how ember works. So under the hood findAll is returning two promises.
findAll without data in the store
find records from the store (resolve first promise -> length 0,
because no data is in the store)
fetch new data in the background (resolves second promise)
findAll with data in the store
find records from the store (resolve first promise with cached data)
fetch new data in the background (resolves second promise with new
data)
If you want to wait for all the data to be loaded you can use query which is returning only one promise.
model() {
return this.store.query('user', {});
}
For findRecord I found the following workaround, which is only working if your backend supports any kind of filtering on the id of your record.
model() {
return this.store.query('user', {
'filter[id]': 1
}).then((users) => {
return users.objectAt(0);
});
}
You can have a look on the following discussion on github
Ember Data 2.0
On Ember Data 2.0 this issue is resolved.
First you should make sure the data is coming in from Mirage as you expect. Open your Ember inspector and verify the models made it into your store. If not, you likely have a problem with the format of the JSON response from your mock route.
To diagnose, check out your console for a log of the JSON response, and ensure it matches what you expect. If you have a custom route handler in your /mirage/config.js for this route, you could also put a debugger statement in there and verify the data is what you think it should be.
If you're using default Ember Data 1.13, it probably means you're using the JSON API serializer/adapter. Is this what you intend? What is the backend for this app ultimately going to look like? If it's going to be JSON API, you'll need to do a bit more work in the Mirage config.js file for now, something like
this.get('/contacts', function(db, request) {
return {
data: db.contacts.map(attrs => {
type: 'contacts',
id: attrs.id,
attributes: attrs
})
};
});
I had a similar problem to the one you are describing when using Ember and Ember data version 1.13. At the time, I was reading the updated documentation of Ember 2.0 without Ember Data 2.0. Once I upgraded both libraries I was able to get the behavior you are trying to achieve with the first code snippet. Namely, the promise is handled correctly with nonzero records with ember data 2.0.
I am brand new to Ember, and am having trouble with getting Ember/Ember Data to cache the results of an API call to the Rails backend.
I found this topic: Ember-Data .find() vs .all() - how to control cache?
Which answered a few questions about what find() vs all() does, using that I found a workaround which looks ugly and I am wondering if there is a better, more concise way than this:
import Ember from 'ember';
export default Ember.Route.extend({
model: function () {
if (this.store.all('facility').get('content.length')) {
return this.store.all('facility');
} else {
return this.store.find('facility');
}
}
});
A couple of notes:
You shouldn't cache data inside model hook. Pass cached data with #link-to or similar API.
You shouldn't cache data at all if you don't have a notification system to tell Ember a resource has been changed. Keep in mind that your app may run for a very long time.
You can also do both at same time.
Here is an example to use cached data and update them in background. (Not tested)
import Ember from 'ember';
export default Ember.Route.extend({
model: function () {
var currentData = this.store.all('facility');
this.store.find('facility'); // Ember automatically picks new changes
return currentData;
}
});
Day 2 learning ember.js...
I'm working on a offline app that needs to save draggable/sortable tile positions to localstorage, and if there is no existing data, load & save from a fixture.
Using: ember 1.0.0-pre4, ember-data rev11, ember-localstorage-adapter, jQ 1.9, jQ UI 1.9
https://github.com/rpflorence/ember-localstorage-adapter
It's working, but I'm a bit of a novice, feel it's not pretty and could use some community advice.
http://jsfiddle.net/Nsbcu/4/
Questions
What is the proper way to check if your DS.Store has loaded and is empty? My method of looking directly at localstorage didn't feel right.
After I createRecords from the App.Tile.DEFAULTS I feel I should commit them, but an error is thrown. I don't have to commit the known defaults, but curious what causes the error and how I should go about committing properly. Also is the App.ready() callback the right place for loading defaults? Error only happens when localstorage is empty
Uncaught Error: Attempted to handle event loadedData on <App.Tile:ember231:1> while in state rootState.loaded.created.inFlight. Called with undefined
On the TilesController I'm using sortProperties which works great until jQ UI Sortable changes the DOM and Ember wants to update my tile order, before I get a chance to set the new order. My current solution is to turn off sortProperties temporarily while updating the model. Again this feels hacky, suggestions on proper way to do this?
=== Edit Feb 3 ===
If I do an async commit the initial error in question #2 is avoided.
App.TilesRoute = Ember.Route.extend({
model: function() {
return App.Tile.find();
},
setupController: function(controller) {
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
// Commit async, else generates error
var _this = this;
setTimeout(function() {
_this.store.commit();
}, 1);
}
}
});
I would put any initial code inside the application or the index Route within the setupController method
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
//*** WARNING: Generates Error ***/
App.Tile.find().get('store').commit();
}
Once you move the code inside the route, replace App.Tile.find().get('store').commit(); by App.store.commit() inside your route
Create your own transaction instead of using the default one, each time you make a call to the store directly you're using the default transaction. You can create a transaction this way
var transaction = App.store.transaction()
transaction.createRecord(App.Foo);
transaction.commit()
transaction.rollback();
Any call to App.store assumes you already created a store, right now you're only extending the DS.Store. Try instead
App.Store = DS.Store.create({
revision: 11,
adapter: 'App.LSAdapter'
});
I would suggest that you do any event handling or transaction management in the router unless it's purely for styling or animation. In that case, the view is the right place for it. I like the router to orchestrate communication between all the assets (controllers, routes, models, views)
A good pattern to remember is a view talks only to a controller, a controller is a mere proxy to a model, a router orchestrates communication between controllers and manages routes.