Knockout Decorators
Decorators for use Knockout JS in TypeScript and ESNext environments
Example
import { observable, computed, component } from "knockout-decorators";
@component("person-view", `
<div>Name: <span data-bind="text: fullName"></span></div>
<div>Age: <span data-bind="text: age"></span></div>
`)
class PersonView {
@observable firstName: string;
@observable lastName: string;
@observable age: string;
@computed get fullName() {
return this.firstName + " " + this.lastName;
}
constructor({ firstName, lastName, age }, element, templateNodes) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
Documentation
@observable
Property decorator that creates hidden ko.observable
with ES6 getter and setter for it
class Model {
@observable field = 123;
};
let model = new Model();
ko.computed(() => { console.log(model.field); });
model.field = 456;
@observableArray
Property decorator that creates hidden ko.observableArray
with ES6 getter and setter for it
class Model {
@observableArray array = [1, 2, 3];
};
let model = new Model();
ko.computed(() => { console.log(model.field); });
model.field = [4, 5, 6];
Functions from ko.observableArray
(both Knockout-specific remove
, removeAll
, destroy
, destroyAll
, replace
and redefined Array.prototype
functions pop
, push
, reverse
, shift
, sort
, splice
, unshift
)
are also presents in decorated poperty.
They works like if we invoke them on hidden ko.observableArray
.
And also decorated array has a subscribe
function from ko.subscribable
class Model {
@observableArray array = [1, 2, 3];
};
let model = new Model();
model.array.subscribe((changes) => { console.log(changes); }, null, "arrayChange");
model.array.push(4);
model.array.remove(val => val % 2 === 0);
@computed
Accessor decorator that wraps ES6 getter and setter (if defined) to hidden ko.pureComputed
class Person {
@observable firstName = "";
@observable lastName = "";
@computed
get fullName() { return this.firstName + " " + this.lastName; }
set fullName(value) { [this.firstName, this.lastName] = value.trim().split(/\s+/g); }
}
let person = new Person();
ko.pureComputed(() => person.fullName).subscribe(console.log.bind(console));
person.fullName = " John Smith "
@observer
Replace original method with factory that produces ko.computed
from original method
@observer
@observer(autoDispose: boolean)
Argument | Default | Description |
---|
autoDispose | true | if true then computed will be disposed when entire decorated class is disposed |
Method that decorated with @observer
evaluates once when explicitely invoked (this call creates hidden ko.computed
)
and every times when it's observable (or computed) dependencies are changed.
Hidden ko.computed
will be disposed when entire decorated class is disposed (if we don't set autoDispose
to false
)
class BlogPage {
@observable postId = 0;
@observable pageData: any;
constructor(blogId: ko.Observable<number>) {
const computed = this.onRoute(blogId);
}
@observer async onRoute(blogId: ko.Observable<number>) {
const resp = await fetch(`/blog/${ blogId() }/post/${ this.postId }`);
this.pageData = await resp.json();
}
}
@extend
Apply extenders to decorated @observable
@extend(extenders: Object)
@extend(extendersFactory: () => Object)
Extenders can be defined by plain object or by calling method, that returns extenders-object.
Note that extendersFactory
invoked with ViewModel instance as this
argument.
class ViewModel {
rateLimit: 50;
@extend({ notify: "always" })
@observable first = "";
@extend(ViewModel.prototype.getExtender)
@observable second = "";
getExtender() {
return { rateLimit: this.rateLimit };
}
}
@subscribe
Subscribe to @observable
by name or by specifying callback explicitely
@subscribe(callback: (value: any) => void, event?: string, autoDispose?: boolean)
@subscribe(targetOrCallback: string | symbol, event?: string, autoDispose?: boolean)
Argument | Default | Description |
---|
callback | | Subscription handler method |
targetOrCallback | | Name of subscription handler method or name of @observable property |
event | null | Knockout subscription event |
autoDispose | true | if true then computed will be disposed when entire decorated class is disposed |
We can define name of handler when we decorate @observable
or define name of @observable
when decorate handler.
Subscriptions will be disposed when entire decorated class is disposed (if we don't set autoDispose
to false
)
class ViewModel {
@subscribe("onFirstChanged")
@observable first = "";
onFirstChanged(value) {}
@observable second = "";
@subscribe("second")
onSecondChanged(value) {}
}
Also whe can pass subscription handler directly.
Then it will be invoked with ViewModel instance as this
argument.
And we can specify subscription event
class ViewModel {
operationLog = [];
@subscribe(ViewModel.prototype.onArrayChange, "arrayChange")
@observableArray array = [1, 2, 3]
onArrayChange(changes) {
this.operationLog.push(...changes);
}
}
@component
Shorthand for registering Knockout component by decorating ViewModel class
@component(name: string, options?: Object);
@component(name: string, template: any, options?: Object);
@component(name: string, template: any, styles: any, options?: Object);
Argument | Default | Description |
---|
name | | Name of component |
template | "<!---->" | Knockout template definition |
styles | | Ignored parameter (used for require() styles by webpack etc.) |
options | { synchronous: true } | Another options that passed directly to ko.components.register() |
By default components registered with synchronous
flag.
It can be overwritten by passing { synchronous: false }
as options.
If template is not specified then it will be replaced by HTML comment <!---->
If ViewModel constructor accepts zero or one arguments,
then it will be registered as viewModel:
in config object.
@component("my-component")
class Component {
constructor(params: any) {}
}
ko.components.register("my-component", {
viewModel: Component,
template: "<!---->",
synchronous: true,
});
If ViewModel constructor accepts two or three arguments,
then createViewModel:
factory is created
and { element, templateNodes }
are passed as arguments to ViewModel constructor.
@component("my-component",
require("./my-component.html"),
require("./my-component.css"), {
synchronous: false,
additionalData: { foo: "bar" }
})
class Component {
constructor(
private params: any,
private element: Node,
private templateNodes: Node[]
) {}
}
ko.components.register("my-component", {
viewModel: {
createViewModel(params, { element, templateNodes }) {
return new Component(params, element, templateNodes);
}
},
template: require("./my-component.html"),
synchronous: false,
additionalData: { foo: "bar" }
});