📖Guide: HTML & JourneyApps iFrame Client

This article provides best practices and guides on how to import interactive custom HTML iFrames into the JourneyApps runtime using the JourneyApps iFrame Client and the html UI component.

The JourneyApps iFrame Client library is used inside an HTML file and allows your JourneyApps application and the HTML file to communicate with one another. This communication is achieved through combining the following methods in the HTML file and the application's JS/TS.

Example Overview

In the two examples below, we’ll display a line chart using Chart.js containing a count of the US population by year, which is stored in your app's DB. Each of these DB objects will have count, nation, and year fields. We’ll then render the count by year for the United States in an html component.

Data Model & Sample Data

Follow the steps below to set up the data model and fetch the population data which will seed the JourneyApps DB with sample data.

Update your Data Model and include the following model:

schema.xml
<model name="population" label="Population">
    <field name="nation" label="Nation" type="text" />
    <field name="year" label="Year" type="text" />
    <field name="count" label="Count" type="number" />
      
    <display>{nation} {year}</display>
</model>

We'll use the Data USA API to get the population count by year and nation, then save this to DB objects in JourneyApps. Each population object will contain a nation, year, and population count. Start by creating a function called getData in our main.js:

main.js
function getData() {
   var url = "https://datausa.io/api/data?drilldowns=Nation&measures=Population";
   var options = {
       method: "GET",
       headers: {
           "Content-Type": "application/json"
       }
   };

   return fetch(url, options)
       .then(function (response) {
           if (response.status < 200 || response.status >= 300) {
               // Request failed
               throw new Error("Failed to make network request: [" + response.status + "]");
           } else {
               return response.json();
           }
       }).then(function (json) {
           var populationArrary = json.data;
           console.log(JSON.stringify(populationArrary, null, 2));
           for(var i = 0; i < populationArrary.length; i++) {
               notification.info("Syncing " + (i + 1) + "/" + populationArrary.length);
               var population = DB.population.first("nation = ? and year = ?", populationArrary[i].Nation, populationArrary[i].Year);
               if(!population) {
                   population = DB.population.create();
                   population.year = populationArrary[i].Year;
                   population.nation = populationArrary[i].Nation;
                   population.count = populationArrary[i].Population;
                   population.save();
               }
           }
           return notification.info("Request completed");
       }).caught(function (error) {
           dialog("Error fetching population data", error.message);
       });
}

Call the getData function in the init() function of your main.js:

main.js
function init() {
    getData();
}

Basic Example

In this basic example, we’ll create a single HTML file and upload this file into OXIDE for further editing.

Create a new HTML file e.g. chart.html using the following template:

<!DOCTYPE html>
<html lang="en" style="height: 100%;">

    <head>
        <meta charset="UTF-8" />
        <title>File</title>
        <!--Reference to the journey-iframe-client, must have in the file-->
        <script src="https://cdn.jsdelivr.net/npm/journey-iframe-client@0.2.0/dist/bundle.min.js"></script>
        <!--Here is the reference to chart.js -->
        <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.min.js"></script>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0" />
    </head>
    
    <body>
    
    </body>

</html>

Notice that we added two script elements, one includes the JourneyApps iFrame Client and the other includes ChartJS. This is the easiest way to reference the iFrame client in your HTML file.

Upload this file to OXIDE by navigating to the Assets workspace and dragging/dropping the chart.html file created in the previous step into the Assets/html directory as shown on the left-hand side of the Assets panel.

Next, the uploaded file in OXIDE and update the body element of the file to include the following:

chart.html
<body style="height: 100%;">
  <style type="text/css">
      .container {
          display: flex;
          flex-direction: row;
          flex-wrap: wrap;
          justify-content: space-evenly;
      }

      .chart-container {
          position: relative;
          margin: auto;
          height: auto;
          width: max(400px, 30vw);
      }
  </style>
  <h3 id="status" style="font-family: monospace;"> Loading Chart </h3>
  <div class="container">
      <div class="chart-container">
          <canvas id="chart_canvas" width="600" height="600"></canvas>
      </div>
  </div>
  <script>
       var client = new JourneyIFrameClient();
       window.addEventListener('DOMContentLoaded', function () {
           client.post("mounted", true)
               .then(function (result) {
                   if(result == "success") {
                       var status = document.getElementById('status');
                       status.remove();
                   }
               });

           client.on("render", function (chartConfig) {
               var ctx = document.getElementById("chart_canvas");
               var chart = new Chart(ctx, chartConfig)
           });
       });
  </script>
</body>

This update to the body includes the following changes:

  • We added a few style attributes for our chart containers

  • Added an h3 element that displays "Loading Chart", but this gets removed when the JourneyApps Container returns a successful response

  • We defined two div elements. One will act as a container, and another will hold our canvas element. This is where the ChartJS chart will be rendered when the JourneyApps Runtime posts the population data to the iFrame.

  • We create a new script element that initializes the JourneyApps iFrame Client (i.e. JourneyIFrameClient)

  • The script also includes an event listener that fires when the HTML has completely loaded (DOMContentLoaded). This will post a message to the JourneyApps runtime letting it know it’s ready and can start accepting data from the JourneyApps runtime

    • This is done by the following: client.post("mounted", true), which sends a message from the HTML file to the app JS runtime.

    • The app JS listens for messages on “mounted” with a on(“mounted”) method definition and returns true once it receives a message on this ‘channel’

  • Finally, the script includes a JourneyIFrameClient event listener in the HTML file. This tells JourneyIFrameClient to wait for the JourneyApps Runtime to post data to the iFrame. In this case, it will post to the ChartJS configuration object. This is done with the client.on("render") definition.

Now that we have an event listener waiting to receive the ChartJS configuration, we will update the main view’s JS in OXIDE to post data to the JourneyApps iFrame Client. Let’s start by adding the chart config above the init() function as a global view variable:

main.js
var chartConfig = {
   type: 'line',
   data: {
       labels: [],
       datasets: [{
           label: 'US Population',
           data: [],
           backgroundColor: [
               'rgba(255, 99, 132, 0.2)',
               'rgba(54, 162, 235, 0.2)',
               'rgba(255, 206, 86, 0.2)',
               'rgba(75, 192, 192, 0.2)',
               'rgba(153, 102, 255, 0.2)',
               'rgba(255, 159, 64, 0.2)'
           ],
           borderColor: [
               'rgba(255, 99, 132, 1)',
               'rgba(54, 162, 235, 1)',
               'rgba(255, 206, 86, 1)',
               'rgba(75, 192, 192, 1)',
               'rgba(153, 102, 255, 1)',
               'rgba(255, 159, 64, 1)'
           ],
           borderWidth: 1
       }]
   },
   options: {
       scales: {
           yAxes: [{
               ticks: {
                   beginAtZero: true
               }
           }]
       }
   }
};

Next, we will update the init() function to include the following:

main.js
function init() {
   component.html({ id: "frame" }).on("mounted", function (bool) {
       updateConfig();
       component.html({ id: "frame" }).post("render", chartConfig);
       return "success";
   });
}

The update to the init() function includes the following changes:

  • First, we add an on event that listens for the mounted event from the iFrame. Without the event listener, it is not possible to post to the JourneyApps iFrame Client before it has completed the DOMContentLoaded event. In this case, the JourneyApps Runtime will not recognize the iFrame and will fail to post.

  • Once the mounted event is invoked we execute the updateConfig function, which queries the database and updates the config. We’ll add this in the next step.

  • Once that function has completed we post the chartConfig object to the render event that is in the iFrame.

  • We then return success to let the iFrame know that the job is completed, which then removes the "Loading Chart" h3 element.

Next, add the updateConfig function in your main view below the init() function. This function is responsible for retrieving the population data from the DB and updating the chartConfig with the correct labels and data to display in the chart:

main.js
function updateConfig () {
   var populationData = DB.population.all().orderBy("year").toArray();
   var labels = populationData.map(function (population) {
       return population.year;
   });
   var stock_levels = populationData.map(function (population) {
       return population.count;
   });
   chartConfig.data.labels = labels;
   chartConfig.data.datasets[0].data = stock_levels;
}  

Finally, we will add the html UI component to our main view XML to display the iFrame:

main.view.xml
<view title="US Population">
    <html id="frame" src="html/chart.html" show-fullscreen-button="true"/>
</view>

Notice that we added an id attribute with a value of frame. This can be used when you have multiple HTML iFrames and need to target specific post or on events. We also reference this id when targeting the component in the runtime using component.html({ id: "frame" }). Also see this component methods section.

Deploy your app and test the result on your device. The result should look like this:

Tip: To customize the chart, update the chartConfig based on options available on chart.js.

Advanced Example

In this example, similar to the Basic Example above, we’ll be creating the HTML file locally and will then upload it to a JourneyApps application. However, all of the development of the iFrame will be done locally in this advanced guide. We achieve this by defining an HTML file and injecting compiled JavaScript into the file. This is especially helpful when developing more advanced iFrame clients.

Prerequisites

  • Node.js: You can install the latest version directly from the Node.js site, or install the latest version using nvm.

  • Yarn: You can install the latest version of yarn here.

  • Complete the Data Model & Sample Data section of this guide to configure and populate the data used in this example.

Setup

Create a new directory on your machine, initialize a Node.js application and create the basic file/folder structure for the project.

> mkdir chart
> cd chart
> yarn init

Install the journey-iframe-client using yarn

yarn add journey-iframe-client

Create the following file/folder structure

In this example, we are not going to write the JavaScript in our HTML as we did in the basic example. Instead, we are going to write our JS in the index.js file and use webpack to compile our JS and HTML files into a single file, which we will upload to OXIDE.

Update your package.json file and add the following as a script node:

package.json
"scripts": {
   "build": "webpack"
}

Update your package.json with the following to your devDependencies:

package.json
"devDependencies": {
    "@types/node": "^10.11.6",
    "css-loader": "^1.0.0",
    "html-webpack-inline-source-plugin": "^0.0.10",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.9.3",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "superagent": "^4.0.0-beta.5",
    "ts-loader": "^5.2.1",
    "typescript": "^3.1.1",
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2",
    "yaml": "^1.0.0"
}

Run yarn install to update your node modules and install all the devDependencies

Then, dd the following to your webpack.config.js file:

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const yaml = require('yaml');
const fs = require('fs');

const config = yaml.parse(fs.readFileSync('config.yml', 'utf8'));
const {outputFileName, htmlTitle} = config;

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: htmlTitle,
            template: './src/template.html',
            filename: outputFileName,
            inlineSource: '.(js css ts)$'
        }),
        new HtmlWebpackInlineSourcePlugin()
    ],
    resolve: {
        extensions: ['.ts', '.tsx', '.js']
    },
    module: {
        rules: [
            { test: /\.tsx?$/, loader: 'ts-loader' },
            {
                test: /\.scss$/,
                use: [
                    "style-loader", // creates style nodes from JS strings
                    "css-loader", // translates CSS into CommonJS
                    "sass-loader" // compiles Sass to CSS, using Node Sass by default
                ]
            }
        ]
    }
};

You’ll also notice that we added rules to our webpack config. These will allow us to style our iFrame using SASS and we’ve added rules for TypeScript.

Add the following to your config.yml file:

config.yml
outputFileName: output.html
htmlTitle: Template name

Replace the values of the outputFileName and htmlTitle as you see fit. These are used by webpack when the file is compiled and sets the output HTML filename as well as the name of the title of the HTML file.

Development

Open the src/template.html file and add the following:

template.html
<!DOCTYPE html>
<html>

    <head>
        <meta charset="UTF-8">
        <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.min.js"></script>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0" />

        <title>
            <%= htmlWebpackPlugin.options.title %>
        </title>
    </head>

    <body>

    </body>
</html>

If you want to store this source code in a remote repository, please make sure you update your .gitignore file accordingly. Here is an example:

.gitignore
dist
node_modules
.DS_Store

You won’t need to push the dist or node_modules files to your remote repository.

Update the body element of the src/template.html with the following:

template.html
<body>
    <h3 id="status" style="font-family: monospace;"> Loading Chart </h3>
    <div class="container">
        <div class="chart-container">
            <canvas id="chart_canvas" width="600" height="600"></canvas>
        </div>
    </div>
</body>

Update the src/index.js with the following:

index.js
const JourneyIFrameClient = require('journey-iframe-client');
const client = new JourneyIFrameClient();

window.addEventListener('DOMContentLoaded', function () {
    this.console.log('Window has loaded, posting mounted event');
    client.post("mounted", true)
        .then(function (result) {
            if(result == "success") {
                var status = document.getElementById('status');
                status.remove();
            }
        });

    client.on("render", function (chartConfig) {
        var ctx = document.getElementById("chart_canvas");
        var chart = new Chart(ctx, chartConfig);
    });
});

Similar to our Basic Example, we’ll add an event listener that fires when DOMContentLoaded occurs. This will post to the JourneyApps runtime informing it that it has loaded and is ready to accept messages.

Add a few styles to your src/sass/styles.scss:

styles.scss
body {
    height: 100%;
}

.container {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-evenly;
}

.chart-container {
    position: relative;
    margin: auto;
    height: auto;
    width: max(400px, 30vw);
}
  1. Run the yarn build command in the terminal. This will run the webpack command and compile all the JS files, SASS files and inject them into the template.html file. The result is found in the dist/output.html (or whatever you specified as the value for the outputFileName key in the config.yml file).

  2. Upload the compiled HTML file to OXIDE under the Assets workspace within the /html directory.

  3. Tip: Avoid opening the compiled HTML file on OXIDE. This file is the compiled version of your iFrame client and should not be edited in its compiled form.

  4. Follow steps 3 - 7 in the Basic Example of this post. These steps will guide you through the process of adding the HTML component to the view XML, retrieving the sample population data from the database, and finally posting the data to the JourneyApps iFrame Client, which will render the chart and display the data. Please note that if you updated the outputFileName key in the config.yml file, make sure you update the src attribute of the JourneyApps HTML component to reference the correct HTML file uploaded to OXIDE.

  5. Finally, test the result in your app to ensure the data is showing correctly.

The complete source code of this HTML iFrame can be found here.

Last updated