How do I set ion-select default value from Firebase Firestore?

Help a newbie? I’ve got an ion-select working great getting it’s ion-select-option values and text from Firebase. But I can’t seem to feed it a default with patchValue. If I use a static array of values it works. If I use a static default value with a dynamic ion-select it does not work.

Using Firebase Firestore, Ionic 5, latest Angular, latest AngularFire, latest Chrome, and latest Capacitor.

MAY BE IMPORTANT: When I click the select to drop down, I see the default value appear. This is before I click any values. And it appears on top of the label, so it doesn’t look right. I think ionChange is firing though, because I inserted a console.log() into my onCoopChange() method and it shows it is being called on page load, with an null value on this.selectedCoop.

I believe the id is indexing correctly because if I change the 0 on this line to 1, it indexes the second item in the collection. I see the default value show up when I click the down arrow, overlaid over the label.
this.selectedCoop = coops[0].id;

This is the database schema:

Broken code, TS:

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { FormGroup, FormControl } from '@angular/forms';

export interface Command { command: string; }
export interface Coop {
  id: number;
  name: string;
}

@Component({
  selector: 'app-status',
  templateUrl: './status.page.html',
  styleUrls: ['./status.page.scss'],
})

export class StatusPage implements OnInit {
  coopSelectorForm: FormGroup;
  private coopCollection: AngularFirestoreCollection<Coop>;
  coops: Observable<Coop[]>;
  selectedCoop: number;

  constructor(private afs: AngularFirestore) {
    this.coopCollection = afs.collection<Coop>(
      'coops', ref => ref.orderBy('name'));
    this.coops = this.coopCollection.valueChanges();
    this.coopSelectorForm = new FormGroup({ selectedCoop: new FormControl(), });
    this.coops.subscribe(coops => {
      this.selectedCoop = coops[0].id;
      this.coopSelectorForm.controls['selectedCoop'].patchValue(this.selectedCoop);
    });
  }

  ngOnInit() {
  }

  onCoopChange(value) {
    this.selectedCoop = value.selectedCoop;
  }

Broken code, HTML:

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

<ion-content>
  <form [formGroup]="coopSelectorForm">
    <ion-list>
      <ion-item class="input-item">
        <ion-label position="floating">Coop</ion-label>
        <ion-select interface="popover" formControlName="selectedCoop"
          (ionChange)="onCoopChange(coopSelectorForm.value)">
          <ion-select-option *ngFor="let coop of coops | async" [value]="coop.id">
            {{coop.name}}
          </ion-select-option>
        </ion-select>
      </ion-item>
    </ion-list>
  </form>
</ion-content>

Working static default value, static array, TS:

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { FormGroup, FormControl } from '@angular/forms';

export interface Command { command: string; }
export interface Coop {
  id: number;
  name: string;
}

@Component({
  selector: 'app-status',
  templateUrl: './status.page.html',
  styleUrls: ['./status.page.scss'],
})

export class StatusPage implements OnInit {
  coopSelectorForm: FormGroup;
  private coopCollection: AngularFirestoreCollection<Coop>;
  coops: Array<Coop>;
  selectedCoop: number;

  constructor(private afs: AngularFirestore) {
    this.coopCollection = afs.collection<Coop>(
      'coops', ref => ref.orderBy('name'));
    this.coops = [{ id: 1, name: 'First' }, { id: 2, name: 'Second' }, { id: 3, name: 'Third' }];
    this.coopSelectorForm = new FormGroup({ selectedCoop: new FormControl(), });
    this.selectedCoop = 1;
    this.coopSelectorForm.patchValue({ selectedCoop: this.selectedCoop });
  }

  ngOnInit() {
  }

  onCoopChange(value) {
    this.selectedCoop = value.selectedCoop;
  }

Working static default value, static array HTML:

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

<ion-content>
  <form [formGroup]="coopSelectorForm">
    <ion-list>
      <ion-item class="input-item">
        <ion-label position="floating">Coop</ion-label>
        <ion-select interface="popover" formControlName="selectedCoop"
          (ionChange)="onCoopChange(coopSelectorForm.value)">
          <ion-select-option *ngFor="let coop of coops" [value]="coop.id">
            {{coop.name}}
          </ion-select-option>
        </ion-select>
      </ion-item>
    </ion-list>
  </form>
</ion-content>
this.selectedCoop = coops[0].id;
// Remove This Line. No Need.
// this.coopSelectorForm.controls['selectedCoop'].patchValue(this.selectedCoop);

Use ngModel in html:

<ion-select 
   [(ngModel)]="selectedCoop"
   interface="popover" formControlName="selectedCoop"
   (ionChange)="onCoopChange(coopSelectorForm.value)">
   <ion-select-option *ngFor="let coop of coops" [value]="coop.id">
   {{coop.name}}
   </ion-select-option>
</ion-select>

Btw; if you know default id, you can set property value at startup:

// selectedCoop: number;
selectedCoop = 1;

As i see you don’t post anything so probably you don’t need a form (and formControls).

To extend @LacOniC’s suggestion, it’s very important to have but a single authoritative source of truth for element bindings. This is somewhat challenging because there are so many options, but I have seen case after case where having multiple bindings fighting over the same territory causes confusion and mysterious bugs.

So I don’t care if you want to use [(ngModel)], formControlName or (ionChange), but I would strongly recommend choosing only one of those three.

Thanks for responding @LacOniC but that didn’t work, the default still does not load, now not even when clicking the popover. Only when I re-add patchValue back in, I do see it when I first click to drop down the menu, as before, but not on page load, and it appears in the wrong place, as in the screenshot.

And when I do that, I get the following error. I’m going to next review @rapropos advice as it may resolve this error, and perhaps solve the problem.

It looks like you’re using ngModel on the same form field as formControlName.
Support for using the ngModel input property and ngModelChange event with
reactive form directives has been deprecated in Angular v6 and will be removed
in Angular v7.
For more information on this, see our API docs here:
https://angular.io/api/forms/FormControlName#use-with-ngmodel

Thanks @rapropos, I’d like to use (ionChange) only. However when I do, the default value is still not showing on page refresh and now it even stopped showing anymore as in the screenshot above when I open the menu. I suppose that’s because patchValue() was adding the default value into the form. (But not showing on initial page launch, and then only showing in the wrong place when clicking to open the select.)

I’ll try formControlName and [(ngModel)] next.

There are two halves of a binding for a form element (such as <ion-select>). You can call them @Input / @Output or “controller-to-template” / “template-to-controller” or “model-to-view” / “view-to-model” or whatever works for you. I like “box” and “banana”, from the shape of the template syntax:

[box] = @Input = controller-to-template = model-to-view
(banana) = @Output = template-to-controller = view-to-model

With ngModel, you can put the banana in the box if you want, giving you two-way binding: [(ngModel)]. [formControl] or [formControlName], despite only having box syntax, also do banana binding. (ionChange) is one-way, banana-only. So to get your initial value from the .ts controller into the template, you need some sort of complementary box binding, such as [ngModel] - note no banana in the box this time.

If you’re still reading, that is indeed a viable way of doing things. However, if you are bothering to create a FormControl, I would use it. I prefer [formControl] over [formControlName], but either is fine. But, you may say, the reason I want (ionChange) in the first place is that I need to do something else special when the value changes. No problem, simply subscribe to the valueChanges Observable of the FormControl, which will deliver a kick when the form control’s value changes - without polluting the template with any of it.

1 Like

Do you really need a form? Do you post that value to somewhere else? Due to your example code, i guess not. So i advice just to use ngModel. Then you can use selectedId in an other function or form.

        [(ngModel)]="selectedCoop"
        (ionChange)="onCoopChange($event)"

Thanks for explaining that @rapropos . I will try to understand and implement your suggestions next. I understand you to be saying I want to subscribe to the valueChanges Observable on the FormControl so I’ll try that next. First I implemented @LacOniC suggestions as I grok’d them better, but still no default value. So now to comprehend and implement your suggestions.

While this code it basically functions, still no default value is shown. When I click the menu to pop it down, the default value is shown on top of the label as before. If I remove patchValue() the default value never shows even incorrectly.

Do you see the same behavior?

Here’s a screenshot of the missing default value. But right now, before I choose anything, I see in the console this.selectedCoop 2.
image

It shows here when I click the menu but have not yet changed anything. I can see the default value is indexing properly, as the default is checked.
image

TS

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { FormGroup, FormControl } from '@angular/forms';

export interface Command { command: string; }
export interface Coop {
  id: number;
  name: string;
}

@Component({
  selector: 'app-status',
  templateUrl: './status.page.html',
  styleUrls: ['./status.page.scss'],
})

export class StatusPage implements OnInit {
  coopSelectorForm: FormGroup;
  private coopCollection: AngularFirestoreCollection<Coop>;
  coops: Observable<Coop[]>;
  selectedCoop: number;

  constructor(private afs: AngularFirestore) {
    this.coopCollection = afs.collection<Coop>(
      'coops', ref => ref.orderBy('name'));
    this.coops = this.coopCollection.valueChanges();
    this.coopSelectorForm = new FormGroup({
      selectedCoop: new FormControl(),
    });
    this.coops.subscribe(coops => {
      this.selectedCoop = coops[1].id;
      this.coopSelectorForm.controls['selectedCoop'].patchValue(this.selectedCoop);
    });
  }

  ngOnInit() {
  }

  onCoopChange(value) {
    this.selectedCoop = value.selectedCoop;
    console.log('this.selectedCoop: ');
    console.log(this.selectedCoop);
  }
}

HTML

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

<ion-content>
  <ion-list>
    <ion-item class="input-item">
      <ion-label position="floating">Coop</ion-label>
      <ion-select interface="popover" [(ngModel)]="selectedCoop" (ionChange)="onCoopChange(coopSelectorForm.value)">
        <ion-select-option *ngFor="let coop of coops | async" [value]="coop.id">
          {{coop.name}}
        </ion-select-option>
      </ion-select>
    </ion-item>
  </ion-list>
</ion-content>

Well @rapropos using only [(ngModel)] makes smaller code but no change in the effect of a missing default value on form reload. I didn’t include screenshots because the effect is the same.

Next I will try subscribing to the FormControl ValueChanges.

Do you see the same behavior?

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';

export interface Command { command: string; }
export interface Coop {
  id: number;
  name: string;
}

@Component({
  selector: 'app-status',
  templateUrl: './status.page.html',
  styleUrls: ['./status.page.scss'],
})

export class StatusPage implements OnInit {
  private coopCollection: AngularFirestoreCollection<Coop>;
  coops: Observable<Coop[]>;
  selectedCoop: number;

  constructor(private afs: AngularFirestore) {
    this.coopCollection = afs.collection<Coop>(
      'coops', ref => ref.orderBy('name'));
    this.coops = this.coopCollection.valueChanges();
    this.coops.subscribe(coops => {
      this.selectedCoop = coops[1].id;
    });
  }

  ngOnInit() {
  }
}
<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>
      Status
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item class="input-item">
      <ion-label position="floating">Coop</ion-label>
      <ion-select interface="popover" [(ngModel)]="selectedCoop">
        <ion-select-option *ngFor="let coop of coops | async" [value]="coop.id">
          {{coop.name}}
        </ion-select-option>
      </ion-select>
    </ion-item>
  </ion-list>
</ion-content>

No good. When I added the following to the constructor, the linter immediately complained. Maybe I’m doing it wrong.
this.coopSelectorForm.controls[‘selectedCoop’].valueChanges()

This expression is not callable.
Type ‘Observable’ has no call signatures.ts(2349)

I’m a newbie so take this with a huge grain of salt, but this feels like a bug. I tested on Chrome Windows, Edge (non-Chromium) Windows, and Android, all have same behavior.

This looks too strange to be normal, but I could just be doing something stupid. I’d love it if someone could validate, try it on your own PC.

image

image

image

I don’t use Firebase, and since it doesn’t seem like that’s an integral part of the situation, I’m going to mock it out. This code demonstrates more or less all of the things I’ve been talking about in this thread so far. truck$ is a stand-in for your firebase collection. When the app starts initially, the store is empty and all the fruit is in the warehouse (you don’t have anything from firebase yet).

The “stock store” button causes the fruit to be delivered from the warehouse to the store (your data comes in from firebase). When it’s pressed, you should see:

  • the store get stocked in the “in store” section
  • the “currently eating” select become usable and default to apple (the first fruit in the shipment, as you are doing with your coops)
  • an entry in the “eaten” section appear, because of the valueChanges subscription

Said valueChanges subscription also will add to the history each time you pop the select and choose a fruit, demonstrating how I am suggesting using it to replace your (ionChange) handler.

In a real-world application, truck$ needs to go into a service, not be in the page, and you need to unsubscribe from the subscriptions made in the constructor to avoid resource leaks. I use ngneat/until-destroy for this.

export interface Fruit {
  id: string;
  name: string;
}

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  eating = new FormControl();
  stocked: Fruit[] = [];
  inWarehouse: Fruit[] = [
    {id: "a", name: "apple"},
    {id: "b", name: "banana"},
    {id: "c", name: "cherry"}
  ];
  truck$ = new Subject<Fruit[]>();
  eaten: Fruit[] = [];

  constructor() {
    this.truck$.subscribe(load => {
      this.stocked = load;
      this.eating.setValue(this.stocked[0]);
    });

    this.eating.valueChanges.subscribe(fruit => {
      this.eaten.push(fruit);
    });
  }

  stockStore(): void {
    this.truck$.next(this.inWarehouse);
  }
}
<ion-content>
    <ion-list>
        <ion-item>
            <ion-label>currently eating</ion-label>
            <ion-select [formControl]="eating">
                <ion-select-option *ngFor="let fruit of stocked" [value]="fruit">{{fruit.name}}</ion-select-option>
            </ion-select>
        </ion-item>
        <ion-item button color="primary" (click)="stockStore()">stock store</ion-item>
    </ion-list>

    <fieldset>
        <legend>in store</legend>
        <div [innerText]="stocked | json"></div>
    </fieldset>

    <fieldset>
        <legend>history</legend>
        <div *ngFor="let fruit of eaten">ate a {{fruit.name}}</div>
    </fieldset>
</ion-content>
1 Like

Tested code, I don’t see any default value until I click Stock store, is this intended?

I did something similar in the opening post, using a static array. Works fine. So I think it’s something to do with Firebase; Perhaps, the place where the default value would go gets rendered as blank initially, but it is not re-rendered when this.coops.subscribe() triggers?

Do you have the ability to test setting the default value of a selector against some database, to induce some kind of delay?

Yes, that is intended to simulate the time it takes for your data to come from Firebase.

You are the delay. You pressing the “stock store” ends the delay.

I’m afraid no change in the behavior. Did I code it right to your specification? Only when I click the drop-down does the default value fills in, and in the wrong place, as in the opening post.

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { FormControl } from '@angular/forms';

export interface Coop {
  id: number;
  name: string;
}

@Component({
  selector: 'app-status',
  templateUrl: './status.page.html',
  styleUrls: ['./status.page.scss'],
})

export class StatusPage implements OnInit {
  private coopCollection: AngularFirestoreCollection<Coop>;
  coops: Observable<Coop[]>;
  selectedCoop: number;
  coopSelector = new FormControl();

  constructor(private afs: AngularFirestore) {
    this.coopCollection = afs.collection<Coop>(
      'coops', ref => ref.orderBy('name'));
    this.coops = this.coopCollection.valueChanges();
    this.coops.subscribe(coops => {
      this.selectedCoop = coops[1].id;
      this.coopSelector.setValue(this.selectedCoop);
    });
    this.coopSelector.valueChanges.subscribe(data => {
      console.log('data: ');
      console.log(data);
      this.selectedCoop = data;
    });
  }

  ngOnInit() {
  }
}
<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>
      Status
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item class="input-item">
      <ion-label position="floating">Coop</ion-label>
      <ion-select interface="popover" [formControl]="coopSelector">
        <ion-select-option *ngFor="let coop of coops | async" [value]="coop.id">
          {{coop.name}}
        </ion-select-option>
      </ion-select>
    </ion-item>
  </ion-list>
</ion-content>

I think you’ve created a race condition with multiple subscriptions. Do not subscribe both in the controller and in the template (via the AsyncPipe).

- coops: Observable<Coop[]>;
+ coops: Coop[] = [];

- this.coops = this.coopCollection.valueChanges();
-    this.coops.subscribe(coops => {
-      this.selectedCoop = coops[1].id;
-      this.coopSelector.setValue(this.selectedCoop);
-    });
+ this.coopCollection.valueChanges().subscribe(coops => {
+   this.coops = coops;
+   this.coopSelector.setValue(coops[1]);
+  });

- <ion-select-option *ngFor="let coop of coops | async" [value]="coop.id">
+ <ion-select-option *ngFor="let coop of coops" [value]="coop">

Same exact behavior as in the OP. And we’re sure this isn’t a bug? I am using Ionic 5 and Capacitor, both newish products. Or maybe a bug in AngularFire…

By the way, teach me please. How did this even work? I thought the ion-select required an async so it doesn’t render before data comes in?

I might seek to implement this differently. I saw there is this, but it looked much more complicated for this newbie.

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { FormControl } from '@angular/forms';

export interface Coop {
  id: number;
  name: string;
}

@Component({
  selector: 'app-status',
  templateUrl: './status.page.html',
  styleUrls: ['./status.page.scss'],
})

export class StatusPage implements OnInit {
  private coopCollection: AngularFirestoreCollection<Coop>;
  coops: Coop[];
  selectedCoop: number;
  coopSelector = new FormControl();

  constructor(private afs: AngularFirestore) {
    this.coopCollection = afs.collection<Coop>(
      'coops', ref => ref.orderBy('name'));
    this.coopCollection.valueChanges().subscribe(coops => {
      this.coops = coops;
      this.coopSelector.setValue(coops[1]);
    });
  }

  ngOnInit() {
  }
}
<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-menu-button></ion-menu-button>
    </ion-buttons>
    <ion-title>
      Status
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item class="input-item">
      <ion-label position="floating">Coop</ion-label>
      <ion-select interface="popover" [formControl]="coopSelector">
        <ion-select-option *ngFor="let coop of coops" [value]="coop">
          {{coop.name}}
        </ion-select-option>
      </ion-select>
    </ion-item>
  </ion-list>
</ion-content>

Here is the dependencies stanza of the package.json of the scratch project I’m using to test. Can you make sure your versions of the stuff in mine match the ones in yours?

"dependencies": {
    "@angular/animations": "^9.0.4",
    "@angular/common": "^9.0.4",
    "@angular/core": "^9.0.4",
    "@angular/forms": "^9.0.4",
    "@angular/platform-browser": "^9.0.4",
    "@angular/platform-browser-dynamic": "^9.0.4",
    "@angular/router": "^9.0.4",
    "@capacitor/android": "^1.5.0",
    "@capacitor/core": "1.4.0",
    "@ionic-native/core": "^5.22.0",
    "@ionic-native/network": "^5.22.0",
    "@ionic-native/splash-screen": "^5.22.0",
    "@ionic-native/status-bar": "^5.22.0",
    "@ionic/angular": "^5.0.4",
    "cordova-plugin-network-information": "^2.0.2",
    "core-js": "^2.5.4",
    "rxjs": "~6.5.1",
    "tslib": "^1.11.1",
    "zone.js": "~0.10.2"
  },

Change detection is one of the most important features of Angular. When you change a controller property, it is reflected in the template. That includes <ion-select>. So it needs something sane in order not to puke, but that can be an empty array until the real data comes in.

Huh. I thought I was using the very latest packages but clearly I was mistaken. I have been using a starter kit and have been working inside that – it occurred to me just now that perhaps that is causing the issues I am seeing.

Sorry that didn’t occur to me sooner! I will try the same code inside a new, empty project. (Later today.)

--- "Your dependencies.txt"      2020-02-29 14:47:52.711055300 -0500
+++ "My dependencies.txt"       2020-02-29 14:47:42.497843000 -0500
@@ -1,21 +1,29 @@
-    "@angular/animations": "^9.0.4",
-    "@angular/common": "^9.0.4",
-    "@angular/core": "^9.0.4",
-    "@angular/forms": "^9.0.4",
-    "@angular/platform-browser": "^9.0.4",
-    "@angular/platform-browser-dynamic": "^9.0.4",
-    "@angular/router": "^9.0.4",
+    "@angular/common": "^8.2.1",
+    "@angular/core": "^8.2.1",
+    "@angular/fire": "^5.2.1",
+    "@angular/forms": "^8.2.1",
+    "@angular/platform-browser": "^8.2.1",
+    "@angular/platform-browser-dynamic": "^8.2.1",
+    "@angular/pwa": "~0.802.1",
+    "@angular/router": "^8.2.1",
+    "@angular/service-worker": "^8.2.1",
     "@capacitor/android": "^1.5.0",
-    "@capacitor/core": "1.4.0",
-    "@ionic/angular": "^5.0.4",
-    "@ionic-native/core": "^5.22.0",
-    "@ionic-native/network": "^5.22.0",
-    "@ionic-native/splash-screen": "^5.22.0",
-    "@ionic-native/status-bar": "^5.22.0",
-    "cordova-plugin-network-information": "^2.0.2",
-    "core-js": "^2.5.4",
-    "rxjs": "~6.5.1",
-    "tslib": "^1.11.1",
-    "zone.js": "~0.10.2"
+    "@capacitor/cli": "^1.5.0",
+    "@capacitor/core": "^1.5.0",
+    "@capacitor/ios": "^1.5.0",
+    "@ionic/angular": "5.0.0",
+    "@ngx-translate/core": "^11.0.1",
+    "@ngx-translate/http-loader": "^4.0.0",
+    "angularfire2": "^5.4.2",
+    "angular-pipes": "^9.0.2",
+    "core-js": "^2.5.7",
+    "dayjs": "1.8.0",
+    "firebase": "^7.9.1",
+    "google-libphonenumber": "^3.2.1",
+    "npm": "^6.13.7",
+    "rxjs": "6.5.2",
+    "tslib": "^1.10.0",
+    "videogular2": "6.4.0",
+    "zone.js": "~0.9.1"
+  "dependencies": {
   },
-"dependencies": {