https://github.com/israel-nogueira/night-proxy
O night-proxy simplifica a aplicação de templates com o Proxy Recursivo, uma técnica inteligente que utiliza proxies do JavaScript para controle total sobre operações em objetos de modelo, oferecendo resultados impressionantes. Experimente e descubra uma nova abordagem intuitiva e poderosa para lidar com templates.
https://github.com/israel-nogueira/night-proxy
mustache mustache-js mustache-templating proxy proxy-pattern template template-engine
Last synced: about 1 month ago
JSON representation
O night-proxy simplifica a aplicação de templates com o Proxy Recursivo, uma técnica inteligente que utiliza proxies do JavaScript para controle total sobre operações em objetos de modelo, oferecendo resultados impressionantes. Experimente e descubra uma nova abordagem intuitiva e poderosa para lidar com templates.
- Host: GitHub
- URL: https://github.com/israel-nogueira/night-proxy
- Owner: israel-nogueira
- Created: 2019-12-07T00:09:14.000Z (over 6 years ago)
- Default Branch: no-reflow
- Last Pushed: 2026-04-26T05:32:13.000Z (3 months ago)
- Last Synced: 2026-04-26T07:22:34.553Z (3 months ago)
- Topics: mustache, mustache-js, mustache-templating, proxy, proxy-pattern, template, template-engine
- Language: HTML
- Homepage: https://israel-nogueira.github.io/night-proxy
- Size: 470 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Feito para quem constrói, não para quem quer aprender um framework.
Sua página PHP/HTML está pronta! E agora você precisa que **uma lista atualize sozinha**, que **um contador mude em tempo real**.
Ou que **um formulário reaja ao que o usuário digita** _(tudo isso sem recarregar a página)_.
Não temos nada contra os grandes frameworks, eles existem por boas razões, resolvem problemas reais e sustentam aplicações enormes. Mas existe um espaço enorme entre "página HTML com PHP" e "SPA completa com React".
## É exatamente esse espaço que o night-proxy.js ocupa.
A ideia é simples: um arquivo, uma tag script, e sua página passa a ser reativa. Sem npm. Sem build. Sem opinião sobre a sua stack.
Você escreve os atributos no HTML, popula os dados via JavaScript, e o DOM se atualiza sozinho de forma granular, eficiente, sem re-renders desnecessários com uma única linha:
```html
```
Ou via CDN:
```html
```
Sem npm. Sem build. Sem configuração. Você escreve HTML normal, adiciona alguns atributos, e a página começa a reagir sozinha às mudanças de dados. Funciona em qualquer projeto, PHP, HTML puro, WordPress, Laravel, o que for.
---
> ✅ Se você já sabe fazer uma página em HTML, você já sabe usar night-proxy.js.
---
## Para quem quer saber como funciona de verdade
night-proxy.js implementa **reatividade declarativa baseada em Proxy Recursivo** com dependency tracking granular, sem virtual DOM, sem dirty checking, sem re-renders desnecessários.
### 🔥 O que isso significa na prática
A maioria das soluções "leves" de reatividade usa uma de duas abordagens ruins: re-renderiza o componente inteiro a cada mudança, ou percorre o DOM em busca de diferenças (dirty checking). Ambas escalam mal.
night-proxy.js faz diferente. Durante o render de cada nó, qualquer leitura de propriedade registra automaticamente uma dependência via `WeakMap`. Quando um valor muda, **apenas os nós que dependem daquela propriedade específica são atualizados!** cirurgicamente, sem tocar no resto.
### 🛡️ Garantias de performance
- **1 dependente** → update síncrono imediato, sem microtask, sem overhead de scheduler
- **N dependentes** → batching via `Promise.resolve()`, agrupa mudanças simultâneas em um único ciclo
- **Mudanças estruturais** (push/splice/substituição de lista) → re-render do `x-for` via microtask
Em testes com 5.000 itens renderizados, um update pontual (`lista[2500].nome = 'x'`) executa em menos de 1 frame (< 16ms), porque toca **exatamente 1 nó**, independente do tamanho da lista.
### ⚙️ Diferenciais técnicos
- **Proxy Recursivo**, qualquer nível de aninhamento é rastreado automaticamente, sem necessidade de declarar observers manualmente
- **Effects com auto-cleanup**, cada nó reativo tem seu próprio Effect que se desregistra e re-registra nas dependências a cada run, evitando memory leaks e renders obsoletos
- **Lifecycle completo**, `$beforeRender`, `$afterRender`, `$beforeDestroy`, `$afterDestroy` com suporte a eventos DOM nativos (`before-render`, `after-destroy`...)
- **Destroy real**, limpa effects, event listeners, watchers e store em cascata; essencial para SPAs que trocam conteúdo via fetch
- **44 testes automatizados**, cobrindo reatividade, granularidade, stress (500 / 1k / 5k itens) e ciclo de vida completo, todos passando
### 🤓 Para quem é indicado
Projetos que precisam de reatividade pontual sem o custo de adotar um framework completo.
Aplicações PHP, páginas com conteúdo dinâmico via fetch, dashboards simples, formulários reativos
e qualquer cenário onde **React ou Vue seria complexidade desnecessária**.
**Um arquivo. Sem dependências. Sem opinião sobre sua stack.**
---
## 🛠️ Instalação
Um único arquivo. Inclua no ``:
```html
```
Nenhuma dependência externa necessária.
---
## Conceito básico
Você define um container reativo no HTML com `x-data` e inicializa o proxy.
Qualquer alteração em `proxy.template` atualiza o DOM automaticamente.
```html
proxy.initProxy();
proxy.template.produto.nome = "Coxinha Supreme";
proxy.template.produto.preco = 9.90;
```
---
## Diretivas
### `x-data="chave"`
Define o container reativo. As expressões internas não precisam de prefixo.
```html
```
```javascript
proxy.initProxy();
proxy.template.produto.nome = "Coxinha Supreme";
```
---
### `x-bind="expr"`
Renderiza um valor reativo como texto. Suporta expressão direta ou interpolação com `{}`.
```html
proxy.initProxy();
proxy.template.produto.nome = "Coxinha Supreme";
proxy.template.produto.preco = 9.90;
proxy.template.produto.qty = 3;
// Qualquer alteração posterior atualiza o DOM automaticamente
proxy.template.produto.nome = "Novo nome";
```
---
### `x-if="expr"`
Oculta o elemento via `display: none` quando a expressão for falsa. O elemento permanece no DOM.
```html
Produto disponível
Fora de estoque
```
```javascript
proxy.initProxy();
proxy.template.produto.ativo = true;
// Alternar visibilidade reativamente
proxy.template.produto.ativo = false;
```
---
### `x-for="alias in lista"`
Loop reativo e aninhável. Suporta duas sintaxes para declarar o índice com nome:
```html
```
Exemplo com aninhamento e índices nomeados:
```html
```
```javascript
proxy.initProxy();
proxy.template.pedido.itens = [
{
nome: "Salgados",
subitens: [{ label: "Coxinha" }, { label: "Pastel" }]
},
{
nome: "Bebidas",
subitens: [{ label: "Suco" }, { label: "Água" }]
}
];
// Adicionar item, re-render automático do x-for
proxy.template.pedido.itens.push({ nome: "Doces", subitens: [] });
// Update cirúrgico, toca só o nó do item[0], não re-renderiza a lista
proxy.template.pedido.itens[0].nome = "Salgadinhos";
```
---
### `x-key="expr"`
Chave única para otimização do `x-for`. O proxy reutiliza nós DOM existentes ao reordenar ou atualizar a lista. Sem `x-key`, um id interno é gerado automaticamente.
```html
```
```javascript
proxy.initProxy();
proxy.template.catalogo.lista = [
{ id: 1, nome: "Item A" },
{ id: 2, nome: "Item B" },
{ id: 3, nome: "Item C" },
];
// Com x-key, ao reordenar a lista o proxy reutiliza os nós DOM existentes
// em vez de recriar tudo, mais eficiente e preserva estado de inputs internos
proxy.template.catalogo.lista = [
{ id: 3, nome: "Item C" },
{ id: 1, nome: "Item A" },
{ id: 2, nome: "Item B" },
];
```
---
### `x-model="caminho"`
Two-way binding com inputs. Suporta `text`, `checkbox`, `radio`, `select` e caminhos profundos.
```html
Azul
Verde
Categoria A
Categoria B
```
```javascript
proxy.initProxy();
// Valor inicial, já aparece nos inputs
proxy.template.produto.nome = "Coxinha";
proxy.template.produto.ativo = true;
proxy.template.produto.cor = "azul";
proxy.template.produto.categoria = "a";
// Alterar via JS também atualiza os inputs (two-way)
proxy.template.produto.cor = "verde";
// Alterar via input também atualiza o proxy (two-way)
//, o usuário digita no campo e proxy.template.produto.nome é atualizado automaticamente
```
---
### `x-on:evento="expr"`
Event listener declarativo com acesso ao escopo do loop e às magic variables.
```html
✕
Selecionar
Log
```
```javascript
proxy.initProxy();
proxy.template.lista.itens = [
{ nome: "Item A" },
{ nome: "Item B" },
];
// Função registrada no escopo, disponível no x-on sem depender de globais
proxy.template.lista.remover = function(idx) {
proxy.template.lista.itens.splice(idx, 1);
};
// Ouvindo o evento emitido pelo $emit dentro do template
document.querySelector('[x-data="lista"]').addEventListener('selecionou', e => {
console.log('índice selecionado:', e.detail.index);
});
```
---
### `x-ref="nome"`
Registra uma referência ao elemento, acessível via `$ref.nome` em qualquer `x-on` do mesmo componente.
```html
Buscar
```
```javascript
proxy.initProxy();
// $ref é resolvido dentro do x-on, não precisa de querySelector manual
// O acesso ao DOM fica encapsulado no próprio componente
```
---
## Magic Variables
Disponíveis dentro de qualquer expressão `x-on`.
### `$root`
Elemento DOM do container `x-data`. Útil para manipulação direta ou para saber em qual componente o evento ocorreu.
```html
Toggle classe
```
```javascript
proxy.initProxy();
// $root é o próprio elemento [x-data="..."]
// Equivale a document.querySelector('[x-data="meu-componente"]')
// mas sem precisar de querySelector, já está no escopo do x-on
```
---
### `$ref`
Acessa elementos marcados com `x-ref` dentro do mesmo componente.
```html
Buscar
```
---
### `$emit`
Dispara um `CustomEvent` no `$root`. Ideal para comunicar ações do template para o JS externo.
```html
Selecionar
```
```javascript
// fora, no JS, quem quiser ouvir
document.querySelector('[x-data="lista"]').addEventListener('item-selecionado', e => {
console.log(e.detail.id);
});
```
---
### `$i` / `$index`
Índice do item atual no loop. Disponíveis quando nenhum alias de índice foi declarado. Quando um alias é declarado (`item, k in lista`), use o alias, `$i` e `$index` ficam como fallback.
```html
✕
```
```javascript
proxy.initProxy();
proxy.template.lista.itens = [
{ nome: "Item A" },
{ nome: "Item B" },
];
proxy.template.lista.remover = function(idx) {
proxy.template.lista.itens.splice(idx, 1);
};
```
---
### `$this`
Objeto rico com informações do item atual no loop.
| Propriedade | Descrição |
|---|---|
| `$this.index` | Índice do item no loop |
| `$this.dom` | Elemento DOM do item |
| `$this.data` | Objeto de dados do item |
| `$this.parent` | `$this` do loop pai (encadeável) |
```html
Log índices
```
Em loops profundamente aninhados, `$this.parent` é especialmente útil via JS:
```javascript
document.querySelector('[x-data="lista"]').addEventListener('meu-evento', e => {
const ctx = e.target.__nightThis;
console.log(ctx.index, ctx.parent.index, ctx.parent.parent.index);
});
```
---
### `event`
Evento DOM nativo, sempre disponível em `x-on`.
```html
Log
```
---
## Lifecycle Hooks
Executam antes e depois de cada ciclo de render do componente. Disponíveis via `proxy.template` ou `addEventListener` no container, ambas as formas funcionam simultaneamente e disparam em todo update, incluindo o render inicial.
```javascript
// via proxy.template
proxy.template.produto.$beforeRender = function(el) {
console.log('vai renderizar', el);
};
proxy.template.produto.$afterRender = function(el) {
console.log('renderizou', el);
};
```
```javascript
// via addEventListener
const el = document.querySelector('[x-data="produto"]');
el.addEventListener('before-render', () => console.log('antes'));
el.addEventListener('after-render', () => console.log('depois'));
```
---
---
## Destroy
Limpa completamente um componente antes de recarregar via fetch ou ao desmontar a página. O destroy remove todos os effects reativos, event listeners declarados com `x-on`, watchers registrados e a entrada no store interno.
### Destruir um componente específico
```javascript
// via método direto
proxy.destroy('produto');
// via shortcut no template
proxy.template.produto.destroy();
```
### Destruir tudo
```javascript
proxy.destroy();
```
### Hooks de destroy
Executam antes e depois do destroy. Disponíveis via `proxy.template` ou `addEventListener`, ambas as formas funcionam simultaneamente.
```javascript
// via proxy.template
proxy.template.produto.$beforeDestroy = function(el) {
console.log('vai destruir', el);
};
proxy.template.produto.$afterDestroy = function(el) {
console.log('destruído', el);
};
```
```javascript
// via addEventListener
const el = document.querySelector('[x-data="produto"]');
el.addEventListener('before-destroy', () => console.log('antes do destroy'));
el.addEventListener('after-destroy', () => console.log('depois do destroy'));
```
### Padrão com fetch
O destroy é o ponto de entrada natural antes de qualquer recarga de conteúdo dinâmico:
```javascript
async function carregarProduto(id) {
// 1. Mata o componente atual
proxy.destroy('produto');
// 2. Busca novo conteúdo
const res = await fetch(`/produto/${id}`);
const html = await res.text();
// 3. Injeta o HTML
document.querySelector('#container').innerHTML = html;
// 4. Reinicializa
proxy.initProxy();
proxy.template.produto.nome = "Novo produto";
}
```
## Watchers
Observa qualquer caminho do store e executa um callback quando o valor muda. Retorna uma função de cancelamento.
```javascript
const unsubscribe = proxy.on('produto.preco', (novoValor, valorAnterior) => {
console.log(`preço: ${valorAnterior} → ${novoValor}`);
});
// Cancelar
unsubscribe();
```
Funciona com caminhos profundos:
```javascript
proxy.on('pedido.itens.0.nome', (novo, velho) => {
console.log('nome do primeiro item mudou');
});
```
---
## Inicialização
```javascript
proxy.initProxy();
```
Chame após o DOM estar pronto. Em seguida, popule os dados via `proxy.template`:
```javascript
proxy.initProxy();
proxy.template.produto.nome = "Coxinha Supreme";
proxy.template.produto.ativo = true;
proxy.template.produto.preco = 9.90;
proxy.template.produto.lista = [
{ titulo: "Com catupiry", subitens: [{ label: "P" }, { label: "G" }] },
{ titulo: "Com bacon", subitens: [{ label: "Único" }] },
];
```
---
## Atualizações reativas
```javascript
// Valor simples, síncrono, atualiza o DOM na hora
proxy.template.produto.nome = "Novo nome";
// Item específico da lista, cirúrgico, toca só o nó do item
proxy.template.produto.lista[0].titulo = "Titulo atualizado";
// Substituir lista inteira
proxy.template.produto.lista = [{ titulo: "Item novo", subitens: [] }];
// Adicionar item
proxy.template.produto.lista.push({ titulo: "Mais um", subitens: [] });
// Remover item
proxy.template.produto.lista.splice(2, 1);
```
---
## Funções no escopo
Registre funções diretamente no `proxy.template` para usá-las nos eventos sem depender de globais.
```html
✕
proxy.initProxy();
proxy.template.lista.itens = [
{ nome: "Item A" },
{ nome: "Item B" },
];
proxy.template.lista.remover = function(idx) {
proxy.template.lista.itens.splice(idx, 1);
};
</>
```
---
## Exemplo completo
```html
<input type="text" x-model="pedido.cliente" placeholder="Nome do cliente">
<input type="checkbox" x-model="pedido.confirmado"> Confirmado
<div x-data="pedido">
<h2 x-bind="cliente"></h2>
<p x-if="confirmado">✅ Pedido confirmado</p>
<div x-for="item, k in itens">
<strong x-bind="{item.nome}"></strong>
<span x-bind="Qtd: {item.qty}"></span>
<button x-on:click="remover(k)">✕</button>
<button x-on:click="$emit('selecionou', { index: k, nome: item.nome })">Selecionar</button>
</div>
</div>
<script>
proxy.initProxy();
proxy.template.pedido.cliente = "João Silva";
proxy.template.pedido.confirmado = true;
proxy.template.pedido.itens = [
{ nome: "Coxinha", qty: 3 },
{ nome: "Pastel", qty: 2 },
{ nome: "Salgado", qty: 5 },
];
proxy.template.pedido.remover = function(idx) {
proxy.template.pedido.itens.splice(idx, 1);
};
proxy.template.pedido.$afterRender = function() {
console.log('pedido renderizado');
};
document.querySelector('[x-data="pedido"]').addEventListener('selecionou', e => {
console.log('selecionado:', e.detail.nome);
});
proxy.on('pedido.cliente', (novo, velho) => {
console.log(`cliente: ${velho} → ${novo}`);
});
```
---
## Referência rápida
### Diretivas
| Diretiva | Descrição |
|---|---|
| `x-data="chave"` | Container reativo, escopo sem prefixo |
| `x-bind="expr"` | Renderiza valor ou interpolação `{var}` como texto |
| `x-if="expr"` | Condicional, oculta via `display: none` |
| `x-for="alias in lista"` | Loop reativo (aninhável) |
| `x-for="alias, k in lista"` | Loop com índice nomeado |
| `x-for="alias in lista as k"` | Loop com índice nomeado (sintaxe alternativa) |
| `x-key="expr"` | Chave única para reuso de nós no `x-for` |
| `x-model="caminho"` | Two-way binding com inputs |
| `x-on:evento="expr"` | Event listener com escopo do loop |
| `x-ref="nome"` | Referência ao elemento via `$ref.nome` |
### Magic Variables (em `x-on`)
| Variable | Descrição |
|---|---|
| `$root` | Elemento DOM do container `x-data` |
| `$ref.nome` | Elemento marcado com `x-ref="nome"` |
| `$emit(nome, detalhe)` | Dispara `CustomEvent` no `$root` |
| `$i` / `$index` | Índice do item (fallback sem alias declarado) |
| `$this.index` | Índice do item atual |
| `$this.dom` | Elemento DOM do item atual |
| `$this.data` | Objeto de dados do item atual |
| `$this.parent` | `$this` do loop pai (encadeável) |
| `event` | Evento DOM nativo |
### API JavaScript
| Método | Descrição |
|---|---|
| `proxy.initProxy()` | Inicializa o proxy |
| `proxy.destroy('key')` | Destrói um componente específico |
| `proxy.destroy()` | Destrói todos os componentes |
| `proxy.template.key.destroy()` | Shortcut para destruir um componente |
| `proxy.on(path, fn)` | Observa um caminho, retorna `unsubscribe()` |
| `proxy.template.key.$beforeRender = fn` | Hook antes do render |
| `proxy.template.key.$afterRender = fn` | Hook após o render |
| `proxy.template.key.$beforeDestroy = fn` | Hook antes do destroy |
| `proxy.template.key.$afterDestroy = fn` | Hook após o destroy |
---
## Como funciona internamente
O proxy usa **dependency tracking** granular: durante o render de cada nó, qualquer leitura de propriedade registra uma dependência. Quando um valor muda:
- **1 dependente** → render síncrono imediato, sem microtask
- **Múltiplos dependentes** → batching via microtask (`Promise.resolve`)
- **Mudanças estruturais** (push/splice/substituição de lista) → re-render do `x-for` via microtask
Isso garante que `lista[250].nome = 'x'` toque **apenas o nó do item 250**, independente do tamanho da lista.