@malv/rendering
v1.2.0
Published
Build-time HTML component rendering engine for Malv
Maintainers
Readme
@malv/rendering
Build-time HTML component rendering engine for Vite. Create reusable HTML components with scoped CSS and slots - no runtime JavaScript required for rendering.
Installation
npm install @malv/renderingQuick Start
1. Create a component
<!-- components/my-button.html -->
<button class="my-button">
<slot></slot>
</button>
<style>
.my-button {
background: blue;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
</style>2. Configure Vite
// vite.config.ts
import { Rendering } from '@malv/rendering'
export default {
plugins: [
Rendering({
'my-button': './components/my-button.html',
})
]
}3. Use in HTML
<!-- index.html -->
<my-button>Click me</my-button>At build time, this becomes:
<button class="my-button">Click me</button>API
Rendering(components, options?)
The main Vite plugin.
import { Rendering } from '@malv/rendering'
Rendering(components: ComponentsMap, options?: RenderingOptions)folder(path)
Register all .html files in a directory as components. File names become tag names.
import { Rendering, folder } from '@malv/rendering'
export default {
plugins: [
Rendering({
...folder('./components'),
// components/my-button.html → <my-button>
// components/my-card.html → <my-card>
})
]
}component(name, source)
Register a single component.
import { Rendering, component } from '@malv/rendering'
export default {
plugins: [
Rendering({
...component('my-button', './components/my-button.html'),
})
]
}components(map)
Register multiple components at once.
import { Rendering, components } from '@malv/rendering'
export default {
plugins: [
Rendering({
...components({
'my-button': './components/my-button.html',
'my-card': './components/my-card.html',
}),
})
]
}Component Sources
Components can be defined in several ways:
{
// Relative file path
'my-button': './components/my-button.html',
// Absolute file path
'my-card': { type: 'absolute', path: '/path/to/my-card.html' },
// Raw HTML content
'my-divider': { type: 'raw', html: '<hr class="divider" />' },
// Dynamic import
'my-dynamic': () => import('./components/my-dynamic.html'),
}Features
Slots
Use <slot> to insert content passed to your component.
<!-- components/card.html -->
<div class="card">
<slot></slot>
</div><!-- Usage -->
<card>
<p>This content goes in the slot</p>
</card>Named Slots
Use #slotname to create named slots.
<!-- components/card.html -->
<div class="card">
<div class="card-header">
<slot name="header"></slot>
</div>
<div class="card-body">
<slot></slot>
</div>
</div><!-- Usage -->
<card>
<h2 #header>Card Title</h2>
<p>Card content goes here</p>
</card>Attributes
Attributes are passed to the #default element (or root element if none specified).
<!-- components/button.html -->
<button #default class="btn">
<slot></slot>
</button><!-- Usage -->
<button disabled data-action="submit">Submit</button>
<!-- Result -->
<button class="btn" disabled data-action="submit">Submit</button>Scoped Styles
Each component's <style> block is extracted and bundled. In development, styles update via HMR without page refresh.
<!-- components/button.html -->
<button class="btn"><slot></slot></button>
<style>
.btn {
background: blue;
color: white;
}
</style>Component Scripts
Add <script> blocks for component-specific JavaScript.
<!-- components/counter.html -->
<div class="counter">
<button class="decrement">-</button>
<span class="count">0</span>
<button class="increment">+</button>
</div>
<script>
document.querySelectorAll('.counter').forEach(counter => {
const count = counter.querySelector('.count')
counter.querySelector('.increment').onclick = () => {
count.textContent = parseInt(count.textContent) + 1
}
counter.querySelector('.decrement').onclick = () => {
count.textContent = parseInt(count.textContent) - 1
}
})
</script>Plugins
Create plugins to dynamically generate components from external data sources.
import { Rendering, type RenderingPlugin } from '@malv/rendering'
import fs from 'fs/promises'
const dataPlugin: RenderingPlugin = {
name: 'product-components',
filePaths: ['./data/products.json'], // Changes trigger full reload
async components() {
const products = JSON.parse(
await fs.readFile('./data/products.json', 'utf-8')
)
return products.map(product => ({
name: `product-${product.id}`,
filePaths: [`./data/products/${product.id}.json`], // Changes trigger component reload
async resolve() {
const data = JSON.parse(
await fs.readFile(`./data/products/${product.id}.json`, 'utf-8')
)
return `
<div class="product">
<h2>${data.name}</h2>
<p>${data.price}</p>
</div>
`
}
}))
}
}
export default {
plugins: [
Rendering({}, { plugins: [dataPlugin] })
]
}Options
interface RenderingOptions {
plugins?: RenderingPlugin[]
}How It Works
- Initialization: Components are loaded and their CSS/JS is extracted
- Transformation: HTML files are processed, replacing component tags with their content
- Slot Resolution: Content is inserted into
<slot>elements - Attribute Mapping: Attributes are applied to designated elements
- CSS Bundling: Extracted styles are bundled into a single CSS file
- HMR (Dev): Changes trigger granular updates without full page reload
Output
In production builds:
dist/
├── index.html # Processed HTML with components expanded
├── mesa-styles/
│ └── indexAb3Cd-mesa.css # Bundled component CSS
└── assets/
└── ... # Other assetsLicense
MIT
