Motor de Templates
El motor de templates de Fox Framework proporciona una forma potente y flexible para generar vistas HTML dinámicas en tus aplicaciones. Diseñado para ser extensible, rápido y compatible con múltiples sistemas de plantillas, este componente te permite separar claramente la lógica de presentación de la lógica de negocio.
Características Principales
- Sistema modular: Soporte para múltiples motores de plantillas
- Extensible: Fácil integración de nuevos motores de templates
- Caché integrada: Optimización de rendimiento para plantillas
- Layouts y partials: Composición de interfaces reutilizables
- Helpers: Funciones de ayuda para manipulación de datos en vistas
- Integración con datos: Paso sencillo de datos desde controladores a vistas
- Tipado completo: Aprovecha TypeScript para detectar errores temprano
- Compilación: Compila plantillas para máximo rendimiento
Configuración Básica
Configuración del Motor
import { FoxFactory } from '@foxframework/core';
import { TemplateEngineFactory } from '@foxframework/templates';
// Crear el servidor con motor de templates
const server = FoxFactory.createServer({
port: 3000,
views: {
// Motor por defecto
engine: 'ejs',
// Directorio de plantillas
directory: './src/views',
// Configuración adicional
options: {
// Extensión de archivos de plantilla
extension: 'ejs',
// Caché de plantillas en producción
cache: process.env.NODE_ENV === 'production',
// Variables globales disponibles en todas las vistas
globals: {
appName: 'Mi Aplicación',
version: '1.0.0'
}
}
},
router
});
// También puedes configurar el motor después de crear el servidor
server.setTemplateEngine(TemplateEngineFactory.create('pug', {
directory: './src/views',
extension: 'pug',
cache: true
}));Uso Básico en Controladores
import { Controller, Get, HttpContext } from '@foxframework/core';
@Controller('/')
export class HomeController {
@Get('/')
async index(ctx: HttpContext) {
// Renderizar vista con datos
return ctx.view.render('home', {
title: 'Bienvenido',
user: ctx.auth.user
});
}
@Get('/about')
async about(ctx: HttpContext) {
// Renderizar vista con layout específico
return ctx.view.render('about', {
title: 'Acerca de',
content: 'Contenido de la página'
}, {
layout: 'layouts/simple'
});
}
}Motores de Plantillas Soportados
Fox Framework soporta varios motores de plantillas populares:
EJS (por defecto)
// Configuración para EJS
server.setTemplateEngine(TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs'
}));
// Ejemplo de plantilla EJS
// views/home.ejs<%- include('partials/header', { title }) %>
<div class="container">
<h1><%= title %></h1>
<% if (user) { %>
<p>Bienvenido, <%= user.name %>!</p>
<% } else { %>
<p>Por favor, inicia sesión</p>
<% } %>
<ul>
<% items.forEach(function(item) { %>
<li><%= item.name %></li>
<% }); %>
</ul>
</div>
<%- include('partials/footer') %>Pug (antes Jade)
// Configuración para Pug
server.setTemplateEngine(TemplateEngineFactory.create('pug', {
directory: './src/views',
extension: 'pug'
}));
// Ejemplo de plantilla Pug
// views/home.pugextends layouts/main
block content
.container
h1= title
if user
p Bienvenido, #{user.name}!
else
p Por favor, inicia sesión
ul
each item in items
li= item.nameHandlebars
// Configuración para Handlebars
server.setTemplateEngine(TemplateEngineFactory.create('handlebars', {
directory: './src/views',
extension: 'hbs'
}));
// Ejemplo de plantilla Handlebars
// views/home.hbs{{> partials/header title=title}}
<div class="container">
<h1>{{title}}</h1>
{{#if user}}
<p>Bienvenido, {{user.name}}!</p>
{{else}}
<p>Por favor, inicia sesión</p>
{{/if}}
<ul>
{{#each items}}
<li>{{this.name}}</li>
{{/each}}
</ul>
</div>
{{> partials/footer}}Nunjucks
// Configuración para Nunjucks
server.setTemplateEngine(TemplateEngineFactory.create('nunjucks', {
directory: './src/views',
extension: 'njk'
}));
// Ejemplo de plantilla Nunjucks
// views/home.njk{% extends "layouts/main.njk" %}
{% block content %}
<div class="container">
<h1>{{ title }}</h1>
{% if user %}
<p>Bienvenido, {{ user.name }}!</p>
{% else %}
<p>Por favor, inicia sesión</p>
{% endif %}
<ul>
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
</div>
{% endblock %}Mustache
// Configuración para Mustache
server.setTemplateEngine(TemplateEngineFactory.create('mustache', {
directory: './src/views',
extension: 'mustache'
}));
// Ejemplo de plantilla Mustache
// views/home.mustache{{> partials/header }}
<div class="container">
<h1>{{title}}</h1>
{{#user}}
<p>Bienvenido, {{name}}!</p>
{{/user}}
{{^user}}
<p>Por favor, inicia sesión</p>
{{/user}}
<ul>
{{#items}}
<li>{{name}}</li>
{{/items}}
</ul>
</div>
{{> partials/footer }}Layouts y Partials
Sistema de Layouts
Los layouts permiten definir la estructura común de tus vistas:
EJS
<!-- views/layouts/main.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %> - <%= globals.appName %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/css/main.css">
</head>
<body>
<header>
<%- include('../partials/navigation') %>
</header>
<main>
<%- body %>
</main>
<footer>
<%- include('../partials/footer') %>
</footer>
<script src="/assets/js/main.js"></script>
</body>
</html>Pug
//- views/layouts/main.pug
doctype html
html
head
title #{title} - #{globals.appName}
meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="stylesheet" href="/assets/css/main.css")
body
header
include ../partials/navigation
main
block content
footer
include ../partials/footer
script(src="/assets/js/main.js")Uso de Partials
Los partials te permiten reutilizar componentes comunes:
EJS
<!-- views/partials/navigation.ejs -->
<nav>
<ul>
<li><a href="/">Inicio</a></li>
<li><a href="/about">Acerca de</a></li>
<li><a href="/contact">Contacto</a></li>
<% if (user) { %>
<li><a href="/profile">Perfil</a></li>
<li><a href="/logout">Cerrar sesión</a></li>
<% } else { %>
<li><a href="/login">Iniciar sesión</a></li>
<% } %>
</ul>
</nav>Handlebars
{{!-- views/partials/navigation.hbs --}}
<nav>
<ul>
<li><a href="/">Inicio</a></li>
<li><a href="/about">Acerca de</a></li>
<li><a href="/contact">Contacto</a></li>
{{#if user}}
<li><a href="/profile">Perfil</a></li>
<li><a href="/logout">Cerrar sesión</a></li>
{{else}}
<li><a href="/login">Iniciar sesión</a></li>
{{/if}}
</ul>
</nav>Helpers para Vistas
Los helpers son funciones que puedes usar en tus plantillas para formatear datos, realizar lógica o generar HTML dinámicamente:
Definir Helpers Globales
import { TemplateEngineFactory } from '@foxframework/templates';
const templateEngine = TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs',
helpers: {
// Formatear fecha
formatDate: (date, format = 'DD/MM/YYYY') => {
return dayjs(date).format(format);
},
// Truncar texto
truncate: (text, length = 100) => {
if (text.length <= length) return text;
return text.substring(0, length) + '...';
},
// Generar URL para assets con versión para cache busting
asset: (path) => {
const version = process.env.APP_VERSION || Date.now();
return `/assets/${path}?v=${version}`;
},
// Verificar si una ruta está activa
isActive: (currentPath, path) => {
return currentPath === path ? 'active' : '';
}
}
});
server.setTemplateEngine(templateEngine);Usar Helpers en Plantillas
EJS
<!-- Formatear fecha -->
<p>Publicado el: <%= formatDate(post.publishedAt, 'DD MMM, YYYY') %></p>
<!-- Truncar texto -->
<p><%= truncate(post.content, 150) %></p>
<!-- URL para asset -->
<link rel="stylesheet" href="<%= asset('css/styles.css') %>">
<!-- Clase activa -->
<li class="<%= isActive(currentPath, '/about') %>">
<a href="/about">Acerca de</a>
</li>Handlebars
<!-- Formatear fecha -->
<p>Publicado el: {{formatDate post.publishedAt "DD MMM, YYYY"}}</p>
<!-- Truncar texto -->
<p>{{truncate post.content 150}}</p>
<!-- URL para asset -->
<link rel="stylesheet" href="{{asset 'css/styles.css'}}">
<!-- Clase activa -->
<li class="{{isActive currentPath '/about'}}">
<a href="/about">Acerca de</a>
</li>Opciones Avanzadas
Múltiples Motores
Puedes configurar múltiples motores de plantillas para diferentes partes de tu aplicación:
import { FoxFactory } from '@foxframework/core';
import { TemplateEngineFactory } from '@foxframework/templates';
// Crear el servidor con configuración básica
const server = FoxFactory.createServer({
// ...otras opciones
});
// Configurar motor principal para vistas HTML
const mainEngine = TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs'
});
// Configurar motor secundario para emails
const emailEngine = TemplateEngineFactory.create('handlebars', {
directory: './src/emails',
extension: 'hbs',
partials: './src/emails/partials'
});
// Registrar motores
server.setTemplateEngine('main', mainEngine); // Motor por defecto
server.setTemplateEngine('email', emailEngine);
// En un controlador
@Controller('/users')
export class UserController {
@Post('/register')
async register(ctx: HttpContext) {
// Registrar usuario
// ...
// Renderizar email con motor específico
const emailHtml = await ctx.view.engine('email').render('welcome', {
user: newUser,
activationUrl: `https://example.com/activate/${token}`
});
// Enviar email
await emailService.send({
to: newUser.email,
subject: 'Bienvenido a nuestra aplicación',
html: emailHtml
});
// Renderizar respuesta con motor principal
return ctx.view.render('registration-success', {
user: newUser
});
}
}Caché de Plantillas
Fox Framework implementa un sistema de caché para optimizar el rendimiento:
import { TemplateEngineFactory, FileSystemCache } from '@foxframework/templates';
// Configurar caché personalizada
const cacheProvider = new FileSystemCache({
directory: './cache/templates',
ttl: 3600 * 1000 // 1 hora
});
// Crear motor con caché
const templateEngine = TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs',
cache: true,
cacheProvider: cacheProvider
});
// También puedes controlar la caché por vista
@Get('/dashboard')
async dashboard(ctx: HttpContext) {
return ctx.view.render('dashboard', {
stats: await this.statsService.getLatest()
}, {
cache: true, // Habilitar caché para esta vista
cacheTtl: 60000, // TTL específico (1 minuto)
cacheKey: `dashboard-${ctx.auth.userId}` // Clave personalizada
});
}Compilación Previa
Para entornos de producción, puedes precompilar tus plantillas para mayor rendimiento:
import { TemplateCompiler } from '@foxframework/templates';
// Script para compilación de plantillas
async function compileTemplates() {
const compiler = new TemplateCompiler({
engine: 'ejs',
srcDirectory: './src/views',
outputDirectory: './dist/compiled-views',
extension: 'ejs'
});
// Compilar todas las plantillas
await compiler.compileAll();
console.log('Plantillas compiladas correctamente');
}
// Ejecutar durante el proceso de build
if (require.main === module) {
compileTemplates().catch(console.error);
}
// En producción, usar las plantillas compiladas
if (process.env.NODE_ENV === 'production') {
const templateEngine = TemplateEngineFactory.create('ejs', {
directory: './dist/compiled-views',
usePrecompiled: true
});
server.setTemplateEngine(templateEngine);
}Contexto de Vistas
El contexto de vistas proporciona datos y funcionalidades accesibles dentro de tus plantillas:
Variables Globales
const templateEngine = TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs',
globals: {
// Información de la aplicación
appName: 'Mi Aplicación',
version: '1.0.0',
// URLs comunes
urls: {
home: '/',
about: '/about',
contact: '/contact',
login: '/auth/login',
register: '/auth/register'
},
// Función para generar URLs
route: (name, params = {}) => {
return router.generateUrl(name, params);
},
// Entorno
isDev: process.env.NODE_ENV === 'development',
isProd: process.env.NODE_ENV === 'production',
// Fecha actual
now: () => new Date()
}
});Datos de Controladores
@Get('/products/:id')
async showProduct(ctx: HttpContext) {
const { id } = ctx.params;
// Cargar datos
const product = await this.productService.findById(id);
const relatedProducts = await this.productService.findRelated(id);
// Verificar si existe
if (!product) {
return ctx.response.notFound('Producto no encontrado');
}
// Renderizar con múltiples datos
return ctx.view.render('products/show', {
title: product.name,
product,
relatedProducts,
reviews: await this.reviewService.getForProduct(id),
// Datos del usuario
user: ctx.auth.user,
// Parámetros de URL
id,
// Estado
inStock: product.stock > 0,
// Metadatos
meta: {
description: product.shortDescription,
keywords: product.tags.join(', ')
},
// Variables para frontend
jsData: {
productId: product.id,
initialPrice: product.price,
hasVariants: product.variants.length > 0
}
});
}Variables Locales en Middleware
Puedes añadir variables para todas las vistas a través de middleware:
const viewContextMiddleware: Middleware = async (ctx, next) => {
// Añadir variables a todas las vistas
ctx.view.locals({
// Usuario actual
user: ctx.auth.user,
// URL actual para marcar elementos de menú activos
currentPath: ctx.request.path,
// Mensajes flash de sesiones anteriores
flashMessages: ctx.session.getFlash(),
// CSRF token
csrfToken: ctx.csrf.token(),
// Variables de configuración
settings: await settingsService.getPublicSettings()
});
return next();
};
// Registrar como middleware global
server.use(viewContextMiddleware);Funcionalidades Avanzadas
Integración con i18n
import { i18n } from '@foxframework/i18n';
// Configurar internacionalización
const i18nConfig = {
locales: ['es', 'en'],
defaultLocale: 'es',
directory: './src/locales'
};
// Inicializar i18n
const translator = await i18n.init(i18nConfig);
// Integrar con motor de templates
const templateEngine = TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs',
helpers: {
// Helper de traducción
t: (key, options = {}) => translator.translate(key, options),
// Formateo de números
formatNumber: (num, options = {}) => translator.formatNumber(num, options),
// Formateo de fechas
formatDate: (date, options = {}) => translator.formatDate(date, options),
// Pluralización
plural: (count, singular, plural) => count === 1 ? singular : plural
},
// Middleware para detectar y establecer idioma
middleware: async (ctx, next) => {
// Detectar idioma de la petición
const locale = ctx.locale || i18nConfig.defaultLocale;
// Establecer idioma para esta petición
translator.setLocale(locale);
// Continuar
return next();
}
});Transformación de Datos
// Transformador de datos antes de pasar a la vista
const templateEngine = TemplateEngineFactory.create('ejs', {
directory: './src/views',
extension: 'ejs',
// Transformar datos antes de renderizar
dataTransformers: [
// Escapar HTML por defecto (seguridad)
(data) => {
if (typeof data === 'string') {
return escapeHtml(data);
}
return data;
},
// Formatear fechas automáticamente
(data, key) => {
if (data instanceof Date) {
return dayjs(data).format('DD/MM/YYYY HH:mm');
}
return data;
},
// Transformar modelos a objetos planos
(data) => {
if (data && typeof data === 'object' && data.toJSON) {
return data.toJSON();
}
return data;
}
]
});Renderización Condicional
@Get('/dashboard')
async dashboard(ctx: HttpContext) {
// Determinar formato basado en header Accept o query param
const format = ctx.accepts(['html', 'json']) || ctx.query.format;
// Cargar datos
const data = {
stats: await this.dashboardService.getStats(),
recentActivity: await this.activityService.getRecent(),
notifications: await this.notificationService.getUnread(ctx.auth.userId)
};
// Renderizar según formato
if (format === 'json') {
return ctx.response.success(data);
} else {
return ctx.view.render('dashboard', {
title: 'Dashboard',
...data
});
}
}Componentes Reutilizables
Patrones para crear componentes reutilizables:
Macros en Nunjucks
{# views/macros/forms.njk #}
{% macro input(name, label, value='', type='text', attributes={}) %}
<div class="form-group">
<label for="{{ name }}">{{ label }}</label>
<input
type="{{ type }}"
name="{{ name }}"
id="{{ name }}"
value="{{ value }}"
{% for attr, val in attributes %}
{{ attr }}="{{ val }}"
{% endfor %}
class="form-control {{ 'is-invalid' if errors[name] }}"
>
{% if errors[name] %}
<div class="invalid-feedback">{{ errors[name] }}</div>
{% endif %}
</div>
{% endmacro %}
{# Uso en una plantilla #}
{% import "macros/forms.njk" as forms %}
<form method="post">
{{ forms.input('name', 'Nombre', user.name) }}
{{ forms.input('email', 'Correo electrónico', user.email, 'email', {required: true}) }}
{{ forms.input('password', 'Contraseña', '', 'password') }}
<button type="submit" class="btn btn-primary">Guardar</button>
</form>Partials con Parámetros en Handlebars
{{!-- views/components/card.hbs --}}
<div class="card {{cardClass}}">
{{#if title}}
<div class="card-header">{{title}}</div>
{{/if}}
<div class="card-body">
{{#if imageUrl}}
<img src="{{imageUrl}}" alt="{{imageAlt}}" class="card-img-top">
{{/if}}
{{#if content}}
<div class="card-text">{{content}}</div>
{{/if}}
{{> @partial-block }}
</div>
{{#if footer}}
<div class="card-footer">{{footer}}</div>
{{/if}}
</div>
{{!-- Uso en una plantilla --}}
{{#> components/card
title="Producto destacado"
imageUrl=product.imageUrl
imageAlt=product.name
cardClass="mb-4"
}}
<h5 class="card-title">{{product.name}}</h5>
<p>{{product.description}}</p>
<div class="price">${{formatNumber product.price}}</div>
<button class="btn btn-primary">Añadir al carrito</button>
{{/components/card}}Creación de Motores Personalizados
Puedes crear motores de templates personalizados implementando la interfaz TemplateEngine:
import { TemplateEngine, TemplateOptions, RenderOptions } from '@foxframework/templates';
import CustomTemplateLib from 'custom-template-lib';
export class CustomTemplateEngine implements TemplateEngine {
private engine: any;
private options: TemplateOptions;
private cache: Map<string, any> = new Map();
constructor(options: TemplateOptions) {
this.options = {
extension: 'custom',
cache: false,
...options
};
this.engine = new CustomTemplateLib({
// Configuración específica del motor
});
// Registrar helpers
if (options.helpers) {
for (const [name, fn] of Object.entries(options.helpers)) {
this.engine.registerHelper(name, fn);
}
}
}
async render(template: string, data: Record<string, any>, options?: RenderOptions): Promise<string> {
const templatePath = this.resolvePath(template);
const renderOptions = { ...this.options, ...options };
try {
// Si caché está habilitada
if (renderOptions.cache) {
const cached = this.cache.get(templatePath);
if (cached) {
return this.engine.renderTemplate(cached, { ...this.options.globals, ...data });
}
}
// Cargar plantilla
const templateSource = await fs.promises.readFile(templatePath, 'utf-8');
// Compilar si es necesario
const compiled = this.engine.compile(templateSource);
// Almacenar en caché si está habilitado
if (renderOptions.cache) {
this.cache.set(templatePath, compiled);
}
// Renderizar con datos
return this.engine.render(compiled, { ...this.options.globals, ...data });
} catch (error) {
throw new Error(`Error rendering template ${template}: ${error.message}`);
}
}
private resolvePath(template: string): string {
const ext = path.extname(template) ? '' : `.${this.options.extension}`;
return path.resolve(this.options.directory, `${template}${ext}`);
}
}
// Registrar el motor personalizado
TemplateEngineFactory.register('custom', (options) => new CustomTemplateEngine(options));
// Uso
const templateEngine = TemplateEngineFactory.create('custom', {
directory: './src/views',
extension: 'custom'
});
server.setTemplateEngine(templateEngine);Testing de Vistas
import { createViewMock } from '@foxframework/testing';
describe('Product View', () => {
let viewEngine;
beforeEach(() => {
// Crear mock del motor de vistas
viewEngine = createViewMock({
directory: './src/views',
extension: 'ejs'
});
});
test('should render product details correctly', async () => {
// Datos de prueba
const product = {
id: '123',
name: 'Test Product',
price: 99.99,
description: 'A test product',
inStock: true
};
// Renderizar vista
const html = await viewEngine.render('products/show', {
product,
title: product.name
});
// Verificar contenido
expect(html).toContain('Test Product');
expect(html).toContain('99.99');
expect(html).toContain('A test product');
expect(html).toContain('En stock');
expect(html).not.toContain('Agotado');
});
test('should show out of stock message', async () => {
const product = {
id: '123',
name: 'Test Product',
price: 99.99,
description: 'A test product',
inStock: false
};
const html = await viewEngine.render('products/show', { product });
expect(html).toContain('Agotado');
expect(html).not.toContain('En stock');
});
test('should render with global variables', async () => {
// Configurar globals para el test
viewEngine.setGlobals({
appName: 'Test App',
currentYear: 2023
});
const html = await viewEngine.render('products/show', {
product: { name: 'Test Product' }
});
expect(html).toContain('Test App');
expect(html).toContain('2023');
});
});Buenas Prácticas
Estructura de Directorios
Una estructura recomendada para tus vistas:
src/
views/
layouts/ # Layouts base
main.ejs # Layout principal
admin.ejs # Layout de administración
email.ejs # Layout para emails
partials/ # Componentes reutilizables
header.ejs
footer.ejs
navigation.ejs
sidebar.ejs
components/ # Componentes de UI
alert.ejs
card.ejs
pagination.ejs
forms/
input.ejs
select.ejs
checkbox.ejs
pages/ # Páginas principales
home.ejs
about.ejs
contact.ejs
auth/ # Vistas de autenticación
login.ejs
register.ejs
forgot-password.ejs
products/ # Vistas específicas de producto
index.ejs
show.ejs
_product-card.ejs # Partial específico de producto
admin/ # Vistas de administración
dashboard.ejs
users/
index.ejs
edit.ejs
errors/ # Páginas de error
404.ejs
500.ejsSeparación de Lógica y Presentación
<!-- ❌ Demasiada lógica en la vista -->
<div class="products">
<%
let filteredProducts = products;
if (category) {
filteredProducts = products.filter(p => p.category === category);
}
if (minPrice) {
filteredProducts = filteredProducts.filter(p => p.price >= parseFloat(minPrice));
}
if (maxPrice) {
filteredProducts = filteredProducts.filter(p => p.price <= parseFloat(maxPrice));
}
filteredProducts.sort((a, b) => a.price - b.price);
%>
<% filteredProducts.forEach(product => { %>
<!-- Mostrar producto -->
<% }); %>
</div>
<!-- ✅ Lógica movida al controlador -->
<div class="products">
<% products.forEach(product => { %>
<!-- Mostrar producto -->
<% }); %>
</div>En el controlador:
@Get('/products')
async listProducts(ctx: HttpContext) {
// Obtener parámetros
const { category, minPrice, maxPrice, sort } = ctx.query;
// Filtrar y ordenar productos
let products = await this.productService.findAll();
if (category) {
products = products.filter(p => p.category === category);
}
if (minPrice) {
products = products.filter(p => p.price >= parseFloat(minPrice));
}
if (maxPrice) {
products = products.filter(p => p.price <= parseFloat(maxPrice));
}
// Ordenar productos
if (sort === 'price-asc') {
products.sort((a, b) => a.price - b.price);
} else if (sort === 'price-desc') {
products.sort((a, b) => b.price - a.price);
}
return ctx.view.render('products/index', {
products,
filters: { category, minPrice, maxPrice, sort }
});
}Uso de Helpers para Código Reutilizable
// Registrar helpers útiles
templateEngine.registerHelpers({
// Formateo de moneda
currency: (amount, currency = 'USD', locale = 'es-ES') => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount);
},
// Formato de fechas relativas
relativeTime: (date) => {
const now = new Date();
const diffMs = now - new Date(date);
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) return 'Justo ahora';
if (diffMins < 60) return `Hace ${diffMins} minutos`;
if (diffHours < 24) return `Hace ${diffHours} horas`;
if (diffDays < 7) return `Hace ${diffDays} días`;
return new Intl.DateTimeFormat('es-ES').format(new Date(date));
},
// Pluralización
pluralize: (count, singular, plural) => {
return count === 1 ? singular : plural;
},
// Generar clases CSS condicionales
classNames: (...args) => {
return args
.flat()
.filter(Boolean)
.join(' ');
}
});Seguridad en Plantillas
// Configurar políticas de seguridad para el motor
templateEngine.setSecurityOptions({
// Auto-escapar HTML
escapeOutput: true,
// Lista de variables que no se escapan
noEscapeVars: ['safeHtml'],
// Desactivar acceso a ciertas propiedades
restrictedProperties: ['constructor', '__proto__', 'prototype'],
// Limitar recursión
maxRecursion: 10,
// Prevenir inyección de scripts
preventScriptInjection: true
});
// Uso en controlador con contenido HTML seguro
@Get('/page/:slug')
async showPage(ctx: HttpContext) {
const { slug } = ctx.params;
// Cargar página
const page = await this.pageService.findBySlug(slug);
if (!page) {
return ctx.response.notFound('Página no encontrada');
}
// Sanitizar contenido HTML
const sanitizedContent = this.securityService.sanitizeHtml(page.content);
// Renderizar vista
return ctx.view.render('pages/show', {
title: page.title,
content: page.content, // Se escapa automáticamente
safeHtml: sanitizedContent // No se escapa (marcado como seguro)
});
}Conclusión
El motor de templates de Fox Framework proporciona una solución versátil y potente para la capa de presentación de tus aplicaciones. Con soporte para múltiples sistemas de plantillas, un rico conjunto de características y un diseño extensible, te permite crear interfaces de usuario dinámicas manteniendo una clara separación entre la lógica de negocio y la presentación. Ya sea que estés construyendo una simple aplicación web o un sistema complejo con múltiples interfaces, el motor de templates te ofrece las herramientas necesarias para crear vistas elegantes, mantenibles y eficientes.