node-red-contrib-file-template
Version:
A Node-RED node that loads HTML template content from the filesystem with automatic file watching, reloading, and built-in file editor
412 lines (358 loc) • 18.3 kB
HTML
<!-- Node-RED File Template Configuration -->
<script type="text/javascript">
RED.nodes.registerType('file-template', {
category: 'function',
color: '#E7E7AE',
defaults: {
name: {value: ""},
filename: {value: "", required: true},
format: {value: "handlebars"},
syntax: {value: "mustache"},
template: {value: ""},
field: {value: "payload", required: true},
fieldType: {value: "msg"},
output: {value: "str"},
loadOnBoot: {value: false},
bootData: {value: "{}"},
bootDelay: {value: 1000},
fileContent: {value: ""}
},
inputs: 1,
outputs: 1,
icon: "file.png",
label: function() {
if (this.name) {
return this.name;
}
if (this.filename) {
return "file-template: " + this.filename.split('/').pop();
}
return "file-template";
},
labelStyle: function() {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
var node = this;
// Initialize field type selector with proper typedInput configuration
$("#node-input-field").typedInput({
default: 'msg',
typeField: $("#node-input-fieldType"),
types: ['msg','flow','global']
});
// File picker button
$("#node-input-filename-picker").click(function() {
// Simple file picker - in real implementation you might want a proper file browser
var filename = prompt("Enter template file path:", node.filename || "Html/TodoList.html");
if (filename) {
$("#node-input-filename").val(filename);
}
});
// NEW: Load file content functionality
function updateFileStatus(message, isError = false) {
$("#file-content-status").text(message);
$("#file-content-status").css('color', isError ? '#d32f2f' : '#666');
}
function updateFileInfo(info) {
$("#file-content-info").text(info);
}
// Load file content button
$("#node-input-load-file-btn").click(function() {
var filename = $("#node-input-filename").val().trim();
if (!filename) {
updateFileStatus("Please specify a template file first", true);
return;
}
updateFileStatus("Loading file...");
// Make AJAX request to load file content
$.ajax({
url: 'file-template/load-file',
type: 'POST',
data: JSON.stringify({ filename: filename }),
contentType: 'application/json',
success: function(data) {
if (data.error) {
updateFileStatus("Error: " + data.error, true);
} else {
$("#node-input-fileContent").val(data.content);
updateFileStatus("File loaded successfully");
updateFileInfo(`${data.size} chars, modified: ${new Date(data.mtime).toLocaleString()}`);
}
},
error: function(xhr, status, error) {
updateFileStatus("Failed to load file: " + error, true);
}
});
});
// Save file content button
$("#node-input-save-file-btn").click(function() {
var filename = $("#node-input-filename").val().trim();
var content = $("#node-input-fileContent").val();
if (!filename) {
updateFileStatus("Please specify a template file first", true);
return;
}
if (!content) {
updateFileStatus("No content to save", true);
return;
}
if (!confirm("Are you sure you want to save the changes to the file?\n\nThis will overwrite: " + filename)) {
return;
}
updateFileStatus("Saving file...");
// Make AJAX request to save file content
$.ajax({
url: 'file-template/save-file',
type: 'POST',
data: JSON.stringify({
filename: filename,
content: content
}),
contentType: 'application/json',
success: function(data) {
if (data.error) {
updateFileStatus("Error: " + data.error, true);
} else {
updateFileStatus("File saved successfully");
updateFileInfo(`${data.size} chars saved`);
}
},
error: function(xhr, status, error) {
updateFileStatus("Failed to save file: " + error, true);
}
});
});
// Auto-load file content when filename changes
$("#node-input-filename").on('blur', function() {
var filename = $(this).val().trim();
if (filename && filename !== node.filename) {
// Auto-load if file content is empty
if (!$("#node-input-fileContent").val().trim()) {
$("#node-input-load-file-btn").click();
}
}
});
// Initialize file content if already stored
if (node.fileContent) {
$("#node-input-fileContent").val(node.fileContent);
updateFileStatus("Content loaded from configuration");
}
// Template format change handler
$("#node-input-format").change(function() {
var format = $(this).val();
if (format === "handlebars" || format === "mustache") {
$("#template-syntax-row").show();
} else {
$("#template-syntax-row").hide();
}
});
// Initialize visibility
$("#node-input-format").trigger('change');
// Load on boot change handler
$("#node-input-loadOnBoot").change(function() {
if ($(this).is(':checked')) {
$("#boot-settings-row").show();
$("#boot-data-row").show();
$("#boot-delay-row").show();
} else {
$("#boot-settings-row").hide();
$("#boot-data-row").hide();
$("#boot-delay-row").hide();
}
});
// Initialize boot settings visibility
$("#node-input-loadOnBoot").trigger('change');
},
oneditsave: function() {
// Save file content to node configuration
this.fileContent = $("#node-input-fileContent").val();
// Validate filename
var filename = $("#node-input-filename").val().trim();
if (!filename) {
return false;
}
}
});
</script>
<!-- Node Configuration Dialog -->
<script type="text/html" data-template-name="file-template">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Node name">
</div>
<div class="form-row">
<label for="node-input-filename"><i class="fa fa-file-o"></i> Template File</label>
<input type="text" id="node-input-filename" placeholder="path/to/template.html" style="width: calc(100% - 30px);">
<button type="button" id="node-input-filename-picker" class="editor-button" style="width: 25px; margin-left: 5px;">
<i class="fa fa-folder-open"></i>
</button>
</div>
<!-- NEW: File Content Editor -->
<div class="form-row">
<label for="node-input-fileContent" style="width: 100%;"><i class="fa fa-edit"></i> Template Content
<button type="button" id="node-input-load-file-btn" class="editor-button" style="float: right; margin-top: -3px;">
<i class="fa fa-refresh"></i> Load File
</button>
<button type="button" id="node-input-save-file-btn" class="editor-button" style="float: right; margin-right: 5px; margin-top: -3px;">
<i class="fa fa-save"></i> Save to File
</button>
</label>
<textarea id="node-input-fileContent" rows="12" placeholder="File content will appear here..."
style="font-family: monospace; font-size: 12px; width: 100%; resize: vertical;"></textarea>
<div style="margin-top: 5px; color: #666; font-size: 11px;">
<span id="file-content-status">Select a file and click "Load File" to edit content</span>
<span style="float: right;" id="file-content-info"></span>
</div>
</div>
<div class="form-row">
<label for="node-input-format"><i class="fa fa-code"></i> Template Format</label>
<select id="node-input-format">
<option value="handlebars">Handlebars/Mustache</option>
<option value="plain">Plain HTML</option>
</select>
</div>
<div class="form-row" id="template-syntax-row">
<label for="node-input-syntax"><i class="fa fa-cogs"></i> Template Syntax</label>
<select id="node-input-syntax">
<option value="mustache">Mustache {{variable}}</option>
<option value="handlebars">Handlebars {{#each}} {{/each}}</option>
</select>
</div>
<div class="form-row">
<label for="node-input-field"><i class="fa fa-edit"></i> Template Data</label>
<input type="text" id="node-input-field" placeholder="payload">
<input type="hidden" id="node-input-fieldType">
</div>
<div class="form-row">
<label for="node-input-output"><i class="fa fa-sign-out"></i> Output</label>
<select id="node-input-output">
<option value="str">Set msg.template to string</option>
<option value="parsed">Set msg.template to parsed object</option>
</select>
</div>
<div class="form-row">
<label for="node-input-template"><i class="fa fa-code"></i> Fallback Template</label>
<textarea id="node-input-template" rows="4" placeholder="Optional inline template as fallback"></textarea>
</div>
<div class="form-row">
<label for="node-input-loadOnBoot"><i class="fa fa-power-off"></i> Load on Boot</label>
<input type="checkbox" id="node-input-loadOnBoot" style="width: auto; margin: 0;">
<span style="margin-left: 10px;">Automatically process template when Node-RED starts</span>
</div>
<div class="form-row" id="boot-settings-row" style="display: none;">
<label style="width: 100%; margin-bottom: 5px;"><i class="fa fa-cog"></i> <strong>Boot Settings</strong></label>
</div>
<div class="form-row" id="boot-delay-row" style="display: none;">
<label for="node-input-bootDelay"><i class="fa fa-clock-o"></i> Boot Delay (ms)</label>
<input type="number" id="node-input-bootDelay" placeholder="1000" min="0" max="30000" style="width: 100px;">
<span style="margin-left: 10px; color: #999;">Delay before processing template on startup</span>
</div>
<div class="form-row" id="boot-data-row" style="display: none;">
<label for="node-input-bootData"><i class="fa fa-database"></i> Boot Data (JSON)</label>
<textarea id="node-input-bootData" rows="3" placeholder='{"title": "Dashboard", "user": "Admin"}' style="font-family: monospace;"></textarea>
<span style="display: block; margin-top: 5px; color: #999; font-size: 12px;">
JSON data for template variable substitution on boot (e.g., values for {{variable}} placeholders). Use {} for static templates without variables.
</span>
</div>
<div class="form-tips">
<p><b>File Template Node</b></p>
<p>Loads HTML template content from the filesystem with automatic file watching and reloading.</p>
<ul>
<li><strong>Template File:</strong> Path to your HTML template file (relative to Node-RED working directory)</li>
<li><strong>Template Content:</strong> Edit file content directly in the editor with load/save functionality</li>
<li><strong>Template Data:</strong> The message property containing data for template substitution</li>
<li><strong>File Watching:</strong> Automatically reloads when the template file changes</li>
<li><strong>Mustache Syntax:</strong> Use {{variable}} for simple substitution, {{#each array}} for iteration</li>
<li><strong>Load on Boot:</strong> Automatically processes template when Node-RED starts up</li>
</ul>
</div>
</script>
<!-- Node Help Text -->
<script type="text/html" data-help-name="file-template">
<p>A Node-RED node that loads HTML template content from the filesystem with automatic file watching and reloading.</p>
<h3>Features</h3>
<ul>
<li><strong>File Editing:</strong> Load, edit, and save template files directly in the Node-RED editor</li>
<li><strong>Live Preview:</strong> See file content and modifications before deployment</li>
<li><strong>Auto-reload:</strong> Changes to files are automatically detected and reloaded</li>
<li><strong>Template Processing:</strong> Supports Handlebars/Mustache syntax for dynamic content</li>
<li><strong>Boot Processing:</strong> Can automatically process templates when Node-RED starts</li>
</ul>
<h3>File Editor</h3>
<p>The Template Content editor allows you to:</p>
<ul>
<li><strong>Load File:</strong> Click "Load File" to populate the editor with the current file content</li>
<li><strong>Edit Content:</strong> Modify the template directly in the monospace editor</li>
<li><strong>Save Changes:</strong> Click "Save to File" to write your changes back to the filesystem</li>
<li><strong>Auto-load:</strong> Content is automatically loaded when you specify a filename</li>
</ul>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>The data object to use for template variable substitution.</dd>
<dt class="optional">topic <span class="property-type">string</span></dt>
<dd>Optional message topic that gets passed through.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>template <span class="property-type">string</span></dt>
<dd>The processed template content with variables substituted.</dd>
<dt>_fileTemplate <span class="property-type">object</span></dt>
<dd>Metadata about the template file including filename, last modified time, and content length.</dd>
</dl>
<h3>Configuration</h3>
<dl>
<dt>Template File</dt>
<dd>Path to the HTML template file, relative to Node-RED working directory.</dd>
<dt>Template Format</dt>
<dd>Choose between Handlebars/Mustache for variable substitution or Plain HTML for static content.</dd>
<dt>Template Data</dt>
<dd>The message property (default: payload) containing the data for template variable substitution.</dd>
<dt>Output</dt>
<dd>Whether to output the result as a string or attempt to parse it as JSON.</dd>
<dt>Fallback Template</dt>
<dd>Optional inline template content to use if the file cannot be loaded.</dd>
<dt>Load on Boot</dt>
<dd>When enabled, automatically processes the template when Node-RED starts up.</dd>
<dt>Boot Delay</dt>
<dd>Delay in milliseconds before processing template on startup (default: 1000ms).</dd>
<dt>Boot Data</dt>
<dd>JSON data for template variable substitution on boot (e.g., values for {{variable}} placeholders). Use {} for static templates without variables.</dd>
</dl>
<h3>Features</h3>
<ul>
<li><strong>File Watching:</strong> Automatically detects changes to the template file and reloads content</li>
<li><strong>Template Processing:</strong> Supports Mustache-style variable substitution ({{variable}})</li>
<li><strong>Error Handling:</strong> Graceful handling of missing files with fallback options</li>
<li><strong>Status Updates:</strong> Visual status indicators showing file load status and errors</li>
<li><strong>Flexible Data Source:</strong> Can pull template data from msg, flow, or global context</li>
<li><strong>Load on Boot:</strong> Automatically processes template when Node-RED starts, eliminating the need for manual trigger flows</li>
</ul>
<h3>Template Syntax Examples</h3>
<p>Simple variable substitution:</p>
<pre>
<h1>{{title}}</h1>
<p>Hello {{user.name}}</p>
</pre>
<p>With input data:</p>
<pre>
{
"title": "My Dashboard",
"user": {
"name": "John Doe"
}
}
</pre>
<p>Results in:</p>
<pre>
<h1>My Dashboard</h1>
<p>Hello John Doe</p>
</pre>
<h3>Status Indicators</h3>
<ul>
<li><span style="color: green;">●</span> Green: Template file loaded successfully</li>
<li><span style="color: yellow;">●</span> Yellow: Using fallback inline template</li>
<li><span style="color: red;">●</span> Red: Error loading file or processing template</li>
<li><span style="color: grey;">●</span> Grey: No file specified or node inactive</li>
</ul>
</script>