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:
- Update
mApp.dnSubmit = 'data_collection_complete' - 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
- Organized - Related code grouped together
- Scannable - Easy to find specific event handlers
- Maintainable - Clear separation of concerns
- 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:
- Static access -
Utils.methodName()- Direct access to the class - 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
- Learn about MerjentApp for workflow automation
- Explore PDF Reports for report generation
- Check the JSDoc API Reference for complete method documentation
Note: Python tools for bulk operations have been moved to merjent-fulcrum-pyTools