Mtools Module for Data Events

Overview

The Mtools module provides commonly used methods for the Fulcrum data events scripting environment. It simplifies field manipulation, form navigation, data validation, and API integrations.

When to Use

Use Mtools when you need to:

  • Get and set field values with less code
  • Validate user input (emails, dates, numbers)
  • Navigate form schema to find fields dynamically
  • Integrate with external APIs (weather data, etc.)
  • Perform calculations and data transformations
  • Handle complex field types (choice fields, repeatables, addresses)

What You Get

  • FieldHelpers - Field manipulation and value formatting
  • FulcrumHelpers - Form schema navigation and parsing
  • DataEventHelpers - Weather APIs and fire danger calculations
  • Utils - General JavaScript utilities
  • RequestHelpers - HTTP request wrappers

Important Things to Remember

Before you start building with Mtools, keep these critical points in mind:

✅ Always Use Region Markers

//#region !START MERJENT FULCRUM TOOLS!
// ... paste merjent-tools-de.mmin.js content here ...
//#endregion !END MERJENT FULCRUM TOOLS!

These markers let Python update tools replace MTools while preserving your custom code.

✅ Use Correct Module Pattern

// ✓ CORRECT
const M = module.exports.Mtools

// ✗ WRONG
const mtools = new Mtools($form)

✅ Use Fulcrum Built-in Functions

// ✓ CORRECT - Use Fulcrum's built-in functions
VALUE('field_name')
SETVALUE('field_name', value)
FIELDNAMES('section', {sections: false})

// ✗ WRONG - These methods don't exist
M.getVal('field_name')
M.setVal('field_name', value)
M.getRepVal('repeatable', 0, 'field')

✅ Centralize Data Name References

// ✓ GOOD - Easy to update if field name changes
const DN_SUBMIT = 'submit_report';
const DN_STATUS = 'status';

ON('change', DN_SUBMIT, updateStatus);

// ✗ AVOID - Hardcoded everywhere
ON('change', 'submit_report', updateStatus);

✅ Use FIELDNAMES() for Dynamic Lists

// ✓ GOOD - Automatically adapts to form changes
const dns = FIELDNAMES('section', {sections: false});
dns.forEach(dn => console.log(LABEL(dn), VALUE(dn)));

// ✗ AVOID - Breaks when fields are added/removed
const dns = ['field1', 'field2', 'field3'];

✅ Follow Recommended Code Layout

Keep your code organized with clear section markers (see "Recommended Code Layout" below).


Installation in Fulcrum

Copy merjent-tools-de.mmin.js from the dist/ folder and paste it into your Fulcrum data event:

//#region !START MERJENT FULCRUM TOOLS!
// ... paste merjent-tools-de.mmin.js content here ...
//#endregion !END MERJENT FULCRUM TOOLS!

// Your custom code below
const M = module.exports.Mtools;

Important: The region markers (//#region and //#endregion) allow the Python update tools to replace MTools code while preserving your custom code.


Basic Usage

Instantiation

const M = module.exports.Mtools;

The $form variable is automatically available in Fulcrum data events.

Getting Field Values

// Simple get
const projectName = VALUE('project_name');

// Get with default value if blank
const status = VALUE('status') || 'Pending';

// Get from repeatable
const observations = VALUE('species_observations');
if (observations && observations.length > 0) {
  const species = observations[0].form_values.species_name;
}

Setting Field Values

// Simple set
SETVALUE('report_id', 'RPT-2025-001');

// Set date to today
SETVALUE('date_observed', Utils.dt2iso(new Date()));

// Clear a field
M.clear('notes');

Field Validation

ON('change', 'email', (event) => {
  if (event.value) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(event.value)) {
      INVALID('Please enter a valid email address');
    }
  }
});

ON('change', 'required_field', (event) => {
  if (Utils.isBlank(event.value)) {
    INVALID('This field is required');
  }
});

Working with Field Names & Data Names

The Problem: Changing Data Names

When you change a field's data_name in Fulcrum, all references in your code break. This is painful when you have complex apps.

The Solution: Dynamic References

The MTools code has been written to minimize hardcoded data names. Here's how:

1. Use Data Event Listeners Sparingly

Only reference data names in:

  • Data event listeners (where required by Fulcrum)
  • MApp property declarations

2. Centralize Data Name References

// BAD: Hardcoded data names everywhere
ON('change', 'submit_report', () => {
  if (VALUE('submit_report') === 'yes') {
    SETSTATUS('Submitted');
  }
});

// GOOD: Reference through MApp properties
mApp.dnSubmit = 'submit_report';
ON('change', mApp.dnSubmit, mApp.updateStatus);

Now if you change the data name:

  1. Update mApp.dnSubmit = 'data_collection_complete'
  2. That's it! The rest of the code still works.

3. Use FIELDNAMES() for Dynamic Discovery

// Get all field data names in a section
const sectionFields = FIELDNAMES('site_information', {sections: false});

// Use in loops or builds
sectionFields.forEach(dn => {
  console.log(LABEL(dn), VALUE(dn));
});

Recommended Code Layout

Use this structure below the MTools region for consistency:

//********************************************* MERJENT APP **********************************************/
// Configure MApp if using merjent-tools-mApp.mmin.js
// mApp.dnSubmit = 'submit_report';
// mApp.dnReviewed = 'coordinator_reviewed';
// mApp.completeLockCondition = ['Complete', 'Resolved'];

//*********************************************** GLOBALS ****************************************************/
// const API_TOKEN = 'your-token-here';
// const EXTERNAL_FORM_ID = '06a438c7-fa1b-450b-9768-5ad690e29c3f';

//****************************************** GENERAL FUNCTIONS ***********************************************/
// function calculateReportID() {
//   const date = VALUE('date');
//   const type = VALUE('report_type');
//   return `RPT-${date}-${type}`;
// }

//*********************************************** ON LOAD ****************************************************/
// function onLoadRecord() {
//   // Initialize form
// }

//*********************************************** ON NEW *****************************************************/
// function clearNew() {
//   M.eachSecDn('review_status', M.clear);
//   const today = new Date();
//   SETVALUE('date', Utils.dt2iso(today));
//   SETSTATUS('Pending');
// }

//*********************************************** ON CLICK ***************************************************/
//********************************************** ON CHANGE ***************************************************/
//********************************************* ON VALIDATE **************************************************/
//*********************************************** ON SAVE ****************************************************/
//******************************************* HTTPS REQUESTS *************************************************/

//********************************************* DATA EVENTS **************************************************/
// ON('new-record', clearNew);
// ON('load-record', onLoadRecord);
// ON('change', 'email', validateEmail);
// ON('save', calculateTotals);

Benefits of This Layout

  1. Organized - Related code grouped together
  2. Scannable - Easy to find specific event handlers
  3. Maintainable - Clear separation of concerns
  4. Consistent - All Merjent apps follow same pattern

Common Patterns

Pattern: Auto-Calculate Report ID

function calculateReportID() {
  const date = VALUE('date');
  const reportType = VALUE('report_type');

  if (date && reportType) {
    const formattedDate = date.replace(/-/g, '');
    const id = `${reportType}-${formattedDate}`;
    SETVALUE('report_id', id);
  }
}

ON('change', 'date', calculateReportID);
ON('change', 'report_type', calculateReportID);

Pattern: Clear Section on Condition

ON('change', 'requires_follow_up', (event) => {
  if (event.value === 'no') {
    // Clear all fields in follow_up section
    M.eachSecDn('follow_up', M.clear);
  }
});

Pattern: Conditional Field Visibility

ON('change', 'has_photos', (event) => {
  if (event.value === 'yes') {
    SETFORMATTRIBUTES('photo_field', {hidden: false});
  } else {
    SETFORMATTRIBUTES('photo_field', {hidden: true});
  }
});

Pattern: Fetch External Data

async function fetchRelatedData() {
  const siteId = VALUE('site_id');

  if (!siteId) return;

  const query = `SELECT * FROM "form_id" WHERE site_id = '${siteId}'`;
  const results = await M.queryRecords(query);

  if (results.length > 0) {
    const record = results[0];
    SETVALUE('site_name', record.form_values.site_name);
    SETVALUE('site_location', record.form_values.location);
  }
}

ON('change', 'site_id', fetchRelatedData);

Weather Data Integration

Fetch NWS Weather Data

ON('click', 'get_weather', async () => {
  const latitude = LATITUDE();
  const longitude = LONGITUDE();

  if (!latitude || !longitude) {
    ALERT('Location required to fetch weather');
    return;
  }

  const weather = await M.fetchNWSData(latitude, longitude);

  if (weather) {
    SETVALUE('temperature', weather.temperature.value);
    SETVALUE('conditions', weather.textDescription);
    SETVALUE('wind_speed', weather.windSpeed.value);
  }
});

Calculate Fire Danger Rating

ON('change-geometry', async () => {
  const lat = LATITUDE();
  const lon = LONGITUDE();

  if (lat && lon) {
    const fireData = await M.getFireDanger(lat, lon);

    if (fireData) {
      SETVALUE('fire_danger_rating', fireData.adjFM1000);
      SETVALUE('fire_weather_index', fireData.fwi);
    }
  }
});

Utils Class Methods

Hybrid Class Pattern

IMPORTANT: Utils is a hybrid class - all methods are available in two ways:

  1. Static access - Utils.methodName() - Direct access to the class
  2. Instance access - M.methodName() - Through the Mtools instance

Both patterns work identically. Choose based on your preference:

// Static access (recommended for calculated fields and standalone use)
Utils.dtDaysDiff(date1, date2)
Utils.isBlank(value)
Utils.arrSort(array)

// Instance access (when you already have M instantiated)
const M = module.exports.Mtools
M.dtDaysDiff(date1, date2)
M.isBlank(value)
M.arrSort(array)

Why this matters:

  • Calculated fields - Use Utils.methodName() (no Mtools instance needed)
  • Data events - Use either pattern, they're equivalent
  • Consistency - Pick one pattern and stick with it in your code

String Utilities

// Convert data_name to Title Case
Utils.dn2Title('project_name');  // "Project Name"

// Proper case
Utils.strProper('JOHN DOE');  // "John Doe"

// Get substring after delimiter
Utils.strAfter('prefix_value', '_');  // "value"

// Get substring before delimiter
Utils.strBefore('prefix_value', '_');  // "prefix"

Date Utilities

// Add days to a date
const futureDate = Utils.dtAddDays(new Date(), 30);

// Calculate days between dates
const days = Utils.dtDaysDiff('2025-01-01', new Date());

// Convert ISO to Date
const date = Utils.iso2dt('2025-01-15T12:00:00Z');

// Convert Date to ISO string (date only)
const isoDate = Utils.dt2iso(new Date());  // "2025-01-15"

Array Utilities

// Sort array
const sorted = Utils.arrSort([3, 1, 2]);  // [1, 2, 3]

// Get unique values
const unique = Utils.arrUnique([1, 2, 2, 3, 3]);  // [1, 2, 3]

// Remove items from array
const arr = ['a', 'b', 'c', 'd'];
Utils.arrRem(arr, ['b', 'd']);  // arr is now ['a', 'c']

// Non-destructive remove
const filtered = Utils.arr2Rem(['a', 'b', 'c'], 'b');  // ['a', 'c']

Validation Utilities

// Check if blank (null, undefined, empty string, empty array, etc.)
Utils.isBlank('');  // true
Utils.isBlank([]);  // true
Utils.isBlank(null);  // true
Utils.isBlank('text');  // false

// Check data types
Utils.isNumber(42);  // true
Utils.isDate('2025-01-15');  // true
Utils.isDateStr('2025-01-15');  // true

Format Values

// Format any value for display
Utils.fVal(null);  // "---"
Utils.fVal('text');  // "text"
Utils.fVal(new Date());  // "2025-01-15"
Utils.fVal(['a', 'b', 'c']);  // "a, b, c"

// Choice field values
const choiceField = VALUE('status');  // {choice_values: ['Active', 'Complete']}
Utils.fVal(choiceField);  // "Active, Complete"

// Address fields
const address = VALUE('location');
Utils.formatAddress(address);  // "123 Main St, Suite 100, City, ST 12345"

Working with Repeatables

Get Repeatable Values

// Get value from specific repeatable item
const observations = VALUE('observations');
const species = observations[0].form_values.species_name;

// Get all values for a field across all repeatable items
const observations = VALUE('observations');
const allSpecies = observations.map(item => item.form_values.species_name);

Loop Through Repeatable

const observations = VALUE('observations');

observations.forEach((item, index) => {
  const species = item.form_values.species_name;
  const count = item.form_values.count;
  console.log(`${species}: ${count}`);
});

Query Repeatable Records

// Query records from a repeatable child form
const childFormId = '7d00...';
const parentRecordId = RECORDID();

const query = `SELECT * FROM "${childFormId}" WHERE @parent_id = '${parentRecordId}'`;
const results = await M.queryRecords(query);

Best Practices

1. Always Instantiate Mtools

// At the top of your data events
const M = module.exports.Mtools;

2. Use Utils Methods (Static or Instance)

// Static access - works anywhere, even in calculated fields (no M needed)
Utils.dtDaysDiff(startDate, endDate)
Utils.isBlank(value)

// Instance access - use when you have M instantiated (equivalent to static)
M.dtDaysDiff(startDate, endDate)
M.isBlank(value)

Tip: Use static Utils. in calculated fields where Mtools isn't available.

3. Validate Before Setting

ON('change', 'email', (event) => {
  if (event.value && !M.isValidEmail(event.value)) {
    INVALID('Invalid email format');
    return;  // Don't proceed
  }
  // Process valid email
});

4. Handle Async Operations

// Always use async/await for API calls
ON('click', 'fetch_button', async () => {
  const data = await M.fetchData();
  // Process data
});

5. Centralize Configuration

// Group all configuration at the top
const CONFIG = {
  apiToken: 'xxx',
  externalFormId: 'yyy',
  defaultStatus: 'Pending'
};

Troubleshooting

Issue: Field value returns undefined

Cause: Data name is incorrect or field doesn't exist

Solution:

// Check if field exists first
const value = VALUE('field_name');
if (value === undefined || value === null) {
  console.log('Field not found or has no value');
}

// Or use default value with || operator
const value = VALUE('field_name') || 'default';

// Or use nullish coalescing
const value = VALUE('field_name') ?? 'default';

Issue: Changes not persisting

Cause: Using event.value instead of SETVALUE()

Solution:

// WRONG
ON('change', 'field', (event) => {
  event.value = 'new value';  // Doesn't persist!
});

// CORRECT
ON('change', 'field', (event) => {
  SETVALUE('field', 'new value');
});

Issue: Async operations not completing

Cause: Not using async/await properly

Solution:

// WRONG
ON('click', 'button', () => {
  M.fetchData();  // Returns immediately, data not loaded
  const result = VALUE('result');  // undefined
});

// CORRECT
ON('click', 'button', async () => {
  await M.fetchData();  // Wait for completion
  const result = VALUE('result');  // Has value
});

Next Steps

Note: Python tools for bulk operations have been moved to merjent-fulcrum-pyTools


Additional Resources