4

I'm using knockout 3.x with Typescript and I want to make a strongly typed collection class that inherits from KnockoutObservableArray<T>, so that my type can BE an observable array but also have its own methods. So I tried this:

export class Emails implements KnockoutObservableArray<Email>
{
    //add a new email to the list
    addItem() : void
    {
        var email = new ContactEmail();
        ...
        this.push(email);
    };
}

But this gives me the following error:

Class Emails declares interface KnockoutObservableArray<Email> but does not implement it:
Type 'Emails' is missing property 'extend' from type 'KnockoutObservableArray<Email>'.

If I try to add the extend method, then it requires that I implement the peek method, and so on, suggesting that I need to implement all of KnockoutObservableArray. But I don't want to reimplement it, I want to extend it, and since KnockoutObservableArray is an interface and not a class, I don't see any way to do that. Is there a way?

Joshua Frank
  • 13,120
  • 11
  • 46
  • 95
  • The typical way to add methods to observable arrays would be to add to the `ko.observableArray.fn` object. I'm not sure if and how typescript has a way to utilize that. – Jeff Mercado Dec 01 '14 at 22:55
  • Is that for adding to all observable arrays, or just one instance? I want to make various typed collection classes, each of which would have its *own* methods, so I want only those instances to have the methods, not *all* arrays. – Joshua Frank Dec 02 '14 at 15:00

4 Answers4

2

In the not too distant past, this was something that used to plague me too.

Why?

Quite simply, for 2 reasons.

The first reason was I didn't really understand JavaScript from an object point of view. To me it had always been a "Scripting Language" and that was it, I'd heard people tell me otherwise, but I was (still am) a C# person, so I largely ignored what was said because it didn't look like C# objects.

All that changed, when I started to use Type Script, and TS showed me in the form of it's JS output what object JS looked like.

The second reason was the very flexibility of JS and more importantly knockout.

I loved the idea of simply just changing my JSON endpoint in C# code, and not really having to make any changes in the front end other than a few tweaks to my HTML.

If I wanted to add a new field to an output object, then I could, followed by simply adding a new column to a table, binding the correct field name and job done.

Fantastic right.

Indeed it was, until I started getting asked to provide row based functionality. It started simple with requests like being able to delete rows and do things like inline editing.

That lead to many strange adventures such as this:

Knockout JS + Bootstrap + Icons + html binding

and this:

Synchronizing a button class with an option control using knockoutjs

But the requests got stranger and more complex, and I started coming out with ever crazy ways of traversing the DOM, to get from the row I was on, up to the parent and back down to my sibling, then yanking the text from adjacent elements and other craziness.

Then I saw the light

I started working on a project with a junior dev who only ever knew and/or lived in JS land, and he simply asked me one question.

"If it's all causing you so much pain, then why don't you just make a view model for your rows?"

And so the following code was born.

var DirectoryEntryViewModel = (function ()
{
  function DirectoryEntryViewModel(inputRecord, parent)
  {
    this.Pkid = ko.observable(0);
    this.Name = ko.observable('');
    this.TelephoneNumber = ko.observable('');
    this.myParent = parent;

    ko.mapping.fromJS(inputRecord, {}, this);

  }

  DirectoryEntryViewModel.prototype.SomePublicFunction = function ()
  {
    // This is a function that will be available on every row in the array
    // You can call public functions in the base VM using "myParent.parentFunc(blah)"
    // you can pass this row to that function in the parent using  "myParent.someFunc(this)"
    // and the parent can simply do "this.array.pop(passedItem)" or simillar
  }

  return DirectoryEntryViewModel;
})();

var IndexViewModel = (function ()
{
  function IndexViewModel(targetElement)
  {
    this.loadComplete = ko.observable(false);
    this.phoneDirectory = ko.observableArray([]);
    this.showAlert = ko.computed(function ()
    {
      if (!this.loadComplete())
        return false;
      if (this.phoneDirectory().length < 1)
      {
        return true;
      }
      return false;
    }, this);

    this.showTable = ko.computed(function ()
    {
      if (!this.loadComplete())
        return false;
      if (this.phoneDirectory().length > 0)
      {
        return true;
      }
      return false;
    }, this);

    ko.applyBindings(this, targetElement);
    $.support.cors = true;
  }

  IndexViewModel.prototype.Load = function ()
  {
    var _this = this;
    $.getJSON("/module3/directory", function (data)
    {
      if (data.length > 0)
      {
        _this.phoneDirectory(ko.utils.arrayMap(data, function (item)
        {
          return new DirectoryEntryViewModel(item, _this);
        }));
      } else
      {
        _this.phoneDirectory([]);
      }
      _this.loadComplete(true);
    });
  };

  return IndexViewModel;
})();

window.onload = function ()
{
  var pageView = document.getElementById('directoryList');
  myIndexViewModel = new IndexViewModel(pageView);
  myIndexViewModel.Load();
};

Now this is not by any stretch of the imagination the best example (I've just yanked it out of a project I had to hand), but it works.

Yes, you have to make sure that if you add a field to your backend in the JSON, that you also add it to the row view model (Which you can load with RequireJS or similar) , as well as adding that column to your table/list/other markup where needed.

But for the sake of an extra line or two, you get to add a function once, easily, that then is available on every row in the collection.

And, just on a note of typescript, the entire JS above was generated by the TS compiler, from a TS source that implements the same pattern.

I have quite a few things running in this manner (There are a couple of examples on my github page - http://github.com/shawty you can clone), some of the apps I have running like this have entire view-models that adapt an entire UI based on a single simple change in a computed observable, I have rows that manage their own state (Including talking directly to the database) then update their state in the parent table once an operation has been successful.

Hopefully that will give you some more food for thought. While you can probably wrestle on down the road your taking, trying to extend the existing KO classes, I think in all honesty you'll find the above pattern much easier to get to grips with.

I tried the same approach as you once over, but I abandoned it fairly quickly once my JS friend pointed out how easy it was to just create a VM for the row and re-use it.

Hope it helps Shawty

Community
  • 1
  • 1
shawty
  • 5,729
  • 2
  • 37
  • 71
  • I **DO** have a view model for each row. What I don't have is a view model for the **collection** of rows. That collection has to be a knockout observable array, but I want it to have custom methods *as well*. The way to do that in .NET is inheritance, and I'm trying to find a way to represent it in Typescript, given that it doesn't appear to be possible to inherit directly. – Joshua Frank Dec 03 '14 at 18:43
  • I have the VM for the collection too, as you can see in the code. I have the TS code that generated it somewhere too. – shawty Dec 03 '14 at 21:50
2

What you're actually looking for is a class that extends KnockoutObservableArray instead of a class that implements it.

Extending however requires an implementation instead of an interface and this implementation is not available in Typescript. There's only a static interface KnockoutObservableArrayStatic which returns an implementation, exactly resembling knockout's ko.observableArray implementation.

I solved my similar case with favoring composition over inheritance. For example:

export class Emails
{
    collection = ko.observableArray<Email>();

    //add a new email to the list
    addItem() : void
    {
        var email = new ContactEmail();
        ...
        this.collection.push(email);
    };
}

You can access the collection property for binding to the DOM or if you need any functionality of the observableArray:

<div data-bind="foreach: myEmails.collection">
...
</div>
Community
  • 1
  • 1
Philip Bijker
  • 4,955
  • 2
  • 36
  • 44
1

The implements keyword is used to state that you will implement the supplied interface... to satisfy this condition, you'll need to implement all of the methods defined in the following interfaces (because you need to follow the whole chain from KnockoutObservableArray<T>.

Alternatively, create a smaller interface to represent what you actually need from your custom class, and if it is a sub-set of all of this, you will be able to use your interface for your custom class as well as for normal KnockoutObservableArray<T> classes (which will pass the interface test structurally).

interface KnockoutObservableArray<T> extends KnockoutObservable<T[]>, KnockoutObservableArrayFunctions<T> {
    extend(requestedExtenders: { [key: string]: any; }): KnockoutObservableArray<T>;
}

interface KnockoutObservable<T> extends KnockoutSubscribable<T>, KnockoutObservableFunctions<T> {
    (): T;
    (value: T): void;

    peek(): T;
    valueHasMutated?:{(): void;};
    valueWillMutate?:{(): void;};
    extend(requestedExtenders: { [key: string]: any; }): KnockoutObservable<T>;
}

interface KnockoutObservableArrayFunctions<T> {
    // General Array functions
    indexOf(searchElement: T, fromIndex?: number): number;
    slice(start: number, end?: number): T[];
    splice(start: number): T[];
    splice(start: number, deleteCount: number, ...items: T[]): T[];
    pop(): T;
    push(...items: T[]): void;
    shift(): T;
    unshift(...items: T[]): number;
    reverse(): T[];
    sort(): void;
    sort(compareFunction: (left: T, right: T) => number): void;

    // Ko specific
    replace(oldItem: T, newItem: T): void;

    remove(item: T): T[];
    remove(removeFunction: (item: T) => boolean): T[];
    removeAll(items: T[]): T[];
    removeAll(): T[];

    destroy(item: T): void;
    destroy(destroyFunction: (item: T) => boolean): void;
    destroyAll(items: T[]): void;
    destroyAll(): void;
}

interface KnockoutSubscribable<T> extends KnockoutSubscribableFunctions<T> {
    subscribe(callback: (newValue: T) => void, target?: any, event?: string): KnockoutSubscription;
    subscribe<TEvent>(callback: (newValue: TEvent) => void, target: any, event: string): KnockoutSubscription;
    extend(requestedExtenders: { [key: string]: any; }): KnockoutSubscribable<T>;
    getSubscriptionsCount(): number;
}

interface KnockoutSubscribableFunctions<T> {
    notifySubscribers(valueToWrite?: T, event?: string): void;
}

interface KnockoutObservableFunctions<T> {
    equalityComparer(a: any, b: any): boolean;
}

Here is an example of the alternative... assuming you need push and remove as an example. (Side note: inheritance isn't possible here as these are all interfaces - if Knockout supports inheritance, the type definition could be changed to allow the work-free extends rather than the hard work implements version of this answer...)

The interesting bit in this example is the first interface - it is a custom one you define, but anything that matches it will do, including a KnockoutObservableArray<Email>. This is an example of structural typing saving you some time. Structural typing wants you to go to the pub.

/// <reference path="scripts/typings/knockout/knockout.d.ts" />

interface MyObservableArray<T> {
    push(...items: T[]): void;
    remove(item: T): T[];
}

class Email {
    user: string;
    domain: string;
}

class ObservableEmailArray implements MyObservableArray<Email> {
    push(...items: Email[]): void {

    }

    remove(item: Email): Email[] {
        return [];
    }
}

var observableArrayOne: KnockoutObservableArray<Email> = ko.observableArray<Email>();
var observableArrayTwo: MyObservableArray<Email> = new ObservableEmailArray();

function example(input: MyObservableArray<Email>) {
    // Just an example - will accept input of type MyObservableArray<Email>...
}

example(observableArrayOne);

example(observableArrayTwo);
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Implementing the whole chain would be a ton of work to accomplish a very simple inheritance situation. Your second paragraph sounds more promising, but I don't really understand what you mean. – Joshua Frank Dec 01 '14 at 18:18
  • Does that added example help to explain? – Fenton Dec 01 '14 at 20:27
  • If all your trying to do is add functionality to rows you pass to a table, I can show you an easier way of doing it, rather than re-defining observablearray. – shawty Dec 01 '14 at 21:43
  • @SteveFenton: Are you saying that because the KO objects are interfaces, they simply can't be extended? I can understand that, although it's kind of limiting, and this is something I do all the time in .NET, and the underlying javascript supports it, so I imagined there'd be a way to express it in Typescript. In your example, it looks like you have to re-implement the whole interface--or some subset that you have to decide--and that is way too messy to be useful, I think. – Joshua Frank Dec 03 '14 at 14:16
  • @shawty: I'm trying to add functionality to the collection of rows, but I'm open to other ways of accomplishing this. What would you recommend? – Joshua Frank Dec 03 '14 at 14:16
  • I'm merely talking in terms of the definition file, not the Knockout library itself. If you provide a type definition with interfaces, you can't have a class that extends those interfaces. This is unrelated to the real code. If the real code allows extension, the definition could be changed to better reflect it. @shawty can describe how to achieve what you want practically, I can probably help you to make the TypeScript type system play with that solution :) – Fenton Dec 03 '14 at 14:18
  • @SteveFenton Let me type up an answer showing you how I get around extending the row objects :-) It'll be easier than trying to describe in the comments. – shawty Dec 03 '14 at 14:28
  • @SteveFenton: I commented on shawty's solution below. The problem is really about how to make a subclass of something that, in the knockout typings file, isn't a class. I am accepting that it's not possible, so now I'm looking for a workaround with reasonably convenient syntax and semantics. – Joshua Frank Dec 03 '14 at 18:45
0

I had the same problem and found workaround.

   export class ShoppingCartVM {
        private _items: KnockoutObservableArray<ShoppingCartVM.ItemVM>;
        constructor() {
            this._items = ko.observableArray([]);
            var self = this;
            (<any>self._items).Add = function() { self.Add(); }
            return <any>self._items;
        }

        public Add() {
            this._items.push(new ShoppingCartVM.ItemVM({ Product: "Shoes", UnitPrice: 1, Quantity: 2 }));
        }
    }

You can't cast ShoppingCartVM into KnockoutObservableArray, but you can introduce common interface like Steve Fenton answered.

zielu1
  • 1,308
  • 11
  • 17