Templater and tp.file.include issues with information shared between templates
Hi. I'd like to share something that I've found out after “investing” a few hours on this task. I am working on modularizing my templates since some time ago (this modularization of mine goes along my older post for template evolution: https://www.reddit.com/r/ObsidianMD/s/C45nNSFC9i / Vault evolution and changes) and during the latest session, I've got stuck with a behavior I couldn't find on Templater docs (at least, not clearly stated, as it was more or less what was present on the examples there).
What I've found is that Templater compiles all tags from all nested includes before executing any script. When a template uses <% myVar %>, Templater tries to resolve it during the parsing phase, i.e., when the file is created and the template is added to it. This occurs before any <%* %> blocks within the parent template are processed in a second step.
During my debugging session, I've tried:
- Writing to
tDatainside an include that callsawait tp.file.rename()but I've found that the rename resets the context, so values written inside that include are gone and can't be used by “upper level” templates - Using
app._tDatato persist this data technically works, but unfortunately the information persists between template executions, so if you create two notes back to back the second one picks up stale data from the first, generating incorrect output on that second note (it is like the template is always one execution behind for the values it will use) - Using
<%* tR += ... %>inside includes to protect the output still hits the same issue when the variable doesn't exist yet, so you just add extra markup for no change at all in the final output
sequenceDiagram
participant User
participant Templater
participant Parent as Parent Template
participant Include as Included Template
participant File as File
User->>Templater: Create new file
Templater->>Parent: Load parent template
Note over Templater: Phase 1: Parsing
Resolve all <% %> before running scripts
Templater->>Include: Load includes
(also parsed first)
Include-->>Templater: Fails if <% var %> depends on parent
Note over Templater: Phase 2: Execute <%* %> blocks
Templater->>Parent: Run initial <%* %> block
Set tData
Parent-->>Templater: tData populated
Parent->>Templater: Call tp.file.include()
Templater->>Include: Execute include
May read tData
Include->>Templater: Execute await tp.file.rename()
Templater->>File: Rename file
Note over Templater: Context reset
tData lost
Parent-->>Templater: Continue execution
(but without data written inside include)The pattern that finally worked for me is what Templater encourages (implicitly) in its own examples:
- Do all your prompts and calculations in the initial
<%* ... -%>block of the parent template - Store anything you need to share with includes in
tData, which must be set before anytp.file.includecall - Use
<% myVar %>output tags only in the parent body, not inside includes - Keep included templates self-contained, they can read from
tDataif needed, but never depend on local variables from the parent
%%{init: {'theme': 'base', 'themeVariables': { 'nodeSpacing': 30, 'rankSpacing': 30 }}}%%
graph LR
subgraph Parent[Parent Template]
P1[Initial <%* %> block]
P2[Set tData]
P3[Body with <% var %>]
end
subgraph Includes[Included Templates]
I1[Read tData]
I2[Do not use parent's <% var %>]
I3[May call rename
context lost]
end
subgraph DataBus[tData]
T1[Values computed in parent]
end
P1 --> P2
P2 --> DataBus
DataBus --> I1
Parent --> Includes
Includes --> ParentFor example, you can use something like this parent template:
<%*
const dateInput = await tp.system.prompt("Date (YYYY-MM-DD)", moment().format("YYYY-MM-DD"));
const baseDate = moment(dateInput, "YYYY-MM-DD");
const day = baseDate.format("YYYY-MM-DD");
tData = { baseDate: baseDate };
-%>
---
<% tp.file.include("[[Your Frontmatter Template]]") %>
---
# Event on <% day %>
and this included “Your Frontmatter Template.md”:
<%*
let tt = (typeof tData !== 'undefined' && tData.baseDate) ? tData.baseDate : moment();
-%>
date: "<% tt.format('YYYY-MM-DD') %>"
weekyear: "<% tt.format('gggg-[W]ww') %>"
In the end tData will work as a “data bus” between templates as long as you set it in the parent template before any include. The moment you try to write to it inside an include that does await tp.file.rename(), you lose the values. And any <% %> output tag inside an include that references a variable from the parent will fail at parse time, before anything runs.
%%{init: {'theme': 'base', 'themeVariables': { 'nodeSpacing': 30, 'rankSpacing': 30 }}}%%
flowchart TD
A[Start of Parent Template] --> B[Execute initial <%* %> block]
B --> C[Collect inputs and compute values]
C --> D[Populate tData]
D --> E{Is tData defined before includes?}
E -->|No| F[Error: includes cannot access data]
E -->|Yes| G[Call tp.file.include]
G --> H[Include executes
May read tData]
H --> I{Does include call tp.file.rename?}
I -->|Yes| J[Context reset
tData lost]
I -->|No| K[Normal execution]
J --> L[Data sharing fails]
K --> M[Return to parent template]
M --> N[Use <% var %> only in parent]
N --> O[End]Hope this saves someone a few hours of testing and debugging.
Published, without the diagrams, at: Reddit