Why am I seeing duplicate HTML content?

Tearing my hair out here. Please assist.

config.page.ts

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
// FIXME Uncomment
import { BtoothService } from '../../btooth/btooth.service';
import { Validators, FormGroup, FormBuilder, FormControl } from '@angular/forms';
import { Router } from '@angular/router';

@Component({
  selector: 'app-config',
  templateUrl: './config.page.html',
  styleUrls: ['./config.page.scss'],
})
export class ConfigPage implements OnInit {
  private wifiAPsUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215D3';
  private wifiSSIDsUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C2';
  private wifiSSIDUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C4';
  private wifiPassUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C5';
  private wifiSecTypeUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C6';
  private wifiHiddenUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C7';
  private wifiIPAssignmentTypeUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215D4';
  private wifiIPUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C8';
  private wifiNetmaskUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C9';
  private wifiGatewayUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CA';
  private wifiPrimaryDNSUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CB';
  private wifiSecondaryDNSUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CC';
  private wifiSaveUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CD';
  public ssids = [];
  public aps = [];
  public ap_form: FormGroup;
  public encrypted = false;
  public display_ip = false;
  public selected_ssid = false;
  public saved_ssid = '';

  validations = {
    'ssid_selector': [
      { type: 'required', message: 'Network name is required.' },
    ],
    'hidden_ssid': [
      { type: 'required', message: 'Network name is required.' },
    ],
    'password': [
      { type: 'required', message: 'Password is required.' },
    ],
    'ip': [
      { type: 'required', message: 'IP address is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
    'netmask': [
      { type: 'required', message: 'Netmask is required.' },
      { type: 'pattern', message: 'Must be formatted like 255.255.255.0' },
    ],
    'gateway': [
      { type: 'required', message: 'Gateway is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
    'primary_dns': [
      { type: 'required', message: 'Primary DNS is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
    'secondary_dns': [
      { type: 'required', message: 'Secondary DNS is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
  };

  constructor(
    private router: Router,
    private cdr: ChangeDetectorRef,
    // FIXME Uncomment
    private btoothService: BtoothService,
    private formBuilder: FormBuilder,
  ) {
    this.ap_form = this.formBuilder.group({
      ssid_selector: new FormControl(''),
      hidden_ssid: new FormControl(''),
      password: new FormControl(''),
      security_type: new FormControl('wpa'),
      ip_assignment_type: new FormControl('dhcp'),
      ip: new FormControl('', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      netmask: new FormControl('', Validators.pattern('^(((255\.){3}(255|254|252|248|240|224|192|128|0+))|((255\.){2}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){2})|((255|254|252|248|240|224|192|128|0+)(\.0+){3}))$')),
      gateway: new FormControl('', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      primary_dns: new FormControl('', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      secondary_dns: new FormControl('', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
    }, {});
  }

  ngOnInit() {
    // FIXME Uncomment
    this.btoothService.read(this.wifiSSIDsUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          this.ssids = JSON.parse(value);
        } catch {
          this.ssids = value;
        }
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiSSIDsUUID) ssids: ', JSON.stringify(value));
        this.cdr.detectChanges();
      },
      error => {
        // FIXME Errors
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiSSIDsUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ssids = ['Home', 'Left neighbor', 'Right neighbor']

    // FIXME Uncomment
    this.btoothService.read(this.wifiAPsUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          this.aps = JSON.parse(value);
        } catch {
          this.aps = value;
        }
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiAPsUUID) aps: ', JSON.stringify(value));
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiAPsUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.encrypted = true;

    // FIXME Uncomment
    this.btoothService.read(this.wifiSSIDUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        // FIXME Also need to decode from wpa_supplicant encoded, I think it's base64?
        console.log('[DEBUG] config ngOnInit() ssid from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no ssid from Bluetooth');
          return;
        }
        this.ap_form.get('password').disable();
        this.ap_form.patchValue({ ssid_selector: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiSSIDUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ap_form.patchValue({ 'ssid_selector': 'Home' });

    // FIXME Uncomment
    this.btoothService.read(this.wifiSecTypeUUID).then(
      value => {
        console.log('[DEBUG] config ngOnInit() SecType from Bluetooth raw bytes:', JSON.stringify(value));
        value = this.btoothService.bytesToString(value);
        console.log('[DEBUG] config ngOnInit() SecType from Bluetooth raw string:', JSON.stringify(value));
        // FIXME Don't try to parse regular strings. Not sure how to tell the difference. Maybe always send JSON?
        //  Maybe do like in Python, attempt to parse, if it fails just pass the value.
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() SecType from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no SecType from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ security_type: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiSecTypeUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ap_form.patchValue({ 'security_type': 'wpa' });

    // FIXME Uncomment
    this.btoothService.read(this.wifiHiddenUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() hidden from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no hidden from Bluetooth');
          return;
        }
        if (value !== 'true') {
          console.log('[DEBUG] config ngOnInit() no hidden from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ ssid_selector: '[Hidden]' });
        this.ap_form.patchValue({ hidden_ssid: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiHiddenUUID) error: ', JSON.stringify(error));
      }
    );
    this.btoothService.read(this.wifiIPUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() IP from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no IP from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ ip: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiIPUUID) error: ', JSON.stringify(error));
      }
    );
    this.btoothService.read(this.wifiNetmaskUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() Netmask from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no Netmask from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ netmask: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiNetmaskUUID) error: ', JSON.stringify(error));
      }
    );
    this.btoothService.read(this.wifiGatewayUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() Gateway from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no Gateway from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ gateway: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiGatewayUUID) error: ', JSON.stringify(error));
      }
    );
    this.btoothService.read(this.wifiPrimaryDNSUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() PrimaryDNS from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no PrimaryDNS from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ primary_dns: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiPrimaryDNSUUID) error: ', JSON.stringify(error));
      }
    );
    this.btoothService.read(this.wifiSecondaryDNSUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() SecondaryDNS from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no SecondaryDNS from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ secondary_dns: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiSecondaryDNSUUID) error: ', JSON.stringify(error));
      }
    );
    this.btoothService.read(this.wifiIPAssignmentTypeUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() IP assignment type from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no IP assignment type from Bluetooth');
          return;
        }
        this.ap_form.patchValue({ ip_assignment_type: value });
        if (value === 'dhcp') {
          this.display_ip = false;
        } else {
          this.display_ip = true;
        }
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiIPAssignmentTypeUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ap_form.patchValue({ 'ip_assignment_type': 'dhcp' });
    // this.display_ip = false;

    this.cdr.detectChanges();
    // TODO Delete selected-ssid service
    this.ap_form.get('ssid_selector').valueChanges.subscribe(
      ssid => {
        this.ap_form.get('password').enable();
        console.log('[DEBUG] ssid: ', JSON.stringify(ssid));
        if (ssid === '[Hidden]') {
          if (this.ap_form.get('security_type').value === 'none') {
            this.encrypted = false;
          } else {
            this.encrypted = true;
          }
          this.cdr.detectChanges();
          return;
        }
        this.selected_ssid = true;
        const selected_AP = this.aps[ssid];
        console.log('[DEBUG] config ngOnInit() ap_form.get(ssid_selector) selected_AP: ', JSON.stringify(selected_AP));
        this.encrypted = selected_AP[1];
        this.cdr.detectChanges();
      },
      error => {
        // FIXME Errors
        console.log('[DEBUG] config ngOnInit() ap_form.get(ssid_selector) error: ', JSON.stringify(error));
      }
    );
    this.ap_form.get('hidden_ssid').valueChanges.subscribe(
      hidden_ssid => {
        if (hidden_ssid !== '') {
          this.selected_ssid = true;
        }
      }
    );
    this.ap_form.get('security_type').valueChanges.subscribe(
      security_type => {
        console.log('[DEBUG] security_type: ', JSON.stringify(security_type));
        if (security_type === 'none') {
          this.encrypted = false;
        } else {
          this.encrypted = true;
        }
        this.cdr.detectChanges();
      }
    );
  }

  display_ip_form(): void {
    this.display_ip = !this.display_ip;
    console.log('[DEBUG] config this.display_ip: ', JSON.stringify(this.display_ip));
    this.cdr.detectChanges();
  }

  onFormSubmit(): void {
    let ssid = '';
    const selected_ssid = this.ap_form.get('ssid_selector').value;
    const hidden_ssid = this.ap_form.get('hidden_ssid').value;
    if (hidden_ssid === '') {
      ssid = selected_ssid;
      console.log('[DEBUG] config saving hidden: ', JSON.stringify('false'));
      // FIXME Uncomment
      const hidden = this.btoothService.stringToBytes('false');
      this.btoothService.write(this.wifiHiddenUUID, hidden);
    } else {
      ssid = hidden_ssid;
      console.log('[DEBUG] config saving hidden: ', JSON.stringify('true'));
      // FIXME Uncomment
      const hidden = this.btoothService.stringToBytes('true');
      this.btoothService.write(this.wifiHiddenUUID, hidden);
    }
    console.log('[DEBUG] config saving ssid: ', JSON.stringify(ssid));
    // FIXME Uncomment
    const bytes_ssid = this.btoothService.stringToBytes(ssid);
    this.btoothService.write(this.wifiSSIDUUID, bytes_ssid);

    // FIXME In the BTooth code, if we pass an ssid but no password or security_type, look to see
    //  if same as existing or in list of APs but if not, if no password assume none, and if
    //  password error since we don't know if WEP or WPA.
    let security_type = this.ap_form.get('security_type').value;
    console.log('[DEBUG] config saving security_type: ', JSON.stringify(security_type));
    // FIXME Uncomment
    security_type = this.btoothService.stringToBytes(this.ap_form.get('security_type').value);
    this.btoothService.write(this.wifiSecTypeUUID, security_type);
    if (this.encrypted) {
      const password = this.ap_form.get('password').value;
      console.log('[DEBUG] config saving password: ', JSON.stringify(password));
      // FIXME Uncomment
      const bytes_password = this.btoothService.stringToBytes(password);
      this.btoothService.write(this.wifiPassUUID, bytes_password);
    }
    const ip_assignment_type = this.ap_form.get('ip_assignment_type').value;
    console.log('[DEBUG] config saving ip_assignment_type: ', JSON.stringify(ip_assignment_type));
    // FIXME Uncomment
    const bytes_ip_assignment_type = this.btoothService.stringToBytes(ip_assignment_type);
    this.btoothService.write(this.wifiIPAssignmentTypeUUID, bytes_ip_assignment_type);
    if (ip_assignment_type === 'static') {
      console.log('[DEBUG] config saving ip: ', JSON.stringify(this.ap_form.get('ip').value));
      // FIXME Uncomment
      const ip = this.btoothService.stringToBytes(this.ap_form.get('ip').value);
      this.btoothService.write(this.wifiIPUUID, ip);
      console.log('[DEBUG] config saving netmask: ', JSON.stringify(this.ap_form.get('netmask').value));
      // FIXME Uncomment
      const netmask = this.btoothService.stringToBytes(this.ap_form.get('netmask').value);
      this.btoothService.write(this.wifiNetmaskUUID, netmask);
      console.log('[DEBUG] config saving primary_dns: ', JSON.stringify(this.ap_form.get('primary_dns').value));
      // FIXME Uncomment
      const primary_dns = this.btoothService.stringToBytes(this.ap_form.get('primary_dns').value);
      this.btoothService.write(this.wifiPrimaryDNSUUID, primary_dns);
      console.log('[DEBUG] config saving secondary_dns: ', JSON.stringify(this.ap_form.get('secondary_dns').value));
      // FIXME Uncomment
      const secondary_dns = this.btoothService.stringToBytes(this.ap_form.get('secondary_dns').value);
      this.btoothService.write(this.wifiSecondaryDNSUUID, secondary_dns);
    }
    console.log('[DEBUG] config asking the device to save all values');
    // FIXME Uncomment
    const save = this.btoothService.stringToBytes('save');
    this.btoothService.write(this.wifiSaveUUID, save);
    let save_successful = false;
    // This setTimeout() is for total time
    // TODO If only I had notifications working...
    setTimeout(() => {
      console.log('[DEBUG] config setTimeout() for read save status total timeout');
      while (!save_successful) {
        // FIXME This just loops
        console.log('[DEBUG] config saving !save_successful');
        // This setTimeout() is for time between tries
        setTimeout(() => {
          console.log('[DEBUG] config setTimeout() for read save status retry');
          // FIXME Uncomment
          this.btoothService.read(this.wifiSaveUUID).then(
            value => {
              console.log('[DEBUG] config saving read save status from Bluetooth value: ', JSON.stringify(value));
              value = this.btoothService.bytesToString(value);
              try {
                value = JSON.parse(value);
              } catch {
                value = value;
              }
              console.log('[DEBUG] config saving read save status from Bluetooth:', JSON.stringify(value));
              if (value === '') {
                console.log('[DEBUG] config saving read save status from Bluetooth is empty (not saved)');
                return;
              }
              if (value !== 'true') {
                console.log('[DEBUG] config saving read save status from Bluetooth is not true (not saved)');
                return;
              }
              save_successful = true;
              // FIXME Uncomment
              // this.router.navigate(['setup/server-connect']);
            },
            error => {
              console.log('[DEBUG] config saving read save status from Bluetooth error: ', JSON.stringify(error));
            }
          );
          // setTimeout() retry milliseconds
        }, 3000);
      }
    }, 10000);
  }
}

config.page.html

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <!-- FIXME Only show during config of existing unit -->
      <!-- TODO Allow the user to download an SD card image and flashing software in a single .exe, Mac .dimg, Linux .tar.gz. -->
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Wi-Fi</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="forms-validations-content">
  <form class="validations-form" [formGroup]="ap_form" (ngSubmit)="onFormSubmit()">
    <ion-list class="inputs-list" lines="full">
      <ion-list-header>
        <ion-label class="header-title">Select network</ion-label>
      </ion-list-header>
      <ion-item class="input-item">
        <ion-select interface="popover" formControlName="ssid_selector" placeholder="Select network">
          <ion-select-option *ngFor="let ssid of ssids" [value]="ssid">
            {{ssid}}
          </ion-select-option>
          <ion-select-option value="[Hidden]">[Hidden]</ion-select-option>
        </ion-select>
      </ion-item>
      <div class="error-container">
        <ng-container *ngFor="let validation of validations.ssid_selector">
          <div class="error-message"
            *ngIf="ap_form.get('ssid_selector').hasError(validation.type) && (ap_form.get('ssid_selector').dirty || ap_form.get('ssid_selector').touched)">
            <ion-icon name="information-circle-outline"></ion-icon>
            <span>{{ validation.message }}</span>
          </div>
        </ng-container>
      </div>
      <div *ngIf="ap_form.controls.ssid_selector.value === '[Hidden]'">
        <ion-item class="input-item">
          <ion-label position="floating">Hidden network name</ion-label>
          <ion-input formControlName="hidden_ssid" clearInput required>
          </ion-input>
        </ion-item>
        <div class="error-container">
          <ng-container *ngFor="let validation of validations.hidden_ssid">
            <div class="error-message"
              *ngIf="ap_form.controls.ssid_selector.value === '[Hidden]' && ap_form.get('hidden_ssid').hasError(validation.type) && (ap_form.get('hidden_ssid').dirty || ap_form.get('hidden_ssid').touched)">
              <ion-icon name="information-circle-outline"></ion-icon>
              <span>{{ validation.message }}</span>
            </div>
          </ng-container>
        </div>
        <ion-label class="header-title">Security type</ion-label>
        <ion-item class="input-item">
          <ion-label>Security type</ion-label>
          <ion-select interface="popover" formControlName="security_type" cancelText="Cancel" okText="OK">
            <ion-select-option value="wpa">WPA</ion-select-option>
            <ion-select-option value="wep">WEP</ion-select-option>
            <!-- FIXME Is it none or open? I can't tell from the code, have to examine a system. -->
            <ion-select-option value="none">None</ion-select-option>
          </ion-select>
        </ion-item>
      </div>
      <div *ngIf="encrypted">
        <ion-item class="input-item">
          <!-- FIXME Remove these packages:
              @angular/material
              @angular/cdk
              @angular/animations
            -->
          <!-- FIXME https://www.chromium.org/developers/design-documents/create-amazing-password-forms -->
          <!-- TODO Eye icon to see password being typed -->
          <ion-label position="floating">Password</ion-label>
          <ion-input type="password" formControlName="password" required></ion-input>
        </ion-item>
        <div class="error-container">
          <ng-container *ngFor="let validation of validations.password">
            <div class="error-message"
              *ngIf="encrypted && ap_form.get('password').enabled && ap_form.get('password').hasError(validation.type) && (ap_form.get('password').dirty || ap_form.get('password').touched)">
              <ion-icon name="information-circle-outline"></ion-icon>
              <span>{{ validation.message }}</span>
            </div>
          </ng-container>
        </div>
      </div>
      <ion-item class="input-item">
        <ion-button class="submit-btn" expand="block" fill="outline" (click)="display_ip_form()">
          IP address</ion-button>
      </ion-item>
      <div *ngIf="display_ip">
        <ion-list-header>
          <ion-label class="header-title">Assignment type</ion-label>
        </ion-list-header>
        <ion-radio-group class="radio-group" formControlName="ip_assignment_type">
          <ion-item class="radio-item">
            <ion-label class="radio-label">Automatic (DHCP)</ion-label>
            <ion-radio slot="start" color="secondary" value="dhcp"></ion-radio>
          </ion-item>
          <ion-item class="radio-item">
            <ion-label class="radio-label">Manual (Static)</ion-label>
            <ion-radio slot="start" color="secondary" value="static"></ion-radio>
          </ion-item>
        </ion-radio-group>
        <div *ngIf="ap_form.controls.ip_assignment_type.value === 'static'">
          <ion-item class="input-item">
            <ion-label position="floating">IP address</ion-label>
            <ion-input formControlName="ip" placeholder="192.168.1.100" clearInput required>
            </ion-input>
          </ion-item>
          <!-- TODO If we start to type something then switch back to DHCP the form won't allow saving -->
          <div class="error-container">
            <ng-container *ngFor="let validation of validations.ip">
              <div class="error-message"
                *ngIf="ap_form.controls.ip_assignment_type.value === 'static' && ap_form.get('ip').hasError(validation.type) && (ap_form.get('ip').dirty || ap_form.get('ip').touched)">
                <ion-icon name="information-circle-outline"></ion-icon>
                <span>{{ validation.message }}</span>
              </div>
            </ng-container>
          </div>
          <ion-item class="input-item">
            <ion-label position="floating">Netmask</ion-label>
            <ion-input formControlName="netmask" placeholder="255.255.255.0" clearInput required>
            </ion-input>
          </ion-item>
          <div class="error-container">
            <ng-container *ngFor="let validation of validations.netmask">
              <div class="error-message"
                *ngIf="ap_form.controls.ip_assignment_type.value === 'static' && ap_form.get('netmask').hasError(validation.type) && (ap_form.get('netmask').dirty || ap_form.get('netmask').touched)">
                <ion-icon name="information-circle-outline"></ion-icon>
                <span>{{ validation.message }}</span>
              </div>
            </ng-container>
          </div>
          <ion-item class="input-item">
            <ion-label position="floating">Gateway</ion-label>
            <ion-input formControlName="gateway" placeholder="192.168.1.1" clearInput required>
            </ion-input>
          </ion-item>
          <div class="error-container">
            <ng-container *ngFor="let validation of validations.gateway">
              <div class="error-message"
                *ngIf="ap_form.controls.ip_assignment_type.value === 'static' && ap_form.get('gateway').hasError(validation.type) && (ap_form.get('gateway').dirty || ap_form.get('gateway').touched)">
                <ion-icon name="information-circle-outline"></ion-icon>
                <span>{{ validation.message }}</span>
              </div>
            </ng-container>
          </div>
          <ion-item class="input-item">
            <ion-label position="floating">Primary DNS</ion-label>
            <ion-input formControlName="primary_dns" placeholder="8.8.8.8" clearInput required>
            </ion-input>
          </ion-item>
          <div class="error-container">
            <ng-container *ngFor="let validation of validations.primary_dns">
              <div class="error-message"
                *ngIf="ap_form.controls.ip_assignment_type.value === 'static' && ap_form.get('primary_dns').hasError(validation.type) && (ap_form.get('primary_dns').dirty || ap_form.get('primary_dns').touched)">
                <ion-icon name="information-circle-outline"></ion-icon>
                <span>{{ validation.message }}</span>
              </div>
            </ng-container>
          </div>
          <ion-item class="input-item">
            <ion-label position="floating">Secondary DNS</ion-label>
            <ion-input formControlName="secondary_dns" placeholder="8.8.4.4" clearInput required>
            </ion-input>
          </ion-item>
          <div class="error-container">
            <ng-container *ngFor="let validation of validations.secondary_dns">
              <div class="error-message"
                *ngIf="ap_form.controls.ip_assignment_type.value === 'static' && ap_form.get('secondary_dns').hasError(validation.type) && (ap_form.get('secondary_dns').dirty || ap_form.get('secondary_dns').touched)">
                <ion-icon name="information-circle-outline"></ion-icon>
                <span>{{ validation.message }}</span>
              </div>
            </ng-container>
          </div>
        </div>
      </div>
      <ion-item class="input-item">
        <ion-button class="submit-btn" type="submit" expand="block" fill="outline"
          [disabled]="!ap_form.valid || !this.selected_ssid">Save
        </ion-button>
      </ion-item>
    </ion-list>
  </form>
</ion-content>

I rebuilt the code in a fresh ionic project, removed the div *ngIf, and moved the code to another page. But still seeing duplicate content; specifically, the validator messages. They appear twice. Argggg. And not only this but the validators aren’t working right. Sometimes the form is marked invalid when it is valid. And when I flip between DHCP and Static, sometimes the form fields stays disabled. Argggggggggg.

This is the most current version of ionic, packages (npm update), using a fresh Ionic Angular conference template, added my setup pages. Phone is Galaxy S6 running Android 7.0. (Nougat)

I receive the following error in the Android console, unclear if related.

2020-09-06 09:28:08.974 17501-17501/io.ionic.starter E/Capacitor/Console: File: http://localhost/vendor-es2015.js - Line 43427 - Msg: ERROR Error: Uncaught (in promise): TypeError: plugin.constructor.getPluginRef is not a function
    TypeError: plugin.constructor.getPluginRef is not a function
        at checkAvailability (http://localhost/vendor-es2015.js:104371:40)
        at callCordovaPlugin (http://localhost/vendor-es2015.js:104469:29)
        at http://localhost/vendor-es2015.js:104277:28
        at http://localhost/vendor-es2015.js:104236:17
        at new ZoneAwarePromise (http://localhost/polyfills-es2015.js:4232:29)
        at tryNativePromise (http://localhost/vendor-es2015.js:104235:20)
        at getPromise (http://localhost/vendor-es2015.js:104255:12)
        at wrapPromise (http://localhost/vendor-es2015.js:104260:13)
        at http://localhost/vendor-es2015.js:104559:20
        at cordova (http://localhost/vendor-es2015.js:104778:96)

Steps to reproduce:

  • Setup the code, you can skip Bluetooth and just replace with dummy values using this.ap_form.patchValue({field: ā€˜value’}), but I have found also that not using patchValue() still causes this.
  • npm update (I received this error and used the workaround.)
  • Leave one of the fields such as Secondary DNS empty
  • Tap DHCP then back to Manual

Ideas?

ip.page.ts:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { BluetoothService } from '../../../../providers/bluetooth.service';
import { Router } from '@angular/router';
import { Validators, FormGroup, FormBuilder, FormControl } from '@angular/forms';

@Component({
  selector: 'app-ip',
  templateUrl: './ip.page.html',
  // styleUrls: ['./config.page.scss'],
})
export class IpPage implements OnInit {
  private wifiIPAssignmentTypeUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215D4';
  private wifiIPUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C8';
  private wifiNetmaskUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215C9';
  private wifiGatewayUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CA';
  private wifiPrimaryDNSUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CB';
  private wifiSecondaryDNSUUID = '2FE11A47-E4B8-4522-9CFF-AA729B8215CC';
  public ip_form: FormGroup;

  validations = {
    'ip': [
      { type: 'required', message: 'IP address is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
    'netmask': [
      { type: 'required', message: 'Netmask is required.' },
      { type: 'pattern', message: 'Must be formatted like 255.255.255.0' },
    ],
    'gateway': [
      { type: 'required', message: 'Gateway is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
    'primary_dns': [
      { type: 'required', message: 'Primary DNS is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
    'secondary_dns': [
      { type: 'required', message: 'Secondary DNS is required.' },
      { type: 'pattern', message: 'Must be formatted like 1.2.3.4' },
    ],
  };

  constructor(
    private router: Router,
    private cdr: ChangeDetectorRef,
    private btoothService: BluetoothService,
    private formBuilder: FormBuilder,
  ) {
    this.ip_form = this.formBuilder.group({
      ip_assignment_type: new FormControl('dhcp'),
      ip: new FormControl({ value: '', disabled: true }, Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      netmask: new FormControl({ value: '', disabled: true }, Validators.pattern('^(((255\.){3}(255|254|252|248|240|224|192|128|0+))|((255\.){2}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){2})|((255|254|252|248|240|224|192|128|0+)(\.0+){3}))$')),
      gateway: new FormControl({ value: '', disabled: true }, Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      primary_dns: new FormControl({ value: '', disabled: true }, Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      secondary_dns: new FormControl({ value: '', disabled: true }, Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
    }, {});
  }

  ngOnInit() {
    this.btoothService.read(this.wifiIPAssignmentTypeUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() IP assignment type from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no IP assignment type from Bluetooth');
          return;
        }
        this.ip_form.patchValue({ ip_assignment_type: value });
        this.staticFormEnable(value);
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiIPAssignmentTypeUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ip_form.patchValue({ ip_assignment_type: 'static' });

    this.btoothService.read(this.wifiIPUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() IP from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no IP from Bluetooth');
          return;
        }
        this.ip_form.patchValue({ ip: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiIPUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ip_form.patchValue({ ip: '192.168.1.100' });

    this.btoothService.read(this.wifiNetmaskUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() Netmask from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no Netmask from Bluetooth');
          return;
        }
        this.ip_form.patchValue({ netmask: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiNetmaskUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ip_form.patchValue({ netmask: '255.255.255.0' });

    this.btoothService.read(this.wifiGatewayUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() Gateway from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no Gateway from Bluetooth');
          return;
        }
        this.ip_form.patchValue({ gateway: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiGatewayUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ip_form.patchValue({ gateway: '192.168.1.1' });

    this.btoothService.read(this.wifiPrimaryDNSUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() PrimaryDNS from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no PrimaryDNS from Bluetooth');
          return;
        }
        this.ip_form.patchValue({ primary_dns: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiPrimaryDNSUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ip_form.patchValue({ primary_dns: '8.8.8.8' });

    this.btoothService.read(this.wifiSecondaryDNSUUID).then(
      value => {
        value = this.btoothService.bytesToString(value);
        try {
          value = JSON.parse(value);
        } catch {
          value = value;
        }
        console.log('[DEBUG] config ngOnInit() SecondaryDNS from Bluetooth:', JSON.stringify(value));
        if (value === '') {
          console.log('[DEBUG] config ngOnInit() no SecondaryDNS from Bluetooth');
          return;
        }
        this.ip_form.patchValue({ secondary_dns: value });
        this.cdr.detectChanges();
      },
      error => {
        console.log('[DEBUG] config ngOnInit() Bluetooth read(this.wifiSecondaryDNSUUID) error: ', JSON.stringify(error));
      }
    );
    // FIXME Remove
    // this.ip_form.patchValue({ secondary_dns: '8.8.4.4' });

    this.ip_form.get('ip_assignment_type').valueChanges.subscribe(
      value => {
        console.log('[DEBUG] ip_assignment_type: ', JSON.stringify(value));
        this.staticFormEnable(value);
        this.cdr.detectChanges();
      }
    );
  }

  staticFormEnable(ip_assignment_type: string) {
    if (ip_assignment_type === 'dhcp') {
      this.ip_form.controls.ip.disable();
      this.ip_form.controls.netmask.disable();
      this.ip_form.controls.gateway.disable();
      this.ip_form.controls.primary_dns.disable();
      this.ip_form.controls.secondary_dns.disable();
    } else {
      this.ip_form.controls.ip.enable();
      this.ip_form.controls.netmask.enable();
      this.ip_form.controls.gateway.enable();
      this.ip_form.controls.primary_dns.enable();
      this.ip_form.controls.secondary_dns.enable();
    }
    this.cdr.detectChanges();
  }

  copyToDevice(): void {
    const ip_assignment_type = this.ip_form.get('ip_assignment_type').value;
    console.log('[DEBUG] config saving ip_assignment_type: ', JSON.stringify(ip_assignment_type));
    const bytes_ip_assignment_type = this.btoothService.stringToBytes(ip_assignment_type);
    this.btoothService.write(this.wifiIPAssignmentTypeUUID, bytes_ip_assignment_type);
    if (ip_assignment_type === 'static') {
      console.log('[DEBUG] config saving ip: ', JSON.stringify(this.ip_form.get('ip').value));
      const ip = this.btoothService.stringToBytes(this.ip_form.get('ip').value);
      this.btoothService.write(this.wifiIPUUID, ip);
      console.log('[DEBUG] config saving netmask: ', JSON.stringify(this.ip_form.get('netmask').value));
      const netmask = this.btoothService.stringToBytes(this.ip_form.get('netmask').value);
      this.btoothService.write(this.wifiNetmaskUUID, netmask);
      console.log('[DEBUG] config saving gateway: ', JSON.stringify(this.ip_form.get('gateway').value));
      const gateway = this.btoothService.stringToBytes(this.ip_form.get('gateway').value);
      this.btoothService.write(this.wifiGatewayUUID, gateway);
      console.log('[DEBUG] config saving primary_dns: ', JSON.stringify(this.ip_form.get('primary_dns').value));
      const primary_dns = this.btoothService.stringToBytes(this.ip_form.get('primary_dns').value);
      this.btoothService.write(this.wifiPrimaryDNSUUID, primary_dns);
      console.log('[DEBUG] config saving secondary_dns: ', JSON.stringify(this.ip_form.get('secondary_dns').value));
      const secondary_dns = this.btoothService.stringToBytes(this.ip_form.get('secondary_dns').value);
      this.btoothService.write(this.wifiSecondaryDNSUUID, secondary_dns);
    }
  }

  onFormSubmit(): void {
    this.copyToDevice();
    this.router.navigate(['setup-wifi-config']);
  }
}

ip.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Wi-Fi</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <form [formGroup]="ip_form" (ngSubmit)="onFormSubmit()">
    <ion-list lines="full">
      <ion-list-header>
        <ion-label>IP address</ion-label>
      </ion-list-header>
      <ion-radio-group formControlName="ip_assignment_type">
        <ion-item>
          <ion-label>Automatic (DHCP)</ion-label>
          <ion-radio slot="start" color="secondary" value="dhcp"></ion-radio>
        </ion-item>
        <ion-item>
          <ion-label>Manual (Static)</ion-label>
          <ion-radio slot="start" color="secondary" value="static"></ion-radio>
        </ion-item>
      </ion-radio-group>
      <ion-item>
        <ion-label position="floating">IP address</ion-label>
        <ion-input formControlName="ip" placeholder="192.168.1.100" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <ng-container *ngFor="let validation of validations.ip">
          <div
            *ngIf="ip_form.get('ip_assignment_type').value === 'static' && ip_form.get('ip').hasError(validation.type) && (ip_form.get('ip').dirty || ip_form.get('ip').touched)">
            <ion-icon name="information-circle-outline"></ion-icon>
            <span>{{ validation.message }}</span>
          </div>
        </ng-container>
      </div>
      <ion-item>
        <ion-label position="floating">Netmask</ion-label>
        <ion-input formControlName="netmask" placeholder="255.255.255.0" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <ng-container *ngFor="let validation of validations.netmask">
          <div
            *ngIf="ip_form.get('ip_assignment_type').value === 'static' && ip_form.get('netmask').hasError(validation.type) && (ip_form.get('netmask').dirty || ip_form.get('netmask').touched)">
            <ion-icon name="information-circle-outline"></ion-icon>
            <span>{{ validation.message }}</span>
          </div>
        </ng-container>
      </div>
      <ion-item>
        <ion-label position="floating">Gateway</ion-label>
        <ion-input formControlName="gateway" placeholder="192.168.1.1" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <ng-container *ngFor="let validation of validations.gateway">
          <div
            *ngIf="ip_form.get('ip_assignment_type').value === 'static' && ip_form.get('gateway').hasError(validation.type) && (ip_form.get('gateway').dirty || ip_form.get('gateway').touched)">
            <ion-icon name="information-circle-outline"></ion-icon>
            <span>{{ validation.message }}</span>
          </div>
        </ng-container>
      </div>
      <ion-item>
        <ion-label position="floating">Primary DNS</ion-label>
        <ion-input formControlName="primary_dns" placeholder="8.8.8.8" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <ng-container *ngFor="let validation of validations.primary_dns">
          <div
            *ngIf="ip_form.get('ip_assignment_type').value === 'static' && ip_form.get('primary_dns').hasError(validation.type) && (ip_form.get('primary_dns').dirty || ip_form.get('primary_dns').touched)">
            <ion-icon name="information-circle-outline"></ion-icon>
            <span>{{ validation.message }}</span>
          </div>
        </ng-container>
      </div>
      <ion-item>
        <ion-label position="floating">Secondary DNS</ion-label>
        <ion-input formControlName="secondary_dns" placeholder="8.8.4.4" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <ng-container *ngFor="let validation of validations.secondary_dns">
          <!-- <div class="error-message" -->
          <div
            *ngIf="ip_form.get('ip_assignment_type').value === 'static' && ip_form.get('secondary_dns').hasError(validation.type) && (ip_form.get('secondary_dns').dirty || ip_form.get('secondary_dns').touched)">
            <ion-icon name="information-circle-outline"></ion-icon>
            <span>{{ validation.message }}</span>
          </div>
        </ng-container>
      </div>
      <ion-item>
        <ion-button type="submit" expand="block" fill="outline" [disabled]="!ip_form.valid">Save
        </ion-button>
      </ion-item>
    </ion-list>
  </form>
</ion-content>

Same problem if I comment the Bluetooth stuff entirely and don’t do any patchValue()s.

While you wait for better answers, if the ā€œworkaroundā€ you’re speaking of involves disabling any internal compiler safety checking, I would suggest reverting it and instead addressing the underlying cause seriously. If that involves ditching something like @angular/flex-layout entirely, so be it.

1 Like

Thanks. I had placed a note to follow up later and see what that did or didn’t do. But I mentioned it so that anyone attempting to repeat this would get on with the main issue, repeating output.

I’m now trying the validators using the boilerplate on the Angular example page.

I think there’s a nontrivial chance that it’s part and parcel of the main issue. If you’ve got dependencies that are incompatible with your build environment, I could certainly see that causing mysterious layout failures.

That error wasn’t seen until I ran npm update, and the first reported problem of duplicate code was the same both before the upgrade and after. So I doubt that’s the issue but, I’m not the expert. Wish someone would try my code and validate.

Just used the reference implementation of validators. Also no good. I think I’m going to open a separate thread because the problems now are somewhat different.

Is there a publicly-accessible buildable repo? The bluetooth service looks so tightly coupled with this page. Have you made a mock of the Bluetooth service so that the app can be built and run in a browser environment?

I had noted that the Bluetooth code can be commented out, but I’ll post code in a few mins here with zero BTooth stuff. Yes it runs in a browser, and no weird problems when I do. Also not the Android emulator. So it’s like something with my phone or something.

If it only happens on device, I would try to eliminate all Cordova plugins from the app, and add them back in batches. I bet one of them is broken.

I’m still kind of new. How would I do that? Installed the conference template, then added these only.
npm install cordova-plugin-ble-central
npm install @ionic-native/ble

Do the same in reverse with uninstall instead of install, and use a mock Bluetooth service provider.

Okay this works. I now have a working baseline and need to build off of this. I stripped down the code and created an entirely blank new project, no Bluetooth this time. Next I’ll re-add Bluetooth.

npm install -g @ionic/cli
(Framework: Angular)
(Integrate your new app with Capacitor to target native iOS and Android? (y/N) y)
ionic start blank blank
cd blank
(Pasted the code into home.page.ts etc.)
npm run build
npx cap copy
npx cap add android
npx cap open android

home.page.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Validators, FormGroup, FormBuilder, FormControl } from '@angular/forms';

@Component({
  selector: 'app-ip',
  templateUrl: './home.page.html',
})
export class HomePage implements OnInit {
  public ipForm: FormGroup;

  constructor(
    private formBuilder: FormBuilder,
  ) {
    this.ipForm = this.formBuilder.group({
      ip: new FormControl('192.168.1.100', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      netmask: new FormControl('255.255.255.0', Validators.pattern('^(((255\.){3}(255|254|252|248|240|224|192|128|0+))|((255\.){2}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){2})|((255|254|252|248|240|224|192|128|0+)(\.0+){3}))$')),
      gateway: new FormControl('192.168.1.1', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      primary_dns: new FormControl('8.8.8.8', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
      secondary_dns: new FormControl('8.8.4.4', Validators.pattern('^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$')),
    }, {});
  }

  ngOnInit() { }

  onFormSubmit(): void {
    console.log('ip: ', JSON.stringify(this.ipForm.get('ip').value));
    console.log('netmask: ', JSON.stringify(this.ipForm.get('netmask').value));
    console.log('gateway: ', JSON.stringify(this.ipForm.get('gateway').value));
    console.log('primary_dns: ', JSON.stringify(this.ipForm.get('primary_dns').value));
    console.log('secondary_dns: ', JSON.stringify(this.ipForm.get('secondary_dns').value));
  }
}

home.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>Manual (Static) IP</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <form [formGroup]="ipForm" (ngSubmit)="onFormSubmit()">
    <ion-list lines="full">
      <ion-item>
        <ion-label position="floating">IP address</ion-label>
        <ion-input id="ip" name="ip" formControlName="ip" placeholder="192.168.1.100" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <div *ngIf="ipForm.get('ip').invalid && (ipForm.get('ip').dirty || ipForm.get('ip').touched)">
          <ion-icon name="information-circle-outline"></ion-icon>
          <span *ngIf="ipForm.get('ip').errors.required">IP is required.</span>
          <span *ngIf="ipForm.get('ip').errors.pattern">Must be formatted like 1.2.3.4</span>
        </div>
      </div>
      <ion-item>
        <ion-label position="floating">Netmask</ion-label>
        <ion-input id="netmask" name="netmask" formControlName="netmask" placeholder="255.255.255.0" clearInput
          required>
        </ion-input>
      </ion-item>
      <div>
        <div *ngIf="ipForm.get('netmask').invalid && (ipForm.get('netmask').dirty || ipForm.get('netmask').touched)">
          <ion-icon name="information-circle-outline"></ion-icon>
          <span *ngIf="ipForm.get('netmask').errors.required">Netmask is required.</span>
          <span *ngIf="ipForm.get('netmask').errors.pattern">Must be formatted like 255.255.255.0</span>
        </div>
      </div>
      <ion-item>
        <ion-label position="floating">Gateway</ion-label>
        <ion-input id="gateway" name="gateway" formControlName="gateway" placeholder="192.168.1.1" clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <div *ngIf="ipForm.get('gateway').invalid && (ipForm.get('gateway').dirty || ipForm.get('gateway').touched)">
          <ion-icon name="information-circle-outline"></ion-icon>
          <span *ngIf="ipForm.get('gateway').errors.required">Gateway is required.</span>
          <span *ngIf="ipForm.get('gateway').errors.pattern">Must be formatted like 1.2.3.4</span>
        </div>
      </div>
      <ion-item>
        <ion-label position="floating">Primary DNS</ion-label>
        <ion-input id="primary_dns" name="primary_dns" formControlName="primary_dns" placeholder="8.8.8.8" clearInput
          required>
        </ion-input>
      </ion-item>
      <div>
        <div
          *ngIf="ipForm.get('primary_dns').invalid && (ipForm.get('primary_dns').dirty || ipForm.get('primary_dns').touched)">
          <ion-icon name="information-circle-outline"></ion-icon>
          <span *ngIf="ipForm.get('primary_dns').errors.required">Primary DNS is required.</span>
          <span *ngIf="ipForm.get('primary_dns').errors.pattern">Must be formatted like 1.2.3.4</span>
        </div>
      </div>
      <ion-item>
        <ion-label position="floating">Secondary DNS</ion-label>
        <ion-input id="secondary_dns" name="secondary_dns" formControlName="secondary_dns" placeholder="8.8.4.4"
          clearInput required>
        </ion-input>
      </ion-item>
      <div>
        <div
          *ngIf="ipForm.get('secondary_dns').invalid && (ipForm.get('secondary_dns').dirty || ipForm.get('secondary_dns').touched)">
          <ion-icon name="information-circle-outline"></ion-icon>
          <span *ngIf="ipForm.get('secondary_dns').errors.required">Secondary DNS is required.</span>
          <span *ngIf="ipForm.get('secondary_dns').errors.pattern">Must be formatted like 1.2.3.4</span>
        </div>
      </div>
      <ion-item>
        <ion-button type="submit" expand="block" fill="outline" [disabled]="!ipForm.valid">Save
        </ion-button>
      </ion-item>
    </ion-list>
  </form>
</ion-content>

home.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HomePage } from './home.page';

import { HomePageRoutingModule } from './home-routing.module';


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    HomePageRoutingModule,
    ReactiveFormsModule,
  ],
  declarations: [HomePage]
})
export class HomePageModule { }

Narrowed down the issue to something after Bluetooth scans and connects. Weird why that would affect the HTML.

I’ve seen things like that with uncaught exceptions in unrelated code torpedoing the ordinary refresh logic. Not doubling like this, but definitely broken styling.

1 Like