Skip to main content

Rendering Implementation

The HCS-12 rendering system in the Standards SDK handles template processing and block composition with context injection for attributes, actions, and results.


What It Does

  • Processes templates with Handlebars-style syntax and XSS protection
  • Injects context including attributes, actions, and results
  • Handles action binding through data attributes in HTML
  • Manages nested block composition through template references
  • Loads external resources with security validation

Template Engine

The SDK includes a Handlebars-compatible template engine with XSS protection:

import { TemplateEngine } from '@hashgraphonline/standards-sdk';

// Create template engine
const templateEngine = new TemplateEngine(logger);

// Render a template with context
const html = await templateEngine.render(templateString, {
attributes: {
title: 'My Block',
count: 42
},
actions: {
increment: '0.0.123456',
decrement: '0.0.123456'
},
actionResults: {
increment: {
success: true,
data: { newValue: 43 }
}
}
});

Template Syntax

Templates use Handlebars-style syntax with additional HashLinks features:

<!-- Variable substitution -->
<h1>{{attributes.title}}</h1>
<p>Count: {{attributes.count}}</p>

<!-- Conditional rendering -->
{{#if attributes.showDetails}}
<div class="details">Detailed information</div>
{{/if}}

{{#unless attributes.hideControls}}
<div class="controls">Control elements</div>
{{/unless}}

<!-- Looping through arrays -->
<ul>
{{#each attributes.items}}
<li>{{this.name}}: {{this.value}}</li>
{{/each}}
</ul>

<!-- Action binding -->
<button
data-action-click="{{actions.increment}}"
data-params='{"step": {{attributes.step}}}'>
Increment
</button>

Block Renderer

The block renderer handles complete block rendering with context injection:

import { BlockRenderer } from '@hashgraphonline/standards-sdk';

// Create block renderer
const blockRenderer = new BlockRenderer(
logger,
gutenbergBridge,
templateEngine,
stateManager
);

// Render a block
const result = await blockRenderer.render(blockDefinition, {
attributes: {
count: 5,
step: 1,
label: 'Counter'
},
actions: {
increment: '0.0.123456',
decrement: '0.0.123456'
},
container: '#block-container'
});

Context Injection

The rendering system injects various contexts into templates:

interface TemplateContext {
attributes?: Record<string, any>;
actions?: Record<string, string>;
actionResults?: Record<string, any>;
actionErrors?: Record<string, any>;
blockId: string;
network: 'mainnet' | 'testnet';
}

Action Binding

Templates support various action binding mechanisms:

<!-- Click actions -->
<button data-action-click="{{actions.submit}}">Submit</button>

<!-- Change actions -->
<select data-action-change="{{actions.update}}">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>

<!-- Form submission -->
<form data-action="{{actions.process}}" data-validate="{{actions.validate}}">
<input type="text" name="username" />
<button type="submit">Process</button>
</form>

<!-- Mouse events -->
<div data-action-mouseover="{{actions.preview}}"
data-action-mouseout="{{actions.hidePreview}}">
Hover for preview
</div>

Parameter Passing

Actions can receive parameters in various ways:

<!-- Static parameters -->
<button data-action-click="{{actions.transfer}}"
data-params='{"amount": 100, "token": "HBAR"}'>
Transfer 100 HBAR
</button>

<!-- Dynamic parameters from attributes -->
<button data-action-click="{{actions.transfer}}"
data-params='{"tokenId": "{{attributes.tokenId}}"}'>
Transfer {{attributes.tokenName}}
</button>

<!-- Form data parameters -->
<form data-action="{{actions.submit}}">
<input name="recipient" type="text" value="{{attributes.defaultRecipient}}" />
<input name="amount" type="number" value="{{attributes.defaultAmount}}" />
<button type="submit">Send</button>
</form>

Nested Block Rendering

The renderer supports nested block composition:

<!-- Parent block template -->
<div class="container-block" data-block-id="{{blockId}}">
<h2>{{attributes.title}}</h2>

<!-- Nested blocks -->
{{#each attributes.childBlocks}}
<div data-hashlink="hcs://12/{{this.blockId}}"
data-attributes='{"parentId": "{{blockId}}"}'>
</div>
{{/each}}

<!-- Conditional nested blocks -->
{{#if attributes.showCounter}}
<div data-hashlink="hcs://12/{{attributes.counterBlockId}}"
data-actions='{"increment": "{{actions.increment}}"}'>
</div>
{{/if}}
</div>

Resource Management

The renderer handles external resource loading:

<!-- CSS resources -->
<link rel="stylesheet"
data-src="hcs://1/0.0.123456"
data-script-id="block-styles" />

<!-- JavaScript resources -->
<script data-src="hcs://3/0.0.789012"
data-dependencies="['jquery', 'react']">
</script>

<!-- Image resources -->
<img data-src="hcs://1/{{attributes.imageTopicId}}"
alt="{{attributes.imageAlt}}"
loading="lazy" />

State Management

The rendering system manages block state:

import { BlockStateManager } from '@hashgraphonline/standards-sdk';

// Create state manager
const stateManager = new BlockStateManager();

// Set block state
stateManager.setBlockState('0.0.123456', {
attributes: {
count: 5,
step: 1
},
actionResults: {
increment: {
success: true,
data: { newValue: 6 }
}
}
});

// Get block state
const state = stateManager.getBlockState('0.0.123456');

Security Features

The renderer includes built-in security features:

// XSS protection
const sanitizedHtml = templateEngine.sanitizeHtml(unsafeHtml);

// Script tag removal
const cleanHtml = templateEngine.removeScriptTags(htmlWithScripts);

// Attribute validation
const isValid = templateEngine.validateAttributes(attributes);

Custom Helpers

The template engine supports custom helper functions:

// Register custom helper
templateEngine.registerHelper('formatCurrency', (amount, currency) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency || 'USD'
}).format(amount);
});

// Use in template
<span>{{formatCurrency attributes.amount "USD"}}</span>

Performance Optimization

The renderer includes performance optimizations:

// Template caching
templateEngine.clearCache(); // Clear cache when needed
const cacheSize = templateEngine.getCacheSize(); // Monitor cache size

// Precompilation
await templateEngine.precompile('counter-template', templateHtml);
const html = await templateEngine.renderCompiled('counter-template', context);

// Batch rendering
const results = await Promise.all([
blockRenderer.render(block1, options1),
blockRenderer.render(block2, options2),
blockRenderer.render(block3, options3)
]);

Browser Integration

The renderer works seamlessly in browser environments:

// Browser-specific rendering
const result = await blockRenderer.render(blockDefinition, {
container: document.getElementById('block-container'),
theme: 'dark',
responsive: true
});

// DOM manipulation
if (result.element) {
document.getElementById('container').appendChild(result.element);
}

// Cleanup
if (result.cleanup) {
result.cleanup();
}

Error Handling

The renderer provides comprehensive error handling:

try {
const result = await blockRenderer.render(blockDefinition, renderOptions);

if (result.html) {
document.getElementById('container').innerHTML = result.html;
}
} catch (error) {
console.error('Rendering failed:', error.message);

// Show error UI
document.getElementById('container').innerHTML = `
<div class="error-message">
Failed to render block: ${error.message}
</div>
`;
}

Best Practices

  • Template Security: Always sanitize templates to prevent XSS
  • Performance: Use caching and precompilation for frequently rendered templates
  • Error Handling: Handle rendering errors gracefully with user-friendly messages
  • Resource Management: Load external resources efficiently
  • State Management: Properly manage and update block state
  • Accessibility: Ensure rendered content is accessible
  • Responsive Design: Make templates responsive to different screen sizes
  • Testing: Test templates with various data scenarios