Hotel Search Tutorial
Learn how to build an Angular app with Capacitor that allows users to search and bookmark hotels using data loaded from a Couchbase Lite database. The complete reference app is available here.
Not an Angular developer? Couchbase Lite concepts and syntax are applicable to all web frameworks.
By completing this tutorial, you'll create an app that:
- Loads and stores data, including bookmarked hotels, in a Couchbase Lite database.
- Uses UI components from Ionic: search bar, bookmarks, icons, list items, and more.
- Runs on iOS, Android, and native Windows apps all from the same codebase.
Setup#
Begin by installing the Ionic CLI then creating an Ionic app:
npm install -g @ionic/cliionic startIn this tutorial, we'll choose the "tabs" starter template, but any template will suffice. Next, install Capacitor, Ionic's native runtime tool for building cross-platform web native apps:
# Add Capacitor along with iOS and Android native projectsionic integrations enable capacitornpx capacitor add iosnpx capacitor add androidIntegrate Couchbase Lite#
With the application setup, it's time to add the Couchbase Lite solution:
npm install @ionic-enterprise/couchbase-litenpx cap syncNext, create a model class to represent Hotel data:
// src/app/data/hotels.tsexport class Hotel { id?: number; name: string; address: string; phone: string; bookmarked: boolean = false;}It's recommended to create a service that encapsulates all Couchbase Lite functionality, so create it:
ionic generate service services/databaseWithin database.service.ts, import the Couchbase Lite integration and the new Hotel model:
// src/app/services/database.service.tsimport { Injectable } from '@angular/core';import { Database, DatabaseConfiguration, MutableDocument} from '@ionic-enterprise/couchbase-lite';import { Hotel } from '../models/hotel';
@Injectable({ providedIn: 'root'})export class DatabaseService { }Next, add some private variables to the beginning of the DatabaseService class. database will hold an open connection to the Couchbase Lite database so we can perform operations on it, bookmarkDocument will be used for allowing users to bookmark hotels, and the two DOC_TYPE constants will be used repeatably in database querying.
// src/app/services/database.service.tsexport class DatabaseService { private database: Database; private DOC_TYPE_HOTEL = "hotel"; private DOC_TYPE_BOOKMARKED_HOTELS = "bookmarked_hotels"; private bookmarkDocument: MutableDocument;}Initialize the Database#
Next, create an initialization function that will run every time the app loads, configuring the Couchbase Lite database.
// src/app/services/database.service.tsprivate async initializeDatabase() { await this.seedInitialData();}Next, create a function to seed the Couchbase database with hotel data. First, create then open the Couchbase Lite database. You can set an encryption key to encrypt the database with if you'd like, too. Give the database a name like "travel."
// src/app/services/database.service.tsprivate async seedInitialData() { /* Note about encryption: In a real-world app, the encryption key should not be hardcoded like it is here. One strategy is to auto generate a unique encryption key per user on initial app load, then store it securely in the device's keychain for later retrieval. Ionic's Identity Vault (https://ionic.io/docs/identity-vault) plugin is an option. Using IV’s storage API, you can ensure that the key cannot be read or accessed without the user being authenticated first. */ let dc = new DatabaseConfiguration(); dc.setEncryptionKey('8e31f8f6-60bd-482a-9c70-69855dd02c39'); this.database = new Database("travel", dc); await this.database.open();Finish up the seedInitialData function by inserting hotel data into the database if this is the first time the app has been loaded. Create a new MutableDocument for each hotel record, then insert it into the database.
// src/app/services/database.service.ts, seedInitialData() const len = (await this.getAllHotels()).length; if (len === 0) { const hotelFile = await import("../data/hotels");
for (let hotel of hotelFile.hotelData) { let doc = new MutableDocument() .setNumber('id', hotel.id) .setString('name', hotel.name) .setString('address', hotel.address) .setString('phone', hotel.phone) .setString('type', this.DOC_TYPE_HOTEL); this.database.save(doc); } }}Next, implement the getAllHotels helper function that returns all hotels from the database. The steps are the same for retrieving data from a Couchbase Lite database: create a query, execute it, then parse the results. SELECT * means select all records, FROM _ means from the current database (Couchbase can query multiple databases at once, but since there is only one, _ refers to the main one), WHERE type = '${this.DOC_TYPE_HOTEL}' means where the type property is equal to 'hotel' (database documents are either of type 'hotel' or 'bookmarked_hotels' in our dataset), and ORDER BY name means to return the results in ascending order using the hotel name property.
// src/app/services/database.service.tsprivate async getAllHotels() { const query = this.database.createQuery(`SELECT * FROM _ WHERE type = '${this.DOC_TYPE_HOTEL}' ORDER BY name`); const result = await query.execute(); return await result.allResults();}Display Hotel Data#
With database setup in place, we can retrieve and display hotel data next. Create a public function that the UI will call, getHotels, that calls the above work we just created to initialize the Couchbase Lite database and return the list of all hotels.
// src/app/services/database.service.tspublic async getHotels(): Promise<Hotel[]> { await this.initializeDatabase();
return await this.retrieveHotelList();}Next, create a function that queries the database for all hotel records and transforms them into an array of Hotel objects. Since Couchbase can query multiple databases at once, the array format is a bit unique. Access each hotel record by using hotelResults[key]["_"].
// src/app/services/database.service.tsprivate async retrieveHotelList(): Promise<Hotel[]> { // Get all hotels const hotelResults = this.getAllHotels(); let hotelList: Hotel[] = []; for (let key in hotelResults) { // Couchbase can query multiple databases at once, so "_" is just this single database. // [ { "_": { id: "1", firstName: "Matt" } }, { "_": { id: "2", firstName: "Max" } }] let singleHotel = hotelResults[key]["_"] as Hotel;
hotelList.push(singleHotel); }
return hotelList;}We now have enough of the DatabaseService functionality built to be able to display hotels to the user. Open tab1.page.ts then import the DatabaseService. Retrieve the hotel data when the page first loads.
// src/app/tab1/tab1.page.tsimport { Component } from '@angular/core';import { Hotel } from '../models/hotel';import { DatabaseService } from '../services/database.service';
@Component({ selector: 'app-tab1', templateUrl: 'tab1.page.html', styleUrls: ['tab1.page.scss']})export class Tab1Page { hotels: Hotel[] = []; hotelsDisplayed: Hotel[] = [];
constructor(private databaseService: DatabaseService) {}
async ngOnInit() { this.hotels = await this.databaseService.getHotels(); this.hotelsDisplayed = this.hotels; }}To display the hotels on the tab1.page.html page, use the ion-list component:
<!-- src/app/tab1/tab1.page.html --><ion-header [translucent]="true"> <ion-toolbar> <ion-title>Hotels</ion-title> </ion-toolbar></ion-header>
<ion-content> <ion-header collapse="condense"> <ion-toolbar> <ion-title size="large">Hotels</ion-title> </ion-toolbar> </ion-header> <ion-list> <ion-item *ngFor="let hotel of hotelsDisplayed;"> <ion-label> <h2>{{ hotel.name }}</h2> <p>{{ hotel.address }}</p> <p>{{ hotel.phone }}</p> </ion-label> </ion-item> </ion-list></ion-content>A list of all hotels are now displayed on the page.
Search Hotels#
Now that all hotels are displayed, we can build some interactive features such as search functionality. Open database.service.ts then create a searchHotels function. First, build a N1QL query that searches the Couchbase Lite database for hotels that match the provided hotel name the user types into a search bar. The query is similar to the one that retrieves all hotel data, except for the LIKE comparison operator which performs string wildcard pattern matching comparisons. In this case, we're searching broadly - any hotel name that includes any of the characters the user enters. % here will match zero or more characters.
Finally, return the list of filtered hotels back to the UI layer.
// src/app/services/database.service.tspublic async searchHotels(name): Promise<Hotel[]> { const query = this.database.createQuery( `SELECT * FROM _ WHERE name LIKE '%${name}%' AND type = '${this.DOC_TYPE_HOTEL}' ORDER BY name`); const results = await (await query.execute()).allResults();
let filteredHotels: Hotel[] = []; for (var key in results) { let singleHotel = results[key]["_"] as Hotel;
filteredHotels.push(singleHotel); }
return filteredHotels;}Over in tab1.page.html, add an ion-searchbar search bar component above the ion-list. The ionChange event fires each time the user enters new text into the search bar.
<!-- src/app/tab1/tab1.page.html --><ion-searchbar placeholder="Search Name..." (ionChange)="searchQueryChanged($event.target.value)"></ion-searchbar>Within searchQueryChanged, pass along the hotel name string into the database service. Since hotelsDisplayed represents the array of filtered list of hotels to display to the end user, the UI will update automatically after each character is typed by the user.
// src/app/tab1/tab1.page.tsasync searchQueryChanged(hotelName) { this.hotelsDisplayed = await this.databaseService.searchHotels(hotelName);}With this code in place, the user can successfully filter through hotels.
Bookmark Hotels#
The final feature we'll add is the ability to bookmark hotels that the user is interested in staying at. This includes saving the bookmark data in Couchbase Lite as well as offering the ability to toggle between all hotels and bookmarked hotels in the UI.
Retrieve Bookmark Data#
First, add a call to findOrCreateBookmarkDocument() in the initialize method:
// src/app/services/database.service.tsprivate async initializeDatabase() { await this.seedInitialData();
// Create the "bookmarked_hotels" document if it doesn't exist this.bookmarkDocument = await this.findOrCreateBookmarkDocument();}The bookmarked hotels will be stored in a Couchbase Lite document as an array of hotel ids. Next, implement the method which will retrieve the bookmark document or create it if it doesn't exist.
// src/app/services/database.service.tsprivate async findOrCreateBookmarkDocument(): Promise<MutableDocument> { // Meta().id is a GUID like e15d1aa2-9be3-4e02-92d8-82bd9d05d8e3 const bookmarkQuery = this.database.createQuery( `SELECT META().id AS id FROM _ WHERE type = '${this.DOC_TYPE_BOOKMARKED_HOTELS}'`); const resultSet = await bookmarkQuery.execute(); const resultList = await resultSet.allResults();
let mutableDocument: MutableDocument; if (resultList.length === 0) { mutableDocument = new MutableDocument() .setString("type", this.DOC_TYPE_BOOKMARKED_HOTELS) .setArray("hotels", new Array()); this.database.save(mutableDocument); } else { const docId = resultList[0]["id"]; const doc = await this.database.getDocument(docId); mutableDocument = MutableDocument.fromDocument(doc); }
return mutableDocument;}Next, update the retrieveHotelList() method to toggle the bookmarked hotel property if the hotel's id is found in the bookmarked hotels document. The bookmarked boolean property determines whether to display the hotel as bookmarked in the UI. Here's the complete function:
// src/app/services/database.service.tsprivate async retrieveHotelList(): Promise<Hotel[]> { // Get all hotels const hotelResults = this.getAllHotels();
// Get all bookmarked hotels let bookmarks = this.bookmarkDocument.getArray("hotels") as number[]; let hotelList: Hotel[] = []; for (let key in hotelResults) { // Couchbase can query multiple databases at once, so "_" is just this single database. // [ { "_": { id: "1", name: "Matt" } }, { "_": { id: "2", name: "Max" } }] let singleHotel = hotelResults[key]["_"] as Hotel;
// Set bookmark status singleHotel.bookmarked = bookmarks.includes(singleHotel.id);
hotelList.push(singleHotel); }
return hotelList;}Finally, update the UI to display each hotel's bookmark status. Show a transparent bookmark icon from Ionicons next to each hotel listing. If the hotel has been bookmarked, then show a red filled-in icon.
<!-- src/app/tab1/tab1.page.html --><ion-list> <ion-item *ngFor="let hotel of hotelsDisplayed;"> <ion-label> <h2>{{ hotel.name }}</h2> <p>{{ hotel.address }}</p> <p>{{ hotel.phone }}</p> </ion-label> <ion-icon *ngIf="!hotel.bookmarked" name="bookmark-outline" (click)="toggleBookmark(hotel)"></ion-icon> <ion-icon *ngIf="hotel.bookmarked" name="bookmark" color="danger" (click)="toggleBookmark(hotel)"></ion-icon> </ion-item></ion-list>Now that bookmarked hotel data is being displayed, we can introduce more interactivity.
Save Bookmarked Hotels#
When the user taps on the bookmark icon next to a hotel entry, we need to update the Couchbase Lite database to reflect that state change. First, implement the toggle function to invert the hotel's bookmark status, then save the change using the database service:
// src/app/tab1/tab1.page.tsasync toggleBookmark(hotel) { hotel.bookmarked = !hotel.bookmarked;
if (hotel.bookmarked) { await this.databaseService.bookmarkHotel(hotel.id); } else { await this.databaseService.unbookmarkHotel(hotel.id); }}Next, implement a function to apply the bookmark change to the Couchbase Lite database. Add the hotel id to the array of hotels that are bookmarked:
// src/app/services/database.service.tspublic async bookmarkHotel(hotelId: number) { let hotelArray = this.bookmarkDocument.getArray("hotels") as number[]; hotelArray.push(hotelId); this.bookmarkDocument.setArray("hotels", hotelArray);
this.database.save(this.bookmarkDocument);}Remove Bookmark#
If the user can bookmark a hotel, they should also be able to remove the bookmark. A similar implementation as bookmarking hotels, but instead of adding the hotel id, remove it from the bookmark array.
// src/app/services/database.service.tspublic async unbookmarkHotel(hotelId: number) { let hotelArray = this.bookmarkDocument.getValue("hotels") as number[]; hotelArray = hotelArray.filter(id => id !== hotelId); this.bookmarkDocument.setArray("hotels", hotelArray);
this.database.save(this.bookmarkDocument);}Filter by Bookmarked Hotels#
The last feature to add is the ability to toggle between all hotels and only bookmarked hotels. Begin by creating a variable to hold the state of the bookmark filter toggle and default it to false.
// src/app/tab1/tab1.page.tsexport class Tab1Page { toggleBookmarkFilter: boolean = false; // snip - other variablesNext, implement a function that toggles between bookmark states. Invert the bookmark filter property and check if it's true. If true, filter the hotel list for only those that have the bookmarked property set to true. Refresh the UI to reflect the updated hotel list.
// src/app/tab1/tab1.page.tsasync toggleShowBookmarks() { this.toggleBookmarkFilter = !this.toggleBookmarkFilter;
if (this.toggleBookmarkFilter) { const filtered = this.hotels.filter(h => h.bookmarked == true); this.hotelsDisplayed = filtered; } else { this.hotelsDisplayed = this.hotels; }}Finally, update the UI so that when a bookmark filter icon is tapped, the toggleShowBookmarks() function is called. Display the same bookmark icon also displayed next to each hotel entry.
<!-- src/app/tab1/tab1.page.html --><ion-content [fullscreen]="true"> <ion-header collapse="condense"> <ion-toolbar> <ion-buttons collapse="true" (click)="toggleShowBookmarks()" slot="end"> <ion-button slot="end"> <ion-icon *ngIf="toggleBookmarkFilter" name="bookmarks" size="large" color="danger"></ion-icon> <ion-icon *ngIf="!toggleBookmarkFilter" name="bookmarks-outline" size="large" color="danger"></ion-icon> </ion-button> </ion-buttons> <ion-title size="large">Hotels</ion-title> </ion-toolbar> </ion-header> <!-- snip --></ion-content>Wrap Up#
That's it! You've built an Angular app that allows users to search and bookmark hotels using data loaded from a Couchbase Lite database. Happy app building!