Row Action with inline snippet to update field

Evan HollondsEvan Hollonds Member
edited March 2019 in Questions
How can I define a Row Action that updates a field within a row. I've attempted the following, but I'm getting a 'Visualforce Remoting Exception: Attempt to de-reference a null object' error. Row Action, Action Type = Custom, using inline snippet as below: skuid.snippet.registerSnippet('orderLineItem.CalculateCost', function(params) { var price = params.item.row.Price__c; var quantity = params.item.row.Quantity__c; var cost = price * quantity; params.model.updateRow(params.item.row, 'Cost', cost); params.model.save(); }; Also, is it possible to just update the value when in edit mode, and let the user initiate the save. I commented out the "params.model.save()", but nothing is updated in the UI?

Comments

  • Zach McElrathZach McElrath Skuad
    edited March 2017
    Hi Evan, The reason you're getting the Remoting Exception is that you're trying to update a Field that does not exist --- "Cost" should be "Cost__c", I believe. As far as getting the Cost__c column in your Table to automatically update whenever Price and Quantity are updated by the user, this is a very good use case for a Custom Field Renderer. Basically, go to your "Cost" column in your table, and change the "Field Renderer" to be "Custom", then for "Render Snippet", enter "CostRenderer". Then, go to your JavaScript Resources, and create a new "Inline (Snippet)" resource, named CostRenderer, with the following Body:
        var field = arguments[0],      value = arguments[1],      $ = skuid.$,      dt = field.metadata.displaytype;    if (field.mode != 'edit') {      skuid.ui.fieldRenderers[dt][field.mode](field,value);  } else {      skuid.ui.fieldRenderers[dt].edit(field,value);   }    var calculateCost = function(){      value = (field.model.getFieldValue(field.row,'Quantity__c',true) || 0)          * (field.model.getFieldValue(field.row,'Price__c',true) || 0);        value = Math.round(value * 100) / 100;        field.model.updateRow(          field.row,          'Cost__c',          value      );            if (field.mode == 'edit') {          field.element.find('input').val(value);      }  };    calculateCost();    var listener = new skuid.ui.Field(field.row,field.model,null,{fieldId: 'Cost__c'});  listener.handleChange = function(){      calculateCost();  };  field.model.registerField(listener,'Price__c');  field.model.registerField(listener,'Quantity__c');    
    The result should be that whenever you change the Price or Quantity fields in your table, your Cost field will be updated.
  • Glenn ElliottGlenn Elliott Member
    edited April 2017
    This is nice. But what if we don't want to write the calculated value to the database? e.g. if we just want to display the cost (calculated as price x quantity), but not store it? It's almost like a custom field renderer for a template field, but that's not possible, is it?
  • Rob HatchRob Hatch Skuad, Sonar ✭✭
    edited September 2016
    Glenn,  Here is a shot in the dark.

    You can create a custom renderer on another field (Which you use just as a dummy placeholder)  and in that renderer call the fields and do your calculations in Javascript.   I'm thinking the dummy field could be set to read only so that a save is not attempted on that field when you save the other fields in the table. 


  • Zach McElrathZach McElrath Skuad
    edited March 2017
    Rob's approach should work --- just add a custom field renderer on a dummy field (e.g. the Id field) and then in your custom field renderer display the calculated value, and don't provide an "Edit" mode renderer at all. Just unilaterally output a number/currency in read mode, e.g.

    var field = arguments[0],   
       value = arguments[1],
        $ = skuid.$;

    field.metadata = {
       displaytype: 'CURRENCY',
       accessible: true,
       precision: 8,
       scale: 2,
       required: false,
       nameField: false,
       label: 'Cost'
    };
    field.required = false;

    var calculateCost = function(){
        value = (field.model.getFieldValue(field.row,'Quantity__c',true) || 0)
            * (field.model.getFieldValue(field.row,'Price__c',true) || 0);
        value = Math.round(value * 100) / 100;
        field.element.empty();
        skuid.ui.fieldRenderers.CURRENCY.readonly(field,value);
    };

    calculateCost();

    var listener = new skuid.ui.Field(field.row,field.model,null,{fieldId: 'Quantity__c',register:true});
    listener.handleChange = function(val){
        calculateCost();
    };
    field.model.registerField(listener,'Price__c');


  • Glenn ElliottGlenn Elliott Member
    edited April 2017
    Thanks gents. That looks like a plan. In fact we tried this yesterday using the id field, but it kept displaying the id rather than our calculation. But looking at your code here I can see some areas where I think we've gone wrong, so will give it another crack.
  • Glenn ElliottGlenn Elliott Member
    edited December 2014
    Yep, this works beautifully. Love it.

    Now, for bonus points ...

    My actual use case is to calculate a Budget Amount and an Actual Amount, which I do using the above technique, but then also calculate a Variance which is simply Budget Amount minus Actual Amount. In the Variance custom renderer, I don't really want to have to repeat the calcs for budget and actual, I want to refer to those values already calculated in the other two fields. Is that possible? I suspect not, given that Budget, Actual and Variance all render simultaneously.
  • Rob HatchRob Hatch Skuad, Sonar ✭✭
    edited September 2016
    As usual there are definitely multiple ways to do this. One idea that comes to mind is to put your JS code in a static resource.  Each field in the table could reference a snippet defined in that common JS file.  Those snippets could in turn reference common calculation functions also contained in the file.  Just a matter of code abstraction.

    Another idea that might not meet you stringent UI standards would be to combine all three items in a single field.  You have well used the "stacked field" model in your tables.  This would allow a single snippet to be do your calculations and then display the three values effectively.  
  • Jack SanfordJack Sanford Member ✭✭
    edited January 2018
    I'm trying to do the same thing and it's not working. I've got a join object FCCA, and I want to update a field on my FC object with a field from my CA object. 

    I've got a table of FCCA's and here's my row action snippet, in its entirety:
    skuid.snippet.registerSnippet('updateMERS', function(params) {   var caMERS = params.item.row.CustomerAccount__r.MERSSearchCompleted__c;   params.model.updateRow(params.item.row, 'Foreclosure__r.MERSSearchComplete__c', caMERS);   params.model.save();   });
    I'm very new here so I may be missing something. 

    Ideally this would be a Mass Action, and happen to all the selected rows, is that possible?

    Here's the error I'm getting:
    image

    The line referred to in the error is var caMERS = params.item.row...

    I may be missing something very basic here. 

  • Zach McElrathZach McElrath Skuad
    edited December 2016
    The problem is that you're trying to update a related object field --- Skuid does not support updating across relationship fields, e.g. 'Foreclosure__r.MERSSearchComplete__c' is not updateable here as you are trying to do it. You could update 'MERSSearchComplete__c' only if your row was the Foreclosure__r record and your Model was on the Foreclosure__r object.

    That being said, this is doable, it will just take a bit more work, you would need to create another Model on whatever object Foreclosure__r is pointing to. This Model should not load any records by default, but it should have a Filterable (Default Off) Condition on the Id field of the object. Then in your Snippet (thinking here about using it for Row or Mass Action) you would need to assemble a list of all the Foreclosure Ids that you need to query for, and then perform a query on the Model to get those records. Then you would do your updates in much the same way as you're doing right now, only you'd be updating the Foreclosure records you queried for. Then you save the Foreclosures Model. Finally to update the field values on your original Model, re-query it.

    So assuming that you have a Model called "SelectedForeclosures" or something like that on your "Foreclosure" object, whatever its name is, and that you have a Filterable Condition on the Id field of that object called "SelectedIds" or something like that, you could do something like this:

    skuid.snippet.registerSnippet('updateMERS', function(params) { 
       var $ = skuid.$;
       var errorHandler = function(message){
          $.blockUI({
             message: 'There was an error updating MERS' + (message ? ': ' + message : '.'),
             // Show the message for 2 seconds
             timeout: 2000
          });
       };

       // Block the UI
       $.blockUI({
          message: 'Updating MERS...'
       });

       // Get our Foreclosures Model and Condition
       var foreclosuresModel = skuid.$M('SelectedForeclosures');
       var selectedIdsCondition = foreclosuresModel.getConditionByName('SelectedIds');

       // get a mapping of Foreclosure records we want to update
       var foreclosureUpdates = {};
       // Is this a Row Action? If so we should have an item and a row
       if (params.item && params.item.row) {
          foreclosureUpdates[params.item.row.Id] = params.item.row.CustomerAccount__r.MERSSearchCompleted__c;
       } 
       // Is this a Mass Action? If so we should have selected items
       else if (params.list && params.list.getSelectedItems().length) {
          $.each(params.list.getSelectedItems(),function(i,item){
             foreclosureUpdates[item.row.Id] = item.row.CustomerAccount__r.MERSSearchCompleted__c;
          });
       }

       // Set the value of our SelectedIds condition to the Ids of the rows we want to update
       foreclosuresModel.setCondition(selectedIdsCondition,Object.keys(foreclosureUpdates));

       // Now query for foreclosures
       foreclosuresModel.updateData()
       .done(function(){
          var updatesToMake = {};
          // Loop over our Foreclosure rows and apply our updates
          $.each(foreclosuresModel.getRows(),function(i,row){
             updatesToMake[row.Id] = {
                'MERSSearchComplete__c': foreclosureUpdates[row.Id]
             };
          });
          // Perform an en-masse update
          foreclosuresModel.updateRows(updatesToMake);
          // Now save our foreclosures Model
          foreclosuresModel.save()
          .done(function(result){
             // If our result was a success,
             // then re-query our principal model
             if (result.totalsuccess) {
                params.model.updateData()
                .done(function(){
                   // Success!
                      $.unblockUI();
                })
                .fail(errorHandler);
             } else {
                errorHandler();
             }
          })
          .fail(errorHandler);
       })
       .fail(errorHandler);
    });



  • Jack SanfordJack Sanford Member ✭✭
    edited May 2016
    Thanks so much! It's not working though...yet. 

    The two fields I'm trying to work with are both Date fields, that's probably what's causing an error. 

    Here's the error I'm getting, "Illegal Value for Primitive":
    image

    The line in VFRemote.js that's referenced is:
    $VFRM.Util={log:function(a,b){if(!("undefined"===typeof console||!console.groupCollapsed||!console.log||!console.groupEnd))if("undefined"!==typeof b&&null!==b)try{console.groupCollapsed(a),console.log(b),console.groupEnd()}catch(c){}else try{console.log(a)}catch(d){}},warn:function(a){if("undefined"!==typeof console&&console.warn&&a)try{console.warn(a)}catch(b){}},error:function(a,b){if("undefined"!==typeof console&&console.error&&a)if(b)try{console.error(a,b)}catch(c){}else try{console.error(a)}catch(d){}},






  • Zach McElrathZach McElrath Skuad
    edited December 2016
    Hi Jack, try this instead, the parts in Bold are what I changed:

    skuid.snippet.registerSnippet('updateMERS', function(params) { 
       var $ = skuid.$;
       var errorHandler = function(message){
          $.blockUI({
             message: 'There was an error updating MERS' + (message ? ': ' + message : '.'),
             // Show the message for 2 seconds
             timeout: 2000
          });
       };

       // Block the UI
       $.blockUI({
          message: 'Updating MERS...'
       });

       // Get our Foreclosures Model and Condition
       var foreclosuresModel = skuid.$M('SelectedForeclosures');
       var selectedIdsCondition = foreclosuresModel.getConditionByName('SelectedIds');

       // get a mapping of Foreclosure records we want to update
       var foreclosureUpdates = {};
       // Is this a Row Action? If so we should have an item and a row
       if (params.item && params.item.row) {
          foreclosureUpdates[params.item.row.Foreclosure__c] = params.item.row.CustomerAccount__r.MERSSearchCompleted__c;
       } 
       // Is this a Mass Action? If so we should have selected items
       else if (params.list && params.list.getSelectedItems().length) {
          $.each(params.list.getSelectedItems(),function(i,item){
             foreclosureUpdates[item.row.Foreclosure__c] = item.row.CustomerAccount__r.MERSSearchCompleted__c;
          });
       }

       // Set the value of our SelectedIds condition to the Ids of the rows we want to update
       foreclosuresModel.setCondition(selectedIdsCondition,Object.keys(foreclosureUpdates));

       // Now query for foreclosures
       foreclosuresModel.updateData()
       .done(function(){
          var updatesToMake = {};
          // Loop over our Foreclosure rows and apply our updates
          $.each(foreclosuresModel.getRows(),function(i,row){
             updatesToMake[row.Id] = {
                'MERSSearchComplete__c': foreclosureUpdates[row.Id]
             };
          });
          // Perform an en-masse update
          foreclosuresModel.updateRows(updatesToMake);
          // Now save our foreclosures Model
          foreclosuresModel.save()
          .done(function(result){
             // If our result was a success,
             // then re-query our principal model
             if (result.totalsuccess) {
                params.model.updateData()
                .done(function(){
                   // Success!
                      $.unblockUI();
                })
                .fail(errorHandler);
             } else {
                errorHandler();
             }
          })
          .fail(errorHandler);
       })
       .fail(errorHandler);
    });
  • Jack SanfordJack Sanford Member ✭✭
    edited May 2016
    Same error, slightly different console log. 

    I'm not sure I made it clear before, my table is full of rows of the 'FCCA' model which is a join object with the API name of FCCAAssociations__c. It looks like it shouldn't matter what the table's model is since the code it just pulling the row, but just in case.

    image

    Here's VFRemote.js:117

    $VFRM.Util={log:function(a,b){if(!("undefined"===typeof console||!console.groupCollapsed||!console.log||!console.groupEnd))if("undefined"!==typeof b&&null!==b)try{console.groupCollapsed(a),console.log(b),console.groupEnd()}catch(c){}else try{console.log(a)}catch(d){}},warn:function(a){if("undefined"!==typeof console&&console.warn&&a)try{console.warn(a)}catch(b){}},error:function(a,b){if("undefined"!==typeof console&&console.error&&a)if(b)try{console.error(a,b)}catch(c){}else try{console.error(a)}catch(d){}},

  • Zach McElrathZach McElrath Skuad
    edited December 2016
    Hmm, I think I'd have to see your full data model to evaluate further, which is probably beyond what I can offer on the support community --- I don't think your fields being Date values should matter here, as from what I can tell, we're just copying a Date from a field on one object to a field on a related object, so as long as the original Date value is good, it should be fine. 
  • Jack SanfordJack Sanford Member ✭✭
    edited May 2016
    The original Date value is good, i've got it showing up in the table to make sure there's a value. Maybe it's because it's having to go to a related object to find the value? Maybe we need to use the model for the related object with the existing value ('CustomerAccount') instead of using CustomerAccount__r ?

    Thanks for all your help so far. Maybe it's something I can work out in the Chicago training in a couple weeks.

    image

  • Zach McElrathZach McElrath Skuad
    edited December 2016
    Ah yes, the Chicago training would be a good time to work it out :) My guess is that I'm misunderstanding exactly how your data model works with the association object.
  • Jack SanfordJack Sanford Member ✭✭
    edited May 2016
    FCCAAssociations__c is a child object with two Master-Detail relationships, one to CustomerAccount__c and one to Foreclosure__c

    It may be that it would work better to have a table of CustomerAccounts, then look up the Foreclosure__c WHERE Id IN Foreclosure__c [field] FROM FCCAAssociations__c WHERE CustomerAccount__c [field] =: {{{Id}}} [Id of row]

    Hmm, starting to seem like maybe I can just do an Apex Class and a standard SF button...
  • RajatRajat Member
    edited July 2018
    Hi Team,

    I just want to convert the business account to person account on skuid, using button.

    Can you please help me to provide me the code to update the recordtypeId on Account ?

    Thanks
Sign In or Register to comment.