New Properties and templater prompts and commands?

When you insert a template into an already existing file, you can’t use the triple-dash fences around new properties. You’ll need to use the processFrontMatter() method, and when dealing with the current file remember to use the setTimeout() as described earlier.

Other than that, it comes down to knowing how to code and pass variables around in your script. If you don’t know that, you’ll need some basic learning of javascript first.

1 Like

thanks, but I have not used triple-dash at any time.

I just put HELLO before applying the template to the new note and it made that so strange. I did it so that the result could be seen.

If, for example, after applying the template and deleting everything except what is in the code, press crtl+z and my original template appears without the parameter data that we have passed to it by code :upside_down_face:

If anyone can tell me a solution to what my obsidian is doing to me, I would greatly appreciate it.

Good day!

With triple-dash I mean the code fence / text in front and after the frontmatter, aka ---. In your last screen shot, you already have a frontmatter with triple-dashes around the father and son property. You then proceed to insert the plantilla Favorito template, which also has a frontmatter, and it inserts the template just as you asked for with another set of triple-dashes.

Instead of inserting that text, from plantilla Favorito, after the frontmatter which already exists, you should have updated the frontmatter using the processFrontMatter() method, and do stuff like:

frontmatter["Padre"] = '"[[Listado de Plantillas]]"'

(Possibly with another syntax to ensure the correct formatting of links. Not sure how they’re treated within processFrontMatter() as I haven’t tested that part)


In other words, when you insert the plantilla Favorito template, it does just what it is asked to do, and that is insert any text from that template at the current cursor location, and execute any template command blocks as it encounters them. In this case it’ll update the father and son part of the frontmatter after a slight delay of 200ms. Just as expected.

1 Like

This is wonderful AlanG, thank you very much! :slight_smile:

I have two suggestions for improvement or enhancement, but unfortunately my JS skills are not sufficient to do this myself:

  • Do you know how to add property-keys to already existing ones and not overwriting them?
  • The disadvantage of using templater templates is, that updating properties in the property pane won’t update templater templates. A workaround of this disadvantage could be, to include normal templates (which will be updated) through templater scripts. … I think there is the function tp.file.include(), which is probably meant for that, but I didn’t manage to get this working… :confused:

Thanks a lot in advance! :slight_smile:

The last part of the code in this post could help you.
I’m using the multi-line YAML key value convention Obsidian expects since the introduction of Properties (you can also set in Linter’s YAML section which key values you want to be multi-line) and the script is working well enough for me to use it as boilerplate now). I expect if you change tags with another key name, it will work too.

1 Like

Hi gino_m,

wow thanks!!! :slight_smile:

Using ChatGPT, I understood that if have to use that snippet of your code:

<%*
// Check if the 'Englishtexttranslated' tag exists before adding it
setTimeout(() => {
  app.fileManager.processFrontMatter(tp.config.target_file, frontmatter => {
    if (!frontmatter['tags'] || !frontmatter['tags'].includes('Englishtexttranslated')) {
      const tagsArray = Array.isArray(frontmatter['tags']) ? frontmatter['tags'] : [];
      if (!tagsArray.includes('Englishtexttranslated')) {
        tagsArray.push('Englishtexttranslated');
        frontmatter['tags'] = tagsArray;
      }
    }
  });
}, 350); // Timeout to allow preceding clipboard operations to complete
_%>

and change “tags” to any key I want and change “Englishtexttranslated” to any value I want to add.

If I want to have more properties added, I have to copy that part of the snippet:

	if (!frontmatter['tags'] || !frontmatter['tags'].includes('Englishtexttranslated')) {
	  const tagsArray = Array.isArray(frontmatter['tags']) ? frontmatter['tags'] : [];
	  if (!tagsArray.includes('Englishtexttranslated')) {
		tagsArray.push('Englishtexttranslated');
		frontmatter['tags'] = tagsArray;
	  }
	}

Thanks a lot gino! :slight_smile:

2 Likes

That’s pretty much what I do as a non-coder; take something that works and re-use it.

My recent efforts came with some issues that I first didn’t entirely understand, then since using timeouts and a large value (say, 2000ms) seems to have sorted out my issues.

As I understand it, Obsidian doesn’t like to have your properties modified while it is in the process of loading the metadatacache + backlinks (+ outlinks). So if I enter a huge file (say, more than 35kbs), I should need to wait to have things settle a bit or effect the changes with a delay.

1 Like

Hi gino,

I use the following script in order to:

  1. if the key “Key1” doesn’t exist in that file, I want to add that property to that file with the value “hello”. If it already exist, I just want to add the “hello”-value to that property as a list-element
  2. if the key “Key2” doesn’t exist in that file, I want to add that property to that file
<%*
// Check if the 'value' tag exists before adding it
setTimeout(() => {
  app.fileManager.processFrontMatter(tp.config.target_file, frontmatter => {
	let key = "Key1"
	let value = "hello"
	if (!frontmatter[key] || !frontmatter[key].includes(value)) {
	  const prop_Array = Array.isArray(frontmatter[key]) ? frontmatter[key] : [];
	  if (!prop_Array.includes(value)) {
		prop_Array.push(value);
		frontmatter[key] = prop_Array;
	  }
	}
	key = "Key2"
	value = ""
	if (!frontmatter[key] || !frontmatter[key].includes(value)) {
	  const prop_Array = Array.isArray(frontmatter[key]) ? frontmatter[key] : [];
	  if (!prop_Array.includes(value)) {
		prop_Array.push(value);
		frontmatter[key] = prop_Array;
	  }
	}
  });
}, 150); // Timeout to allow preceding clipboard operations to complete
await tp.file.move("Atlas/Notes/Things/" + tp.file.title)
_%>

When I use this script it produces the following frontmatter:

---
Key1:
  - hello
Key2:
  - ""
---

Question

Do you know how can I get rid of that -“” as “Key2”-value?

You set it to be the empty value, so why are you surprised is empty? What do you want instead?

I guess he just wants the property created with an empty string with no "" showing up.

A workaround is to put in null, as I cannot run the script with any types of quotes without getting ""'s in the value field.

  • That’s null, without quotes, by the way.

Also, for a larger vault, adding a timeout larger than the current 150ms could be necessary.

Also, Silias, delete the clipboard word from the comment part. You are not doing clipboard operations, as I told you somewhere else.

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;
4 Likes

Thanks for this and the use case with the DV callout as well.

Haven’t had time to thoroughly test it but it seems to me that if e.g. tags (or maybe other properties) are present with no value, then it doesn’t add the defined values. If there is at least one valid tag value, then it adds them below as expected.
If there is an empty value with tag but the dash is there, then the values are added but not in first position, but 2nd, 3rd, etc.
Same when - null is below tags property.

1 Like

Hi Gino, thanks for testing and the feedback.

I tried to replicate your experience when adding tags to an empty tags property, but I couldn’t replicate the issue. In my case, the tag is added. See this test:

20240201_115340-ezgif.com-optimize

Can you clarify this part? I’m not sure I follow. Which dash are you referring to? Like if you are in source mode, and there is only a dash below the tags: key? Also, why would there be a - null?

I did test these scenarios, if I understand them correctly, and could confirm that the tag is added below the empty dash or - null. But I don’t understand why these should be present in any case. If you create a tags property using the property editor, without adding any values, the format is:
tags: [].

As for the dataview callout, I can’t take credit for that, as I got the idea from the Ideaverse vault :wink: I did make some modifications, though, because it wasn’t working as intended.

@gino_m Actually, I tested again and got the same result as you this time. I was wrong about the format that the property editor creates. Before testing the previous time, I added a tag with the editor, the removed it again.

This left this format: tags: [ ]

When I just create properties from scratch using --- in live preview mode, and then add the tags property using the editor, the format is just: tags: with nothing following the key.

When I tested in this condition, no tag was added.

Hi again @gino_m,

I think I solved the issue with this refactor:

Refactored mergeFrontmatter script
async function mergeFrontmatter(properties, tp) {
    try {
        const file = await tp.config.target_file;
        const fileCache = app.metadataCache.getFileCache(file)?.frontmatter;

        await Promise.all(properties.map(async (prop) => {
            if (!fileCache) {
                console.log("Frontmatter is empty");
            } else if (fileCache.hasOwnProperty(prop.key)) {
                console.log(`${file.basename} contains property: ${prop.key}`);
                const value = fileCache[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++) {
                            if (!value.includes(prop.value[i])) {
                                console.log(`adding ${prop.value[i]} to ${prop.key} in ${file.basename}`);
                                frontmatter[prop.key].push(prop.value[i]);
                            }
                        }
                    });
                } else if (!value && prop.value != null) {
                    console.log(`adding ${prop.value} to ${prop.key} in ${file.basename}`);
                    await app.fileManager.processFrontMatter(file, (frontmatter) => {
                        frontmatter[prop.key] = prop.value;
                    });
                }
                return;
            }

            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;

It’s still non-destructive (won’t replace existing values), but you can make the current script destructive (will replace existing values) by removing the !value condition on line 22.

I am experimenting with a version where you can specify the treatment in the properties array with the options: add only, update (replace existing value), and delete.

It seems to be working so far, but it’s a bit lengthy, so I’m gonna try refactoring it.

Increment should probably be another option, but that could perhaps be automated based on whether the property is a number, or something like that.

1 Like

Okay, just arrived to my desktop.

Indeed, when linting a file, [] is added.
I may have ended up with zero strings at tags following a mass search and replace…? I don’t quite know.

I tried the new js and is working well or better.
I am not sure in what scenario an empty string in a multi-line array list would be created with the dash (where the ‘dash’ or ‘hyphen’ is the standard prefix or whatyoumaycallit) and the - null value would probably be made if used with Linter (when forcing the multi-line values on certain properties)? (Again, I cannot see or list all scenarios as to what does what and what it’ll look like.)

I said ‘better’ because the empty multi-line value still is not overwritten with the new value but is added second in line. Properties UI doesn’t dislike this though, truth be told and when you lint the file with Linter, the empty value is duly deleted the new values are bumped back up into first, second, etc. position.

Thanks for testing again. To be clear, I didn’t try to solve the issue with tags being added below existing empty dashes, because I didn’t see the value of this scenario. What I mean is, there shouldn’t be empty dashes or - null. It would probably be more worthwhile to figure out, why Linter is adding these things, if that’s the case.

So I only solved the logic issue that prevented the properties value being added, if the key is entirely empty, like tags: with nothing to follow.

In the scenario you raise, the empty dash or - null makes the value of the tags key be considered an array, and therefore, the values you add with mergeFrontmatter are pushed to the array and appended below.

That’s fine. Thanks. I’m only bringing these up because the thread is public and everyone has different use cases, plugins installed and their own ways to mess up (search and replaces globally may cause what I wrote about and Linter is very well integrated with Obsidian as well so I cannot be sure what may cause - <empty> or - null if anything).

As the js script stands now, it should suffice.

But I’m curious about what benefits the hook and the separate js might give the user?

As someone dipping his toes in scripting (Js for Obsidian and Python for outside of Obsidian use), my challenges usually involve timing issues (with js) and the merging changes automatically pop-ups – will this script or method prevent those pop-ups? I think I saw it once yesterday…

The merging changes pop-up issues I raised with @AlanG (author of the date modified updater plugin) back in the day and showed that time and again some gibberish (e.g. ‘wwwork’ or ‘dddoes’) is entered in the editor. I want to eliminate these especially.
When I work, I use Templater scripts to add tags or status values and hop in and out of notes rather quickly, so I need to beware that no rubbish is left in my notes. I even mentioned cases when a full note was exchanged with the content of another note…

It is a quirk in Obsidian one needs to be aware of and it is about the every 2 second automatic save, which when coinciding with editor changes (including YAML property updates) happens – as far as I know…

1 Like

tp.hooks.on_all_templates_executed is very useful because:

  1. It removes the need to set fixed and arbitrary timeouts before processing the frontmatter that could either be insufficient or excessive, introducing lag.
  2. It ensures that the frontmatter is not modified, until the rest of the template has been added and processed. This ensures that the cache has been updated, and it prevents conflicts where one part of the template might overwrite another.

I made the separate userscript because I wanted to be able to merge new properties into existing properties, without modifying the existing properties.

At the time, I was experimenting with some templater scripts that were multi-step. Basically, I was defining some properties based on a note I was extracting information from. Then, I got a template suggester for which template to apply to the new note. The mergeFrontmatter userscript allows the template to seamlessly apply its properties without overwriting the properties defined in the previous step.


So the bottom line is, this allows me to insert templates into any existing file, and the template will apply its properties without any issues, merging them with the existing ones.

I think it does, or at least in my case, I haven’t gotten those in a while. But I am not sure. When you say you saw one yesterday, do you mean that you saw one when testing my user script?