How to make a single template to fit multiple purposes with templater

While working with Templater I realized I was accumulating a lot of templates. Some of the templates have properties in common and tried to find an efficient way to save creating more template files.

I came up with the idea of having a unique common template which could be modified on the fly from a selector. With some JavaScript and templater I would be able to delete, change and insert new properties in the common template, which has the name “My Stuff Template Test”. The selector templater is called “My Stuff Template Selector”.

The template selector will offer choices of different objects such as “My Code”, “My Computers”, “My Devices”, etc. When selecting any of these choices the common template will be modified according to the mapping in a dictionary. Finally, the resulting template will have the properties I need for that particular Obsidian page.

This is the content of “My Stuff Template Test.md”:

---
aliases:
type: 
author: 
up: 
related: 
summary: 
status: 
location: 
rating: 
tags: 
purpose: 
assigned: 
purchased: 
brand: 
cost: 
url: 
folder: 
partition: 
language: 
OS: 
archetype: "[[My Stuff]]"
uuid: '<% tp.file.creation_date("YYYYMMDDHHmmss") %>'
---

And the following code belongs to the more elaborated “My Stuff Template Selector”:

<%*
const choices = ["Minimal", "My Code", "My Computers", "My Devices", "My Repositories", "My Virtual Machines"]

const choice = await tp.system.suggester(choices, choices);
let output = ""
let template = "[[My Stuff Template Test]]"
let fm_actions;
let yaml_str;

switch(choice) {
	case "My Code":
		fm_actions = { 
			"location": {Value: null, Action: "Delete"}, 
			"author": {Value: null, Action: "Delete"}, 
			"purchased": {Value: null, Action: "Delete"}, 
			"brand": {Value: null, Action: "Delete"}, 
			"cost": {Value: null, Action: "Delete"}, 
			"assigned": {Value: null, Action: "Delete"}, 
			"purpose": {Value: null, Action: "Delete"}, 
			"tags": {Value: "content/code", Action: "Replace"}, 
			"up": {Value: "[[Programming]]", Action: "Replace"},
		};	
		yaml_str = await tp.file.include(template);
		break;
	case "My Computers":
		fm_actions = { 
			"url": {Value: null, Action: "Delete"}, 
			"folder": {Value: null, Action: "Delete"}, 
			"language": {Value: null, Action: "Delete"}, 
			"author": {Value: null, Action: "Delete"}, 
			"purpose": {Value: null, Action: "Delete"}, 
			"tags": {Value: "personal/assets", Action: "Replace"}, 
			"up": {Value: "[[Equipment]]", Action: "Replace"},
			"location": {Value: "[[Home]]", Action: "Replace"},
		};		
		yaml_str = await tp.file.include(template)
		break;		
	case "My Devices":
		fm_actions = { 
			"url": {Value: null, Action: "Delete"}, 
			"folder": {Value: null, Action: "Delete"}, 
			"language": {Value: null, Action: "Delete"}, 
			"OS": {Value: null, Action: "Delete"}, 
			"author": {Value: null, Action: "Delete"}, 
			"assigned": {Value: null, Action: "Delete"}, 
			"tags": {Value: "personal/assets", Action: "Replace"}, 
			"up": {Value: "[[Equipment]]", Action: "Replace"},
			"location": {Value: "[[Home]]", Action: "Replace"},
		};		
		yaml_str = await tp.file.include(template)
		break;
	case "My Repositories":
		fm_actions = { 
			"OS": {Value: null, Action: "Delete"}, 
			"purchased": {Value: null, Action: "Delete"}, 
			"brand": {Value: null, Action: "Delete"}, 
			"cost": {Value: null, Action: "Delete"}, 
			"assigned": {Value: null, Action: "Delete"}, 		
			"location": {Value: null, Action: "Delete"}, 
			"tags": {Value: "content/code", Action: "Replace"}, 
			"up": {Value: "[[Programming]]", Action: "Replace"},
			"status": {Value: "unknown", Action: "Replace"},
		};	
		yaml_str = await tp.file.include(template);
		break;		
	case "My Virtual Machines":
		fm_actions = { 
			"location": {Value: null, Action: "Delete"}, 
			"author": {Value: null, Action: "Delete"}, 
			"purchased": {Value: null, Action: "Delete"}, 
			"brand": {Value: null, Action: "Delete"}, 
			"cost": {Value: null, Action: "Delete"}, 
			"assigned": {Value: null, Action: "Delete"},
			"language": {Value: null, Action: "Delete"}, 
			"url": {Value: null, Action: "Delete"}, 		
			"tags": {Value: "task/virtualization", Action: "Replace"}, 
			"up": {Value: "[[Virtual Machines]]", Action: "Replace"},
			"status": {Value: "active", Action: "Replace"},
		};		
		yaml_str = await tp.file.include(template);
		break;		
	default:
		output = await tp.file.include("[[Default Minimal Template]]")
		fm_up = "[[None]]"
		break;							
		// new Notice("No Matching Template")
}

for (var i=0; i<Object.entries(fm_actions).length; i++) {
	dEntry = Object.entries(fm_actions)[i]
	yaml_str = processYaml(i, yaml_str, dEntry);
 };
tR += yaml_str

function processYaml(i, yml, d) {
	let key = d[0];
	let kv = d[1];
	let value  = kv.Value;
	let action = kv.Action;

	console.log("process(", i.toString(), ")\n", key, action, value)
	
	switch(action) {
		case "Delete":
			yml = removeKey(yml, key);
			return yml
			break;
		case "Replace":
			yml = replaceKey(yml, key, value);
			return yml
			break;	
		case "Insert":
			yml = insertKey(yml, key, value, "end");
			return yml
			break;						
		default:
			new Notice("default pass")
			// console.log(yml)
			return yml;
			break;							
	}
}

function removeKey(yml, key) {
	yml = yml.split('\n')
	.filter(function(line) {
		return line.indexOf( key ) == -1;
	}).join('\n')
	// console.log("removeKey\n", yml)
	console.log("= removeKey; ", "key:", key, "\n", yml)
	return yml
}

function replaceKey(yml, key, value) {
	const regex = new RegExp(key + ":\\s.*", "g")
	const key_colon = key + ":"
	const space = " ";
	const apost = '"'
	
	yml = yml.replace(regex, 
		  key_colon
		+ space 
		+ apost
		+ value
		+ apost);
	console.log("= replaceKey; ", "key:", key, "; value:", value, "\n", yml)
	return yml;
}

_%>

Am I reinventing something here?

I know about the plugin “Metadata Menu” but I have not been able to make it work with templater. Almost nil documentation on the subject.

I have also tried using processFrontmatter but only works on the current template, not in the template that is being created, which I want to modify its properties.

Is there already a better way of doing this template customization without generating lots of templates?

1 Like

It seems like you’re doing this in a rather convoluted way. Why do you define loads of stuff, which you then need to delete or change later on? Why not just define what you need when you need it?

I’m imaginening that your template should rather look something like:

<%*
// Define my document types
const choices = [
  { name: "Minimal",
    fields: []
  },
  { name: "My Code",
    fields: [
      "aliases: ", "type: ", "related:  ",
      "summary:  ", "status:  ",  "rating:  ",
      "url:  ", "folder:  ",
      "partition:  ",  "language:  ", "OS: ",
      'archetype: "[[My Stuff]]"',
      'up: "[[Programming]]"',
      "status: active", 
    ]
  }, 
  { name: "My Computers",
    fields: [
      "aliases: ", "type: ", "related:  ",
      "summary:  ", "status:  ", "rating:  ",
      "assigned:  ", "purchased:  ", "brand:  ",
      "cost: ", "partition:  ",  "OS: ",
      'archetype: "[[My Stuff]]"',
      "tags: personal/assets",
      'up: "[[Equipment]]"',
      'location: "[[Home]]"',
    ]
  }, 
  { name: "My Devices",
    fields: [
      "aliases: ", "type: ", "related:  ",
      "summary:  ", "status:  ", "rating:  ",
      "purpose:  ", "purchased:  ", "brand:  ",
      "cost:  ", "partition:  ",
      'archetype: "[[My Stuff]]"',
      "tags: personal/assets",
      'up: "[[Equipment]]"',
      'location: "[[Home]]"',
    ]
  },
  { name: "My Repositories",
    fields: [
      "aliases: ", "type: ", "author:  ","related:  ",
      "summary:  ", "status:  ", "rating:  ",
      "purpose:  ", "url:  ", "folder:  ",
      "partition:  ",  "language:  ",
      'archetype: "[[My Stuff]]"',
		"tags: content/code",
		'up: "[[Programming]]"',
		"status: unknown"
	 ]
	},
]

// Select document type
const choice = await tp.system.suggester(t => t.name, choices)

// Build frontmatter
tR += "---\n" + choice.fields.join("\n") + "\n---\n"

// Include some templates
if (choice.name !== "Minimal" )
 tR += await tp.file.include("[[Default Minimal Template]]")
else
 tR += await tp.file.include("[[My Stuff Template Test]]") 
_%>

The logic here should be easier to follow instead of defining loads of stuff to delete afterwards, which hides what’s really happening. The outcome (when all entries are defined) should be the same in either case, but now you don’t need to use app.fileManager.processFrontMatter(). That is a tool which I preserve for adding templates or doing stuff after the file has been created, and I want to rework some issues.

2 Likes

Excellent advice!
You are right. I over-engineered the solution. It works but there is always Okam’s Razor, right?

I just have a little problem. When I apply your solution I get the fresh created page with duplicate ---. Like this:

---
aliases: 
type: 
related:  
summary:  
status:  
rating:  
url:  
folder:  
partition:  
language:  
OS: 
archetype: "[[My Stuff]]"
up: "[[Programming]]"
status: active
---
---
tags: []
up: 
related: []
uuid:  '20240217103747'
---

I know there are several ways of fixing this but wanted your opinion what would be better without disturbing much of your elegant code.

Shall I make the change:

Here?

// Select document type
tR += "---\n" + choice.fields.join("\n") + "\n---\n"

Or, here?

// Include some templates
if (choice.name !== "Minimal" )
 tR += await tp.file.include("[[Common Minimal Template]]")
else
 tR += await tp.file.include("[[My Stuff Template Test]]") 

Or in the templates themselves?

A combination? You’re most likely having the triple dashes in the other files, so it’s got to be removed somewhere for things to start behaving well…

Is there anything else in those other templates?outside of frontmatter stuff, that is.

Do you even need the other template of its only providing the uuid and duplicates of other fields?

I rather not like javascript solutions with search and replaces and YAML separator insertions.
I’m actually surprised our learned friend recommended using that.

If you are patient enough to wait out the outcome of a script being worked on at the end of this thread, we’re likely to see solutions for properties manipulation that are not confined to the current file and much more.

2 Likes

Me? What do you think is strange with which recommendation?

// Build frontmatter
tR += "---\n" + choice.fields.join("\n") + "\n---\n"

Obsidian can handle that, no?
Or are you an Insider on 1.5.5?

This is the latest iteration of the two templates: the selector and the footer template.

The footer template “Common Minimal Template”, which will be appended at the end of the selector template, is simple:


[[My Stuff]]  

# [[<% tp.file.title %>]]

The selector template “My Stuff Template Selector” is:

<%*
// Define my document types
const common = {
	  name: "Common",
	  fields: `
	    aliases: 
		type: 
		archetype: "[[My Stuff]]"
		summary:  
	    purpose: 
	    rating: 
	  `
	}

const choices = [
  { name: "Minimal",
    fields: []
  },
  { name: "My Code",
    fields: `
		gist_url: 
		partition: 
		folder: 
		language: 
		related: 
		status: active
		tags: content/code
    `
  }, 
  { name: "My Computers",
    fields: `
		purchased: 
		brand: 
		cost: 
		CPU: 
		RAM:  
		OS: 
		use: 
		assigned: 
		location: "[[At Home]]"
		up: "[[Equipment]]"
		related:  
		status: unknown
		tags: personal/assets
    `
  }, 
  { name: "My Devices",
    fields: `
		purchased: 
		brand: 
		cost: 
		use: 
		location: "[[At Home]]"
		up: "[[Equipment]]"
		related: 
		status: unknown
		tags: personal/assets
    `
  },
  { name: "My Repositories",
    fields: `
	    url:  
	    branch: 
	    partition: "[[2560x]]"
	    folder: 
	    language: 
	    up: "[[Programming]]"
	    related: 
	    status: unknown
	    is_synced: false
	    tags: content/code
	 `
	},
]

// Select document type
const choice = await tp.system.suggester(t => t.name, choices)

if (choice.name !== "Minimal" ) {
	// merge common with exclusive fields 
	let yml = common.fields + choice.fields
	
	/* Clean up: to remove blank lines, indentations, spaces at the start use 
	   this regex. Indentations are added when we try to make lists and object prettier
	*/
	const regex = /^(?=\n)$|^\s*|s*$|\n\n+/gm   
	yml = yml.split('\n')
	.map(function(line) {
		return line.replace(regex, "")})
	.filter(function(x) {return x}).join("\n");
	
	// Build frontmatter
	tR += "---\n" 
	tR += yml + "\n"
	tR += "uuid: " + "'" + tp.file.creation_date("YYYYMMDDHHmmss") + "'"
	tR += "\n---\n" 
	
	tR += await tp.file.include("[[Common Minimal Template]]")
}
else
	tR += await tp.file.include("[[Default Minimal Template]]") 

_%>

This is where all templates are built. It has some familiarity with other multi-template solutions but instead of the ugly nested “if” we use “switch”, lists, and objects.

The differences with @holroy’s are minimal:

  • I used a string instead of an object for the frontmatter declaration, It makes a faster entry and move them around.
  • Added an object for the common properties at the top
  • Added function to remove blank lines and indentations that occur when we indent text inside the object.

Sorry to butt in (again).
You are clearly comfortable with code.
May I interest you in a multi-template meta templater method as was suggested here:

  • It may be right up your alley.
1 Like

Nothing wrong with that combination, Gino. What was wrong was the inclusion of frontmatter from the other file, which they now has eliminated.

I’m not on the Insider build, so my Obsidian is still good to go.

1 Like

I understand.

The reason I mentioned it is because I myself am in the midst of (thinking of) re-writing my templates to include solutions that are programatically more solid, preferably less reliant on functions that may not be available in the future, devoid of workaround-ish hacks mentioned (insertions and replacements) and mainly making use of Obsidian API (processFrontmatter will normally insert YAML separator – unless one is currently on the version referred to) – which (Obsidian API) can, granted, also be changed in the future.

So when frequent posters and idea makers chip in here and there, I am trying to make use of their ideas if they can be applied to my use case. Usage and solutions can, of course, vary.

I am not particularly bothered about my situation (I am on no deadlines and my days are spent alternating between working on my material and finding ways to further customize or hone my Obsidian experience), and OP can clearly handle himself, but I’m thinking that the people who pop in on the forum for a quick fix should then be told the best possible and future-proof way to deal with their automation (templates).
I’m thinking of some syncretic solution or approach.

Lately processFrontmatter was the way to go (I could ping names who instructed the masses to use that). Now I know that OP knew about that and said that it

But I think that we (you) (OP can could help as well, being handy with code) should join forces and finalize the merge_frontmatter javascript code @Feralflora was working on in the other thread once and properly in a way that the forum users could download that and would only need to be instructed what to define and how in their markdown template files.
This is what I had in mind or envisaged: a sort of one size fits all solution – I’m just saying that I wouldn’t like that effort on the other thread taken slightly or abandoned and I’m kind of prodding you to help to have that crystallize into a full-fledged powerhouse of a code that would handle current file, other target files in a loop, etc.

My work here is more like that of an ambassador of the little people. Challenging you in computer questions (I can see now the script was not using Obsidian API but pure Js) was not of course prudent of me, but I was using this as an excuse to alert you to that other job at hand.

Best,
G.

1 Like

Don’t worry, I am already working on incorporating your and @holroy 's feedback on my mergeFrontmatter script, so it won’t be abandoned anytime soon. But of course, all help is welcome, if others want to chip in.

And btw, I think we have a similar vision for the end product. However, as holroy mentioned, there’s a lot of scenarios to cover.

1 Like

Btw, why not just have separate template files, and then make a picker for that? Seems like a more adaptable approach. I do that in my workflow.

@Feralflora I thought about that. There are some drawbacks in creating a template for each of the case uses in the creation of notes. The main disadvantage is that you have to open everyone of them to see the properties and code. It is wild and time consuming. I just gave up on following that approach.

On the other hand, having the properties for each of the note use cases in one file makes it easier to maintain and keep homogeneity. Also, the logic of the code can be modified in only one place.

Isn’t this beautiful?

const choices = [
  { name: "Minimal",
    fields: []
  },
  { name: "My Code",
    fields: `
		gist_url: 
		partition: 
		folder: 
		language: 
		related: 
		status: active
		tags: content/code
    `
  }, 
  { name: "My Computers",
    fields: `
		purchased: 
		brand: 
		cost: 
		CPU: 
		RAM:  
		OS: 
		use: 
		assigned: 
		location: "[[At Home]]"
		up: "[[Equipment]]"
		related:  
		status: unknown
		tags: personal/assets
    `
  }, 
  { name: "My Devices",
    fields: `
		purchased: 
		brand: 
		cost: 
		use: 
		location: "[[At Home]]"
		up: "[[Equipment]]"
		related: 
		status: unknown
		tags: personal/assets
    `
  },
  { name: "My Repositories",
    fields: `
	    url:  
	    branch: 
	    partition: "[[2560x]]"
	    folder: 
	    language: 
	    up: "[[Programming]]"
	    related: 
	    status: unknown
	    is_synced: false
	    tags: content/code
	 `
	},
]

I don’t understand why you opted for just one string for all the fields in a given variant, which requires the extra handling later on. It’s personal preference, but I think it looked a lot nicer with a list of the separate fields. If each was on its own line it would also be easy to copy back and forth if you change your mind on which field to use where, or want the order slightly altered, and so on.

I liked the extraction of the common fields too, and think all in all this is a solution I see as a nice approach for your use case.

You could use others like that other script, or even going all in and defining file classes through the Metadata menu plugin (or similar), but one just needs to define what’s the best alternative for one self.

1 Like

Could you please point me to couple of examples of Templater working along with Metadata Menu? I found nothing.

Main reason is to avoid dealing with two extra apostrophes. Second, little bit more visually appealing to me. The handling of the blank lines and space/tabs is not a big deal. Which reinforces the point of having templates that share similarities in only one template file to gain central control over functions that are being called by each template of the dozens we have around.

Just for the fun of it I’ve implemented a simple FileClass for some test files of mine, and I’m not referring necessarily referring to a combination of those two as much as that your various notes belong to various FileClasses and it could describe each of them with corresponding fields and structure.

One could imagine having a simple template choosing the correct file class, and then allowing that plugin to fill in the blanks, and keep the structure intact and coherent across the board.

I just see that the plugin could offer more along the lifespan of a note with regards to keeping some metadata structure, then what Templater can initially do for any given file. But I’m not proficient enough to predict the future of whatever solution or system someone wants to use, or dictate which solutions/plugins will exist in the future or not.

1 Like

@holroy were you able to use Templater and Metadata Menu together? Like calling the MM API from a Templater script.

That I haven’t even tried, but it sounds like an interesting idea, as that could fill out all the fields for a given class.

Update: The API do indeed offer an insertMissingFields() which also can accept the file class. This I’m going to play with some day…