Mtools Module for PDF Reports
Overview
The ReportHelpers class (accessed as M in reports) provides methods for generating professional PDF reports from Fulcrum data. It handles HTML grid generation, photo galleries, table building, and field value formatting.
When to Use
Use ReportHelpers when you need to:
- Generate PDF reports from Fulcrum records
- Display fields in organized grids and tables
- Create photo galleries with captions
- Handle repeatable sections
- Format field values (checkboxes, choice fields, addresses)
- Build consistent, professional-looking reports
What You Get
- Grid layouts - Automatic 12-column responsive grids
- Photo galleries - Customizable column layouts with captions
- Table generation - Open tables for tabular data
- Value formatting - Automatic checkbox/choice field conversion
- Section builders - Pre-configured section templates
- CSS styling - Professional Merjent report styles
Important Things to Remember
Before building your PDF report, keep these critical points in mind:
✅ Always Use Region Markers
//#region !START MERJENT FULCRUM TOOLS!
// ... paste merjent-tools-report.mmin.js content here ...
//#endregion !END MERJENT FULCRUM TOOLS!
These markers let Python update tools replace MTools while preserving your custom report code.
✅ M is Pre-Instantiated
// The dist file already contains:
// const M = new ReportHelpers();
// ✓ CORRECT - Just use M directly after the region markers
<%- M.buildDefaultSection(secDn, mFlds, secOpts) %>
// ✗ WRONG - Don't re-instantiate
const M = new ReportHelpers(CONFIG, data);
✅ Grid Spans Must Add Up to 12
// ✓ CORRECT - Total = 12
mFlds.m('field1').span = 4; // 4
mFlds.m('field2').span = 4; // 4
mFlds.m('field3').span = 4; // 4
// ✗ WRONG - Total = 15 (will wrap incorrectly)
mFlds.m('field1').span = 5;
mFlds.m('field2').span = 5;
mFlds.m('field3').span = 5;
✅ Use FIELDNAMES() for Dynamic Lists
// ✓ GOOD - Automatically adapts to form changes
const dns = FIELDNAMES('section', {sections: false});
const mFlds = M.mkMflds(dns);
// ✗ AVOID - Breaks when fields are added/removed
const dns = ['field1', 'field2', 'field3'];
✅ Use fldVal() for Complex Field Types
// ✓ CORRECT - Handles choice fields, addresses, etc.
<%=M.fldVal('choice_field')%>
<%=M.fldVal('address_field')%>
// ✗ WRONG - Shows [object Object]
<%=VALUE('choice_field')%>
<%=VALUE('address_field')%>
✅ Test with Edge Cases
Always preview your report with:
- Records with no data (blank fields)
- Records with all fields filled
- Records with many repeatable items (10+)
- Records with no photos
- Records with long text fields
✅ Group Related Code
Keep section setup code together:
<%
const secDn = 'site_information';
const dns = FIELDNAMES(secDn, {sections: false});
const mFlds = M.mkMflds(dns);
const secOpts = new SecOpt(null, LABEL(secDn), null);
%>
<%- M.buildDefaultSection(secDn, mFlds, secOpts) %>
Installation
Copy merjent-tools-report.mmin.js into your Fulcrum PDF report template:
//#region !START MERJENT FULCRUM TOOLS!
// ... paste merjent-tools-report.mmin.js content here ...
//#endregion !END MERJENT FULCRUM TOOLS!
// M is already instantiated in the dist file as: const M = new ReportHelpers();
// Your custom code starts here
Important: The region markers allow Python update tools to replace MTools code while preserving your custom report logic.
Note: Report templates (HTML blocks) used by ReportHelpers come from the merjent-fulcrum-core sibling repository. These are bundled into the dist file during build.
Basic Concepts
Report Environment
Fulcrum PDF reports use:
- EJS templates -
<%= %>for output,<% %>for logic - Predefined functions -
VALUE(),LABEL(),FIELDNAMES(), etc. - Data object - Contains all record data
- CONFIG object - Report configuration
Grid System
ReportHelpers uses a 12-column grid by default:
- Each row spans 12 columns total
- Fields can span 1-12 columns
- Responsive layout automatically wraps
|--- 4 cols ---|--- 4 cols ---|--- 4 cols ---| (3 fields per row)
|------ 6 cols ------|------ 6 cols ------| (2 fields per row)
|------------ 12 cols ----------------------| (1 field per row)
Field Collections
The MfldColl class manages collections of fields for grids:
// Create field collection
const fields = M.mkMflds(['field1', 'field2', 'field3']);
// Modify individual fields
fields.m('field1').span = 6; // Make field1 span 6 columns
fields.m('field2').opt = '-hn'; // Hide if null
Instantiation
The dist file already instantiates M for you:
// This is already in the dist file - you don't need to add it
const M = new ReportHelpers();
Just paste the dist file content inside the region markers and start using M directly.
Building Sections
Default Grid Section
The most common pattern - displays fields in a responsive grid:
<!-- SECTION: Site Information -->
<%
const secDn = 'site_information';
const dns = FIELDNAMES(secDn, {sections: false});
const mFlds = M.mkMflds(dns);
const secOpts = new SecOpt(null, LABEL(secDn), null);
%>
<%- M.buildDefaultSection(secDn, mFlds, secOpts) %>
What this does:
- Gets all field data names in the section
- Creates a field collection
- Builds a grid with section header
- Auto-formats values (checkboxes for yes/no, etc.)
Result:
<div class="mjnt-sectionHeader spacer-top">Site Information</div>
<div class="grid-lo default">
<div class="rows">
<div style="grid-column: span 4;">
<div>Project Name</div>
<div>West Ridge Survey</div>
</div>
<div style="grid-column: span 4;">
<div>Date</div>
<div>2025-01-15</div>
</div>
<!-- ... more fields -->
</div>
</div>
Customizing Fields in Grid
Change Column Span
const dns = FIELDNAMES('site_info', {sections: false});
const mFlds = M.mkMflds(dns);
// Make project_name span full width
mFlds.m('project_name').span = 12;
// Make date and time each span half width
mFlds.m('date').span = 6;
mFlds.m('time').span = 6;
Hide Fields When Null
mFlds.m('optional_field').opt = '-hn'; // Hide if null
Choice Fields as Checkboxes
// By default, choice fields show as: "Option1, Option2"
// To show as checkboxes:
mFlds.m('habitat_types').opt = '-cb'; // Show all options
mFlds.m('threats').opt = '-hcb'; // Hide unchecked options
Remove Fields from Grid
const dns = FIELDNAMES('site_info', {sections: false});
// Remove fields you don't want
const filtered = M.arrRem(dns, ['internal_notes', 'gps_accuracy']);
const mFlds = M.mkMflds(filtered);
Yes/No Table Section
For sections with many yes/no fields, display as a table:
<!-- SECTION: Equipment Checklist -->
<%
const secDn = 'equipment_checklist';
const dns = FIELDNAMES(secDn, {sections: false});
const mFlds = M.mkMflds(dns);
const secOpts = new SecOpt(null, LABEL(secDn), null);
%>
<%- M.buildYNSection(secDn, mFlds, secOpts) %>
Result:
Equipment Checklist
□ GPS Device
☑ Camera
☑ Field Notebook
□ First Aid Kit
Repeatable Sections
Repeatable Default Grid
For repeatable sections with few items:
<!-- REPEATABLE: Species Observations -->
<%
const secDn = 'species_observations';
const dns = FIELDNAMES(secDn);
const mFlds = M.mkMflds(dns);
const secOpts = new SecOpt(null, LABEL(secDn), null);
%>
<div class="mjnt-sectionHeader spacer-top"><%= LABEL(secDn) %></div>
<%- M.buildRepeatableSubsec(secDn, mFlds, secOpts) %>
Result: Each repeatable item displayed as a subsection with grid layout.
Repeatable Table
For repeatable sections with many items, use table format:
<!-- REPEATABLE TABLE: Survey Points -->
<div class="clump">
<%
const secDn = 'survey_points';
const dns = FIELDNAMES(secDn);
const repFormId = '7d00...'; // Child form ID
const rows = M.repRequestRecords([RECORDID()], FORM().id, repFormId, null, M.addCH(dns));
const divRows = M.rows2Divs(rows, dns);
%>
<div class="mjnt-sectionHeader spacer-top"><%=LABEL(secDn)%></div>
<!-- Adjust column count in style -->
<div class="grid-lo open-table" style="grid-template-columns:repeat(5, 1fr)">
<!-- Table Headers -->
<div class="header">
<%- M.makeTableHeaders(dns) %>
</div>
<!-- Table Rows -->
<div class="rows">
<%- divRows %>
</div>
</div>
</div>
Adjusting columns:
/* 3 columns */
style="grid-template-columns:repeat(3, 1fr)"
/* 5 columns with custom widths */
style="grid-template-columns: 2fr 1fr 1fr 1fr 3fr"
Photo Sections
Simple Photo Field
<!-- PHOTOS: Site Photos -->
<%
const dn = 'site_photos';
const photos = VALUE(dn);
const secOpts = new SecOpt(null, LABEL(dn), null);
%>
<%- M.buildPhotoSection(photos, secOpts) %>
Customizing columns:
// 1 column (default)
M.buildPhotoSection(photos, secOpts);
// 2 columns
M.buildPhotoSection(photos, secOpts, 2);
// 3 columns
M.buildPhotoSection(photos, secOpts, 3);
Photos in Repeatable
<!-- PHOTOS: Observation Photos (in repeatable) -->
<%
const repDn = 'species_observations';
const dn = 'observation_photos';
const secOpts = new SecOpt(null, LABEL(dn), null);
%>
<%- M.buildRepeatablePhotos(repDn, dn, secOpts) %>
This automatically groups photos by repeatable item.
Text Sections
For long-text fields (notes, descriptions):
<!-- SECTION: Field Notes -->
<div class="clump">
<div class="mjnt-sectionHeader spacer-top"><%=LABEL('field_notes')%></div>
<div style="white-space:pre-line;text-align:left;min-height:150px">
<%=M.fldVal('field_notes')%>
</div>
</div>
Formatting options:
white-space:pre-line- Preserves line breakstext-align:left- Left-align textmin-height- Ensure minimum vertical space
Bullet Lists
Convert paragraph text to HTML bullet lists:
<!-- SECTION: Recommendations -->
<%
const recommendations = VALUE('recommendations');
const bulletHTML = M.buildBullets(recommendations);
%>
<div class="clump">
<div class="mjnt-sectionHeader spacer-top">Recommendations</div>
<div class="bullet-list">
<%- bulletHTML %>
</div>
</div>
Input:
Replace damaged fencing in northwest corner
Schedule follow-up survey in spring
Update habitat maps with new boundaries
Output:
<ul>
<li>Replace damaged fencing in northwest corner</li>
<li>Schedule follow-up survey in spring</li>
<li>Update habitat maps with new boundaries</li>
</ul>
Value Formatting
fldVal() Method
The fldVal() method (also available as M.fVal()) automatically formats field values for display:
M.fldVal('field_name');
M.fldVal('field_name', 'default_value');
Automatic formatting:
| Field Type | Input | Output |
|---|---|---|
| Null/Undefined | null |
"---" |
| Yes/No | "yes" |
☑ (checkbox) |
| Yes/No | "no" |
□ (empty box) |
| Choice | {choice_values: ['A', 'B']} |
"A, B" |
| Address | {thoroughfare: '123 Main', locality: 'City'} |
"123 Main, City, ST 12345" |
| Date | "2025-01-15" |
"2025-01-15" |
| Array | ['a', 'b', 'c'] |
"a, b, c" |
Customizing Yes/No Display
As Checkboxes (Default)
M.fldVal('completed'); // ☑ or □
As Text
const value = VALUE('completed');
// Display "Yes" or "No" instead of checkbox
<%%= value === 'yes' ? 'Yes' : 'No' %>
Customizing Choice Fields
As Comma-Separated (Default)
M.fldVal('habitat_types'); // "Wetland, Forest, Grassland"
As Checkboxes
mFlds.m('habitat_types').opt = '-cb'; // All options with checkboxes
mFlds.m('habitat_types').opt = '-hcb'; // Only checked options
Section Options (SecOpt)
The SecOpt class configures section appearance:
new SecOpt(hidden, title, note);
Parameters
- hidden (
boolean) - Hide section if true - title (
string) - Section header text - note (
string) - Optional note below header
Examples
// Basic section
new SecOpt(null, 'Site Information', null);
// Section with note
new SecOpt(null, 'Weather Conditions', 'Recorded at start of survey');
// Conditionally hide section
const hasPhotos = VALUE('site_photos').length > 0;
new SecOpt(!hasPhotos, 'Site Photos', null); // Hidden if no photos
Converting to HTML
const secOpts = new SecOpt(null, 'Section Title', 'Optional note');
// Generate section header HTML
<%- secOpts.toHtml() %>
Report Template Structure
Complete Report Example
//#region !START MERJENT FULCRUM TOOLS!
// ... merjent-tools-report.mmin.js ...
//#endregion !END MERJENT FULCRUM TOOLS!
// M is already instantiated in the dist file
const mjntLogo = 'https://example.com/logo.png';
%>
<!--***************** BODY *****************-->
<div class="generic-theme2">
<!--***************** TITLE BLOCK *****************-->
<div id="titlebox">
<div class="titleleft">
<div><%= form.name %></div>
<div><%= VALUE('project_name') %></div>
<div><%= VALUE('report_id') %></div>
</div>
<div class="clear"></div>
<div class="titleright">
<div><img src="<%= mjntLogo %>"/></div>
</div>
</div>
<!--***************** SITE INFORMATION *****************-->
<%
const siteDns = FIELDNAMES('site_information', {sections: false});
const siteFlds = M.mkMflds(siteDns);
const siteOpts = new SecOpt(null, 'Site Information', null);
%>
<%- M.buildDefaultSection('site_information', siteFlds, siteOpts) %>
<!--***************** OBSERVATIONS *****************-->
<%
const obsDn = 'species_observations';
const obsDns = FIELDNAMES(obsDn);
const obsFlds = M.mkMflds(obsDns);
const obsOpts = new SecOpt(null, LABEL(obsDn), null);
%>
<div class="mjnt-sectionHeader spacer-top"><%= LABEL(obsDn) %></div>
<%- M.buildRepeatableSubsec(obsDn, obsFlds, obsOpts) %>
<!--***************** PHOTOS *****************-->
<%
const photos = VALUE('site_photos');
const photoOpts = new SecOpt(null, 'Site Photos', null);
%>
<%- M.buildPhotoSection(photos, photoOpts, 2) %>
<!--***************** NOTES *****************-->
<div class="clump">
<div class="mjnt-sectionHeader spacer-top">Field Notes</div>
<div style="white-space:pre-line;text-align:left">
<%=M.fldVal('field_notes')%>
</div>
</div>
</div>
CSS Styling
MJNT_Report.css Classes
The report stylesheet provides these classes:
Layout Classes
.generic-theme2 /* Main report container */
.mjnt-sectionHeader /* Section headers */
.mjnt-subsectionHeader /* Subsection headers */
.spacer-top /* Top margin spacing */
.clump /* Group related elements */
.clear /* Clear floats */
Grid Classes
.grid-lo.default /* Default 12-column grid */
.grid-lo.open-table /* Table layout */
.grid-lo.yn-table /* Yes/No table */
Usage Examples
<!-- Add spacing above section -->
<div class="mjnt-sectionHeader spacer-top">Section Title</div>
<!-- Group elements together -->
<div class="clump">
<div class="mjnt-sectionHeader">Title</div>
<p>Content</p>
</div>
Advanced Techniques
Conditional Sections
<!-- Only show if field has value -->
<% if (VALUE('follow_up_required') === 'yes') { %>
<%
const followUpDns = FIELDNAMES('follow_up_details', {sections: false});
const followUpFlds = M.mkMflds(followUpDns);
const followUpOpts = new SecOpt(null, 'Follow-Up Details', null);
%>
<%- M.buildDefaultSection('follow_up_details', followUpFlds, followUpOpts) %>
<% } %>
Custom Grid Layouts
<!-- Mixed column spans -->
<%
const dns = ['project_name', 'date', 'time', 'surveyor', 'weather'];
const mFlds = M.mkMflds(dns);
// Full width
mFlds.m('project_name').span = 12;
// Half width
mFlds.m('date').span = 6;
mFlds.m('time').span = 6;
// Third width
mFlds.m('surveyor').span = 4;
mFlds.m('weather').span = 8;
const secOpts = new SecOpt(null, 'Survey Details', null);
%>
<%- M.buildDefaultSection('survey_details', mFlds, secOpts) %>
Photo Grid with Custom Captions
<!-- Photos with custom captions -->
<%
const photos = VALUE('site_photos');
const photoGrid = photos.map((photo, index) => {
return `
<div class="photo-item">
<img src="${photo.url}" alt="Photo ${index + 1}"/>
<div class="caption">
Photo ${index + 1}: ${photo.caption || 'No caption'}
<br>Location: ${photo.latitude}, ${photo.longitude}
</div>
</div>
`;
}).join('');
%>
<div class="mjnt-sectionHeader spacer-top">Site Photos</div>
<div class="photo-grid">
<%- photoGrid %>
</div>
Summary Statistics
<!-- Calculate and display statistics -->
<%
const observations = VALUE('species_observations');
const totalCount = observations.reduce((sum, obs) => {
return sum + (obs.form_values.count || 0);
}, 0);
const speciesList = observations.map(obs => obs.form_values.species_name);
// Utils methods are available statically in report templates
// You can also use M.arrUnique() if preferred (they're equivalent)
const uniqueSpecies = Utils.arrUnique(speciesList).length;
%>
<div class="clump">
<div class="mjnt-sectionHeader spacer-top">Survey Summary</div>
<div class="grid-lo default">
<div class="rows">
<div style="grid-column: span 6;">
<div>Total Observations</div>
<div><%= observations.length %></div>
</div>
<div style="grid-column: span 6;">
<div>Total Individual Count</div>
<div><%= totalCount %></div>
</div>
<div style="grid-column: span 12;">
<div>Unique Species Observed</div>
<div><%= uniqueSpecies %></div>
</div>
</div>
</div>
</div>
Best Practices
1. Use FIELDNAMES() for Flexibility
// GOOD: Dynamic field list
const dns = FIELDNAMES('section_name', {sections: false});
// AVOID: Hardcoded field list
const dns = ['field1', 'field2', 'field3'];
This way, adding/removing fields in Fulcrum automatically updates the report.
2. Group Related Code
<!-- SECTION: Site Information -->
<%
// All section setup together
const secDn = 'site_information';
const dns = FIELDNAMES(secDn, {sections: false});
const mFlds = M.mkMflds(dns);
const secOpts = new SecOpt(null, LABEL(secDn), null);
%>
<%- M.buildDefaultSection(secDn, mFlds, secOpts) %>
3. Use Comments Liberally
<!--***************** OBSERVATIONS TABLE *****************-->
<!--
This section displays all species observations in a table format.
Columns: Species Name, Count, Behavior, Location
-->
4. Test with Edge Cases
Test your report with:
- Records with no data
- Records with all fields filled
- Records with many repeatable items
- Records with no photos
- Records with long text fields
5. Preview Before Deploying
Always generate a preview PDF before deploying to production:
- Use test record with representative data
- Check layout at different page breaks
- Verify photos display correctly
- Test with different field values
Troubleshooting
Issue: Grid layout looks wrong
Cause: Column spans don't add up to 12
Solution:
// Check your spans
mFlds.m('field1').span = 4;
mFlds.m('field2').span = 4;
mFlds.m('field3').span = 4;
// Total = 12 ✓
// WRONG
mFlds.m('field1').span = 5;
mFlds.m('field2').span = 5;
mFlds.m('field3').span = 5;
// Total = 15 ✗ (will wrap incorrectly)
Issue: Photos not displaying
Cause: Photo URLs not loading or incorrect photo object
Solution:
// Check if photos exist
const photos = VALUE('site_photos');
if (!photos || photos.length === 0) {
// No photos
}
// Check photo URL
console.log(photos[0].url);
Issue: Values showing as "[object Object]"
Cause: Not using fldVal() for complex field types
Solution:
// WRONG
<%=VALUE('address')%> // Shows [object Object]
// CORRECT
<%=M.fldVal('address')%> // Shows formatted address
Next Steps
- Review Mtools Tutorial for data event helpers
- Explore MerjentApp Tutorial for workflow automation
- Check the API documentation for all methods
- Experiment with custom layouts and styling