Integrating with JavaScript applications

In this task we will create two basic JavaScript applications from an existing model. In the first iteration we will embed the generated code into an html web page that will run one of your calculations based on user input. In a second iteration we will move the calculation logic into a Node.js server application that the web page will access via HTTP which is the preferred way when your calculations contain confidential logic or parameters.

../_images/integrate-javascript-build-option.png
  1. Before we start we will need a valid model with at least one calculation defined. If you have finished our Getting Started tutorial then that project will be a good starting point. You may also create a new project from one of our prepared samples to start with.
  2. You will need to build a downloadable JavaScript library from your calculations to work with. This can be accomplished by setting up your build configuration to prepare a build for the JavaScript target. Navigate to the “Deployment” page of your project. You may create a new build config by clicking “New” or modify an existing one by selecting from the dropdown and clicking “Options”. In the dialog window make sure that the “JavaScript” target is enabled. Click “Create” or “Update” depending on your previous choice.
  3. Click “Build”. As a first step your model is validated and issues with your model will be displayed when detected. You will need to fix these problems before proceeding.
  4. Click “Next” to continue. This step displays the changes in your calculation API since your last released version. If you have not released any previous builds then this will include all your calculations marked as additions. Click “Build” to let the code generation begin. This should not take long to complete.
  5. Open the build by clicking it. You will need to download the .zip archive generated for the JavaScript target. The “Overview” tab contains the general documentation of your calculations and the “JavaScript” tab contains language-specific usage instructions. A download link is available under both tabs.
  6. Download the zip file and extract it. The downloaded zip file will contain the .js file that you will need to include in your application. The language-specific guide also gets included in the zip file.

Embedding the calculations into a web page

The method of loading the provided JavaScript module will depend on your choice of module loading. The generated JavaScript file exposes your calculation API with the UMD (Universal Module Definition) pattern allowing you to load it as an AMD module, a CommonJS module or as a global variable in browsers.

We will load our JavaScript module with a script tag in your html making your calculation API accessible through the global (window) object.

<script src="./mortgageCalculations.js"></script>

Note

For more complex applications loading JavaScript this way is not practical. It’s recommended to use a module bundler like webpack and transpile the code with Babel to have support for new JavaScript features in any browser. The usage of these tools is beyond the scope of this guide.

Loading the module with the <script> tag will expose your calculations on the window object as window.mortgageCalculations. For example to call the calculation pmt in calculation group mortgagePmt you would write something similar:

const mortgageCalculations = window.mortgageCalculations;
try {
  const result = mortgageCalculations.mortgagePmt.pmt({
    principal: 200000,
    interestYearly: 6.5,
    term: 30
  });
  handleResult(result);
} catch(err) {
  handleError(err);
}

How you implement the user interface depends on the UI framework of your choice. The following steps will demonstrate a simple framework-independent UI implementation. Source on GitLab

To follow along you will need the style.css and mortgageCalculations.js files from the GitLab repository.

  1. Let’s start by creating our HTML that displays a form providing inputs for calculation parameters and displaying output values.
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Mortgage calculator example</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="style.css">
  <script src="mortgageCalculations.js"></script>
</head>
<body>
  <form>
    <div class="row">
      <label for="paramPrincipal">principal:</label>
      <input type="text" id="paramPrincipal">
    </div>
    <div class="row">
      <label for="paramPrincipal">interestYearly:</label>
      <input type="text" id="paramInterestYearly">
    </div>
    <div class="row">
      <label for="paramPrincipal">term:</label>
      <input type="text" id="paramTerm">
    </div>
    <div class="row">
      <div id="errorMessage" class="hidden"></div>
    </div>
    <div class="row">
      <button type="submit" id="buttonCalculate">Calculate</button>
    </div>
    <div class="row">
      <label for="resultMonthlyPayment">monthlyPayment:</label>
      <input type="text" id="resultMonthlyPayment" disabled="disabled">
    </div>
  </form>
  <script>
    // our script will go here...
  </script>
</body>
</html>
  1. To interact with the elements on our page we must query them by id from the DOM and save them as variables.
const paramPrincipalEl = document.getElementById('paramPrincipal');
const paramInterestYearlyEl = document.getElementById('paramInterestYearly');
const paramTermEl = document.getElementById('paramTerm');
const buttonCalculateEl = document.getElementById('buttonCalculate');
const errorMessageEl = document.getElementById('errorMessage');
const resultMonthlyPaymentEl = document.getElementById('resultMonthlyPayment');
  1. The following functions will help us update the page with the calculation results.
function displayResult(result) {
  if (result === null) {
    resultMonthlyPaymentEl.value = '';
  } else {
    resultMonthlyPaymentEl.value = result.monthlyPayment;
  }
}

function displayErrorMessage(errorMessage) {
  if (errorMessage === null) {
    errorMessageEl.textContent = '';
    errorMessageEl.classList.add('hidden');
  } else {
    errorMessageEl.textContent = errorMessage;
    errorMessageEl.classList.remove('hidden');
  }
}
  1. We want to execute the calculation when the user clicks the “Calculate” button so we must add a click event listener to our button. If you use a <form> that you do not want to submit any data to the server make sure to use preventDefault() on your button to avoid reloading the page.
buttonCalculateEl.addEventListener('click', onCalculateClick, true);

function onCalculateClick(evt) {
  evt.preventDefault(); // do not submit the form
}
  1. Before invoking our calculation we should reset the UI to clear any previous calculation output. After this we obtain the values assigned by the user from the input elements.

Conversion to Number is not necessary as the calculation can handle string inputs. The calculation will also validate the parameters against the mathematical domains defined in your calculation API so it’s best to leave that task to the calculation. Any validation or runtime error will be thrown so make sure to catch them.

function onCalculateClick(evt) {
  evt.preventDefault(); // do not submit the form
  displayResult(null); // reset previous result
  displayErrorMessage(null); // reset error message

  const principal = paramPrincipalEl.value;
  const interestYearly = paramInterestYearlyEl.value;
  const term = paramTermEl.value;

  try {
    const result = window.mortgageCalculations.mortgagePmt.pmt({
      principal,
      interestYearly,
      term
    });
    displayResult(result);
  } catch(err) {
    displayErrorMessage(err.message);
  }
}

That’s it. The complete source is available at GitLab.

Embedding the calculations in a Node.js server application

When your calculations contain confidential logic or parameters it’s a good solution to move the calculation logic to a server component inaccessible to end-users.

Continuing the previous example we will implement a simple Node.js server application with the Express framework that will execute the calculation instead of the browser. The browser will communicate with the server via HTTP.

Note

Securing your server and implementing authentication is beyond the scope of this guide.

In Node.js we load the JavaScript module with CommonJS syntax:

const mortgageCalculations = require('./mortgageCalculations.js');

Your calculation will work the same in Node.js as in the browser:

try {
  const result = mortgageCalculations.mortgagePmt.pmt({
    principal: 200000,
    interestYearly: 6.5,
    term: 30
  });
  handleResult(result);
} catch(err) {
  handleError(err);
}
  1. Make sure that you have Node.js installed. Create a folder for your project, initialize an npm package and install Express as a dependency.
$ mkdir formulator-express-example
$ cd formulator-express-example
$ npm init
$ npm install --save express
  1. Create a file called server.js. Our example is simple so this one file will contain all your server code. The following snippet creates an express app that listens on the port 3000. It also defines a /api/calculate route that will handle POST requests.
const express = require('express');

const app = express();
const port = 3000;

app.post('/api/calculate', (req, res) => {
  // request handling will go here...
});

// start listening
app.listen(port, () => {
  console.log(`Example app started at http://localhost:${port}`);
});
  1. By default express does not parse the body of an HTTP request. We should use the built-in json middleware to setup automatic body parsing of every /api request. Any new route you may define besides calculate will also use this middleware.
// ...

app.use('/api', express.json());

app.post('/api/calculate', (req, res) => {

// ...
  1. As the previous code snippets suggest we will define a generic /api/calculate endpoint. You could of course create a differently named endpoint for every calculation, it’s up to you. In the case of our example we require the request body to match the following format:
{
  "calculation": "group/calculation",
  "params": {
    // input variable values by id
  }
}

To be able to call the calculation requested we will need to split() the string into two parts:

const [calcGroup, calc] = req.body.calculation.split('/');

Load the calculation module at the top of our file:

const express = require('express');
const mortgageCalculations = require('./mortgageCalculations.js');

// ...

With this set up we may invoke the requested calculation and respond with the proper html status codes and values:

try {
  const result = mortgageCalculations[calcGroup][calc](req.body.params);
  res.status(200).send(result);
} catch (err) {
  res.status(500).send(err);
}

Here is the route handler’s source completed with validations:

app.post('/api/calculate', (req, res) => {
  if (!req.body || typeof req.body.calculation !== 'string') {
    return res.status(400).send({
      message: 'missing property "calculation"'
    });
  }
  const [calcGroup, calc] = req.body.calculation.split('/');
  if (!calcGroup || !calc) {
    return res.status(400).send({
      message: 'property "calculation" must include the calculationGroup id and ' +
        'calculation id separated by a slash character e.g.: "group/calculation"'
    });
  }
  if (!mortgageCalculations[calcGroup] || !mortgageCalculations[calcGroup][calc]) {
    return res.status(404).send({
      message: 'calculation not found'
    });
  }
  try {
    const result = mortgageCalculations[calcGroup][calc](req.body.params);
    res.status(200).send(result);
  } catch (err) {
    res.status(500).send(err);
  }
});
  1. Your web server is almost ready. Try it out by starting it and calling it with curl:
$ node server.js

in another terminal:

$ curl --request POST \
  --url 'localhost:3000/api/calculate' \
  --header 'content-type: application/json' \
  --include \
  --data '{
    "calculation": "mortgagePmt/pmt",
    "params": {
      "principal": 200000,
      "interestYearly": 6.5,
      "term": 30
    }
  }'

You should get something like this:

X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 26
ETag: W/"1a-0cc14ZiD4l0TozBEsK3RmNJlyKM"
Date: Fri, 26 Oct 2018 13:00:25 GMT
Connection: keep-alive

{"monthlyPayment":1264.14}
  1. Only one thing is missing from our web server and that is hosting your web client. Create a folder named public and copy over the html, css files from the first iteration Embedding the calculations into a web page. When we run our server it says “Example app started at http://localhost:3000” but only our api is accessible from it. We would like to open localhost:3000 in our browser and see the UI of our application. This can be achieved by setting up a catch-all handler at the end of our route definitions to handle every request that did not match the preceding routes by serving files from our directory named public.
// ...

app.post('/api/calculate', (req, res) => {
  // ...
});

app.use('/', express.static('public'));

app.listen(port, () => {
  console.log(`Example app started at http://localhost:${port}`);
});

If you now start the server and open localhost:3000 in your browser you will be greeted by the UI written earlier.

  1. We’re not finished yet as the client still uses the embedded calculations instead of our shiny new server implementation. First we should write a new function that will perform the HTTP request for us.
async function invokeCalculation(calculation, params) {
  const response = await fetch('/api/calculate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json; charset=utf-8'
    },
    body: JSON.stringify({
      calculation,
      params
    })
  });
  const responseJson = await response.json();
  if (response.ok) { // status is in 200-299
    return responseJson;
  } else {
    throw responseJson;
  }
}

Note

We are using async/await and the Fetch API, two fairly new additions to the language. Make sure you try the example with modern browsers and use transpilation and polyfills for production use.

We will call this function instead of window.mortgageCalculations.mortgagePmt.pmt. The difference is that the previously used function call was synchronous and we must change it to an asynchronous one.

So instead of this:

function onCalculateClick(evt) {
  // ...
  try {
    const result = window.mortgageCalculations.mortgagePmt.pmt({
      principal,
      interestYearly,
      term
    });
    displayResult(result);
  } catch(err) {
    displayErrorMessage(err.message);
  }
}

We will write this:

async function onCalculateClick(evt) {
  // ...
  try {
    const result = await invokeCalculation('mortgagePmt/pmt', {
      principal,
      interestYearly,
      term
    });
    displayResult(result);
  } catch(err) {
    displayErrorMessage(err.message);
  }
}
  1. Now that we are executing calculations server-side we do not need that <script> tag that loads the calculation module in the browser. Remove it from the <head> and we are done.

The finished source for this second iteration is available at GitLab.