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