Async Loading/Unloading of content using TypeScript and Promises Code Answer

Hello Developer, Hope you guys are doing great. Today at Tutorial Guruji Official website, we are sharing the answer of Async Loading/Unloading of content using TypeScript and Promises without wasting too much if your time.

The question is published on by Tutorial Guruji team.

I have created a framework for async loading/unloading of content using TypeScript, Knockout, Generic Promises for TypeScript (https://github.com/pragmatrix/Promise) and async (https://github.com/caolan/async).

While the logic works correctly and the events are triggered and happen in the correct order, while a NavigationItem is loading the UI does not update with new selection and load is not started on the new item. Can anyone see why this would be?

The core of the logic is in the NavigationItem class:

export class NavigationItem {
    constructor(public Data: INavigationData) {
        this.data = ko.observable(Data);
        this.data.subscribe(n => Data = n);
        this.status = ko.observable(NavigationItemStatus.Unloaded);
        this.isLoading = ko.computed(() => this.status() == NavigationItemStatus.Loading);
        this.isLoaded = ko.computed(() => this.status() == NavigationItemStatus.Loaded);
        this.isUnloaded = ko.computed(() => this.status() == NavigationItemStatus.Unloaded);
        this.isUnloading = ko.computed(() => this.status() == NavigationItemStatus.Unloading);
    }
    public data: KnockoutObservable<INavigationData>;
    public status: KnockoutObservable<NavigationItemStatus>;
    public isLoading: KnockoutComputed<boolean>;
    public isLoaded: KnockoutComputed<boolean>;
    public isUnloading: KnockoutComputed<boolean>;
    public isUnloaded: KnockoutComputed<boolean>;
    public closed: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
    public navigationItemAdded: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
    private queue: AsyncQueue<boolean> = async.queue((s, c) => {
        if (s)
            this.loadWorker().done(() => c());
        else
            this.unloadWorker().done(() => c());
    }, 1);
    load() : Promise<boolean>{
        var d = defer<boolean>();
        this.queue.push(true, () => d.resolve(true));
        return d.promise();
    }
    unload() : Promise<boolean>{
        var d = defer<boolean>();
        this.queue.push(false, () => d.resolve(true));
        return d.promise();
    }
    private unloadWorker(): Promise<boolean> {
        var d = defer<boolean>();
        this.doUnload().done(s => this.onUnloaded(s, d));
        this.onUnloading();
        return d.promise();
    }
    private loadWorker(): Promise<boolean>{
        var d = defer<boolean>();
        if (this.isLoaded())
        {
            this.unload();
            this.load();
            d.resolve(false);
        }
        else {
            this.doLoad().done(s => this.onLoaded(s, d));
            this.onLoading();
        }
        return d.promise();
    }
    private onLoaded(loadStatus: boolean, promise: P.Deferred<boolean>) {
        this.status(NavigationItemStatus.Loaded);
        promise.resolve(loadStatus);
    }
    private onUnloaded(unloadStatus: boolean, promise: P.Deferred<boolean>) {
        this.status(NavigationItemStatus.Unloaded);
        promise.resolve(unloadStatus);
    }
    private onLoading() {
        this.status(NavigationItemStatus.Loading);
    }
    private onUnloading() {
        this.status(NavigationItemStatus.Unloading);
    }
    doLoad(): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(true);
        return d.promise();
    }
    doUnload(): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(true);
        return d.promise();
    }
    close() {
        if(this.status() != NavigationItemStatus.Unloaded)
            this.unload();
        this.closed.trigger(this);
    }
    addNavigationItem(navigationItem : NavigationItem) {
        this.navigationItemAdded.trigger(navigationItem);
    }
}

When load() is called it queues up a load worker, when unload() is called it queues up an unload worker, the queue has a concurrency of 1. The NavigationItemCollection class extends NavigationItem, exposes an observable array and implements doLoad and doUnload.

export class NavigationItemCollection<T> extends NavigationItem {
    constructor(data: INavigationData) {
        super(data);
        this.items = ko.observableArray<T>();
    }
    public items: KnockoutObservableArray<T>;
    doLoad(): Promise<boolean> {
        var d = defer<boolean>();
        super.doLoad().done(() => {
            this.getItems().done(i => {
                if (i != null) {
                    for (var k: number = 0; k < i.length; k++) {
                        this.items.push(i[k]);
                    }
                }
                d.resolve(true);
            });
        });
        return d.promise(); 
    }
    doUnload(): Promise<boolean> {
        var d = defer<boolean>();
        super.doUnload().done(() => {
            this.items.removeAll();
            d.resolve(true);
        });
        return d.promise();
    }
    getItems(): Promise<T[]> {
        var d = defer<T[]>();
        d.resolve(null);
        return d.promise();
    }
}

The RepositoryNavigationItem class then implements NavigationItemCollection and implements getItems().

export class RepositoryNavigationItem<TViewModel, TEntity> extends ViewModels.Navigation.NavigationItemCollection<TViewModel>{
    constructor(data: ViewModels.Navigation.INavigationData, public Repository: Northwind.Repository.IRepositoryGeneric<TEntity>) {
        super(data);
    }
    getItems(): Promise<TViewModel[]> {
        var d = defer<TViewModel[]>();
        this.Repository.GetAll().done(i => {
            var vms: TViewModel[] = [];
            if (i != null) {
                for (var k: number = 0; k < i.length; k++) {
                    vms.push(this.createViewModel(i[k]));
                }
            }
            d.resolve(vms);
        });
        return d.promise();
    }
    createViewModel(entity : TEntity): TViewModel {
        return null;
    }
}
export class ProductsNavigationItem extends RepositoryNavigationItem<Northwind.Product, Northwind.IProduct>{
    createViewModel(entity: Northwind.IProduct): Northwind.Product {
        return Northwind.Product.Create(entity);
    }
}

The Repository is implemented as followed:

export class Repository<TEntity> implements IRepositoryGeneric<TEntity>{
    constructor(public ServiceLocation: string) { }
    GetAll(): Promise<TEntity[]> {
        var d = defer<TEntity[]>();
        $.ajax({
            type: "GET",
            url: this.ServiceLocation + "GetAll",
            success: data => d.resolve(<TEntity[]>data),
            error: err => d.resolve(null)
        });
        return d.promise();
    }
}

The MainWindowViewModel then instantiates the NavigationItems (using IoC dependency injection) and controls the load/unload control flow as NavigationItems are selected and deselected.

export class MainWindowViewModel {
    constructor(private Container: Lind.IoC.IContainer, navigationData: ViewModels.Navigation.INavigationData[]) {
        this.navigationItems = ko.observableArray<ViewModels.Navigation.NavigationItem>();
        this.selectedNavigationItem = ko.observable<ViewModels.Navigation.NavigationItem>();
        this.selectedNavigationItemType = ko.computed(() => {
            var navItem = this.selectedNavigationItem();
            if (navItem != null)
                return navItem.data().Name;
            return "Loading";
        });
        this.selectedNavigationItem.subscribe(n => {
            if (n != null)
                n.unload();
        }, this, "beforeChange");
        this.selectedNavigationItem.subscribe(n => {
            if(n != null)
                n.load();
        });
        for (var i: number = 0; i < navigationData.length; i++) {
            var navItem = Container.Resolve<ViewModels.Navigation.NavigationItem>(typeof ViewModels.Navigation.NavigationItem, navigationData[i].Name,
                [new Lind.IoC.ConstructorParameterFactory("data", () => navigationData[i])]);
            navItem.closed.add(this.onNavigationItemClosed);
            navItem.navigationItemAdded.add(this.onNavigationItemAdded);
            this.navigationItems.push(navItem);
        }
        this.selectedNavigationItem(this.navigationItems.peek()[0]);
    }
    public navigationItems: KnockoutObservableArray<ViewModels.Navigation.NavigationItem>;
    public selectedNavigationItem: KnockoutObservable<ViewModels.Navigation.NavigationItem>;
    public selectedNavigationItemType: KnockoutComputed<string>;
    private onNavigationItemClosed(item: ViewModels.Navigation.NavigationItem) {
        item.closed.remove(this.onNavigationItemClosed);
        item.navigationItemAdded.remove(this.onNavigationItemAdded);
        this.navigationItems.remove(item);
        if (this.selectedNavigationItem() == item)
            this.selectedNavigationItem(this.navigationItems.peek()[0]);
    }
    private onNavigationItemAdded(item: ViewModels.Navigation.NavigationItem) {
        item.navigationItemAdded.add(this.onNavigationItemAdded);
        item.closed.add(this.onNavigationItemClosed);
        this.navigationItems.push(item);
        this.selectedNavigationItem(item);
    }
}

The view is then as followed:

<div id="rightNav" style="float:left">
    <ul data-bind="foreach: navigationItems">
        <li>
            <div data-bind="style:{ background: isLoading() == true ? 'yellow' : (isLoaded() == true ? 'green' : (isUnloading() == true ? 'gray' : (isUnloaded() == true ? 'white' : 'red')))}">
                <span><a data-bind="text:data().DisplayName, click: $parent.selectedNavigationItem"></a><button data-bind="visible:data().IsCloseable == true, click: close" >x</button></span>
            </div>
        </li>
    </ul>
</div>
<div id="leftContent" data-bind="template: { name: selectedNavigationItemType(), data: selectedNavigationItem }"></div>
<script type="text/html" id="Products">
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Supplier</th>
                <th>Category</th>
                <th>Unit Price</th>
                <th>Units in Stock</th>
                <th>Discontinued</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: items">
            <tr>
                <td><span data-bind="text:productName" /></td>
                <td><span data-bind="text: supplier().companyName" /></td>
                <td><span data-bind="text: category().categoryName" /></td>
                <td><span data-bind="text: unitPrice" /></td>
                <td><span data-bind="text: unitsInStock" /></td>
                <td><input type="checkbox" data-bind="checked: discontinued" disabled="disabled" /></td>
            </tr>
        </tbody>
    </table>
</script>

Answer

I added a mock repository and tested that with a mock view:

module Northwind.Repository.Mock {
export class MockRepository<TEntity> implements IRepositoryGeneric<TEntity>{
    constructor(public ServiceLocation: string) { }
    Delete(id: number): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(null);
        return d.promise();
    }
    GetAll(): Promise<TEntity[]> {
        var d = defer<TEntity[]>();
        setTimeout(() => {
            d.resolve(null);
        }, 5000);
        return d.promise();
    }
    Get(id: number): Promise<TEntity> {
        var d = defer<TEntity>();
        d.resolve(null);
        return d.promise();
    }
    Add(entity: TEntity): Promise<TEntity> {
        var d = defer<TEntity>();
        d.resolve(null);
        return d.promise();
    }
    Update(entity: TEntity): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(false);
        return d.promise();

    }
}
}

When mocked this functions correctly so it looks like the jQuery ajax call is somehow blocking, what is going on?

Well it’s an IE Bug! Wtf?

We are here to answer your question about Async Loading/Unloading of content using TypeScript and Promises - If you find the proper solution, please don't forgot to share this with your team members.

Related Posts

Tutorial Guruji