Templater script to find the maximum value of a frontmatter key in a group of notes within a folder, and increment it by one to assign to the same key in the created note

What I’m trying to do

I have a folder with a series of session notes. I want to be able to create a new session note in that folder from a Templater template, and have its serial number automatically calculated and added to the frontmatter variable ‘sessionNumber’. It seems simple, but I have spent more than a week trying in vain to do it. I am new to Dataview, Templater, and JavaScript, so this has not helped, and I have been learning and troubleshooting as fast as I can.

Things I have tried

I have written a script in CommonJS module specification that I call using

<% tp.user.getNewSessionNumber %>

from the session note template. Its purpose is to find the new session number by checking the largest existing value of the sessionNumber key in the frontmatter of the existing notes in the folder, and adding 1. At the moment, I am attempting to use Dataview to do this.

My script is finally working (it can be called by the template and throws no errors when it has safeguards). A new note is created with much of the desired initial content, but I cannot get it to assign the right session number (I get the result undefined). What I am currently trying to run is as follows:

function getNewSessionNumber (tp) {
    let thisPatientSessionFolderPath = tp.file.folder(true);
   
	// Get an array of all the pages in the specified folder
	let pages = app.plugins.plugins.dataview.api.pages(`"${thisPatientSessionFolderPath}"`);
	console.log("pages array: " + pages);
	
	// Create a new array to store the session numbers
	let sessionNumberList = [];
	
	// Loop through each page and check if it has a session type and a valid sessionNumber property
	for (let i = 0; i < pages.length; i++) {
  		let page = pages[i];
		console.log("pages("+ i + "): " + sessionNumberList);
		sessionNumberList.push(page.frontmatter.sessionNumber);
	}
	console.log("sessionNumberList: " + sessionNumberList);
	
	// Get the largest session number in the sessionNumberList array
	let largestSessionNumber = 0;
	for (let i = 0; i < sessionNumberList.length; i++) {
  		let sessionNumber = sessionNumberList[i];
		console.log("sessionNumberList(" + i + "):" + sessionNumber)
  		if (sessionNumber > largestSessionNumber) {
    		largestSessionNumber = sessionNumber;
  		}
	}
	
	// If session number list is empty, give it one value equal to 0
	if (sessionNumberList.length === 0) {
  		sessionNumberList = [0];
	}
	
	// Calculate the next session number by adding 1 to the largest session number
	let newSessionNumber = largestSessionNumber + 1;
	console.log('newSessionNumber: ' + newSessionNumber);

    return newSessionNumber;	
}
module.exports = getNewSessionNumber;

This is an expanded and simplified version of the script in order to troubleshoot it. It has the safeguards removed. The previous version which is more compact and completed, and to which I may return when I solve the issues replaces the for loop with

//Compact alternative to for loop, with safeguards
	let sessionNumberList = app.plugins.plugins.dataview.api
    	.pages(`"${thisPatientSessionFolderPath}"`)	
  	.where(page => page.frontmatter && page.frontmatter.type === 'session' && Number.isInteger(page.frontmatter.sessionNumber))
  		.map(page => page.frontmatter.sessionNumber);

To find out where the problem lies, I created a test folder with only one session note whose content is simply

---
type: session
sessionNumber: 1
---

This means that I am sure that note in the folder has the required frontmatter.

When I try to create the new session note from the template, I get the following error in the Obsidian console:

Templater Error: Template parsing error, aborting. 
 Cannot read properties of undefined (reading 'sessionNumber')
log_error @ plugin:templater-obsidian:61
errorWrapper @ plugin:templater-obsidian:81
await in errorWrapper (async)
write_template_to_file @ plugin:templater-obsidian:3718
on_file_creation @ plugin:templater-obsidian:3819

I cannot understand this, as sessionNumber is clearly defined with the value 1.

What is essentially happening when I run the full script is that the script finds no session numbers to populate the sessionNumberList array, which results in it being empty.

I am really stumped as to what is going on here. If anyone can see the problem, I would be grateful if you could let me know.

In your original query, the main error is two-folded:

  1. You don’t actually check whether it has any sessionNumber (nor type), just blindly attempt to use it
  2. The correct name of a variable within the frontmatter, in your context is page.file.frontmatter.sessionNumber

The first is not critical the way you’ve written your query, it just populates the sessionNumberList with potentially many undefined entries. The second caused every entry to be undefined. So after correcting that, the query actually produced a value.

Code review of your script

Even though the script now produces a value, there are some things to be said about the code itself, and some practices which would make it look nicer and easier to debug. Lets tackle some of these, if you don’t mind me doing so.

Use for .. of construct

The way you traverse the various arrays, which is the old and trustworthy way, can be made a whole lot simpler. Let’s keep the focus on the loop I’ve already quoted, and present another version of it:

for (const page of pages) {
  console.log("page: ", page, " sessionNumberList: ", sessionNumberList)
  if ( page.type == "session" &&
       page.file.frontmatter.sessionNumber ) { 
    sessionNumberList.push( page.file.frontmatter.sessionNumber )
  }
}

For some alternate version of this construct, amongst them one which includes the index again, see javascript - Access to ES6 array element index inside for-of loop - Stack Overflow

Do you really need the sessionNumberList ?

Your main goal is to get the maximum session number, but do you really need the list of them? And if you wanted the list, do you really need a separate loop to find the higher number?

The answer I think is “No”, I most likely rather use an intermediate variable to keep track of the highest number, and keep updating that instead of the list, so something like this:

  let largestSessionNumber = 0
  
  for (const page of pages) {
    const pageSessionNo = page.file.frontmatter.sessionNumber
    console.log("page: ", page.file.name,  "this number: ", pageSessionNo, " highest so far: ", largestSessionNumber)
    if ( page.type == "session" && pageSessionNo &&
         pageSessionNo > largestSessionNumber ) {
      largestSessionNumber = pageSessionNo
    } 
  }
The entire script so far

Doing both of these, and removing the unnecessary semicolon (which I know is a matter of personal preference), the script now looks like:


function getNewSessionNumber (tp) {
    let thisPatientSessionFolderPath = tp.file.folder(true)
   console.log("folder: ", thisPatientSessionFolderPath)
   
	// Get an array of all the pages in the specified folder
	let pages = app.plugins.plugins.dataview.api.pages(`"${thisPatientSessionFolderPath}"`)
	console.log("pages array: " + pages);
	
 	let largestSessionNumber = 0

	for (const page of pages) {
      const pageSessionNo = page.file.frontmatter.sessionNumber
      console.log("page: ", page.file.name,  "this number: ", pageSessionNo, " highest so far: ", largestSessionNumber)
      if ( page.type == "session" && pageSessionNo &&
           pageSessionNo > largestSessionNumber ) {
        largestSessionNumber = pageSessionNo
     } 
   }

  // Calculate the next session number by adding 1 to the largest session number
  const newSesstionNuimber = largestSessionNumber + 1
  console.log('newSessionNumber: ' + newSessionNumber);
  return newSessionNumber;
}
module.exports = getNewSessionNumber;

And it does produce the highest session number within this folder.

Slightly alternate approach

Another approach, resembling your second query, is to do even more of the looping and selecting within the dataview environment. So If we were to have this within a note to showcase the next session number, we could use something like the following:

Next sessionnumber: `$= dv.pages('"' + dv.current().file.folder + '"')
              .where(p => p.type == "session" && p.sessionNumber)
              .sort(p => p.sessionNumber, "desc")
              .map(p => p.sessionNumber)
              .limit(1)[0] + 1 `

Here we utilise the somewhat cleaner syntax of the where clauses related to doing the inline query which dataviewjs allows us to do. In addition, we select just the session number, limit to a list of one entry, and pick out that entry before adding 1 to get the next number.

Within the templater user function context this would look like:

function getNewSessionNumber (tp) {
  const dv = app.plugins.plugins.dataview.api
  const thisPatientSessionFolderPath = tp.file.folder(true)
  console.log("folder: ", thisPatientSessionFolderPath)
  
   return dv.pages(`"${ thisPatientSessionFolderPath }"`)
              .where(p => p.type == "session" && p.sessionNumber)
              .sort(p => p.sessionNumber, "desc")
              .map(p => p.sessionNumber)
              .limit(1)[0] + 1
}

module.exports = getNewSessionNumber;

Potentially with the removal of the console.log() lines… :slight_smile:

This query should provide the correct result, and it’s compact and nice and has safeguards, but still it’s not the first generation of such a script, and it’s potentially a little hard to read if you’re not accustomed to javascript, dataview and templater.


So in summary, your original long query was very close to actually working, as it only need the change to page.file.frontmatter.sessionNumber, though optimisations could be made. Two versions was presented do those optimisations, and all three variations (including your correct one) should produce the correct value when using in conjunctions with templater code like:

<% tp.user.getNewSessionNumber(tp) %>
1 Like

Thank you so much @holroy for your prompt and most helpful reply.

Specifically, thank you for identifying the problem with my function, for taking the time to explain how my code could be improved, and for showing me the most concise way of implementing it.

This has not only enabled me make this function work, but has also been instructive to me in the ways of Templater, Dataview and JavaScript. There is nothing like struggling with a problem, and then having someone who understands it guide you through there parts where they see that you are falling short. I will revisit your answer as I continue my learning.

1 Like

Well presented answer, and I appreciate that I learn something new from reading it. Kudos!

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.