0

I make an Ionic app with Angular and I need to read barcodes with a physical barcode reader.
This reader is typically like a physical keyboard which sends keyboard event after it read a barcode.
So I made a script to capture Keyboard event on the window object because the user can read a barcode outside inputs, and I transform keyboard events to observable which will "emit" the complete barcode. This script work as expected but I talk about it because I think there is a link with my problem…
In the TypeScript file of my page (Ionic Page, an angular component), I subscribe to my observable (the one I talked about previously…).
The subscription is quiet simple, I just add the barcode into a Set<string> after I made some checking steps. Or, these steps return a Promise, and I add the barcode when the promise is resolved …
The Set of barcodes is shown in the html file in a ngFor loop.
When the barcode reader read the barcode, it is added to the set but the UI is not refreshed…
I'm pretty sure I missed something, and maybe it's about NgZone which I do not really know…

If I add the barcode without calling checking steps (no async code called) the UI is refreshed.
I also tried to call checking steps with a hard coded button which simulate the barcode scanner and it worked…
So the problem is when the barcode is added after the promise is resolved AND the barcode is from the Observer made by window Keyboard Events…

To observe a codebar reader:

export namespace LecteurCodebarrePhysique {
    // L'évènement est-il dans un input ?
    const inInput = (event) => {return event.target instanceof Element && event.target.nodeName.toLowerCase() === 'input'};
    // La touche relachée est-elle un caractère ?
    const isTextKey = (event) => {return !inInput(event) && event.key.length === 1};
    // La touche relachée est-elle la touche entrée ?
    const isEnter = (event) => {return !inInput(event) && event.keyCode === 13};

    /**
     * Observable émettant le codebarre lu par un lecteur physique
     */
    export function codebarreLu(): Observable<{text: string, format: string}> {
        // Observable initiale : évèrement clavier
        const keyup: Observable<KeyboardEvent> = fromEvent(window, 'keyup');
        return keyup.pipe(
            // On ne garde que les touches représentant un caractère
            filter(ev => isTextKey(ev)),
            // On ne garde que la valeur du caractère
            map(ev => ev.key),
            // On «bufferise» en attendant la touche entrée
            buffer(keyup.pipe(filter(ev => {
                const enter = isEnter(ev);
                if (enter) {
                    ev.preventDefault();
                    ev.stopPropagation();
                }
                return enter;
            }))),
            // Quand la touche entrée est relachée, on concatène les caractères
            // Et on essaye de déterminer si c'es un EAN13 (13 caractères numériques)
            map(chars => {
                const codebarre = chars.reduce((code, char) => code + char, '');
                const isEan13 = /\d{13}/.test(codebarre);
                return {text: codebarre, format: isEan13 ? 'EAN_13' : 'INCONNU'};
            })
        );
    }
}  

The TypeScript file of the page (MArticle is a service with different methods for Article objects. In this class, I use it to check if a barcode is already known on an Article object in DB):

export class ArticlesNouveauPage {
    codebarres = new Set<string>();
    codebarreLuSub: Subscription;
    article = new Article();

    constructor(private mArticle: MArticle) {}

    ionViewWillEnter() {
        // On souscrit à la letcure de codebarre physique
        this.codebarreLuSub = LecteurCodebarrePhysique.codebarreLu().subscribe(resultat => this.ajouterCodebarre(resultat));
    }

    ionViewWillLeave() {
        // Quand on quitte l'écran on ne souscrit plus à la lecture des codebarres physiques
        this.codebarreLuSub.unsubscribe();
    }

    /**
     * Ajout d'un codebarre
     * @param resultat
     */
    private ajouterCodebarre(resultat: {text: string}) {
        // If an «Article» object is found with the barcode, we show an error message
        return this.mArticle.getInstanceByGtin(resultat.text)
            .then(article => {
                    this.tools.afficherMessage(`Le codebarre ${resultat.text} est déjà assigné à l'article "${article.libelle}" !`);
            })
            .catch(() => {
                // If the promise is rejected, the barcode is unknown, we can add it to the list
                this.addCodebarreToList(resultat.text);
            });
    }

    private addCodebarreToList(codebarre: string) {
        this.codebarres.add(codebarre);
    }

    testAddBarcode() {
        this.ajouterCodebarre({text: `1234567890123`});
    }
}

The HTML code of the page:

<ion-content >
    <form #f="ngForm">
        <ion-item-group>
            <ion-item-divider color="secondary">Article</ion-item-divider>
            <ion-item>
                <ion-label color="primary" fixed>Libellé</ion-label>
                <ion-input type="text" [(ngModel)]="article.libelle" name="libelle" required></ion-input>
            </ion-item>
            <ion-item>
                <ion-label color="primary" fixed>Prix</ion-label>
                <ion-input type="number" [(ngModel)]="article.prix" name="prix" required></ion-input>
            </ion-item>
            <ion-item>
                <ion-label color="primary" fixed>Code</ion-label>
                <ion-input type="text" [(ngModel)]="article.code" name="code"></ion-input>
            </ion-item>
        </ion-item-group>
    </form>

    <ion-item-group>
        <ion-item-divider color="secondary">
            Codebarres associés
        </ion-item-divider>
        <ion-item *ngFor="let codebarre of codebarres">
            <ion-icon name="barcode" item-start color="secondary"></ion-icon>
            <h2>{{codebarre}}</h2>
        </ion-item>
    </ion-item-group>

    <ion-fab left bottom>
        <button ion-fab color="danger" (click)="testAddBarcode()"><ion-icon name="add"></ion-icon></button>
    </ion-fab>
</ion-content>

When I click on the «plus» button, the barcode is added to the list and UI is refreshed.
When I scan a barcode with the physical barcode scanner, the barcode is added but the UI is not refreshed.
I did expect the same behavior between both mode…
I think it's maybe a NgZone problem but I'm not an expert about it…
I think I missed something, but what…

Jason Aller
  • 3,541
  • 28
  • 38
  • 38

2 Answers2

2

You can use ChangeDetectorRef that is build in angular, object it into the constructor say private CD: ChangeDetectorRef and then you can use it to change the view, when you code is executed to change the array in ngFor you can type the method this.CD.detectChanges() this will detect the changes and refresh your UI In angular as well as an ionic app

Felipe Augusto
  • 7,733
  • 10
  • 39
  • 73
  • Thank you but I already did that, and it worked. But I would lixe to understand why it does not worked without the changeDetectorRef … – Bruno Desprez Jan 14 '19 at 13:06
  • Basically it changes when the life cycle has already completed! And for it to capture major changes the detectChanges method triggers a check on all the objects by its references! And hence each parent view is checked and consequently the each child view is checked! So after the view has been rendered! This is the way to trigger a check and refresh you view! – Shivam Rajvir Jan 14 '19 at 13:13
  • Does this work for Ionic 4? I tried this.CD.detectChanges() and am getting "Property 'CD' does not exist on type 'Tab2Page'" – Robert Smith Feb 07 '20 at 15:40
0

Angular will only listen to changes defined within its framework. As Window is a native object, Angular does not listen to its changes and update the view. Either use ChangeDetectorRef to manually update the view, or create an angular service out of window, like so: https://stackoverflow.com/a/37176929/5108158.

Edit: The following should work, and update changes correctly:

import { Component, Renderer } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {
  constructor(private renderer: Renderer) {};

  ngAfterViewInit() {
    this.renderer.listenGlobal('body', 'keypress', (event) => {
      console.log(event);
    })
  }
}

This will work in any component, but best off on the top level app component.

Maximillion Bartango
  • 1,533
  • 1
  • 19
  • 34
  • I chose the first option. But I think there is another simple solution … My goal is to catch keyup event outside any input. Maybe I'm too far with window. Is it possible to listen keyup event on the Ionic page ? – Bruno Desprez Jan 15 '19 at 14:06
  • Have you tried creating an event listener in `app.component.ts`? Not certain, but this is the top level component for angular and I assume all events would propagate to it. – Maximillion Bartango Jan 15 '19 at 14:38
  • I created a npm project for the part to read barcodes. I now listen on document and no more on window (nothing happends on iOS devices ...). Maybe with these modifications it will work better... I'll keep you posted guys – Bruno Desprez Jan 17 '19 at 23:05