Good talk yesterday at the Firebase Summit about emulators! I was able to get the Functions emulator to work with AngularFire 6. I can't get the Firestore emulator or the Functions emulator to work with AngularFire 7. Here's my app.module.ts:
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { AppComponent } from './app.component';
import { initializeApp,provideFirebaseApp } from '#angular/fire/app';
import { environment } from '../environments/environment';
import { provideFirestore,getFirestore } from '#angular/fire/firestore';
import { USE_EMULATOR as USE_FIRESTORE_EMULATOR } from '#angular/fire/compat/functions';
import { USE_EMULATOR as USE_FUNCTIONS_EMULATOR } from '#angular/fire/compat/functions';
import { FormsModule } from '#angular/forms';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideFirestore(() => getFirestore()),
],
providers: [
{ provide: USE_FIRESTORE_EMULATOR, useValue: environment.useEmulators ? ['localhost', 8080] : undefined },
{ provide: USE_FUNCTIONS_EMULATOR, useValue: environment.useEmulators ? ['localhost', 5001] : undefined }
],
bootstrap: [AppComponent]
})
export class AppModule { }
There's a smell here. I'm initializing Firebase using AngularFire 7 but I'm importing the emulator from AngularFire 6.1.0. Firebase can be initialized with AngularFire 6 or AngularFire 7 but not both, i.e., you can't mix AngularFire 6 and 7.
How do I import the emulators without using AngularFire 6?
In environments.ts I made a property useEmulators:
export const environment = {
firebase: {
projectId: 'my-awesome-project',
appId: '1:234567890:web',
storageBucket: 'my-awesome-project.appspot.com',
apiKey: 'ABCdef',
authDomain: 'my-awesome-project.firebaseapp.com',
messagingSenderId: '0987654321',
},
production: false,
useEmulators: true
};
My Cloud Function runs great in the cloud but doesn't run in the emulators.
Each time I make a change in a Cloud Function, deploy the update to the cloud, wait a minute for the deploy to propagate, test my function, and wait for the logs to show up in the Firebase Console is ten minutes. I'm looking forward to using the emulators to speed up this development cycle.
Here's the rest of my code. I doubt there's anything wrong with these files.
The Cloud Function triggers from writing a message to Firestore, changes the message to uppercase, and writes the uppercase message to a new field in the document.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.uppercaseMe = functions.firestore.document('Triggers/{docId}').onCreate((snap, context) => {
var original = snap.data().message;
functions.logger.log('Uppercasing', context.params.docId, original);
var uppercase = original.toUpperCase();
return snap.ref.set({ uppercase }, { merge: true });
});
The HTML view has a form for submitting a message. It displays the data that was written to Firestore and then displays the results from the Cloud Function.
<form (ngSubmit)="triggerMe()">
<input type="text" [(ngModel)]="message" name="message" placeholder="Message" required>
<button type="submit" value="Submit">Submit</button>
</form>
<div>{{ data$ }}</div>
<div>{{ upperca$e }}</div>
The app.component.ts controller writes the message to Firestore, reads back the message from Firestore, then sets up a document listener to wait for the cloud function to write a new field to the document.
import { Component } from '#angular/core';
import { Firestore, doc, getDoc, collection, addDoc, onSnapshot } from '#angular/fire/firestore';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
data$: any;
docSnap: any;
message: string | null = null;
upperca$e: string | null = null;
unsubMessage$: any;
constructor(public firestore: Firestore) {}
async triggerMe() {
try {
// write to Firestore
const docRef = await addDoc(collection(this.firestore, 'Triggers'), {
message: this.message,
});
this.message = null; // clear form fields
// read from Firestore
this.docSnap = await getDoc(doc(this.firestore, 'Triggers', docRef.id));
this.data$ = this.docSnap.data().message;
// document listener
this.unsubMessage$ = onSnapshot(doc(this.firestore, 'Triggers', docRef.id), (snapshot: any) => {
this.upperca$e = snapshot.data().uppercase;
});
} catch (error) {
console.error(error);
}
}
}
Firebase emulators work independently of Angular or other apps! I reread the documentation and learned that you just spin up the emulators,
firebase emulators:start
open your browser to http://localhost:4000, and you can write data in Firestore and then see the results of your function appear in Firestore. You can also read the logs. This only works with triggerable functions, not with callable functions.
Amazing what you can learn by reading the documentation. :-)
Related
I have created a very simple custom validator for a simple form control, e.g. MatInput, which would always return non-null e.g. invalid. I hav also added one of the pre-built validators e.g. required. When I start my app I can see that status = INVALID and errors.required = true.
Once I start typing, I expected that status will remain INVALID and errors.myError = true, but this does not happen. What am I doing wrong? I have built my example on StackBlitz. I have also added the contents on my TS & HTML files below
TS
import { Component } from '#angular/core';
import { AbstractControl, FormControl, ValidationErrors, ValidatorFn, Validators} from '#angular/forms';
export function myValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return { "myError": true };
};
}
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = new FormControl('', [Validators.required, myValidator]);
}
HTML
<label>
Name:
<input type="text" [formControl]="name">
</label>
{{ name | json }}
I am quite new to Angular and I am not sure how to debug this. What can I try next?
TL;DR:
export class AppComponent {
name = new FormControl('', [Validators.required, myValidator()]);
}
Explanation:
myValidator is not being called, so you are not getting the ValidatorFn
I installed the Vuex-ORM Graphql Plugin into an existing Nuxt project with Laravel/GraphQL API, so that I could try avoiding using the Apollo Cache. In one of my components though, I'm running:
<script>
import Notification from '~/data/models/notification';
export default {
computed: {
notifications: () => Notification.all()
},
async mounted () {
await Notification.fetch();
}
}
</script>
however I'm receiving the error [vuex] unknown action type: entities/notifications/fetch.
I looked through the debug log and found several available getters (entities/notifications/query, entities/notifications/all, entities/notifications/find, and entities/notifications/findIn). I tried running await Notification.all() in the mounted method which removed the error, however looking in Vuex the Notifications data object is empty.
Here is the rest of my setup:
nuxt.config.js
plugins: [
'~/plugins/vuex-orm',
'~/plugins/graphql'
],
plugins/vuex-orm.js
import VuexORM from '#vuex-orm/core';
import database from '~/data/database';
export default ({ store }) => {
VuexORM.install(database)(store);
};
plugins/graphql.js
/* eslint-disable import/no-named-as-default-member */
import VuexORM from '#vuex-orm/core';
import VuexORMGraphQL from '#vuex-orm/plugin-graphql';
import { HttpLink } from 'apollo-link-http';
import fetch from 'node-fetch';
import CustomAdapter from '~/data/adapter';
import database from '~/data/database';
// The url can be anything, in this example we use the value from dotenv
export default function ({ app, env }) {
const apolloClient = app?.apolloProvider?.defaultClient;
const options = {
adapter: new CustomAdapter(),
database,
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};
if (apolloClient) {
options.apolloClient = apolloClient;
} else {
options.link = new HttpLink({ uri: options.url, fetch });
}
VuexORM.use(VuexORMGraphQL, options);
};
/data/adapter.js
import { DefaultAdapter, ConnectionMode, ArgumentMode } from '#vuex-orm/plugin-graphql';
export default class CustomAdapter extends DefaultAdapter {
getConnectionMode () {
return ConnectionMode.PLAIN;
}
getArgumentMode () {
return ArgumentMode.LIST;
}
};
/data/database.js
import { Database } from '#vuex-orm/core';
// import models
import Notification from '~/data/models/notification';
import User from '~/data/models/user';
const database = new Database();
database.register(User);
database.register(Notification);
export default database;
/data/models/user.js
import { Model } from '#vuex-orm/core';
import Notification from './notification';
export default class User extends Model {
static entity = 'users';
static eagerLoad = ['notifications'];
static fields () {
return {
id: this.attr(null),
email: this.string(''),
first_name: this.string(''),
last_name: this.string(''),
// relationships
notifications: this.hasMany(Notification, 'user_id')
};
}
};
/data/models/notification.js
import { Model } from '#vuex-orm/core';
import User from './user';
export default class Notification extends Model {
static entity = 'notifications';
static fields () {
return {
id: this.attr(null),
message: this.string(''),
viewed: this.boolean(false),
// relationships
user: this.belongsTo(User, 'user_id')
};
}
};
package.json
"#vuex-orm/plugin-graphql": "^1.0.0-rc.41"
So in a Hail Mary throw to get this working, I ended up making a couple of changes that actually worked!
If other people come across this having similar issues, here's what I did...
In my nuxt.config.js, swapped the order of the two plugins to this:
plugins: [
'~/plugins/graphql',
'~/plugins/vuex-orm',
],
In my graphql.js plugin, I rearranged the order of the options to this (database first, followed by adapter):
const options = {
database,
adapter: new CustomAdapter(),
url: env.NUXT_ENV_BACKEND_API_URL,
debug: process.env.NODE_ENV !== 'production'
};
I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.
Tested with Android sdk emulator and on real device.
AppComponent
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';
import { AuthenticationService } from './authentication.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
public user$: Observable<any>;
private subscriptions: Subscription[] = [];
constructor(private appService: AppService, private authenticationService: AuthenticationService) {}
public ngOnInit(): void {
this.user$ = this.authenticationService.user$;
this.subscriptions.push(
this.authenticationService.user$.subscribe((user: any) => {
console.log('user', !!user);
})
);
}
public ngOnDestroy(): void {
if (this.subscriptions) {
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
async signIn() {
await this.appService.signIn();
}
async signOut() {
await this.appService.signOut();
}
}
AuthenticationService
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
public user$: Observable<any>;
constructor(private appService: AppService) {
this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
}
}
AppService
import { Injectable, NgZone } from '#angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const user$ = new BehaviorSubject<User>(null);
#Injectable({
providedIn: 'root',
})
export class AppService {
constructor(private ngZone: NgZone) {
addAuthStateListener({
onAuthStateChanged: ({ user }) => {
this.ngZone.run(() => {
user$.next(user);
});
},
});
}
public authState(): Observable<User> {
return user$.asObservable().pipe(distinctUntilChanged());
}
async signIn() {
return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
(error: string) => {
throw {
message: error,
};
}
);
}
signOut() {
logout();
}
}
ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.
If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.
If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.
Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.
const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));
timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions
Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:
subscription.add(() => console.log(`everybody's done.`));
One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)
<page-router-outlet
(loaded)="outletLoaded($event)"
(unloaded)="outletUnLoaded($event)"></page-router-outlet>
Then you cn use outletLoaded(args: EventData) {} to initialise your code
respectively outletUnLoaded to destroy your subscriptions.
This is helpful in cases where you have access to the router outlet (in App Component for instance)
In case when you are somewhere inside the navigation tree you can listen for suspend event
Application.on(Application.suspendEvent, (data: EventData) => {
this.backFromBackground = true;
});
Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.
It works pretty well for me.
Hope that help you as well.
I am unable to get the nativescript-videorecorder to work in my app.
I had installed the plugin and used the code given on the github https://github.com/triniwiz/nativescript-videorecorder for typescript. I created a simple HTML ui and tried to call a function to open the camera to record video. However it is does not work
For app.component.html i used the tag <Image src="videoCam" (tap)="onCam()">
For app.component.ts i used the below code
import { VideoRecorder, Options as VideoRecorderOptions } from 'nativescript-videorecorder';
#Component({
moduleId: module.id,
selector: "ns-app",
templateUrl: "app.component.html"
})
export class AppComponent {
export class AppComponent {
onCam() {
const options: VideoRecorderOptions = {
hd: true
saveToGallery: true
};
const videorecorder = new VideoRecorder(options);
videorecorder.record().then((data) => {
console.log(data.file)
}).catch((err) => {
console.log(err)
});
}
}
Is there anything i am missing to get this to work ?
I have an app I have inherited that is getting data from an API endpoint. We have found that when we change data on the API, the changes are not reflected in the app. If we uninstall and re-install the app on a mobile device, then the new data from the API is displayed. Here is an example of the Building Detail page:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from "#angular/router";
import { switchMap } from 'rxjs/operators';
import { Building } from "../shared/building/building";
import { HttpService } from "../services/http/http.service";
import {
getString,
setString
} from "application-settings";
#Component({
moduleId: module.id,
selector: 'building-detail',
templateUrl: 'building-detail.component.html',
styleUrls: ["./building-detail-common.css"],
providers: [ Building, HttpService ]
})
export class BuildingDetailComponent implements OnInit {
paramName: string;
constructor(
private route: ActivatedRoute,
public building: Building,
private httpService: HttpService) {
this.route.params.subscribe(
(params) => {
this.paramName = params['name']
}
);
}
ngOnInit() {
console.log("ON INIT FIRED " + this.paramName);
let buildingInfo = JSON.parse(getString("buildingInfo"));
for (let item of buildingInfo) {
if (item.attributes.title === this.paramName) {
this.building.name = item.attributes.title;
this.building.desc = item.attributes.body.value;
let imageEndpoint = "file/file/" + item.relationships.field_building_image.data.id;
let imageUrl = this.httpService.getData(imageEndpoint)
.subscribe(data => {
this.building.image = "https://nav.abtech.edu" + data['data'].attributes.url;
console.log("The building image URL is " + this.building.image);
}, (error) => {
console.log("Error is " + error);
});
}
}
}
}
I am happy to share other files/code if you would like to look at those. Thanks!
The reason your data is not being updated is not because the ngOnInit is not being executed, it's because you're caching the old value and reloading it each time the app is run. You're caching the data persistently across app runs with appSettings and that's why you are seeing the values stay the same until you uninstall.
If you don't want to show a cached value then don't read from the app settings, or at least don't read from appSettings until you've refreshed the data once.
ngOnInit is something that is executed only when your component is created, it will never be executed again.
Also there is difference between app launch and resume, if you want to update data every time when user opens the app, you should listen to resume event and perform apis calls inside ngZone
You may even use push notification / data message if you want to notify user immediately when data changes on backend