@preact-mvc/core
v0.1.5
Published
Angular-inspired modules/DI/decorators + class-based JSX views rendered by Preact + signal reactivity
Readme
@preact-mvc/core
Angular-inspired architecture rendered by Preact: @Module/@Component
decorators, hierarchical dependency injection, class-based View
components, controllers, an injector-scoped router, and signal reactivity
via @preact/signals — all over plain JSX, with no template compiler.
Install
npm install @preact-mvc/core preact @preact/signalsPoint JSX at Preact (tsconfig.json):
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"experimentalDecorators": true,
"useDefineForClassFields": false
}
}A view is a class
View subclasses are constructed once per mount inside their own injector
scope — inject() works in field initializers and constructors, render()
stays hook-free, and signals drive re-rendering:
import { Component, View, inject, signal, computed } from '@preact-mvc/core';
@Component()
export class CounterView extends View {
count = signal(0);
doubled = computed(() => this.count.value * 2);
render() {
return (
<section>
<button onClick={() => (this.count.value -= 1)}>−</button>
<span>{this.count.value}</span>
<button onClick={() => (this.count.value += 1)}>+</button>
<p>doubled: {this.doubled.value}</p>
</section>
);
}
}Modules are the app map
import { Module, RouterModule } from '@preact-mvc/core';
import { AppShell } from './app.shell';
import { routes } from './app.routes';
@Module({
imports: [RouterModule.forRoot(routes)],
providers: [{ provide: API_URL, useValue: '/api' }],
bootstrap: [AppShell],
})
export class AppModule {}// main.tsx
import { bootstrap } from '@preact-mvc/core';
import { AppModule } from './app/app.module';
bootstrap(AppModule, document.getElementById('app')!);components/bootstrap declarations are enforced ownership: a declared
view may only mount where its module's injector is an ancestor.
Undeclared views mount anywhere — structure is opt-in per declaration.
DI, controllers, router
@Injectable/@Controllerclasses resolve through the element-injector hierarchy — view providers → module providers → root, closest scope wins.OnInit/OnDestroylifecycle plusDestroyRefmake teardown declaration-driven instead ofuseEffectcleanup.RouterModule.forRoot(routes)+<RouterOutlet/>+<Link/>give you nested routes whosemoduleboundaries get their own child injectors.
Published builds ship compiled dist/ (.js + .d.ts).
