npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

knockout-decorators

v2.0.0

Published

Decorators for use Knockout JS in TypeScript and ESNext environments

Downloads

932

Readme

Knockout Decorators

Decorators for use Knockout JS in TypeScript and ESNext environments

Build Status Coverage Status GitHub license npm version bundle size

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

Change Log

@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)

@observable(options: { deep?: boolean, expose?: boolean });
@observable;

By default, shallow observable will be created

import { observable } from "knockout-decorators";

class Model {
  @observable field = 123;
  @observable collection = [];
};
let model = new Model();

ko.computed(() => { console.log(model.field); }); // [console] ➜ 123
model.field = 456;                                // [console] ➜ 456

If { deep: true } option is provided then all nested object properties are recursively converted to @observable

import { observable } from "knockout-decorators";

class ViewModel {
  @observable({ deep: true })
  deepObservable = {            // like @observable
    firstName: "Clive Staples", // like @observable
    lastName: "Lewis",          // like @observable

    array: [],                  // like @observableArray

    object: {                   // like @observable
      foo: "bar",               // like @observable
      reference: null,          // like @observable
    },
  }
}

const vm = new ViewModel();

vm.deepObservable.object.reference = {
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
};

vm.deepObservable.array.push({
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
});

If { expose: true } option is provided then hidden ko.observable will be exposed as non-enumerable property with same name prefixed by _.

import { observable } from "knockout-decorators";

class Model {
  @observable({ expose: true })
  field = 123;
};

const model = new Model();
const hiddenObservable = model._field; // ko.observable

@computed

Accessor decorator that wraps ES6 getter to hidden ko.computed or ko.pureComputed

@computed(options: { pure: boolean });
@computed;

By default it creates 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);
  }

  @computed({ pure: false })
  get initials() {
    return this.firstName.substr(0, 1) + "." + this.LastName.substr(0, 1)+ ".";
  }
}
let person = new Person();

ko.pureComputed(() => person.fullName).subscribe(console.log.bind(console));

person.fullName = "  John  Smith  " // [console] ➜ "John Smith"

@observableArray

Property decorator that creates hidden ko.observableArray with ES6 getter and setter for it

@observableArray(options: { deep?: boolean, expose?: boolean });
@observableArray;

By default, shallow observableArray will be created

import { observableArray } from "knockout-decorators";

class Model {
  @observableArray array = [1, 2, 3];
};
let model = new Model();

ko.computed(() => { console.log(model.field); }); // [console] ➜ [1, 2, 3]
model.field = [4, 5, 6];                          // [console] ➜ [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 property. 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);                      // [console] ➜  [{ status: 'added', value: 4, index: 3 }]
model.array.remove(val => val % 2 === 0); // [console] ➜  [{ status: 'deleted', value: 2, index: 1 },
                                          //                { status: 'deleted', value: 4, index: 3 }]
  • 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; // this changes are observed
  model.array[2] = 300; // when mutation callback stops execution
});
  • 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) // this change is observed

console.log(model.array); // [console] ➜ [1, 2, 300]
console.log(oldValue);    // [console] ➜ 3

@extend

Apply extenders to decorated @observable, @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 };
  }
}

Caveats

@extend({ notify: "always" }) will not work with: subscribe() function.
Instead we can use unwrap(). But other extenders should work.

const vm = new ViewModel();
// this subscription will run only when `vm.first` actually changed
subscribe(() => vm.first, (val) => { console.log(val); });
// use `unwrap()` function to get RAW ko.observable()
unwrap(vm, "first").subscribe((val) => { console.log(val); });

@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) {}
}
// ▼▼▼ results to ▼▼▼
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" } // consider non-standard field
})
class Component {
    constructor(
        private params: any,
        private element: Node,
        private templateNodes: Node[]
    ) {}
}
// ▼▼▼ results to ▼▼▼
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" } // consider non-standard field
});

@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);
    });
    
    // `subscription` type is `ko.Subscription`
    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);

// emit @event
producer.myEvent(123, "test");
// [console] ➜ lambda:  123  "test"
// [console] ➜ method:  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 }
): ko.Subscription;

Or subscribe to some @event property

subscribe<T1, T2, ...>(
  event: (arg1: T1, arg2: T2, ...) => void,
  callback: (arg1: T1, arg2: T2, ...) => void,
  options?: { once?: boolean }
): ko.Subscription;

| Argument | Default | Description | |:------------------|:-----------|:--------------------------------------------------------------------| | dependencyOrEvent | | (1) Function for getting observable 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 | | options.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); // TypeScript detects that `value` type is `number`
    });

    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); // TypeScript detects that `arg` type is `string`
    });
    
    subscribe(this.myEvent, (arg) => {
      console.log(arg);
    }, { once: true });
    
    // `subscription` type is `ko.Subscription`
    const subscription = subscribe(this.myEvent, (arg) => {
      console.log(arg);
    });
    
    // unsubscribe from @event
    subscription.dispose();
    
    // emit @event
    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): ko.Observable<T>;

| Argument | Default | Description | |:---------|:--------|:-------------------------------| | instance | | Decorated class instance | | key | | Name of @observable property |

KnockoutValidation example

Using { expose: true }:

import { observable, extend } from "knockout-decorators";

class MyViewModel {
  @extend({ required: "MyField is required" })
  @observable({ expose: true })
  myField = "";

  checkMyField() {
    alert("MyField is valid: " + this._myField.isValid());
  }
}
<div>
  <input type="text" data-bind="value: myField"/>
  <button data-bind="click: checkMyField">check</button>
  <p data-bind="validationMessage: _myField"></p>
</div>

Using unwrap():

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());
  }

  // pass `unwrap` function to data-bindings
  unwrap(key: string) {
    return unwrap(this, key);
  }
  
  // from TypeScript 2.1 you can use keyof
  // to restrict to keys of the given type
  unwrap(key: keyof MyViewModel){
    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>

Disposable() mixin

Mixin that injects to class shorthands for utility functions and provides automatic disposing of created subscriptions (see MDN or TypeScript 2.2 docs)

function Disposable(Base? /* optional */) {
  return class extends Base {
    subscribe(...): ko.Subscription;
    dispose(): void;
    unwrap(propName: string): ko.Observable;
  }
}
  • Disposable.subscribe(...) Shorthand for subscribe() utility function that also store created subscription in hidden class property.
  • Disposable.dispose() Automatically dispose all subscriptions created by Disposable.subscribe(...) method.
  • Disposable.unwrap() Shorthand for unwrap() utility function that returns hidden Knockout observable for decorated class property.
import { observable, computed, Disposable } from "knockout-decorators";

class Derived extends Disposable(Base) {
  @observable text = "";
  
  @computed({ pure: false })
  get upperCase() {
    return this.text.toUpperCase();
  }
  
  constructor() {
    super();
    // subscribe to computed changes
    // and store created subscription in hidden class property 
    this.subscribe(() => this.upperCase, (value) => {
      console.log(value);
    });
  }
  
  dispose() {
    // dispose all subscriptions that created by this.subscribe()
    super.dispose();
    // unwrap and dispose hidden Knockout computed
    this.unwrap("upperCase").dispose();
  }
}

// Base class is optional
class Component extends Disposable() { }

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 {
  // import from TypeScript namespace (JavaScript global variable)
  const { observable, computed } = KnockoutDecorators; 
  
  export class MyClass {
    @observable field = "";
  }
}