Difficulty accessing actual objects in "didInsertElement" code in Ember - javascript

I'm having a repeated problem with accessing elements, specifically elements obtained via reference from another element, in Ember. I have javascript running in a "didInsertElement" hook in the view, which is what most places on the web seem to indicate is the correct place to be running that logic.
However, the ember object I'm trying to access isn't real - Ember (presumably EmberData) is returning a blank placeholder object that doesn't have any of the required values. Every attribute is undefined.
I've tried waiting for the promise to resolve... but it's not a promise, it's just a fake object with undefined attributes. I've tried using Ember.run.next, to insure things finished loading first. Neither of these two solutions, or any of the others I've tried, have worked.
Here's an example of what I mean:
template:
{{#each entry in list.entries}}
{{display-note note=entry}}
{{/each}}
component:
didInsertElement: function(){
alert(this.get("note").get("title"));
}
This inconsistently returns "undefined", even more inconsistently in situations where I'm passing a list of objects (something like {{display-notes notes=list.entries}} ).
Doing things like {{list.entry.title}} in the template works fine, though - the data is loading, it's just that it's being replaced by a "fake object" until that happens, and I don't know how to tell the ember app to wait until it's real to run my code and try and access it.
This is especially true for self references. Sometimes, rarely, the above code seems to work - but if I have a tree of, say, comments, which hasMany comments as children, attempting to access children fails even in the template. This might be due to another problem, though, or a combination of the two issues.

I'm guessing that note is undefined here:
didInsertElement: function(){
alert(note.get("title"));
}
With didInsertElement, start from the current view (i.e. this) and get your data from there. Assuming you are in the NoteView it would look like this:
App.NoteView = Ember.View.extend({
title : 'hello world'
didInsertElement: function (){
alert(this.get('title'));
}
})

Related

then() returning a null value when it should be pending

Ember 2.17
I am calling an helper from my template :
{{#each invoice.invoiceLines as |line| }}
{{pricings/full-pricing line.pricing}}
{{/each}}
invoice,invoiceLine, as well as pricing are ember models.
Here is how invoice is created in model () :
model(params) {
let invoice= this.store.findRecord('invoice',params.invoice_id)
return Ember.RSVP.hash({
invoice: invoice,
allShares: invoice.then((i)=>{return i.allShares()}),
detailShares: invoice.then((i)=>{return i.detailShares()})
});
}
The goal of the helper is to take pricing, extract numbers (everything is in the model, no more relations) and return a string formatting the initial price and the subscription price.
The helper is as following :
import { helper } from '#ember/component/helper';
export function pricingsFullPricing([pricing]) {
return pricing.then(
p=>{
debugger
},p=>{
}
)
}
export default helper(pricingsFullPricing);
When I run the page, debugger is called twice (the template loop run once).
First time p is null, the second time it is a pricing.
Isn't then supposed to prevent that? Why does it behave like that?
Your route is wrong, routes are promise aware (that's what hash is for), it should be:
model(params) {
return Ember.RSVP.hash({
invoice: this.store.findRecord('invoice',params.invoice_id)
//allShares: invoice.then((i)=>{return i.allShares()}),
//detailShares: invoice.then((i)=>{return i.detailShares()})
});
}
Then your handlebars is just:
{{#each model.invoice.invoiceLines as |line| }}
{{line}}
{{/each}}
You also shouldn't call methods like you are on a model. It's not really clear what allShares(), etc does but these should (probably) be computed in the controller. Something along the lines of:
import { computed } from '#ember/object';
export default Controller.extend({
allShares:computed('model.invoice', function(){
return this.get('model.invoice').allShares();
});
});
Though this doesn't seem ideal. Like I said, it's hard to be explicit as it's not clear what your trying to do here. It'd probably make more sense if your extracted these methods into a service.
You then don't need the helper at all. This appears to be just trying to work around promises.
It makes life a lot easier if you try and load all server side data in the route before load.
First rule of helpers
Each time the input to a helper changes, the compute function will be called again.
Second, there's nothing about helpers that will make this block subsequent calls because you are returning a promise.
export function pricingsFullPricing([pricing]) {
return pricing.then(
p=>{
debugger
},p=>{
}
)
}
You've created a simple helper here that will use the promise itself as the value. Look at ember-promise-helpers/await to see how a class based helper is used to manually set the value that's displayed in the template.
Now, if you're wondering why the recomputation is happening, I'm going to have to speculate based off the knowledge I have of Ember data just from being part of the Ember community (I've never actually used Ember Data). You know line.pricing is a promise? I can then assume your using some sort of relationship, which will most likely have to be loaded via an ajax call (hence the promise). But these relationships in Ember data, iirc, use this PromiseProxyMixin that allow them to behave simultaneously like a promise or like an object (depending on whether the data is in the store already or not). This is what allows you to reference the promise in your template without then
See this article for a better understanding of what I mean

Why do I need to call a dynamic route ":_id" and not whatever I want?

Here's a rather standard way to set a route in Iron Router:
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne({_id: this.params._id}) }
});
Experimenting around a little, beginner as I am, I tried:
Router.route('/posts/:whatever', {
name: 'postPage',
data: function() { return Posts.findOne({_id: this.params.whatever}) }
});
This works well, up to a point. True, whatever will scoop up whatever is after /posts/ as its value, and the data context will indeed be the same as before... but linking to specific posts now won't work!
So,
{{title}}
simply won't work doing it "my" way (linking to nothing at all).
I can't fully wrap my head around this, and I'm too much of a novice to grasp the source code for Iron Router, so my hope is that someone here can explain it in a manner that even a beginner like me can comprehend.
Preferably like something like this:
First {{pathFor 'postPage'}} looks inside the routes to find the one named postPage.
It sees that this route corresponds to /posts/ followed by something else.
Looking inside the data context it finds that only one post is returned, namely the one with the same _id as whatever comes after /posts/.
It understands that it should link to this post, cleverly setting the url to /posts/_id.
This is wrong, most likely, and it doesn't explain why it would work when whatever is turned into _id. But it would help me immensely to see it parsed in a similar fashion.
Edit: Cleaned up my question so it is easier to grasp.
There's a simple set of circumstances that together lead to confusion:
The Posts.findOne issue is explained by the fact that the first argument can be either a selector or a document _id. So it's not really a shortcut but rather a documented feature.
As you found, putting :something in the iron:router URL causes that value to be reported as this.params.something inside the route function. This also registers something as an parameter to that route, which brings us to how pathFor works.
The pathFor helper takes two inputs: first the name of the route (in this case 'postPage') and second an object of parameters, which can come either from the second argument as in {{pathFor 'postPage' params}} or from the data context like so: {{#with params}}{{pathFor 'postPage'}}{{/with}}.
Now, here's why passing in a document from the database works if you call the parameter _id but not if you call it whatever: the post object that you retrieved from the database _has an _id field, but it doesn't have a whatever field. So when you pass it into pathFor, it only passes along the correct _id if the parameter to the route also happens to be called _id.
Let me know if that makes sense, I agree that this is somewhat confusing and that this "shortcut" hides what exactly pathFor and params actually do.

Computed vs Observes on Controller in Ember

It was always my understanding that .observes('someProperty') and .property('someProperty') worked exactly the same, except that the former is used for triggering function calls and the latter is used to keep object properties up to date.
But now I'm having a problem. My controller code looks like this:
_logChange: function(){
console.log('model array observer fired');
}.observes('model.#each'),
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each')
The observer and computed property both watch model.#each but for some reason, the observer fires on every model change and the property only updates TWICE before mysteriously going dead. statsData is calculated once on initial page load, and once on the first route transition, then after that, none of the transitions (with the changes in the underlying model they make) affect it.
What's going on here? Shouldn't they respond to change in the same way?
Note that I am using the statsData property in my template.
observers fire immediately, computed's fire as part of the run loop and scheduled in a debounced fashion. Currently all you're watching is that you add or remove an item to the collection, not whether or not a property on one of the items in the collection has changed. If you want to watch a particular property, you need to specify it.
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each.cost')
if you just want to watch the collection changing you should just use []
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.[]')
Thanks to the lovely folks on Ember IRC, I was able to figure it out. The problem was that I was passing statsData to a component, like this: {{common-statistics values=statsData}} and in the component, I had this function:
_validateValues: function(){
var values = this.get('values');
if(!values || !Ember.isArray(values) || values.length === 0)
{
this.set('values',[]);
}
}.on('willInsertElement')
which is, as you can see, setting values if it's not what the component is expecting. Unfortunately, this was affecting statsData on the controller as well, thanks to this JavaScript language feature. By setting statsData in the component, I was breaking the computed property on the controller.
So it was never a problem with Ember at all. I just failed to realize that object properties on Ember objects behave the same way they do on "regular JavaScript objects."

Missing view.context or templateContext in an Ember event handler

I'm trying to push the object that populated a view into an array, but the reference is somehow getting lost. I've got an Ember view, with a defined eventManager:
FrontLine.NewProductButton = Em.View.extend({
tagName: 'button',
classNames: ['addtl_product',],
templateName: 'product-button',
eventManager: Ember.Object.create({
click: function(event, view) {
FrontLine.ProductsController.toggleProductToCustomer(event, view);
}
})
})
That view renders a bunch of buttons that are rendered with properties that come from objects in the ProductsController using the #each helper. That part works great. And when I click on any of those buttons, the click event is firing and doing whatever I ask, including successfully calling the handler function (toggleProductToCustomer) I've designated from my ProductsController:
FrontLine.ProductsController = Em.ArrayController.create({
content: [],
newProduct: function(productLiteral) {
this.pushObject(productLiteral);
},
toggleProductToCustomer: function(event, view){
FrontLine.CustomersController.currentCustomer.productSetAdditional.pushObject(view.context);
}
});
I'm trying to use that function to push the object whose properties populated that view into an array. Another place in my app (a simple search field), that works perfectly well, using pushObject(view.context). Here, however, all that gets pushed into the array is undefined. I tried using view.templateContext instead, but that doesn't work any better. When I try console.log-ing the button's view object from inside those functions, I get what I'd expect:
<(subclass of FrontLine.NewProductButton):ember623>
But either view.context or view.templateContext return undefined. How do I access the object I'm after, so I can add it to my array?
The simple answer is that it was one letter's difference:
view.content
or:
view.get('content')
provides the source object in that particular situation, rather than view.context.
(My only real challenge with Ember so far is that accessors for objects and properties vary so much from situation to situation, and there's no real documentation for that. Sometimes the object is at view.context, sometimes it's at view.content, sometimes _parentView.content, etc., etc. It would be awesome if there were a chart with the umpteen different syntaxes for accessing the same data, depending on which particular aperture you're reaching through to get it. I'm still discovering them...)

How to clear/remove observable bindings in Knockout.js?

I'm building functionality onto a webpage which the user can perform multiple times. Through the user's action, an object/model is created and applied to HTML using ko.applyBindings().
The data-bound HTML is created through jQuery templates.
So far so good.
When I repeat this step by creating a second object/model and call ko.applyBindings() I encounter two problems:
The markup shows the previous object/model as well as the new object/model.
A javascript error occurs relating to one of the properties in the object/model, although it's still rendered in the markup.
To get around this problem, after the first pass I call jQuery's .empty() to remove the templated HTML which contains all the data-bind attributes, so that it's no longer in the DOM. When the user starts the process for the second pass the data-bound HTML is re-added to the DOM.
But like I said, when the HTML is re-added to the DOM and re-bound to the new object/model, it still includes data from the the first object/model, and I still get the JS error which doesn't occur during the first pass.
The conclusion appears to be that Knockout is holding on to these bound properties, even though the markup is removed from the DOM.
So what I'm looking for is a means of removing these bound properties from Knockout; telling knockout that there is no longer an observable model. Is there a way to do this?
EDIT
The basic process is that the user uploads a file; the server then responds with a JSON object, the data-bound HTML is added to the DOM, then the JSON object model is bound to this HTML using
mn.AccountCreationModel = new AccountViewModel(jsonData.Account);
ko.applyBindings(mn.AccountCreationModel);
Once the user has made some selections on the model, the same object is posted back to the server, the data-bound HTML is removed from then DOM, and I then have the following JS
mn.AccountCreationModel = null;
When the user wishes to do this once more, all these steps are repeated.
I'm afraid the code is too 'involved' to do a jsFiddle demo.
Have you tried calling knockout's clean node method on your DOM element to dispose of the in memory bound objects?
var element = $('#elementId')[0];
ko.cleanNode(element);
Then applying the knockout bindings again on just that element with your new view models would update your view binding.
For a project I'm working on, I wrote a simple ko.unapplyBindings function that accepts a jQuery node and the remove boolean. It first unbinds all jQuery events as ko.cleanNode method doesn't take care of that. I've tested for memory leaks, and it appears to work just fine.
ko.unapplyBindings = function ($node, remove) {
// unbind events
$node.find("*").each(function () {
$(this).unbind();
});
// Remove KO subscriptions and references
if (remove) {
ko.removeNode($node[0]);
} else {
ko.cleanNode($node[0]);
}
};
You could try using the with binding that knockout offers:
http://knockoutjs.com/documentation/with-binding.html
The idea is to use apply bindings once, and whenever your data changes, just update your model.
Lets say you have a top level view model storeViewModel, your cart represented by cartViewModel,
and a list of items in that cart - say cartItemsViewModel.
You would bind the top level model - the storeViewModel to the whole page. Then, you could separate the parts of your page that are responsible for cart or cart items.
Lets assume that the cartItemsViewModel has the following structure:
var actualCartItemsModel = { CartItems: [
{ ItemName: "FirstItem", Price: 12 },
{ ItemName: "SecondItem", Price: 10 }
] }
The cartItemsViewModel can be empty at the beginning.
The steps would look like this:
Define bindings in html. Separate the cartItemsViewModel binding.
<div data-bind="with: cartItemsViewModel">
<div data-bind="foreach: CartItems">
<span data-bind="text: ItemName"></span>
<span data-bind="text: Price"></span>
</div>
</div>
The store model comes from your server (or is created in any other way).
var storeViewModel = ko.mapping.fromJS(modelFromServer)
Define empty models on your top level view model. Then a structure of that model can be updated with
actual data.
storeViewModel.cartItemsViewModel = ko.observable();
storeViewModel.cartViewModel = ko.observable();
Bind the top level view model.
ko.applyBindings(storeViewModel);
When the cartItemsViewModel object is available then assign it to the previously defined placeholder.
storeViewModel.cartItemsViewModel(actualCartItemsModel);
If you would like to clear the cart items:
storeViewModel.cartItemsViewModel(null);
Knockout will take care of html - i.e. it will appear when model is not empty and the contents of div (the one with the "with binding") will disappear.
I have to call ko.applyBinding each time search button click, and filtered data is return from server, and in this case following work for me without using ko.cleanNode.
I experienced, if we replace foreach with template then it should work fine in case of collections/observableArray.
You may find this scenario useful.
<ul data-bind="template: { name: 'template', foreach: Events }"></ul>
<script id="template" type="text/html">
<li><span data-bind="text: Name"></span></li>
</script>
Instead of using KO's internal functions and dealing with JQuery's blanket event handler removal, a much better idea is using with or template bindings. When you do this, ko re-creates that part of DOM and so it automatically gets cleaned. This is also recommended way, see here: https://stackoverflow.com/a/15069509/207661.
I think it might be better to keep the binding the entire time, and simply update the data associated with it. I ran into this issue, and found that just calling using the .resetAll() method on the array in which I was keeping my data was the most effective way to do this.
Basically you can start with some global var which contains data to be rendered via the ViewModel:
var myLiveData = ko.observableArray();
It took me a while to realize I couldn't just make myLiveData a normal array -- the ko.oberservableArray part was important.
Then you can go ahead and do whatever you want to myLiveData. For instance, make a $.getJSON call:
$.getJSON("http://foo.bar/data.json?callback=?", function(data) {
myLiveData.removeAll();
/* parse the JSON data however you want, get it into myLiveData, as below */
myLiveData.push(data[0].foo);
myLiveData.push(data[4].bar);
});
Once you've done this, you can go ahead and apply bindings using your ViewModel as usual:
function MyViewModel() {
var self = this;
self.myData = myLiveData;
};
ko.applyBindings(new MyViewModel());
Then in the HTML just use myData as you normally would.
This way, you can just muck with myLiveData from whichever function. For instance, if you want to update every few seconds, just wrap that $.getJSON line in a function and call setInterval on it. You'll never need to remove the binding as long as you remember to keep the myLiveData.removeAll(); line in.
Unless your data is really huge, user's won't even be able to notice the time in between resetting the array and then adding the most-current data back in.
I had a memory leak problem recently and ko.cleanNode(element); wouldn't do it for me -ko.removeNode(element); did. Javascript + Knockout.js memory leak - How to make sure object is being destroyed?
Have you thought about this:
try {
ko.applyBindings(PersonListViewModel);
}
catch (err) {
console.log(err.message);
}
I came up with this because in Knockout, i found this code
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
ko.utils.domData.set(node, boundElementDomDataKey, true);
}
So to me its not really an issue that its already bound, its that the error was not caught and dealt with...
I have found that if the view model contains many div bindings the best way to clear the ko.applyBindings(new someModelView); is to use: ko.cleanNode($("body")[0]); This allows you to call a new ko.applyBindings(new someModelView2); dynamically without the worry of the previous view model still being binded.
<div id="books">
<ul data-bind="foreach: booksImReading">
<li data-bind="text: name"></li>
</ul>
</div>
var bookModel = {
booksImReading: [
{ name: "Effective Akka" },
{ name: "Node.js the Right Way" }]
};
ko.applyBindings(bookModel, el);
var bookModel2 = {
booksImReading: [
{ name: "SQL Performance Explained" },
{ name: "Code Connected" }]
};
ko.cleanNode(books);
ko.applyBindings(bookModel2, books);

Categories

Resources