craftMethod
Wraps a generator so it can be called like an ordinary method — from a template, an event handler, anywhere outside the craft driver — while still resolving its dependencies with yield*.
Use it when a click handler or a component method needs a service. Not inside a craft factory — there, yield* works directly.
Import
import { craftMethod } from '@craft-ng/core';Overview
craftMethod is designed for component methods such as click handlers, submit handlers, and small UI orchestration callbacks.
The returned method carries the yieldable-method contract. When it is consumed from a Craft component template, its template view can delegate it with yield*; the component renderer drives the callback with the Craft generator runtime while preserving the method's injector and wrappers.
The method runs inside the injection context captured when craftMethod(...) is created.
That makes it useful when a component method needs to:
- call Browser Boundaries with
yield* - compose crafted services through
yield* SomeService() - keep the handler colocated with component-local signals
All dependencies are cached, which helps to detect missing providers at compile time.
Signatures
function craftMethod<Name extends string, This, Args extends unknown[], Result>(
name: Name,
factory: (this: This, ...args: Args) => Generator<unknown, Result, unknown>,
): (this: This, ...args: Args) => Result;
function craftMethod<Name extends string, This, Args extends unknown[], Result>(
name: Name,
self: This,
factory: (this: This, ...args: Args) => Generator<unknown, Result, unknown>,
): (...args: Args) => Result;The first argument is the host name: it is required and must match the property (or variable) the method is assigned to. It is the value used to tag the injector context — same role as provideHostName(...). The craft-ng/craft-method-name-match ESLint rule enforces the match and offers a quick fix.
The common case — inside a Craft component
In a Craft component's logic factory there is no this: declare the method with craftMethod(name, fn) and return it in the context.
import { button, craftComponent, div, p } from '@craft-ng/component';
import { Console, craftMethod, state } from '@craft-ng/core';
export const Counter = craftComponent(
'Counter',
{},
function* () {
const counter = yield* state('counter', 0, ({ update }) => ({ update }));
const increment = craftMethod('increment', function* (step = 1) {
yield* Console.log('increment is called');
counter.update((value) => value + step);
});
return { counter, increment };
},
({ counter, increment }) => [
p(counter),
button({ click: () => void increment() }, 'Increment'),
],
);The generator closes over counter, so nothing needs binding. When the method is consumed from a Craft template, the renderer drives it with the Craft generator runtime while preserving its injector and wrappers.
Composing crafted services
craftMethod is not limited to Browser Boundaries — it consumes the same crafted service graph as craftService:
const increment = craftMethod('increment', function* (value: number) {
return yield* CounterWorker.set(value);
});Angular class components — capturing this
In an Angular @Component class the generator usually needs the instance, which is what the two this-aware overloads are for.
Recommended form — capture this
Use craftMethod(name, this, fn) when the generator needs component state.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Console, craftMethod } from '@craft-ng/core';
@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>{{ counter() }}</p>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
readonly counter = signal(0);
readonly increment = craftMethod('increment', this, function* (step = 1) {
yield* Console.log('increment is called');
this.counter.update((value) => value + step);
});
}This overload captures the instance once, so the callback still works after extraction:
const increment = component.increment;
increment();Receiver-based form
Use craftMethod(name, fn) when you want the exact (click)="increment()" shape and are fine with the receiver-dependent behavior.
In strict TypeScript, annotate this explicitly inside the generator:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Console, craftMethod } from '@craft-ng/core';
@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<button (click)="increment(2)">Increment</button>`,
})
export class CounterComponent {
readonly counter = signal(0);
readonly increment = craftMethod(
'increment',
function* (this: CounterComponent, step = 1) {
yield* Console.log('increment is called');
this.counter.update((value) => value + step);
return this.counter();
},
);
}Composing services from a class
@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<button (click)="increment()">Increment</button>`,
})
export class CounterComponent {
readonly increment = craftMethod(
'increment',
this,
function* (value: number) {
return yield* CounterWorker.set(value);
},
);
}Caveats
craftMethod(...)must be created inside an injection context, typically during component instantiation.- The first argument is a required host name; it must match the property or variable name. The
craft-ng/craft-method-name-matchESLint rule enforces this and provides a quick fix. craftMethod(name, fn)depends on the receiver used at call time. If you extract the callback,thisis no longer guaranteed unless you bind it yourself.craftMethod(name, this, fn)is the recommended form whenever the generator reads or writesthis.onAppStart(...)is not supported insidecraftMethod.