-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
135 additions
and
52 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { ComponentRef } from './component-ref.js'; | ||
import { ElementRef } from './element-ref.js'; | ||
import { HydroActiveComponent } from './hydroactive-component.js'; | ||
import { Signal } from './signals.js'; | ||
|
||
/** TODO */ | ||
export abstract class Component<State extends {} = {}> | ||
extends HydroActiveComponent { | ||
protected readonly ref = ElementRef.from(this); | ||
protected readonly comp = ComponentRef._from(this.ref); | ||
|
||
#state: State | undefined; | ||
protected get state(): State { | ||
if (!this.#doneHydrating) { | ||
throw new Error('`state` is not accessible during hydration.'); | ||
} | ||
|
||
return this.#state!; | ||
} | ||
|
||
protected get props(): Record<string, Signal<unknown>> { | ||
return new Proxy(this, { | ||
get(target: Component<State>, prop: string | symbol): Signal<unknown> { | ||
const propName = typeof prop === 'symbol' ? prop.description : prop; | ||
const internalProp = `_${propName}`; | ||
return (target as any)[internalProp]; | ||
} | ||
}) as unknown as Record<string, Signal<unknown>>; | ||
} | ||
|
||
#doneHydrating = false; | ||
protected override hydrate(): void { | ||
this.#state = this.onHydrate() ?? ({} as State); | ||
this.#doneHydrating = true; | ||
} | ||
|
||
protected abstract onHydrate(): State | undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,57 @@ | ||
import { component } from 'hydroactive'; | ||
import { cached, signal } from 'hydroactive/signals.js'; | ||
|
||
const Serializer = Symbol('HydroActiveSerializer'); | ||
class User { | ||
public constructor(private name: string) {} | ||
|
||
static [toSerializer](): Serializer<User> { | ||
return { | ||
serialize(user: User): string { | ||
return JSON.stringify({ name: user.name }); | ||
}, | ||
|
||
deserialize(serial: string): User { | ||
const { name } = JSON.parse(serial); | ||
return new User(name); | ||
}, | ||
}; | ||
}; | ||
import { Component } from 'hydroactive'; | ||
import { WriteableSignal, property, signal } from 'hydroactive/signals.js'; | ||
|
||
// Need to define an interface. | ||
interface State { | ||
readonly count: WriteableSignal<number>; | ||
} | ||
|
||
/** Automatically increments the count over time. */ | ||
export const AutoCounter = component('auto-counter', (comp) => { | ||
const label = comp.host.query('span'); | ||
const count = signal(Number(label.text)); | ||
const doubleCount = cached(() => count() * 2); | ||
|
||
comp.connected(() => { | ||
const id = setInterval(() => { | ||
count.set(count() + 1); | ||
}, 1_000); | ||
|
||
return () => { | ||
clearInterval(id); | ||
}; | ||
}); | ||
|
||
comp.effect(() => { | ||
label.native.textContent = count().toString(); | ||
console.log(`Double count: ${doubleCount()}`); | ||
}); | ||
}); | ||
class AutoCounter extends Component<State> { | ||
// Need to define the property twice to get an internal and external type. | ||
@property() | ||
public accessor incrementBy = 2; | ||
|
||
protected override onHydrate() { | ||
const label = this.ref.query('span')!; | ||
const count = signal(Number(label.text)); | ||
|
||
this.comp.connected(() => { | ||
const id = setInterval(() => { | ||
// Too easy to accidentally use `this.incrementBy` and get a | ||
// non-reactive value. | ||
|
||
// No way to type `this.props` based on `@property` accessors? | ||
count.set(count() + (this.props['incrementBy']!() as number)); | ||
}, 1_000); | ||
return () => clearInterval(id); | ||
}); | ||
|
||
this.comp.effect(() => { | ||
label.native.textContent = count().toString(); | ||
}); | ||
|
||
// Can't use `this.state` here or in any called function. | ||
// `this.state.count()` -> Error. | ||
// At least can do a decent error. | ||
|
||
// Not clear that this is the initial value of `this.state`. | ||
return { count }; | ||
} | ||
|
||
// Have to prefix `this.state` everywhere except the `hydrate` function. | ||
public increment(): void { | ||
this.state.count.set(this.state.count() + 1); | ||
} | ||
|
||
public decrement(): void { | ||
this.state.count.set(this.state.count() - 1); | ||
} | ||
} | ||
|
||
customElements.define('auto-counter', AutoCounter); | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
'auto-counter': AutoCounter; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { HydrateLifecycle, component } from './component.js'; | ||
export { Component } from './component-class.js'; | ||
export { ComponentRef, OnDisconnect, OnConnect } from './component-ref.js'; | ||
export { ElementRef } from './element-ref.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Component } from 'hydroactive'; | ||
import { Signal, WriteableSignal } from './types.js'; | ||
import { signal } from './signal.js'; | ||
|
||
type SignalPropertyDecorator<Comp extends Component, Value> = ( | ||
target: ClassAccessorDecoratorTarget<Comp, Value>, | ||
context: ClassAccessorDecoratorContext<Comp, Value>, | ||
) => ClassAccessorDecoratorResult<Comp, Value> | void; | ||
|
||
export function property<Comp extends Component>(): SignalPropertyDecorator<Comp, any> { | ||
return (_target, ctx) => { | ||
const propName = typeof ctx.name === 'symbol' ? ctx.name.description : ctx.name; | ||
const internalProp = `_${propName}`; | ||
return { | ||
init(value): void { | ||
(this as any)[internalProp] = signal(value); | ||
}, | ||
|
||
get(): unknown { | ||
return ((this as any)[internalProp] as Signal<unknown>)(); | ||
}, | ||
|
||
set(value: unknown): void { | ||
((this as any)[internalProp] as WriteableSignal<unknown>).set(value); | ||
}, | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters