Dynamic addition of multiple properties via notes suggester, templater script

Something interesting and powerful to share when you want to add one or multiple custom properties to the current note.

You define the property, or properties, inside a table in a “property note”. I chose Extras/properties to add as many notes as I need.

The properties note will look like this:

or

or

This is how you call the templater note:

<%* await tp.user["add-properties-from-template"](tp) %>

This is the templater script saved under your templater scripts folder:

// File: add-properties-from-template.js
// Templater script to add properties from property definition notes
// Place this in your Templater scripts folder
//
// Oct 18: Modified to prevent ovewriting an existing property values

module.exports = async (tp) => {
    const app = tp.app;
    const propertiesFolder = "Extras/properties";
    
    try {
        // Get all files in the properties folder
        const folder = app.vault.getAbstractFileByPath(propertiesFolder);
        
        if (!folder) {
            new Notice(`Properties folder not found: ${propertiesFolder}`);
            return "";
        }
        
        // Get all markdown files in the folder
        const propertyFiles = app.vault.getMarkdownFiles()
            .filter(file => file.path.startsWith(propertiesFolder + "/"));
        
        if (propertyFiles.length === 0) {
            new Notice("No property definition notes found in " + propertiesFolder);
            return "";
        }
        
        // Create suggester options
        const fileNames = propertyFiles.map(f => f.basename);
        
        // Show suggester to user
        const selectedFileName = await tp.system.suggester(fileNames, fileNames, false, "Select a property template:");
        
        if (!selectedFileName) {
            return ""; // User cancelled
        }
        
        // Find the selected file
        const selectedFile = propertyFiles.find(f => f.basename === selectedFileName);
        const content = await app.vault.read(selectedFile);
        
        // Parse the table
        const parsedProperties = parsePropertiesTable(content);
        
        if (!parsedProperties) {
            new Notice("⚠️ No valid property table found in the selected note");
            await createExampleNote(app);
            return "";
        }
        
        if (parsedProperties.length === 0) {
            new Notice("⚠️ Table format is incorrect. Check the example note.");
            await createExampleNote(app);
            return "";
        }
        
        // Get current file and its properties
        const currentFile = app.workspace.getActiveFile();
        
        // Store properties in outer scope so the callback can access them
        const propertiesToAdd = parsedProperties;
        
        await app.fileManager.processFrontMatter(currentFile, (frontmatter) => {
            let addedCount = 0;
            let skippedCount = 0;
            
            propertiesToAdd.forEach(prop => {
                if (!(prop.name in frontmatter)) {
                    // Property doesn't exist, add it
                    frontmatter[prop.name] = prop.value;
                    addedCount++;
                } else {
                    // Property exists, skip it
                    skippedCount++;
                }
            });
            
            // Build notification message
            const messages = [];
            if (addedCount > 0) messages.push(`${addedCount} added`);
            if (skippedCount > 0) messages.push(`${skippedCount} skipped (already exist)`);
            
            if (messages.length > 0) {
                new Notice(`✓ Properties updated: ${messages.join(', ')}`);
            } else {
                new Notice("No changes made");
            }
        });
        
    } catch (error) {
        console.error("Error adding properties:", error);
        new Notice("Error: " + error.message);
    }
    
    return "";
    
    // Helper functions defined inside the main function
    function parsePropertiesTable(content) {
        // Find markdown table in content
        const lines = content.split('\n');
        
        let inTable = false;
        let headerFound = false;
        const properties = [];
        
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i].trim();
            
            // Check if this is a table row
            if (line.startsWith('|') && line.endsWith('|')) {
                const cells = line.split('|')
                    .map(cell => cell.trim())
                    .filter(cell => cell.length > 0);
                
                // Check for header row
                if (!headerFound && cells.length === 2) {
                    if (cells[0].toLowerCase() === 'name' && cells[1].toLowerCase() === 'value') {
                        headerFound = true;
                        inTable = true;
                        continue;
                    }
                }
                
                // Skip separator row
                if (inTable && line.includes('---')) {
                    continue;
                }
                
                // Parse data rows
                if (inTable && headerFound && cells.length === 2) {
                    const name = cells[0];
                    let value = cells[1];
                    
                    // Parse value - try to detect arrays
                    value = parseValue(value);
                    
                    properties.push({ name, value });
                }
            } else if (inTable) {
                // End of table
                break;
            }
        }
        
        return headerFound ? properties : null;
    }
    
    function parseValue(valueStr) {
        valueStr = valueStr.trim();
        
        // Handle empty values - return empty string instead of skipping
        if (valueStr === '' || valueStr === '""' || valueStr === "''") {
            return '';
        }
        
        // Check if it looks like an array (contains commas outside of links)
        // Simple heuristic: if there are commas, try to parse as array
        if (valueStr.includes(',')) {
            // Split by comma, but be careful with [[links]]
            const parts = [];
            let current = '';
            let inLink = false;
            
            for (let i = 0; i < valueStr.length; i++) {
                const char = valueStr[i];
                const nextChar = valueStr[i + 1];
                
                if (char === '[' && nextChar === '[') {
                    inLink = true;
                    current += char;
                } else if (char === ']' && nextChar === ']') {
                    inLink = false;
                    current += char;
                } else if (char === ',' && !inLink) {
                    parts.push(current.trim());
                    current = '';
                } else {
                    current += char;
                }
            }
            
            if (current.trim()) {
                parts.push(current.trim());
            }
            
            if (parts.length > 1) {
                return parts;
            }
        }
        
        // Return as-is for single values
        return valueStr;
    }
    
    async function createExampleNote(app) {
        const exampleContent = `# Property Table Example

Properties must be defined in a table with this exact format:

| name | value |
| ---- | ----- |
| collection | [[my-collection]] |
| context | Machine Learning |
| related | [[note1]], [[note2]] |
| status | active |
| priority | 1 |
| description |  |

**Important:**
- Table must have exactly two columns: "name" and "value"
- Multiple values can be comma-separated
- Links should use [[wiki-link]] format
- Text, numbers, and links are preserved as-is
- Empty values are allowed - just leave the value cell blank or use ""

**Behavior when property exists:**
- **Existing properties are skipped** - their values won't be changed
- Only new properties are added
`;
        
        const fileName = "Property Table Example.md";
        const filePath = `${fileName}`;
        
        try {
            // Check if file already exists
            const existingFile = app.vault.getAbstractFileByPath(filePath);
            if (!existingFile) {
                await app.vault.create(filePath, exampleContent);
                new Notice(`Created example note: ${fileName}`);
                
                // Open the example note
                const newFile = app.vault.getAbstractFileByPath(filePath);
                await app.workspace.getLeaf().openFile(newFile);
            } else {
                new Notice(`Example note already exists: ${fileName}`);
                await app.workspace.getLeaf().openFile(existingFile);
            }
        } catch (error) {
            console.error("Error creating example note:", error);
        }
    }
};

Assign a hotkey to the templater note, something like Alt+P. Then, in the current note, just press the hotkey, and select the property note you want to append to your frontmatter.

This is how the suggester would look after adding property-notes with their respective property:value pairs tables:

Features

  1. Dynamic definition of properties to append to several notes
  2. Properties are entered in a table inside a note
  3. Table with two columns: name and value, just as any dictionary
  4. Values can be text, numbers, links, lists, etc. Can be empty too
  5. Several property-notes allowed in a common folder
  6. Property-notes can be selected from a suggester that is called via a hotkey
  7. Core script is a templater JavaScript living in the typical templater script folder
  8. The core templater user script is called from a one-liner in a standard templater note
  9. Values can be skipped, overwritten, or appended
  10. Error handling when table format is incorrect, or selected note doesn’t have a table

Enjoy!

EDIT
Oct 18: Modified to prevent ovewriting an existing property values