Add current weather to a record

This example assumes you've signed up for an API key from wunderground.com and have a text field in your app (weather_summary below) to store the current weather summary.

NOTE: Weather Underground API is a paid for service. There are some alternatives below. If you use one of these alternatives, you will likely need to reference their API docs to ensure that the calls are properly configured.

  • NWS has a free API.
  • OpenWeatherMap has an API that is free with a 60 calls per minute limit, and the data is <2 hours old.
  • Dark Sky has an API that is free with a 1000 calls per day limit.

Here we're listening for the 'change-geometry' event for a record, and then using the REQUEST function to make an API call to wunderground.com. Once we get the response we parse it as JSON and use SETVALUE to update the form value.

function getWeather() {
  var apiKey = 'your_api_key';

  var options = {
    url: 'https://api.wunderground.com/api/' + apiKey + '/conditions/q/' + LATITUDE() + ',' + LONGITUDE() + '.json'
  };

  REQUEST(options, function(error, response, body) {
    if (error) {
      ALERT('Error with request: ' + error);
    } else {
      var data = JSON.parse(body).current_observation;
      SETVALUE('weather_summary', data.weather + ', ' + data.wind_string);
    }
  });
}

ON('change-geometry', getWeather);

If you only want to fetch weather when explicitly requested by the end user, you can listen for the 'click' event for a hyperlink field. Just add a new hyperlink field to your app and give it a descriptive label, "Tap to Add Weather Data" for example. Leave the default url blank and set up your data event like so.

// This assumes you've still got the getWeather function defined from the example above

ON('click', 'your_hyperlink_field', getWeather)

These examples add basic weather metrics, but many others are available and could be added to multiple fields in your app.

Alert user if photo is not geotagged

This example loops through all the fields in the app and adds an add-photo event to look for location metadata. If latitude and longitude are missing, it will alert the user to enable geotagging on their device and prevent the photo from being attached to the record.

function validateGeotags(event) {
  if (!event.value.latitude || !event.value.longitude) {
    INVALID('This photo is NOT geotagged. Enable photo geotagging on your device and try again.');
  }
}

ON('load-record', function (event) {
  DATANAMES('PhotoField').forEach(function(dataName) {
    ON('add-photo', dataName, validateGeotags);
  });
});

Alert user to take horizontal media

When photos are added to a field named photos, alert the user with a message if they're taken as portrait/vertical (where the width would be less than the height). For videos, we want to check the orientation property.

Note that using INVALID with add-photo or add-video media events prevents the media file from being attached to the record.

ON('add-photo', 'photos', function(event) {
  if (event.value.width < event.value.height) {
    INVALID('Please retake the photo in landscape orientation.');
  }
});

ON('add-video', 'videos', function(event) {
  if (event.value.orientation == 90 || event.value.orientation == -90) {
    OPENURL('https://www.youtube.com/watch?v=Bt9zSfinwFA');
    INVALID('Please retake the video in landscape orientation.');
  }
});

Autoincrement values

IMPORTANT NOTE: It is important to note that this will only work if a single device is being used to collect data. The STORAGE() function only stores data locally on the device/browser being used at the time the function is being used. So if multiple devices/browser are being used this example will NOT work for your use case.

This example will store a value in the local storage on the device when a record is saved. The value stored comes from the field in the record. The next time a record is created on that device/browser the value in storage will be referenced and have 1 added to in and then placed in the field.

var storage = STORAGE();

ON('new-record', function(event) {

  if(storage.getItem('key')){
    var count = storage.getItem('key');
    SETVALUE('field', NUM(count) + 1);
  }
  else {
    SETVALUE('field', 1);
  }

});

ON('save-record', function(event) {
   storage.setItem('key', $field);
});

Auto-populate Last Value

This example can be utilized to pull the value in a field in the last child record and then automatically populate that value into another field in a new child record.

ON('new-repeatable', 'repeatable_section', function(event){
  var last = LAST(REPEATABLEVALUES($repeatable_section, 'field_2'));
  SETVALUE('field_1', last);
});

Basic Timer

This example can be utilized in situations where survey details need to be timed and documented, such as wildlife observations or recording various occurrence durations.

Please consider the Record Auditing Duration metrics which are now captured in your Fulcrum records - if those features may meet your needs for time capture.

The SETTIMEOUT function can also be used for similar situations, such as alerting a user with a message after a specified amount of time.

Timer GIFTimer GIF

Timer GIF

// this Data Event utilizes a Yes/No field (timer) with the N/A choice enabled,
// respectively representing Start/Pause and Reset actions,
// and a readonly Text field (elapsed_time).

var seconds = 0, minutes = 0, hours = 0;

function timer(event) {
  if (VALUE('timer') === 'start') {
    interval = SETINTERVAL(function() {
      seconds++;
      if (seconds >= 60) {
        seconds = 0;
        minutes++;
        if (minutes >= 60) {
          minutes = 0;
          hours++;
        }
      }
      SETVALUE('elapsed_time', (hours ? (hours > 9 ? hours : "0" + hours) : "00") + ":" + (minutes ? (minutes > 9 ? minutes : "0" + minutes) : "00") + ":" + (seconds > 9 ? seconds : "0" + seconds));
    }, 1000);
  } else if (VALUE('timer') === 'pause') {
    CLEARINTERVAL(interval);
  } else if (VALUE('timer') === 'reset') {
    CLEARINTERVAL(interval);
    seconds = 0; minutes = 0; hours = 0;
    SETVALUE('elapsed_time', '00:00:00');
  }
}

ON('change', 'timer', timer);

// IF YOU ARE USING THIS IN A REPEATABLE SECTION, ADD THE FOLLOWING
// ON('new-repeatable', 'repeatable_section', function(event) {
//   seconds = 0, minutes = 0, hours = 0;
// })


Capturing vector coordinates

The examples below demonstrate how to use the CURRENTLOCATION function in conjunction with SETINTERVAL and CLEARINTERVAL to capture the coordinates of simple line or polygon features. This could be used for "digitizing" pipe lengths, pavement features, wetland boundaries, etc. While Fulcrum only currently supports point geometries in the map view, this workflow allows you to capture simple vector geometries for use outside of Fulcrum in various GIS applications.

Capturing A Line Feature

In the example below, our form is setup with the following fields:

  • start_digitizing: Hyperlink button used to trigger GPS logging at the start of the feature
  • stop_digitizing: Hyperlink button used to stop GPS logging the end of the feature
  • line: Text field used to store the line feature coordinates in WKT format.
var lineCoords = [],
  digitizeLine;

ON('click', 'start_digitizing', function(event) {
  lineCoords = [];
  digitizeLine = SETINTERVAL(function() {
    if (CURRENTLOCATION()) {
      lineCoords.push(CURRENTLOCATION().longitude + ' ' + CURRENTLOCATION().latitude);
    }
    SETVALUE('line', 'LINESTRING (' + lineCoords + ')');
  }, 1000);
});

ON('click', 'stop_digitizing', function(event) {
  CLEARINTERVAL(digitizeLine);
});

Capturing A Polygon Feature

In the example below, our form is setup with the following fields:

  • start_digitizing: Hyperlink button used to trigger GPS logging at the start of the feature
  • stop_digitizing: Hyperlink button used to stop GPS logging the end of the feature
  • polygon: Text field used to store the polygon feature coordinates in WKT format.
var polygonCoords = [],
  digitizePolygon;

ON('click', 'start_digitizing', function(event) {
  polygonCoords = [];
  digitizePolygon = SETINTERVAL(function() {
    if (CURRENTLOCATION()) {
      polygonCoords.push(CURRENTLOCATION().longitude + ' ' + CURRENTLOCATION().latitude);
    }
    SETVALUE('polygon', 'POLYGON ((' + polygonCoords + ',' + polygonCoords[0] + '))');
  }, 1000);
});

ON('click', 'stop_digitizing', function(event) {
  CLEARINTERVAL(digitizePolygon);
});

In the above examples, the GPS capture interval is 1 second (1,000 milliseconds). This could be modified to emphasize greater precision (more coordinate pairs) for smaller features- or a longer interval for a more general representation of larger features.

To view the polygon vector geometries in CARTO, you could use the following SQL query, where polygon is the name of the geometry field and repeatable_vector_geometries is the name of the table:

SELECT cartodb_id, ST_Transform (ST_GeomFromText(polygon, 4326), 3857) AS the_geom_webmercator FROM repeatable_vector_geometries

Note also that this workflow should be considered highly experimental and is not officially supported. Capturing lots of coordinates at a fast interval while Fulcrum is running in the background could cause memory and performance issues.


Change a record's status

This example listens for changes to the inspection_date field and updates the record status to 'inspected' if a date value was entered. If the date was cleared (no value), the status reverts to the default state, 'created'.

function changeStatus(event) {
  if (event.value) {
    // There is a value, so set the status
    SETSTATUS('inspected');
  } else {
    // There is no value. It could have been cleared from the field.
    // Revert status to 'created'.
    SETSTATUS('created');
  }
}

ON('change', 'inspection_date', changeStatus);

Another common use case is to just change the record's status any time the record is saved on the mobile device. Here we'll listen to the 'save-record' event and simply set it to 'inspected'.

function changeStatus(event) {
  SETSTATUS('inspected');
}

ON('save-record', changeStatus);

This example sets the status of a record based upon a choice list. In this example damage_type is a single choice field, which will get its values from the Status Field. On a change to damage_type the status is also changed. The damage_type field is required and the Status Field may optionally be hidden, or set to read-only to prevent users from manually overriding it.

This workflow allows you to place the status choice anywhere in your form, and supports visibility and requirement rules.

function setChoices(event) {
  // Get the array of status choice objects
  var statuses = this.form.status_field.choices;
  // Get choice labels and values
  var choices = [];
  for (var i = 0; i < statuses.length; i++) {
    choices.push([statuses[i].label, statuses[i].value]);
  }
  SETCHOICES('damage_type', choices);
}

function changeStatus(event) {
  SETSTATUS(CHOICEVALUE($damage_type));
}

ON('load-record', setChoices);
ON('change', 'damage_type', changeStatus);

Compare photo location to record location

This example demonstrates how to compare the geotagged location of an attached photo with the location of the record to alert the user if there may be an issue.

Photo LocationPhoto Location

Photo Location

// source: https://www.geodatasource.com/developers/javascript
function findDistance(lat1, lon1, lat2, lon2, unit) {
  var radlat1 = Math.PI * lat1 / 180;
  var radlat2 = Math.PI * lat2 / 180;
  var theta = lon1 - lon2;
  var radtheta = Math.PI * theta / 180;
  var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
  dist = Math.acos(dist);
  dist = dist * 180 / Math.PI;
  dist = dist * 60 * 1.1515;
  if (unit == "K") {
    dist = dist * 1.609344;
  }
  if (unit == "N") {
    dist = dist * 0.8684;
  }
  return dist;
}

function validateDistance(event) {
  if (event.value.latitude && event.value.longitude) {
    var kilometers = findDistance(event.value.latitude, event.value.longitude, LATITUDE(), LONGITUDE(), 'K');
    var meters = Math.round(kilometers * 1000);
    if (meters > 20) {
      ALERT('This photo is over 20 meters from the record location. (' + meters + ' meters)');
    }
  }
}

ON('load-record', function(event) {
  // loop through the photo fields
  DATANAMES('PhotoField').forEach(function(dataName) {
    // listen for add-photo event
    ON('add-photo', dataName, validateDistance);
  });
});

Compare photo timestamp with current time

This example demonstrates how to compare the timestamp of an attached photo with the current time to alert the user that they need to use a more recent photo. Using INVALID with the add-photo event prevents the photo from being attached to the record.

function checkTime(event) {
  if (event.value.timestamp) {
    var minutes = Math.round((new Date().getTime() - new Date(event.value.timestamp.replace(/-/g, '/')).getTime()) / 60000);
    if (minutes > 15) {
      INVALID('This photo was taken over 15 minutes ago. Please use a more recent photo.');
    }
  }
}

ON('load-record', function(event) {
  // loop through the photo fields
  DATANAMES('PhotoField').forEach(function(dataName) {
    ON('add-photo', dataName, checkTime);
  });
});

Conditionally persist values across records

This example demonstrates how to persist values across new records depending on a condition being met. Using this, you can achieve similar behavior to the "Default to Previous Value" field setting except you can control the logic used to determine when it's saved for the next record. In this example, we have a Yes/No field with a data name of save_value and a Text Field with a data name of text_field. The Yes/No field controls whether or not to save the value of Text Field with a data name of text_field as the current default value for usage on the next record.

var storage = STORAGE();

// when saving the record, save the value to storage to use next time
ON('save-record', function(event) {
  var shouldPersistValue = false;

  // modify the logic here to save the current values to storage
  if ($save_value === 'yes') {
    shouldPersistValue = true;
  }

  // either save or clear the value, 'last_text_value' is an arbitrary key used to store the variable
  if (shouldPersistValue) {
    storage.setItem('last_text_value', $text_field);
  } else {
    storage.removeItem('last_text_value');
  }
});

ON('new-record', function(event) {
  // when creating a new record, set the value of the text field to the last value used
  SETVALUE('text_field', storage.getItem('last_text_value'));
});

Update CARTO table from Fulcrum

This example demonstrates how to build a SQL statement to create or update records in CARTO. When the Fulcrum record is saved, a POST request is sent to the CARTO SQL API. While you can easily sync your Fulcrum app to CARTO via Data Shares and Synced Tables, or write custom webhooks, this method instantly updates your CARTO table without having to wait for a scheduled CARTO sync or even a Fulcrum sync.

var username = 'fulcrum',
  api_key = 'your-carto-api-key',
  type;

ON('new-record', function(event) {
  type = 'create';
});

ON('edit-record', function(event) {
  type = 'update';
});

ON('save-record', function(event) {
  if (type === 'create') {
    query = 'INSERT INTO fulcrum_points_table (fulcrum_id, name, number, color,  the_geom) VALUES ($$' + RECORDID() + '$$, $$' + $name + '$$, ' + $number + ', $$' + STATUS() + '$$, ST_SetSRID(ST_Point(' + LONGITUDE() + ', ' + LATITUDE() + '),4326))';
    postToCarto(query);
  } else if (type === 'update') {
    query = 'UPDATE fulcrum_points_table SET name=$$' + $name + '$$, number=' + $number + ', color=$$' + STATUS() + '$$, the_geom=ST_SetSRID(ST_Point(' + LONGITUDE() + ', ' + LATITUDE() + '),4326) WHERE fulcrum_id=$$' + RECORDID() + '$$';
    postToCarto(query);
  }
});

function postToCarto(query) {
  var options = {
    url: 'https://' + username + '.carto.com/api/v2/sql?q=' + encodeURIComponent(query) + '&api_key=' + api_key,
    method: 'POST'
  };

  REQUEST(options, function(error, response, body) {
    if (error) {
      ALERT('Error with request: ' + INSPECT(error));
    } else {
      ALERT('This record has been successfully posted to CARTO!');
    }
  });
}

Disable requirements by user or role

The examples below show how to disable requirement validation for all of your form fields, based on a list of user emails or Fulcrum roles.

Email:

ON('load-record', function(event) {
  var emails = ['[email protected]', '[email protected]'];
  if (CONTAINS(emails, EMAIL())) {
    DATANAMES().forEach(function(dataName) {
      SETREQUIRED(dataName, false);
    });
  }
});

Role:

ON('load-record', function(event) {
  if (ISROLE('Owner', 'Manager')) {
    DATANAMES().forEach(function(dataName) {
      SETREQUIRED(dataName, false);
    });
  }
});

Display the current GPS info on the form

This example shows how you can display the current GPS data within your form using the CURRENTLOCATION and SETLABEL functions. For the below example to work, add a Label field and set its label to GPS Info so that we can reference it in the Data Events script as gps_info.

ON('load-record', function(event) {
  var updateLocationInfo = function() {
    // get the current device location
    var location = CURRENTLOCATION();

    // if there is no location, display a special message
    if (!location) {
      SETLABEL('gps_info', 'No Location Available');
      return;
    }

    // format the display of the location data
    var message = [
      'Latitude: ' + location.latitude,
      'Longitude: ' + location.longitude,
      'Accuracy: ' + location.accuracy,
      'Altitude: ' + location.altitude,
      'Course: ' + location.course,
      'Speed: ' + location.speed,
      'Time: ' + new Date(location.timestamp * 1000).toLocaleString()
    ].join('\n');

    // set the label property of the label on the form
    SETLABEL('gps_info', message);
  };

  // go ahead and update it now...
  updateLocationInfo();

  // ... and every 3 seconds
  SETINTERVAL(updateLocationInfo, 3000);
});

Dynamically translate form elements

This example demonstrates how to use the SETLABEL and SETCHOICES functions to dynamically update the form elements when a user selects their language from a choice list. The translations are stored as simple JavaScript objects. Note that in addition to labels and choices, you can also use this method to translate field descriptions with SETDESCRIPTION.

var labels = {
  'name': {
    'Spanish': 'Nombre',
    'French': 'Nom'
  },
  'age': {
    'Spanish': 'Años',
    'French': 'Âge'
  },
  'gender': {
    'Spanish': 'Género',
    'French': 'Sexe'
  }
};

var choices = {
  'gender': {
    'Spanish': [
      {
        'label': 'Varón',
        'value': 'Male'
      },
      {
        'label': 'Hembra',
        'value': 'Female'
      }
    ],
    'French': [
      {
        'label': 'Mâle',
        'value': 'Male'
      },
      {
        'label': 'Femelle',
        'value': 'Female'
      }
    ]
  }
};

function translate(e) {
  var language = CHOICEVALUE($language);
  DATANAMES().forEach(function(dataName) {
    // Update field labels
    if (labels[dataName]) {
      SETLABEL(dataName, labels[dataName][language]);
    } else {
      SETLABEL(dataName, null);
    }
    // Update choice values
    if (choices[dataName] && choices[dataName][language]) {
      SETCHOICES(dataName, choices[dataName][language]);
    } else {
      SETCHOICES(dataName, null);
    }
  });
}

ON('load-record', translate);
ON('change', 'language', translate);

Set the status field options based on role

This example shows how to conditionally control which status options are available based on the user's role. This is useful if you'd like to reserve certain status options for admins only.

For this to work, all of the status options need to be defined in the builder. In this example, we have the following status options:

  • pending - Pending
  • submitted - Submitted
  • approved - Approved
  • completed - Completed

We only want the field users to be able to select pending and submitted.

ON('load-record', function(event) {
  var fieldUserRoles = ['Standard User', 'Custom Field User Role'];

  // if the current role is one of the designated field user roles...
  if (ISROLE(fieldUserRoles)) {
    // set the status field filter
    SETSTATUSFILTER(['pending', 'submitted']);
  }
});

Get elevation information

Similar to automating weather collection, data events allow you to tap into any API that supports point coordinates. This example uses the MapQuest Open Elevation API to determine the elevation at the point you are collecting data. It assumes you have a numeric (integer) field called mq_elevation.

function getElevation() {
  var options = {
    url: 'https://open.mapquestapi.com/elevation/v1/profile',
    qs: {
      key: 'your_api_key',
      shapeFormat: 'raw',
      latLngCollection: LATITUDE() + ',' + LONGITUDE()
    }
  };

  REQUEST(options, function(error, response, body) {
    if (error) {
      ALERT('Error with request: ' + INSPECT(error));
    } else {
      elevation = JSON.parse(body);
      SETVALUE('mq_elevation', elevation.elevationProfile[0].height);
    }
  });
}

ON('change-geometry', getElevation);

Query Esri REST services

This example passes your Fulcrum latitude and longitude as a point parameter in an Intersect query against data hosted on an Esri REST Service. Three text fields are required in the app, for the particular properties we're interested in here.

function getFloodInfo() {

  var options = {
    url: 'https://hazards.fema.gov/gis/nfhl/rest/services/public/NFHLWMS/MapServer/28/query',
    qs: {
      geometry: LONGITUDE() + ',' + LATITUDE(),
      geometryType: 'esriGeometryPoint',
      inSR: '4326',
      spatialRel: 'esriSpatialRelIntersects',
      outFields: '*',
      returnGeometry: false,
      f: 'pjson'
    }
  };

  REQUEST(options, function (err, res, body) {
    if (err) {
      ALERT('Error: ' + err.message);
    } else {
      var result = JSON.parse(body);
      if (result && result.features[0]) {
        SETVALUE('flood_zone', result.features[0].attributes['FLD_ZONE']);
        SETVALUE('flood_zone_subtype', result.features[0].attributes['ZONE_SUBTY']);
        SETVALUE('dfirm_id', result.features[0].attributes['DFIRM_ID']);
      } else {
        SETVALUE('flood_zone', 'NA');
        SETVALUE('flood_zone_subtype', 'NA');
        SETVALUE('dfirm_id', 'NA');
      }
    }
  });
}

ON('change-geometry', getFloodInfo);

Working with inline GeoJSON

This example populates a choice list from an inline GeoJSON Feature Collection. The GeoJSON can also be used as a lookup to set other field values from the properties of the selected feature.

var lookup = {};
var geojson = {
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "properties": {
      "name": "Buffalo"
    },
    "geometry": {
      "type": "Point",
      "coordinates": [-78.881948, 42.881924]
    }
  }, {
    "type": "Feature",
    "properties": {
      "name": "New York"
    },
    "geometry": {
      "type": "Point",
      "coordinates": [-73.981963, 40.751925]
    }
  }, {
    "type": "Feature",
    "properties": {
      "name": "Rochester"
    },
    "geometry": {
      "type": "Point",
      "coordinates": [-77.621896, 43.172371]
    }
  }, {
    "type": "Feature",
    "properties": {
      "name": "Syracuse"
    },
    "geometry": {
      "type": "Point",
      "coordinates": [-76.150014, 43.049994]
    }
  }]
};

ON('load-record', function(event) {
  var features = geojson.features;
  var choices = [];

  for (var i = 0; i < features.length; i++) {
    choices.push({
      label: features[i].properties.name,
      value: features[i].properties.name
    });

    lookup[features[i].properties.name] = {
      lng: features[i].geometry.coordinates[0],
      lat: features[i].geometry.coordinates[1]
    };
  }

  SETCHOICES('geojson_feature', choices);
});

ON('change', 'geojson_feature', function(event) {
  var choice = CHOICEVALUE($geojson_feature);
  SETVALUE('geojson_lat', lookup[choice].lat);
  SETVALUE('geojson_lng', lookup[choice].lng);
});

Query a GeoServer instance

This example demonstrates how to build an OGC Filter to query a GeoServer WFS instance and use the results in a choice list. For this particular example, we want to list all the streets within 100 meters of our current location so we can do some field addressing without having to enter the street names. MassGIS maintains a GeoServer instance which includes roads from the Massachusetts Department of Transportation (MassDOT). We will be querying the massgis:GISDATA.EOTROADS_ARC layer, parsing the results as GeoJSON, and adding the STREETNAME property to a choice list.

ON('change-geometry', function(event) {

  // build spatial "distance within" filter (http://docs.geoserver.org/latest/en/user/filter/filter_reference.html#filter-fe-reference)
  var filter = "<Filter xmlns:gml='http://www.opengis.net/gml'>" +
    "<DWithin>" +
      "<PropertyName>SHAPE</PropertyName>" +
      "<gml:Point srsName='http://www.opengis.net/gml/srs/epsg.xml#4326'>" +
        "<gml:coordinates>" + LONGITUDE() + "," + LATITUDE() + "</gml:coordinates>" +
      "</gml:Point>" +
      "<Distance units='m'>100</Distance>" +
    "</DWithin>" +
  "</Filter>";

  // configure request options
  var options = {
    url: 'https://giswebservices.massgis.state.ma.us/geoserver/wfs',
    qs: {
      request: 'getfeature',
      version: '1.0.0',
      service: 'wfs',
      typename: 'massgis:GISDATA.EOTROADS_ARC',
      propertyname: 'STREETNAME',
      outputformat: 'json',
      filter: filter
    }
  };

  // give the user a loading indicator while it's fetching the data from GeoServer
  PROGRESS('Searching for nearby streets ...');

  // configure the http request
  REQUEST(options, function(error, response, body) {
    PROGRESS();  // dismiss the loading indicator
    if (error) {
      ALERT('Error with request: ' + INSPECT(error));
    } else {
      var data = JSON.parse(body); // parse the JSON response
      var features = data.features; // grab the GeoJSON features
      var streets = []; // array holder for nearby streets
      // loop through the features returned
      for (var i = 0; i < features.length; i++) {
        var streetName = features[i].properties.STREETNAME;
        // if array doesn't already contain streetname, add it
        if (streets.indexOf(streetName) === -1) {
          streets.push(streetName);
        }
      }
      // if we've got some streets, use them in the choice list (sorted alphabetically)
      if (streets.length > 0) {
        SETCHOICES('street_name', streets.sort());
      } else {
        ALERT('No nearby streets found... Are you sure you are in Massachusetts?');
      }
    }
  });
});

Integrating Fulcrum with what3words

what3words is a unique combination of just 3 words that identifies a 3mx3m square, anywhere on the planet. The what3words API provides programmatic access to convert a 3 word address to coordinates (forward geocoding) and to convert coordinates to a 3 word address (reverse geocoding). You can sign up for a free what3words API key at https://what3words.com/select-plan.

The example below demonstrates how to listen for a 'change-geometry' event to automatically update a text field (w3w_address) with the what3words address for the record's location. It also demonstrates using the SETLOCATION function to manually update the record's location from a known what3words address.

Both the getw3w and setw3w functions use the REQUEST function to make an API call to what3words to fetch the info we need. The JSON response from the API is parsed and used to update the Fulcrum record accordingly.

var w3wApiKey = 'my_api_key';

function getw3w() {
  var options = {
    url: 'https://api.what3words.com/v3/convert-to-3wa',
    qs: {
      key: w3wApiKey,
      coordinates: LATITUDE() + ',' + LONGITUDE()
    }
  };

  PROGRESS('Loading', 'Finding the right words...');

  REQUEST(options, function(error, response, body) {
    PROGRESS();
    if (error) {
      ALERT('Error with request: ' + INSPECT(error));
    } else {
      var result = JSON.parse(body);
      SETVALUE('w3w_address', result.words);
    }
  });
}

function setw3w() {
  if ($w3w_address && $w3w_address.split('.') && $w3w_address.split('.').length == 3) {
    var options = {
      url: 'https://api.what3words.com/v3/convert-to-coordinates',
      qs: {
        key: w3wApiKey,
        words: $w3w_address
      }
    };

    PROGRESS('Loading', 'Finding the location...');

    REQUEST(options, function(error, response, body) {
      PROGRESS();
      if (error) {
        ALERT('Error with request: ' + INSPECT(error));
      } else {
        var result = JSON.parse(body);
        if (result.coordinates) {
          SETLOCATION(result.coordinates.lat, result.coordinates.lng);
          ALERT('Succes!', 'Your position has been updated to: ' + result.coordinates.lat + ', ' + result.coordinates.lng);
        } else if (result.error) {
          ALERT(result.error.code, result.error.message);
        }
      }
    });
  } else {
    ALERT('Error', 'A 3 word address must be provided in the following format: index.home.raft');
  }
}

ON('change-geometry', getw3w);
ON('click', 'update_location', setw3w);
var w3wApiKey = 'my_api_key';

function getw3w() {
  var options = {
    url: 'https://api.what3words.com/v2/reverse',
    qs: {
      key: w3wApiKey,
      coords: LATITUDE() + ',' + LONGITUDE()
    }
  };

  PROGRESS('Loading', 'Finding the right words...');

  REQUEST(options, function(error, response, body) {
    PROGRESS();
    if (error) {
      ALERT('Error with request: ' + INSPECT(error));
    } else {
      var result = JSON.parse(body);
      SETVALUE('w3w_address', result.words);
    }
  });
}

function setw3w() {
  if ($w3w_address && $w3w_address.split('.') && $w3w_address.split('.').length == 3) {
    var options = {
      url: 'https://api.what3words.com/v2/forward',
      qs: {
        key: w3wApiKey,
        addr: $w3w_address
      }
    };

    PROGRESS('Loading', 'Finding the location...');

    REQUEST(options, function(error, response, body) {
      PROGRESS();
      if (error) {
        ALERT('Error with request: ' + INSPECT(error));
      } else {
        var result = JSON.parse(body);
        if (result.geometry) {
          SETLOCATION(result.geometry.lat, result.geometry.lng);
          ALERT('Succes!', 'Your position has been updated to: ' + result.geometry.lat + ', ' + result.geometry.lng);
        } else if (result.message) {
          ALERT('w3w message', result.message);
        }
      }
    });
  } else {
    ALERT('Error', 'A 3 word address must be provided in the following format: index.home.raft');
  }
}

ON('change-geometry', getw3w);
ON('click', 'update_location', setw3w);

Hide fields based on user role

This example shows how to conditionally display fields depending on the user's role. Sometimes it's desirable to hide certain portions of the form for certain users because it's either irrelevant or sensitive information.

For this to work, each field you want to make hidden should have the 'Hidden' checkbox checked in the app builder so that the field is hidden by default. It's desirable to have the logic be based on who can see it, rather than who cannot see it.

ON('load-record', function(event) {
  var adminRoles = ['Owner', 'Manager', 'Custom Admin Role'];

  // if the current role is one of the designated admin roles...
  if (ISROLE(adminRoles)) {
    // make the fields visible
    SETHIDDEN('sensitive_field_1', false);
    SETHIDDEN('sensitive_field_2', false);
  }
});

Opening Google Maps & Street View

Google Street View provides panoramic views from positions along many streets in the world. This example assumes you have a Hyperlink field with a data name of show_street_view and shows how you can script a click event to directly open Street View at your record location when the button is tapped.

ON('click', 'show_street_view', function (event) {
  if (LATITUDE() && LONGITUDE()) {
    OPENURL('https://maps.google.com/maps?layer=c&cbll=' + LATITUDE() + ',' + LONGITUDE());
  } else {
    ALERT('No location provided!', 'A location is required to show Street View.')
  }
});

Note that the Google Maps app must be installed for this to work on mobile.

Alternatively if you would like to use regular Google Maps for routing to a record location, this example can be used with a Hyperlink field having a data name open_google_maps and uses a click event to directly open the Google Maps app at your record location when the button is tapped.

ON('click', 'open_google_maps', function (event) {
  if (LATITUDE() && LONGITUDE()) {
    OPENURL('https://maps.google.com/?q=' + LATITUDE() + ',' + LONGITUDE());
  } else {
    ALERT('No location provided!', 'A location is required to show Google Maps.')
  }
});

Make a phone call

This example creates a button on your form that makes a phone call when it's tapped. The phone number to call is pulled from another field on the form. It assumes you have a text field to enter the phone number with a data name of phone_number and a hyperlink field with a data name of call_phone. It also includes a check for the platform since the web browser does not support calling phone numbers.

ON('click', 'call_phone', function(event) {
  if (!EXISTS($phone_number)) {
    ALERT('You must enter a phone number.');
    return;
  }

  if (!ISMOBILE()) {
    ALERT('Only mobile devices support making phone calls.');
    return;
  }

  OPENURL('tel:' + $phone_number);
});

Verify special choice option

This script checks the value of a choice field for a specific option. If the specific option is chosen, it confirms with the user the intent to select that option. This can be useful if some choice options within a choice field are reserved for special cases where extra precaution might be necessary to prevent accidental entry.

ON('change', 'severity', function (event) {
  if (CHOICEVALUE($severity) === 'Critical') {
    var options = {
      title: 'Warning',
      message: 'You selected a damage severity level of Critical. Are you sure you want to do this?',
      buttons: ['Yes, Critical', 'No']
    };

    MESSAGEBOX(options, function(result) {
      if (result.value !== 'Yes, Critical') {
        // Clear the field if the user answers No
        SETVALUE('severity', null);
      }
    });
  }
})

Repeatable requirement validation alerts

This example will populate the warning box that is typically displayed when attempting to save a record that has empty required fields with more information about which child record has no value in a field when saving the root level record.

$repetable_section should be replaced with the repeatable section being used and repeatable_field should be replaced with the field that you wish to have the alert work for. The text ('Repeatable Section record' and ' is missing a repeatable field value.') in the INVALID('Repeatable Section' + index + ' is missing a repeatable field value.') should also be replaced with the section's name and the field with the requirement.

ON('validate-record', function (event) {
 var conditions = REPEATABLEVALUES($repetable_section, 'repeatable_field');
  for (var i = 0; i < conditions.length; i++) {
    if (conditions[i] === undefined) {
      var index = i+1;
      INVALID('Repeatable Section record' + index + ' is missing a repeatable field value.');
    }
  }
})

Note: If the fields are still set as required though the app designer, both the system and the data event alerts will be displayed when you save the root level record. Removing the system field requirements will result in a cleaner alert message when saving the root level record, but will also remove the alert presented when saving the child record in the repeatable section.


Phone number validation

If you're using Fulcrum to collect contact information, the Data Events REQUEST function can be used to validate phone numbers against various API's. This can help ensure you collect good data instantly at the point of collection, instead of discovering later that the information is incorrect.

Twilio offers a REST API that allows you to lookup meta-data about phone numbers and much more. The example below shows how to return the CallerName property from phone numbers within the United States which are registered in the CNAM database. Twilio also offers support for International numbers.

This example also shows an implementation of HTTP transaction basic access authentication (a method for an HTTP user agent to provide a user name and password when making a request).

var accountSid = 'your_account_sid',
    authToken = 'your_auth_token';

//b2a is an alternative of the btoa function and creates a base-64 encoded ASCII string from a "string" of binary data. Source: https://gist.github.com/JavaScript-Packer/6a00b61b270f387e2453
function b2a(a) {
    var c, d, e, f, g, h, i, j, o, b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
        k = 0,
        l = 0,
        m = "",
        n = [];
    if (!a) return a;
    do c = a.charCodeAt(k++), d = a.charCodeAt(k++), e = a.charCodeAt(k++), j = c << 16 | d << 8 | e,
        f = 63 & j >> 18, g = 63 & j >> 12, h = 63 & j >> 6, i = 63 & j, n[l++] = b.charAt(f) + b.charAt(g) + b.charAt(h) + b.charAt(i); while (k < a.length);
    return m = n.join(''), o = a.length % 3, (o ? m.slice(0, o - 3) : m) + '==='.slice(o || 3);
}

//verify_with_twilio is a hyperlink field. You may want to consider using regex validation for phone_number in a text field but a numeric field works too.
ON('click', 'verify_with_twilio', function(event) {
    var options = {
        url: 'https://lookups.twilio.com/v1/PhoneNumbers/' + $phone_number,
        qs: {
            'Type': 'carrier',
            'Type': 'caller-name'
        },
        headers: {
            'Authorization': 'Basic ' + b2a(accountSid + ':' + authToken)
        }
    };
    REQUEST(options, function(error, response, body) {
        if (error) {
            ALERT('Error with request: ' + error);
        } else {
            var data = JSON.parse(body);
            //twilio_verification is a read-only text field which begins as hidden. We unhide it with SETHIDDEN if there is data returned.
            if (data && data.caller_name && data.caller_name.caller_name) {
                SETVALUE('twilio_verification', data.caller_name.caller_name);
                SETHIDDEN('twilio_verification', false);
            } else if (data.caller_name.caller_name === null) {
                //There may not be a CallerName registered for that number, but let's try returning Carrier name and type.
                ALERT('Sorry, Twilio does not have a name associated with this number. It appears to be a ' + data.carrier.type + ' number with the ' + data.carrier.name + ' carrier however.');
            } else if (data.status == 404) {
                //Alert the user if the requested resource was not found.
                ALERT(data.message);
            } {

            }
        }
    });
});

Point-in-polygon with CARTO API

Data events allow you to grab information about a point's location, i.e. which polygon the point lies in.

This example illustrates how to set up two data events. Both take advantage of CARTO's querying ability through the SQL API. The first one uses a nearest neighbor query to find the nearest brewery. The second example pulls in the name of the neighborhood polygon in which the point is contained.

These examples require having data stored in CARTO tables. If you are unsure of what the query should be returning, we recommend testing out example data:

https://denverstartup.carto.com/api/v2/sql?q=SELECT%20*%20FROM%20sanfran_neighborhoods%20where%20ST_Contains(the_geom,%20ST_GeomFromText(%27POINT(-122.4613380%2037.78048)%27,4326))

which returns:

{
    "rows": [{
        "cartodb_id": 801,
        "the_geom": "0106000020E61...",
        "the_geom_webmercator": "0106000020110F...",
        "restaurants": 147,
        "city": "San Francisco",
        "county": "San Francisco",
        "name": "Inner Richmond",
        "bike_park_count": 46,
        "bike_share_count": 0,
        "co_working": 0,
        "bus_stops": 131,
        "total": 324
    }]
}

Tip: To keep data private, you will want to use &api={secret_api_key} at the end of the URL

function nearestBrewery() {
    var query = 'SELECT * FROM denver_breweries ORDER BY the_geom <-> ST_Transform(CDB_LatLng(' + LONGITUDE() + ',' + LATITUDE() + '),4326) LIMIT 60';

    var options = {
    url: 'https://denverstartup.carto.com/api/v2/sql',
    qs: {
      f: 'geojson',
      q: query
    }
  };

  REQUEST(options, function(error, response, body) {
    if (error) {
      ALERT('Error with request: ' + error);
    } else {
      var data = JSON.parse(body);
      SETVALUE('nearest_brewery', data.rows[0].name);
    }
  });
}
ON('save-record', nearestBrewery);
ON('edit-record', nearestBrewery);
ON('click', 'nearest_brewery', nearestBrewery);

function identifyNeighborhood() {
    var query = "SELECT * FROM denver_neighborhoods WHERE ST_Contains(the_geom, ST_GeomFromText('POINT("+ LONGITUDE() + " " + LATITUDE()+ ")', 4326));"

    var options = {
    url: 'https://denverstartup.carto.com/api/v2/sql',
    qs: {
      f: 'geojson',
      q: query
    }
  };

  REQUEST(options, function(error, response, body) {
    if (error) {
      ALERT('Error with request: ' + error);
    } else {
      var data = JSON.parse(body);
      SETVALUE('neighborhood', data.rows[0].nbhd_name);
    }
  });
}
ON('save-record', identifyNeighborhood);
ON('edit-record', identifyNeighborhood);
ON('click', 'neighborhood', identifyNeighborhood);

Prevent changes after signing

This example shows how to set all of your form fields to read-only once a signature has been added to the record.

ON('edit-record', function (event) {
  if ($signature) {
    DATANAMES().forEach(function(dataName) {
      SETREADONLY(dataName, true);
    });
  }
});

Prevent users from uploading photos from the device gallery

This example will enforce users to take a new photo instead of uploading photos from the gallery.

ON('load-record', function() {  
  var config = {
    media_gallery_enabled: false
  };
 SETFORMATTRIBUTES(config);
});

Send push notifications with Pushbullet

This example demonstrates integrating Fulcrum with Pushbullet to automatically send a push notification when a record is saved. It uses the Pushbullet API to create a push notification, which includes some record data and a link to the record location on Google Maps. This is useful when you need immediate notifications from the field without having to wait for syncing.

ON('save-record', function (event) {
  var options = {
    url: 'https://api.pushbullet.com/v2/pushes',
    method: 'POST',
    headers: {
      'Access-Token': 'my-access-token',
      'Content-Type': 'application/json'
    },
    json: {
      'email': '[email protected]',
      'title': 'Fulcrum record saved in: ' + this.form.name,
      'body': USERFULLNAME() + ' just saved a record with a status of: ' + STATUS(),
      'type': 'link',
      'url': 'http://maps.google.com/maps?q=loc:' + LATITUDE() + ',' + LONGITUDE() + ' (' + $name + ')'
    }
  };

  REQUEST(options, function(error, response, body) {
    if (error) {
      ALERT('Error: ' + INSPECT(error));
    }
  });
});

Fetch data from a Socrata service

Socrata is a popular cloud-based platform for enabling government organizations to make their data available online. Sites like NYC OpenData and medicare.gov leverage Socrata to provide access to tremendously valuable public datasets. While Socrata makes it easy to visualize and export these datasets, it also provides a powerful API for programmatically accessing this data. The Socrata Open Data API (SODA) has an SQL-like query language called the "Socrata Query Language" or “SoQL”. Datasets with location information can be spatially queried so you can retrieve information on the features near you.

The example below demonstrates how to query for the 5 closest Nursing Homes from the Nursing Home Provider Info table on the data.medicare.gov site, which is powered by Socrata.

var nearbyNursingHomes; // global variable to hold data returned by query

// listen for geometry changes
ON('change-geometry', function(event) {

  // build the url passing our location to the special SoQL `distance_in_meters` spatial function, including a "distance" column in miles
var options = {
    url: 'https://data.medicare.gov/resource/b27b-2uc7.json',
    qs: {
      '$order': "distance_in_meters(location, 'POINT (" + LONGITUDE() + " " + LATITUDE() + ")')",
      '$limit': 5,
      '$select': "*, distance_in_meters(location, 'POINT (" + LONGITUDE() + " " + LATITUDE() + ")') * 0.000621371 AS distance"
    }
  };

  // give the user a loading indicator while it's fetching the data from Socrata
  PROGRESS('Searching for nearby Nursing Homes...');

  // make the http request
  REQUEST(options, function(error, response, body) {
    PROGRESS(); // dismiss the loading indicator
    if (error) {
      ALERT('Error with request: ' + INSPECT(error));
    } else {
      var data = JSON.parse(body); // parse the JSON response
      var choices = []; // array holder for choices
      nearbyNursingHomes = data; // set the global variable with the data returned from the query
      // if data is returned, loop through the records and push them into the choice array
      if (data && data.length > 0) {
        for (var i = 0; i < data.length; i++) {
          choices.push({
            label: data[i].provider_name + ' (' + Number(data[i].distance).toFixed(1) + ' miles)',
            value: data[i].federal_provider_number // set the choice value to a unique value
          });
        }
        SETCHOICES('nearby_nursing_homes', choices);
      } else {
        ALERT('No data found.');
      }
    }
  });
});

// listen for the choice list change
ON('change', 'nearby_nursing_homes', function(event) {
  // loop through the list of nearby Nursing Homes
  for (var i = 0; i < nearbyNursingHomes.length; i++) {
    var nh = nearbyNursingHomes[i];
    // if the unique id matches our choice value, fill in the form values
    if (event.value && nh.federal_provider_number == event.value.choice_values[0]) {
      SETVALUE('nursing_home_name', nh.provider_name);
      SETVALUE('federal_provider_number', nh.federal_provider_number);
      SETVALUE('ownership_type', nh.ownership_type);
      SETVALUE('number_of_certified_beds', nh.number_of_certified_beds);
      SETVALUE('number_of_residents_in_certified_beds', nh.number_of_residents_in_certified_beds);
      SETVALUE('overall_rating', nh.overall_rating);
      SETVALUE('health_inspection_rating', nh.health_inspection_rating);
      SETVALUE('qm_rating', nh.qm_rating);
      SETVALUE('staffing_rating', nh.staffing_rating);
      SETVALUE('rn_staffing_rating', nh.rn_staffing_rating);
      SETVALUE('reported_cna_staffing_hours_per_resident_per_day', nh.reported_cna_staffing_hours_per_resident_per_day);
      SETVALUE('expected_cna_staffing_hours_per_resident_per_day', nh.expected_cna_staffing_hours_per_resident_per_day);
      SETVALUE('number_of_facility_reported_incidents', nh.number_of_facility_reported_incidents);
    }
  }
});

Make fields read-only based on user role

This example shows how to conditionally disable fields depending on the user's role. Sometimes it's desirable to disable portions of the form for certain users.

For this to work, each field you want to make disabled should have the 'Read Only' checkbox checked in the app builder so that the field is disabled by default. If you leave the fields enabled in the builder you will need to negate the logic in the example.

ON('load-record', function(event) {
  var adminRoles = ['Owner', 'Manager', 'Custom Admin Role'];

  // enable the fields if the current role is one of the designated admin roles...
  if (ISROLE(adminRoles)) {
    // make some fields editable by turning off the read-only flag
    SETREADONLY('field_1', false);
    SETREADONLY('field_2', false);
    SETREADONLY('@status', false); // @status is the special data name for the status field
  }
});

Make all fields within a section read-only

This example uses the FIELDNAMES function to grab the fields within a section and set them to read-only if the record is being updated. The sections: true option includes fields within nested sections.

ON('load-record', function (event) {
  if (ISUPDATE()) {
    var dataNames = FIELDNAMES('my_section', {sections: true, repeatables: false});
    dataNames.forEach(function(field) {
      SETREADONLY(field, true);
    });
  }
});

Require field based on date comparison

This example uses the DATEADD and SETREQUIRED functions to compare today's date with the start_date date field. This example assumes start_date was entered in a previous version of the record, so we can listen to the load-record event, add 7 days to the start_date value and if the current date is a week or more later, we require the end_date field be filled out.

ON('load-record', function (event) {
  var today = new Date();
  if (today >= DATEADD($start_date, 7)) {
    SETREQUIRED('end_date', true);
  }
});

This works well in cases where you are revisiting an existing record, but you could also use validate-record to compare dates for the current version.

ON('validate-record', function (event) {
  var today = new Date();
  if (today >= DATEADD($start_date, 7)) {
    INVALID('End Date is required!');
  }
});

Require captions for a photo field

This example uses the validate-record event in conjunction with the INVALID function to prevent saving a record if any photos in the photos field are missing captions.

ON('validate-record', function (event) {
  // if there are any photos, loop through the $photos objects and test the caption property for null
  if ($photos) {
    for (var i = 0; i < $photos.length; i++) {
      // if caption is missing, alert the user and prevent the record from saving
      if ($photos[i].caption === null) {
        INVALID('All photos must have captions!');
      }
    }
  }
});

This can be expanded to look through all photo fields in your app

ON('validate-record', function (event) {
  // loop through the photo fields
  DATANAMES('PhotoField').forEach(function(dataName) {
    // get the photo field value
    var photos = VALUE(dataName);
    // if there are any photos, loop through the photo objects and test the caption property for null
    if (photos) {
      for (var i = 0; i < photos.length; i++) {
        // if caption is missing, alert the user and prevent the record from saving
        if (photos[i].caption === null) {
          INVALID('All photos must have captions!');
        }
      }
    }
  });
});

Require project

This example uses the validate-record event in conjunction with the INVALID function and PROJECTNAME expression to prevent saving if the user has not associated a project with the record.

ON('validate-record', function (event) {
  if (!PROJECTNAME()) {
    INVALID('Please select a project before saving.');
  }
});

Array of fields with certain settings

This example can be used to create an array of all the fields that have their field options enabled for required, hidden, or read-only when a record is loaded.

This array can then be used to make these fields not be in this state or event back to this state under defined conditions within other data events.

The example below is for fields that are set to be required. If you wish to obtain an array of the fields that are set to be hidden element.required can be changed to element.hidden. If you wish to obtain an array of the fields that are set to be read-only element.required can be changed to element.disabled.

var requiredFields = [];

ON('load-record', function(event){
  var elements = this.elements;
  elements.forEach(function(element){
    if(element.required == true){
      requiredFields.push(element.data_name);
    }
  });
});

Send a text message

This example creates a button on your form that sends an SMS when it's tapped. The phone number to send it to is pulled from another field on the form. It assumes you have a text field to enter the phone number with a data name of phone_number and a hyperlink field with a data name of send_sms. It also uses a text field with the data name sms to use as the body text of the SMS message. This is just an example, so you can also build the text dynamically using any other form fields or functions. It also includes a check for the platform since the web browser does not support sending text messages.

ON('click', 'send_sms', function(event) {
  if (!EXISTS($phone_number)) {
    ALERT('You must enter a phone number.');
    return;
  }

  if (!ISMOBILE()) {
    ALERT('Only mobile devices support making phone calls.');
    return;
  }

  // iOS uses the '&' character to separate the number from the sms text
  // Note that not all SMS apps on Android support passing the body text.
  if (PLATFORM() === 'iOS') {
    OPENURL(FORMAT('sms:%s&body=%s',
      $phone_number, encodeURIComponent($sms)));
  } else {
    OPENURL(FORMAT('sms:%s?body=%s',
      $phone_number, encodeURIComponent($sms)));
  }
});

Add a tally counter to your form

This example shows how to add a tally counter to your form. It displays a button on the form that increments a numeric field when tapped.

For this to work, you will need a numeric field with the data name tally_count and a hyperlink field with the data name of increment.

ON('click', 'increment', function(event) {
  // increment the numeric field by 1
  SETVALUE('tally_count', ($tally_count || 0) + 1);
});

Another approach to a tally counter uses a numeric field with the data name tally_count and a Default Value of 0. Also used is a Yes/No field with the data name of tally with these substitutions below:

Tally example Yes/No field settingsTally example Yes/No field settings

Tally example Yes/No field settings

Tally GIFTally GIF

Tally GIF

ON('change', 'tally', function (event) {
  if ($tally == 'add') {
    SETVALUE('tally_count', $tally_count + 1);
    SETVALUE('tally', null);
  }
  if ($tally == 'subtract') {
    SETVALUE('tally_count', $tally_count - 1);
    SETVALUE('tally', null);
  }
});

Prevent editing an old record

This example listens for record edits and compares the current timestamp with the created_at timestamp of the record. If the record was created more than 30 days ago, the user receives an ALERT that the record can no longer be edited and prompts them to create a new record. The record is also marked as INVALID so edits cannot be saved.

function daysBetween(date1, date2) {
  // The number of milliseconds in one day
  var oneDay = 1000 * 60 * 60 * 24;
  // Convert both dates to milliseconds
  var date1MS = date1.getTime();
  var date2MS = date2.getTime();
  // Calculate the difference in milliseconds
  var differenceMS = Math.abs(date1MS - date2MS);
  // Convert back to days and return
  return Math.round(differenceMS/oneDay);
}

ON('edit-record', function (event) {
  var now = new Date();
  var createdAt = new Date(this.featureCreatedAt * 1000);
  var age = daysBetween(now, createdAt);
  if (age > 30) {
    var message = 'This record was created over 30 days ago and can no longer be edited. Please create a new record';
    ALERT('Warning!', message);
    ON('validate-record', function (event) {
     INVALID(message);
    });
  }
});

Intelligently updating record locations

This example demonstrates how to update the location of a record when it meets the following criteria:

  • It is saved on mobile
  • It was not originally captured by GPS
  • Your current location is less than 100 meters from record location

This is useful for automatically updating the location of records which may have been imported with inaccurate coordinates. This can add value to your datasets without any additional effort in the field.

var currentLatitude, currentLongitude;

// source: https://www.geodatasource.com/developers/javascript
function findDistance(lat1, lon1, lat2, lon2, unit) {
  var radlat1 = Math.PI * lat1 / 180;
  var radlat2 = Math.PI * lat2 / 180;
  var theta = lon1 - lon2;
  var radtheta = Math.PI * theta / 180;
  var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
  dist = Math.acos(dist);
  dist = dist * 180 / Math.PI;
  dist = dist * 60 * 1.1515;
  if (unit == 'K') {
    dist = dist * 1.609344;
  }
  if (unit == 'N') {
    dist = dist * 0.8684;
  }
  return dist;
}

ON('load-record', function(event) {
  var updateLocationInfo = function() {
    // get the current device location
    var location = CURRENTLOCATION();
    // if there is no location, stop
    if (!location) {
      return;
    }
    // set current location values
    currentLatitude = location.latitude;
    currentLongitude = location.longitude;
  };
  // go ahead and update it now...
  updateLocationInfo();
  // ... and every 3 seconds
  SETINTERVAL(updateLocationInfo, 3000);
});

ON('save-record', function(event) {
  // check to see if initial record location was not captured by GPS (imported or created on the web)
  var recordAccuracy = this.recordHorizontalAccuracy;
  if (!recordAccuracy && currentLatitude && currentLongitude) {
    var kilometers = findDistance(currentLatitude, currentLongitude, LATITUDE(), LONGITUDE(), 'K');
    var meters = Math.round(kilometers * 1000);
    // if record was not captured by GPS and current location is less than 100 meters from record location, update with current location
    if (meters < 100) {
      SETLOCATION(currentLatitude, currentLongitude);
    }
  }
});

Validate record is within geographic area

This example uses the INVALID, LATITUDE, and LONGITUDE functions to keep records from being saved when their geometry isn't within the state of Colorado, a conveniently rectangular state.

function validateLocation() {
  // The rough bounds of Colorado
  var minLatitude = 36.985;
  var maxLatitude = 40.979;
  var minLongitude = -109.028;
  var maxLongitude = -102.063;

  // The latitude and longitude of the record
  lat = LATITUDE();
  lng = LONGITUDE();

  if (!(lat <= maxLatitude && lat >= minLatitude && lng <= maxLongitude && lng >= minLongitude)) {
    INVALID("It looks like this record isn't within the State of Colorado. Please adjust the record's location to be within Colorado.");
  }
}

ON('validate-record', validateLocation);

Enforce regex pattern validation by role

This example uses the ISROLE and INVALID functions to programmatically validate a text field against a regex pattern based on the user role. This could be used to force field user comments to be more succinct while allowing manager comments to be more detailed.

ON('validate-record', function(event) {
  if (ISROLE('Standard User', 'Field Crew')) {
    var str = $name;
    var re = /^[A-Za-z]{5}$/;
    var result = re.test(str);
    if (result == false) {
      INVALID('The value for the Name field must be exactly 5 characters');
    }
  }
});

Validate yes/no in repeatable and set status

This example will loop through yes/no fields throughout the form. It will total up the number of yes responses in your form which can be used in another calculation. Another option is to use it to set the status. If no is the choice made on any yes/no field within the repeatable section then the status of the record is set to no, otherwise the status is set to yes.

var yesNoResponse = [];

function findYesNoQuestions(repeatableField, array, parent){
  var fields = FIELDNAMES(repeatableField);
  for(var i = 0; i < fields.length; i++){
    if(FIELDTYPE(fields[i]) == 'YesNoField'){
      yesNoResponse.push(REPEATABLEVALUES(VALUE(parent), FLATTEN(ARRAY(FLATTEN(array), fields[i]))));
      if(i == fields.length - 1){
        array.pop();
      }
    }
    if(FIELDTYPE(fields[i]) == 'Repeatable'){
      array.push(fields[i]);
      findYesNoQuestions(fields[i], array, parent);
    }
  }
}

ON('validate-record', function(event){
  var count = 0;
  yesNoResponse = [];
  DATANAMES().forEach( function(element){
    if(FIELDTYPE(element) == 'Repeatable'){
      if(VALUE(element)){
        var fieldArray = [];
        findYesNoQuestions(element, fieldArray, element);
      }
    }
    else if(FIELDTYPE(element) == 'YesNoField'){
      if(VALUE(element)){
        yesNoResponse.push(VALUE(element));
      }
    }
  });

  //Gets count of yes responses to be used in a calculation
  FLATTEN(yesNoResponse).forEach(function(element){
    if(element == 'yes'){
      count++;
    }
  });
  SETVALUE('num', count);

  //Sets status of record if a no answer is found.
  /*
  if(CONTAINS(yesNoResponse, 'no'))
    SETSTATUS('no');
  else
    SETSTATUS('yes');

*/
});

Create a summary field of comments entered when record is edited multiple times

This example will create a summary field to keep track of comments past users have entered. You will need to add a text field (read only optional) to your form. To utilize the below code you will name the summary text field summary and have a text field called comment.

ON('save-record', function(event){
  var name = USERFULLNAME();
  var time = TIMESTAMP();
  if($summary){
    var temp = $summary;
    SETVALUE('summary', temp + CONCAT(name, ' at ', time, ' : ', $comment, '\n'));
  }
  else
  {
    SETVALUE('summary', CONCAT(name, ' at ', time, ' : ', $comment, '\n'));
  }
});

Check fields in sections for values and either add a checkmark or a X to the section label

When the 'Check Fields' hyperlink field is clicked the code will check the values in fields within a section three different ways. If the fields have values then a checkmark will be added to the section label, otherwise, a X will be added to the section label.

ON('click', 'check_fields', function (event) {
    //Check if all fields within a section have values.
    var section1 = FIELDNAMES('section_1');
    var check1 = [];

    section1.forEach(function (element) {
        if (VALUE(element)) {
            check1.push('y');
        } else {
            check1.push('n');
        }
    });

    if (CONTAINS(check1, 'n')) {
        //if not all fields have values add a X to the section label
        SETLABEL('section_1', String.fromCharCode(10060) + ' Section 1');
    } else {
        //if all fields have values add a checkmark to the section label
        SETLABEL('section_1', String.fromCharCode(9989) + ' Section 1');
    }

    //Check specific fields in a section to see if they have values
    if ($yesno2 && $text2) {
        //if fields have values add a checkmark to the section label
        SETLABEL('section_2', String.fromCharCode(9989) + ' Section 2');
    } else {
        //if fields do not have values add a X to the section label
        SETLABEL('section_2', String.fromCharCode(10060) + ' Section 2');
    }

    //Check specific field types within a section for values
    var section3 = FIELDNAMES('section_3');
    var check1 = [];

    section3.forEach(function (element) {
        if (VALUE(element) && FIELDTYPE(element) == 'YesNoField') {
            //if a yes/no field has a value add a checkmark to the section label
            SETLABEL('section_3', String.fromCharCode(9989) + ' Section 3');
        } else {
            //if a yes/no field does not have a value add a X to the section label
            SETLABEL('section_3', String.fromCharCode(10060) + ' Section 3');
        }
    })
});

How to show hyperlink field's default URL

When viewing record's hyperlink field on record dashboard, it does not show the URL that is set to default. In order to show this on the table, copy and paste the following code in the data event.

ON('save-record', function(event) {
  if(!$hyperlink_field) {
    SETVALUE('hyperlink_field', 'hyperlink_url');
  }
})

Did this page help you?