Code Widget Fields - Advanced Guide

Extend you apps and sites with custom code.

The purpose of the Buzzy Code Widget fields and components are to give Buzzy Creators an extension point to include custom code, when the out-of-the-box Buzzy components cannot provide capability or interactivity required.

Custom code can be hand written or include 3rd party Javascript libraries. Some examples include charts, calendaring, threeJS. See some examples like Chart/Graphs and Product Market Fit survey

Code Widget fields have 3 modes (Trusted iframe, Embedded Markup & Sandbox iframe), each allowing a different level of customization and configuration options.

Trusted iframe

What is a trusted iframe?

This is the simplest, option allowing you to embed a trusted site into your application. as in addition to some basic formatting options it takes a parameter of an external link to embed. As a trusted iframe. By trusted, this means the external site has full access to the parent (Buzzy) window.

When to use trusted iframes in Buzzy?

You probably will want to use a Trusted iframe when using out-of-the-box 3rd party sites that you trust. the common examples are YouTube, Vimeo etc. But these can be customized on a per app basis.

Besides the basic formatting (eg dimensions) and choosing the URL, you cannot change anything else, as you've handed over control to the 3rd party site you're embedding.

How to use trusted iframes in your Buzzy app?

Each Buzzy app can customize which domains that are trusted.

Reminder - only grant access to sites you Trust!

Important Security Consideration when using Trusted iframes

Do not add a domain to your settings unless you really know that the code is going to be "well behaved" and secure. So ensure it's from a reputable source or written by yourself. If in doubt, avoid this option. Also, be careful that the embedding site does not allow a redirect as part of their API, as this could allow a url to redirect to a nefarious site.

Also, when allowing end-users to enter their own URL (user generated content) to be embedded in an iframe URL, take special care to ensure this is safe. Also otherwise, it's only Owners & Authors of an application that has access to create/configure the Code w\idget.

Be very careful when using Trusted iframes, as they can include code that gets full access to the Buzzy parent window and can operate, via code, like the logged in user.

Q: If it could be potentially bad, why does it exist?

A: Certain sites like youtube require full access to the parent window, so that they can track users, access local storage and so on, they cannot be embedded in a sandboxed secure iframe with , they won't work.

Embedded markup

What is embedded markup

Embedded Markup allows you to add dynamic, via Handlebars HTML and CSS only. Any Javascript and certain tags like <iframe> that could introduce 3rd party code are not allowed and will be stripped.

When to use embedded markup

Use Embedded mark up for quick simple custom code widgets that don't need any extra script. You can use Handlebars in your markup, which allows you to dynamically generate the markup, using data from the Buzzy application to do so.

For example, you could display a table of rows of a datatable, where the rows are dynamically generated based on the content in the datatable.

If you have a Code widget field in a Datatable, the default scope is the row. This allows you to access the row properties/fields with no helpers by directly referencing the fields.

For example, if your row has a Name field you can access this with handlebars like this {{Name}}. So if your Embedded Markup had `Hello {{Name}}!` if the row had a name value of `Bob` then `Hello Bob!` Would be rendered.

In addition to the current rows field, Embedded Mark up has access to the following helpers that can be accessed from Handlebars:

topResourceAndChildren

takes a parameter of the appID , eg {{#with topResourceAndChildren <appid>}} would set the contents of the with blot to the object returned by the `topResourceAndChildren` helper.

The helper returns the JSON object of the entire application and currently available datatables. The return object will look like:

{
    topResource: {
        _id: unique id of app,
        title: title of the app,
        etc...
    },
    children: [ 
        array of resources (datatables and other resources)
        each datatable is an object of: {
            _id: datatable ID (microappID),
            microAppData: [
                array of datatable (previously microapp) rows, for example:
                {
                    name: Bob,
                    age: 50
                    etc...
                },
                {
                    name: Mary,
                    age: 40
                    etc...
                }
            ]
        }
    ]
}

Here are some examples of handlebars code to traverse this returned object

Firstly, here's the data structure of the app: Invoice (datatable) , each invoice has many Invoice Lines and many Sales People

To start with, let's say we wanted to display the app and children (Datatables), the following code could do that:

<p>Invoice Detault Context: {{_id}}</p>
{{#with (topResourceAndChildren "91d1397bc9176813106789d7")}}
  {{#with topResource}}
    <p>The Top Resource Title is {{title}}</p>
  {{else}}
    <p>Top Resource Not Found</p>
  {{/with}}
  <ul>
     {{#each children}}
      <li>child:{{title}}</li>
    {{else}}
      <li>No children found</li>
    {{/each}}
  </ul>
{{/with}}

So, children is array of the child datatables of the app, each of them will contain an array of the actual data.

While ugly (you can use your awesome design skills to make it look awesome ;-) , the result would look like:

So the handlebars is traversing the app and showing each childdatatable from the `children` array. You could then drill into each datatable's microappdata and render that, if needed.

getDataTableRowAndSubTables

When you are using embeddedApp fieldsyou need the ability to drill down into the filtered datatbale for each of the

The getDataTableRowAndSubTables helper takes one parameter, a rowID and it recursively returns an object that includes the row and all the all the rows for each embeddedMicroApp field. This will recurse down the tree of embedded fields, in turn getting all their child rows or any embeddedMicroApp fields. The object returned is:

{
 row: {<the current row as an object>},
 subTables: [<array of objects, each representing an embeddedMicroapp (datatable) field>]
}

As an example to traverse the above Invoice, Invoice Lines and Sales People, see this example

{{#with (getDataTableRowAndSubTables _id)}}
  {{#with row}}
    <p>getDataTableRowAndSubTables Context row:{{_id}} To:{{To}}</p>
  {{/with}}
  {{#with subTables}}
    {{#with Lines}}
      <p>Lines: </p>
      {{#each this}}
        <p> Item: {{row.Item}} Qty: {{row.Qty}}</p>
      {{/each}}
    {{/with}}
    <p>Sales People</p>
    {{#with [Sales People]}}
      <p>Sales People:</p>
      {{#each this}}
        <p> Name: {{row.Name}} Role: {{row.Role}}</p>
      {{/each}}
    {{/with}}
  {{/with}}
{{/with}}

Note {{#with Lines}} where Lines is the (invoice) Lines datatable (embeddedMicroApp Field). So this will render, example again:

Note it's getting the row of a specific Invoice with ID AHYCc... and then it's finding all the Lines: Item A..., Item B..., etc then doing the same for Sales People and listing each sales person, Name: Mary... Again, you can customize the HTML & Handlebars to meet you UX requirements.

In addition to the standard out of the box Handlebars Helpers, the Buzzy Code widget field also provides some add-on helpers to help manipulate and organize data.

The following sets helpers are also available for Web ONLY: math, string, array, comparison, object from the handlebars-helpers package.

The following helpers are also availed for Native: eq (equal), ne (not equal), lt (less than), le (less than and equal), gt (greater than), ge (greater than or equal to), pluck (takes and array object and fieldName param and returns the value, avg (average an array of numbers). These match the syntax for the web, so you can use this subset of helpers that work on both web and native.

Sandbox iframe

What is a Sandbox iframe?

Sandboxed iframes allow you to add custom markup and code to your app, but are sandboxed, for security reason, meaning they don’t have access to the root domain.

When to use Sandbox iframe

The Buzzy Sandbox iframe option on the Code widget field allows you to go further with your custom code by allowing the use of custom Javascript. So you can use both the Embedded Markup, as above, with the extension of custom Javascript.

This is an implementation of an HTML iframe, with the sandbox option set. Specifically allow-same-origin is NOT set, so the sandbox iframe cannot access the parent window, this is because we allow scripts, with the allow-scripts tag set, and you do not want to enable both together as it would be less secure (ref).

Note: You can override these settings, when you publish your app to your own environment (e.g. app.yourdomain.com) but this should be done with caution.

This means that the iframe cannot directly access the parent window where Buzzy is running directly using Javascript. To follow best practices the Buzzy uses the postMessage to send messages between Buzzy and the iframe and addEventListener to listen and receive messages. This works on both Buzzy web and native clients when using the Code widget field/resource.

When any data in Buzzy changes this is provided to the Buzzy iframe via a message. This includes a data object which includes the following parameters:

 { 
   rowJSON: <object of the current row>, 
   resourceJSON: <object representing the full app>, 
   frameToken: <secure token passed from the parent>
 }

The resourceJSON is the same object as described here and now you can traverse it using Javascript.

Example - custom search panel

In this example, there are a list of contacts. Each contact has a name and age field. We wanted to use a Code widget field/resource to create a custom search panel that allows you to search for an age range. For example, give me contacts that are greater than or equal to a number and less than or equal to a number. In this example, there are only 4 contacts in the datatable and it's showing all of them between the ages of 0 and 120

If you put in a query, let's say show me contacts between 10 and 30 (inclusive) it shows 2 results:

Ok, so let's look at how this was, created, firstly the Figma file, we are using a HTML field for the custom search filter

An here's the definition of that Code widget field


<!DOCTYPE html>
<html>
    <head>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap" rel="stylesheet">

        <meta name="viewport" content="width=device-width, initial-scale=1">

        <style>
            /* Minimal reset */
            *, *::before, *::after {
                box-sizing: border-box;
            }
            * {
                margin: 0;
                padding: 0;
                font: inherit;
            }
            html {
                color-scheme: dark light;
            }
            body {
                min-height: 100vh;
            }
            img, picture, svg, video {
                display: block;
                max-width: 100%;
            }

            /* Custom */
            html {
                font-family: 'Poppins', Arial, Helvetica, sans-serif;
                font-size: 16px;
                line-height: 24px;
                color-scheme: dark;
                color: rgba(255,255,255,0.4);
                background: #121D2B;
            }
            .customfilter {
                display: flex;
                flex-direction: row;
                align-items: flex-end;
                gap: 8px;
            }
            .customfilter-section {
                display: flex;
                align-items: center;
                flex: 1 0 0;
                gap: 8px;
            }
            .customfilter label {
                display: inline;
                font-weight: 500;
            }
            .customfilter input {
                display: inline;
                width: 100%;
                color: #FFFFFF;
                background: rgba(255, 255, 255, 0.1);
                border: 2px solid rgba(255, 255, 255, 0.1);
                border-radius: 8px;
                padding: 8px 10px;

                font-weight: 500;
                font-size: 16px;
                line-height: 24px;
            }

            .customfilter-buttons-row {
                display: flex;
                gap: 10px;
                padding: 0 20px 20px 20px;
                overflow: auto;
            }

            .customfilter button {
                color: #B554D7;
                background: rgba(255, 255, 255, 0.1);
                border: 2px solid rgba(255, 255, 255, 0.1);
                border-radius: 8px;
                padding: 8px 20px;
                font-weight: 500;
                font-size: 16px;
                line-height: 24px;
                white-space: nowrap;
                cursor: pointer;
            }
            .customfilter button:disabled {
                color: #FFFFFF;
                background: transparent;
                border: 2px solid #FFFFFF;
                cursor: auto;
                opacity: 0.2;
            }
            
            .customfilter button.button-primary {
                color: #121D2B;
                background: #B554D7;
                border: 2px solid #B554D7;
            }
            .customfilter button.button-primary:disabled {
                /* same as default button */
            }

            /* Helper */
            .sr-only {
                position:absolute;
                left:-10000px;
                top:auto;
                width:1px;
                height:1px;
                overflow:hidden;
            }

        </style>

        <script>
          const SLOT_DURATION = 0.5;
          let appData;
          let initialized = false;
          let params;
          let selectedDate = new Date( new Date().toDateString() + " 12:00:00 AM").getTime();
          let selectedSlot;
         
          console.log('IFRAME WINDOW', window, 'PARENT', parent, 'TOP PARENT', window.top.parent)

          function updateTest(){
            const newAge = 100 * 30;
            window.top.parent.postMessage({
              frameToken:params.frameToken, 
              action:'updatemicroapprow', 
              body: {
                rowID: "b83c900e25c4c12462eee259",
              	rowData:{
              		"Age":newAge
              	}
              } 
            },'*');
            
          }
          function hoursMillis(hours){
            return  Math.ceil(hours * 60 * 60 * 1000);
          };
        
         
          function filterResults(){
            console.log('filterResults', params);
            if (!params) return;
           
            const lowVal = Number(document.getElementById('lowValue')?.value);
            const highVal = Number(document.getElementById('highValue')?.value);
            console.log(`lowVal: ${lowVal} highVal: ${highVal}`);
            window.top.parent.postMessage({
              frameToken:params.frameToken, 
              action:'filterview', 
              body: {
                microAppID:"b83c900e25c4c12462eee259",
                subLimit: 100,
                embeddingRowID:params.rowJSON._id,
                viewFilters:[ {sortVal5: {$gte:lowVal}},{sortVal5: {$lte:highVal}}],
                viewFilterIsMongoQuery: true,
                filterContext: 'My age filter'
                
              } 
            },'*');
           
          }
          
          window.addEventListener('message', function (e) {
           
            var mainWindow = e.source;
            // console.log('EVENT LANDED FROM', e);
            const { data } = e || {};
            appData = data;
              const { rowJSON, resourceJSON, frameToken } = appData || {};
            params = { rowJSON, resourceJSON, frameToken };
   
          });
          
         
        </script>
    </head>
    <body>
        <div class="customfilter">
            <div class="customfilter-section">
                <label for="lowValue" class="sr-only">FROM</label>
                <input type="number" id="lowValue" min="0" value=0>
            </div>
            <div class="customfilter-section">
                <label for="highValue">TO</label>
                <input type="number" id="highValue" value=120>
            </div>
            <button type="button" class="button-primary" onclick="filterResults(this);">Filter</button>
            <br>
            <button type="button" class="button-primary" onclick="updateTest();">Update</button>
        </div>
    </body>
</html>

In this example, it includes the HTML and CSS to make it look pretty. The interesting code is that when you click the filter button it sends a message to Buzzy (parent window), here's the button which calls a function filterResults

 <button type="button" class="button-primary" onclick="filterResults(this);">Filter</button>

The filterResults function, does a few things, it grabs the lowVal and highVal values from the input fields and then it makes a filterview call by doing a postMessage to the parent (Buzzy) window and includes the parameters to filter the results, specifically viewFilters:[ {sortVal5: {$gte:lowVal}},{sortVal5: {$lte:highVal}}]

The viewFilterIsMongoQuery boolean allows you to use MongoDB query language, for example logical and comparison operators, to create complex queries.

For improved performance, it's recommended to use the sortVal, SortVal2...6 and sotValGeometry (for GEO spatial queries).

Note the frameToken:params.frameToken, is passed back, so the parent Buzzy windows that will process this message, knows it's got the right frameToken before processing it. So any messages without a valid frameToken will be ignored.

function filterResults(){
  console.log('filterResults', params);
  if (!params) return;
  
  const lowVal = Number(document.getElementById('lowValue')?.value);
  const highVal = Number(document.getElementById('highValue')?.value);
  console.log(`lowVal: ${lowVal} highVal: ${highVal}`);
  window.top.parent.postMessage({
    frameToken:params.frameToken, 
    action:'filterview', 
    body: {
      microAppID:"b83c900e25c4c12462eee259",
      subLimit: 100,
      embeddingRowID:params.rowJSON._id,
      viewFilters:[ {sortVal5: {$gte:lowVal}},{sortVal5: {$lte:highVal}}],
      viewFilterIsMongoQuery: true,
      filterContext: 'My age filter'
      
    } 
},'*');

In this example, there is no code present in the function to retrieve or display the actual results of the filter as it's handed by Buzzy. If there was a need to use the new filtered data in the HTML iFrame, it will be received asynchronously via a new message picked up by the event listener: window.addEventListener('message',. As above, the display/formatting of the results is done by Buzzy as usaual using a Buzzy view (as defined in Figma) component that has the filterContext: 'My age filter' applied. See the search/filter example for more information.

So the custom HTML code for this example is purely to provide and capture the filter user interface and send it to Buzzy. Besides filterview, the following API calls are supported from the HTML:

fetchembeddedrowdata

used to fetch the embedded datatable rows when you have a row. It requires the following input parameters:

{
    microAppID: <id of the datatable (microapp)>
    embeddingRowID: <the embedding row ID>
    subLimit: <limit, for performance reason, the amoutn of data returned>
    viewFilters: <add custom filters to return a sub-set of data>
    viewSort: <add sort params>
}

For examples of viewFilters and viewSort click their respective links

This call only asks the parent app (Buzzy) to asynchronously send in the new data, as will arrive via the window.addEventListener('message',.

insertmicroapprow - used for inserting into a datatable (microapp) see here

updatemicroapprow - updated row data see here

Also there is an example of this in the HTML above, when the user clicks the update field <button type="button" class="button-primary" onclick="updateTest();">Update</button> which calls the following function:

function updateTest(){
  const newAge = 100 * 30;
  window.top.parent.postMessage({
    frameToken:params.frameToken, 
    action:'updatemicroapprow', 
    body: {
      rowID: "b83c900e25c4c12462eee259",
    	rowData:{
    		"Age":newAge
    	}
    } 
},'*');

removemicroapprow - to delete a row in a datatable see here

Tips/Debugging

  • Ensure the user that is running has the correct access to be able to reach the data. For the HTML code, it will communicate with the wrapping/parent app (Buzzy) and be operating as the currently logged in user.

  • Use the browser console, to see what is being logged out by your debug console.log statements.

Example - Custom Booking application - complex example

The above example, is realtively simple, just passing filter parameters to Buzzy. But what if you wanted to do more in the HTML field like display data, insert, update, remove data...

Here's another scenario, let's say we had a resort and you wanted to allow patrons to make booking s of facilities like a Tennis Court, Jacuzzi or Sauna? While there ar out of the box Javascript calendaring tools, a "booking" scenario is slightly different.

Last updated