knockout.js - (TypeScript) Utöka KnockoutObservableArray


Jag vill skapa en TypeScript-klass som är löst MyKOArray<T> extends KnockoutObservableArray<T>. Är detta ens möjligt? Om så är fallet, finns det ett kompakt sätt att göra det på?

  • Jag läste Knockouts avsnitt om extenders, men det verkar inte tillämpligt eftersom jag försöker (bland annat) lägga till ett state-attribut -- inte påverka den befintliga subscribable.
  • Jag skulle kunna lista de saker jag har försökt och de sätt de har gått fel på (t.ex. TS2507, TS2322, etc.) men jag tvivlar på att de skulle vara informativa. Som en grundnivå tror jag att några av de väsentliga detaljerna är:

    • KnockoutObservableArray är ett gränssnitt så jag behöver verkligen "utöka" ko.observableArray ("fabriks"-funktionen).
    • Signaturen för ko.observableArray är inte korrekt (dvs. ingen new och den hanterar det initiala värdet i funktionen istället för en constructor-metod).
    • Jag skulle kunna return ko.observableArray(initial) från en konstruktor, men har inte lyckats få prototypkedjan rätt (t.ex. metoder är inte tillgängliga).
    • Jag skulle kunna skriva över @@create i JS för att return ko.observableArray(initial), men jag har haft problem med att hitta ett JS-exempel (för att inte tala om en giltig TS-implementering).
  • Jag hittade också en extremt gammal diskussion om ett liknande ämne. Slutsatsen där var att det var omöjligt på grund av brist på prototypning i KO, men den nuvarande källkoden (t.ex. ko.utils.setPrototypeOfOrExtend) antyder att denna slutsats kan vara föråldrad.
  • Jag har också grävt igenom massor av generiska TS extends-frågor (t.ex. Kan man utöka en funktion i TypeScript?) utan att hitta något som verkade passa.

===

REDIGERA: Varför?

Jag försöker koppla bort mitt system med följande mönster (dokumenterar endast det "intressanta fallet"):

  • ViewModel gör en begäran till en datatjänst (indirekt via en fabrikmetod på en klass). Datatjänsten returnerar en instans omedelbart (MyClass eller KnockoutObservableArray<MyClass> för en Singleton respektive Collection).
  • Asynkront laddar/populerar tjänsten (och hanterar realtidsuppdateringar för) den returnerade instansen. Detta kräver en prenumeration på en WebSocket för händelser och REST-anrop för att ladda data (inklusive möjligheten till försök igen).
  • Tjänsten förmedlar sin framsteg till instansen genom att anropa metoder på instansen ("callback" och "errback" i mitt mönster).
  • Dessa metoder uppdaterar ett state-attribut (d.v.s. observerbart) och kan uppdatera något annat (t.ex. fylla dess värden eller fylla i ett observerbart felmeddelande).
  • Gränssnittet frågar state-observablet för att kommunicera laddningsstatus (och andra aktiviteter) till användaren.

Detta fungerar utmärkt på mina enskilda instanser. state-metoden frågas (och uppdateras) flitigt för att säkerställa giltiga åtgärder.

Jag skulle vilja duplicera mönstret på mina KOArrays. På det sättet kan jag skilja mellan en tom array som ännu inte har laddats och en laddad array som råkar vara tom. Jag kan också lägga till en state och error_message till KOArray för att spegla beteendet hos mina instanser.

Svar #1

Kort sagt, det är inte möjligt på det sätt du beskrev.

Längre berättelse

Knockout är inte implementerat på ett objektorienterat sätt. Så i princip är nästan allt det exponerar funktioner eller vanliga objekt, etc. Prototyp-hanteringen används där för att förlänga funktionsinstansen som returneras av till exempel ko.observable. Så i princip är arv från någon form av Knockout-grejer inte möjligt i en OOP-sinne.

Om du behöver arv för att lägga till funktionalitet kan du lägga till dina nya metoder till ko.observableArray.fn-objektet. Då kommer denna funktion vara anropbar på alla observableArray-instanser efteråt. Ett snabbt exempel finns här.

ko.observableArray.fn.hasElements = function() {
  return this().length > 0;
}
...
if (myModel.selectedItems.hasElements()) {
  ...
}

Om du behöver ärvningen för att begränsa vissa saker, kan du till exempel lägga till din egen fabriksfunktion så här.

ko.myRestrictedObservableArray = function(initialValue) {
  if (!initialValue || !initialValue.length) {
    throw new "You can create an observable array only with a non-empty initial value";
  }

  return ko.observableArray(initialValue);
}

Du kan till och med skriva över den ursprungliga funktionen om du vill, till exempel så här. Men detta är inte en bra praxis i de flesta fall.

var _original = ko.observableArray;
ko.observableArray = function(initialValue) {
  // ... do your custom things
  var array = _original.call(this, initialValue);
  // ... do your custom things 
  return array;
}

Om du har ytterligare ett krav, vänligen beskriv det mer detaljerat i din fråga.

Svar #2

Baserat på @Zoltan:s feedback slutade jag med följande. Jag är säker på att det finns buggar och möjligheter att optimera, men det lät mig skriva allt i en Typescript-klass (med lämplig typkontroll åt båda hållen):

I ren JS (t.ex. en <script> i HTML-filen eller separat JS-fil):

function RestCollection(initialValue) {
    const fn = ko.observableArray(initialValue);
    // Static properties
    ko.utils.extend(fn.constructor, RestCollectionMixin);
    // Methods (already constructed so must attach directly, not to prototype)
    const methods = Object.getOwnPropertyNames(RestCollectionMixin.prototype);
    for (let i=0; i<methods.length; i++) {
        if (methods[i] == "constructor") continue;  // don't move over the constructor
        fn[methods[i]] = RestCollectionMixin.prototype[methods[i]].bind(fn)
    }
    // Constructed properties
    ko.utils.extend(fn, new RestCollectionMixin());
    return fn;
} {}

// RestCollectionMixin needs this; don't return or it will break the constructor
function KnockoutObservableArrayClass() {}

I en TS-fil:

// abstract is not passed to JS so this prevents misuse without breaking the RestCollection constructor
abstract class RestCollectionMixin<T> extends KnockoutObservableArrayClass<T> { // extends handles parent method access
    static FSM = class {
        static STATE_INIT = 'initializing';
        static STATE_READY = 'ready';
        static STATE_SAVING = 'save_requested';
        static STATE_DELETING = 'delete_requested';
        static STATE_DELETED = 'deleted';
        static STATE_ERROR = 'error';
    };
    public states() { // convenience accessor
        return this.constructor.FSM
    }
    public state:KnockoutObservable<string> = ko.observable(this.states().STATE_INIT);

    public processRestError(results) {
        this.state(this.states().STATE_ERROR)
        ...
    }
    ...
}

Slutligen, en definitionsfil för RestCollection och KnockoutObservableArrayClass så att TS-kontroll fungerar:

// provide KnockoutObservableArray properties and methods to the Mixin
interface KnockoutObservableArrayClass<T> extends KnockoutObservableArray<T> {
    new<T>(initialValue): KnockoutObservableArray<T>
}
declare const KnockoutObservableArrayClass:KnockoutObservableArrayClass<any>;

// Define the focal class, capturing all extensions in RestCollectionMixin
interface RestCollection<T> extends RestCollectionMixin<T> {
    new<T>(initialValue): RestCollection<T>
}
declare const RestCollection:RestCollection<any>;