Skip to content

Lesson 11. Writing multiple asynchronous operations

Updated pdexter 2022-12-20

This lesson depends on the completion of Lesson 10.

A following question to the issue of asynchronous functions is that of how to elegantly handle running an asynchronous operation over multiple objects of an array.

In this lesson, we'll do that, using a new concept in our rulesets -- an array processing rule. This will add onto the work we did in Lesson 10.

Step 1. Add a new Rule to Rental Car - OnFieldChange

  • Open the OnFieldChange ruleset for Rental Car.
  • Add a new rule, "ruleUpdateChildBookings".
  • For the ruleCondition, run only if the buttonPropagateChanges was clicked, and also that ntf.bookingDocs was set from the previous rule "ruleGetChildBookings".
ruleCondition : function(ntf) { 
    return (
        ntf.context.fieldChanged === 'buttonPropagateChanges'
        && ntf.bookingDocs?.length
    );
}   

Note: any property/attribute that is set on ntf is then carried through for the lifetime of the ruleset run instance. So for above, ntf.bookingDocs is still in effect from the previous rule.

In this way, we can maintain states and variables that may feed into subsequent rules.

  • Now we change from the usual way we construct our rule. Instead of a ruleAction, we next define a function property called arrayToProcess.
arrayToProcess : function(ntf) {
    return ntf.bookingDocs;
},

This simply returns the array of objects or values that we want to process.

  • Add a function called preLoop, which runs before the first item is processed.
  • Add some script to run when the first item is being processed, to inform the user that something is happening.
preLoop : function(ntf) {
    var ft3 = ntf.scope;
    ft3.showNotification(`Processing child Booking documents (${ntf.bookingDocs.length})`);

}
  • Add a new function property called ruleItemProcess
ruleItemProcess : function(ntf, item, index, callback) {

}

This function will be run over each item of the array proviced in arrayToProcess. index is also provided, and also the callback argument, since it is assumed that this is an asynchronous process.

We can in fact change the name of the item argument, to reflect our own purposes.

ruleItemProcess : function(ntf, bookingDoc, index, callback) {

}
  • Build the change we want to make to our child bookings. For this lesson, we simply want to pass the summaryName of this document to the linking field in the booking documents.
ruleItemProcess : function(ntf, bookingDoc, index, callback) {

    ... 

    var tempCarRel = bookingDoc.rentalCarRel;
    tempCarRel[0].name = ntf.document.systemHeader.summaryName;

    var change = {
        rentalCarRel : tempCarRel
    }
}

When a change is done to a document, that changes its summaryName, it does not automatically update the displayed name in related document fields of other documents that link to it. Generally we are not too concerned about this, since the link is still clickable from the other document; we do it here simply as an exercise.

  • Implement the change using ft3.updateDocument
ruleItemProcess : function(ntf, bookingDoc, index, callback) {
    var ft3 = ntf.scope;
    ...
    ...

    ft3.updateDocument(bookingDoc.documentId, change, ntf.userId, function(err,doc) {
        if (err) {
            ntf.logger.error('Error on update: ' + err.message);
            ft3.showNotification('Update Error: ' + err.message, 'error');
        }
        else {
            ntf.logger.info('Booking document ' + bookingDoc.documentId 
                 + ' updated ok.');
            ft3.showNotification('Booking document ' + bookingDoc.documentId 
                 + ' updated ok.');
        }
        callback();
    });
}

Note here, that we call callback() within the function block of ft3.updateDocument. This tells the ruleItemProcess function that we are finished with this item of the array, and we can proceed to the next item.

  • Finally, we now add ruleAction. In this context though, this is the function that we want to run when all the items have been processed.
ruleAction : function(ntf) {
    var ft3 = ntf.scope;
    ntf.logger.info('All child booking documents processed.');
    ft3.showNotification('All child booking documents processed.', 'success');
}

Please note, in this case, we cannot use a callback for ruleAction. It is meant simply to provide quick (synchronous) finishing tasks.

The full rule should look like this

ruleUpdateChildBookings : {
    ruleCondition : function(ntf) { 
        return (
            ntf.context.fieldChanged === 'buttonPropagateChanges'
            && ntf.bookingDocs && ntf.bookingDocs.length
        );
    },

    arrayToProcess : function(ntf) {
        return ntf.bookingDocs;
    },

    preLoop : function(ntf) {
        var ft3 = ntf.scope;
        ft3.showNotification(`Processing child Booking documents (${ntf.bookingDocs.length})`);

    },

    ruleItemProcess : function(ntf, bookingDoc, index, callback) {
        var ft3 = ntf.scope;

        var tempCarRel = bookingDoc.rentalCarRel;
        tempCarRel[0].name = ntf.document.systemHeader.summaryName;

        var change = {
            rentalCarRel : tempCarRel
        }

        ft3.updateDocument(bookingDoc.documentId, change, ntf.userId, function(err,doc) {
            if (err) {
                ntf.logger.error('Error on update: ' + err.message);
                ft3.showNotification('Update Error: ' + err.message, 'error');
            }
            else {
                ntf.logger.info('Booking document ' + bookingDoc.documentId 
                                + ' updated ok.');
                ft3.showNotification('Booking document ' + bookingDoc.documentId 
                                     + ' updated ok.');
            }
            callback();
        });
    },

    ruleAction : function(ntf) {
        var ft3 = ntf.scope;
        ntf.logger.info('All child booking documents processed.');
        ft3.showNotification('All child booking documents processed.', 'success');
    }
}

Step 2. Adding some result counting

What you might notice about the above exercise, is that we have no persistent information on whether any of the updates failed or succeeded.

Here we'll add some count setting in order to have a reported success/fail count.

  • Modify the beginning of preLoop to initialise counters, as follows:
preLoop : function(ntf) {
    var ft3 = ntf.scope;

    ft3.showNotification(`Processing child Booking documents (${ntf.bookingDocs.length})`);
    ntf.buSuccessCount = 0;
    ntf.buFailCount = 0;
}
  • Under the ft3.updateDocument function block, for the error case and the success case, increment the appropriate counter.
...
    ft3.updateDocument(bookingDoc.documentId, change, ntf.userId, function(err,doc) {
        if (err) {
            ntf.logger.error('Error on update: ' + err.message);
            ft3.showNotification('Update Error: ' + err.message, 'error');
            ntf.buFailCount += 1;
        }
        else {
            ntf.logger.info('Booking document ' + bookingDoc.documentId 
                            + ' updated ok.');
            ft3.showNotification('Booking document ' + bookingDoc.documentId 
                                 + ' updated ok.');
            ntf.buSuccessCount += 1;
        }
        ...
  • Under the finalising ruleAction function, display the counts.
ruleAction : function(ntf) {
    var ft3 = ntf.scope;
    ntf.logger.info('All child booking documents processed.');
    ft3.showNotification('All child booking documents processed.', 'info');

    if (ntf.buFailCount > 0) {
        ft3.ModalService.openModal({
            title : 'Propagate Changes', 
            html : 'Not all Bookings were successfully updated.<br/>'
            + 'Successful: ' + ntf.buSuccessCount + '<br/>'
            + 'Failed: ' + ntf.buFailCount,
            type : 'warning'
        });
    }
    else {
        ft3.ModalService.openModal({
            title : 'Propagate Changes', 
            html : ntf.buSuccessCount + ' Bookings successfully updated.',
            type : 'success'
        });
    }
}

Because this uses the dialog services library, we must include the SweetAlert library into our ruleset thus:

#include "SweetAlert Dialog",

Again, place this at top of the ruleset, beneath any other #include lines.

Step 3. Testing

  • Open an existing Rental Car document that you've added some Bookings to. If not, create some new Bookings.

  • Note the display name shown in the Bookings' Rental Car field. This should match the naming of the Rental Car document (the title showing on the top toolbar of Formbird).

  • Change the Make of the Rental Car, eg setting the first character to 'X'.

  • Save the Rental Car.

  • Click the "Propagate Changes" button.

  • A number of scrolling notifications should appear in the top right corner of Formbird, then a final dialog declaring success.

  • Inspect the Booking documents' Rental Car field.

  • The Rental Car field should have changed to display the new name of the Rental Car.

If the above is true, you have successfully implemented an asynchronous array processing rule!

Lesson Items covered

  • Implementation of an asynchronous array processing rule
  • Use of ft3.updateDocument
  • Use of Modal Dialog