TypeScript Event Model



Headlines

Keyboard events

Example (keyup user interface event: see also here… and there…) Shopping.ts.zip 

class Shopping {
    …
    static _Handle(event: KeyboardEvent) {
        if (event.defaultPrevented)
            window.console.log("'event.preventDefault()' already done..."); // By construction, propagation goes on...
        /**
         * Test
         */
        // window.console.log(`${event.key}` + ' <-event.key - event.code-> ' + `${event.code}`);
        // window.console.log('match: ' + event.key.match(Shopping._Query_content));
        if (Shopping._Query !== null)
            Shopping._Query.innerHTML += `${event.key}`; // Caution: raw display...
        /**
         * End of test
         */
        if (event.code === 'Backspace')
            Shopping._Buffer.pop();
        if (event.code === 'Escape' || event.code === 'Enter') {
            Shopping._Buffer.length = 0;
            if (window.confirm("Erase screen?"))
                Shopping._Query.innerHTML = "";
        }
        if (Shopping._Query_content.test(event.key) && event.key.length === 1) { // Test again regular expression...
            let last = Shopping._Buffer.pop();
            if (event.code === 'Space') {
                if (last !== undefined) {
                    if (!last.includes(" ")) // 'includes' tests equality as well...
                        Shopping._Buffer.push(last); // Re-inject 'last' at last position
                    Shopping._Buffer.push(event.key);
                }
            } else { // Something else that 'Space'...
                if (last === undefined)
                    Shopping._Buffer.push(event.key);
                else {
                    Shopping._Buffer.push(last); // Re-inject 'last' at last position
                    Shopping._Buffer.push(event.key);
                    if (!last.includes(" ")) { // 'includes' tests equality as well...
                        window.console.log("Attempt to search: " + Shopping._Buffer.join(''));
                        Shopping._Search(Shopping._Buffer.join(''));
                    }
                }
            }
        }
//        if (Shopping._Query !== null)
//            Shopping._Query.innerHTML = Shopping._Buffer.join(''); // Nice display...
    }
    …
}
window.addEventListener('keyup', Shopping._Handle);

Touch events

User Interface -UI- events may depend upon browser/device capabilities like “touch”. Event subscription can then be guided by these capabilities.

/** https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events, compatibility: 'caniuse.com/#feat=pointer' */
// Chrome: 'chrome://flags' and Firefox: dom.w3c_pointer_events.enabled to 'true' in 'about:config' <- 'false' in old Firefox versions!
if (window.PointerEvent) { // Chrome v.76, Firefox v.68, but it does not work with Safari 12.1 (v.13 required)
    window.document.getElementById('NoLanguage').onpointerdown = NoLanguagePlayground.prototype.pointerdown.bind(this);
    window.document.getElementById('NoLanguage').onpointermove = NoLanguagePlayground.prototype.pointermove.bind(this);
    window.document.getElementById('NoLanguage').onpointerover = NoLanguagePlayground.prototype.pointerover.bind(this);
    window.document.getElementById('NoLanguage').onpointerup = NoLanguagePlayground.prototype.pointerup.bind(this);
} else {
// Note: 'window.navigator.maxTouchPoints === undefined' with Firefox on an ordinary desktop computer
// We must then test that the 'TouchEvent' API is enabled *AND* the device is really touch-based
    if ('ontouchstart' in window && window.navigator.maxTouchPoints > 0) {
        window.document.getElementById('NoLanguage').addEventListener('touchstart', NoLanguagePlayground.prototype.pointerdown.bind(this), false); /* Etc. */ }
    else { // Safari v12.1:
        window.document.getElementById('NoLanguage').addEventListener('mousedown', NoLanguagePlayground.prototype.pointerdown.bind(this), false); /* Etc. */ }
}

Rule(s)

Example (mouse events)

var My_controller = function (…) { // 'My_controller' class (JavaScript 5 style)
    …
    this._my_canvas = window.document.getElementById("my_canvas");
    …
    this._my_canvas.addEventListener('mousedown', My_controller.prototype.pointerdown.bind(this), false);
    this._my_canvas.addEventListener('mousemove', My_controller.prototype.pointermove.bind(this), false);
    this._my_canvas.addEventListener('mouseover', My_controller.prototype.pointerover.bind(this), false);
    this._my_canvas.addEventListener('mouseup', My_controller.prototype.pointerup.bind(this), false);
    …
};
…
My_controller.prototype.pointerdown = function (mouse_event) {
    mouse_event.preventDefault(); // Default behaviors are canceled
    mouse_event.stopPropagation(); // The event occurrence is processed once and for all
    if (mouse_event.button === 0) { // Mouse left button pressed…
        …
    }
    if (mouse_event.button === 2) { // Mouse right button pressed…
        …
    }        
    …
};

Drag&drop events

Example (drag&drop management based on utility -static- function in BPMiNer class)

public static readonly File_reader = new FileReader();

public static readonly Drag_and_drop = (): void => {
    // FYI: 'drag', 'dragend' and 'dragstart' are *NEVER* fired when dragged entities come from the OS (compared to draggable entities in the browser having "draggable = true")
    // Corollary: 'setDragImage' won't work here...
    // Mouse events are inhibited when drag&drop: http://blog.teamtreehouse.com/implementing-native-drag-and-drop
    window.document.body.addEventListener('dragenter', (event: DragEvent) => {
        event.preventDefault();
        event.stopImmediatePropagation();
    }, false);
    // 'preventDefault' in 'dragover' is MANDATORY to later allow 'drop':
    window.document.body.addEventListener('dragover', (event: DragEvent) => {
        event.preventDefault();
        event.stopImmediatePropagation();
    }, false);
    window.document.body.addEventListener('drop', (event: DragEvent) => {
        event.preventDefault();
        event.stopImmediatePropagation();
// Caution: 'event.dataTransfer.types.length === 2' with Firefox while 'event.dataTransfer.types.length === 1' with Chrome
// 'event.dataTransfer.types.length === 7' with Safari!
        if ('dataTransfer' in event) { // 'event.dataTransfer.items' might be 'undefined' for Safari!
            let i = 0;
            while (event.dataTransfer!.items[i].kind !== 'file')
                i++;
            // Dropped file name (OK with Chrome, Firefox and Safari):
            // window.alert(event.dataTransfer!.items[i].getAsFile()!.name);
            BPMiNer.Current_test_case(14, event.dataTransfer!.items[i].getAsFile()!.name.substring(0, event.dataTransfer!.items[i].getAsFile()!.name.indexOf(".")));
            // Load XML:
            BPMiNer.File_reader.readAsText(event.dataTransfer!.items[i].getAsFile()!); // UTF-8
         else
            throw new Error("'Drag_and_drop' >> ''dataTransfer' in event', untrue.");
    }, false);
};
Event model

Rule(s)

Example (user-defined events) Asynchronous_programming.Three.js.ts.zip 

class Asynchronous_programming {
    private readonly _image: HTMLImageElement = new Image();
    …
    constructor(private readonly _image_URL: string, private readonly _count_down = 20, private readonly _steps = 4) {
        …
        window.document.addEventListener("Image_is_ready", this._create_3D_object, false);
        window.document.addEventListener("3D_object_is_ready", () => { // 'custom_event' is ignored
            this._interval_id = window.setInterval(this._time_out, this._count_down / this._steps * 1000);
        }, false);
        window.document.addEventListener("3D_object_is_ready", (custom_event: Event) => {
            if ((custom_event as CustomEvent).detail.name !== this._image_URL)
                throw ("Abnormal situation...");
            this._animation_id = window.requestAnimationFrame(this._animate_3D_object);
        }, false);

        this._image.onload = () => window.document.dispatchEvent(new Event("Image_is_ready"));
        this._image.src = this._image_URL;
        …
    }
    
    private _create_3D_object: () => void = () => {
        // Test:
        window.console.assert(this._image && this._image instanceof Image && this._image.complete, 'Contract violation: \'this._image\'');
        // Versus defensive programming:
        if (!(this._image && this._image instanceof Image && this._image.complete))
            throw ('Contract violation: \'this._image\'');

       // Construction of 3D object based on Three.js…

        window.document.dispatchEvent(new CustomEvent("3D_object_is_ready", {'detail': {name: this._image_URL}}));
    }
    …
}
Timers are the way of performing periodic and/or repetitive tasks: setInterval (cyclic timer) and setTimeout (one-shot timer) are ready-to-use facilities for time management.

Example Asynchronous_programming.Three.js.ts.zip 

class Asynchronous_programming {
    …
    private _time_left; // In sec...

    private _animation_id: number = 0; // '0' means that animation hasn't yet started...
    private _interval_id!: number;
    …
    constructor(private readonly _image_URL: string, private readonly _count_down = 20, private readonly _steps = 4) {
        …
        window.document.addEventListener("3D_object_is_ready", () => { // 'custom_event' is ignored
            this._interval_id = window.setInterval(this._time_out, this._count_down / this._steps * 1000);
        }, false);
        …
    }
    
    private _time_out = () => {
        if ((this._time_left -= this._count_down / this._steps) > 0)
            // Go on...
        else {
            // Stop animation:
            window.cancelAnimationFrame(this._animation_id);
            window.clearInterval(this._interval_id);
            …
        }
    }
}
Animation is the ability to register a function, which is called each time the browser repaints frames by means of the window.requestAnimationFrame facility.

Rule(s)

Example Asynchronous_programming.Three.js.ts.zip 

class Asynchronous_programming {
    …
    private _time_left; // In sec...

    private _animation_id: number = 0; // '0' means that animation hasn't yet started...
    private _interval_id!: number;
    …
    constructor(private readonly _image_URL: string, private readonly _count_down = 20, private readonly _steps = 4) {
        …
        window.document.addEventListener("3D_object_is_ready", (custom_event: Event) => {
            if ((custom_event as CustomEvent).detail.name !== this._image_URL)
                throw ("Abnormal situation...");
            this._animation_id = window.requestAnimationFrame(this._animate_3D_object);
        }, false);
        …
    }

    private _animate_3D_object = (timestamp?: number) => {
        if (timestamp !== undefined) { /* first call: 'timestamp === undefined' */
            // One may handle the elapsed time from the first call to '_animate_3D_object'
        }

        // Three.js stuff here...

        this._animation_id = window.requestAnimationFrame(this._animate_3D_object);
    }

    private _time_out = () => {
        if ((this._time_left -= this._count_down / this._steps) > 0)
            // Go on...
        else {
            // Stop animation:
            window.cancelAnimationFrame(this._animation_id);
            window.clearInterval(this._interval_id);
            …
        }
    }
}
Asynchronous programming

Rule(s)

Example (resolve class method) Promises.ts.zip 

const Firefox = Promise.resolve(window.navigator.userAgent.includes("Firefox"));
Firefox.then(result => {
    if (result)
        window.alert(window.navigator.userAgent);
});

const Chrome = Promise.resolve(window.navigator.userAgent.includes("Chrome"));
Chrome.then(result => {
    if (result)
        window.alert(window.navigator.userAgent);
});

Example (race class method) Promises.ts.zip 

const image = new Image();
const is_image_loaded = new Promise(send => {
    image.onload = () => {
        window.console.assert(image.complete);
        send(image.complete);
    };
});
image.src = "./img/Night_sky.jpg"; // Image load…

const is_image_loaded_in_less_than_100ms = new Promise((yes, no) => {
    window.setTimeout(() => no(new Error('more than 100ms')), 100); // Change to '1000' afterwards...
});

const who_wins = Promise.race([is_image_loaded, is_image_loaded_in_less_than_100ms]);
who_wins.then(image_complete => window.alert("Image complete: " + image_complete));
who_wins.catch(error => window.alert(error));

Rule(s)

Example (all class method) Promises.ts.zip 

// 'fetch' API support at 'http://caniuse.com/#feat=fetch'
Promise.all([Promise.resolve(window.fetch !== undefined), (window as any).DOM_ready, window.fetch("./img/Night_sky.jpg").then((response: Response) => {
    return response.blob();
})]).then((parameters: Array<any>) => {
    // 'parameters[1]' guarantees that DOM is ready:
    const image = window.document.getElementById("night_sky") as HTMLImageElement;
    if (parameters[0] === true)  // 'fetch' API is supported by the browser
        // 'parameters[2]' -> Image is available (i.e., 'complete') and transformed into blob:
        image.src = URL.createObjectURL(parameters[2] as Blob); // 'image' is loaded from "./img/Night_sky.jpg"
});

Rule(s)

Example LiveBPMN.com.Node.js.ts.zip 

/* Node.js program */
// https://developer.chrome.com/docs/puppeteer/
// Download problem: 'sudo npm i puppeteer --unsafe-perm=true --allow-root'
// Check whether 'puppeteer.js' has been installed: 'npm list -depth=0'
import * as puppeteer from "puppeteer";

const scrape = async () => {
    const browser = await puppeteer.launch({headless: false});
    const page = await browser.newPage();
    await page.goto("https://LiveBPMN.com");
    await page.screenshot({path: "./LiveBPMN.png"});
    await browser.close();
    return "https://LiveBPMN.com";
};

scrape().then((value) => {
    console.log(JSON.stringify(value)); // Success!
});

Example Carrefour_Items.Java.zip  (App. has to be launched from Java server side)

private readonly _item_data: Promise<Open_Food_Facts_item>;

private constructor(private readonly _gtin: string) { // '_gtin' *field* automatic inclusion
    // => this._gtin = _gtin; -> no need!
    this._item_data = new Promise((ready: Ready, problem) => { // 'problem' aims at being called when 'Promise' object has difficulty in achieving its job...
        this._get_Open_Food_Facts_item(ready, problem);
    });
};

public async get_Open_Food_Facts_item() { // So returned type is inferred from 'async'...
    return await this._item_data; // Wait for '_get_item_data' (i.e., 'ready(offi);')
}

private _get_Open_Food_Facts_item(ready: Ready, problem, time_out?: number): void { // Call may omit 'time_out' optional parameter
…

Catching window.document ready as a promise

let DOM_ready = null;
Object.defineProperty(window, "DOM_ready", {
    value: new Promise(launched_function_when_DOM_ready => {
        DOM_ready = launched_function_when_DOM_ready;
    }), enumerable: false, configurable: false, writable: false
});
window.document.onreadystatechange = DOM_ready;

// In other places, one may check whether the DOM is ready:
(window as any).DOM_ready.then(() => {
    window.document.body.offsetWidth; // Access to 'body' content...
    window.document.body.offsetHeight; // Access to 'body' content...
    // Etc.
});

Catching window loaded as a promise

let Window_loaded: ((event: Event) => void) | undefined = undefined;
Object.defineProperty(window, "Window_loaded", {
    value: new Promise(launched_function_when_Window_loaded => {
        Window_loaded = launched_function_when_Window_loaded;
    }), enumerable: false, configurable: false, writable: false
});
window.addEventListener('load', Window_loaded!);

// In other places, one may check whether both the DOM is ready and the browser window (images, sounds, videos...) is loaded:
Promise.all([(window as any).DOM_ready, (window as any).Window_loaded]).then(value => {
    /* 'value' is an array of results provided by 'window.DOM_ready' and 'window.Window_loaded' */
    window.alert("Everything is now ready for the Web!");
});

for await...of

 // Generator function, which delivers values based on async. constraints:
async function* primes(): AsyncGenerator<number> {
    // 'Promise' simulates the fact that some time is required to get prime number:
    yield await new Promise(serve => {
        setTimeout(() => {
            serve(2);
        }, 10); // Each 10 ms...
    });
    yield await new Promise(serve => {
        setTimeout(() => {
            serve(3);
        }, 10); // Each 10 ms...
    });
    yield await new Promise(serve => {
        setTimeout(() => {
            serve(5);
        }, 10); // Each 10 ms...
    });
    yield await new Promise(serve => {
        setTimeout(() => {
            serve(7);
        }, 10); // Each 10 ms...
    });
    yield await new Promise(serve => {
        setTimeout(() => {
            serve(11);
        }, 10); // Each 10 ms...
    });
}

(async () => {
    for await (const prime of primes()) // 'for' awaits prime values...
        console.info(prime);
})(); // Lambda is immediately called for test only...