Data in Foundry progresses through many stages to be rendered in a document sheet. This guide is intended to be a reference for developers trying to understand where they should define data the order in which it is processed.
This guide will reference file locations rather than the official API docs because this is focusing on the internal workings of methods that are not well-commented. All file paths are all relative to foundryInstallPath\resources\app
. Technically your installation just loads the general public\scripts\foundry.js
file, but the files in client
and common
are smaller and easier to follow.
Legend
Class.method is a static method or property on the given class
Class#method is an instance method or property
The server loads up data from the various databases as part of opening a world from the setup screen, transmitting it to users who login to or refresh the /game
page.
Foundry begins its path with client\tail.js
, which adds an event listener, DOMContentLoaded
, that fires after all of the files Foundry is pulling in have loaded (e.g. all the esmodules
). Game.create
is called and the returned reference is stashed in globalThis.game
. This method establishes a socket connection, uses it to fetch the raw data from the world databases with Game.getData()
, and constructs the Game
instance, putting all the raw database info in game.data
- the first step of our Document journey.
The next step is Game#initialize
, where we get that init
Hook call and "Foundry VTT | Initializing Foundry Virtual Tabletop Game"
printed to console. This method eventually calls Game#_initializeGameView
, which is really just a license-check wrapper for Game#setupGame
. Inside that method we get Game#initializePacks
and Game#initializeDocuments
, the second step of our Document journey (after which the setup
hook is called).
The first of these two methods, initializePacks
, only instantiates the indices of the packs. The second, initializeDocuments
, loops through all of the document data stashed in game.data
, calls the appropriate constructor, and then puts it in the appropriate world collection. For example, game.data.actors
gets fed into new Actor
and the result is poured inside game.actors
. When all of these documents are loaded "Foundry VTT | Prepared World Documents in ${Math.round(dt)}ms"
is printed to console.
The following sections describe what happens inside these constructor calls as well as what happens after a database update
.
The top level inheritance for documents traces back to the DataModel class, found in common\abstract\data.mjs
, which controls the earliest steps of processing. These are run
In the DataModel constructor, the data is first run through DataModel#_initializeSource
and then stored in _source
. This method applies DataModel#migrateDataSafe
, DataModel#cleanData
, and DataModel#shimData
, ensuring the constructed document matches the specifications from its schema
property before saving it in _source
. After putting the cleaned up data in _source
, DataModel#validate
runs for one last check.
Validation: Most validation occurs on the DataField
level. If a validator returns a boolean, no further checks are needed - e.g. _validateSpecial
will return either true
or false
if the value
is null
or undefined
, but if it's neither then it will return void
to pass the value on to be checked by _validateType
. Unlike the other steps in _initializeSource
, validate
is re-run every update.
This method copies data from _source
to the top level, using the schema
field as a guide. Each property of the schema gets run through the appropriate DataField#initialize
method, which performs operations like transforming a stored ID string in ForeignDocumentField#initialize
to a pointer. Unlike _initializeSource
, this runs after every document update.
TypeDataField: This field stands out because it grabs a reference to CONFIG?.[this.documentName]?.dataModels?.[type]
to check if there's a class to instantiate for the system
property, otherwise it's created as a plain object. This means that system
is baseline an open-ended ObjectField; it will save any data so long as you don't directly try to save 'doc.system': false
or some kind of non-object value.
If you're only using template.json
to define properties, any missing properties will get filled in from the template but otherwise it provides no other validation or handling; if a user updates a property to be a boolean but your template.json
value is a string, Foundry won't reject the update, but if a user deletes a property provided by template.json
then it will replace that property with the default.
The next step is specific to client documents, as these processes are not run in the server. The ClientDocumentMixin
is found in client\data\abstract\client-document.js
and takes a Document
as its argument, for example BaseActor
. This document provides the necessary schema information for all of the other processes to work.
Our main function of concern here is ClientDocument#prepareData
, which is wrapped by ClientDocument#_safePrepareData
with error-catching protections. The actual function is quite straightforward:
/**
* Prepare data for the Document. This method is called automatically by the DataModel#_initialize workflow.
* This method provides an opportunity for Document classes to define special data preparation logic.
* The work done by this method should be idempotent. There are situations in which prepareData may be called more
* than once.
* @memberof ClientDocumentMixin#
*/
prepareData() {
const isTypeData = this.system instanceof foundry.abstract.TypeDataModel;
if ( isTypeData ) this.system.prepareBaseData();
this.prepareBaseData();
this.prepareEmbeddedDocuments();
if ( isTypeData ) this.system.prepareDerivedData();
this.prepareDerivedData();
}
On initial world load, this call is deferred until after ALL documents have been created. Afterwards, it's called for a document after it's created or updated. It can be manually called again as a side effect of other operations that wouldn't otherwise trigger it.
Everything here should be performed using standard javascript assignment (=
). Calling update
within prepareData
or its subsidiary methods can easily trigger an infinite loop, as that update
call then creates another prepareData
call.
When the docs state that this method should be idempotent, they mean that running prepareData
more than once should result in the same end state as running it exactly once. Side effects should be self-validating to ensure they result in a consistent state.
This is a good place to initialize any values that aren't stored in the database but are targets for active effects, such as creating a blank array or setting a value to 0. More complicated calculations should usually wait until prepareDerivedData
after active effects have been applied, but your specific use case may disagree.
If you have a system data model, you can run type-specific logic here. Keep in mind that you're operating within the system
object, so you'll need to call this.parent
to access the actual document properties, e.g. this.parent.items
to access the items collection.
Most documents do not do anything here natively; only ActiveEffect, Scene, and Token natively do things here, and ActiveEffect's handling covers a deprecated field.
Baseline, this loops through each of the Document's collections and calls prepareData
on each document. Each document has a defined hierarchy to handle possible multiple collections; in the case of actors, Items are prepared before ActiveEffects. The Actor class follows this up with a call to applyActiveEffects
, which provides native handling for data modifications.
Developers usually don't need to make direct edits here; they're better off making changes in the appropriate prepareData
methods of the embedded documents or in applyActiveEffects
.
This is the final place to manipulate a document's data in a way that is generally accessible within the foundry API. Derived data is the place to calculate modifiers, encumbrance, and all of the other pieces of information you want available. What actually needs to be derived depends enormously on what you're building; trad games like D&D, PF, or SWADE have lots of derived data, while more rules light games like various PbtAs have very little derived data.
If you have a system data model, you can run type-specific logic here. Keep in mind that you're operating within the system
object, so you'll need to call this.parent
to access the actual document properties, e.g. this.parent.items
to access the items collection.
Many document classes have their own native handling here that you should keep in mind to call super.prepareDerivedData()
for:
playing
propertyAfter all documents have prepared their data for the first time the setup
hook fires.
The final step in the journey to displaying a document involves the application class stack. Many classes extend DocumentSheet
, which can be found in client\apps\form.js
. This extends FormApplication
which extends Application
. There's a lot of logic here that isn't relevant to our goal, which is "How does the document data we've defined in the previous sections get displayed to an end user"; other features are covered in Application.
The journey starts with a call to Application#render(true)
, which "forces" the application to render; by default, render
will only refresh an already displayed application. This encapsulates _render
with some error handling.
Inside Application#_render
, there are several stages of processing possible application render states, including printing "Foundry VTT | Rendering ${this.constructor.name}"
and merging in the options until finally a call is made to Application#getData
. The return of this is used by Application#_renderInner
to call renderTemplate(this.template, data)
, constructing the html that is eventually displayed.
For a DocumentSheet
, there are many layers of overrides to that getData
call. Between it and FormApplication
, you end up with a javascript object that looks like this:
const isEditable = this.isEditable;
{
object: this.object,
options: this.options,
title: this.title,
cssClass: isEditable ? "editable" : "locked",
editable: isEditable,
document: this.document,
data: data,
limited: this.document.limited,
options: this.options,
owner: this.document.isOwner,
title: this.title
}
(note: object
is the target of the FormApplication and is the same as document
for a DocumentSheet)
Classes like ActorSheet
or ItemSheet
often add pieces to this, e.g. adding another pointer named after the document class. What happens here though is ultimately only for the purposes of being fed into a renderTemplate
call, and so everything that happens here is for display logic.
Things that are good to do in getData
selectOptions
for dropdown fieldsAfter the template is rendered, the context
is handed off to the renderDocumentSheet
hook and then evaporates, which is why any non-display logic should be processed in the previous steps.
Alternatively, for HandlebarsApplicationMixin(DocumentSheetV2)
, the relevant rendering processes involve three classes; ApplicationV2
, DocumentSheetV2
, and HandlebarsApplicationMixin
, all of which can be found in the resources/app/client-esm/applications/api
directory.
The journey starts with ApplicationV2#render({ force: true })
; this options
object is of type ApplicationRenderOptions. Like AppV1, force
controls whether to freshly-render an application not displayed on the screen. Unlike AppV1, render
is asynchronous and returns a promise that resolves when the application is fully rendered.
The render
method then calls its private partner, ApplicationV2##render
. This calls a series of protected functions, only some of which are used by the three classes used above. You can always extend or override these; just keep in mind that the choice of if and where to call super
can impact expected functionality.
_canRender
, which in DocumentSheetV2 checks against the user's viewPermission
._configureRenderOptions
, which is used by ApplicationV2 to update the window and position properties and by HandlebarsApplicationMixin to decide what parts to show (by default all)._prepareContext
, which is left blank by default but very important for developers (details below)._preFirstRender
, which is used by HandlebarsApplicationMixin to load the additional templates necessary for rendering._preRender
, which is left blank by default._renderFrame
, which is used by both ApplicationV2 and DocumentSheetV2 to initialize key application properties such as the close and UUID buttons._attachFrameListeners
, which is used by ApplicationV2 to attach listeners to the whole frame such as Form Submission handling._renderHTML
, which is implemented by HandlebarsApplicationMixin to generate the HTML of all the parts. This function calls _preparePartContext
for additional support._replaceHTML
, which is implemented by HandlebarsApplicationMixin and replaces any previously rendered HTML for a part with the freshly generated HTML._insertElement
, which is used by ApplicationV2 to add the Application to the window._updateFrame
, which is used by ApplicationV2 to update the header bar properties._onFirstRender
, which is used by DocumentSheetV2 to add the application to the document's apps
record._onRender
, which is left blank by default. This is also the part where the renderApplication
is called, but separate from this function so no super
call is necessary.Broadly speaking, ApplicationV2
handles the window frame + header, while HandlebarsApplicationMixin
is responsible for the internal HTML. Everything until _replaceHTML
is asynchronous and awaited, while from that point forwards everything is synchronous (although you can always call asynchronous code as a side effect; it just won't be awaited inside the render
promise).
The most important step for the typical developer and one that almost certainly needs overrides are the asynchronous functions _prepareContext
(added by ApplicationV2) and _preparePartContext
(added by HandlebarsApplicationV2). The docs do not currently provide the signature of the latter, so it's copied here for reference
/**
* Prepare context that is specific to only a single rendered part.
*
* It is recommended to augment or mutate the shared context so that downstream methods like _onRender have
* visibility into the data that was used for rendering. It is acceptable to return a different context object
* rather than mutating the shared context at the expense of this transparency.
*
* @param {string} partId The part being rendered
* @param {ApplicationRenderContext} context Shared context provided by _prepareContext
* @param {HandlebarsRenderOptions} options Options which configure application rendering behavior
* @returns {Promise<ApplicationRenderContext>} Context data for a specific part
* @protected
*/
async _preparePartContext(partId, context, options) {
context.partId = `${this.id}-${partId}`;
return context;
}
The reason this step is important is that the context
object returned by these functions is fed into a renderTemplate
call by HandlebarsApplicationMixin#_renderHTML
. This is where all display logic should be handled, such as:
selectOptions
for dropdown fieldsA very basic _prepareContext
call should look something like the following. this.document
and this.isEditable
are two getters defined by DocumentSheetV2.
async _prepareContext(options) {
return {
document: this.document,
system: this.document.system,
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
isEditable: this.isEditable
}
}
Then, in _preparePartContext
, you can wrap your logic in a switch(partId) {}
statement to prepare elements as needed. This can both reduce the amount of work if you're only selectively rendering parts (such as if a user only has limited permissions) as well as let you cleanly redefine variables across different parts.
Don't be afraid to delegate complex preparation logic to protected or private functions;
context.items = await this._prepareItems(context, options)
can help keep your_prepareContext
function readable.
After the context from _preparePartContext
is used in the renderTemplate
call it's discarded, so non-display logic should always happen in the earlier document preparation steps. Similarly, while the context
object from _prepareContext
is passed into many of the functions outlined earlier (e.g. _onFirstRender
), it is not "saved" anywhere; it's not even passed to the renderApplicationV2
hook.
This hopefully was a helpful walkthrough of how information flows from the database to actually being rendered on a user's screen.