Table column rendered by javascript snippet using model.updateData(callback) does not wait for the callback in V11.1.6

  • 1
  • Problem
  • Updated 8 months ago
  • In Progress
That title is a mouthful, but it is a radical behavior change from v9.5.17. We have a table that uses javascript to render one of the columns by looking up data from another model. In v9, each row is updated with the lookup data applicable to that row. In v11, each row is updated with the lookup data from the last row in the table. In other words, each row displays redundant information which is wrong, except on the last row.

The javascript used to render the field in table sets a condition on the secondary model, then queries the model with updateData(callback). The callback function calls the standard renderer, using data from the secondary model as value in the renderer.

Although our use case involves a set of custom objects, you can see the behavior with the User object doing a redundant lookup to itself. The primary model is Users, the secondary model is Users2. There is no condition on Users; Users2 has a filterable condition on Id equal to a single specified value, on by default. Both models include the Id and Name fields.

The Table on the page displays Name and Id with the standard renderer. A third column is also the Name field, but it is rendered by a javascript snippet. The snippet looks like this:

var field = arguments[0];
var userId = field.row.Id;

var mymodel = skuid.model.getModel("Users2");
var condition = mymodel.getConditionByName("Id");
mymodel.emptyData();
mymodel.setCondition(condition, userId);

console.log('Updates with ' + condition.value);

mymodel.updateData( function(result) { updateDone(result) });

function updateDone(result)
{
    console.log('Display with ' + mymodel.data[0].Name);
    //skuid.ui.fieldRenderers[field.metadata.displaytype].read( field, mymodel.data[0].Name );
    skuid.ui.getFieldRenderer(field.metadata.displaytype).read( field, mymodel.data[0].Name );
} 

This is the result in v9.5.17:
This is the result in v11.1.6:


The console log shows that the script is not waiting for the for the updateData() to complete.


Is this a bug, or is it an undocumented change of behavior in Millau?

If this method needs coding of a deferred promise, some sample code would be appreciated.

FWIW, I've also tried adding return result to the callback function, testing result for totalsuccess (which seems not to be an attribute of the object), and returning the entire mymodel.updateData(...) call.

Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb

Posted 8 months ago

  • 1
Photo of Josh Lewis

Josh Lewis

  • 1,218 Points 1k badge 2x thumb
Hi Mike,

In taking a first look at what you have going on with your code, it looks like there might be a deprecated field renderer API in use that might be part of the undesirable behavior. https://docs.skuid.com/v11.1.6/en/release-notes.html#use-getrenderer-and-getfieldrenderer-instead-of-directly-accessing-properties This Doc outlines the change in behavior that I mentioned. If you could take a look and let me know if this helps that would be great!

Thanks,

Josh
Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb
Thanks, Josh. I came across the deprecation of fieldRenderers while trying to resolve this issue. You'll notice in my posted javascript that I have commented that out and replaced it with getFieldRenderer. After looking at the Doc, I also tried returning the result of the API call, but with no impact on the display.
Photo of Josh Lewis

Josh Lewis

  • 1,218 Points 1k badge 2x thumb
Hi Mike,

I took a second pass and found another deprecated API. It looks like the best practice is to not use skuid.model.updateData and instead use skuid.model.Model.update. There is a running list of features/API functions found in our documentation here: https://docs.skuid.com/latest/en/skuid/deprecated-features-api.html. My guess is that the framework of this is close to right, but a handful of changes have occurred between versions and likely the code needs a combing over to update to currently supported APIs. 

Thanks,

Josh
Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb
Please explain with an example. I used skuid.model.getModel("otherUsers"), which returns the skuid.model.Model type. I believe I have only included one deprecated API in the script, and it is commented out. What am I missing?
Photo of Josh Lewis

Josh Lewis

  • 1,218 Points 1k badge 2x thumb
Mike,

I was referring to this line: 
mymodel.updateData( function(result) { updateDone(result) });I think that there was a change to skuid.model.Model.update instead of using the .updateData API, additionally the document I shared covers any other use cases where behaviors have changed, which is why I shared that more as an FYI given the jump betweeen versions.Thanks,Josh
Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb
You are apparently referring to this:

But from https://docs.skuid.com/latest/en/skuid/api/skuid_model_model.html, we have:


So, is there a documentation problem? I am still very confused.
Photo of Josh Lewis

Josh Lewis

  • 1,218 Points 1k badge 2x thumb
Hi Mike,

It seems like there is context being lost at some point in regards to the the current row. Could you try doing a do a console.log('result') inside the updateDone, to see what the updateData result is returning? I am wondering if the data being returned is coming back different that what you expect?

My interpretation is that you are hoping to see the result come back 5 times and that each time the result would be a distinct record in the model you are re-querying, is that right?

Thanks,

Josh
Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb
updateData() returns an object that looks like this:

It has no material value to my code. In fact, in my ongoing testing I have removed the argument completely and get the same results behavior. The updateData() API returns a different object than save(), where one might test for result.totalsuccess.

And, yes, I want the results to come back from the updated model 5 times and have each individual result set rendered in the table cell.

The actual use case in simplified form is a structure where two SObjects are each children in a master-detail relationship with the same master SObject.

[Detail1] is a child to ---v
+--- [Master]
[Detail2] is a child to ---^
The table on the page is populated from a model based on [Detail1], and one of the fields/columns in that table is a lookup to [Detail2] where the master reference value is the same.

I think we might do this with a LOOKUP() formula, except that the number of records in the Detail2 object would exceed the apex heap limit.

And, yes, the javascript method is terribly non-performant, but the data reported is valuable enough that the users are willing to wait for it.
Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb
Here is a more generic question: What is the expected behavior of this snippet?
console.log('start javascript');
var myModel = skuid.model.getModel("myModel");
var condition = myModel.getConditionByName("Id");
myModel.setCondition(condition, '005t0000000cfDhAAI');
myModel.updateData( function(result) { 
    console.log('run the callback');
});
console.log('finish javascript');
return true; 
Will the console display:
start javascript
run the callback
finish javascript

or will the console display:
start javascript
finish javascript
run the callback

And if it is the latter, then what exactly is the purpose of the callback, and when exactly is it run?
Photo of Jackson Stone

Jackson Stone

  • 70 Points
Hey Mike, I'll try to pop the stack as it were with these questions.

The expected result is as follows in nearly all scenarios, assuming the model does not error out during its load: 

start javascript
finish javascript
run the callback

The purpose of the callback is identical to the callback function in a .then block of a promise.

So: model.updateData(foo) would be identical in nearly all scenarios to model.updateData().then(foo)

The reason your snippets broke is a bit in the weeds so warning: 

WARNING - Nerdy Explanation Inbound

It seems before we actually make the request for your model data, we do other asynchronous operations now. So before we actually send any request "over the wire" with your updated condition value, the javascript event loop runs all other snippets for your renderers (which is a synchronous operation), so by the time the request is actually being sent over the network, the end result is 10+ identical requests all with the final condition value you updated things to. This is why all rows now get the same result.

It's confusing I know. I was surprised when I saw it too. But I wouldn't consider this a bug as much as a change in internal structure. The docs as written still hold true. Skuid is free to make the network request at any point during the Async function, and it is unreliable to assume when the formatting of the request will actually take place. Though admittedly it is very unfortunate that we've tripped you up the way we have.

However, I think there is a relatively straightforward way to fix this. And the fix will work in both Environments. 

Rather than having the snippet refer to a model that exists outside the snippet, and calling updateData on this model many times, create a new model on the spot with the characteristics you want. Call updateData on this model, and pulling the first row in this temporary model should have the data you are expecting.

Creating a skuid model with JavaScript can be seen in this Gist:  https://gist.github.com/zachelrath/4eef2eceaa2d56be9cf2 (lines 19-58)

Let me know if that was clear/unclear! Good Luck! 

P.S. I find your use of render snippets pretty cool
Photo of Mike Dwyer

Mike Dwyer

  • 3,390 Points 3k badge 2x thumb
Thanks, Jackson. We'll take a look at that. We may instead be able to create a server-side model on the "sibling" object (the one with the same master object) using the same fixed conditions as the primary model, We would then use the javascript to populate from the sibling model using a loop.
Photo of Jackson Stone

Jackson Stone

  • 70 Points
Yep if possible remove the need for multiple models! Much better!