Reatividade declarativa

Sua página PHP
Agora reativa!

Um arquivo, uma tag <script>, e sua página passa a ser reativa. Proxy recursivo, dependency tracking granular, ~8.7KB gzip. Funciona em qualquer stack.

Zero dependências Sem build tools MIT License
demo.html
Preview ao vivo
● Disponível
● Fora de estoque
~8.7KB
gzipped
0
dependências
15min
curva de aprendizado
<16ms
update em 5k itens
1
arquivo necessário
// exemplos interativos

Veja funcionando agora

Cada exemplo abaixo está rodando de verdade com shadow-proxy. Edite, clique, experimente.

index.html
<div x-data="produto">
  <h2 x-bind="{nome}"></h2>
  <p  x-bind="R$ {preco}"></p>

  <div x-if="ativo">✅ Disponível</div>
  <div x-if="!ativo">❌ Indisponível</div>
</div>

<input x-model="produto.nome">

<script>
  shadowProxy.initProxy();

  shadowProxy.template.produto.nome  = "Coxinha Supreme";
  shadowProxy.template.produto.preco = 9.90;
  shadowProxy.template.produto.ativo = true;
</script>
Resultado ao vivo
✅ Disponível
❌ Indisponível
lista-reativa.html
<div x-data="tarefas">
  <div x-for="item, k in lista">
    <div class="todo-check {item.done ? 'done' : ''}"
         x-on:click="toggleTodo(k)"
         x-bind="{item.done ? '✓' : ''}"></div>
    <span class="todo-text {item.done ? 'done' : ''}"
          x-bind="{item.texto}"></span>
    <button x-on:click="removeTodo(k)"></button>
  </div>
</div>

<input id="todo-input" type="text">
<button x-on:click="addTodo()">+ Adicionar</button>

<script>
  shadowProxy.initProxy();

  shadowProxy.template.tarefas.lista = [
    { texto: "Aprender shadow-proxy", done: false },
    { texto: "Refatorar o projeto", done: false },
  ];

  // só muda o dado — classe "done", ✓ e riscado seguem sozinhos
  shadowProxy.template.tarefas.toggleTodo = function(k) {
    var item = shadowProxy.template.tarefas.lista[k];
    item.done = !item.done;
  };

  shadowProxy.template.tarefas.removeTodo = function(k) {
    shadowProxy.template.tarefas.lista.splice(k, 1);
  };

  shadowProxy.template.tarefas.addTodo = function() {
    var inp = document.getElementById('todo-input');
    if (!inp.value.trim()) return;
    shadowProxy.template.tarefas.lista.push({ texto: inp.value, done: false });
    inp.value = '';
  };
</script>
Resultado ao vivo
watcher.html
<div x-data="contador">
  <h2 x-bind="{valor}"></h2>
</div>

<input type="range" x-model="contador.valor">

<script>
  shadowProxy.initProxy();
  shadowProxy.template.contador.valor = 50;

  // Observa qualquer mudança no valor
  shadowProxy.on('contador.valor', (novo, velho) => {
    console.log(`${velho} → ${novo}`);
  });
</script>
Resultado ao vivo
contador.valor
// shadowProxy.on log
carrinho.html
<div x-data="carrinho">
  <div x-for="item, k in itens">
    <span x-bind="{item.nome}"></span>
    <span x-bind="R$ {item.preco}"></span>
    <button x-on:click="decrementar(k)">-</button>
    <span x-bind="{item.qty}"></span>
    <button x-on:click="incrementar(k)">+</button>
  </div>

  <div>Total: <span x-bind="R$ {total}"></span></div>
</div>

<script>
  shadowProxy.initProxy();

  shadowProxy.template.carrinho.itens = [
    { nome: "Teclado Mecânico", preco: 249.90, qty: 1 },
    { nome: "Mouse Gamer", preco: 89.90, qty: 2 },
  ];

  // shadowProxy.on ouve o array inteiro — muda quando qualquer item muda
  function recalcularTotal() {
    var itens = shadowProxy.template.carrinho.itens;
    var soma = itens.reduce(function(acc, i) {
      return acc + i.preco * i.qty;
    }, 0);
    shadowProxy.template.carrinho.total = soma.toFixed(2);
  }
  recalcularTotal();
  shadowProxy.on('carrinho.itens', recalcularTotal);

  shadowProxy.template.carrinho.incrementar = function(k) {
    shadowProxy.template.carrinho.itens[k].qty++;
  };
  shadowProxy.template.carrinho.decrementar = function(k) {
    var item = shadowProxy.template.carrinho.itens[k];
    if (item.qty > 1) item.qty--;
  };
</script>
Resultado ao vivo
Total
busca.html
<div x-data="busca">
  <input type="text" x-model="busca.termo"
         placeholder="Buscar produto...">

  <div x-for="item in resultado">
    <div x-bind="{item.nome}"></div>
  </div>

  <p x-if="resultado.length === 0">Nada encontrado</p>
</div>

<script>
  shadowProxy.initProxy();

  shadowProxy.template.busca.produtos = [
    { nome: "Teclado Mecânico" }, { nome: "Mouse Gamer" },
    { nome: "Monitor 4K" },      { nome: "Headset" },
  ];
  shadowProxy.template.busca.termo = '';

  function filtrar() {
    var termo = shadowProxy.template.busca.termo.toLowerCase();
    shadowProxy.template.busca.resultado = shadowProxy.template.busca.produtos.filter(function(p) {
      return p.nome.toLowerCase().includes(termo);
    });
  }
  filtrar();
  shadowProxy.on('busca.termo', filtrar);
</script>
Resultado ao vivo

Nada encontrado

seguranca.html
// registra o handler global de erros ANTES de tudo
shadowProxy.onError = function(err) {
  if (err.type === 'x-on-unsafe') {
    logConsole('🚫 Bloqueado: ' + err.expr, false);
  }
};

// tenta bindar uma expressão perigosa em runtime
shadowProxy.template.seguranca.testarPerigoso = function() {
  var el = document.createElement('button');
  el.setAttribute('x-on:click', "window.location='http://evil.com'");

  // nunca vira listener — bloqueado antes de existir
  shadowProxy.bindEvents(el, {}, 'seguranca');
};

shadowProxy.template.seguranca.testarSeguro = function() {
  logConsole('✅ Ação permitida — sem acesso a window/document/eval', true);
};
Resultado ao vivo
// clique nos botões abaixo pra ver o sandbox em ação
performance.html
shadowProxy.template.perf.gerarLista = function() {
  var arr = [];
  for (var i = 0; i < 3000; i++) arr.push({ nome: 'Item #' + i });

  var t0 = performance.now();
  shadowProxy.template.perf.itens = arr; // dispara render de 3.000 nós

  requestAnimationFrame(function() {
    var t1 = performance.now();
    shadowProxy.template.perf.tempoRender = (t1 - t0).toFixed(1);
  });
};

shadowProxy.template.perf.atualizarUm = function() {
  var itens = shadowProxy.template.perf.itens;
  var idx = Math.floor(Math.random() * itens.length);

  var t0 = performance.now();
  itens[idx].nome = 'Atualizado ✓ #' + idx; // toca só esse nó

  requestAnimationFrame(function() {
    var t1 = performance.now();
    shadowProxy.template.perf.tempoUpdate = (t1 - t0).toFixed(2);
  });
};
Resultado ao vivo — medido no seu navegador agora
Render de itens
Update cirúrgico (1 item)

// por que usar

Tudo que você precisa,
nada que você não precisa

shadow-proxy entrega reatividade real sem as abstrações desnecessárias dos grandes frameworks.

Proxy Recursivo
Qualquer nível de aninhamento é rastreado automaticamente. Sem observers manuais, sem boilerplate.
🎯
Update Cirúrgico
lista[250].nome = 'x' toca exatamente 1 nó no DOM, independente do tamanho da lista.
🔗
Two-way Binding
x-model funciona com input, checkbox, radio e select. Mudanças no DOM refletem no objeto automaticamente.
🏗️
Loops Aninháveis
x-for com índice nomeado, sintaxe flexível, e suporte a arrays vazios sem dados dummy.
🛡️
Lifecycle Hooks
$beforeRender, $afterRender, $beforeDestroy, $afterDestroy — controle total sobre o ciclo de vida.
🔍
Destroy Real
Limpa effects, event listeners, watchers e store em cascata. Essencial para SPAs com fetch.

// comparação

Por que não Alpine, Vue ou React?

Existe um espaço enorme entre "página HTML com PHP" e "SPA completa com React". shadow-proxy ocupa exatamente esse espaço.

Característica ShadowProxy Alpine.js Vue.js (CDN) React (CDN)
Tamanho (min+gzip) ~8.7KB ~13.5KB ~34KB ~45KB
Build tools Não precisa
Dependências Zero Zero Zero Zero
Curva de aprendizado 15 minutos 1 hora 2 dias 1 semana
Two-way binding
Error handler customizável
Destroy real (SPA/fetch)
Funciona em PHP puro

// instalação

Pronto em 30 segundos

Um arquivo. Sem npm, sem config, sem opinião sobre sua stack.

Via CDN (mais rápido)
<script src="https://cdn.jsdelivr.net/gh/israel-nogueira/shadow-proxy@no-reflow/shadow-proxy.min.js"></script>
Download direto
<script src="./shadow-proxy.js"></script>
✓ É só isso. Inclua o script no <head>, adicione x-data no HTML e chame shadowProxy.initProxy(). Sua página já é reativa.