I am using init() in my component to load some data for a dropdown. On refresh it works but when I leave the tab to another tab then come back I get the following error:
index.js:143322 Uncaught TypeError: this.get(...).then is not a function
This code is in my init function and I suspect it has something to do with how ember.js renders but I am struggling to figure out how to make it work. I tried using the other lifestyle hooks but none of them worked.
This is the init function which is in a component:
init() {
this._super(...arguments)
this.get('popularTags').then(function(result) {
const newArray = []
for (var i = 0; i < result.length; i++) {
newArray.push({
name: result[i],
value: result[i],
group: 'Popular'
})
}
const popularTags = this.get('popularTags')
this.set('popularTags', newArray)
this.get('queryResults').then(function(result) {
const tagArray = []
for (var i = 0; i < result.length; i++) {
let popular = newArray.filter(tag => tag['value'] === result[i].value)
if (popular.length === 0) {
tagArray.push({
name: result[i].value,
value: result[i].value,
group: ''
})
}
}
const queryResults = this.get('queryResults')
return this.set('queryResults', tagArray)
}.bind(this))
}.bind(this))
},
There is something about your above example that I just don't understand. You seem to be getting and setting both the popularTags and queryResults properties. I'm not sure if that's just an issue in your example or something else - I'm going to assume it's an example issue and break this down a bit more generally:
Doing this much work in init isn't generally a good idea, so much so that it is slated for removal from the upcoming glimmer component API. In particular set inside any of the lifecycle hooks is a recipe for weird errors when the component gets removed from the DOM. While you can use a tool like ember-concurrency to help break this up and deal with set my suggestion would be to split this up into several computed properties. This might look something like:
import Component from '#ember/component';
import { computed } from '#ember/object';
export default Component.extend({
popularTags: computed('tags.[]', function(){
return this.tags.filter(tag => tag.isPopular);
}),
queryResults: computed('popularTags.[]', function(){
return this.popularTags.map(tag => {
return {
name: tag.name,
value: tag.description
};
});
}),
});
Computed Properties like these are the way to express data transformations in Ember. They rely on some initial data that is passed into the component and then modify it for use. In my above example I've assumed that tags gets passed in, but you can see that queryResults relies on the results of popularTags, in this way several different data transformations can be executed in order.
While loading asyncronous data in components can work just fine when you are first building and Ember.js application I would suggest that you confine all of your data loading to the Route's Model Hook as it is better suited to async work and will then give you data you can pass directly into the component without needing to worry about the difficulties in loading it there.
Your problem could be that youre calling the component with curlies and passing popularTags:
{{your-component popularTags=something}}
This is two-way bound. Precisely this means that changing popularTags inside the component will change something on the caller.
This means that if you remove this component and re-create it later (what your mention of some tabbing indicates) you've changes something on the outside. And your component expects popularTags (and so something) to be a promise (when calling this.get('popularTags').then). However because you changes it (with this.set('popularTags', newArray)) its no longer a promise but an array.
Generally I would recommend you to be careful when changing passed attributes.
Related
I am building an Ember tooltip module to create dynamic content on hover.
<div class="custom-tool-wrapper">
{{#custom-tool-tipster
side="right"
content=(or getContent question.id)
contentAsHTML=true
class="tool-tipster-field"}}
Preview
{{/custom-tool-tipster}}
</div>
in the ember controller - the function doesn't return the variable "question.id" --- it comes back as 0 always - when it should be a string "q-1"
export default Ember.Component.extend({
getContent(tips){
console.log("tips1")
console.log("question", tips);
},
});
I think what you're actually trying to achieve is best done via computed property on the question model object (your question is still really vague).
content: computed('id', function(){
//this.tips is a part of the model object
//compute and return whatever the content is
return "content";
}
and then just say:
{{#custom-tool-tipster
side="right"
content=model.content
contentAsHTML=true
class="tool-tipster-field"}}
Preview
{{/custom-tool-tipster}}
If you needed to actually invoke a function (which it's rare to think of an instance where the computed property isn't a better solution whenever state is involved), you would use a custom handlebars helper.
(or a b) is (a || b) and isn't function invocation like you're attempting if you're using the ember truth helpers lib for the or helper. It looks like you're trying to accomplish what ember-invoke allows
import Ember from 'ember';
import { helper } from '#ember/component/helper';
export function invokeFunction([context, method, ...rest]) {
if (typeof context[method] !== 'function') {
throw new Error(`Method '${method}' is not defined or cannot be invoked.`);
}
return Ember.get(context,method).apply(context, rest);
}
export default helper(invokeFunction);
which can be used like content=(invoke this "getContent" question.id) to invoke and return the value of a function on the passed in context object (the controller if this in the case of a route's template). Let me be clear, I think this invoke approach is a terrible idea and really gets rid of your separation of concerns and I'm not advocating that you do it. Templates shouldn't contain your logic and definitely shouldn't be calling arbitrary functions on the controller when you have such a nice facility like computed properties.
I am testing my application, so I am doing the following:
I show an index view (#/locators/index), of Locator objects, which I initially load with App.Locator.find();
I modify the backend manually
Manually (with a button/action) I trigger a refresh of the data in the ember frontend, without changing the route. I do this with App.Locator.find().then(function(recordArray) {recordArray.update();});. I see via console logging that a list request is sent to the backend, and that the up-to-date data is received. I assume this is used to update the store.
BUT: The view does not update itself to show this new data
Why does the view not get automatically updated when the store receives new data? Isn't that the whole point of the data binding in Ember?
If I now do the following:
Open any other route
Go back to the locators index route (#/locators/index)
Ember sends a new request to list the locators
The index view is shown, with the correct data (since it was already in the store?)
New data is received
(I am not 100% sure that 4 and 5 happen in that order, but I am quite certain)
So, my impression is that the data is properly updated in the store, but that somehow a full re-rendering of the view is needed to display this new data, for example by leaving and re-entering the route. Is this true? Can I force this re-rendering programmatically?
Ember changes view data when the underlying model is changed by the controller(Which is binded to the view)
(Only when the state of the application changes(url changes) router hooks are called)
Your problem could be solved when you do this.refesh() inside your route by capturing the action triggered by your view.
App.IndexRoute = Ember.Route.extend({
actions: {
dataChanged: function() {
this.refresh();
}
},
//rest of your code goes here
});
for this to work your handlebar template which modifies the data shoud have an action called dataChanged
example :
Assume this action is responsible for changing/modifying/deleting the underlying data
<button {{action 'dataChanged'}}> Change Data </button>
Refresh method actually does a model refresh and passes it to the corresponding controller which indeed changes the view.
There a couple of things that come to mind you could try:
If you are inside of an ArrayController force the content to be replaced with the new data:
this.replaceContent(0, recordArray.get('length'), recordArray);
Or try to call reload on every single record trough looping the recordArray:
App.Locator.find().then(function(recordArray) {
recordArray.forEach(function(index, record) {
record.reload();
}
}
And if the second approach works, you could also override the didLoad hook in your model class without having to loop over them one by one:
App.Locator = DS.Model.extend({
...
didLoad: function(){
this.reload();
}
});
If this works and you need this behaviour in more model classes consider creating a general mixin to use in more model classes:
App.AutoReloadMixin = Ember.Mixin.create({
didLoad: function() {
this._super();
this.reload();
}
});
App.Locator = DS.Model.extend(App.AutoReloadMixin, {
...
});
App.Phone = DS.Model.extend(App.AutoReloadMixin, {
...
});
Update in response to your answer
Handlebars.registerHelper is not binding aware, I'm sure this was causing your binding not to fire. You should have used Handlebars.registerBoundHelper or simply Handlebars.helper which is equivalent:
Handlebars.helper('grayOutIfUndef', function(property, txt_if_not_def) {
...
});
Hope this helps.
Somehow this seems to be due to the fact that I am using custom handlebar helpers, like the following:
Handlebars.registerHelper('grayOutIfUndef', function(property, txt_if_not_def) {
// HANDLEBARS passes a context object in txt_if_not_def if we do not give a default value
if (typeof txt_if_not_def !== 'string') { txt_if_not_def = DEFAULT_UNDEFINED_STR; }
// If property is not defined, we return the grayed out txt_if_not_def
var value = Ember.Handlebars.get(this, property);
if (!value) { value = App.grayOut(txt_if_not_def); }
return new Handlebars.SafeString(value);
});
Which I have been using like this:
{{grayOutIfUndef formattedStartnode}
Now I have moved to a view:
{{view App.NodeIconView nodeIdBinding="outputs.startnode"}}
Which is implemented like this:
App.NodeIconView = Ember.View.extend({
render: function(buffer) {
var nodeId = this.get('nodeId'), node, html;
if (nodeId) {
node = App.getNode(nodeId);
}
if (node) {
html = App.formattedLabel.call(node, true);
} else {
html = App.grayOut(UNDEFINED_NODE_NAME);
}
return buffer.push(html);
}
});
I am not sure why, but it seems the use of the custom handlebars helper breaks the property binding mechanism (maybe my implementation was wrong)
I'm having trouble with a computed property.
It's a complex manipulation on an ArrayController. The problem is, Ember attempts to calculate it before the data has loaded. For example, part of it is
var counts = this.getEach('hours').forEach(function(hours) {
var d = hours.find(function(_hour) {
return +(_hour.date.substring(11, 13)) === 10;
});
return d.count;
});
I get an error because this.getEach('hours') returns something like
[ Array[24], undefined ]
while the AJAX request is loading, so the code breaks.
I'm sure others have run into this before - what's the solution?
Update: Here's how I get the data. When a user clicks a month in a view, I pass the clicked month's id to my MonthsController. It has a toggleMonth method:
App.MonthsController = Ember.ArrayController.extend({
toggleMonth: function(id) {
var month = App.Month.find(id),
index = this.indexOf(month);
if (index === -1) {
this.pushObject(month);
} else {
this.removeAt(index);
}
}
});
App.Month.find(id) sends the correct AjAX request + the data returns, but perhaps this is not the correct way to populate the months controller.
Also, this is happening within the IndexRoute (i.e. I have no separate route for the MonthsController. So, I never specify a model hook or setupController for the MonthsController.
The general approach to this problem is promises: asynchronous requests immediately return a promise, which is basically a promise of value, which can be resolved later down the line. All Ember models are promises behind the scenes. See ember models as promises, and How are Ember's Promises related to Promises in general, and specifically jQuery's Promises?
Could you explain the context of the first block of code? What is this in this.getEach('hours').forEach and when is that block executed?
I am trying to use observers to observe a change on my model after XHR. This is because the earlier approach of extending a fn and calling super is not allowed any more.
Running into this weird issue where my observer doesn't fire:
App = Ember.Application.create({
ready: function () {
console.log('Ember Application ready');
this.topCampaignsController = Ember.ArrayController.create({
content: null
});
App.TopCampaignsModel.create({
// Calling super is no longer allowed in object instances
//success: function () {
// this._super();
// App.topCampaignsController.set('content', this.get('data'));
//},
onDataChange: function () {
console.log('data property on the object changed');
App.topCampaignsController.set('content', this.get('data'));
}.observes('data')
});
}
});
App.TopCampaignsModel = Ember.Object.extend({
data: null,
// this will be actually called from an XHR request
success: function () {
this.set('data', [5,10]);
},
init: function () {
console.log('TopCampaignsModel created');
this.success();
console.log(this.get('data'));
}
});
Jsfiddle here: http://jsfiddle.net/gdXfN/26/
Not sure why the console doesn't log "data property on the object changed". Open to alternative approaches on how I can override the 'success' fn in my instance.
After this commit in December last year, it is no longer possible to set observers during object creation. This resulted in a huge performance win.
To set observers on create you need to use:
var Object = Object.createWithMixins({
changed: function() {
}.observes('data')
});
Here's a fiddle demonstrating this.
The API documentation should be updated accordingly, something I will do later on.
However, I don't advise you to do that, but instead set observers during object definition. The same result can be achieved: http://jsfiddle.net/teddyzeenny/gdXfN/32/
That said, there are two things you are doing that go against Ember concepts:
You should not create controller instances yourself, you should let Ember create them for you:
App.TopCampaignsController = Em.Controller.extend({ content: null });
When the App is initialized, Ember will generate the controller for you.
Models should not be aware of controller existence. Controllers should access models not the other way round.
Models and Controllers will interact together through routes.
For the last two points, you can watch the tutorial at http://emberjs.com/guides/ to see how the Application, Controllers, Models, and Routes should interact. Since you're not using
Ember Data, just ignore DS.Model and imagine an Ember.Object instead. The tutorial can give you a pretty good overview of how objects should interact.
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.