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
Work with KnockoutValidation
Usage without module loaders
Changes from v0.7.1
@observable
Property decorator that creates hidden ko.observable
with ES6 getter and setter for it
If initialized by Array then hidden ko.observableArray
will be created (see @observableArray)
import { observable } from "knockout-decorators";
class Model {
@observable field = 123;
@observable collection = [];
};
let model = new Model();
ko.computed(() => { console.log(model.field); });
model.field = 456;
@computed
Accessor decorator that wraps ES6 getter to hidden ko.pureComputed
Setter is not wrapped to hidden ko.pureComputed
and stays unchanged
import { observable, computed } from "knockout-decorators";
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 "
@reactive
Like @observable, but creates "deep observable" property (see example below)
If initialized by Array then hidden ko.observableArray
will be created (see @observableArray)
import { reactive } from "knockout-decorators";
class ViewModel {
@reactive deepObservable = {
firstName: "Clive Staples",
lastName: "Lewis",
array: [],
object: {
foo: "bar",
reference: null,
},
}
}
const vm = new ViewModel();
vm.deepObservable.object.reference = {
firstName: "Clive Staples",
lastName: "Lewis",
};
vm.deepObservable.array.push({
firstName: "Clive Staples",
lastName: "Lewis",
});
@observableArray
Property decorator that creates hidden ko.observableArray
with ES6 getter and setter for it
import { observableArray } from "knockout-decorators";
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(callback: (value: any[]) => void)
function from ko.subscribable
,
import { observableArray, ObservableArray } from "knockout-decorators";
class Model {
@observableArray array = [1, 2, 3] as ObservableArray<number>;
};
let model = new Model();
model.array.subscribe((changes) => { console.log(changes); }, null, "arrayChange");
model.array.push(4);
model.array.remove(val => val % 2 === 0);
- a new
mutate(callback: () => void)
function that runs callback in which we can mutate array directly,
import { observableArray, ObservableArray } from "knockout-decorators";
class Model {
@observableArray array = [1, 2, 3] as ObservableArray<number>;
};
let model = new Model();
model.array.mutate(() => {
model.array[1] = 200;
model.array[2] = 300;
});
- a new
set(i: number, value: any): any
function that sets a new value at specified index and returns the old value.
import { observableArray, ObservableArray } from "knockout-decorators";
class Model {
@observableArray array = [1, 2, 3] as ObservableArray<number>;
};
let model = new Model();
let oldValue = model.array.set(2, 300)
console.log(model.array);
console.log(oldValue);
@extend
Apply extenders to decorated @observable
, @reactive
, @observableArray
or @computed
@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.
import { observable, computed, extend } from "knockout-decorators";
class ViewModel {
rateLimit: 50;
@extend({ notify: "always" })
@observable first = "";
@extend(ViewModel.prototype.getExtender)
@observable second = "";
@extend({ rateLimit: 500 })
@computed get both() {
return this.first + " " + this.second;
}
getExtender() {
return { rateLimit: this.rateLimit };
}
}
@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.
import { component } from "knockout-decorators";
@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.
import { component } from "knockout-decorators";
@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" }
});
@autobind
Bind class method to class instance. Clone of core-decorators.js @autobind
import { observable, component, autobind } from "knockout-decorators";
@component("my-component", `
<ul data-bind="foreach: array">
<li data-bind="click: $component.remove">remove me</li>
</ul>
`)
class MyComponent {
@observable array = [1, 2, 3] as ObservableArray<number>;
@autobind
remove(item: number) {
this.array.remove(item);
}
}
@event
Create subscribable function that invokes it's subscribers when it called.
All arguments that passed to @event
function are translated to it's subscribers.
Internally uses hidden ko.subscribable
.
Subscribers can be attached by calling .subscribe()
method of EventType
type or by subscribe()
utility.
import { event, EventType } from "knockout-decorators";
class Producer {
@event myEvent: EventType;
}
class Consumer {
constructor(producer: Producer) {
producer.myEvent.subscribe((arg1, arg2) => {
console.log("lambda:", arg1, arg2);
});
const subscription = producer.myEvent.subscribe(this.onEvent);
}
@autobind
onEvent(arg1, arg2) {
console.log("method:", arg1, arg2);
}
}
const producer = new Producer();
const consumer = new Consumer(producer);
producer.myEvent(123, "test");
subscribe
Subscribe to @observable
(or @computed
) dependency with creation of hidden ko.computed()
subscribe<T>(
dependency: () => T,
callback: (value: T) => void,
options?: { once?: boolean, event?: string }
): KnockoutSubscription;
Or subscribe to some @event
property
subscribe<T1, T2, ...>(
event: (arg1: T1, arg2: T2, ...) => void,
callback: (arg1: T1, arg2: T2, ...) => void,
options?: { once?: boolean }
): KnockoutSubscription;
Argument | Default | Description |
---|
dependencyOrEvent | | (1) Function for getting observeble property (2) @event property |
callback | | Callback that handle dependency changes or @event notifications |
options | null | Options object |
options.once | false | If true then subscription will be disposed after first invocation |
optons.event | "change" | Event name for passing to Knockout native subscribe() |
Subscribe to @observable
changes
import { observable, subscribe } from "knockout-decorators";
class ViewModel {
@observable field = 123;
constructor() {
subscribe(() => this.field, (value) => {
console.log(value);
});
subscribe(() => this.field, (value) => {
console.log(value);
}, { once: true });
subscribe(() => this.field, (value) => {
console.log(value);
}, { event: "beforeChange" });
}
}
Subscribe to @event
property
import { event, subscribe } from "knockout-decorators";
class ViewModel {
@event myEvent: (arg: string) => void;
constructor() {
subscribe(this.myEvent, (arg) => {
console.log(arg);
});
subscribe(this.myEvent, (arg) => {
console.log(arg);
}, { once: true });
const subscription = subscribe(this.myEvent, (arg) => {
console.log(arg);
});
subscription.dispose();
this.myEvent("event argument")
}
}
unwrap
Get hidden ko.observable()
for property decodated by @observable
or hidden ko.pureComputed()
for property decodated by @computed
unwrap(instance: Object, key: string | symbol): any;
unwrap<T>(instance: Object, key: string | symbol): KnockoutObservable<T>;
Argument | Default | Description |
---|
instance | | Decorated class instance |
key | | Name of @observable property |
KnockoutValidation example
import { observable, extend, unwrap } from "knockout-decorators";
class MyViewModel {
@extend({ required: "MyField is required" })
@observable myField = "";
checkMyField() {
alert("MyField is valid: " + unwrap(this, "myField").isValid());
}
unwrap(key: string) {
return unwrap(this, key);
}
}
<div>
<input type="text" data-bind="value: myField"/>
<button data-bind="click: checkMyField">check</button>
<p data-bind="validationMessage: unwrap('myField')"></p>
</div>
Usage without module loaders (in global scope)
layout.html
<script src="/{path_to_vendor_scrpts}/knockout.js"></script>
<script src="/{path_to_vendor_scrpts}/knockout-decorators.js"></script>
script.ts
namespace MyTypescriptNamespace {
const { observable, computed } = KnockoutDecorators;
export class MyClass {
@observable field = "";
}
}
Breaking changes from v0.7.1
- Removed
@subscribe
decorator - Removed
@reaction
decorator - Added
subscribe(() => this.observableProp, (value) => { ... })
function - Added
unwrap(this, "observablePropName")
function
Native ko.computed
with side effects can be used in all places
where we use @reaction
decorator.
In v0.7.1 and earlier @subscribe
decorator can be used only with @observable
but not with @computed
. To avoid this restriction we can create ko.pureComputed
and subscribe to it:
class ViewModel {
@computed get computedProp() { ... }
constructor() {
ko.pureComputed(() => this.computedProp).subscribe((value) => { ... });
}
}
So from v0.8.0 instead of @subscribe
decorator there is shorthand function subscribe
with some extra functionality like "subscribe once":
class ViewModel {
@computed get computedProp() { ... }
constructor() {
subscribe(() => this.computedProp, (value) => { ... });
}
}