Quantcast
Viewing all 183 articles
Browse latest View live

From React Web to Native Mobile App with Capacitor & Ionic

You can build a native mobile app from your React app without even using React Native. Sounds crazy?

All of this is possible due to the advancements in web technology. Just like Electron claimed its space as the default to build desktop applications with web code, you can use Capacitor to do the same for mobile apps!

In this tutorial we will create a simple React app with a dummy login page and inside page and then convert it into a native mobile application using Capacitor.

Image may be NSFW.
Clik here to view.
react-capacitor-app-ionic

The fascinating thing is that the process takes like 10 minutes, and you open up your app (and your developer career) to infinite possibilities!

Prefer watching videos? Here’s the tutorial on my YouTube channel (to which you should definitely subscribe if you haven’t):

Want to continue reading? Here we go with our React mobile app!

App Setup

Let’s start with a pretty basic React app using create-react-app and the typescript template:

npx create-react-app capacitor-app --template typescript
cd ./capacitor-app

npm i react-router-dom

The only additionally dependency we need right now is the router to easily navigate to another page.

Now add an empty folder at src/routes and create two new files at:

  • src/routes/Dashboard.tsx
  • src/routes/Login.tsx

You can fill both of them with some placeholder code like this:

function Dashboard() {


  return (
    <>
      Dashboard
    </>
  );
}

export default Dashboard;

Follow the same scheme for the login page and rename the keywords to Login!

With our two pages in place we can define the routes for the React router inside of our src/index.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
} from "react-router-dom";
import Login from './routes/Login';
import Dashboard from './routes/Dashboard';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />}>
          <Route path="" element={<Login />} />
          <Route path="/app/dashboard" element={<Dashboard/>} />
          <Route
            path="*"
            element={
              <main style={{ padding: "1rem" }}>
                <p>There's nothing here!</p>
                <Link to="/">Back home!</Link>
              </main>
            }
          />
        </Route>
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);

Let me explain what we did within the BrowserRouter:

  • Defined a route / to use the App component
  • Added all other routes below this so the router will always load the app file first and then resolve the rest of the path
  • If no other path component is present the Login will be rendered, and for /app/dashboard the Dashboard will be used
  • If no path match we have a fallback HTML code right in there, but we could easily supply our own 404 page here

For the fallback page we are using the wildcard path and also directly use the Link element from the React router so it triggers the right route of our app!

Additionally we need to add an Outlet somewhere in our view so the React router can render the actual elements within.

Because the App element is the parent of all routes, we can do it in there so bring up the src/App.tsx and change it to:

import './App.css';
import { Outlet } from "react-router-dom";

function App() {
  return (
    <div style={{ margin: "1rem" }}>
    <h1>Simons App</h1>
    <Outlet />
  </div>
  );
}

export default App;

Now we have some header text and under it the routes would be rendered when navigating to the URL.

At this point you should be able to serve your application by running:

npm run start

You can see the default page being the login, and you can also directly navigate to http://localhost:3000/app/dashboard to see the second page.

Off to a great start, now let’s implement a dummy login flow.

Creating a Simple React Login

Because we don’t really have a backend we gonna keep things easy and just fake a loading of our login and then use the useNavigate hook to route to our dashboard from code.

Besides that we can put in useState to show or hide a loading indicator while we are “submitting” our form.

Go ahead and change the src/routes/Login.tsx to this now:

import { useState } from "react";
import { useNavigate } from "react-router-dom";

function Login() {
  const [loading, setLoading] = useState(false);
  const navigate = useNavigate();

  const onSubmit = (event: any) => {
    event.preventDefault();
    setLoading(true);

    console.log("submit!");

    setTimeout(() => {
      setLoading(false);
      navigate("/app/dashboard");
    }, 1500)
  };

  return (
    <main style={{textAlign: "center"}}>
      <h2>Login</h2>
      <form onSubmit={onSubmit}>
        <div style={{display: "flex", flexDirection: "column", alignItems: "center", gap: "10px"}}>
          <div>
            <label>Email:</label>
            <input type="text" ></input>
          </div>

          <div>
            <label>Password:</label>
            <input type="password"></input>
          </div>

          {loading ? <div>Loading...</div> :
          <button type="submit">Login</button>
        }
        </div>
      </form>
    </main>
  );
}

export default Login;

Not sure about those hooks? Check out the useState docs to learn more about it!

On our dashboard page we will simply render a different title and button to go back, so bring up the src/routes/Dashboard.tsx and quickly change it to:

import { useNavigate } from "react-router-dom";

function Dashboard() {
  const navigate = useNavigate();

  const onLogout = () => {
    navigate('/');
  };

  return (
    <>
      <h2>Dashboard</h2>
      <button onClick={onLogout}>Logout</button>
    </>
  );
}

export default Dashboard;

Nothing really new in here!

Image may be NSFW.
Clik here to view.
react-login-page

I know, probably not the best login page you’ve ever seen. More like the worst.

At this point we are able to navigate around and finished our most basic example of a React login page. Now let’s see how fast we can build a mobile app from this.

Installing Capacitor

To wrap any web app into a native mobile container we need to take a few steps – but those have to be done just one initial time, later it’s as easy as running a sync command.

First of all we can install the Capacitor CLI locally and then initialise it in our project.

Now we need to install the core package and the respective packages for the iOS and Android platform.

Finally you can add the platforms, and that’s it!

# Install the Capacitor CLI locally
npm install @capacitor/cli --save-dev
 
# Initialize Capacitor in your React project
npx cap init
 
# Install the required packages
npm install @capacitor/core @capacitor/ios @capacitor/android
 
# Add the native platforms
npx cap add ios
npx cap add android

At this point you should see a new ios and android folder in your React project.

Those are real native projects!

To open the Android project later you should install Android Studio, and for iOS you need to be on a Mac and install Xcode.

Additionally you should see a capacitor.config.ts in your project, which holds some basic settings for Capacitor that are used during sync. The only thing you need to worry about is the webDir which should point to the output of your build command, but usually this should already be correct.

Give it a try by running the following commands:

npm run build
npx cap sync

The first command will simply build your React project, while the second command will sync all the web code into the right places of the native platforms so they can be displayed in an app.

Additionally the sync command might update the native platforms and install plugins to access native functionality like camera, but we are not going that far today.

Without noticing you are now actually done, so let’s see the app on a device!

Build and Deploy native apps

You now need Xcode for iOS and Android Studio for Android apps on your machine. Additionally you need to be enrolled in the Apple Developer Program if you want to build and distribute apps on the app store, and same for the Google Play Store.

If you never touched a native mobile project, you can easily open both native projects by running:

npx cap open ios
npx cap open android

Inside Android Studio you now just need to wait until everything is ready, and you can deploy your app to a connected device without changing any of the settings!

Image may be NSFW.
Clik here to view.
android-studio-deploy-angular

Inside Xcode it’s almost the same, but you need to setup your signing account if you wan to deploy your app to a real device and not just the simulator. Xcode guides you through this if you’ve never done it (but again, you need to be enrolled in the Developer Program).

After that it’s as easy as hitting play and run the app on your connected device which you can select at the top!
Image may be NSFW.
Clik here to view.
xcode-deploy-app-angular

Congratulations, you have just deployed your React web app to a mobile device!

Image may be NSFW.
Clik here to view.
react-app-wrong-size

But there are still some challenges ahead, especially on iOS the UI doesn’t look good yet. Before we fix that, let’s make our debugging process faster.

Capacitor Live Reload

By now you are used to have live reload with all modern frameworks, and we can have the same functionality even on a mobile device with minimum effort!

The idea is to make your locally served app with live reload available on your network, and the Capacitor app will simply load the content from that URL.

First step is figuring out your local IP, which you can get on a Mac by running:

ipconfig getifaddr en0

On Windows, run ipconfig and look for the IPv4 address.

Now we only need to tell Capacitor to load the app directly from this server, which we can do right in our capacitor.config.ts with another entry:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'capacitor-app',
  webDir: 'build',
  bundledWebRuntime: false,
  server: {
    url: 'http://192.168.x.xx:3000',
    cleartext: true
  },
};

export default config;

Make sure you use the right IP and port, I’ve simply used the default React port in here.

To apply those changes we can now copy over the changes to our native project:

npx cap copy

Copy is mostly like sync, but will only copy over the changes of the web folder and config, not update the native project.

Now you can deploy your app one more time through Android Studio or Xcode and then change something in your React app – the app will automatically reload and show the changes!

Caution: If you install new plugins like the camera, this still requires a rebuild of your native project because native files are changed which can’t be done on the fly.

Fix the Mobile UI of React

Now we can tackle the open issues of our mobile app more easily.

To begin with, we need to change a meta tag inside our public/index.html and also include viewport-fit:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

By doing this we can now access an environment variable that gives us information about the iOS notch area at the top which you usually need to keep empty.

Therefore bring up the src/App.tsx and include the value as paddingTop for our wrapper element:

import './App.css';
import { Outlet } from "react-router-dom";

function App() {
  return (
    <div style={{ margin: "0 1rem 0 1rem", paddingTop: "env(safe-area-inset-top)" }}>
    <h1>Simons App</h1>
    <Outlet />
  </div>
  );
}

export default App;

Now we got the padding right in our app, and if you have built a responsive good looking React app before you are pretty much done at this point!

However, if you also want to test a simply way to add adaptive mobile styling to our React app then we can take things a step further.

Using Ionic UI Components with React

I’ve worked years with Ionic to build awesome cross platform applications and it’s one of the best choices if you want a really great looking mobile UI that adapts to iOS and Android specific styling.

To use it, we only need to install the Ionic react package now:

npm install @ionic/react

Additionally Ionic usually ships with Ionicons, a great icon library that again adapts to the platform it’s running on. Let’s also add it by running:

npm install ionicons

In order to use the styling of Ionic we now need to change our src/App.tsx and include:

/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css'; // Remove if nothing is visible
import '@ionic/react/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
import { setupIonicReact } from '@ionic/react';

setupIonicReact();

To use Ionic components we need to import them one by one from the package we just installed and use them instead of the div elements we had before.

Learning the Ionic syntax for all of those components takes time, but I’m running an online school called Ionic Academy to help developers learn Ionic in the fastest possible way!

In our case we can re-style our login page to hold some items, labels and input fields and a button with one of those Ionicons.

Additionally there are even some hooks included like useIonAlert or useIonLoading which allow us to call certain Ionic overlays or components directly from code!

This means we can now easily display a loading screen calling present() and showing a native alert modal using alert() with the according values for the elements within the alert.

Go ahead now and change the src/routes/Login.tsx to this:

import { IonButton, IonCard, IonCardContent, IonIcon, IonInput, IonItem, IonLabel, useIonAlert, useIonLoading } from "@ionic/react";
import { useNavigate } from "react-router-dom";
import { logIn } from 'ionicons/icons';

function Login() {
  const navigate = useNavigate();
  const [alert] = useIonAlert();
  const [present, dismiss] = useIonLoading();

  const onSubmit = async (event: any) => {
    event.preventDefault();
    await present({message: 'Loading...'})
    
    setTimeout(() => {
      dismiss();
      if(Math.random() < 0.5) {
        alert({
          header: 'Invalid credentials',
          message: 'There is no user with that name and password.',
          buttons: [{text: 'Ok'}]
        })
      } else {
        navigate("/app/dashboard");
      }
    }, 1500)
  };

  return (
    <>
      <IonCard>
        <IonCardContent>
          <form onSubmit={onSubmit}>
            <IonItem>
              <IonLabel position="floating">Email</IonLabel>
              <IonInput type="email"></IonInput>
            </IonItem>

            <IonItem>
              <IonLabel position="floating">Password</IonLabel>
              <IonInput type="password"></IonInput>
            </IonItem>

            <div className="ion-margin-top">
            <IonButton expand="full" type="submit" color="secondary">
                <IonIcon icon={ logIn } slot="start"/>
                Login</IonButton>
            </div>
          </form>
        </IonCardContent>
      </IonCard>
    </>
  );
}

export default Login;

With a little random factor included we can now sometimes see the alert or move to the (yet unstyled) dashboard of our React app!

Image may be NSFW.
Clik here to view.
react-ionic-alert

All of this took us just a few minutes of setup time, and we could now already roll out this app to the native app stores.

Conclusion

Going from a React app to a native mobile app for iOS and Android can be that easy – no need to use React Native and learn a ton of new libraries.

If you now bring the good old performance argument, please stop and take a look at the Ionic vs React Native performance comparison first.

With Capacitor you are betting on web technology, so you benefit from all advancements made in that area and improvements to mobile web views and devices in general.

The post From React Web to Native Mobile App with Capacitor & Ionic appeared first on Devdactic - Ionic Tutorials.


Angular Material Mobile App with Capacitor

Angular Material is an awesome library of Material Design components for Angular apps, and with its sidenav plus the help of Capacitor we can easily build a native mobile app from one codebase!

In this tutorial we will go through all the steps of setting up an Angular project and integrating Angular Material. We will create a responsive navigation which shows a side menu on smaller screen and a regular top navigation bar on desktops screens.

Image may be NSFW.
Clik here to view.
angular-material-capacitor

Finally we will install Capacitor to quickly build a native iOS and Android app from our Angular app.

This means we are able to cover 3 different platforms all from one codebase!

Creating an Angular Material Sidenav

Let’s begin with a new Angular project. After creating the project you can use a schematic to add all required packages and changes to our project for using Angular Material.

You can basically select yes for everything during that wizard, and afterwards run another schematic from Angular Material which automatically bootstraps the basic sidenav component for us!

# Start a new app
ng new materialApp --routing --style=scss
cd ./materialApp

# Schematic to add Angular Material
ng add @angular/material

# Generate a component with sidenav
ng generate @angular/material:navigation app-navigation

When we are done, we can change our src/app/app.component.html as we want to display our new component instead of all the dummy code that default app comes with:

<app-app-navigation></app-app-navigation>

This will load our component instead, and now it’s a good time to take a look at the general setup of the sidenav:

  • The whole code is surrounded by the mat-sidenav-container component
  • The actual side menu that we can display is inside mat-sidenav
  • Everything inside mat-sidenav-content is the actual main area which later shows our different pages

If you run the app right now you will see the side menu even on bigger screens, but usually you will use a top navigation in that case (although some websites also use a side menu).

To achieve this behaviour, we will change the src/app/app-navigation/app-navigation.component.html and remove the opened property from the mat-sidenav:

<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'"
  >
    <mat-toolbar>Menu</mat-toolbar>
    <mat-nav-list>
      <a mat-list-item href="#">Link 1</a>
      <a mat-list-item href="#">Link 2</a>
      <a mat-list-item href="#">Link 3</a>
    </mat-nav-list>
  </mat-sidenav>

  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
        *ngIf="isHandset$ | async"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      <span>My App</span>
    </mat-toolbar>
    <!-- Add Content Here -->
    MY CONTENT HERE
  </mat-sidenav-content>
</mat-sidenav-container>

If you now run the app you should only see the menu icon on smaller screens and otherwise just the toolbar with our app title at the top!

This page automatically comes with an isHandset Observable that you can find inside the src/app/app-navigation/app-navigation.component.ts, and it uses the Angular BreakpointObserver to emit if we have reached a “handset” device size.

Since this is an Observable, all occurrences to the isHandset variable use the Angular async pipe to subscribe to it.

Adding Navigation with different Routes

So far we only have this one page, now it’s time to add more pages and we start by generating some components, one even using another schematic from Angular Material to setup a nice little dashboard:

ng generate @angular/material:dashboard dashboard
ng generate component about
ng generate component error

Now we need to reference the different components and creating the according routing entries so the Angular router can resolve a URL to a specific component.

For this, open the src/app/app-routing.module.ts and change it to:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AboutComponent } from './about/about.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { ErrorComponent } from './error/error.component';

const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: '',
        component: DashboardComponent,
      },
      {
        path: 'about',
        component: AboutComponent,
      },
      {
        path: '404',
        component: ErrorComponent,
      },
      {
        path: '**',
        redirectTo: '404',
      },
    ],
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

We have now different routes in our app:

  • / will open the Dashboard
  • /about will open About
  • /404 will open the Error page
  • Everything else will be redirected to the error page

If you’d like to show something like a login before these pages, take a look at my login app template with Ionic!

Right now the routing doesn’t work because we haven’t added the router-outlet in any place of our app.

The best place for it is actually towards the bottom of our sidenav where we have a comment by default, so bring up the
src/app/app-navigation/app-navigation.component.html and insert:

<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'"
  >
    <mat-toolbar color="primary">Menu</mat-toolbar>
    <mat-nav-list>
        <!-- TODO -->
    </mat-nav-list>
  </mat-sidenav>

  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
        *ngIf="isHandset$ | async"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      <span>My App</span>
      <!-- TODO -->
    </mat-toolbar>

    <!-- Add Content Here -->
    <router-outlet></router-outlet>
    
  </mat-sidenav-content>
</mat-sidenav-container>

Now you should be able to directly navigate to the different routes that we created before!

Image may be NSFW.
Clik here to view.
angular-material-dashboard

But of course we need the navigation links in our menu and nav bar as well, so we need to change the file again.

This time we add the routerLink items in two places.

In the mat-sidenav we also add a click handler to directly close the sidenav menu when we select an entry using the drawer template reference.

In the full screen navigation bar we add a space element so we can move the buttons to the right hand side using the flexbox layout later!

Go ahead and change the src/app/app-navigation/app-navigation.component.html again:

<mat-sidenav-container class="sidenav-container">
  <mat-sidenav
    #drawer
    class="sidenav"
    [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
    [mode]="(isHandset$ | async) ? 'over' : 'side'"
  >
    <mat-toolbar color="primary">Menu</mat-toolbar>
    <mat-nav-list>
      <a
        mat-list-item
        routerLink="/"
        routerLinkActive="active-link"
        [routerLinkActiveOptions]="{ exact: true }"
        (click)="drawer.toggle()"
        >Dashboard</a
      >
      <a
        mat-list-item
        routerLink="/about"
        routerLinkActive="active-link"
        (click)="drawer.toggle()"
        >About</a
      >
    </mat-nav-list>
  </mat-sidenav>

  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="drawer.toggle()"
        *ngIf="isHandset$ | async"
      >
        <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
      </button>
      <span>My App</span>
      <span class="spacer"></span>
      <div [class.hidden]="isHandset$ | async">
        <a
          mat-button
          routerLink="/"
          routerLinkActive="active-link"
          [routerLinkActiveOptions]="{ exact: true }"
          >Dashboard</a
        >
        <a mat-button routerLink="/about" routerLinkActive="active-link"
          >About</a
        >
      </div>
    </mat-toolbar>
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

Additional all buttons and items for routing will receive the active-link CSS class when the route becomes active, and to make sure our dashboard path isn’t activated for every possible page we apply exact match to the routerLinkActiveOptions!

Finally our top navigation is wrapped inside a div which will be hidden when we reach a the handset device width – at that point we have our burger menu and don’t need those items anyway.

Now we just need to add the missing classes and comment out one initial class since this would overwrite the stylig of the toolbar inside the menu. Therefore bring up the src/app/app-navigation/app-navigation.component.scss and change it to:

.sidenav-container {
  height: 100%;
}

.sidenav {
  width: 200px;
}

// .sidenav .mat-toolbar {
//   background: inherit;
// }

.mat-toolbar.mat-primary {
  position: sticky;
  top: 0;
  z-index: 1;
}

.hidden {
  display: none;
}

.spacer {
  flex: 1 1 auto;
}

.active-link {
  color: #ffc000;
}

In order to show some more content and make it easier to route, let’s put in some dummy content into our src/app/about/about.component.html:

<div class="grid-container">
  <h1 class="mat-h1">About</h1>
  This is my epic project!
  <button mat-button routerLink="/asd" color="primary">Broken link</button>
</div>

Let’s also do the same for the src/app/error/error.component.html:

<div class="grid-container">
  <h1 class="mat-h1">Error</h1>
  This page doesn't exist -
  <button mat-button routerLink="/" color="primary">Back home</button>
</div>

And finally add a global styling for both pages inside the src/styles.scss:

.grid-container {
  margin: 20px;
}

With all of that in place we have a decent responsive web application with different routes and a super clean UI – this could be your template for your next web app!

But we won’t stop here…

Adding Capacitor to our Angular Project

With Capacitor we are able to easily build a native application from our web app code – without any actual changes to the code itself!

To setup Capacitor we install the CLI as a local dependency and call the init command. Simply hit enter for every question for now!

Additionally you need to install a few packages for the core and the native platforms iOS/Android that you want to use and add them in the end one time:

# Install the Capacitor CLI locally
npm install @capacitor/cli --save-dev

# Initialize Capacitor in your Angular project
npx cap init

# Install the required packages
npm install @capacitor/core @capacitor/ios @capacitor/android

# Add the native platforms
npx cap add ios
npx cap add android

At this point you should see an error because Capacitor is looking in the wrong place for the build of your Angular app, so let’s open the capacitor.config.ts and change it to:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'material-app',
  webDir: 'dist/material-app',
  bundledWebRuntime: false,
};

export default config;

Now we are pointing to the right build folder, and we can create a new build and sync those files into our native platforms:

# Build the Angular project
ng build

# Sync our files to the native projects
npx cap sync

Now you just need to deploy the app to your device!

Build and Deploy native apps

You now need Xcode for iOS and Android Studio for Android apps on your machine. Additionally you need to be enrolled in the Apple Developer Program if you want to build and distribute apps on the app store, and same for the Google Play Store.

If you never touched a native mobile project, you can easily open both native projects by running:

npx cap open ios
npx cap open android

Inside Android Studio you now just need to wait until everything is ready, and you can deploy your app to a connected device without changing any of the settings!

Image may be NSFW.
Clik here to view.
android-studio-deploy-angular

Inside Xcode it’s almost the same, but you need to setup your signing account if you wan to deploy your app to a real device and not just the simulator. Xcode guides you through this if you’ve never done it (but again, you need to be enrolled in the Developer Program).

After that it’s as easy as hitting play and run the app on your connected device which you can select at the top!
Image may be NSFW.
Clik here to view.
xcode-deploy-app-angular

Congratulations, you have just deployed your Angular web app to a mobile device!

But there are still some challenges ahead, especially on iOS the UI doesn’t look good yet. Before we fix that, let’s make our debugging process faster.

Capacitor Live Reload

By now you are used to have live reload with all modern frameworks, and we can have the same functionality even on a mobile device with minimum effort!

The idea is to make your locally served app with live reload available on your network, and the Capacitor app will simply load the content from that URL.

First step is figuring out your local IP, which you can get on a Mac by running:

ipconfig getifaddr en0

On Windows, run ipconfig and look for the IPv4 address.

With that information you can now tell Angular to use it directly as a host (instead of the keyword localhost) or you can simply use 0.0.0.0 which did the same in my test:

ng serve -o --host 0.0.0.0

# Alternative
ng serve -o --host 192.168.x.xx

Now we only need to tell Capacitor to load the app directly from this server, which we can do right in our capacitor.config.ts with another entry:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'material-app',
  webDir: 'dist/material-app',
  bundledWebRuntime: false,
  server: {
    url: 'http://192.168.x.xx:4200',
    cleartext: true
  },
};

export default config;

Make sure you use the right IP and port, I’ve simply used the default Angular port in here.

To apply those changes we can now copy over the changes to our native project:

npx cap copy

Copy is mostly like sync, but will only copy over the changes of the web folder and config, not update the native project.

Now you can deploy your app one more time through Android Studio or Xcode and then change something in your Angular app – the app will automatically reload and show the changes!

Caution: If you install new plugins like the camera, this still requires a rebuild of your native project because native files are changed which can’t be done on the fly.

Fixing the Mobile UI of Angular Material

Now we can tackle the open issues of our mobile app more easily.

To begin with, we need to change a meta tag inside our src/index.html and also include viewport-fit:

<meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, viewport-fit=cover"
    />

By doing this we can now access an environment variable that gives us information about the iOS notch area at the top which you usually need to keep empty.

The easiest way to correctly apply this in our case is to open the src/app/app-navigation/app-navigation.component.scss and adding a new rule:

mat-toolbar {
  padding-top: env(safe-area-inset-top);
  height: calc(56px + env(safe-area-inset-top));
}

Because we change the top padding, we also need to calculate a new height of the toolbar. But with this in place, you have a very native UI!
Image may be NSFW.
Clik here to view.

An additional “bug” can be seen when you drag the view down on iOS, something know as over-scroll:

Image may be NSFW.
Clik here to view.
angular-material-overscroll

There seems to be no easy solution for this that works in all scenarios, but I found a pretty decent one in this Github issue.

We can disable the behaviour directly inside Xcode – Capacitor allows us to change the native projects however we want, and the changes won’t be overwritten by some magical script!

Therefore, generate a new DisableBounce.m file inside Xcode and insert this (select yes for generating a bridging header):
ios/App/App/DisableBounce.m

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@implementation UIScrollView (NoBounce)
- (void)didMoveToWindow {
    [super didMoveToWindow];
    self.bounces = NO;
}
@end

And with that fix in place, no more over-scroll on iOS! This means, people won’t really notice they are actually using a web app – which most of us have already done without noticing most likely!

Recap

Angular Material is a great library of components to build beautiful Angular apps, and we haven’t even touched all the other amazing components that you could now use!

By using a sidenav we can also make our page responsive quite easily, and later add in Capacitor to the mix to get the benefit of building a mobile app from our existing codebase.

If you got any questions just let me know, and you can also check out a video version of this tutorial below!

The post Angular Material Mobile App with Capacitor appeared first on Devdactic - Ionic Tutorials.

Angular Landing Page with Airtable Integration and Netlify Functions

If you want to build a public landing page with Angular and email signup you likely don’t want to expose any secret keys, and I’ll show you how with Netlify cloud functions.

In this tutorial we will connect a simple Airtable table as some sort of backend to store emails to our Angular landing page. We do this with the help of Netlify where we will both host our application and also deploy a cloud function!

If you don’t want to use Airtable for storing your users emails you could easily plug in any email service into the cloud function as well. The only thing that matters is that we don’t want to expose any secret keys inside our Angular app and instead keep that kind of information hidden.

Once the technical side is done we will also make the page more polished by adding Tailwind to our Angular app and add relevant meta tags.

You ready? Let’s do this!

Creating an Airtable Table

We begin by setting up our Airtable table, so simply create a new table, remove all the fluff and just make it have one column Email. Don’t mind the two rows in the image below, I later removed the Name column.

Image may be NSFW.
Clik here to view.
airtable-setup

Inside the URL of that table we can find most relevant information for the connection to Airtable, so right now just leave that tab open until we come back to it.

Creating the Angular Project

Next step is creating a new Angular project using the Angular CLI – install it and additionally the Netlify CLI if you haven’t done before.

We create our app including a routing file and SCSS styling, and we also generate a new landing component inside. Finally we install additional pakages for our cloud functions, so go ahead now and run:

# Install CLIs if you haven't done before
npm install -g netlify-cli @angular/cli 

# Setup new Angular project
ng new prelaunch-page --routing --style=scss
cd ./prelaunch-page

# Generate a new component
ng generate component landing

# Install the Airtable package
npm i airtable

# Netlify functions package
npm i @netlify/functions

Because we will later encounter some Typescript warnings, we now need to add one key to the tsconfig.json to solve that issue right upfront:

"compilerOptions": {
    "allowSyntheticDefaultImports": true,

Besides that we should import all necessary modules so we can perform Http calls with the HttpClientModule and build a form with the ReactiveFormsModule.

Therefore open up the src/app/app.module.ts and change it to:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LandingComponent } from './landing/landing.component';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [AppComponent, LandingComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ReactiveFormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Since we want to display our new component instead of the default page, we can now touch the src/app/app-routing.module.ts and include a path to our new component like this:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LandingComponent } from './landing/landing.component';

const routes: Routes = [
  {
    path: '',
    component: LandingComponent,
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Finally we remove all the boilerplate code of the src/app/app.component.html and change it to just one line so the Angular router simply displays the right information/component in our app:

<router-outlet></router-outlet>

If you haven’t done until now you should bring up the live preview of your app by running:

ng serve -o

At this point you should see your landing component on the screen with a small message, which means we are ready to continue.

Creating the Angular Landing Page

Now it’s time to implement the most basic version of our landing page to capture an email. We will tweak the UI later with Tailwind, right now we just want the fields and the logic to work.

Get started by defining a form inside the src/app/landing/landing.component.ts where we make our one email field required and add an empty submit function for the moment:

import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-landing',
  templateUrl: './landing.component.html',
  styleUrls: ['./landing.component.scss'],
})
export class LandingComponent {
  form: FormGroup;

  constructor(private http: HttpClient, private fb: FormBuilder) {
    this.form = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
    });
  }

  submit() {
    console.log('SUBMIT THIS: ', this.form.value);
  }
}

To use the FormGroup we connect it to a form tag and define the ngSubmit function. The one input is connected to the email field through its formControlName, and if a button inside the form has the submit type our submit action will be called!

Go ahead and change our src/app/landing/landing.component.html to this now:

<form [formGroup]="form" (ngSubmit)="submit()">
  <input formControlName="email" type="email" placeholder="saimon@devdactic.com" />
  <button type="submit">Submit</button>
</form>

It’s not really a great landing page yet, but we should be able to insert our data and see a log when we submit the form.

Image may be NSFW.
Clik here to view.
angular-landing-preview

Before we make it look nicer, we focus on the technical side which is the actual cloud function.

Adding a Netlify Cloud Function to the Angular Project

A cloud function is a simple piece of code that is executed in the cloud without you having to run and maintain your own server.

In our case, we want to use Netlify for this but there are several other hosting options available which offer the same functionality by now.

We begin by creating a new file (and the according folders in our project) at netlify/functions/signup.ts, and we insert the following:

import { Handler, HandlerEvent } from '@netlify/functions';
import Airtable from 'airtable';

// Initialize Airtable connection
const { AIRTABLE_KEY } = process.env;

// USE YOUR TABLE BASE HERE
const base = new Airtable({ apiKey: AIRTABLE_KEY }).base('appXYZ');

const handler: Handler = async (event: HandlerEvent, context: any) => {
  try {
    // Parse the body of the request
    const data = JSON.parse(event.body || '');

    // Make sure we got all data
    if (!data.email) {
      return {
        statusCode: 400,
        body: 'Please include email.',
      };
    }

    // USE YOUR TABLE NAME
    // Insert our data into the table columns
    await base('tbXYZ').create({
      Email: data.email,
    });

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Thanks for signing up!',
      }),
    };
  } catch (e: any) {
    return {
      statusCode: 500,
      body: e.message,
    };
  }
};

export { handler };

Quite a lot going on:

  • We initialize a connection to Airtable using an AIRTABLE_KEY and the base name (which you can find in the URL from the beginning)
  • We define a handler for our function which tries to load the email from the body
  • We call the create() function to insert a row into our table (using the table name from the URL at the beginning)
  • We return the right status codes and messages along the way

It’s a straight forward function, and you could make any kind of external API call in here if you are using a different API like ConvertKit.

By loading the AIRTABLE_KEY from our process environment we make sure it’s save and not exposed anywhere, but right now we don’t even have that environment. But hold on, we will get to that part soon.

For now we will create another file at the roo of our project called netlify.toml which holds some information for Netlify about how to build and deploy our project:

[build]
  publish = "dist/prelaunch-page"
  command = "ng build"
  functions = "./netlify/functions"
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

By defining this we also know upfront that our cloud function will be available at .netlify/functions/signup on our domain.

Since we want to make this work both locally and when deployed, we can now open up our src/app/landing/landing.component.ts again and implement the submit function to send the data to this endpoint and use the current window location before that:

submit() {
    const baseUrl = window.location.origin;
    this.http
      .post(`${baseUrl}/.netlify/functions/signup`, this.form.value)
      .subscribe({
        next: (res: any) => {
          alert(res.message);
        },
        error: (err) => {
          alert('ERROR: ' + err.error);
        },
      });
  }

As you can see, making a call to a cloud function is a simple Http call, nothin more, nothing less!

Now we can also use the Netlify CLI to run all of this locally, so simply type:

netlify dev

This would host your Angular app and also deploy your cloud function locally, but it will most likely fail because we have no Airtable key defined yet.

To fix this right now you can simply insert your Airtable key hardcoded into our cloud function.

BTW, you can find that secret key inside your Airtable account page!

Once you used a real value and the Airtable connection works, you should already have a working prototype from which you can submit your email to Airtable!

Host our Angular App with Netlify

But of course we don’t want it hardcoded, and we also want to host our app on Netlify. For all of this we need a new Github (or similar Git service) repository, so create a new one now.

Image may be NSFW.
Clik here to view.
create-github-repo

Back in your Angular project you can add all files, commit them and add the Github repository as your remote and finally push the code:

git add .
git commit -am "First commit"

# If you are not using main as default
git branch -M main

git remote add origin https://github.com/...
git push -u origin main

Now your app is available on Github and we can create a connection to Netlify. We can do this through the admin UI or simply from the CLI by running:

netlify init

Go through the steps of “Create & configure a new site” and you can keep everything as it is. I’m positive that this already takes the information from the toml file we created before!

When this is done we can finally add our secret Airtable key to our Netlify environment by running:

netlify env:set AIRTABLE_KEY YOUR_AIRTABLE_KEY

Of course replace the last part with your real value – and you could also do this through the Netlify web UI if you wanted to.

At this point you can remove the hard coded value from the cloud function as even our dev environment will have access to the key!

Note: If your deployed version on Netlify isn’t working yet, it might not have picked up the new environment variable. Simple rebuild the project and the whole process including the cloud function should work!

You can find the URL to your Netlify project in your account or also on the CLI after you ran the init command.

Adding Tailwind

At this point you have a very ugly but working Angular landing page – that’s already a great success!

But if you want to make this a serious landing page you could now go ahead and add a UI framework like Tailwind to your Angular app, which I’ll show you real quickly.

Begin with these commands to install all necessary packages, and a new file for the Tailwind configuration will be created:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

Now we need to include all of our templates in the configuration so open the new tailwind.config.js and change it to:

module.exports = {
  content: ["./src/**/*.{html,ts}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

Finally we also need to import the Tailwind styling, which we can do right in our src/styles.scss like this:

@tailwind base;
@tailwind components;
@tailwind utilities;

You can now try any Tailwind selector in your app and it should work fine. For the landing page you could now use the following block inside our
src/app/landing/landing.component.html:

<div class="bg-white py-16 sm:py-24">
  <div class="relative sm:py-16">
    <div class="mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:max-w-7xl lg:px-8">
      <div class="relative rounded-2xl px-6 py-10 bg-indigo-600 overflow-hidden shadow-xl sm:px-12 sm:py-20">
        <div class="relative">
          <div class="sm:text-center">
            <h2 class="text-3xl font-extrabold text-white tracking-tight sm:text-4xl">Get notified when we&rsquo;re launching.</h2>
            <p class="mt-6 mx-auto max-w-2xl text-lg text-indigo-200">Subscribe to our prelaunch list and be the first to join our upcoming app!</p>
          </div>
          <form [formGroup]="form" (ngSubmit)="submit()" class="mt-12 sm:mx-auto sm:max-w-lg sm:flex">
            <div class="min-w-0 flex-1">
              <label for="cta-email" class="sr-only">Email address</label>
              <input
                type="email"
                formControlName="email"
                class="block w-full border border-transparent rounded-md px-5 py-3 text-base text-gray-900 placeholder-gray-500 shadow-sm focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600"
                placeholder="Enter your email"
              />
            </div>
            <div class="mt-4 sm:mt-0 sm:ml-3">
              <button
                type="submit"
                class="block w-full rounded-md border border-transparent px-5 py-3 bg-indigo-500 text-base font-medium text-white shadow hover:bg-indigo-400 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600 sm:px-10"
              >
                Notify me
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>

Adding Tailwind snippets is horrible, but hopefully your app now looks a lot nicer.

Image may be NSFW.
Clik here to view.
angular-landing-tailwind

You can now commit and push your changes and the project will build again on Netlify, but before you actually launch your page I recommend one more thing.

Adding Meta Tags

If some shares your Angular SPA the OG tags for social sharing don’t exist. If you have multiple pages you would need a specific package to dynamically set these tags, but if you plan just a simple landing page go with the fastest method, which is simply adding the required tags and information to your src/index.html like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Angular Prelaunch</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />

    <meta name="Description" content="An epic app coming soon." />

    <meta property="og:site_name" content="Angular Prelaunch" />
    <meta property="og:url" content="https://super-cool-site-by-saimon24.netlify.app/" />
    <meta property="og:description" content="An epic app coming soon." />
    <meta property="og:image" content="https://super-cool-site-by-saimon24.netlify.app/assets/meta.png" />
    <meta property="og:image:secure_url" content="https://super-cool-site-by-saimon24.netlify.app/assets/meta.png" />
    <meta name="twitter:text:title" content="Angular Prelaunch" />
    <meta name="twitter:image" content="https://super-cool-site-by-saimon24.netlify.app/assets/meta.png" />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

You can now paste your URL into a tool like MetaTags and see the improved preview of your page (of course only after committing and building your project)!

Image may be NSFW.
Clik here to view.
angular-landing-meta

Just be aware that these tags would be used for all different pages of your app, so once you add more pages, you would need a dedicated package for this.

Teardown

It’s always challenging to connect a private API to your Angular application as all your source code is basically public.

To overcome this challenge you can use a cloud function which holds the secret key to an API you want to use within the process environment and therefore sheltered from the outside world.

To make this even more secure you could also make the cloud function only callable from your domain, but by preventing access to your key you’ve already taken the most important step to building a landing page with Angular.

If you want to see the full process in action, you can find a video version of this tutorial below.

The post Angular Landing Page with Airtable Integration and Netlify Functions appeared first on Devdactic - Ionic Tutorials.

Viewing all 183 articles
Browse latest View live