Updating frontmater programatically

Hi all

I am trying to setup a script to switch a project note status between active and closed.
This templater script will be triggered with a button.

The status is updated but right after the frontmatter is updated, the previous status comes back to its original state.

Here is my code :

---
status: active
tags: [project]
---

<%*
let file = app.workspace.getActiveFile();
const {update} = app.plugins.plugins["metaedit"].api;
const status = tp.frontmatter.status;

if (status === "active") {
 await update("status", "closed", file );
 console.log("Closing...")
}
if (status === "closed") {
await update("status", "active", file );
console.log("Opening...")
}
-%>

Any idea why this is happening?

When I use the new app.fileManager.processFrontMatter, the console shows an updated frontmatter, but the file itself is not modified.
I am probably missing something…

let file = app.workspace.getActiveFile();
const status = tp.frontmatter.status;

if (status === "active") {
 app.fileManager.processFrontMatter(file, (frontmatter) => {
    frontmatter["status"] = "closed";
    frontmatter["test"] = "test";
    console.log(frontmatter);
    });
 console.log("Closing...")
}
if (status === "closed") {
app.fileManager.processFrontMatter(file, (frontmatter) => {
	console.log(frontmatter);
    frontmatter["status"] = "active";
    console.log(frontmatter);
    });
console.log("Opening...")
}
-%>

Here is the console log:

image

How do you trigger the template? Are you running “Templater: Replace templates in the active file”? Are you doing an “Templater: Open Insert Template modal”? Are you triggering it through a button, or other means?

The thing is I suspect your code is actually working, but when the template concludes it’s work it also updates the file, but now based upon the previous version, hence the revert of the last changes done by your script.

I’m not sure how to counter that effect, but I do have some ideas I want to test out (tomorrow, not tonight). However, to get the best test setup it would be nice to know how you trigger your template.

One other thing you could test, was to change from <%* in the project note, into <%*+, and then switch to reading mode. Does the change then happen, and stay changed?

Logic flaw in your first version

In you first version you say:

  • If status is active, change to closed
  • Afterwards, if status is closed then change to active

So, what’s the net effect of that? A change from active to closed to active again… (It does depend a little on other logic, but it doesn’t look good like it is now)

You should change the middle section to:

} else if ( status === "closed" ) {

This should be done, but there might be other issues, as well

I realize the following side steps the matter of getting the API to modify the frontmatter and it’s not a generalized solution, but in my testing it does toggle the values.

<%*
const status = tp.frontmatter.status;
let change = false;
let oldStat = '';
let newStat = '';

if (status === 'active')
{
   change = true;
   oldStat = 'status: active\n';
   newStat = 'status: closed\n';
}
if (status === 'closed')
{
   change = true;
   oldStat = 'status: closed\n';
   newStat = 'status: active\n';
}
if (change)
{
   const content = tp.file.content.split(oldStat).join(newStat);
   await app.vault.modify(app.workspace.getActiveFile(),content);
}
-%>

This could have also been done more concisely with some minor code redundancy.

<%*
const status = tp.frontmatter.status;
if (status === 'active')
{
   const content = tp.file.content.split('status: active\n').join('status: closed\n');
   await app.vault.modify(app.workspace.getActiveFile(),content);
}
if (status === 'closed')
{
   const content = tp.file.content.split('status: closed\n').join('status: active\n');
   await app.vault.modify(app.workspace.getActiveFile(),content);
}
-%>

Thanks a lot for your help.

The final goal is to attach the template to a button in order to trigger the code.

Thanks for your comment on the logic issue. I was probably tired yesterday night :slight_smile:. Behaviour is the same though.

So far, I tried these triggers which all produce the same behaviour:

  • Button triggering a template
  • Templater: Replace templates in the active file

Your assumption is probably correct as the file is getting change when the code is triggered and then imeediately changed back.

So to summarize, when I trigger the code:

  • The value flickers once and then shows its initial state
  • The template disapears (expected)

When I click ctrl + z

  • the correct value is shown :slight_smile:
  • the template comes back

This is so weird…

I tried deactivating all plugins except templater. Same result.

Oh I also tried an example available in chhoumann/MetaEdit: MetaEdit for Obsidian. And it works. But it is displaying a table listing tasks and I don’t want a table…

const {update} = this.app.plugins.plugins["metaedit"].api
const {createButton} = app.plugins.plugins["buttons"]

dv.table(["Name", "Status", "Due Date", ""], dv.pages("#projects")
    .sort(t => t["due-date"], 'desc')
    .where(t => t.status != "Completed")
    .map(t => [t.file.link, t.status, t["due"], 
    createButton({app, el: this.container, args: {name: "Close"}, clickOverride: {click: update, params: ['status', 'closed', t.file.path]}})])
    )

This one even works if the table is inside the targeted file…

Hi

Thanks a lot for you answer.

I tried your code and the result is the same. The value is updated and immediately flicks back to its original value.

See my other answer for more details.

Sorry for the brevity, I’m on mobile and in a little bit of a rush, but do the following work:

```dataviewjs 
const {update} = this.app.plugins.plugins["metaedit"].api
const {createButton} = app.plugins.plugins["buttons"]

createButton({app, el: this.container, args: {name: "Close"}, clickOverride: {click: update, params: ['status', 'closed', t.file.path]}})
   
```

Either like this, or with some change to the this.container bit?

This is showing an error:

Evaluation Error: TypeError: Cannot read properties of undefined (reading 'path')
    at eval (eval at <anonymous> (plugin:dataview), <anonymous>:4:130)
    at DataviewInlineApi.eval (plugin:dataview:18370:16)
    at evalInContext (plugin:dataview:18371:7)
    at asyncEvalInContext (plugin:dataview:18381:32)
    at DataviewJSRenderer.render (plugin:dataview:18402:19)
    at DataviewJSRenderer.onload (plugin:dataview:17986:14)
    at e.load (app://obsidian.md/app.js:1:864580)
    at DataviewApi.executeJs (plugin:dataview:18921:18)
    at DataviewPlugin.dataviewjs (plugin:dataview:19423:18)
    at eval (plugin:dataview:19344:124)

I forgot a reference to the file at end.

The following works in my sandbox:

Status:: isjas

```dataviewjs
const {update} = this.app.plugins.plugins["metaedit"].api
const {createButton} = app.plugins.plugins["buttons"]

createButton({app, el: this.container, args: {name: "Done!"}, clickOverride: {click: update, params: ['Status', 'Done!', dv.current().file.path]}})
createButton({app, el: this.container, args: {name: "or not!"}, clickOverride: {click: update, params: ['Status', 'isjas', dv.current().file.path]}})
```

However, this is two buttons, and so far I’ve not found how to toggle the value more than once. And that has to do with the dataviewjs script and caching, and I’m still on mobile so debugging is a little tricky.

More fiddling on a way too small keyboard, and a call to getPropertyValue I arrived at this:

Status:: Completed

```dataviewjs
const {update, getPropertyValue} = this.app.plugins.plugins["metaedit"].api;

const toggleButton = (pn, pv, pvalt, fpath) => {
  const btn = this.container.createEl('button', {"text": "Toggle"});
  const file = this.app.vault.getAbstractFileByPath(fpath)
  btn.addEventListener('click', async (evt) => {
    evt.preventDefault();
    const current = await getPropertyValue (pn, file)
    const newValue = current == pv ? pvalt : pv;
    await update(pn, newValue, file);
  });
  return btn; 
}

toggleButton(
  'Status', 
  'Completed',
  'Not done',
  dv.current().file.path)
```

And it keeps toggling the value at each press! :grinning:

2 Likes

Wow that’s perfect. It works.

Thanks a lot @holroy and @ninjineer!

I am still wondering why it works in dataviewjs but not in templater. If someone still wants to look at it I guess it would be instructive.

In the meantime, here is my final code. I have added the closed date modification as well as the interactive button name.

const {update, getPropertyValue} = this.app.plugins.plugins["metaedit"].api;

const toggleButton = async (pn, pv, pvalt, fpath) => {
	
	let btnName = "";
	
	const file = this.app.vault.getAbstractFileByPath(fpath)
	const initCurrent = await getPropertyValue (pn, file)
	
	if (initCurrent == "active") {
		btnName = "Close Project";
	} else {
		btnName = "Reopen Project";
	}
	const btn = this.container.createEl('button', {"text": btnName});
	
	btn.addEventListener('click', async (evt) => {
		evt.preventDefault();
		const current = await getPropertyValue (pn, file)
		console.log(moment().format('YYYY-MM-DD'));
		if (current == pv) {
		await update(pn, pvalt, file);
		await update("closed", "", file);
	} else {
		await update(pn, pv, file);
		await update("closed", moment().format('YYYY-MM-DD'), file);
	}
	});
	return btn; 
}

toggleButton(
  'status', 
  'closed',
  'active',
  dv.current().file.path)

Part of it is that the dataviewjs query is by nature presenting a dynamic result, but the code in the source file never changes (until you manually edit , of course). So dataview doesn’t change the source file, nor has the need by itself to save the file.

However, Templater by nature is changing the source file, and will always try to save the new and updated file. And this creates, from my point of view, a race condition when the templater code actually changes the source file outside of the stuff the templater code knows it’s changing.

I’ve not tested whether it’s possible to circumvent this somehow, but it kind of reminds me of sitting on a branch you’re trying to saw off…

Oh yes I see…
This is a pure templater question then.

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