Contenido
Componentes
Los componentes de Carats se parecen a los de React, pero tienen diferencias importantes en cómo tratan los atributos y los eventos.
Estructura de un componente
Los componentes son archivos .tsx que exportan una función por defecto. Reciben un único objeto de props, pueden declarar defaultProps y renderizan props.children donde deben aparecer los hijos. Pueden importar cualquier archivo que Vite entienda, y así es como se adjunta el SASS a un componente.
import './_layout.sass';
export default function Layout(props: JSX.ComponentProps) {
return (
<>
<header>Hello World</header>
<main>{props.children}</main>
<footer>Carats</footer>
</>
)
}
Componente de ejemplo
import './home.sass';
export default function Home() {
return (
<div class="home">
<h1>Welcome to Carats</h1>
<p>Build amazing applications with JJSX</p>
</div>
);
}
El archivo SASS se importa por su efecto secundario, que es lo que adjunta la hoja de estilos a la página. Un componente puede importar cualquier cosa que Vite entienda: SASS, CSS, JSON o imágenes.
Props e hijos
Las props se pasan a los componentes como es de esperar, y los hijos van dentro de las etiquetas del componente. Un componente bruñido recibe en su lugar el valor de retorno de su culet como props.
export default function Card(props: { title: string; children?: JSX.Element }) {
return (
<article class="card">
<h2>{props.title}</h2>
{props.children}
</article>
);
}
// Usage
<Card title="My Card">
<p>Card content here</p>
</Card>
Props por defecto
defaultProps rellena las props que no se han proporcionado. Es la forma habitual de hacer que una página se pueda renderizar en una prueba, donde no hay props del servidor disponibles.
import { CaratsComponent } from '@carats/render';
const Profile: CaratsComponent<User> = function (user) {
return <h1>{user.name}</h1>;
};
Profile.defaultProps = {
id: '0',
name: 'Guest',
username: 'guest',
email: '',
phone: '',
website: ''
};
export default Profile;
Etiquetas head dinámicas
Los componentes pueden proporcionar etiquetas head dinámicas (title, meta) asignando a this.head dentro de la función del componente. Las etiquetas se recogen durante el renderizado en el servidor y se inyectan en el marcador <!--app-head--> de la plantilla HTML.
import { CaratsComponent } from '@carats/render';
export default function Home(this: CaratsComponent) {
this.head = <>
<title>Home Page</title>
<meta name="description" content="Welcome to my app" />
</>;
return <div>Home</div>;
}
this solo existe en un componente declarado con la palabra clave function. Una función de flecha no tiene un this propio, así que no puede asignar this.head; usa una declaración de función, o quita las etiquetas head y deja el componente como función de flecha. Un componente de flecha normal se tipa como CaratsComponent cuando se usa directamente en una ruta, y como CaratsComponentWithThis cuando hace falta la anotación para una expresión de función simple.
El tipo `CaratsComponent` debe importarse; lo exporta `@carats/render`. El parámetro `this` es solo una anotación de tipo y desaparece al compilar.
Atributos HTML
Los atributos HTML se conservan tal cual y se tratan como cadenas. A diferencia de React, no puedes pasar una función a un atributo de evento como onclick u onsubmit; usa addEventListener dentro de una llamada afterMount. Esta es la diferencia más habitual al portar código de React.
Incorrecto (no funciona)
<button onclick={() => handleClick()}>Click me</button>
La función no es un valor válido para un atributo HTML, así que acaba en el marcado como un desastre y nunca se llama a nada.
Correcto
import { afterMount } from '@carats/hooks';
export default function MyButton() {
let handler: (() => void) | undefined;
afterMount(() => {
const button = document.getElementById('my-button');
handler = () => console.log('Clicked!');
button?.addEventListener('click', handler);
return () => button?.removeEventListener('click', handler!);
});
return <button id="my-button">Click me</button>;
}
La misma regla se aplica a onsubmit, oninput, onchange y a cualquier otro atributo de evento. Dale al elemento un id y búscalo dentro de una llamada de hook.
Componentes bruñidos
Los componentes bruñidos obtienen datos de su culet automáticamente. Envuelve tu componente con la función Burnish de @carats/render para activar esta funcionalidad. El envoltorio relaciona la ruta del componente con un culet de la misma ruta, así que ambos deben registrarse con una cadena de ruta idéntica.
Con el contexto this
import { Burnish } from '@carats/render';
export default Burnish<User>(function (user) {
this.head = <title>{user.name}</title>;
return <h1>Hello {user.name}</h1>;
});
Con la opción recast
El framework guarda en caché las props del servidor más recientes de una ruta. Pasa { recast: true } para volver a obtenerlas en cada renderizado en lugar de reutilizar el valor cacheado.
export default Burnish<User>((user) => <h1>Hello {user.name}</h1>, { recast: true });
Un componente bruñido sigue aceptando props como cualquier otro componente, y eso es lo que lo hace comprobable sin servidor.
Punto de entrada del cliente
El punto de entrada monta las facetas en el navegador y luego inicia el renderizado en el cliente. clientRender() es obligatorio siempre que el enrutamiento dentro de la aplicación esté activo, y debería llamarse después de mount.
import { mount, clientRender } from '@carats/csr';
import facets from './facets.cara';
import './base.sass';
mount(facets);
clientRender();
La hoja de estilos global se importa aquí y no en una página, porque contiene los tokens de diseño y los resets base de los que depende cada página.
Configuración de Vite
La configuración de Vite para el cliente compila desde src/client y emite en dist/client con un manifiesto, lo que permite al servidor referenciar el archivo de entrada con su hash.
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
publicDir: path.resolve(import.meta.dirname, 'public'),
root: path.resolve(import.meta.dirname, 'src/client'),
base: '/',
appType: 'custom',
server: {
middlewareMode: true
},
build: {
outDir: path.resolve(import.meta.dirname, 'dist/client'),
emptyOutDir: true,
manifest: true,
minify: true,
rollupOptions: {
treeshake: true
}
}
})