Similar to this question, I'm looking for a way to load templates via Javascript promises. The issue I'm running into is returning the compiled template to the Backbone View. Note: this.getFile() returns a new Promise, which fetches the template as expected.
template: function(attributes) {
this.getFile('/path/to/template').then(function(tpl) {
var compiled = Handlebars.compile(tpl);
return compiled(attributes);
}, function(error) {
console.error('Failed!', error);
});
}
Within the View's initialize, I have a listener set to
initialize: function() {
this.model.on('change', this.render, this);
}
which triggers as expected when data is fetched from the server.
The problem is when the render function executes the line
render: function() {
...
this.$el.html(this.template(attributes));
...
}
nothing renders to the html as I would expect. I'm sure I'm missing something simple within the template promise callback, but no matter what I change, the html does not render and no errors appear.
Like #muistooshort have pointed out. The template function does not return anything, it simply does an async call to another function and deal with it.
However, your render function is syncronous, it execute template function, then add whatever is returned directly to the $el. Which in this case, there is nothing to add.
There are two solutions to this problem.
1. Make the template function to return a promise. And in render, wait for the template to finish before adding stuff to $el.
Example:
template: function(attributes) {
return this.getFile('/path/to/template').then(function(tpl) {
var compiled = Handlebars.compile(tpl);
return compiled(attributes);
});
}
Note this return this.getFile..., will return the promise, then will resolve with the value compiled(attributes).
Then your render function could be:
render: function() {
var self=this;
this.template(attributes).then(function(data){
self.$el.html(data);// data is the stuff that was compiled.
})
}
However, may I suggest a different solution as in how you manage your templates?
As a personal opinion, the templates, either ejs, jade, or hbs, are actually very small when compiled. So these data could be compiled in your js files using require. What I do is I use a JStemplate file
module.exports={
t1:require(path/to/template1),
t2:require(path/to/template2),
}
Then, in other backbone view js files. I could do
var allTemplates=require('./JStemplate.js');
...
render:function(){
this.$el.html(allTemplates.t1(attributes));
}
This in my opinion is easier to handle, and faster in client side, because you don't have to get the file in client.
Related
I have a computed property where I'm attempting to create a record if one does not exist, but keep getting a jQuery.Deferred exception when attempting to render the computed property.
Here's what I have so far:
deadlineDay: computed(function() {
const oneWeekFromNow = moment().startOf('day').add(1, 'week');
const store = this.get('store');
return store.queryRecord('deadline-day', {
filter: {
date: oneWeekFromNow
}
}).then(result => {
if (!result) {
result = store.createRecord('deadline-day', {
date: oneWeekFromNow
});
result.save();
}
return result;
});
}),
Then in my templates I'm attempting to render with a simple helper:
{{display-date deadlineDay.date}}
The {{display-date}} helper just calls return date.format('dddd, MMM Do')
It looks like Ember is attempting to render the promise itself instead of waiting for it to resolve.
This results in an error since .format is not a method of the promise.
I imagine this is an extremely common use-case, but that I have a lapse in understanding. Much help appreciated!
I'm not sure if it is relevant, but my backing store is sessionStorage via ember-local-storage
I agree that Ember may be attempting to render the promise itself instead of waiting for the promise to resolve. Unfortunately, I am not able to reproduce the error at this time.
In Ember.js it is typically recommended to place data calls in the route file. This will allow your query/save and all other data gathering to occur prior to the template file being loaded. Your depicted computed property shows no dependent keys, so this may justify moving the calls to the route file for your scenario.
generic route.js example:
import Ember from 'ember';
export default Ember.Route.extend({
async model() {
let record;
record = await this.store.queryRecord('record', { queryParams });
if (!record) {
record = this.store.createRecord('record', { properties });
}
return record.save();
},
});
However, if a promise within a computed property is of use, the author of Ember Igniter may have some additional helpful guidance that may be worthwhile.
The Guide to Promises in Computed Properties
I've noticed Marionette is very un-opinionated as far things go for the freedom they give you to choose a method to render data. It seems like there are a lot of ways to initially render a template with custom data
Returning a template with data:
template: function () {
var myTemplate = $('.someTemplate')
return _.template(myTemplate.html())({some: data});
}
Very similarly:
render: function () {
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, {
model: this.model.toJSON(),
customData: this.customData
});
this.$el.html(html);
}
Serialize data:
serializeData : function () {
var customData = {some: 'data'};
var model = this.model.toJSON()
return _.extend(customData, model);
}
I've seen a lot of people in different code use variations of the first and the second. I personally prefer using serializeData but am curious: is there an advantage or use case where it would be appropriate to use the first two methods instead of serializeData?
The first case is not efficient - you are recompiling the template every time you want to render.
Anyway, your use case is exactly why Marionette has templateHelpers. Its the most concise way to provide extra data to the template while also passing serialized model.
So you would write:
templateHelpers : function () {
return {some: 'data'};
}
Or if its just static stuff:
templateHelpers: {some: 'data'}
More examples on how to use it here.
I think it's all about exploring natural behaviour of these things. Backbone View render is empty function by default. Marionette ItemView render extends Backbone's with this code.
It takes template by getTemplate method, by default it gives what is stored in template option. You can override getTemplate if you want to choose between several templates.
Then it collects data needed to be rendered by runing serializeData and extending it with templateHelpers. First one by default returns your model or collection toJSON method result, there you can prepare your data some way on every render. Second one is for helpers that will be calculated (if they are functions) if needed in template.
Template and data then go to Marionette.Renderer where just return template(data) by default happens. And then result can be attached to view's element.
I have a trouble with asynchronously loaded models in Ember. I thought I have already understood the whole "background Ember magic", but I haven't.
I have two models, let's say foo and boo with these properties:
foo: category: DS.belongsTo("boo", { async: true })
boo color: DS.attr("string")
In my route, I load all foos:
model: function(params) {
return this.store.findAll("task", "");
},
than in my template I render a component: {{my-component model=model}}. In the component's code I need to transform the model into another form, so I have:
final_data: function() {
this.get("model").forEach(function(node) {
console.log(node.get("category"));
});
return {};
}.property("model"),
When I try to access the "category" in the model, my code crashes:
EmberError#http://localhost:4200/assets/vendor.js:25705:15
ember$data$lib$adapters$errors$$AdapterError#http://localhost:4200/assets/vendor.js:69218:7
ember$data$lib$adapters$rest$adapter$$RestAdapter<.handleResponse#http://localhost:4200/assets/vendor.js:70383:16
ember$data$lib$adapters$rest$adapter$$RestAdapter<.ajax/</hash.error#http://localhost:4200/assets/vendor.js:70473:25
jQuery.Callbacks/fire#http://localhost:4200/assets/vendor.js:3350:10
jQuery.Callbacks/self.fireWith#http://localhost:4200/assets/vendor.js:3462:7
done#http://localhost:4200/assets/vendor.js:9518:1
.send/callback#http://localhost:4200/assets/vendor.js:9920:8
It seems to me, like the Ember didn't load the boos. How should I access them right to make Ember load them?
It's trying to load category, but the adapter is encountering some error. Can't tell what from your example.
Check your network tab.
When you access an async association from a template, Ember knows what to do. From code, such as your component's logic, Ember has no idea it needs to retrieve the association until you try to get it. The get will trigger the load, but will return a promise. You can do this:
get_final_data: function() {
Ember.RSVP.Promise.all(this.get("model") . map(node => node.get('category'))
.then(vals => this.set('final_data', vals));
}
As I'm sure you can see, this creates an array of promises for each node's category, calls Promise.all to wait for them all to complete, then stores the result into the final_data property.
Note, this is not a computed property; it's a function/method which must be called at some point, perhaps in afterModel.
I am using iron:router in my app and I have a controller that subscribes to one document in a collection by using a parameter in the path. I can access all of the documents in my collection on the server, so I know that there is stuff in there, but when I try to access the data that I subscribe to in the waitOn method of the controller, the data is undefined. Here is the relevant code for this problem.
Router code:
this.route('unit', { path: 'unit/:unitId', template: 'unit', controller: 'UnitController' });
UnitController = BaseController.extend({
waitOn: function () {
return Meteor.subscribe('getUnit', this.params.unitId);
},
data: function () {
var id = this.params.unitId;
templateData = {
unit: Collections.units.model(Collections.units.getUnit(id))
};
return templateData;
}
});
Publication:
Meteor.publish('getUnit', function(id) {
return Collections.units.data.find({ unitId: id });
});
Here I have created an object for various things to do with my collection(I only included the important parts here):
Collections.units = {
data: new Mongo.Collection("units"),
getUnit: function (id) {
return this.data.findOne({ unitId: id });
},
model: function(unitEntity) {
return {
unitId: unitEntity.unitId,
createdAt: unitId.createdAt,
packets: unitEntity.packets,
getLastPacket: function (id) {
return _.last(this.packets);
}
};
}
};
I have been trying to debug this for quite a while and I can do all the things to the collection I want to on the server and in the publish method, but when it gets to the controller, I can't access any of the info. In the data method this.params.unitId returns exactly what I want so that isn't the issue. Where the exception gets thrown is when I try to read properties of unitEntity when I'm making the model but that is just because it is undefined.
Have any ideas what I am doing wrong? Thanks in advance for any responses.
The main problem that I was trying to solve's solution was to wrap the code inside the data method inside of if (this.ready()){ ... } and add an action hook with if (this.data()) { this.render(); }. After I got the subscription to work, I found that Christian was right in the comments with saying that my controller setup might mess things up. It was causing other strange exceptions which I fixed by just moving the hooks to each route instead of using the controller. As for my Collections setup, it may be unconventional, but all of that is working fine (as of right now). I may want to set them up the standard way at a later point, but as of right now its pretty handy for me to do things with the collections with the methods already written in the object.
Using AngularJS 1.0.4
One of our Angular apps is dependent on a resource being loaded before anything else can be loaded. We do this from a service that gets initialized in app.run() and then broadcast an event that everything else listens for to start loading.
In the controllers we also need to have access to the resulting resource. So I then have the following in each one:
$scope.parent = null;
if(!svc.parent) {
$scope.on('parentLoaded', function() {
$scope.parent = svc.parent;
});
} else {
$scope.parent = svc.parent;
}
Each of the controllers is tied to a view and can be called in any order. So it's not guaranteed that the resource is loaded when the controller gets called, although it can be if another controller was called before hand. The load event only gets trigger the first time the service is initialized when the app first loads.
Is there a better way to this?
It seems kind of redundant & not clean.
I would just use a promise. You would have something like:
var deferred = $q.defer();
$http.get('/application').then(function(res) {
deferred.resolve(res);
});
function fetch() {
return deffered.promise;
}
To load your initial resource, we'll call the resource "application" for example. Then, to load your next portion, you can do:
application.fetch().then(function(svc) {
//res is whatever is returned from our $http.get, earlier
$scope.parent = svc.parent
//do whatever required your resource here
});
Instead of doing this:
$scope.parent = null;
Create an empty array instead (or object depending on what resource is).
$scope.parent = [];
This way angular object watchers will react to changes of the array. For more advanced issues can use promises when loading data