Skip to content

Templater multi-select suggester

Posted on:May 10, 2023

We can (kindof) do a multi-select suggester by sticking the suggester in a while loop, letting the user keep selecting options and adding them to a list of selected options until the user hits the escape key to exit out of the loop.

Here’s a simple example of how to do this.

<%*
// List of items
let items = ["Value1", "Value2"];
// List of items that are selected in the suggester
const selectedItems = [];
// Looping to keep suggester modal open until escape is hit
while (true) {
  const selectedItem = await tp.system.suggester(item => item, items);
  // If escape is hit, break out of loop to close suggester modal
  if (!selectedItem) {
    break;
  }
  // Otherwise, add selected item to list of selected items, remove item from multi-select, and keep looping
  selectedItems.push(selectedItem);
  items = items.filter(item => item !== selectedItem);
}
if (selectedItems.length > 0) {
  // Do something with selected items
  tR += selectedItems;
}
_%>

Here’s an example getting a list of tags as a multi-select and setting those tags in the current file’s frontmatter.

<%*
// List of tags without # symbol (# denotes a comment in YAML)
let tags = Object.keys(app.metadataCache.getTags()).map(x => x.replace("#", ""));
const selectedTags = [];
while (true) {
  const selectedTag = await tp.system.suggester(tag => tag, tags);
  if (!selectedTag) {
    break;
  }
  selectedTags.push(selectedTag);
  tags = tags.filter(tag => tag !== selectedTag);
}
// If any tags were selected
if (selectedTags.length > 0) {
  const file = tp.file.find_tfile(tp.file.title);
  await app.fileManager.processFrontMatter(file, (frontmatter) => {
    // Ensure tags exist in frontmatter
    frontmatter["tags"] = frontmatter["tags"] || [];
    // Add selected tags to frontmatter
    frontmatter["tags"].push(...selectedTags);
  });
}
_%>

User script

To make this easier to use, we could abstract this functionality away into a user script.

/**
 * Spawns a multi-select suggester prompt and returns the user's chosen items.
 * @param {object} tp Templater tp object.
 * @param {string[] | ((item: T) => string)} textItems Array of strings representing the text that will be displayed for each item in the suggester prompt. This can also be a function that maps an item to its text representation.
 * @param {T[]} items Array containing the values of each item in the correct order.
 * @param {boolean} throwOnCancel Throws an error if the prompt is canceled, instead of returning a null value.
 * @param {string} placeholder Placeholder string of the prompt.
 * @param {number} limit Limit the number of items rendered at once (useful to improve performance when displaying large lists).
 * @returns A list of selected items.
 */
async function multiSuggester(
  tp,
  textItems,
  items,
  throwOnCancel = false,
  placeholder = "",
  limit = undefined
) {
  // List of items that are selected in the suggester
  const selectedItems = [];
  // Looping to keep suggester modal open until escape is hit
  while (true) {
    const selectedItem = await tp.system.suggester(
      textItems,
      items,
      throwOnCancel,
      placeholder,
      limit
    );
    // If escape is hit, break out of loop to close suggester modal
    if (!selectedItem) {
      break;
    }
    // Otherwise, add selected item to list of selected items, remove item from multi-select, and keep looping
    selectedItems.push(selectedItem);
    const selectedItemIndex = items.findIndex((item) => item === selectedItem);
    if (selectedItemIndex >= 0) {
      items.splice(selectedItemIndex, 1);
      if (Array.isArray(textItems)) {
        textItems.splice(selectedItemIndex, 1);
      }
    }
  }

  return selectedItems;
}

module.exports = multiSuggester;

Then use it in our template like this.

<% tp.user.multiSuggester(tp, item => item, ["Value1", "Value2"]) %>