Rettangoli Frontend
A modern frontend framework that uses YAML for view definitions, web components for composition, and Immer for state management. Build reactive applications with minimal complexity using just 3 types of files.
Features
- 🗂️ Three-File Architecture -
.view.yaml
, .store.js
, .handlers.js
files scale from single page to complex applications
- 📝 YAML Views - Declarative UI definitions that compile to virtual DOM
- 🧩 Web Components - Standards-based component architecture
- 🔄 Reactive State - Immer-powered immutable state management
- ⚡ Fast Development - Hot reload with Vite integration
- 🎯 Template System - Jempl templating for dynamic content
- 🧪 Testing Ready - Pure functions and dependency injection for easy testing
Quick Start
Production usage (when rtgl is installed globally):
rtgl fe build
rtgl fe watch
rtgl fe scaffold
Architecture
Technology Stack
Runtime:
Build & Development:
- ESBuild - Fast bundling
- Vite - Development server with hot reload
Browser Native:
- Web Components - Component encapsulation
Development
Prerequisites
- Node.js 18+ or Bun
- A
rettangoli.config.yaml
file in your project root
Setup
bun install
- Create project structure:
node ../rettangoli-cli/cli.js fe scaffold --category components --name MyButton
node ../rettangoli-cli/cli.js fe build
node ../rettangoli-cli/cli.js fe watch
Project Structure
src/
├── cli/
│ ├── build.js # Build component bundles
│ ├── watch.js # Development server with hot reload
│ ├── scaffold.js # Component scaffolding
│ ├── examples.js # Generate examples for testing
│ └── blank/ # Component templates
├── createComponent.js # Component factory
├── createWebPatch.js # Virtual DOM patching
├── parser.js # YAML to JSON converter
├── common.js # Shared utilities
└── index.js # Main exports
Usage
Component Structure
Each component consists of three files:
component-name/
├── component-name.handlers.js # Event handlers
├── component-name.store.js # State management
└── component-name.view.yaml # UI structure and styling
View Layer (.view.yaml)
Views are written in YAML and compiled to virtual DOM at build time.
Basic HTML Structure
template:
- div#myid.class1.class2 custom-attribute=abcd:
- rtgl-text: "Hello World"
- rtgl-button: "Click Me"
Compiles to:
<div id="myid" class="class1 class2" custom-attribute="abcd">
<rtgl-text>Hello World</rtgl-text>
<rtgl-button>Click Me</rtgl-button>
</div>
Component Definition
elementName: my-custom-component
template:
- rtgl-view:
- rtgl-text: "My Component"
Attributes vs Props
When passing data to components, there's an important distinction:
template:
- custom-component title=Hello .items=items
- Attributes (
title=Hello
): Always string values, passed as HTML attributes
- Props (
.items=items
): JavaScript values from viewData, passed as component properties
Attributes become HTML attributes, while props are JavaScript objects/arrays/functions passed directly to the component.
Variable Expressions
Views do not support complex variable expressions like ${myValue || 4}
. All values must be pre-computed in the toViewData
store function:
❌ Don't do this:
template:
- rtgl-text: "${user.name || 'Guest'}"
- rtgl-view class="${isActive ? 'active' : 'inactive'}"
✅ Do this instead:
export const toViewData = ({ state, props, attrs }) => {
return {
...state,
displayName: state.user.name || 'Guest',
statusClass: state.isActive ? 'active' : 'inactive'
};
};
template:
- rtgl-text: "${displayName}"
- rtgl-view class="${statusClass}"
Styling
styles:
'#title':
font-size: 24px
color: blue
'@media (min-width: 768px)':
'#title':
font-size: 32px
Event Handling
refs:
submitButton:
eventListeners:
click:
handler: handleSubmit
template:
- rtgl-button#submitButton: "Submit"
Templating with Jempl
Loops:
template:
- rtgl-view:
projects:
$for project, index in projects:
- rtgl-view#project-${project.id}:
- rtgl-text: "${project.name}"
- rtgl-text: "${project.description}"
- rtgl-text: "Item ${index}"
Props caveats
❌ This will not work. Prop references can only be taken from viewDate, not from loop variables
template:
- rtgl-view:
- $for project, index in projects:
- rtgl-view#project-${project.id}:
- custom-component .item=project:
✅ This is the workaround
template:
- rtgl-view:
- $for project, index in projects:
- rtgl-view#project-${project.id}:
- custom-component .item=projects[${index}]:
Conditionals:
template:
- rtgl-view:
$if isLoggedIn:
- user-dashboard: []
$else:
- login-form: []
template:
- rtgl-view:
$if user.age >= 18 && user.verified:
- admin-panel: []
$elif user.age >= 13:
- teen-dashboard: []
$else:
- kid-dashboard: []
For more advanced templating features, see the Jempl documentation.
Data Schemas
Define component interfaces with JSON Schema:
viewDataSchema:
type: object
properties:
title:
type: string
default: "My Component"
items:
type: array
items:
type: object
propsSchema:
type: object
properties:
onSelect:
type: function
attrsSchema:
type: object
properties:
variant:
type: string
enum: [primary, secondary]
State Management (.store.js)
Initial State
export const INITIAL_STATE = Object.freeze({
title: "My App",
items: [],
loading: false
});
View Data Transformation
export const toViewData = ({ state, props, attrs }) => {
return {
...state,
itemCount: state.items.length,
hasItems: state.items.length > 0
};
};
Selectors
export const selectItems = (state) => state.items;
export const selectIsLoading = (state) => state.loading;
Actions
export const setLoading = (state, isLoading) => {
state.loading = isLoading;
};
export const addItem = (state, item) => {
state.items.push(item);
};
export const removeItem = (state, itemId) => {
const index = state.items.findIndex(item => item.id === itemId);
if (index !== -1) {
state.items.splice(index, 1);
}
};
Event Handlers (.handlers.js)
Special Handlers
export const handleOnMount = (deps) => {
const { store, render } = deps;
store.setLoading(true);
loadData().then(data => {
store.setItems(data);
store.setLoading(false);
render();
});
return () => {
};
};
Event Handlers
export const handleSubmit = async (event, deps) => {
const { store, render, attrs, props } = deps;
event.preventDefault();
const formData = new FormData(event.target);
const newItem = Object.fromEntries(formData);
store.addItem(newItem);
render();
deps.dispatchEvent(new CustomEvent('item-added', {
detail: { item: newItem }
}));
};
export const handleItemClick = (event, deps) => {
const itemId = event.target.id.replace('item-', '');
console.log('Item clicked:', itemId);
};
Dependency Injection
const componentDependencies = {
apiClient: new ApiClient(),
router: new Router()
};
export const deps = {
components: componentDependencies,
pages: {}
};
Access in handlers:
export const handleLoadData = async (event, deps) => {
const { apiClient } = deps.components;
const data = await apiClient.fetchItems();
};
Configuration
Create a rettangoli.config.yaml
file in your project root:
fe:
dirs:
- "./src/components"
- "./src/pages"
setup: "setup.js"
outfile: "./dist/bundle.js"
examples:
outputDir: "./vt/specs/examples"
Testing
View Components
Use visual testing with rtgl vt
:
rtgl vt generate
rtgl vt report
Examples
For a complete working example, see the todos app in examples/example1/
.