Templater script : Create a new template -> open in split view -> paste link to the new note

What I’m trying to do

I have templates for entities - people, places, topics etc. While adding these notes from my daily notes I want to remove any friction for creating specialized notes for these “entities”. This is what I want to be triggered on a keyboard shortcut

  1. Create a new note as per the template I choose
  2. Rename the file as per some file naming conventions (for better searchability)
  3. Move the file to a specific folder
  4. Open the newly created note in a right split view
  5. Paste a link to the newly created note in the original note

I have been able to do 1,2 and 3 above using templater. I am not sure how to get 4 and 5 above (opening in the right pane and pasting a link in the original file)

Things I have tried

I tried searching the forum for templater scripts for 4 and 5, tried using the note refactor plugin etc - but was unable to get functionality 1-5 above all in a single step.

1 Like

That’s a great idea! I’ve used it to improve my new note template.

Here’s how you do the split:

const file = app.vault.getAbstractFileByPath('path/to/new_note.md')
// Create the new leaf
const newLeaf = this.app.workspace.getLeaf('split')
await newLeaf.openFile(file)
// Set the view to edit mode
const view = newLeaf.getViewState()
view.state.mode = 'source'
newLeaf.setViewState(view)
// Give focus to the new leaf
this.app.workspace.setActiveLeaf(newLeaf, { focus: true })
// Move the cursor to the end of the new note
this.app.workspace.activeLeaf.view.editor.setCursor({line: 999, ch: 0})

And here’s my full template which does all 5 of your steps. This is the template I use for all my new notes, with the new addition of the split (which I really like!):

async function main() {
  const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object
  tp.user._init_template(this)

  const templates = [
    {
      label: 'Default note',
      template: 'Templates/Default note'
    },
    {
      label: 'Project',
      template: 'Templates/Project',
      destination: '05 Project management/Projects'
    },
    {
      label: 'Person',
      template: 'Templates/Person note',
      destination: 'People',
      title: '👤'
    }
  ]

  // Present the list of templates to choose from
  const chosen = await this.tp.system.suggester(templates.map(x => x.label), templates)
  if (!chosen) return ''
  const name = (await this.tp.system.prompt('Name for the new file:', chosen.title || '')) || moment().format('YYYY-MM-DD HH.mm.ss')
  const addLink = await this.tp.system.prompt('Insert link in the current file?', 'Enter for yes or Escape for no')
  const destination = chosen.destination || this.tp.file.folder(true)
  const res = await this.createFromTemplate(chosen.template, name, destination, !addLink)
  if (addLink) {
    // Open the new file in a pane to the right
    const file = app.vault.getAbstractFileByPath(`${destination}/${name}.md`)
    // Create the new leaf
    const newLeaf = this.app.workspace.getLeaf('split')
    await newLeaf.openFile(file)
    // Set the view to edit mode
    const view = newLeaf.getViewState()
    view.state.mode = 'source'
    newLeaf.setViewState(view)
    // Give focus to the new leaf
    this.app.workspace.setActiveLeaf(newLeaf, { focus: true })
    // Move the cursor to the end of the new note
    this.app.workspace.activeLeaf.view.editor.setCursor({ line: 999, ch: 0 })
  }
  return res
}
module.exports = main

It’s written in Templater “Users script” format, so you’ll need to put it in your users script folder, and then call it from a normal template like this:

<% tp.user.newNote(tp) %>

Assign that template to a hotkey like Ctrl + N, for new note.

When you run it, you’ll be presented with a list of new note templates, which will be created in the appropriate folder and with the appropriate content:

image

You can chose whether to add a link into the current file, which will open up the split to the right, or you can simply open into the new note without adding a link in the current file:

6 Likes

Out of curiosity, could you simply put this code into a template ? Must it be in a user script ? Thanks…

Absolutely, although you would need to make a few changes, like removing all the this.s.

It doesn’t need to be in a user script - I just do it that way because I build up a library of functions that are able to call each other. So I might decide to start the “New template” script from somewhere else, and it helps to have the function available for it.

All my Templater scripts start by calling this init user script, which provides a bunch of useful functions like manipulating the contents of files, changing the edit mode, and makes Dataview and Templater functions available inside the user script:

function main(target) {
  const dv = app.plugins.plugins.dataview.api
  const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object

  target.app = app
  target.dv = dv
  target.tp = tp
  target.page = dv.page(tp.file.path(true))
  target.file = target.page.file
  target.view = app.workspace.activeLeaf.view

  target.sleep = async function (ms) {
    await new Promise(resolve => setTimeout(resolve, ms))
  }

  /**
   * Create a new file from a template, and return the link or open the file
   * @param {string} templatePath 
   * @param {string} newNoteName 
   * @param {string} destinationFolder - Path to the destination folder. Defaults to current folder
   * @param {boolean} openNewNote - Open the note in a new window, or return a link
   * @returns 
   */
  target.createFromTemplate = async function (templatePath, newNoteName, destinationFolder, openNewNote) {
    destinationFolder = destinationFolder || tp.file.folder(true)
    await tp.file.create_new(tp.file.find_tfile(templatePath), newNoteName, openNewNote, app.vault.getAbstractFileByPath(destinationFolder))
    return openNewNote ? '' : `[[${newNoteName}]]`
  }

  /**
   * Returns true if file is in editing mode
   * @returns {boolean}
   */
  target.isEditMode = function () {
    const curr = app.workspace.activeLeaf.getViewState()
    return curr.state.mode === 'source'
  }

  /**
   * Set file to edit or read mode
   * @param {boolean} canEdit 
   */
  target.setEditMode = function (canEdit) {
    const curr = app.workspace.activeLeaf.getViewState()
    curr.state.mode = canEdit ? 'source' : 'preview'
    app.workspace.activeLeaf.setViewState(curr)
    if (canEdit) {
      target.view.editor?.focus()
    }
  }

  /**
   * Get the text contents of a file, specified by string path
   * @param {string} [path] Optional path, otherwise use current file
   */
  target.getContents = async function (path) {
    const file = app.vault.getAbstractFileByPath(path || target.tp.file.path(true))
    return app.vault.read(file)
  }

  /**
   * Replace the contents of a file
   * @param {string} contents The new file contents
   * @param {string} [path] Optional path, otherwise use current file
   * @returns 
   */
  target.setContents = async function (contents, path) {
    const file = app.vault.getAbstractFileByPath(path || target.tp.file.path(true))
    return app.vault.modify(file, contents)
  }

  /**
   * Get the text of the current line, or false if not in editing mode
   * @returns {false|string}
   */
  target.getCurrentLine = function () {
    if (!target.isEditMode()) {
      // Not in edit mode, current line is unknowable
      return false
    } else {
			const lineNumber = target.view.editor.getCursor().line
			return target.view.editor.getLine(lineNumber)
    }
  }

  target.goToFile = async function (path) {
    const file = app.vault.getAbstractFileByPath(path)
    if (path !== target.file.path) {
      await app.workspace.getLeaf(false).openFile(file)
    }
  }
}
module.exports = main
7 Likes

What do you pass in as target? just an empty object or something more complicated?


Edit: never mind, I see that you pass this in as the parameter in your original script.

I pass in this, which is the Templater execution context.

You can see it in the 3rd line of the script in my first reply in this thread:

async function main() {
  const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object
  tp.user._init_template(this)

Totally missed that lol.

One question though, the functions you define make use of the app and tp variables that you assign above, but would you not need to use this.app and this.tp?

Not at all - from the perspective of the inner functions it has already been defined in the outer context.

Neat, thank you

Have a look at Quick Add. In combination with a template, this can easily be done. The only caveat is that you have to have the original file open and selected for it to work.

If you can’t figure out how, I can post an example later.

1 Like

Why you attach TP again? it’s already in the context of every template, isn’t it?

In script user functions, you can still access global namespace variables like app or moment.

However, you can’t access the template engine scoped variables like tp or tR. If you want to use them, you must pass them as arguments for your function.

https://silentvoid13.github.io/Templater/user-functions/script-user-functions.html#global-namespace

Sorry, maybe I misunderstood your explanation. I got the impression that you were attaching those to use within the template, not in user functions/scripts. In fact, that snippet is already a user script, so you are already proving that quoute wrong

You might be misunderstanding what a Templater user script is.

Anything that is called from tp.user will not have tp available in it. Only code inside a <%* %> section will have tp available. This is in the docs I quoted above.

Yes, but you said that you are calling your user script from a template, and attach several things to the template object, some of them being tp and app, which are already available in the template scope

Re-reading the conversation agan now I see what you meant.

I thought you were calling the _init_template function from a templater template, not a user script. Now I see what do you mean.
I thought your flow was:

start with a templater template block<% tp.user.main(this) %> that calling main (and I thought, why not pass tp as arguemt??) and then the main doing a lot more of initialization.

I was planning to imitate your flow, and have all my templater templates (not user scripts) call a init function.

The other reason it’s useful is that it makes code which is reusable anywhere.

If I’m defining tp myself, I can call that same script from Dataview and it will still work. If I’m relying on Templater to pass the tp variable, then that script is only useful from Templater.

That is indeed very interesting, worth going to my garden :smile:
Two extra questions if you don’t mind:

  1. does the tp properties that depend on the currently active file properly update every time you call your method?
  2. Why not just put everything in user scripts and call tp.user ? Even if it is through the longer app.plugins.plugins['however-templater-is-called'].user.yourScript ?

Thanks for sharing your findings!

It does indeed :+1:

That is what I do - see the 3rd line in my first script posted above:

async function main() {
  const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object
  tp.user._init_template(this)

Sorry, my question was not clear enough.
I assume that main is just a templater user script, right? Or that is what you do in other places like dataviewjs blocks? What I mean is, rather than attaching all the methods in one go, why not just access each one individually as separate user scripts. So instead of what you do like

tp.user.main(this)
this.utility_a()
this.utility_b()

Do it like this:

  const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object
tp.user.utility_a()
tp.user.utility_b()

By the way @AlanG , do you name all your user scripts functions main? That also confused me a bit, I was not sure what things were user scripts and what things were templates. Do you mind outlining your templte structure? What calls what and how each file is nmed?