Reputation: 18961
I am developing a simple Angular application and I want to store some data in IndexedDB.
I have a service that initializes private db: IDBDatabase;
in its constructor:
However, when I try to use the initialized DB, by the constructor, in another method call, this DB is never defined. I know that this probably has something to do with async calls, callbacks and promises but I can't figure out what ... so the only solution right now that I can think of is to call
window.indexedDB.open("MyTestDatabase", 1);
everytime
import {Injectable} from '@angular/core';
import {Directory, Bookmark} from "./models";
import {connectableObservableDescriptor} from "rxjs/internal/observable/ConnectableObservable";
@Injectable({
providedIn: 'root'
})
export class BookmarksService {
private db: IDBDatabase;
private directoriesStoreName = "directoreis";
private bookmarksStoreName = "bookmakrs";
constructor() {
console.log("Calling Bookmarks Service Constructor .... ");
if (!window.indexedDB) {
console.log("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.");
}
let openDBRequest: IDBOpenDBRequest = window.indexedDB.open("MyTestDatabase", 1);
console.log("Let's see if the DB will open .. ")
/**
* The only place where a data can be defined is onupgradeneeded callback !
* @param event
*/
openDBRequest.onupgradeneeded = (event: any) => {
console.log("onupgradeneeded fired");
this.db = event.target.result;
};
openDBRequest.onsuccess = (event: any) => {
console.log("seems that db is opened ! ");
console.log(event);
this.db = event.target.result;
this.db.onerror = x => {
console.log("An error occurred while working with DB! ");
}
};
openDBRequest.onerror = event => {
console.log("can't open IndexedDB");
console.log(event);
}
getAllChildDirs(parentId: number): Directory[] {
if (this.db) { // this is NEVER defined, why ?!
var os = this.db.transaction(this.directoriesStoreName).objectStore(this.directoriesStoreName);
var request = os.index("parent_id");
var dirs: Directory[] = [];
request.openCursor(IDBKeyRange.only(parentId)).onsuccess = (event: any) => {
var cursor = event.target.result;
if (cursor) {
// cursor.key is a name, like "Bill", and cursor.value is the whole object.
console.log("Name: " + cursor.key + ", SSN: " + cursor.value);
dirs.push(cursor.value);
cursor.continue();
}
};
return dirs;
}
}
}
Then I have a component like this:
export class DirectoriesListComponent implements OnInit {
@Input() bookmarks: Bookmark[];
@Input() directories: Directory[];
isCollapsed = false;
common: CommonService;
bookmarksService: BookmarksService;
constructor(commons: CommonService, bookmarksService: BookmarksService) {
this.common = commons;
this.bookmarksService = bookmarksService;
}
ngOnInit(): void {
this.directories = this.bookmarksService.getAllRootDirs(); // ALWAYS returns empty array ?! becuase the db is never defined ...
this.bookmarks = this.bookmarksService.getBookmarks();
//listens on when button is clicked to collapse the menu !
this.common.dirsMenuCollapsed.subscribe(val => {
this.isCollapsed = val
})
}
Upvotes: 1
Views: 1487
Reputation: 1914
Actually the problem is that you don't control when the onsucess function will be called as it's a callback, and will be executed sometime that you won't control.
However you can do something to control it, check it out:
Something like this:
import {Injectable} from '@angular/core';
import {Directory, Bookmark} from "./models";
import {connectableObservableDescriptor} from "rxjs/internal/observable/ConnectableObservable";
@Injectable({
providedIn: 'root'
})
export class BookmarksService {
private db: IDBDatabase;
private directoriesStoreName = "directoreis"
private bookmarksStoreName = "bookmakrs"
constructor() {
console.log("Calling Bookmarks Service Constructor .... ")
// :refac: you can even call it from the constructor, but won't wait as the constructor can't wait async functions to be completed
this.initializeDatabase()
}
private async getDatabase(): Promise<IDBDatabase> {
// :refac: Now we create a Promise<IDBDatabase> and wait for it when needed
return new Promise<IDBDatabase>((resolve, reject) => {
//
const openDBRequest: IDBOpenDBRequest = window.indexedDB.open("MyTestDatabase", 1)
console.log("Let's see if the DB will open .. ")
/**
* The only place where a data can be defined is onupgradeneeded callback !
* @param event
*/
openDBRequest.onupgradeneeded = (event: any) => {
console.log("onupgradeneeded fired")
const db = event.target.result
resolve(db)
};
openDBRequest.onsuccess = (event: any) => {
console.log("seems that db is opened ! ");
console.log(event)
const db = event.target.result
db.onerror = x => {
console.log("An error occurred while working with DB! ");
}
resolve(db)
};
openDBRequest.onerror = event => {
console.log("can't open IndexedDB");
console.log(event)
reject()
}
})
}
async initializeDatabase(): Promise<void> {
if (!window.indexedDB)
return console.log("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
else if (!this.db)
this.db = await this.getDatabase()
}
async getAllChildDirs(parentId: number): Promise<Directory[]> {
await this.initializeDatabase()
if (this.db) {
const os = this.db.transaction(this.directoriesStoreName).objectStore(this.directoriesStoreName)
const request = os.index("parent_id")
const dirs: Directory[] = []
request.openCursor(IDBKeyRange.only(parentId)).onsuccess = (event: any) => {
const cursor = event.target.result
if (cursor) {
// cursor.key is a name, like "Bill", and cursor.value is the whole object.
console.log("Name: " + cursor.key + ", SSN: " + cursor.value)
dirs.push(cursor.value)
cursor.continue()
}
}
return dirs
}
}
}
And as you are returning a promise, you should use await when calling it:
async ngOnInit(): Promise<void> {
this.directories = await this.bookmarksService.getAllRootDirs();
this.bookmarks = await this.bookmarksService.getBookmarks();
//listens on when button is clicked to collapse the menu !
this.common.dirsMenuCollapsed.subscribe(val => {
this.isCollapsed = val
})
}
Upvotes: 3