Un framework JavaScript ligero y moderno para desarrollo basado en componentes. Diseñado para ser simple, eficiente y poderoso.
- ⚡ Renderizado Quirúrgico: Solo actualiza lo que realmente ha cambiado
- 🧩 Componentes Inteligentes: Encapsulación completa con estado reactivo
- 🔄 Data Binding Automático: Sincronización bidireccional sin configuración
- 🚪 Modales Elegantes: Sistema de ventanas emergentes con animaciones
- 🛣️ Router SPA: Navegación fluida para aplicaciones de una sola página
- 🎨 CSS Scoped: Estilos encapsulados que no interfieren entre sí
- 📱 Mobile-First: Optimizado para dispositivos móviles
- 🪶 Ultraligero: Solo ~15KB minificado
- 🔧 Zero Dependencies: Sin dependencias externas
<!-- En el <head> de tu HTML -->
<script src="https://unpkg.com/@pablotheblink/scopejs@2.0.4/js/ScopeJS.js"></script>
<script>
// Las funciones están disponibles globalmente
const MiComponente = ScopeJS.Component({
controller: class {
constructor() {
this.mensaje = "¡Hola Mundo!";
}
},
render() {
return `<h1>${this.mensaje}</h1>`;
},
});
// Renderizar cuando la página cargue
window.addEventListener("load", () => {
const container = document.getElementById("app");
MiComponente.render(container);
});
</script>
<!-- En el <head> de tu HTML -->
<script type="module">
import { Component, Modal, Router } from "https://unpkg.com/@pablotheblink/scopejs@2.0.4/js/ScopeJS.js";
const MiComponente = Component({
controller: class {
constructor() {
this.mensaje = "¡Hola Mundo!";
}
},
render() {
return `<h1>${this.mensaje}</h1>`;
},
});
// Renderizar cuando la página cargue
window.addEventListener("load", () => {
const container = document.getElementById("app");
MiComponente.render(container);
});
</script>
npm install @pablotheblink/scopejs
import { Component, Modal, Router } from "@pablotheblink/scopejs";
Los componentes en ScopeJS son como ladrillos LEGO inteligentes que encapsulan todo lo que necesitas:
import { Component } from "@pablotheblink/scopejs";
const Counter = Component({
// Controlador con lógica
controller: class {
constructor() {
this.count = 0;
this.step = 1;
}
increment() {
this.count += this.step;
this.apply(); // Re-renderizar
}
decrement() {
this.count -= this.step;
this.apply();
}
reset() {
this.count = 0;
this.apply();
}
},
// Template HTML
render() {
return `
<div class="counter-widget">
<h3>Contador: ${this.count}</h3>
<div class="controls">
<button >- ${this.step}</button>
<button >+ ${this.step}</button>
</div>
<button >Reset</button>
</div>
`;
},
// Estilos CSS scoped
style: `
padding: 1rem;
border: 2px solid #9333ea;
border-radius: 0.5rem;
text-align: center;
background: white;
`,
// Tag personalizado (opcional)
tagName: "my-counter",
});
// Usar el componente
const container = document.getElementById("app");
Counter.render(container);
const TodoList = Component({
controller: class {
constructor() {
this.todos = [];
this.newTodo = "";
this.filter = "all";
}
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
id: Date.now(),
text: this.newTodo,
completed: false,
});
this.newTodo = "";
this.apply();
}
}
toggleTodo(id) {
const todo = this.todos.find((t) => t.id == id);
if (todo) {
todo.completed = !todo.completed;
this.apply();
}
}
deleteTodo(id) {
this.todos = this.todos.filter((t) => t.id != id);
this.apply();
}
get filteredTodos() {
if (this.filter === "completed") {
return this.todos.filter((t) => t.completed);
}
if (this.filter === "active") {
return this.todos.filter((t) => !t.completed);
}
return this.todos;
}
},
render() {
return `
<div class="todo-app">
<h3>Lista de Tareas</h3>
<div class="add-todo">
<input type="text" model="newTodo"
placeholder="Nueva tarea..."
>
<button >Agregar</button>
</div>
<div class="filters">
<button
class="${this.filter === "all" ? "active" : ""}">
Todas
</button>
<button
class="${this.filter === "active" ? "active" : ""}">
Activas
</button>
<button
class="${this.filter === "completed" ? "active" : ""}">
Completadas
</button>
</div>
<ul class="todo-list">
${this.filteredTodos
.map(
(todo) => `
<li class="todo-item ${todo.completed ? "completed" : ""}">
<input type="checkbox"
${todo.completed ? "checked" : ""}
pl-s1">${todo.id})">
<span>${todo.text}</span>
<button pl-s1">${todo.id})">❌</button>
</li>
`
)
.join("")}
</ul>
</div>
`;
},
style: `
padding: 1rem;
border: 2px solid #9333ea;
border-radius: 0.5rem;
background: white;
.active { background: #9333ea; color: white; }
.completed { text-decoration: line-through; opacity: 0.6; }
`,
});
Crea ventanas emergentes que no molestan, sino que encantan:
import { Modal } from "@pablotheblink/scopejs";
function openUserModal() {
Modal({
controller: class {
constructor() {
this.message = "¡Hola desde el modal!";
this.userName = "";
}
saveUser() {
if (this.userName.trim()) {
alert(`Usuario guardado: ${this.userName}`);
this.close();
}
}
},
render() {
return `
<div class="p-6">
<h3 class="text-xl font-bold mb-4">${this.message}</h3>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">
Nombre de usuario:
</label>
<input type="text" model="userName"
class="w-full px-3 py-2 border rounded"
placeholder="Escribe tu nombre">
</div>
<div class="flex gap-2">
<button
class="bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700">
Guardar
</button>
<button
class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
Cancelar
</button>
</div>
</div>
`;
},
hideWhenClickOverlay: true, // Cerrar al hacer clic fuera
});
}
Sistema de navegación que funciona como debe:
import { Router, Component } from "@pablotheblink/scopejs";
// Definir las vistas/componentes
const HomePage = Component({
controller: class {
constructor() {
this.title = "Página de Inicio";
this.message = "¡Bienvenido a ScopeJS!";
}
},
render() {
return `
<div class="p-6 text-center">
<h1 class="text-3xl font-bold text-purple-800 mb-4">
${this.title}
</h1>
<p class="text-gray-600">${this.message}</p>
<a href="/usuario/123" class="text-purple-600 hover:underline">
Ver perfil de usuario
</a>
</div>
`;
},
});
const UserProfile = Component({
controller: class {
constructor() {
this.userId = null;
this.userName = "";
}
init(params) {
this.userId = params.id;
this.userName = `Usuario ${params.id}`;
this.apply();
}
},
render() {
return `
<div class="p-6">
<h1 class="text-2xl font-bold text-purple-800 mb-4">
Perfil de ${this.userName}
</h1>
<p class="text-gray-600 mb-4">ID: ${this.userId}</p>
<button
class="bg-purple-600 text-white px-4 py-2 rounded">
Volver
</button>
</div>
`;
},
});
// Configurar el router
const AppRouter = Router([
{
path: "/",
component: HomePage,
alias: "inicio",
},
{
path: "/usuario/:id",
component: UserProfile,
alias: "perfil-usuario",
middleware: (params, next) => {
// Validar que el ID sea un número
if (isNaN(params.id)) {
alert("ID de usuario inválido");
return false;
}
next();
},
},
]);
// Renderizar el router en el DOM
const container = document.getElementById("app");
AppRouter.render(container);
// Navegación programática
AppRouter.navigate("/usuario/123");
// Escuchar cambios de ruta
AppRouter.listen((route, params) => {
console.log("Navegando a:", route, "Parámetros:", params);
});
ScopeJS no es de esos frameworks pesados que actualizan toda la página. Nosotros somos más listos: solo tocamos lo que realmente ha cambiado.
- Precisión ninja: Si cambias un texto, solo se actualiza ese texto
- Eficiencia máxima: Si cambias una clase CSS, solo se modifica esa clase
- Resultado: Animaciones fluidas y experiencia de usuario que enamora
const Counter = Component({
controller: class {
constructor() {
this.count = 0;
}
increment() {
this.count++; // Solo este valor cambia
this.apply(); // Solo se actualiza el número en pantalla
}
},
render() {
return `
<div>
<h1>Mi App Genial</h1>
<p>Contador: ${this.count}</p>
<button >+</button>
</div>
`;
},
});
Con el atributo model
, los datos se sincronizan automáticamente:
const FormExample = Component({
controller: class {
constructor() {
this.user = {
name: "",
email: "",
age: "",
};
}
onSubmit(e) {
e.preventDefault();
console.log("Usuario:", this.user);
}
},
render() {
return `
<form >
<input type="text" model="user.name" placeholder="Nombre">
<input type="email" model="user.email" placeholder="Email">
<input type="number" model="user.age" placeholder="Edad">
<p>Hola ${this.user.name}! 👋</p>
<button type="submit">Enviar</button>
</form>
`;
},
});
Manejo de eventos que funciona como esperas:
const EventExample = Component({
controller: class {
constructor() {
this.message = "";
}
// Eventos sin parámetros (recibe el evento)
onClick(event) {
console.log(event.target);
}
// Eventos con parámetros
onButtonClick(id, action, event) {
console.log(id, action, event);
this.message = `Botón ${id} - Acción: ${action}`;
this.apply();
}
},
render() {
return `
<div>
<button >Clic Simple</button>
<button >Clic con Parámetros</button>
<p>${this.message}</p>
</div>
`;
},
});
Propiedad | Tipo | Descripción |
---|---|---|
controller |
Class | Clase controladora del componente |
render |
Function | Función que retorna el HTML del componente |
style |
String | CSS scoped para el componente |
tagName |
String | Nombre del elemento personalizado |
postRender |
Function | Callback ejecutado después del render |
title |
String | Título de la página |
meta |
Array | Meta tags para SEO |
Propiedad | Tipo | Descripción |
---|---|---|
controller |
Class | Controlador del modal |
render |
Function | Función de renderizado |
hideWhenClickOverlay |
Boolean | Cerrar al hacer clic fuera |
className |
String | Clase CSS adicional |
referrer |
Element | Elemento de referencia para posición |
Método | Parámetros | Descripción |
---|---|---|
navigate |
path, body | Navegar a una ruta específica |
render |
container | Renderizar el router en un contenedor |
listen |
callback | Escuchar cambios de ruta |
unlisten |
uuid | Remover listener de cambios |
Función | Parámetros | Descripción |
---|---|---|
enableDebugger |
boolean | Activar/desactivar modo debug |
apply |
- | Re-renderizar componente |
close |
...args | Cerrar modal (disponible en modales) |
<img fadeIn src="mi-imagen-genial.jpg" />
<!-- Aparece suavemente cuando entra en el viewport -->
<img lazy src="imagen-pesada.jpg" />
<!-- Solo se carga cuando realmente se necesita -->
<!-- Page1 -->
<img id="image_1" src="foto1.jpg" />
<!-- Page2 -->
<img id="image_1" src="foto2.jpg" />
<!-- Transición automática entre páginas usando el mismo ID -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mi App con ScopeJS</title>
<!-- Cargar ScopeJS -->
<script src="https://unpkg.com/@pablotheblink/scopejs@2.0.4/js/ScopeJS.js"></script>
</head>
<body>
<div id="app">
<!-- Tu aplicación se renderizará aquí -->
</div>
<script>
// Tu código de la aplicación aquí
const App = ScopeJS.Component({
controller: class {
constructor() {
this.titulo = "Mi Primera App ScopeJS";
this.contador = 0;
}
incrementar() {
this.contador++;
this.apply();
}
},
render() {
return `
<div style="text-align: center; padding: 2rem;">
<h1>${this.titulo}</h1>
<p>Contador: ${this.contador}</p>
<button >
Incrementar
</button>
</div>
`;
},
});
// Inicializar cuando la página cargue
window.addEventListener("load", () => {
const container = document.getElementById("app");
App.render(container);
});
</script>
</body>
</html>
- Usar apply() solo cuando sea necesario - Solo después de cambios de estado
- Componentes pequeños y enfocados - Un componente = una responsabilidad
- Aprovechar lazy loading para imágenes -
<img lazy src="...">
- Usar fadeIn para animaciones -
<div fadeIn>...</div>
- Estilos scoped por componente - Evita conflictos CSS
- Evitar manipulación DOM directa - Usar el sistema reactivo
- Validar datos antes de renderizar - Especialmente arrays/objetos nulos
- No llamar apply() después de cambiar el estado
- Manipular DOM directamente en lugar de usar el sistema reactivo
- No limpiar listeners en onDestroy
- Componentes demasiado grandes - Mejor dividir en subcomponentes
- No validar datos antes de renderizar
- Usar innerHTML directamente - Mejor usar el sistema de templates
- No gestionar errores en operaciones asíncronas
- SPAs pequeñas y medianas con Router
- Dashboards interactivos con widgets reutilizables
- Formularios complejos con validación
- Aplicaciones CRUD básicas
- Prototipos rápidos y MVPs
- Landing pages dinámicas
- Componentes web independientes
- Aplicaciones de gestión simples
- Aplicaciones muy grandes (mejor React/Vue/Angular)
- Necesidades de SSR críticas (server-side rendering)
- Ecosistemas muy específicos (ej: React Native)
- Equipos grandes que necesitan tooling avanzado
- Aplicaciones con estado muy complejo (state management)
- Necesidades de testing avanzado (unit testing frameworks)
- ✅ Chrome 60+
- ✅ Firefox 55+
- ✅ Safari 11+
- ✅ Edge 79+
- ✅ Navegadores móviles modernos
- ❌ Internet Explorer (porque ya es hora de dejarlo ir)
MIT License - Básicamente puedes hacer lo que quieras, pero no nos culpes si te vuelves adicto.
Pablo Martínez - El tipo que pensó que el mundo necesitaba notificaciones más sexys.
ScopeJS v2.0.4 - El framework JavaScript que no te va a dar dolores de cabeza 😎