Skip to content

Commit

Permalink
WIP: Class authoring format.
Browse files Browse the repository at this point in the history
  • Loading branch information
dgp1130 committed Dec 19, 2023
1 parent 81463e8 commit 47e3b4b
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 36 deletions.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@web/test-runner-puppeteer": "^0.14.2",
"http-server": "^14.1.1",
"jasmine-core": "^4.5.0",
"typescript": "^4.9.4"
"typescript": "^5.3.3"
},
"files": [
"/*.js",
Expand Down
38 changes: 38 additions & 0 deletions src/component-class.ts
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;
}
81 changes: 57 additions & 24 deletions src/demo/auto-counter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
import { component } from 'hydroactive';
import { cached, signal } from 'hydroactive/signals.js';

/** 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()}`);
});
});
import { Component } from 'hydroactive';
import { WriteableSignal, property, signal } from 'hydroactive/signals.js';

// Need to define an interface.
interface State {
readonly count: WriteableSignal<number>;
}

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;
}
}
1 change: 1 addition & 0 deletions src/index.ts
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';
1 change: 1 addition & 0 deletions src/signals/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { cached } from './cached.js';
export { effect } from './effect.js';
export { property } from './property.js';
export { Equals, signal } from './signal.js';
export { MacrotaskScheduler } from './schedulers/macrotask-scheduler.js';
export { Action, CancelAction, Scheduler } from './schedulers/scheduler.js';
Expand Down
28 changes: 28 additions & 0 deletions src/signals/property.ts
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);
},
};
};
}
6 changes: 3 additions & 3 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
Expand All @@ -27,7 +27,7 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

/* Modules */
"module": "es2020", /* Specify what module code is generated. */
"module": "Node16", /* Specify what module code is generated. */
"rootDir": "src/", /* Specify the root folder within your source files. */
"moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
Expand Down

0 comments on commit 47e3b4b

Please sign in to comment.