TarVK / model-react

A data model system to use with react hooks
https://tarvk.github.io/model-react/examples/build/
MIT License
13 stars 3 forks source link

How to add a preload callback to a datasource ? #30

Closed JeanLucPons closed 3 years ago

JeanLucPons commented 3 years ago

Hello,

I'm progressing well with your lib. No particular issue found :) I would like to add a onPreLoad callback on the dataloader I made. For instance to handle a login panel before loading the data. I think I can do it by adding an other Field to my AppDataLoader but is there a more efficient way to do that ? Many thanks.

import {FC, ReactNode} from "react";
import {DataLoader,Field,IDataHook,useDataHook} from "model-react";

/**
 * A component to handle the loading or error state of loadable data sources
 */
export const AppLoader: FC <{
    /** The Loadable */
    source: AppDataLoader;
    /** The content to show when there are no exceptions and data loaded */
    whenLoaded: (loadingStatus: string)=>ReactNode;
    /** The node to show while loading */
    onLoading?: ReactNode | ((loadingStatus: string,progress: string) => ReactNode);
    /** The node to show if an error occurred */
    onError?: ReactNode | ((exceptions: any[]) => ReactNode);
}> = ({source, children, whenLoaded, onLoading, onError}) => {

    const [h, {isLoading, getExceptions}] = useDataHook();
    const loadingStatus = source.getSource().get(h);
    const progress = source.getProgress(h);

    if (isLoading())
        return <>{onLoading instanceof Function ? onLoading(loadingStatus,progress) : onLoading}</>;

    if (getExceptions) {
        const exceptions = getExceptions();
        if (exceptions.length > 0)
            return <>{onError instanceof Function ? onError(exceptions) : onError}</>;
    }

    return <>{whenLoaded(loadingStatus)}</>;
};

/**
 * Application loader
 */
export class AppDataLoader {

    protected progress:Field<string>;
    protected dataLoader:DataLoader<string,string>;

    constructor(
        loader: (loader:AppDataLoader) => Promise<string>,
        initialStatus: string,
        initialProgress: string = "0%"
    ) {
        this.dataLoader = new DataLoader(async ()=>{ return loader(this) } ,initialStatus,true,false);
        this.progress = new Field(initialProgress);
    }

    getSource() {
       return this.dataLoader;
    }

    setProgress(value: string) {
        this.progress.set(value);
    }
    getProgress(h: IDataHook): string {
        return this.progress.get(h);
    }

}
JeanLucPons commented 3 years ago

Here is how i implemented the onPreload callback. I used only Fields and the hook I defined in this post It works well and may be adapted to model-react.

import { FC, ReactNode } from "react";
import { DataSource } from "./DataSource";
import { Field } from "./Field";
import { useData } from "./useData";

/**
 * A component to handle the loading or error state of loadable data sources
 */
export const AppLoader: FC<{

    /** The Loadable */
    source: AppDataLoader;

    /** The content to show when there are no exceptions and data loaded */
    whenLoaded: ReactNode | ((loadingInfo: any) => ReactNode);
    /** The content to show before loading the data, when the component set 
     * the loadingInfo , the AppLoader switch to onLoading if defined, whenLoaded otherwise */
    onPreLoad?: ReactNode | ((loadingInfo: DataSource) => ReactNode);
    /** The node to show while loading */
    onLoading?: ReactNode | ((progress: any, loadingInfo: any) => ReactNode);
    /** The node to show if an error occurred during loading, it has to be defined if a loader is used */
    onError?: ReactNode | ((exception: Error) => ReactNode);

}> = ({ source, whenLoaded ,onPreLoad, onLoading, onError }) => {

    // -------------------------------------------------------
    // Internal function
    // -------------------------------------------------------
    function preloading() {
        return <>{onPreLoad instanceof Function ?
            onPreLoad(source.LoadingInfo()) :
            onPreLoad}</>;
    }

    function loading() {
        source.load();
        return <>{onLoading instanceof Function ?
            onLoading(source.Progress().get(), source.LoadingInfo().get()) :
            onLoading}</>;
    }

    function loaded() {
        return <>{whenLoaded instanceof Function ? whenLoaded(source.LoadingInfo().get()) : whenLoaded}</>;
    }

    function error() {
        return <>{onError instanceof Function ? onError(source.Error().get()) : onError}</>;
    }

    // -------------------------------------------------------
    // Listen to data
    // -------------------------------------------------------
    let event = useData([source.LoadingInfo(), source.Progress(), source.Loaded(), source.Error()]);

    if (event === undefined) {

        if (onPreLoad) {
            return preloading();
        } else if (onLoading) {
            return loading();
        } else {
            return loaded();
        }

    } else if (event === source.LoadingInfo()) {

        if (onLoading) {
            return loading();
        } else {
            return loaded();
        }

    } else if (event == source.Progress()) {

        return loading();

    } else if (event === source.Error()) {

        return error();

    } else { // event === source.Loaded()

        return loaded();

    }

};

/**
 * Application loader
 */
export class AppDataLoader {

    protected loadingInfo: Field<any>;     // Loading info coming from preLoad
    protected progress: Field<any>;        // Progress field updated by the loader
    protected error: Field<Error>;         // Exception thrown by the the loader
    protected loaded: Field<string>;       // Result of the loader promise

    protected isLoading: boolean;
    protected loader: (loader: AppDataLoader) => Promise<string>;

    constructor(
        loader: (loader: AppDataLoader) => Promise<string>,
        initialStatus: string,
        initialProgress: any = {}
    ) {
        this.loader = loader;
        this.isLoading = false;
        this.loadingInfo = new Field({});
        this.progress = new Field(initialProgress);
        this.loaded = new Field(initialStatus);
        this.error = new Field(new Error(""));
    }

    /**
     * Containing loading inforamtion, it must be set by the onPreload callback of the 
     * AppLoader component (if used).
     */
    LoadingInfo() {
        return this.loadingInfo;
    }

    /**
     * Sets the progress value.
     * It can be set only from the loader function
     */
    Progress() {
        return this.progress;
    }

    /**
     * Signals that the loader ends its execution
     */
    Loaded() {
        return this.loaded;
    }

    /**
     * Signals that the loader ends its execution with an exception
     */
    Error() {
        return this.error;
    }

    /**
     * Launch the loader function
     */
    async load(): Promise<void> {

        if (!this.isLoading) {
            this.isLoading = true;
            try {
                let s = await this.loader(this);
                this.loaded.set(s);
            } catch (e) {
                this.error.set(e);
            }
        }

    }

}
TarVK commented 3 years ago

Interesting idea. I think having a loader source with progress tracking could be useful for Model-react, but it wouldn't be in this form. As we discussed in the other thread, this approach doesn't really support all features I'm after.

Maybe you could make your own library for your version of the system :) I think there will probably be interest in your simplified (and more performant) system. I just really need the flexibility of mine myself.

JeanLucPons commented 3 years ago

"My" lib will integrate a set of advanced Material UI/React components to build application for a control system (SCADA) and will be submitted to code reviews. The MVC kernel will be just 3 files (abstract datasoure class, Field class and useData hook) and I need to prove that it is safe, that it will not miss callbacks and that it can handle thousands of listeners with asynchronous refreshers that poll data and also events coming from outside through WebSocket. I don't think I will made a small lib of 3 files for the MVC kernel. i let you inform when I'll commit something it but still heavy work to perform. I'm just starting the global skeleton and it needs to be strong enough...