JSON Template Parser
A powerful and flexible JSON template parser for NocoBase that supports dynamic template rendering with custom filters and scoped functions.
Features
- Template-based JSON transformation
- Custom filter support
- Scoped function handling
- Liquid template syntax
- Nested object and array support
Installation
npm install @nocobase/json-template-parser
Basic Usage
import { createJSONTemplateParser } from '@nocobase/json-template-parser';
const parser = createJSONTemplateParser();
const template = {
name: "Hello {{name}}",
age: "{{age}}"
};
const result = await parser.render(template, {
name: "John",
age: 25
});
// Result: { name: "Hello John", age: "25" }
API Reference
JSONTemplateParser
Methods
render(template: string | object, data: object, context?: object): Promise<any>
Renders a template with provided data and context.
template
: The template string or object to renderdata
: Data object containing values for template variablescontext
: Optional context object for additional rendering context
registerFilter(filter: Filter): void
Registers a custom filter for template processing.
interface Filter {
name: string;
title: string;
handler: (...args: any[]) => any;
group: string;
uiSchema?: any;
sort: number;
}
registerFilterGroup(group: FilterGroup): void
Registers a group of filters.
interface FilterGroup {
name: string;
title: string;
sort: number;
}
Scope Functions
Scope functions are special functions that start with $
and provide advanced data manipulation capabilities. They are defined in the data object passed to the render method. Scope functions enable dynamic data fetching, transformation, and complex computations during template rendering.
Key Concepts
-
Namespace: Scope functions are identified by a
$
prefix (e.g.,$user
,$settings
). This creates a dedicated namespace for accessing related data. -
Lazy Evaluation: Fields are only evaluated when they are actually used in the template, providing better performance.
-
Context Awareness: Scope functions have access to:
fields
: List of fields requested in the templatedata
: The complete data object passed to rendercontext
: Additional context provided during rendering
-
Two-Phase Processing:
getValue
: Retrieves the raw value for a fieldafterApplyHelpers
: Post-processes the value after filters are applied
Detailed Interface
interface ScopeFnWrapper {
(params: {
fields: string[]; // List of fields used in template
data: any; // Complete data object
context?: Record<string, any>; // Additional context
}): Promise<{
getValue: (params: {
field: string[]; // Field path being accessed
keys: string[]; // Full path in template
}) => any;
afterApplyHelpers: (params: {
field: string[]; // Field path
keys: string[]; // Full path
value: any; // Value after filters
}) => any;
}>;
}
Common Use Cases
1. Dynamic Data Fetching
const template = {
user: "{{$api.user}} ({{$api.role}})"
};
await parser.render(template, {
$api: async ({ fields }) => {
// Fetch only required fields
const data = await fetchUserData(fields);
return {
getValue: ({ field }) => data[field],
afterApplyHelpers: ({ value }) => value
};
}
});
2. Computed Properties
const template = {
total: "{{$calc.sum}}",
average: "{{$calc.average}}"
};
await parser.render(template, {
numbers: [1, 2, 3, 4, 5],
$calc: ({ data }) => ({
getValue: ({ field }) => {
const numbers = data.numbers;
switch (field) {
case 'sum':
return numbers.reduce((a, b) => a + b, 0);
case 'average':
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
},
afterApplyHelpers: ({ value }) => value
})
});
3. Context-Based Transformation
const template = {
greeting: "{{$i18n.welcome}}",
message: "{{$i18n.notification}}"
};
await parser.render(template, {
$i18n: ({ context }) => ({
getValue: ({ field }) => {
const translations = {
en: {
welcome: "Welcome",
notification: "You have new messages"
},
es: {
welcome: "Bienvenido",
notification: "Tienes nuevos mensajes"
}
};
return translations[context.language][field];
},
afterApplyHelpers: ({ value }) => value
})
}, { language: 'es' });
4. Nested Data Access with Validation
const template = {
permissions: "{{$acl.user.permissions}}",
role: "{{$acl.user.role.name}}"
};
await parser.render(template, {
$acl: ({ fields }) => {
const userData = {
permissions: ['read', 'write'],
role: { name: 'admin', level: 1 }
};
return {
getValue: ({ field, keys }) => {
// Safely traverse nested paths
return keys.reduce((obj, key) => obj?.[key], userData);
},
afterApplyHelpers: ({ value, keys }) => {
// Validate sensitive data
if (keys.includes('permissions')) {
return Array.isArray(value) ? value : [];
}
return value;
}
};
}
});
Best Practices
-
Field Optimization:
$data: ({ fields }) => { // Only fetch required fields const requiredData = await fetchDataForFields(fields); return { getValue: ({ field }) => requiredData[field] }; }
-
Error Handling:
getValue: ({ field }) => { try { return computeValue(field); } catch (error) { console.error(`Error computing ${field}:`, error); return null; // Provide fallback value } }
-
Caching Results:
$cached: ({ fields }) => { const cache = new Map(); return { getValue: ({ field }) => { if (!cache.has(field)) { cache.set(field, expensiveComputation(field)); } return cache.get(field); } }; }
-
Context Utilization:
$data: ({ context }) => ({ getValue: ({ field }) => { return context.isAdmin ? sensitiveData[field] : limitedData[field]; } })
Performance Considerations
- Use the
fields
parameter to optimize data fetching - Implement caching for expensive computations
- Avoid unnecessary transformations in
afterApplyHelpers
- Keep scope functions focused and specific
- Use appropriate error handling and fallbacks
Advanced Examples
Using Custom Filters
const parser = createJSONTemplateParser();
// Register a filter group
parser.registerFilterGroup({
name: 'string',
title: 'String Operations',
sort: 1
});
// Register a custom filter
parser.registerFilter({
name: 'uppercase',
title: 'Convert to Uppercase',
handler: (value) => String(value).toUpperCase(),
group: 'string',
sort: 1
});
const template = {
message: "{{name | uppercase}}"
};
const result = await parser.render(template, {
name: "john"
});
// Result: { message: "JOHN" }
Nested Templates
const template = {
user: {
info: {
fullName: "{{firstName}} {{lastName}}",
contact: {
email: "{{email}}",
phone: "{{phone}}"
}
},
stats: ["{{stat1}}", "{{stat2}}"]
}
};
const result = await parser.render(template, {
firstName: "John",
lastName: "Doe",
email: "john@example.com",
phone: "123-456-7890",
stat1: "Active",
stat2: "Premium"
});
License
This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. For more information, please refer to: https://www.nocobase.com/agreement