Here’s my solution for merging template properties into existing files using a user script called merge_frontmatter.js
, and the new hooks module (tp.hooks.on_all_templates_executed
) instead of a timeout.
To use the user script, you pass it a properties array with the properties (keys) and values you want added to the properties / frontmatter. For example, here’s what I use in my concept note template:
<%*
tp.hooks.on_all_templates_executed(async () => {
const date = tp.date.now("YYYY-MM-DD HH:mm");
const parentFile = tp.config.active_file;
const parentLink = app.fileManager.generateMarkdownLink(parentFile, tp.file.folder(true));
const properties = [
{key:'connections', value: null},
{key:'aliases', value: null},
{key:'tags', value: ['concept']},
{key:'status', value: null},
{key:'date-created', value: date},
{key:'date-modified', value: date},
{key:'origin', value: parentLink},
];
await tp.user.merge_frontmatter(properties, tp);
});
-%>
If you just want to add the key without a value, set the value to null. If you want to add multiple values (like multiple tags), use square brackets like so:
{key:'tags', value: ['concept', 'tag2']}
See the full concept note template here, for how it’s used in context of adding properties to the current note:
Concept note usage example
> [!info]+ Definition
> <% tp.file.cursor(0) %>
## Notes
- <% tp.file.cursor(1) %>
> [!tip]+ Unrequited notes
> These notes point directly to this note. But this note doesn't point back (yet).
>
> ```dataview
> LIST
> FROM [[]]
> and !outgoing([[]])
> SORT file.mtime desc
> ```
<%*
tp.hooks.on_all_templates_executed(async () => {
const date = tp.date.now("YYYY-MM-DD HH:mm");
const parentFile = tp.config.active_file;
const parentLink = app.fileManager.generateMarkdownLink(parentFile, tp.file.folder(true));
const properties = [
{key:'connections', value: null},
{key:'aliases', value: null},
{key:'tags', value: ['concept']},
{key:'status', value: null},
{key:'date-created', value: date},
{key:'date-modified', value: date},
{key:'origin', value: parentLink},
];
await tp.user.merge_frontmatter(properties, tp);
});
-%>
I made the merge_Frontmatter.js
user script non-destructive, in that it will not overwrite anything. You can follow the logic from the console logs.
Of course, if there is no exiting frontmatter, it just adds everything as is. But if there is a frontmatter in the note, it makes some checks. First, it checks if the key exists. If it does, it checks is the key is an array. If it is, it adds the value to the array. If the key doesn’t exist, it creates the key with the value specified. So it won’t replace a non-array value with another value.
User script
async function mergeFrontmatter (properties, tp) {
try {
let file = await tp.config.target_file;
await Promise.all(properties.map(async (prop) => {
if (app.metadataCache.getFileCache(file)?.frontmatter == null) {
console.log("Frontmatter is empty");
await app.fileManager.processFrontMatter(file, (frontmatter) => {
console.log(`adding new property: ${prop.key} to ${file.basename} with value: ${prop.value}`);
frontmatter[prop.key] = prop.value;
});
} else if (app.metadataCache.getFileCache(file)?.frontmatter?.hasOwnProperty(prop.key)) {
console.log(`${file.basename} contains property: ${prop.key}`);
const value = app.metadataCache.getFileCache(file)?.frontmatter[prop.key];
if (Array.isArray(value) && prop.value != null) {
console.log(`${prop.key} is an array`);
await app.fileManager.processFrontMatter(file, (frontmatter) => {
for (let i = 0; i < prop.value.length; i++) {
console.log(`adding ${prop.value[i]} to ${prop.key} in ${file.basename}`);
frontmatter[prop.key].push(prop.value[i]);
}
});
}
} else {
console.log(`${file.basename} doesn't contain ${prop.key}`);
console.log(`adding property: ${prop.key} to ${file.basename} with value: ${prop.value}`);
await app.fileManager.processFrontMatter(file, (frontmatter) => {
frontmatter[prop.key] = prop.value;
});
}
}));
} catch (error) {
console.error("Error in processing frontmatter: ", error);
}
}
module.exports = mergeFrontmatter;