DataProvider - guiled/LRE GitHub Wiki
Data Provider
Introduction
Data Providers are one of the major features of LRE. This structure provides a powerful way to handle, transform and analyze data with chainable methods. They work like enhanced Let's Role Tables: each Data Provider has rows (entries) and columns (properties). Each row has a unique id.
Think of Data Providers as a smart data container that:
- Keeps your data synchronized with its source
- Allows you to transform data without modifying the original
- Provides chainable methods for filtering, sorting, and joining data
- Can be used directly with component methods like
setChoices()
Which entities are Data Providers?
LRE automatically adds the Data Provider features to the following entities:
The following components have methods that return a Data Provider:
- Repeater via
repeater.provider() - MultiChoice via
multichoice.checked()andmultichoice.unchecked() - Container via
container.provider()
You can also create your own Data Provider with lre.dataProvider():
const myData = lre.dataProvider("myDataId", function () {
return {
item1: { name: "Sword", damage: 10 },
item2: { name: "Shield", armor: 5 },
};
});
Understanding providedValue
The method providedValue() is the central method for Data Providers. It returns the up-to-date data in a usable form: a JavaScript object whose keys are the row id and values are the row data.
Take this example showing the "races" Table content:
| id | name | bonus | description |
|---|---|---|---|
| human | Human | +1 | Versatile and adaptable |
| elf | Elf | +2 | Agile and attuned to nature |
| dwarf | Dwarf | +3 | Sturdy and resilient |
| orc | Orc | +2 | Strong and fierce |
| halfling | Halfling | +1 | Small and stealthy |
The following code shows you the kind of data that providedValue() returns:
const races = Tables.get("races");
log(races.providedValue());
// Output:
// {
// "human": { "id": "human", "name": "Human", "bonus": "+1", "description": "Versatile and adaptable" },
// "elf": { "id": "elf", "name": "Elf", "bonus": "+2", "description": "Agile and attuned to nature" },
// "dwarf": { "id": "dwarf", "name": "Dwarf", "bonus": "+3", "description": "Sturdy and resilient" },
// "orc": { "id": "orc", "name": "Orc", "bonus": "+2", "description": "Strong and fierce" },
// "halfling": { "id": "halfling", "name": "Halfling", "bonus": "+1", "description": "Small and stealthy" }
// }
Utility methods
each
dataProvider.each(function (value, key, originalValue) {})
This method allows you to traverse the data of the DataProvider. It can be stopped early by returning false in the given callback.
Parameters:
value: the current row valuekey: the current row idoriginalValue: the value taken from the very first data provider in a chain matching the given key (useful to access Table data through atransform()orselect()DataProvider)
const races = Tables.get("races");
races.each(function (row, id, original) {
log("Race " + id + " is named " + row.name);
});
// Output:
// Race human is named Human
// Race elf is named Elf
// ...
// Stop early by returning false
let count = 0;
races.each(function (row, id) {
log(row.name);
count++;
if (count >= 2) return false; // stops after 2 iterations
});
refresh
dataProvider.refresh()
This method allows you to reload the data of the DataProvider when it is not done automatically. This is useful if you use a variable in the data source callback and want to reload after changing the variable value.
refresh() will refresh the sources of the DataProvider (re-launch the callback, refresh the original DataProvider, etc.).
let category = "weapons";
const items = lre.dataProvider("filteredItems", function () {
return allItems.where("category", category).providedValue();
});
// Later, change the category and refresh
category = "armor";
items.refresh(); // Data now contains armor items
refreshDerived
dataProvider.refreshDerived()
On the other way, refreshDerived() will manually trigger a refresh of all elements that took the DataProvider as source (a component value, another DataProvider derived from it).
Methods that return a value
count and length
dataProvider.count(): number
dataProvider.length(): number
Returns the number of elements in the data. Both methods are equivalent.
const races = Tables.get("races");
log(races.count()); // Output: 5
log(races.length()); // Output: 5
countDistinct
dataProvider.countDistinct(): number
dataProvider.countDistinct(column: string): number
dataProvider.countDistinct((value, key, data) => value): number
This method counts all different (unique) values in the DataProvider.
- With no parameter, it counts distinct values based on the entire row
- If the parameter is a string, it counts distinct values in the specified column
- If the parameter is a function, it applies the function to each row and counts distinct returned values
If the value is an object, it will be compared recursively to the other values to distinguish them.
const items = Tables.get("items");
// items contains: { a: {type: "weapon"}, b: {type: "armor"}, c: {type: "weapon"} }
log(items.countDistinct("type")); // Output: 2 (weapon and armor)
singleValue
dataProvider.singleValue(): value
Returns a single value from the DataProvider:
- If the provided data is an array, returns the first element
- If the provided data is an object, returns the first value found in the object
- In all other cases, returns the provided data itself
This is very useful after filtering data to get the single matching result.
const races = Tables.get("races");
const elfData = races.where("id", "elf").singleValue();
log(elfData); // Output: { "id": "elf", "name": "Elf", "bonus": "+2", ... }
singleId
dataProvider.singleId(): string | number
Returns a single id from the DataProvider:
- If the provided data is an object, returns the first key of this object
- If it is an array and the first element is an object containing "id", returns this id value
- In other cases, returns 0 (first index in an array)
const races = Tables.get("races");
const minRace = races.min("bonus");
log(minRace.singleId()); // Output: "human" (the race with lowest bonus)
sum
dataProvider.sum(): number
dataProvider.sum(column: string): number
dataProvider.sum((value, key, data) => number): number
This method calculates the sum of all numeric values in the DataProvider.
- With no parameter, it sums the default value for each row
- If a string is provided, it sums the values in the specified column
- If a function is provided, it applies the function to each row and sums the returned values
Non-numeric values are ignored, but a warning may be issued in debug mode.
const inventory = Tables.get("inventory");
// inventory: { sword: {weight: 5}, shield: {weight: 8}, potion: {weight: 1} }
log(inventory.sum("weight")); // Output: 14
// With a function
log(inventory.sum(function (item, id, data) {
return item.weight * item.quantity;
}));
getBy
dataProvider.getBy(column: string, value: any): object | undefined
dataProvider.getBy((value, key, data) => any, searchValue: any): object | undefined
Finds and returns the first row where the specified column (or computed value) matches the given value.
- If the first parameter is a string, it searches in that column
- If the first parameter is a function, it applies the function to each row and compares the result
const races = Tables.get("races");
// Find by column value
const elf = races.getBy("name", "Elf");
log(elf); // Output: { "elf": { "id": "elf", "name": "Elf", ... } }
// Find using a calculated value
const races = Tables.get("races").select("name");
const found = races.getBy(function (value, key, data) {
return Number(data.bonus);
}, 3);
log(found); // Output: { "dwarf": "Dwarf" } (dwarf has bonus +3)
toArray
dataProvider.toArray(): Array
dataProvider.toArray((value, key, data) => transformedValue): Array
Converts the DataProvider data to an array.
- Without parameter, each row becomes an object with
idadded (andvalueif it was a scalar) - With a transform function, each element is the result of that function
const races = Tables.get("races");
// Default transformation
log(races.select("name").toArray());
// Output: [
// { id: "human", value: "Human" },
// { id: "elf", value: "Elf" },
// { id: "dwarf", value: "Dwarf" },
// ...
// ]
// Custom transformation
const names = races.toArray(function (row, id, data) {
return row.name + " (" + row.bonus + ")";
});
log(names); // Output: ["Human (+1)", "Elf (+2)", "Dwarf (+3)", ...]
getData
dataProvider.getData(): object
dataProvider.getData(id: string | number): object
dataProvider.getData(ids: Array): object
Returns the original data for the specified id(s). This is particularly useful when working with transformed or filtered DataProviders to access the original row data.
- Without parameter, returns all original data matching current keys
- With a single id, returns the original data for that row
- With an array of ids, returns an object with original data for each id
const races = Tables.get("races");
const selected = races.select("name"); // Only keeps the name column
// Get original data through the transformed provider
log(selected.getData("elf"));
// Output: { "id": "elf", "name": "Elf", "bonus": "+2", "description": "Agile and attuned to nature" }
log(selected.getData(["human", "dwarf"]));
// Output: {
// "human": { "id": "human", "name": "Human", ... },
// "dwarf": { "id": "dwarf", "name": "Dwarf", ... }
// }
Methods that return a DataProvider
These methods can be chained to create different views of the same data source. Each method returns a new DataProvider, leaving the original unchanged.
select
dataProvider.select(column: string): DataProvider
Returns a DataProvider whose rows contain only the specified column. This is very useful for populating a Choice component.
The column parameter can be a dynamic value.
const races = Tables.get("races");
const raceNames = races.select("name");
log(raceNames.providedValue());
// Output:
// {
// "human": "Human",
// "elf": "Elf",
// "dwarf": "Dwarf",
// "orc": "Orc",
// "halfling": "Halfling"
// }
// Perfect for populating choices
sheet.get("raceChoice").setChoices(raceNames);
sort and sortBy
dataProvider.sort(): DataProvider
dataProvider.sort(column: string): DataProvider
dataProvider.sort(column: string, direction: "ASC" | "DESC"): DataProvider
dataProvider.sort(comparator: function): DataProvider
Returns a DataProvider with sorted data.
- Without parameter, sorts by value (alphabetically or numerically)
- With a column name, sorts by that column's values
- Direction can be "ASC" (default, ascending) or "DESC" (descending)
- With a comparator function, uses custom comparison logic
const items = Tables.get("items");
// Sort by name ascending
const byName = items.sort("name");
// Sort by weight descending
const heaviest = items.sort("weight", "DESC");
// Custom sort function
const custom = items.sort(function (a, b) {
return Number(a.weight) - Number(b.weight);
});
dataProvider.sortBy((value, key, data) => sortValue, direction?: "ASC" | "DESC"): DataProvider
Sorts using a computed value. The function receives each row and must return a value used for comparison.
const items = Tables.get("items");
// Sort by total value (price * quantity)
const byTotalValue = items.sortBy(function (item, key, data) {
return item.price * item.quantity;
}, "DESC");
filter
dataProvider.filter((value, key, data) => boolean): DataProvider
Returns a DataProvider containing only rows where the callback returns true.
const items = Tables.get("items");
// Keep only items with weight less than 10
const lightItems = items.filter(function (item, key, data) {
return item.weight < 10;
});
log(lightItems.providedValue());
where
dataProvider.where(condition: value | Array | function): DataProvider
dataProvider.where(column: string, condition: value | Array | function): DataProvider
Returns a DataProvider filtered by the given condition. This is a more convenient way to filter data than using filter() directly.
- With one parameter: filters rows where the value equals the condition
- With two parameters: filters rows where the specified column matches the condition
- Condition can be:
- A single value (exact match)
- An array of values (matches any value in the array)
- A function returning boolean
- A component (uses component's value for comparison)
const races = Tables.get("races");
// Filter by value (after select)
const strongRaces = races.select("name").where("Elf");
// Filter by column value
const bonusTwo = races.where("bonus", "+2");
log(bonusTwo.providedValue());
// Output: { "elf": {...}, "orc": {...} }
// Filter by multiple values
const someRaces = races.where("id", ["human", "elf", "dwarf"]);
// Filter with a function
const highBonus = races.where(function (row, key, data) {
return Number(row.bonus) >= 2;
});
// Filter using a component value
const raceChoice = sheet.get("selectedRace");
const selectedRaceData = races.where("id", raceChoice);
transform
dataProvider.transform(column: string): DataProvider
dataProvider.transform(mapping: object): DataProvider
dataProvider.transform((value, key, data) => newValue): DataProvider
Returns a DataProvider with transformed data.
- With a column name: extracts that column's value (accessing original data if needed)
- With a mapping object: renames/remaps columns
- With a function: applies custom transformation to each row
const races = Tables.get("races");
// Extract a single column value (different from select - gets the actual value)
const bonuses = races.transform("bonus");
log(bonuses.providedValue());
// Output: { "human": "+1", "elf": "+2", ... }
// Rename columns
const renamed = races.transform({
raceName: "name",
modifier: "bonus"
});
log(renamed.providedValue());
// Output: {
// "human": { raceName: "Human", modifier: "+1" },
// "elf": { raceName: "Elf", modifier: "+2" },
// ...
// }
// Custom transformation with function
const labels = races.transform(function (row, key, data) {
return {
label: row.name + " (" + row.bonus + ")",
desc: row.description
};
});
// Computed values in mapping
const computed = races.transform({
name: "name",
totalBonus: function (row, key, data) {
return Number(row.bonus) * 2;
}
});
min and max
dataProvider.min(): DataProvider
dataProvider.min(column: string): DataProvider
dataProvider.min((value, key, data) => value): DataProvider
dataProvider.max(): DataProvider
dataProvider.max(column: string): DataProvider
dataProvider.max((value, key, data) => value): DataProvider
Returns a DataProvider containing only the row with the minimum or maximum value.
- Without parameter: compares row values directly
- With a column name: compares values in that column
- With a function: compares computed values
const races = Tables.get("races");
// Get race with highest bonus
const strongest = races.max("bonus");
log(strongest.singleValue()); // Output: { "id": "dwarf", "name": "Dwarf", "bonus": "+3", ... }
log(strongest.singleId()); // Output: "dwarf"
// Get race with lowest bonus after select
const weakest = races.select("name").min("bonus");
log(weakest.providedValue()); // Output: { "human": "Human" }
// Using original data column through select
const minByBonus = races.select("name").min("bonus");
log(minByBonus.getData()); // Access original data of the result
limit
dataProvider.limit(count: number): DataProvider
Returns a DataProvider containing only the first count rows. Very useful combined with sort().
const items = Tables.get("items");
// Get top 3 heaviest items
const top3 = items.sort("weight", "DESC").limit(3);
log(top3.providedValue());
// Get the 5 cheapest items
const cheapest5 = items.sort("price").limit(5);
search
dataProvider.search(column: string, value: any): DataProvider
Returns a DataProvider containing all rows where the specified column equals the given value. This also searches by id if the column is "id".
const items = Tables.get("items");
// Find all weapons
const weapons = items.search("type", "weapon");
// Find by id
const sword = items.search("id", "sword");
log(sword.providedValue());
// Output: { "sword": { id: "sword", name: "Sword", type: "weapon", ... } }
union
dataProvider.union(otherDataProvider: DataProvider): DataProvider
Combines two DataProviders into one. If both have rows with the same id, the row from the second DataProvider overwrites the first.
const meleeWeapons = Tables.get("meleeWeapons");
const rangedWeapons = Tables.get("rangedWeapons");
// Combine both weapon tables
const allWeapons = meleeWeapons.union(rangedWeapons);
// Use for a choice
sheet.get("weaponChoice").setChoices(allWeapons.select("name"));
innerJoin and leftJoin
dataProvider.innerJoin(otherProvider: DataProvider, leftColumn: string, rightColumn?: string): DataProvider
dataProvider.innerJoin(otherProvider: DataProvider, options: JoinOptions): DataProvider
dataProvider.innerJoin(otherProvider: DataProvider, comparator: function): DataProvider
dataProvider.leftJoin(otherProvider: DataProvider, leftColumn: string, rightColumn?: string): DataProvider
dataProvider.leftJoin(otherProvider: DataProvider, options: JoinOptions): DataProvider
dataProvider.leftJoin(otherProvider: DataProvider, comparator: function): DataProvider
Joins two DataProviders together, similar to SQL joins.
innerJoin: keeps only rows that have a match in both providersleftJoin: keeps all rows from the first provider, with matching data from the second (or empty if no match)
Join parameters:
leftColumn: the column in the first provider to matchrightColumn: the column in the second provider to match (defaults to "id")comparator: a function(rowLeft, rowRight, keyLeft, keyRight, dataLeft, dataRight) => boolean
When columns have the same name in both providers, the right provider's columns are suffixed with _.
const characters = Tables.get("characters");
// { "char1": { id: "char1", name: "Gandalf", raceId: "human" } }
const races = Tables.get("races");
// { "human": { id: "human", name: "Human", bonus: "+1" } }
// Join characters with their race data
const withRace = characters.innerJoin(races, "raceId", "id");
log(withRace.providedValue());
// Output:
// {
// "char1": {
// id: "char1",
// name: "Gandalf",
// raceId: "human",
// name_: "Human", // suffixed because "name" already exists
// bonus: "+1"
// }
// }
// Join with custom comparator
const custom = characters.innerJoin(races, function (charRow, raceRow) {
return charRow.raceId === raceRow.id;
});
Reactivity and automatic refresh
One of the most powerful features of DataProviders is their automatic reactivity. When you create a DataProvider using components in its source function, the DataProvider automatically refreshes when those components change.
const filteredItems = lre.dataProvider("filtered", function () {
const category = sheet.get("categoryChoice").value();
return Tables.get("items").where("category", category).providedValue();
});
// When categoryChoice changes, filteredItems automatically updates!
sheet.get("itemList").setChoices(filteredItems.select("name"));
Manual subscription
You can manually subscribe to DataProvider updates:
const items = Tables.get("items");
items.subscribeRefresh("mySubscription", function () {
log("Items data has changed!");
});
// Later, unsubscribe if needed
items.unsubscribeRefresh("mySubscription");
Complete example
Here's a complete example showing how DataProviders can simplify complex data operations:
// Assume we have these tables:
// - "items": all game items with columns: id, name, type, weight, price
// - "inventory": player's items with columns: id, itemId, quantity
init = function (sheet) {
const items = Tables.get("items");
const inventory = Tables.get("inventory");
// Create a combined view of inventory with item details
const inventoryWithDetails = inventory
.innerJoin(items, "itemId", "id")
.transform({
name: "name_", // item name (suffixed from join)
quantity: "quantity",
totalWeight: function (row) {
return row.weight * row.quantity;
},
totalPrice: function (row) {
return row.price * row.quantity;
}
});
// Display total encumbrance
sheet.get("totalWeight").value(function () {
return inventoryWithDetails.sum("totalWeight");
});
// Display total inventory value
sheet.get("totalValue").value(function () {
return inventoryWithDetails.sum("totalPrice");
});
// Get the heaviest item
const heaviest = inventoryWithDetails.max("totalWeight");
sheet.get("heaviestItem").value(function () {
return heaviest.singleValue()?.name || "None";
});
// Filter weapons only and sort by name
const weapons = inventoryWithDetails
.where("type", "weapon")
.sort("name");
sheet.get("weaponChoice").setChoices(weapons.select("name"));
};