Quantcast
Channel: Devdactic – Ionic Tutorials
Viewing all 183 articles
Browse latest View live

Building an Eisenhower App with Ionic 4 Drag and Drop

$
0
0

There are many ways to add some drag and drop functions to your Ionic app, but using the Dragula package has been my favorite so far and something you enjoyed with Ionic 3 previously as well.

Today we will take things a step further and build an app that incorporates the Eisenhower time management method with a nice UI, drag & drop and of course Ionic 4!

ionic-4-drag-drop

We will use the ng2-Dragula wrapper for Angular which we’ve used back then as well, but it’s updated to version 2 by now and some parts have slightly changed.

Starting our Drag and Drop App

To get started with our app we create a blank new project and simply install the ng2-dragula package:

ionic start devdacticDrag blank --type=angular
cd devdacticDrag
npm install ng2-dragula

To make use of the package we have to add it to our app module just like many other packages, so go ahead and change your app/app.module.ts to:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { DragulaModule } from 'ng2-dragula';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    DragulaModule.forRoot()],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

At the time writing this there’s a tiny problem with the package and we need to add a workaround to our src/polyfills.ts to make it work, so simply add this to the bottom of your file:

(window as any).global = window;

Finally we need some CSS so the drag animation looks nicely. We could load it directly from the package, however they recommend a slightly different code so add this to your src/global.scss now:

/* in-flight clone */
.gu-mirror {
  position: fixed !important;
  margin: 0 !important;
  z-index: 9999 !important;
  opacity: 0.8;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
  filter: alpha(opacity=80);
  pointer-events: none;
}

/* high-performance display:none; helper */
.gu-hide {
  left: -9999px !important;
}

/* added to mirrorContainer (default = body) while dragging */
.gu-unselectable {
  -webkit-user-select: none !important;
  -moz-user-select: none !important;
  -ms-user-select: none !important;
  user-select: none !important;
}

/* added to the source element while its mirror is dragged */
.gu-transit {
  opacity: 0.2;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
  filter: alpha(opacity=20);
}

That’s it for the basic setup of the Dragula package!

Adding the Drag & Drop Logic

If you want to use the functionality in one of your pages you’ll have to to import it inside the module file of your lazy loaded pages. In our case we can change our app/home/home.module.ts to this:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { DragulaModule } from 'ng2-dragula';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    DragulaModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

Now it’s time to actually work with the package.

With Dragula you can move objects around between groups if they share the same Dragula name. In our case we will have four different arrays (for the quadrants of our app) that hold the information, and we want to drag and drop items between them. Therefore, all of them will be in the same Dragula group called “bag“.

Within our constructor we can now use the Dragula service to subscribe to various events. Let’s talk about the ones we use:

  • drag(): An item is currently being dragged. In that case we will change the background color
  • removeModel(): An item was dropped, but not inside another group but outside any group. At that point the item will be removed and we’ll show a little toast
  • dropModel(): An item was dropped into a new group. We’ll again change the color to reflect that something happened
  • createGroup(): This one defines some options for our group, in our case that all items dropped outside the group should be spilled which means removed

All of these Observables can return a lot of values, and you can pick the ones you need. In our case we mostly need the actual item which holds the information of the object inside our data array, but you can get information about almost anything at that point! Simply check out the signature of those functions.

We are also changing the color in two different ways; While dragging we directly set the color attribute of the HTML item, later we use the color property. Just wanted to show both options in here as they do pretty much the same.

Finally we also have a function to add a new todo to a specific quadrant. We need some logic here to check which color should be added but basically it just adds the information to the appropriate array.

Now go ahead and change your app/home/home.page.ts to:

import { Component } from '@angular/core';
import { DragulaService } from 'ng2-dragula';
import { ToastController } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  q1 = [
    { value: 'Buy Milk', color: 'primary' },
    { value: 'Write new Post', color: 'primary' }
  ];
  q2 = [
    { value: 'Schedule newsletter', color: 'secondary' },
    { value: 'Find new Ionic Academy topics', color: 'secondary' }
  ];
  q3 = [
    { value: 'Improve page performance', color: 'tertiary' },
    { value: 'Clean the house', color: 'tertiary' }
  ];
  q4 = [
    { value: 'Unimportant things', color: 'warning' },
    { value: 'Watch Netflix', color: 'warning' }
  ];

  todo = { value: '', color: '' };
  selectedQuadrant = 'q1';

  constructor(private dragulaService: DragulaService, private toastController: ToastController) {
    this.dragulaService.drag('bag')
    .subscribe(({ name, el, source }) => {
      el.setAttribute('color', 'danger');
    });

    this.dragulaService.removeModel('bag')
    .subscribe(({ item }) => {
      this.toastController.create({
        message: 'Removed: ' + item.value,
        duration: 2000
      }).then(toast => toast.present());
    });

    this.dragulaService.dropModel('bag')
      .subscribe(({ item }) => {
        item['color'] = 'success';
      });

    this.dragulaService.createGroup('bag', {
      removeOnSpill: true
    });
  }

  addTodo() {
    switch (this.selectedQuadrant) {
      case 'q1':
        this.todo.color = 'primary';
        break;
      case 'q2':
        this.todo.color = 'secondary';
        break;
      case 'q3':
        this.todo.color = 'tertiary';
        break;
      case 'q4':
        this.todo.color = 'warning';
        break;
    }
    this[this.selectedQuadrant].push(this.todo);
    this.todo = { value: '', color: '' };
  }

}

We also added a few initial items to our array so the app is not so empty while testing.

Creating the Eisenhower Matrix View

Right now we have added all the logic and learned to use the Dragula service, now it’s time to connect everything with our view.

First of all we need the information for a new todo which is the area at the top of our view. It’s not really anything new, but you might have noticed the forceOverscroll on our ion-content. This helps to prevent some crazy scrolling on a device while dragging elements between the lists!

At the bottom we have the four quadrants, and we could have another array to iterate so it would be only one code block but I thought it would be a bit more clearly how the package works if we write things out.

So within each column we have a little header for that area followed by a list of the todo items of that quadrant.

The most important part is to specify dragula="bag" on all of the groups between which you want to drag and drop items! Also, you need to pass in the [(dragulaModel)] which is the connection to the array with information for each quadrant.

If you set up these things correctly you have established a connection between the different lists and also a 2 way data binding between the Dragula list and the actual array containing the information.

With all of that said, open your app/home/home.page.html and replace it with:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Devdactic Eisenhower
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content forceOverscroll="false">
  <ion-grid>

    <ion-row align-items-center>
      <ion-col size="12">
        <ion-item>
          <ion-label position="fixed">New Todo:</ion-label>
          <ion-input [(ngModel)]="todo.value"></ion-input>
        </ion-item>

      </ion-col>

      <ion-col size="12">
        <ion-item>
          <ion-label>Quadrant:</ion-label>
          <ion-select [(ngModel)]="selectedQuadrant">
            <ion-select-option value="q1">Do</ion-select-option>
            <ion-select-option value="q2">Schedule</ion-select-option>
            <ion-select-option value="q3">Delegate</ion-select-option>
            <ion-select-option value="q4">Dont do</ion-select-option>
          </ion-select>
        </ion-item>
      </ion-col>

    </ion-row>

    <ion-button expand="block" fill="outline" (click)="addTodo()">
      <ion-icon name="add" slot="start"></ion-icon>
      Add Todo
    </ion-button>

    <ion-row no-padding class="matrix">
      <ion-col size="6" class="q1">
        <div class="q-header">Do</div>
        <ion-list dragula="bag" [(dragulaModel)]="q1" lines="none">
          <ion-item *ngFor="let item of q1" [color]="item.color" expand="block" text-wrap>
            {{ item.value }}
          </ion-item>
        </ion-list>
      </ion-col>

      <ion-col size="6" class="q2">
        <div class="q-header">Schedule</div>
        <ion-list dragula="bag" [(dragulaModel)]="q2" lines="none">
          <ion-item *ngFor="let item of q2" [color]="item.color" expand="block" text-wrap>
            {{ item.value }}
          </ion-item>
        </ion-list>
      </ion-col>

      <ion-col size="6" class="q3">
        <div class="q-header">Delegate</div>
        <ion-list dragula="bag" [(dragulaModel)]="q3" lines="none">
          <ion-item *ngFor="let item of q3" [color]="item.color" expand="block" text-wrap>
            {{ item.value }}
          </ion-item>
        </ion-list>
      </ion-col>

      <ion-col size="6" class="q4">
        <div class="q-header">Don't do</div>
        <ion-list dragula="bag" [(dragulaModel)]="q4" lines="none">
          <ion-item *ngFor="let item of q4" [color]="item.color" expand="block" text-wrap>
            {{ item.value }}
          </ion-item>
        </ion-list>
      </ion-col>
    </ion-row>
  </ion-grid>

  <ion-row class="delete-area" align-items-center justify-content-center>
    <ion-icon name="trash" color="medium"></ion-icon>
  </ion-row>

</ion-content>

Whether you are directly following this tutorial or use your own view, adding some styling is important because Dragula might not work as expected otherwise. First, we had to add this overall CSS so we see anything happen, now it’s important to give your lists height:100%; in order to make them work when the array is empty!

Besides that I played around with some other styling properties but feel free to experiment a bit more on a real device! Here’s my styling that you can add to your app/home/home.page.scss:

.q1, .q2, .q3, .q4 {
    border: 4px solid #fff;
}

.matrix {
    margin-top: 30px;

    ion-col {
        --ion-grid-column-padding: 0px;
        min-height: 150px;
    }

    .list {
        padding: 0px;
        height: 100%;
    }

    ion-item {
        margin-bottom: 2px;
    }
}

.q-header {
    background-color: var(--ion-color-light);
    height: 20px;
    text-align: center;
}

.delete-area {
    border: 2px dashed var(--ion-color-medium);
    margin: 10px;
    height: 100px;
    ion-icon {
       font-size: 64px;
    }
}

The result should look pretty much like the image below. You can drag the items between the different lists, you can add new items and also drop them outside to remove them. The delete area is basically just a UI indication to drop it, actually you can drop them anywhere outside the lists to remove them!

ionic-4-drag-drop-eisenhower

Conclusion

Drag and drop is a pretty cool functionality if you can integrate it in a useful way inside your Ionic app. Dragula is not the only package that helps us to implement such a feature, but it’s one of the easiest and fastest ways to add it.

You can also find a video version of this tutorial below.

The post Building an Eisenhower App with Ionic 4 Drag and Drop appeared first on Devdactic.


5 Animation Packages You Can Immediately Use Inside Your Ionic App

$
0
0

With Ionic and Angular you have a lot of options when it comes to animations inside your app. You can actually get started with Angular Animations or any other package that you can install in a matter of minutes!

In this post we will inspect 5 different animation packages that we can plug into our app to create and use predefined animations or to have a framework where we can easily define animations.

ionic-animation-packages-example

You can start a blank Ionic 4 app like this to better follow some of the snippets:

ionic start animationPackages blank --type=angular

We’ll not have the full excerpt but only show the important parts of how to integrate each package.

1. Anime.js

This package can be installed and used immediately inside our app without anything further to include. Simply run the install like this and you are ready:

npm install animejs

With this package you can create great animations within your Javascript code. This is the area where most packages differ: They either allow to add CSS classes or they allow to create special animations using a specific syntax within your class.

With Anime.js you can easily animate elements on the screen and move them around, so this is the code for creating a little box (plus some CSS so the box actually has a size on the screen) and then a function to create the animation.

// HTML
<div class="animate-me" #box></div>
  
// SCSS

.animate-me {
    width: 50px;
    height: 50px;
    padding: 20px;
    background: #0000ff;
}

// TS
import * as anime from 'animejs';

callAnime() {
    anime({
      targets: '.animate-me',
      translateX: [
        { value: 100, duration: 1200 },
        { value: 0, duration: 800 }
      ],
      rotate: '1turn',
      backgroundColor: '#ff00ff',
      duration: 2000
    });
}

For the target we can simply use the CSS class of the element we want to animate. The other parameters are pretty much self-explanatory and that’s great about this package:

You can get started quickly and create powerful animations with basic commands that you’ll understand fast. You won’t have to study a long API to create your first animations with this package.

2. Magic CSS

The next package relies on pre defined CSS animations that can be added to your elements. You can install the package just like before:

npm install magic.css

But this time you also need to import the actual CSS file from the node module, and in order to do so you’ll have to change your src/global.scss and add another import like this:

@import '~magic.css/magic.min.css';

Now the animations of Magic CSS are available within your app and you can either directly add them as classes on your element or, if you want to use them at a specific time, add them to the classList of an element that you can get by using the @ViewChild() annotation like this:

// HTML
<div class="animate-me" #box></div>

// TS
@ViewChild('box') box: ElementRef;

doMagic() {
    this.box.nativeElement.classList.add('magictime');
    this.box.nativeElement.classList.add('foolishIn');
}

You’ll always add the magictime class and then the actual name of the animation you want to use.

This package doesn’t offer so many customisation options but if you want simple and fast CSS animations you can give it a try!

3. Number Flip

This is not a huge animation package but I discovered it recently and really enjoyed the animation. It’s only useful if you want to have one specific effect inside your app like you can see on the Github page, but the integration works easy again like:

npm install number-flip

Now let’s say you have some sort of counter inside the top bar of your Ionic app and want to change numbers and do it with style.

In this case the number flip package is awesome as you can flip an element from one number to another with a cool animation. I’ve added some code that creates the reference to the element once and when you later trigger the flip() function again it will simply call flipTo() of the animation package:

// HTML
<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Animations
    </ion-title>
    <ion-buttons slot="end">
      <div #numberbtn></div>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

// TS
import { Flip } from 'number-flip';

@ViewChild('numberbtn', { read: ElementRef }) private btn: ElementRef;

flip() {
  if (!this.flipAnim) {
    this.flipAnim = new Flip({
      node: this.btn.nativeElement,
      from: '9999',
    });
  }

  this.flipAnim.flipTo({
    to: Math.floor((Math.random() * 1000) + 1)
  });
}

Of course this package doesn’t replace more advanced animations but it handles one specific case really great so keep it in mind if you work with timers or numbers that you need to animate!

4. Animate CSS

This is most likely the biggest player of all and the one with most Github stars. This package advertises with “Just-add-water CSS animation” and it’s really that simple. The installation works like before:

npm install animate.css

Because this package relies on its CSS again we have to add an import to the src/global.scss again if we want to use it:

@import '~animate.css/animate.min.css';

Now we can enjoy all the awesome predefined animations of this package (there’s really an animation for every use case) and we can even add additional classes like infinite so the animation repeats all the time or directly add a delay to the start of the animation.

In one example I animated an ngFor using the index for the delay (ok, might be a bit long in a real world app though) and also used the ViewChildren list to add a class if we want to fly out the elements of the list.

// HTML
<h1 text-center class="animated infinite rubberBand delay-1s">Example</h1>

<ion-list>
    <ion-item *ngFor="let val of ['First', 'Second', 'Third']; let i = index;" 
    class="animated fadeInLeft delay-{{ i }}s" #itemlist>
      {{ val }} Item
    </ion-item>
</ion-list>

// TS
@ViewChildren('itemlist', { read: ElementRef }) items: QueryList<ElementRef>;

animateItems() {
  let elements = this.items.toArray();
  elements.map(elem => {
    return elem.nativeElement.classList.add('zoomOutRight')
  })
}

If you want a great arsenal of predefined CSS animations this package is definitely the one you should give a try. While it has a lot of pre defined stuff you can still compose the things to fit your needs!

5. Bounce.js

Finally I wanted to test another more flexible package where we can compose our animations from Javascript again. The package can be installed just like the others:

npm install bounce.js

This package has a big documentation so you’ll likely spend some time exploring all the options, for one example I just picked one of the advertised snippets on their page:

// HTML
<ion-button expand="block" (click)="bounce()" #bouncebtn>Bounce</ion-button>

// TS
import * as Bounce from 'bounce.js';

@ViewChild('bouncebtn', { read: ElementRef })bouncebtn: ElementRef;

bounce() {
  var bounce = new Bounce();
  bounce
    .translate({
      from: { x: -300, y: 0 },
      to: { x: 0, y: 0 },
      duration: 600,
      stiffness: 4
    })
    .scale({
      from: { x: 1, y: 1 },
      to: { x: 0.1, y: 2.3 },
      easing: "sway",
      duration: 800,
      delay: 65,
      stiffness: 2
    })
    .scale({
      from: { x: 1, y: 1 },
      to: { x: 5, y: 1 },
      easing: "sway",
      duration: 300,
      delay: 30,
    })
    .applyTo(this.bouncebtn.nativeElement);
}

As you can see, everything happens inside your Javascript code! You can create huge keyframe animations with this package and define all steps very granular.

This flexibility comes at a price, you have to dig into the docs and it’ll take more time to get started then with the other packages. Still, if you can invest the time it will pay off as you’ll be able to create exactly the animations your app needs!

Conclusion

Some of these packages give you fast results, others come with their own syntax that you have to learn. Some have everything pre defined while others allow more flexibility when it comes to creating animations. Some are just CSS, some just JS.

There’s not really “the best” as all of them have their strengths in different areas. Also, it’s always a good idea to keep an eye on the package size of your app and don’t add anything that hurts your download rate in the end.

Finally, besides all these packages you can of course as well use the standard Angular animations and there’s a special course dedicated to that topic inside the Ionic Academy as well!

You can also find a video version of this tutorial below.

The post 5 Animation Packages You Can Immediately Use Inside Your Ionic App appeared first on Devdactic.

10 Tips & Tricks for Building Websites with Ionic 4

$
0
0

It’s nothing new that Ionic apps can easily be deployed as a website (which is very common these days given the popularity of PWAs) or desktop version wit Electron, but once you got so much space available it’s easy to just make your mobile design bigger.

In most cases, the result will be very unsatisfying but there are some tricks I learned just recently when creating the Ionic Jobs board which is available as a website and PWA!

Therefore, I compiled all my learnings into a list not only for you but also for myself as I’m 100% sure I’ll come back to this post (actually I look up a lot of old things on my own blog).

Here are my 10 tips for everyone getting started with a website first but still using Ionic!

1. Getting the Most out of Ionic Grid

The Ionic Grid is a great way to build dynamic and responsive layouts. But it’s not enough to simple use the general column setup, you need to tweak the columns and how your row looks using the common breakpoints which are:

  • xs < 540px
  • sm > 576px
  • md > 768px
  • lg > 992px
  • xl > 1200px

Testing these breakpoints can be difficult if you are developing on an iMac where when the XL size looks like a quarter of your screen 😀

So when your design gets smaller you might want to give more space to specific rows and you can do this but setting the size for all breakpoints individually. Besides that you can also add a specific offset for a breakpoint if you want to allow more space (see also point 8).

<ion-grid>
  <ion-row> 
    <ion-col size="12" size-lg="8" offset-lg="2" size-xl="6" offset-xl="3" style="background: var(--ion-color-primary);">
      I am full and have offset on bigger screens!
    </ion-col>
  </ion-row>
</ion-grid>

The result can bee seen in the following image.
responsive-row

By applying different sizes and offsets you can make sure your design does not only fit each screen, but also make sure it’s not looking oversized!

2. Dynamically Hiding Grid Elements

Now the problem with the previous grid is that once you invest more time and thoughts into your design, you might want to leave out certain columns at a breakpoint.

If the screen get’s smaller, you might want to hide some not so important information, but simply setting the row size to zero is not going to work. You need a little help of CSS!

@media (max-width: 576px) {
    .hide-xs {
      display: none;
    }
}

@media (min-width: 576px) and (max-width: 768px) {
    .hide-sm {
      display: none;
    }
}

With a CSS like this in place you can make sure a certain column won’t even be painted on the screen once the media query applies:

<ion-row>
  <ion-col size="12" size-md="5" offset-md="2" size-lg="4" offset-lg="3" style="background: var(--ion-color-primary);">
    I am full and have offset on bigger screens!
  </ion-col>
  <ion-col class="hide-xs hide-sm" size-md="3" size-lg="2" style="background: var(--ion-color-secondary);">
    I want some space as well!
  </ion-col>
</ion-row>

Now the second column takes some of the space (I also changed the numbers for the first row, always make sure it sums up to 12) but once we hit the SM and later XS breakpoint the row will be hidden!

responsive-row-hide

By combining the first grid setup and additional media queries through CSS you can craft your design super responsible and make it work as best as possible on every screen size!

3. Fixed Content Height

Ok enough of responsive designs (I feel like we’re talking about responsive and mobile-first since 10 years) and to something else that really bugged me and took hours to get at least rightish.

When you visit a website, every legit website has a footer. Now Ionic does have a footer component, but on a regular website you don’t want your footer to flow above the content all the time! You want that footer to be at the end of the page where a users expects it, even when there are only a few paragraphs of text.

One solution is to establish a bit hacky fixed element like this:

[ion-fixed] {
    min-height: calc(100% - 98px); // adjust to your footer height!
}

Now you can wrap all of your content into this element like:

<ion-content>
  <ion-button (click)="toggle = !toggle">Toggle Long content</ion-button>
  
  <div ion-fixed>
    <div [hidden]="!toggle">
      <p *ngFor="let i of [].constructor(20)">
        My regular content goes here, works as expected<br>
      </p>
    </div>

    <div [hidden]="toggle">
      a page with short content
    </div>

  </div>

  <div style="background: var(--ion-color-medium)" text-center>
    And here is my footer at the bottom!<br>
    Legal Link<br>
    Whatever Link
  </div>

</ion-content>

I added a little button for the demo below, of course that’s not needed in general!

ionic-fixed-footer

Of course it’s a bit hacky and yes, inside an app you might still want to have a regular footer bar but for websites this should give you the desired effect!

4. Global Header & Footer

Continuing with the idea of the last point – you of course don’t want to define all your footer stuff over and over again!

For that, simply use the CLI to create a new component like:

ionic g component footer

Inside of the footer add whatever code you need and then also make sure to create a shared module so you can import the footer component accordingly into your lazy loaded pages which all got their own module file!

To do so, generate a new module and declare/export all of your custom components. Also note that you will have to import the IonicModule inside this shared module file if you plan to use Ionic components in there.

# Generate the Module

ionic g module components/sharedComponents --spec=false --flat


# Change your Module File

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FooterComponent } from './footer/footer.component';
import { IonicModule } from '@ionic/angular';

@NgModule({
  declarations: [FooterComponent],
  imports: [
    CommonModule,
    IonicModule // <- Import this if you want to use ion-* stuff in your components!
  ],
  exports: [FooterComponent]
})
export class SharedComponentsModule { }

Now you can simply reuse this component all over the place, same if you want a custom header menu of course.

If you want to go even more crazy, you can create a layout page, add your routing in there and use another in there.

This is a bit more tricky and requires some more changes so let me know if you are interested that as well. I used this behaviour on my recent project as then the header and footer stay the same and only the content area is replaced which results in less flickering on the page.

5. Standard Dropdown Select

Another area that is not a must is input forms, and here especially the select field. While I have used the ion-select in the past on websites, especially with the popover option, I think it’s not really looking like a website thing.

Maybe this is just my personal feeling, but for a website I still prefer the good old select. The difference is super small:

<ion-item>
  <ion-label position="stacked">Gender</ion-label>
  <ion-select placeholder="Select One">
    <ion-select-option value="f">Female</ion-select-option>
    <ion-select-option value="m">Male</ion-select-option>
  </ion-select>
</ion-item>

<!-- VS -->

<ion-item lines="none">
  <ion-label position="stacked">Gender</ion-label>
  <select>
    <option disabled selected>Select One</option>
    <option value="f">Female</option>
    <option value="m">Male</option>
  </select>
</ion-item>

With some CSS the select also looks good:

select {
    border: 1px solid #ededed;
    border-radius: 5px;
    background: #fbfbfb;
    width: 100%;
    height: 34px;
}

Now you can decide which one you enjoy more on a website!

ionic-select

What’s your choice?

6. Skeleton Views

This one is so easy to implement but can go a long way in terms of perceived performance. Justin Willis has talked about it on the Ionic blog before, and I just wanted to bring some more attention to it again.

The idea is to show a skeleton of your view (hence the name ofc) while your app is loading some async data. This gives the user the impression he can start in a second and he’s not getting tired by an endless spinning wheel.

I actually think this comes from ages ago when internet was sometimes super slow and on websites you would first see some general structure (which meant you actually connected in some way!) and then seconds or minutes later the rest would load.

In order to implement this inside your app, you can either use the ngx-skeleton package or simply use the built in ion-skeleton-text:

<ion-item *ngFor="let fake of [{i: 250, j: 85}, {i:200, j:125}]">
  <ion-label text-wrap>
    <ion-skeleton-text [width]="fake.i + 'px'"></ion-skeleton-text>
    <ion-skeleton-text [width]="fake.j + 'px'"></ion-skeleton-text>
  </ion-label>
</ion-item>

For this example I added a little loop with values to make the skeletons more dynamic, you can also generate the numbers from your JS code and use the random function to generate a nice looking skeleton view!

7. Form Validation

Not really a hack or so but something I found very useful for validating forms and especially giving the user feedback.

Just recently I learned that you should not simply hide the submit button without giving a clue what’s missing (which should be common sense), but simply highlight the fields that are missing/wrong once the user has clicked the submit button and the form is not yet valid.

Reactive forms offer already a lot of opportunities to add classes to fields when they are wrong and touched and so on, but maybe a field was never even touched but is still not valid?

Somewhere I found a great function which iterates through all fields of a form and marks them as touched:

validateAllFormFields(formGroup: FormGroup) {
  Object.keys(formGroup.controls).forEach(field => {
    const control = formGroup.get(field);
    if (control instanceof FormControl) {
      control.markAsTouched({ onlySelf: true });
    } else if (control instanceof FormGroup) {
      this.validateAllFormFields(control);
    }
  });
}

isFieldInvalidTouched(field: any) {
  // myForm is your FormGroup variable
  return this.myForm.get(field).invalid && this.myForm.get(field).touched;
}

Now inside your view you can use the second function of that snippet to check if field was touched and is invalid and then add a class:

<ion-input formControlName="company_name" [class.invalid]="isFieldInvalidTouched('company_name')"></ion-input>

This means, you simply need to check if your form is valid once the user hits the submit button, and if not call the function above to mark all fields as touched. Your view will be updated, and all fields that are invalid get the invalid class (for which you need to add some CSS) even if they were not touched before.

Now your users can fill out all the invalid fields and is happy about the information you provided!

8. More Space

I just wanted to add this one more time after talking about responsive design in the beginning of this Ionic website tutorial:

Please, please, give your design and elements room to breathe!

If your team got a designer he/she will most likely already insist on this, but if your are running a one man show (like I do) then there is nobody to blame besides you.

Make sure to use offset or enough margin/padding, don’t try to force 7 big columns into a tiny iPhone screen.

Show information in different places and find better ways to display bigger amounts of data.

Your users will be very thankful.

9. Production Builds & Console Logs

Raise your hand if you never forgot about a console.log() inside your production build!

I certainly did because I use it all the time and I’m sometimes just too lazy to clean up after myself. Good thing, there’s a tiny hack we can add so all these logs won’t log out a single line when they are running as a website.

And especially if you deploy your Ionic app as a website, it’s super easy for everyone to check the console logs.

So inside your app, open the src/main.ts and add a little block to the already existing production check:

if (environment.production) {
  enableProdMode();

  if(window){
    window.console.log=function(){};
  }
}

Now whenever you make a production build (which you should do for deployment) the log function will be replaced with an empty function.

10. Hosting

Please never tell anyone again you don’t know where to host a website – there are countless great free options and the one I like to pick is Firebase hosting.

The only thing you have to do is create a new Firebase project, and then run these 4 commands (and answer some prompts):

# Install the tools if you haven't
npm install -g firebase-tools

# Log in to your account
firebase login

' Init the services inside your Ionic project
firebase init

# Deploy the www folder to Firebase
firebase deploy

Once you are done you will get the URL to your project. But not enough, you can even add your own custom domain and get a free SSL certificate so it’s not only some free cheap hosting but a serious alternative for your Ionic website.

Conclusion

When writing up these points I had one famous quote from good old Steve on my mind the whole time:

Most people make the mistake of thinking design is what it looks like. People think it’s this veneer – that the designers are handed this box and told, “Make it look good!” That’s not what we think design is. It’s not just what it looks like and feels like. Design is how it works. – Steve Jobs

Keep this in mind all the time and both your websites and apps will be great!

PS: Would you like to know more about building a PWA with Ionic 4? Just leave a comment below!

You can also find a video version of this article below.

The post 10 Tips & Tricks for Building Websites with Ionic 4 appeared first on Devdactic.

10 Tools & Services Every Ionic Developer Should Know

$
0
0

We live in great times where we can use free frameworks and tools, even additional services with powerful tools, to create and realise our own apps without any real cost besides hardware and time!

In this post I’ll show you 10 tools and services you might or might not yet know about that can help you to develop, build and market your Ionic apps – either as an individual or as a company.

I’m aware that this list is by no means complete so if you say “xyz has to be on that list” just leave a comment below!

Let’s see if you already know and use all of them…

1. Visual Studio Code

I put this one first because it’s the most asked question on all my videos: My code editor is Visual Studio Code (VSC) and I can say that it’s the editor most people in the Angular/Typescript area are using currently.

10-tools-vsc

You are using a different IDE?

That’s totally fine. Everyone has their favorite tools, be it Sublime, Atom, Webstorm.. or even VIM if you are in full nerd mode and can get to great speed with it! It’s important that you are comfortable and can work fast with the tools you have.

But there is one tool every Ionic developer should look forward to, and it’s the recently announced Ionic Studio . From the first looks this could become the first choice for us!

As of writing this list, it’s not yet released as a free community edition but I’m optimistic the Ionic team won’t forget about their huge developer base.

2. Browser Tools & Ionic DevApp

Although everyone should know how to debug apps I think it’s worth mentioning, based on the questions I see on some Videos and articles.

The browser developer tools are Ionic developers best friend!

10-tools-browser-tools

Wether you are using Chrome, Safari or something else, there’s a menu to open the developer tools and debug your running Ionic instance (even remote on your device!). Within this view you can see the logs, set breakpoints in your code and inspect all view elements with their attributes.

Fine tuning some CSS?

Due it live in here and just paste the code into the files later!

All of this helps to find your bugs but sometimes it’s even better to see your app on a real device.

And while deploying your app on your phone doesn’t take too long, it’s still a step you can save by using the Ionic DevApp. You can get this app for both iOS and
Android.

10-tools-dev-app

Now you can open the app and directly connect to your local running Ionic instance. It’s magic!

PS: The Ionic app you see in the images above is Ionic Jobs, the job board I created for all Jobs and projects related to Ionic!

3. Git

Not all tools need to be specific for Ionic but for software developers in general. If you work at a company, chances are high you are already using a version control system (and also very likely Git).

I’m not going to explain what Git is, you can find all the information around it here. I’m not even sure if we still have to talk about this but I’ve seen people use Dropbox for their code even in 2018.

10-tools-git

Even if you work just for yourself, it’s a good idea to use it and you can (as of now) even create free private repositories on Github! There is really no reason not to use git.

Just want to try out some new code?

Make your changes, mess up all code and make one git checkout . and your old code is back without thinking about which changes you applied. That’s just one of hundred reasons for using version control.

If you are scared of all the command line operations simply grab a tool with a visual representation of everything – I’m currently using Sourcetree but there are many more just like that out there.

4. Ionic Appflow

We are now entering one of the paid tools – it’s one of the reasons why Ionic is still in the game as a company with a free framework. And it’s one of the reasons people love Ionic and the things they do.

With Ionic Appflow (previously called Ionic Pro) you get a suite of tools geared towards DevOps and all tasks along the chain to build and deploy your Ionic apps. It’s like the missing continuous integration tool for Ionic apps!

10-tools-appflow

To highlight a few of the main aspects:

  • Hot code deployments without app store submission
  • Native app builds in the cloud
  • Git based workflow for your team

This is by no means a tool you have to have to build Ionic apps. You can build great apps with Ionic for free, starting right now.

But especially if you are working as a team or enterprise, this can highly increase the speed of your app delivery workflow and therefore save you a lot of money in the long run.

5. Firebase

Whether you have no experience in building a backend or simply no interest in setting everything up, Firebase is the choice for the biggest amount of Ionic developers.

With Firebase you get a free realtime database, cloud storage, user authentication, messaging and even cloud functions (last might be paid only). You can start for free and once your app gets traction upgrade your instance. Most of these functionalities will help you to build the first version of your app super quickly, and if you want more knowledge on Ionic + Firebase check out the courses inside the Ionic Academy!

10-tools-firebase

Firebase was acquired by Google in 2014 and has become one of the biggest players in the game of Backend as a Service solutions, if not the biggest already.

Additionally, you can use Firebase hosting to upload your Ionic app to release it as a website as well and especially for PWA deployment. Again, getting started is free so get your apps out!

6. Dribbble

We are now leaving the development area and focus on other tasks – yes, creating a great app is not only about writing awesome code (although we hate to rely on designers).

The UI plays a huge role in whether you app might become a success, and if you are like a typical developer, you don’t really have a graduate in color theory or UX design. The good news is you don’t necessarily have to!

10-tools-dribbble

With websites like Dribbble you can browse the designs of other people around the world, inspect color schemes that work great together or find inspiration for how your next login screen might look like.

I found that creating some matching colors for the identity of your app and then using them with the Ionic color generator already changes your app from “another default blue Ionic app” to “hey that’s my idea in action!”.

7. Free Icons

Although you should know about it already when using Ionic, let’s just highlight the awesome Ionicons once again that come shipped with your app!

10-tools-ionicons

These icons give you a great initial entry point and can of course be used for free when you ship your app as well. But if you need more icons, which you sometimes inevitably do, another (to some degree free) package called Font Awesome is your friend.

This package should contain basically every icon that you ever need and it’s also easy to use with Ionic 4. For some of those icons you might need a paid license. But it might still be cheaper than hiring a designer.

8. Image Assets

While some of you might have a designer or the money to hire one, some will be on their own without any big resources they can spend upfront. For those of you looking for great professional assets, here comes UnDraw.

I discovered it just recently but already love it. On UnDraw you find a variety of SVG icons you can use for free and the best part about it?

10-tools-undraw

Have you seen it?

You can simply change the color picker (e.g. with the color you picked from cool Dribbble designs!) and all assets change their color. Plus most of them really look like a designer created them for you.

9. Code Documentation

We’ve seen some free tools, services and assets for your project and to keep up the high quality of your code and apps you should also always have appropriate code documentation in place. For Angular, I really recommend Compodoc!

Just like version control, this is a point everyone should at least know about. Having the time and doing it stands on another page (just being honest, we’ve all seen it before).

If you want to get started you can check out my Ionic Academy Quick Win on Ionic Code Documentation with Compodoc.

10. Ionic Academy

All the tools mentioned before are awesome but if you are just starting out or simply can’t find any free information out there you might need some premium expert material on Ionic.

ionic-academy-ad-2019

For all of you who are serious about Ionic app development, who want to get started quickly or just can’t afford to waste time looking for answers on the Internet I created the Ionic Academy almost 2 years ago!

This membership side combines Ionic video courses (currently 30+) with written material, a Roadmap to help you find your way and a great community with a private Slack channel to answer all your questions and meet fellow Ionic developers.

What else?

These are just some of the tools and services I use on a daily basis to help others learn Ionic or to build apps for business clients.

What are your favorite tools and what did I leave out?

You can also find a video version of this list below.

The post 10 Tools & Services Every Ionic Developer Should Know appeared first on Devdactic.

How to Build An Ionic 4 App with Firebase and AngularFire 5

$
0
0

Your favorite tech combinations is back!

Firebase continues to be the leading hosting platform in Ionics 2018 Developer Survey and it’s no wonder – getting started is super easy and you get the benefits of a real backend in only minutes!

In this tutorial we will walk through the steps of creating an Ionic App using the AngularFire package to connect our App to Firebase.

ionic-4-firebase-app

That means, we’ll use the new routing concepts, environment variables, services and the connection to the Firestore database which is by now actually out of beta!

Creating the Firebase App

Before we dive into Ionic we need to make sure we actually have a Firebase app configured. If you already got something in place you can of course skip this step.

Otherwise, make sure you are signed up (it’s free) and then hit Add project inside the Firebase console. Give your new app a name, optional select a region and then create your project once you checked the boxes like below.

ionic-4-firebase

After a short time you will be brought to the dashboard of your app and the only thing we need from here is the information for our Ionic app. We’ll copy this soon, but for now we also need to navigate to Database which will automatically open a security information (you might have to click get started first).

ionic-4-firestore

Here we can set the default security rules for our database and because this is a simple tutorial we’ll roll with the test mode which allows everyone access.

User authentication, security rules and more topics are covered in the courses of the Ionic Academy!

Starting our Ionic App & Firebase Integration

Now we get into the Ionic side of things. We create a new blank Ionic 4 app and install the Firebase and AngularFire package from NPM. Additionally we need 2 more pages to navigate around and also a service so we can separate our Firebase calls from our pages, so go ahead and run:

ionic start devdacticFire blank --type=angular
cd devdacticFire

npm install firebase @angular/fire

ionic g page pages/ideaList
ionic g page pages/ideaDetails
ionic g service services/idea

Once the app is ready we need the information from Firebase to connect our Ionic app to it. Therefore, navigate to your dashboard and click on the code icon for web above Add an app to get started which will bring up a screen like below.

ionic-4-firebase-add-to-app

You can now simply the information from the config object and paste it below into your src/environments/environment.ts under a new firebase key like this:

export const environment = {
  production: false,
  firebase: {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
  }
};

The cool thing about the environment is that we could have different information in this and the .prod file which would be used if build our app later with that command line flag!

In all of our files the import will stay the same – it’s just a different file that will be used in the end.

Just by pasting the information into the environment we are not yet done. Now it’s time to let AngularFire know about this information and we do so inside our app/app.module.ts

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { AngularFireModule } from '@angular/fire';
import { environment } from '../environments/environment';
import { AngularFirestoreModule, FirestoreSettingsToken } from '@angular/fire/firestore';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    { provide: FirestoreSettingsToken, useValue: {} }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Because of a recent update to the Firebase SDK we can also add the provide block at the end to prevent an error message in our log. You can find more information on this issue here, maybe it’s also solved at a later point!

The last thing we need now is to setup our routing information.

Our app should have a list of ideas (yeah, just wanted something else than a todo list..) and also a details page for an idea, a very classic pattern you’ll most likely have in all your apps.

Therefore change the routing information inside your app/app-routing.module.ts where the pages and modules have been automatically added to:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: './pages/idea-list/idea-list.module#IdeaListPageModule' },
  { path: 'idea', loadChildren: './pages/idea-details/idea-details.module#IdeaDetailsPageModule' },
  { path: 'idea/:id', loadChildren: './pages/idea-details/idea-details.module#IdeaDetailsPageModule' },
];

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

Now your app will work already and you shouldn’t see any error logs – let’s continue with the Firebase interaction.

Creating a Firebase Data Service

We can use the AngularFire service from all of our pages – but I think it makes sense to still keep the interaction with Firebase in a specific service which will simply return the data to our pages later.

Therefore we’ve created a service upfront. In this service we will store a reference to the ideas collection which is basically a link to one collection in our Firestore database.

Through this connection we receive all information about current documents but also add, remove and update documents.

We also got this strange map() block in the snapshotChanges function. This means, whenever the data changes this block will triggered and we transform the data a bit – because we need both the real data of the document but also the ID so we can apply changes to documents later, otherwise this key would not exist in the response object.

All further functionality is the simple usage of AngularFire on our collection reference. Only for getting one idea by id we add some more rxjs code. It’s mostly the same case like before – the document itself doesn’t contain its ID, therefore we map the data so it now also has it.

That’s just to make our life easier at a later point but nothing mandatory to have! Ok enough talking, here’s the code for your services/idea.service.ts

import { Injectable } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument, DocumentReference } from '@angular/fire/firestore';
import { map, take } from 'rxjs/operators';
import { Observable } from 'rxjs';

export interface Idea {
  id?: string,
  name: string,
  notes: string
}

@Injectable({
  providedIn: 'root'
})
export class IdeaService {
  private ideas: Observable<Idea[]>;
  private ideaCollection: AngularFirestoreCollection<Idea>;

  constructor(private afs: AngularFirestore) {
    this.ideaCollection = this.afs.collection<Idea>('ideas');
    this.ideas = this.ideaCollection.snapshotChanges().pipe(
      map(actions => {
        return actions.map(a => {
          const data = a.payload.doc.data();
          const id = a.payload.doc.id;
          return { id, ...data };
        });
      })
    );
  }

  getIdeas(): Observable<Idea[]> {
    return this.ideas;
  }

  getIdea(id: string): Observable<Idea> {
    return this.ideaCollection.doc<Idea>(id).valueChanges().pipe(
      take(1),
      map(idea => {
        idea.id = id;
        return idea
      })
    );
  }

  addIdea(idea: Idea): Promise<DocumentReference> {
    return this.ideaCollection.add(idea);
  }

  updateIdea(idea: Idea): Promise<void> {
    return this.ideaCollection.doc(idea.id).update({ name: idea.name, notes: idea.notes });
  }

  deleteIdea(id: string): Promise<void> {
    return this.ideaCollection.doc(id).delete();
  }
}

We also used the according Typing information for all functions using our very own idea interface that we defined at the top – cool typing in all pages incoming!

Displaying a Firestore Collection List

The first page of our app is the list that will display all documents of the collection. Because we created everything important upfront the actual page doesn’t have a lot of logic, but see self and add this to your pages/idea-list/idea-list.page.ts

import { Component, OnInit } from '@angular/core';
import { IdeaService, Idea } from 'src/app/services/idea.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-idea-list',
  templateUrl: './idea-list.page.html',
  styleUrls: ['./idea-list.page.scss'],
})
export class IdeaListPage implements OnInit {

  private ideas: Observable<Idea[]>;

  constructor(private ideaService: IdeaService) { }

  ngOnInit() {
    this.ideas = this.ideaService.getIdeas();
  }
}

I didn’t lie, did I?

We now have an Observable to which we haven’t subscribed yet so right now you wouldn’t get any data from Firebase at all. We let our view take care of handling the subscription by using the async pipe which is most of the time the cleanest way of handling subscriptions in your view!

In order to create a new idea we also add a little FAB button and also construct the correct routerLink on our items by combining the path with the id of the idea object of each iteration.

Now change your pages/idea-list/idea-list.page.html to this:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>My Ideas</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button routerLink="/idea">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>

  <ion-list>
    <ion-item button [routerLink]="['/idea', idea.id]" *ngFor="let idea of (ideas | async)">
      {{ idea.name }}
    </ion-item>
  </ion-list>
</ion-content>

As you can see the route to our details page is basically the same – it works because we specified that both of these routes resolve to the same page and module in the beginning!

Working with a Firestore Document

This means our detail page either receives an id or not – that’s the way to tell if we are about to create a new document or are simply updating an existing one.

To access this information of the URL we can use the activatedRoute of the Angular router. So if we got an idea we need to load the details for the document which we through our service!

Also, all other functions are simply based on the service function we’ve created upfront. The only thing we need to take care of is how we react (pun intended) to those functions.

Sometimes we might want to display a little toast as information for the user, and sometimes we also want to directly go back once the operation has finished which we can do through the router as well.

Although the code is long there’s not really a lot of magic to it so go ahead and change your pages/idea-details/idea-details.page.ts:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { IdeaService, Idea } from 'src/app/services/idea.service';
import { ToastController } from '@ionic/angular';

@Component({
  selector: 'app-idea-details',
  templateUrl: './idea-details.page.html',
  styleUrls: ['./idea-details.page.scss'],
})
export class IdeaDetailsPage implements OnInit {

  idea: Idea = {
    name: '',
    notes: ''
  };

  constructor(private activatedRoute: ActivatedRoute, private ideaService: IdeaService,
    private toastCtrl: ToastController, private router: Router) { }

  ngOnInit() { }

  ionViewWillEnter() {
    let id = this.activatedRoute.snapshot.paramMap.get('id');
    if (id) {
      this.ideaService.getIdea(id).subscribe(idea => {
        this.idea = idea;
      });
    }
  }

  addIdea() {
    this.ideaService.addIdea(this.idea).then(() => {
      this.router.navigateByUrl('/');
      this.showToast('Idea added');
    }, err => {
      this.showToast('There was a problem adding your idea :(');
    });
  }

  deleteIdea() {
    this.ideaService.deleteIdea(this.idea.id).then(() => {
      this.router.navigateByUrl('/');
      this.showToast('Idea deleted');
    }, err => {
      this.showToast('There was a problem deleting your idea :(');
    });
  }

  updateIdea() {
    this.ideaService.updateIdea(this.idea).then(() => {
      this.showToast('Idea updated');
    }, err => {
      this.showToast('There was a problem updating your idea :(');
    });
  }

  showToast(msg) {
    this.toastCtrl.create({
      message: msg,
      duration: 2000
    }).then(toast => toast.present());
  }
}

One Note: In a first attempt I had the logic for loading the data for one idea inside the ngOnInit which caused the app to freeze after some fast navigation back and forth, that’s why I moved the block to one of the Ionic lifecycle events. This issue might need deeper investigation.

For now though we wrap up our app by completing the details view of our idea page that needs input fields for the name and notes of an idea.

The only cool thing here is the footer which we use to display the buttons we need – either with operations to update or delete the idea or to save it. We could add even more logic as it might flash up right now as we set the initial value of idea as if we would always edit the idea.

Maybe getting the id inside onInit and calling the subscribe in viewEnter might work to prevent this!

Anyhow, for now finish your view by changing the pages/idea-details/idea-details.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <ion-title>Idea Details</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <ion-item>
    <ion-label position="stacked">Name</ion-label>
    <ion-input [(ngModel)]="idea.name"></ion-input>
  </ion-item>

  <ion-item>
    <ion-label position="stacked">Notes</ion-label>
    <ion-textarea [(ngModel)]="idea.notes" rows="8"></ion-textarea>
  </ion-item>
</ion-content>

<ion-footer *ngIf="!idea.id">
  <ion-toolbar color="success">
    <ion-button expand="full" fill="clear" color="light" (click)="addIdea()">
      <ion-icon name="checkmark" slot="start"></ion-icon>
      Add Idea
    </ion-button>
  </ion-toolbar>
</ion-footer>

<ion-footer *ngIf="idea.id">
  <ion-row no-padding text-center>
    <ion-col size="6">
      <ion-button expand="block" fill="outline" color="danger" (click)="deleteIdea()">
        <ion-icon name="trash" slot="start"></ion-icon>
        Delete
      </ion-button>
    </ion-col>
    <ion-col size="6">
      <ion-button expand="block" fill="solid" color="success" (click)="updateIdea()">
        <ion-icon name="save" slot="start"></ion-icon>
        Update
      </ion-button>
    </ion-col>
  </ion-row>
</ion-footer>

You can now run your app (browser or device; doesn’t matter) and play around with your connection to Firebase.

The coolest thing is still to observe how your database is updated in realtime so open it in another tab like the one below and see how your cool elements roll in or get removed!

ionic-4-firebase-firestore

Conclusion

Firebase is one of the best choices as a backend for Ionic developers because what you’ve seen above – the integration works in minutes and you got a fully functional realtime database.

Take some more time and you have an authentication system, file storage and even more!

If you want to learn more about Ionic & Firebase you can become a member of the Ionic Academy today and learn everything Ionic!

You can also find a video version of this tutorial below.

The post How to Build An Ionic 4 App with Firebase and AngularFire 5 appeared first on Devdactic.

How to Build Your SaaS With Javascript (and why it’s Awesome)

$
0
0

Last year I started a cool idea with a friend together and although the idea was pretty basic, it took me months to implement the first version.

Why?

Because although we live in great times where we can include epic services – sometimes even for free – using all of them in your code and combining each of them to form a startup app is a process of trial & error if you don’t really know what you need or want, or if you just never worked with all of these services.

I’ve been there, and I have made the research for you. Now in order to help other developers save those countless hours I spent, I created The Software Startup Manual – my first Full Stack course on building a complete front & backend using Javascript.

This course will teach you two things:

  • How to build a server with Node.js and MongoDB to create a REST API that you can use with any frontend
  • How to build an Ionic 4 app that consumes your API – deployed as both a website and native app!

But right now let’s take a step back and look what you got to setup to make your idea shine.

“Just some Boilerplate”, right?

In the beginning there is the great idea – and be it just a better version of Todoist or your favorite notes application. You can feel the fire burning and are powered up for all the cool things you will implement. Until….

Until you realise that there are a few things you definitely need for your first version:

I don’t want to shatter your dreams and aspirations, and there might be special cases where you need different things in the beginning and do other things simply by hand. But this rather short list is a good starting point in general.

startup-app-boilerplate

The good thing is that each of them isn’t hard to integrate in general, but they all take time. Time you could spent on the important aspects of your MVP. And if you do it the first time, you can multiply the time you need with 2 because you have to lookup everything at least once.

Up until this point you haven’t even added any real functionality – all the business logic that your app needs comes once you are done with the mandatory basics or the boilerplate code.

So to get your app to the point where you are fine with it (which depends from developer to developer), it already takes you a lot of time.

At least thats what happened to me back then, which was the reason that I based the Software Startup Manual on these issues.

Not-mandatory-but-should-exist Features

After you got the basics right you work on some core functionality and feel pretty good about your progress – until you read a post about automatic email sending. Or Push Notifications. Or *insert cool tech*.

startup-app-mailgun-email

Then, one by one, you start digging into these non-essential things that can rise the quality of your application a lot.

So wouldn’t it be great to:

  • Send welcome Emails using Mailgun?
  • Show Push Notifications with OneSignal if relevant things to the user happens?
  • Upload user generated content to some storage – maybe AWS S3?

And one by one technology you improve the quality of your app – and got to dig more into each of these services to somehow integrate them into your backend.

Just like before – none of these things is rocket science and with enough time you might figure things out. But trust me, getting a guide on just using them correctly would save you many days of work.

The amazing thing throughout all of these core and nice to have features is that we can use every service with Javascript these days – most of the time completely for free!

With just one language you are able to build an amazing backend with a lot of fancy technology under the hood – and if you already come from Angular or any other popular framework you’ll feel right at home.

And if you have used another language before I’m sure you will catch up on Javascript rapidly!

Where is the Frontend?

If you happen to be a real startup with a dev team, chances are good you have a dedicated backend and frontend team.

That’s not always the case when you got a cool idea with your best friend – especially if the friend is not a developer.

Spotlight on you!

The good news is that when you know Javascript you got many choices to build your frontend. It’s still work, but you got choices because you can use your previously created REST API from basically everything you build now.

But if you want to get your startup out, most likely you (still) need a website today. And if you have some cool functionality, chances are high you also want a mobile app at the same time!

My recommended framework of choice for this is 100% the Ionic Framework.

Yes I might be biased given I also run the Ionic Academy, but here’s why I would recommend it anyway and use it again as well:

With Ionic, you will have everything you need in one codebase. You can go mobile for iOS and Android App stores, you can deploy it as a website and if you might need it you could even convert it to a desktop application.

Oh and of course you get first class PWA support.

I don’t know about you but I appreciate to write the code only once and enjoy how it works on multiple platform and screen sizes!

ssm-responsive-app

But we don’t want to blow the trumpet for Ionic too hard (didn’t we already?) and just concede it’s one of the best ways to build fast for multiple platforms. There are other players like React, Flutter and smaller options but they don’t bet on the web as hard as Ionic does which gives you the chance to basically run your app everywhere with the same code.

How to get everything Deployed & Live?

Now once you reach a point where you got a solid backend and a functional frontend that works for the majority of platforms that you want to cover, it’s time to release everything (who needs testing anyway?).

Hopefully you know a thing or 2 about how to host and connect everything, otherwise you face the next challenge at this point.

So here’s what you can/should do:

  • Deploy your Node.js Server to Heroku
  • Connect the MongoDB and all other needed live instances to your Server
  • Upload your Ionic app as a website to some hosting like Firebase
  • Submit your native app builds for iOS and Android to their according stores

The backend part is actually the easier one at this point as most services (not just Heroku) make it super simple to upload and build your app in basically one command.

saas-deploy-heroku

To release your app in the app stores, you have to pay a fee for both the iOS and also Android membership (while the second is a lot cheaper and only one time). The process of creating the app store entries is also pretty boring but time consuming.

At some point you got all the texts, assets and information filled out so the classic waiting game begins!

The teams will review your app and hopefully accept the submission. Then your app is finally live after everything you’ve gone through!

The Alternatives

With all that being said, of course there are many crossroads where you could have picked a different route. It starts with the backend, where some of you might immediately scream “Why not Firebase?” and yes, that would be a very legit and to some degree easier solution (and would also hone your Javascript skills!).

While Firebase is a great choice in many projects, sometimes you just want to have full control. You don’t need to rely on cloud functions to write custom code, you won’t hit any walls that prevent changing some texts or elements and you are simply more free to do whatever you want to do.

Additionally, there’s basically an alternative to every tool, service and framework I presented before that you could have picked if you enjoy it more or if you happen to have more money (everything shown here was free besides app store membership).

There are many ways to build your next startup app – this is one relying on Javascript and reusing your skills & code as best as possible to save you time.

Don’t be scared!

If you started this post all fired up and now feel confused and scared by all the choices you have to make trust me: You don’t have to!

All of this is a process, and when you go through all of this the first time it’s an exploration and learning journey.

My offer is to make your learning journey a bit more guided by showing you how to use everything we talked about inside my Software Startup Manual course. We’ve got more than 8 hours of video material to get you some nice basic SaaS app with all the cool technologies you need.

Even if you are a beginner you can manage your way through all of this – remember that every developer started with 0 knowledge at some point!

Hopefully the overview of tools and services I used inspires others to start and make their own projects as well.

If you did or do, leave a comment below and let me know what you are working on!

You can also watch a video version with some notes on this below.

The post How to Build Your SaaS With Javascript (and why it’s Awesome) appeared first on Devdactic.

How to Build an Ionic 4 Calendar App

$
0
0

Because there is still no calendar component directly shipping with Ionic 4 it’s time to revisit how to build one yourself using a great package we already used in a previous post.

The problem with creating a tutorial like this is that the need for a calendar view varies in almost every app. We’ll dive into building a calendar using the ionic2-calendar package which offers some different views and can be customised to some degree, but if you have very specific requirements you might need to create everything from the ground up yourself.

ionic-4-calendar

I’ve also talked about alternatives and using a different package inside Building a Calendar for Ionic With Angular Calendar & Calendar Alternatives.

Starting our Calendar

We start with a blank app and add our calendar package. Don’t be shocked by the name – it still works great with v4 so go ahead and run:

ionic start devdacticCalendar blank --type=angular
cd devdacticCalendar
npm i ionic2-calendar

Now we don’t need to add it to our main module but to the actual module file of the page where we want to add the calendar. In our app we already got the default page so let’s simply add it to the app/home/home.module.ts like:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { NgCalendarModule  } from 'ionic2-calendar';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    NgCalendarModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

There are no further imports for dates or whatsoever right now and we can dive right into it!

Creating the Calendar View

The first part of our view consists of a bunch of elements that we need to control the calendar and add new events.

We can control different aspects of the calendar, for example:

  • Set the view to focus today
  • Change the mode of the calendar which changes the view to month/week/day
  • Move our calendar view forward/back

Also we add a little card to create a new event to our calendar because it will be pretty empty. You can also take a look at the demo code which uses a random events function to fill your calendar for testing!

All of these things are not really essential and the important part is at the end of our view: The actual calendar component!

On this component we can set a lot of models and functions and the best way to explore all of them is the options of the package.

You will see what the functions do and what’s used once we move on to the class, so for now just open your app/home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      {{ viewTitle }}
    </ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="today()">Today</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>

  <!-- Card for adding a new event -->
  <ion-card>
    <ion-card-header tappable (click)="collapseCard = !collapseCard">
      <ion-card-title>New Event</ion-card-title>
    </ion-card-header>
    <ion-card-content *ngIf="!collapseCard">

      <ion-item>
        <ion-input type="text" placeholder="Title" [(ngModel)]="event.title"></ion-input>
      </ion-item>
      <ion-item>
        <ion-input type="text" placeholder="Description" [(ngModel)]="event.desc"></ion-input>
      </ion-item>
      <ion-item>
        <ion-label>Start</ion-label>
        <ion-datetime displayFormat="MM/DD/YYYY HH:mm" pickerFormat="MMM D:HH:mm" [(ngModel)]="event.startTime" [min]="minDate"></ion-datetime>
      </ion-item>
      <ion-item>
        <ion-label>End</ion-label>
        <ion-datetime displayFormat="MM/DD/YYYY HH:mm" pickerFormat="MMM D:HH:mm" [(ngModel)]="event.endTime" [min]="minDate"></ion-datetime>
      </ion-item>
      <ion-item>
        <ion-label>All Day?</ion-label>
        <ion-checkbox [(ngModel)]="event.allDay"></ion-checkbox>
      </ion-item>
      <ion-button fill="outline" expand="block" (click)="addEvent()" [disabled]="event.title == ''">Add Event</ion-button>

    </ion-card-content>
  </ion-card>

  <ion-row>
    <!-- Change the displayed calendar mode -->
    <ion-col size="4">
      <ion-button expand="block" [color]="calendar.mode == 'month' ? 'primary' : 'secondary'" (click)="changeMode('month')">Month</ion-button>
    </ion-col>
    <ion-col size="4">
      <ion-button expand="block" [color]="calendar.mode == 'week' ? 'primary' : 'secondary'" (click)="changeMode('week')">Week</ion-button>
    </ion-col>
    <ion-col size="4">
      <ion-button expand="block" [color]="calendar.mode == 'day' ? 'primary' : 'secondary'" (click)="changeMode('day')">Day</ion-button>
    </ion-col>

    <!-- Move back one screen of the slides -->
    <ion-col size="6" text-left>
      <ion-button fill="clear" (click)="back()">
        <ion-icon name="arrow-back" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-col>

    <!-- Move forward one screen of the slides -->
    <ion-col size="6" text-right>
      <ion-button fill="clear" (click)="next()">
        <ion-icon name="arrow-forward" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-col>
  </ion-row>

  <calendar 
  [eventSource]="eventSource" 
  [calendarMode]="calendar.mode" 
  [currentDate]="calendar.currentDate"
  (onEventSelected)="onEventSelected($event)"
  (onTitleChanged)="onViewTitleChanged($event)"
  (onTimeSelected)="onTimeSelected($event)" 
  startHour="6"
  endHour="20"
  step="30"
  startingDayWeek="1">
  </calendar>

</ion-content>

The only fixed properties we have set are these:

  • startHour/endHour: Pretty self explanatory, right?
  • step: How detailed the hour blocks will be divided in the week/day view
  • startingDayWeek: Welcome all US friends, in Germany the week starts on Monday so put a 0 or 1 here whatever you prefer

Now we need to put some logic behind all of this.

Note: We are using the calendar in its basic version. By now there’s really a lot you can customise, especially by using custom templates for cells and different other slots that you can easily add. If it doesn’t do the job immediately for you try to play around with these elements to make it fit your needs.

Adding the Calendar Logic

We start with the basics for adding new events. For this we will keep an array eventSource that we also previously connected to the calendar.

If you have an API where you pull the data from, you gonna add the data to this source as well!

Also, normally the events only consists of a title, the times and an all day flag but actually you can have more info in there like a description, id or whatever – you will see that it get’s passed through when you click on an even later.

We also have to make some date transformation as the ion-datetime expects an ISO string for the date while the calendar wants it to be a Date object.

Finally, if the new event should be an all day event we have to manually patch the starting and end times using a little transformation as well.

Now go ahead with the first part of your app/home/home.page.ts:

import { CalendarComponent } from 'ionic2-calendar/calendar';
import { Component, ViewChild, OnInit, Inject, LOCALE_ID } from '@angular/core';
import { AlertController } from '@ionic/angular';
import { formatDate } from '@angular/common';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {

  event = {
    title: '',
    desc: '',
    startTime: '',
    endTime: '',
    allDay: false
  };

  minDate = new Date().toISOString();

  eventSource = [];
  viewTitle;

  calendar = {
    mode: 'month',
    currentDate: new Date(),
  };

  @ViewChild(CalendarComponent) myCal: CalendarComponent;

  constructor(private alertCtrl: AlertController, @Inject(LOCALE_ID) private locale: string) { }

  ngOnInit() {
    this.resetEvent();
  }

  resetEvent() {
    this.event = {
      title: '',
      desc: '',
      startTime: new Date().toISOString(),
      endTime: new Date().toISOString(),
      allDay: false
    };
  }

  // Create the right event format and reload source
  addEvent() {
    let eventCopy = {
      title: this.event.title,
      startTime:  new Date(this.event.startTime),
      endTime: new Date(this.event.endTime),
      allDay: this.event.allDay,
      desc: this.event.desc
    }

    if (eventCopy.allDay) {
      let start = eventCopy.startTime;
      let end = eventCopy.endTime;

      eventCopy.startTime = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()));
      eventCopy.endTime = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate() + 1));
    }

    this.eventSource.push(eventCopy);
    this.myCal.loadEvents();
    this.resetEvent();
  }
}

Now we are already able to add events to the calendar, but we also need some more functions that we connected inside the view previously.

Let’s talk about the them real quick so we can finish our basic calendar implementation:

  • next/back: Manually change the view (which normally works by swiping) using the underlying Swiper instance because the calendar uses Ionic Slides
  • changeMode: Change between the month/week/day views
  • today: Focus back to the current date inside the calendar
  • onViewTitleChanged: The view changed and the title is automatic updated

Next to these basic functionality we also receive the click event when we select an event inside the calendar.

As said before, here we get the full object from our source so if you use some API data you now got the ID and can easily show the details for an event. In our case we use the Angular date pie from code to transform the dates and then just present an alert for testing.

Finally whenever you click on a time slot in the calendar the onTimeSelected will be triggered and we use it to set the current start and end date of our event.

Normally you might want to immediately open an overlay to create a new event when a user clicks on a time slot like they are used to from Google or Apple calendar! We also have to make sure to use ISO strings for the date again at this point because of the component we used.

Now wrap up everything by adding these functions below your existing code inside the app/home/home.page.ts but of course still inside the class:

// Change current month/week/day
 next() {
  var swiper = document.querySelector('.swiper-container')['swiper'];
  swiper.slideNext();
}

back() {
  var swiper = document.querySelector('.swiper-container')['swiper'];
  swiper.slidePrev();
}

// Change between month/week/day
changeMode(mode) {
  this.calendar.mode = mode;
}

// Focus today
today() {
  this.calendar.currentDate = new Date();
}

// Selected date reange and hence title changed
onViewTitleChanged(title) {
  this.viewTitle = title;
}

// Calendar event was clicked
async onEventSelected(event) {
  // Use Angular date pipe for conversion
  let start = formatDate(event.startTime, 'medium', this.locale);
  let end = formatDate(event.endTime, 'medium', this.locale);

  const alert = await this.alertCtrl.create({
    header: event.title,
    subHeader: event.desc,
    message: 'From: ' + start + '<br><br>To: ' + end,
    buttons: ['OK']
  });
  alert.present();
}

// Time slot was clicked
onTimeSelected(ev) {
  let selected = new Date(ev.selectedTime);
  this.event.startTime = selected.toISOString();
  selected.setHours(selected.getHours() + 1);
  this.event.endTime = (selected.toISOString());
}

You now got a fully functional calendar with a lot of ways to customise the appearance and react to user input and different events!

Conclusion

The Ionic2-calendar package remains one of the best ways to add a calendar to your Ionic app fast. If you depend on drag & drop functionality you still have to look somewhere else as this is not yet included (maybe in the future?).

Have you used a better calendar inside your Ionic app yet?

Let me know your experiences in the comments!

You can also find a video version of this article below.

The post How to Build an Ionic 4 Calendar App appeared first on Devdactic.

How to Build A Simple Ionic 4 WordPress Client

$
0
0

As basically the whole internet is powered by WordPress, chances are high you have to interact with it at if a client approaches you for a mobile app.

Because by now WordPress includes a great REST API we can easily connect our Ionic app to the data it holds. While we previously used the wp-api-angular package, it seems like the plugin is basically dead and not maintained so let’s just use the API directly!

ionic-4-wordpress

Setting up our Ionic WordPress App

We start with a blank Ionic 4 app and just create some additional pages and a service, so no further plugin needed!

ionic start devdacticWordpress blank --type=angular
cd devdacticWordpress

ionic g page pages/posts
ionic g page pages/post
ionic g service services/wordpress

Now we also need to include the module to make Http calls to the API so change your app/app.module.ts to:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Finally we need to setup our routing. We will have a list of WordPress posts and also a details page for one post. Therefore, connect the two pages we created before like this inside our app/app-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'posts', pathMatch: 'full' },
  { path: 'posts', loadChildren: './pages/posts/posts.module#PostsPageModule' },
  { path: 'posts/:id', loadChildren: './pages/post/post.module#PostPageModule' },
];

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

Now we start at the list page and can navigate into the details of one specific blog post!

Creating the WordPress Service

Before we go into creating our views we setup the logic to retrieve all data. In our post we will simply make a call to get a list of posts, but you can get basically all data from the WordPress API like categories, tags, pages…

We will also limit our calls to only retrieve 5 posts at a time and also use the page parameter so we can later add some cool loading to our list.

Besides that we need to make another change if you want to know how many pages/results the API actually holds:
First, we have to add the observe key to our options and pass response as the value. But because this results in a Typescript error, we also need to have it as 'body' but maybe this will be fixed anytime soon.

Now the response of the API call holds more information than normally – it also contains the header fields! In this value we can find x-wp-totalpages and x-wp-total which are the actual information we need!

I also added a little conversion directly here so we can later access the image more easily which is buried deep down in the response otherwise. Same for the function to retrieve a single post of course.

All of this goes into your app/services/wordpress.service.ts like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class WordpressService {

  url = `https://yourwordpresspage.com/wp-json/wp/v2/`;
  totalPosts = null;
  pages: any;

  constructor(private http: HttpClient) { }

  getPosts(page = 1): Observable<any[]> {
    let options = {
      observe: "response" as 'body',
      params: {
        per_page: '5',
        page: ''+page
      }
    };

    return this.http.get<any[]>(`${this.url}posts?_embed`, options).pipe(
      map(resp => {
        this.pages = resp['headers'].get('x-wp-totalpages');
        this.totalPosts = resp['headers'].get('x-wp-total');

        let data = resp['body'];

        for (let post of data) {
          post.media_url = post['_embedded']['wp:featuredmedia'][0]['media_details'].sizes['medium'].source_url;
        }
        return data;
      })
    )
  }

  getPostContent(id) {
    return this.http.get(`${this.url}posts/${id}?_embed`).pipe(
      map(post => {
        post['media_url'] = post['_embedded']['wp:featuredmedia'][0]['media_details'].sizes['medium'].source_url;
        return post;
      })
    )
  }
}

Just make sure you insert your own WordPress blog URL of course.

Now we got everything prepared and can use our functions to display our WordPress posts easily!

Listing Your WordPress Posts

We start with the list where we need to load our first handful of posts right in the beginning. At this point you also got access to the count and pages variable that we retrieved previously, but we don’t really use it a lot in here.

The reason is that we can also implement infinite scrolling inside our view which will automatically load new posts once we reach (or approach) the end of the list!

That’s also the reason why we keep track of the current page. You could also structure this with some sort of pagination and number of pages like you are used on a website, however this pattern here makes more sense for a mobile app.

Finally, if you reach the final page you should also disable the infinite loading like we do inside our loadMore() function.

Now go ahead and change your pages/posts/posts.page.ts to:

import { WordpressService } from './../../services/wordpress.service';
import { Component, OnInit } from '@angular/core';
import { LoadingController } from '@ionic/angular';

@Component({
  selector: 'app-posts',
  templateUrl: './posts.page.html',
  styleUrls: ['./posts.page.scss'],
})
export class PostsPage implements OnInit {

  posts = [];
  page = 1;
  count = null;

  constructor(private wp: WordpressService, private loadingCtrl: LoadingController) { }

  ngOnInit() {
    this.loadPosts();
  }

  async loadPosts() {
    let loading = await this.loadingCtrl.create({
      message: 'Loading Data...'
    });
    await loading.present();

    this.wp.getPosts().subscribe(res => {
      this.count = this.wp.totalPosts;
      this.posts = res;
      loading.dismiss();
    });
  }

  loadMore(event) {
    this.page++;

    this.wp.getPosts(this.page).subscribe(res => {
      this.posts = [...this.posts, ...res];
      event.target.complete();

      // Disable infinite loading when maximum reached
      if (this.page == this.wp.pages) {
        event.target.disabled = true;
      }
    });
  }

}

The view now just consists of a list of cards that make use of some data that is stored in the response. Just log it to your console and you will see a ton of information in there!

Some of those fields contain HTML characters, that’s why we sometimes use the innerHTML of elements and directly use the value which will then be displayed correctly.

If you want infinite loading, simply add it to the bottom of your page and attach the appropriate loading function. You can also define a text or loading indicator like described on the docs!

To build our simple view, go ahead and insert into your pages/posts/posts.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Devdactic Blog</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <div text-center *ngIf="count">Found {{ count }} posts</div>

  <ion-card *ngFor="let post of posts">
    <ion-card-header>
      <ion-card-title [innerHTML]="post.title.rendered"></ion-card-title>
      <ion-card-subtitle>{{ post.date_gmt | date }}</ion-card-subtitle>
    </ion-card-header>
    <ion-card-content>
      <img [src]="post.media_url">
      <div [innerHTML]="post.excerpt.rendered"></div>
      <ion-button expand="full" fill="clear" [routerLink]="['/', 'posts', post.id]" text-right>Read More...</ion-button>
    </ion-card-content>
  </ion-card>

  <ion-infinite-scroll threshold="100px" (ionInfinite)="loadMore($event)">
    <ion-infinite-scroll-content loadingText="Loading more posts...">
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>

</ion-content>

We try to keep it simple so we just use the posts, but we could also easily load all pages and display them in a side menu for example!

Showing The WordPress Post Details

We also added a button to our cards below the excerpt with the routerLink in order to reach the details page with the ID of the post.

We could actually also pass the whole object to the next page using the state, but I still think using the ID approach is better as we don’t keep the information in the state and we actually got a URL to the post page that will work all the time, even with refresh!

For us this means we need to get the ID from the route snapshot and then retrieve the information for a single post using our service!

I just added a functionality to open the actual post page as well to make some us of the data of the post, but of course there’s a lot more you could do with the data.

For now simply change your pages/post/post.page.ts to:

import { WordpressService } from './../../services/wordpress.service';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-post',
  templateUrl: './post.page.html',
  styleUrls: ['./post.page.scss'],
})
export class PostPage implements OnInit {
  post: any;

  constructor(private route: ActivatedRoute, private wp: WordpressService) { }

  ngOnInit() {
    let id = this.route.snapshot.paramMap.get('id');
    this.wp.getPostContent(id).subscribe(res => {
      this.post = res;
    });
  }

  openOriginal() {
    // Add InAppBrowser for app if want
    window.open(this.post.link, '_blank');
  }
}

Regarding the view of the details page we’ll also keep it simple and just use the image (like we did before, remember the media_url is a field we created in the service!) and the actual post content.

Again, to display the HTML correctly we need to use innerHTML which will render the tags correctly.

There’s really not a lot more to the page, so finish your simple Ionic WordPress app by changing your pages/post/post.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/posts"></ion-back-button>
    </ion-buttons>
    <ion-title>{{ post?.title.rendered }}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  
  <div *ngIf="post">
    <img [src]="post.media_url" [style.width]="'100%'">
    <div [innerHTML]="post.content.rendered" padding></div>
  </div>

</ion-content>

<ion-footer color="secondary">
  <ion-toolbar>
    <ion-button expand="full" fill="clear" (click)="openOriginal()">
      Open Original
    </ion-button>
  </ion-toolbar>
</ion-footer>

Another idea at this point would be to add social sharing which you very well might need if you want your articles to get shared directly from your WordPress app!

Conclusion

You don’t need a package to make use of the WordPress REST API – simply make calls to the endpoints like described in their reference and use that data directly inside your Ionic app!

There are so many ways to extend this and build Ionic apps powered with WordPress data – have you built one before or plan to do so?

You can also find a video version of this tutorial below.

The post How to Build A Simple Ionic 4 WordPress Client appeared first on Devdactic.


How to Build an Ionic 4 App with SQLite Database & Queries (And Debug It!)

$
0
0

If your app needs a solid database or you already got data that you want to inject in your Ionic application, there’s the great underlying SQLite database inside that week can use just like any other storage engine to store our data.

But the usage of the SQLite database is a bit more tricky than simply using Ionic Storage, so in this tutorial we will go through all the steps needed to prepare your app, inject some seed data and finally make different SQL queries on our database.
ionic-4-sqlite

This tutorial is by no means a general SQL introduction, you should know a bit about it when you incorporate this into your Ionic 4 app!

Setting up our SQLite App

To get started we create a blank new app, add two pages and a service so we got something to work with and then install both the SQLite package and also the SQLite porter package plus the according Cordova plugins.

Now go ahead and run:

ionic start devdacticSql blank --type=angular
cd devdacticSql

ionic g service services/database
ionic g page pages/developers
ionic g page pages/developer

npm install @ionic-native/sqlite @ionic-native/sqlite-porter

ionic cordova plugin add cordova-sqlite-storage
ionic cordova plugin add uk.co.workingedge.cordova.plugin.sqliteporter

As said in the beginning, we will inject some initial seed data that you might have taken from your existing database. You could also infject JSON data using the porter plugin as well!

So for our case I created a simple file at assets/seed.sql and added this data for testing:

CREATE TABLE IF NOT EXISTS developer(id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT,skills TEXT,img TEXT);
INSERT or IGNORE INTO developer VALUES (1, 'Simon', '', 'https://pbs.twimg.com/profile_images/858987821394210817/oMccbXv6_bigger.jpg');
INSERT or IGNORE INTO developer VALUES (2, 'Max', '', 'https://pbs.twimg.com/profile_images/953978653624455170/j91_AYfd_400x400.jpg');
INSERT or IGNORE INTO developer VALUES (3, 'Ben', '', 'https://pbs.twimg.com/profile_images/1060037170688417792/vZ7iAWXV_400x400.jpg');

CREATE TABLE IF NOT EXISTS product(id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT, creatorId INTEGER);
INSERT or IGNORE INTO product(id, name, creatorId) VALUES (1, 'Ionic Academy', 1);
INSERT or IGNORE INTO product(id, name, creatorId) VALUES (2, 'Software Startup Manual', 1);
INSERT or IGNORE INTO product(id, name, creatorId) VALUES (3, 'Ionic Framework', 2);
INSERT or IGNORE INTO product(id, name, creatorId) VALUES (4, 'Drifty Co', 2);
INSERT or IGNORE INTO product(id, name, creatorId) VALUES (5, 'Drifty Co', 3);
INSERT or IGNORE INTO product(id, name, creatorId) VALUES (6, 'Ionicons', 3);

This SQL should create 2 tables in our database and inject a few rows of data. As you might have seen from the data, there are developers and also products, and products have the creatorId as a foreign key so we can build a nice join later!

Before using the plugins, like always, you need to make sure you add them to your app/app.module.ts and also the HttpClientModule as we need it to load our local SQL dump file, so go ahead and change it to:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { SQLitePorter } from '@ionic-native/sqlite-porter/ngx';
import { SQLite } from '@ionic-native/sqlite/ngx';

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    SQLite,
    SQLitePorter
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Finally we need to make a tiny change to our routing to include the two pages we created. One will host lists of data, the second is to display the details for one entry so simply change the app/app-routing.module.ts to:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'developers', pathMatch: 'full' },
  { path: 'developers', loadChildren: './pages/developers/developers.module#DevelopersPageModule' },
  { path: 'developers/:id', loadChildren: './pages/developer/developer.module#DeveloperPageModule' },
];

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

Alright, that’s everything for the setup phase, let’s dive into using all of this.

Accessing our SQLite Database with a Service

Because you don’t want to end up with all the database calls in your pages we build a service that holds all relevant functionality that our app needs (which is always a good idea).

In the constructor of our service we first need to perform a few steps:

  1. Wait until the platform is ready
  2. Create the database file, which will also open it if it already exists
  3. Fill the Database with our initial SQL data

All of this is simply chaining the different actions. But what’s more important is how to let your classes know the database is ready?

For this we are using a BehaviourSubject like we did in other scenarios with user authentication as well.

That means, our classes can later easily subscribe to this state to know when the database is ready. And we will also use this mechanism to store the data in our service as well in order to limit SQL queries.

But before we get into the SQL queries, go ahead with the first part of our services/database.service.ts and insert:

import { Platform } from '@ionic/angular';
import { Injectable } from '@angular/core';
import { SQLitePorter } from '@ionic-native/sqlite-porter/ngx';
import { HttpClient } from '@angular/common/http';
import { SQLite, SQLiteObject } from '@ionic-native/sqlite/ngx';
import { BehaviorSubject, Observable } from 'rxjs';

export interface Dev {
  id: number,
  name: string,
  skills: any[],
  img: string
}

@Injectable({
  providedIn: 'root'
})
export class DatabaseService {
  private database: SQLiteObject;
  private dbReady: BehaviorSubject<boolean> = new BehaviorSubject(false);

  developers = new BehaviorSubject([]);
  products = new BehaviorSubject([]);

  constructor(private plt: Platform, private sqlitePorter: SQLitePorter, private sqlite: SQLite, private http: HttpClient) {
    this.plt.ready().then(() => {
      this.sqlite.create({
        name: 'developers.db',
        location: 'default'
      })
      .then((db: SQLiteObject) => {
          this.database = db;
          this.seedDatabase();
      });
    });
  }

  seedDatabase() {
    this.http.get('assets/seed.sql', { responseType: 'text'})
    .subscribe(sql => {
      this.sqlitePorter.importSqlToDb(this.database, sql)
        .then(_ => {
          this.loadDevelopers();
          this.loadProducts();
          this.dbReady.next(true);
        })
        .catch(e => console.error(e));
    });
  }

  getDatabaseState() {
    return this.dbReady.asObservable();
  }

  getDevs(): Observable<Dev[]> {
    return this.developers.asObservable();
  }

  getProducts(): Observable<any[]> {
    return this.products.asObservable();
  }
}

Now we can focus on the second part which contains the actual SQL fun. Yeah!

Everything we do in the following functions is based on calling executeSql() on our database to perform whatever query you want to do.

The problematic part is actually going on once you get the data from database, as you need to iterate the rows of the result to get all the entries. For example to get all developers, we need to go through all rows and add them one by one to another array variable before we can finally use it.

In this example we also added a Typescript interface called Dev but that’s just some syntactic sugar on top!

Note: Because things were to easy I also wanted to show how to store an array under “skills”. To do so, you have to convert your data a few times and you’ll see it multiple times along the tutorial. When we read the data from the database we have to parse the JSON that we have written to it, because you can’t store an array as array in SQLite databases, that type doesn’t exist!

When you are done iterating the data you could return it, but we decided to simply call next() on our BehaviourSubject for developers (and later same for products) so again, everyone subscribed will receive the change!

By doing this you classes don’t have to make an additional call to receive new data all the time.

Let’s go ahead and add the second part inside your services/database.service.ts:

loadDevelopers() {
    return this.database.executeSql('SELECT * FROM developer', []).then(data => {
      let developers: Dev[] = [];

      if (data.rows.length > 0) {
        for (var i = 0; i < data.rows.length; i++) {
          let skills = [];
          if (data.rows.item(i).skills != '') {
            skills = JSON.parse(data.rows.item(i).skills);
          }

          developers.push({ 
            id: data.rows.item(i).id,
            name: data.rows.item(i).name, 
            skills: skills, 
            img: data.rows.item(i).img
           });
        }
      }
      this.developers.next(developers);
    });
  }

  addDeveloper(name, skills, img) {
    let data = [name, JSON.stringify(skills), img];
    return this.database.executeSql('INSERT INTO developer (name, skills, img) VALUES (?, ?, ?)', data).then(data => {
      this.loadDevelopers();
    });
  }

  getDeveloper(id): Promise<Dev> {
    return this.database.executeSql('SELECT * FROM developer WHERE id = ?', [id]).then(data => {
      let skills = [];
      if (data.rows.item(0).skills != '') {
        skills = JSON.parse(data.rows.item(0).skills);
      }

      return {
        id: data.rows.item(0).id,
        name: data.rows.item(0).name, 
        skills: skills, 
        img: data.rows.item(0).img
      }
    });
  }

  deleteDeveloper(id) {
    return this.database.executeSql('DELETE FROM developer WHERE id = ?', [id]).then(_ => {
      this.loadDevelopers();
      this.loadProducts();
    });
  }

  updateDeveloper(dev: Dev) {
    let data = [dev.name, JSON.stringify(dev.skills), dev.img];
    return this.database.executeSql(`UPDATE developer SET name = ?, skills = ?, img = ? WHERE id = ${dev.id}`, data).then(data => {
      this.loadDevelopers();
    })
  }

  loadProducts() {
    let query = 'SELECT product.name, product.id, developer.name AS creator FROM product JOIN developer ON developer.id = product.creatorId';
    return this.database.executeSql(query, []).then(data => {
      let products = [];
      if (data.rows.length > 0) {
        for (var i = 0; i < data.rows.length; i++) {
          products.push({ 
            name: data.rows.item(i).name,
            id: data.rows.item(i).id,
            creator: data.rows.item(i).creator,
           });
        }
      }
      this.products.next(products);
    });
  }

  addProduct(name, creator) {
    let data = [name, creator];
    return this.database.executeSql('INSERT INTO product (name, creatorId) VALUES (?, ?)', data).then(data => {
      this.loadProducts();
    });
  }

As you can see we implemented all of the basic operations like GET, CREATE, DELETE or UPDATE for the developers table. All of them follow the same scheme, you just need to make sure when you need to refresh your local data or what/how your functions return.

Same goes for the product table, here we are also using a little JOIN but again, that’s basic SQL (and actually my knowledge about more advanced SQL statements was lost after university).

Besides the general SQL queries you can also replace the values of your statements with some data by simply putting in a ? in the right places and then passing an array with arguments as the second parameter to the executeSql() function!

You could also do this inline within the string, that’s totally up to you. I just included some different ways to do it for the different calls in which we need to store data to the DB or find something by its ID.

Loading and Displaying our Database Rows

So by now we have created the layer that will return us all relevant data and functionality, let’s make use of it.

In our pages, especially the first, we need to make sure the database is ready by subscribing to the Observable of the state. Only once we know the database is ready, we can also use it safely.

Actually we could also subscribe to the developers and products before but just in case to make sure everything is fine we do it in there as well. Again in here I show two ways of doing it, you can either subscribe right here to the data and then use it locally or simply use the async pipe inside the view.

We also create the add functionality which only needs to be called on the service – remember, the array of our data will update and emit the new values automatically because we have added the calls in our service upfront after we add data.

At this point you also see another part of the array conversion: In the view it’s a simple input with comma separated values, so here we convert it to an array that will later be stringified for the database.

I know, storing the array would be more easy. Or having another table. Or saving it with commas. So many options to do everything!

Ok now go ahead and change the pages/developers/developers.page.ts to:

import { DatabaseService, Dev } from './../../services/database.service';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-developers',
  templateUrl: './developers.page.html',
  styleUrls: ['./developers.page.scss'],
})
export class DevelopersPage implements OnInit {

  developers: Dev[] = [];

  products: Observable<any[]>;

  developer = {};
  product = {};

  selectedView = 'devs';

  constructor(private db: DatabaseService) { }

  ngOnInit() {
    this.db.getDatabaseState().subscribe(rdy => {
      if (rdy) {
        this.db.getDevs().subscribe(devs => {
          this.developers = devs;
        })
        this.products = this.db.getProducts();
      }
    });
  }

  addDeveloper() {
    let skills = this.developer['skills'].split(',');
    skills = skills.map(skill => skill.trim());

    this.db.addDeveloper(this.developer['name'], skills, this.developer['img'])
    .then(_ => {
      this.developer = {};
    });
  }

  addProduct() {
    this.db.addProduct(this.product['name'], this.product['creator'])
    .then(_ => {
      this.product = {};
    });
  }

}

We’ve already had a lot of code in here so let’s keep the view short. At least the explanation.

We need a way to display both the developers and products, and for this we can use an ion-segment with two views.

Also, both views need a simple input so we can create new developers and also new products. For products, we also use a little select so we can assign a creator to a new product and use the ID of the creator as value. The select comes in really handy there!

That’s basically all the view needs to do. The only further thing is that clicking on a developer will bring us to the details page using the appropriate routerLink.

So go ahead and put this into your pages/developers/developers.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Developers</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <ion-segment [(ngModel)]="selectedView">
    <ion-segment-button value="devs">
      <ion-label>Developer</ion-label>
    </ion-segment-button>
    <ion-segment-button value="products">
      <ion-label>Products</ion-label>
    </ion-segment-button>
  </ion-segment>

  <div [ngSwitch]="selectedView">
    <div *ngSwitchCase="'devs'">
      <ion-item>
        <ion-label position="stacked">What\'s your name?</ion-label>
        <ion-input [(ngModel)]="developer.name" placeholder="Developer Name"></ion-input>
      </ion-item>
      <ion-item>
        <ion-label position="stacked">What are your special skills (comma separated)?</ion-label>
        <ion-input [(ngModel)]="developer.skills" placeholder="Special Skills"></ion-input>
      </ion-item>
      <ion-item>
        <ion-label position="stacked">Your image URL</ion-label>
        <ion-input [(ngModel)]="developer.img" placeholder="https://..."></ion-input>
      </ion-item>
      <ion-button expand="block" (click)="addDeveloper()">Add Developer Info</ion-button>

      <ion-list>
        <ion-item button *ngFor="let dev of developers" [routerLink]="['/', 'developers', dev.id]">
          <ion-avatar slot="start">
            <img [src]="dev.img">
          </ion-avatar>
          <ion-label>
            <h2>{{ dev.name }}</h2>
            <p>{{ dev.skills }}</p>
          </ion-label>
        </ion-item>
      </ion-list>
    </div>

    <div *ngSwitchCase="'products'">
      <ion-item>
        <ion-label position="stacked">Product name</ion-label>
        <ion-input [(ngModel)]="product.name" placeholder="Name"></ion-input>
      </ion-item>
      <ion-item>
        <ion-label position="stacked">Creator?</ion-label>
        <ion-select [(ngModel)]="product.creator">
          <ion-select-option *ngFor="let dev of developers" [value]="dev.id">{{ dev.name }}</ion-select-option>
        </ion-select>
      </ion-item>
      <ion-button expand="block" (click)="addProduct()">Add Product</ion-button>

      <ion-list>
        <ion-item *ngFor="let prod of products | async">
          <ion-label>
            <h2>{{ prod.name }}</h2>
            <p>Created by: {{ prod.creator }}</p>
          </ion-label>
        </ion-item>
      </ion-list>
    </div>
  </div>

</ion-content>

Now you can already run your app but remember, because we use Cordova plugins you need to execute it on a device!

Updating & Deleting data from the SQLite Database

Finally we need the simple view to update or delete a row of data. In there we can again make use of the service to first of all get the details for one database entry using the ID we passed through the route.

Again, to display the skills we need to have a little conversion in both directions when we update the database row. Nothing special besides that, so open your pages/developers/developer.page.ts and change it to:

import { DatabaseService, Dev } from './../../services/database.service';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToastController } from '@ionic/angular';

@Component({
  selector: 'app-developer',
  templateUrl: './developer.page.html',
  styleUrls: ['./developer.page.scss'],
})
export class DeveloperPage implements OnInit {
  developer: Dev = null;
  skills = '';

  constructor(private route: ActivatedRoute, private db: DatabaseService, private router: Router, private toast: ToastController) { }

  ngOnInit() {
    this.route.paramMap.subscribe(params => {
      let devId = params.get('id');

      this.db.getDeveloper(devId).then(data => {
        this.developer = data;
        this.skills = this.developer.skills.join(',');
      });
    });
  }

  delete() {
    this.db.deleteDeveloper(this.developer.id).then(() => {
      this.router.navigateByUrl('/');
    });
  }

  updateDeveloper() {
    let skills = this.skills.split(',');
    skills = skills.map(skill => skill.trim());
    this.developer.skills = skills;

    this.db.updateDeveloper(this.developer).then(async (res) => {
      let toast = await this.toast.create({
        message: 'Developer updated',
        duration: 3000
      });
      toast.present();
    });
  }
}

Now we just need the last piece of code to display the details view – basically the same input fields we had before.

The only thing to note here is that the skills input is now an own variable which we filled after we got the initial developer data, and later we will use that variable to create the array which we then pass to the update function.

Wrap things up by changing your pages/developers/developer.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="developers"></ion-back-button>
    </ion-buttons>
    <ion-title>Developer</ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="delete()">
        <ion-icon name="trash"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <div *ngIf="developer">
    <ion-item>
      <ion-label position="stacked">What\'s your name?</ion-label>
      <ion-input [(ngModel)]="developer.name" placeholder="Developer Name"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label position="stacked">What are your special skills (comma separated)?</ion-label>
      <ion-input [(ngModel)]="skills" placeholder="Special Skills"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label position="stacked">Your image URL</ion-label>
      <ion-input [(ngModel)]="developer.img" placeholder="https://..."></ion-input>
    </ion-item>
    <ion-button expand="block" (click)="updateDeveloper()">Update Developer Info</ion-button>
  </div>
</ion-content>

That’s it for the code, but before we leave let’s quickly talk about debugging as I’m 100% there will be problems.

Debug Device Database

The biggest problem for most is debugging the code or in this case, debugging the database. We can’t simply run this code on the browser because we are using Cordova plugins, so all of the debugging needs to take place on a device/simulator!

So we need a way to access the database and check if it was created correctly, if data was added and so on, and to open the file you might want to get a tool like the SQLite Browser.

Android

For Android you can do all of the work from the command line. Simply build your app like always, install it with ADB and once you want to get the databse you can use the ADB shell as well to extract it from your app to your local filesystem like this:

ionic cordova build android
 
# Install the APK with adb install
 
# Run the app through the shell and copy the DB file
adb -d shell "run-as io.ionic.starter cat databases/data.db" > ~/androiddb.db

Just make sure that you are using the bundle ID of your application that you configured in the config.xml.

iOS

For iOS, things are a bit more complicated. But we have to distinguish between using the simulator or a real device.

So for the iOS simulator you can run a bunch of commands to get the active running simulator ID, then finding the folder on your system and finally finding the database in it like this:

# Find the ID of the running Simulator
$ ps aux | grep 'CoreSimulator/Devices'
 
# Use ID inside the Path!
$ cd  ~/Library/Developer/CoreSimulator/Devices/6EE9F4ED-C1FE-4CE8-854A-D228099E7D4A/data/
 
# Find the database file within the folder
$ find ./ -type f -name 'data.db'

If you are trying things out on your connected iOS device, you need to open Xcode -> Window -> Devices & Simulators to bring up a list of your connected devices.

In there you should see your device and the installed apps. From there, select your app, click the settings wheel and pick Download Container
ionic-xcode-download-container

Hold on, we are not yet finished!

Find the container you just donwloaded and right click it and pick Show Package Contents which will let you dive into the application data. From there you can find the database file like in the image below.
ionic-ios-container-database

Now you can open the database file with the SQLite Browser (for example) and inspect all the data of your application!

Conclusion

Although it’s not super tricky to work with an SQLite database, things can take a lot of time as you always need to go through a new deployment when you test new functionality.

The good thing is we can debug all aspects of the process to find any errors and therefore build great apps with underlying SQL queries!

You can also find a video version of this tutorial below.

The post How to Build an Ionic 4 App with SQLite Database & Queries (And Debug It!) appeared first on Devdactic.

How to Host Your Ionic App as a Website on Firebase & Standard Web Servers

$
0
0

By now you might have heard that your Ionic application is basically a website that can run on all platforms where the web is supported. But how do you actually go about this?

In this post I’ll answer one of the YT questions I got lately:

How do I actually go about hosting my ionic app on Firebase or any hosting site? I would love to see a tutorial about how to host an ionic app on Firebase hosting and setting it all up with a domain name…

We don’t really need to build a new app, just create a blank one or use one of your existing apps to follow along the process to deploy Ionic to the world wide web!

Creating the Firebase App

Before we dive into Ionic we need to make sure we actually have a Firebase app configured. If you already got something in place you can of course skip this step.

Otherwise, make sure you are signed up (it’s free) and then hit Add project inside the Firebase console. Give your new app a name, optional select a region and then create your project once you checked the boxes like below.

ionic-4-firebase

If you want to see more on working with the Firebase Database, check out my post on How to Build An Ionic 4 App with Firebase and AngularFire 5!

Building Your App for Production

We start by creating the actual web application folder so create a production build in any of your Ionic 4 apps by running:

ionic build --prod --release

Tip: By using the –prod flag you can also automatically switch to the production environment variables that you have (hopefully) defined inside the app/environments/environment.prod.ts!

Of course you can also leave that part out if you are not working with any specific environments.

Add this point you might get some errors from TypeScript because now the checking is quite intense. Make sure to fix those errors and simply run the build again until you end up with a nice www folder that holds your Ionic app.

This is basically what we need to deploy wherever we want (or can)!

Let’s try to make this available as easy as possible to the world.

Firebase Hosting

If you haven’t you need to install the Firebase tools now and then you can login from the CLI like this:

npm install -g firebase-tools
firebase login
firebase init

Now the dialog will bring up some questions and we need to select Hosting (using space, then hit enter).

In the process it will also bring up a selection to which Firebase project you want to connect your app – select one you created already upfront because creation of a project is (as of now) not possible with the CLI!

Then a few more questions come up so answer them as following:

ionic-firebase-clit

Now you can take a look at the firebase.json that was created inside your project, it should look like this:

{
  "hosting": {
    "public": "www",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

This means, whenever you deploy to Firebase it will look up your local www/ folder and upload those files.

Actually, that’s the next thing you can do right now so simply run:

firebase deploy

This might now take some time but once your are finished your should see a success message that also includes the link to your hosted Ionic app!

firebase-deploy-ionic

Custom URL

If you just want to get your app up and show it to some testers the link your get might be fine, but if you truly want to use it as hosting – like I did with the Ionic Job Board – you want to plug in your own domain name.

Actually, the process is pretty straight forward so here we go..

First, open the Hosting tab of your Firebase app and click on Connect domain

ionic-firebase-custom-domain

In the window insert the domain name that you would like to use. Of course you have to own the domain! You can buy domains basically everywhere.

ionic-firebase-custom-domain-txt

In the next step you have to add TXT record to the DNS configuration of your domain. While you might have never done this before, it’s actually pretty easy.

Simply find the DNS settings where you purchased the domain and add a new record with the text that Firebase gave you! This could look like in the image below for Digitalocean, but you can also find a guide for your specific hosting company for sure or simply ask them how to.

ionic-firebase-custom-domain-txt-do

This could take some time, but you can always check if your records are already available with a tool DNS Checker.

Once you are done with that, you also need to add some A records but that procedure follows basically the same type as before!

Now your Ionic app is hosted on Firebase and uses a custom URL that you purchased wherever before. Great success!

Standard Webserver

If you happen to have any kind of webserver up and running (or now how to start one) it’s actually super easy to deploy your Ionic app as a website as well.

As we’ve seen in the beginning, the www folder contains all the magic for our application, so we just need to put that folder on our webserver!

On many servers you’ll find your data in /var/www, so simply make an easy rsync of your build folder like this:

rsync -avz /path/to/project/www/*  username@123.45.678.910:/var/www/

Now with this command you would server your Ionic website at the root level, which most likely is already occupied by your real website.

Let’s say I want to host the application at www.devdactic.com/devhosting, in that case I would:

  • Create a subfolder in /var/www called devhosting
  • Update the base href inside the www/index.html to the folder you use on your domain (like “/ionic/”
  • Copy (rsync) all files into that folder

The change of the base HREF is needed when you deploy your files to a subfolder, otherwise the links to the scripts are broken!

ionic-4-subfolder-server

Conclusion

Always remember – your Ionic application can live everyhwere that the web runs. We are only targeting native platforms with the help of Cordova, but at its core the Ionic app is a website with specific styling.

Therefore, you can take the build artefact and deploy it on any server that’s suitable for hosting a website, or simply use Firebase as a super easy and convenient way to get your app out into the world!

You can also find a video version of this article below.

The post How to Host Your Ionic App as a Website on Firebase & Standard Web Servers appeared first on Devdactic.

How to Send Push Notifications to Your Ionic 4 App With OneSignal

$
0
0

Push notifications have become pretty much standard and needed for almost any app, but many people still fear the setup process as it was very cumbersome in the past.

However, this has become a lot easier and with the help of a great service called OneSignal the whole process becomes so easy that you can’t hide behind any excuse anymore. Of course the same can be done with Firebase, in fact there’s a whole course on Firebase push notifications inside the Ionic Academy.

ionic-onesignal-app-push

In this tutorial we will setup a new Ionic 4 app, integrate push notifications for both iOS and Android using OneSignal and finally push out some messages to our users so let’s go!

About Push Notifications

Chances are high you have seen a push notification on your mobile device before, but how does it actually work?

If you do everything from your own servers, handling device tokens, expiration times, failed notifications and so on can become very complicated especially given some iOS rules. Therefore, it makes sense to use a service for this part of your app.

The flow of events then looks roughly like this:

  1. A user opens the app the first time and the device is registered at OneSignal. This is mostly happening in the background if we integrate the plugin into our Ionic app.
  2. At some point you want to send a push to your users, most of the time even automated from your own server. We will do this from the OneSignal page, but it’s also easily possible through their REST API.
  3. What we don’t see now is how OneSignal fulfils the push: The service will make a request to the Apple or Android servers which then actually send out the notification to the final device.
  4. Finally, the message will arrive on the device of the user. Inside our Ionic app we can catch the event and also perform actions based on the data we added to the notification.

That’s basically how push works, so now let’s create our app on OneSignal.

Creating Your OneSignal App

Inside your OneSignal dashboard click + Add a new App which is the project for our new app. Give it a reasonable name and continue. You will see an introduction dialog where you can add different platforms to the project, but we haven’t worked on our app so we will set those things up later.

one-signal-add-app

When you now navigate to the settings and select Keys & IDs of your app you will find a ONESIGNAL APP ID and REST API KEY.

These 2 values are needed in order to connect your server to the OneSignal REST API to create new notifications, but for our next step we only need the app id for our Ionic app.

Integrating OneSignal into our Ionic App

Once you got the OneSignal app, it’s time to integrate it into out app. So let’s start a blank new Ionic app and install the Ionic native package and the Cordova plugin for OneSignal:

ionic start devdacticPush blank --type=angular
cd devdacticPush
ionic cordova plugin add onesignal-cordova-plugin
npm install @ionic-native/onesignal

To make use of the plugin we need to include it in our app/app.module.ts like always:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { OneSignal } from '@ionic-native/onesignal/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    OneSignal
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now the actual easy part follows, which is in our case just to react to incoming push notifications. In order to do so, you need to perform a little initialisation in your app where your app will connect to OneSignal.

We are not going deeper into sending tags or other data to OneSignal, but there’s a lot more that you can do at this point. If you want more guidance, just check out the according course in the Ionic Academy.

For now we just want to react to incoming messages and subscribe to both the handleNotificationReceived and handleNotificationOpened and then try to extract the data from the push notification that we received.

Therefore, open your app/app.component.ts and change it to:

import { Component } from '@angular/core';

import { Platform, AlertController } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { OneSignal } from '@ionic-native/onesignal/ngx';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    private oneSignal: OneSignal,
    private alertCtrl: AlertController
  ) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();

      if (this.platform.is('cordova')) {
        this.setupPush();
      }
    });
  }

  setupPush() {
    // I recommend to put these into your environment.ts
    this.oneSignal.startInit('YOUR ONESIGNAL APP ID', 'YOUR ANDROID ID');

    this.oneSignal.inFocusDisplaying(this.oneSignal.OSInFocusDisplayOption.None);

    // Notifcation was received in general
    this.oneSignal.handleNotificationReceived().subscribe(data => {
      let msg = data.payload.body;
      let title = data.payload.title;
      let additionalData = data.payload.additionalData;
      this.showAlert(title, msg, additionalData.task);
    });

    // Notification was really clicked/opened
    this.oneSignal.handleNotificationOpened().subscribe(data => {
      // Just a note that the data is a different place here!
      let additionalData = data.notification.payload.additionalData;

      this.showAlert('Notification opened', 'You already read this before', additionalData.task);
    });

    this.oneSignal.endInit();
  }

  async showAlert(title, msg, task) {
    const alert = await this.alertCtrl.create({
      header: title,
      subHeader: msg,
      buttons: [
        {
          text: `Action: ${task}`,
          handler: () => {
            // E.g: Navigate to a specific screen
          }
        }
      ]
    })
    alert.present();
  }
}

As you can see, besides the message and title we also extract additional data and specific a task key. You can supply whatever information here through the push notifications so that’s a great way if you want to perform a specific action in your app later!

That’s it for our Ionic code today – but the real work to connect our app follows now. We’ll see how to test this code at the end of the tutorial!

Just make sure that you add the native platforms with the commands below and only test on them – these push notifications won’t work inside your browser.

# iOS
ionic cordova platform add ios
ionic cordova build ios

# Android
ionic cordova platform add android
ionic cordova build android

Preparing Push for iOS

In order to receive push notifications on iOS we need to create some certificates inside our Developer Account, then add these to OneSignal and then our app is ready, so let’s go through each of those steps.

Note: You can only build iOS apps from a Mac environment, not Windows! Also, you need to be a member in the Apple Developer Program!

Your Bundle ID

Your iOS app (and Android as well) needs a bundle id, something it can be identified with. This should be something like “com.yourcomapny.appname” and we need to set it inside our config.xml file right at the beginning as the id of the widget:

<widget id="com.devdactic.ionpush" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">

If you now build your app for a native platform this id will be used. You might have to remove the iOS platform once and add it again if there are any complaints.

Register App ID

​Now you need to log in to your Developer account and create an app id which is also mandatory for submitting your app to the app store later. To do so, navigate to the Certificates, Identifiers & Profiles area of your account.

push_ios_0

Inside that new area, go to Identifiers -> App IDs and click the plus icon at the top right to create a new ID. Now insert a name and below the bundle ID you have previously also used for your config.xml file.

That’s all we need to do manually in our Apple developer account – the rest will be done with a cool tool!

OneSignal Integration

The process for creating the right certificates involves a few more steps normally but OneSignal created an awesome tool to handle the creation of those certificates for us.

Therefore, head over to your OneSignal account and open the Provisionator tool and go through the steps and select the App ID you created in the previous step.

onesignal-provisionator

This will give you a .p12 file that is secured with a password. Now locate that file on your Mac and go to the app settings page inside your OneSignal app. From there, click on Apple iOS to bring up the dialog where you can add your p12 file from before and add its password.

push_ios_3

Upload and save the dialog and now you should see your app id linked to your OneSignal app inside that screen for iOS. If the dialog asks to select a SDK now, you can select Phonegap, Cordova, Ionic & Intel XDK Installation but this will only give you some tips for the installation and won’t change the settings for your OneSignal app!

Xcode Settings

To wrap things up open your native Xcode project by opening the platforms/ios/*.workspace file.

If you select your app and then general tab you should now see your app id (otherwise rebuild with the commands from before) and below you should select “automatically manage signing” if it’s not yet selected.

If this is your first time using it you might also have to create a Development Certificate for your machine but Xcode should help you with that as well.

push_ios_5

When you go to the Capabilities tab of your project you should also see the Push notifications enabled, and if not activate them which should give you a checkmark if you configured everything correctly!

push_ios_6

Because you very likely have the latest Xcode version you need to change one setting in order to build the app without any problems. For this, click on File -> Workspace settings in the top bar of your screen. Set this to Legacy Build System and now your are ready to deploy your app to your phone!

All these steps are important because we can’t test push notifications on the browser or even simulator (or at least not real push, web push is a different story), so make sure that you are able to build the app for iOS using the build command, connect your device to your Mac and hit the play button at the top left inside Xcode to deploy it to your device (and make sure the device is selected, not a simulator)!

Preparing Push for Android

The push integration for Android is a lot easier, but also make sure that you have set the bundle ID like described before!

The way things go for Android is through Firebase, a great service that you can use as a backend for your app (there are also many courses on that topic inside the Ionic Academy).

If you haven’t used it before make sure to create a free Firebase account now and then head over to your console in order to create a new project.

ionic-onesignal-android

Once your new project is ready you will be on the dashboard of the Firebase project. From here, click the little gear icon and go to the Project settings.

We don’t need to generate anything as we can find all important information already inside the Cloud messaging tab ready for us to use.

push_android_4

Leave that screen open and now also open the OneSignal app settings again and this time click on Google Android in order to bring up a dialog where you need to paste the Server key from your Firebase project and also the Sender ID in the according fields!

push_android_5

That’s all you need to handle for Android upfront – pretty comfortable, right?

Sending Push Notifications

Hope you are still with me, because now the fun of sending push notifications can finally begin! We have connected all of our apps and once we launch our app on a device, the user will be asked for permission to receive notifications.

If we accept that dialog we will see that a new user was added to the records of our OneSignal app. I recommend you add your own user to the test user segment like you see below so you can find yourself easily later on during the testing!

ionic-onesignal-testuser

One Signal Dashboard

Now we can head over to the messages tab of our app and compose a new message. Simply select your test device, add the message you want to send.

ionic-onesignal-compose

But we can also add additional data at this point! Just scroll down all the information (that you don’t need to fill out now) and find the additional data block. Here you can send whatever information you want with your push, and in our case we wrote some code to extract a task field from this data, so simply supply something like this:

ionic-onesignal-data

Hit the send button and you should immediately see the push – either if your app is opened the alert or otherwise the notification at the top you know from so many other apps!

One Signal REST API

The last thing I wanted to show you and why I think OneSignal can be used basically with every project is the REST API. You know that you normally get some data from your server through such an API, but in this case we can perform all operations we can do inside the web dashboard of OneSignal also through this API.

I recommend you use a testing tool like Postman for this, and if you do so, here’s basically the information you need to put into the body of your POST call:

onesignal-postman

Also, you will have to add a header field with the key Authorization and the value Basic RESTAPIKEY” where you insert the REST API KEY that you can find in the Keys & IDs page of your OneSignal app!

Conclusion

Push notification integration takes some initial time to set up but isn’t something you need to be scared about anymore. With a ton of great services out there, this task becomes more easy all the time!

OneSignal is just one great example which offers a lot of segmentation and filtering features plus their amazing API. If you want more information on this great service or use Firebase instead, check out the Ionic Academy for great courses on both topics!

You can also find a video version of this tutorial below.

The post How to Send Push Notifications to Your Ionic 4 App With OneSignal appeared first on Devdactic.

Building an Ionic Firebase Location Tracker with Capacitor & Google Maps

$
0
0

With Capacitor 1.0 just days ago released it’s time to get into it – and what would be a better scenario than refreshing and updating one of the most read tutorials?

In this tutorial we will go through the steps of building a Capacitor App with Ionic 4, Firebase integration and Google maps.

ionic-4-firebase-capcitor

This means we will combine different technologies but none of them on their own are hard to handle, so just follow along and you’ll have your first Capacitor app ready soon!

Setting up our Ionic App, Capacitor & Firebase

As a small prerequisite make sure you got a Firebase app, so simply create a free account and start a new project like you can also see in my previous Firebase tutorial!

To use Capacitor you don’t have to install anything specific, and you got two options for integration:

  • Start a new Ionic project and automatically integrate it
  • Add Capacitor to an existing project

We are going with the first option, but the second works totally fine as well.

So simply run the commands below to create a new project, build the project once (which is important) and then you can add the native platforms:

ionic start devdacticTracking blank --capacitor
cd devdacticTracking
npm install firebase @angular/fire

// Build at least once to have a www folder
ionic build

npx cap add android
npx cap add ios

We will use a geolocation plugin from Capacitor, and normally Cordova would add some permissions for iOS and Android automatically.

However, Capacitor is different. You can read more about it in the release post and as well in the information about native projects.

In general this means that the native iOS/Android project should now be a real part of your project – not just an autogenerated folder that you don’t touch!

Therefore, you will have to get into the native projects and specify the right permissions like described for iOS and Android.

But from my observation, the iOS changes were already applied upfront.

If you run into problems on your Mac also make sure to check your pods version:

pod --version #should be >= 1.6
# Update if needed
sudo gem install cocoapods
pod setup

So if needed update your pods which are a system like NPM for native iOS apps that manages dependencies.

Now your Capacitor app is basically ready but we need to integrate a few more things.

Preparing our App

As we want to show a map with locations we need an API key from Google. Apparently this can be done in seconds following this link and generating a new key.

Once you got the key, open your index.html and add this snippet at the end of the head block:

<script src="https://maps.googleapis.com/maps/api/js?key=YOURKEY"></script>

Now we also need to tell our app about Firebase, and as we are using Angularfire we need to initialize it with the information from Firebase.

ionic-firebase-capacitor-dialog

In the previous part you should have created a Firebase project, and now it’s time to copy the project settings that you should see after you have added a platform to your project. The information goes straight to your environments/environment.ts file and should look like this:

export const environment = {
  production: false,
  firebase: {
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
  }
};

Now we just need to load this environment information in our app/app.module.ts and also import the Auth and Store module so we got access to all the functionality that we need:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { environment } from '../environments/environment';

import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireAuthModule } from '@angular/fire/auth';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

As we are using a simply anonymous login make sure to enable it inside your Firebase authentication settings as well!

ionic-capacitor-firebase-anon-auth

We don’t need anything else for Capacitor as we have set up our app with it and it was already included. The geolocation plugin is already part of the core API!

Building our Geolocation Tracker

Now it’s time to get to the meat of the article, in which we build and combine everything.

First of all our app needs to load the Google map on startup, and to prevent any Typescript errors we can simply declare the variable google at the top. Remember, we have added the maps script to our index.html so we don’t need any other package.

We will also automatically sign in a user – anonymously. This makes the tutorial a bit easier but of course you could simply add a login and authentication system as well.

There are great courses on this topic inside the Ionic Academy as well!

After the automatic login we establish a connection to our Firebase collection at locations/${this.user.uid}/track. This means, we can store geolocation points for each anon user in their own separate list.

Now go ahead and add this first bit of code to your app/home/home.page.ts:

import { Component, ViewChild, ElementRef } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import {
  AngularFirestore,
  AngularFirestoreCollection
} from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { Plugins } from '@capacitor/core';
const { Geolocation } = Plugins;

declare var google;

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  // Firebase Data
  locations: Observable<any>;
  locationsCollection: AngularFirestoreCollection<any>;

  // Map related
  @ViewChild('map') mapElement: ElementRef;
  map: any;
  markers = [];

  // Misc
  isTracking = false;
  watch: string;
  user = null;

  constructor(private afAuth: AngularFireAuth, private afs: AngularFirestore) {
    this.anonLogin();
  }

  ionViewWillEnter() {
    this.loadMap();
  }

  // Perform an anonymous login and load data
  anonLogin() {
    this.afAuth.auth.signInAnonymously().then(res => {
      this.user = res.user;

      this.locationsCollection = this.afs.collection(
        `locations/${this.user.uid}/track`,
        ref => ref.orderBy('timestamp')
      );

      // Make sure we also get the Firebase item ID!
      this.locations = this.locationsCollection.snapshotChanges().pipe(
        map(actions =>
          actions.map(a => {
            const data = a.payload.doc.data();
            const id = a.payload.doc.id;
            return { id, ...data };
          })
        )
      );

      // Update Map marker on every change
      this.locations.subscribe(locations => {
        this.updateMap(locations);
      });
    });
  }

  // Initialize a blank map
  loadMap() {
    let latLng = new google.maps.LatLng(51.9036442, 7.6673267);

    let mapOptions = {
      center: latLng,
      zoom: 5,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };

    this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
  }
}

There will be errors as some functions are still missing but we’ll get there now.

You can see that we also apply the standard map() on the Firebase collection data which is needed to also retrieve the ID from the objects! And the ID again is needed to later reference and remove a single entry.

We’ll also subscribe to any changes to the array and update our map once we got new data in order to display all marker (and clean old markers).

Now we can also dive into Capacitor and basically use it like you have previously used Cordova and Ionic native. We watch for any location changes and whenever we get a new position, we will add the geolocation with a timestamp to Firebase using our locationsCollection.

I’d say more about it but that’s already all the magic going on here – it’s that easy!

Now append the following functions inside your app/home/home.page.ts:

// Use Capacitor to track our geolocation
startTracking() {
  this.isTracking = true;
  this.watch = Geolocation.watchPosition({}, (position, err) => {
    if (position) {
      this.addNewLocation(
        position.coords.latitude,
        position.coords.longitude,
        position.timestamp
      );
    }
  });
}

// Unsubscribe from the geolocation watch using the initial ID
stopTracking() {
  Geolocation.clearWatch({ id: this.watch }).then(() => {
    this.isTracking = false;
  });
}

// Save a new location to Firebase and center the map
addNewLocation(lat, lng, timestamp) {
  this.locationsCollection.add({
    lat,
    lng,
    timestamp
  });

  let position = new google.maps.LatLng(lat, lng);
  this.map.setCenter(position);
  this.map.setZoom(5);
}

// Delete a location from Firebase
deleteLocation(pos) {
  this.locationsCollection.doc(pos.id).delete();
}

// Redraw all markers on the map
updateMap(locations) {
  // Remove all current marker
  this.markers.map(marker => marker.setMap(null));
  this.markers = [];

  for (let loc of locations) {
    let latLng = new google.maps.LatLng(loc.lat, loc.lng);

    let marker = new google.maps.Marker({
      map: this.map,
      animation: google.maps.Animation.DROP,
      position: latLng
    });
    this.markers.push(marker);
  }
}

When we start the tracking we also keep track of the ID so we can later clear our watch with that ID again.

And regarding our map – we don’t really got the code completion here but all of this is documented by Google pretty well. So whenever we want to update our markers, we set the map to null on all existing markers and then create new ones. Feel free to improve this logic of course by keeping the old ones and only changing what needs to be changed.

If you now also want to draw a route between these points, check out my tutorial on creating a Geolocation tracker!

We now just want to get our map, a few buttons to start/stop the tracking and a list to see our Firebase data update in realtime, so in general just some basic elements.

Note that our locations array is still an Observable so we need to use the async pipe to automatically get the latest data here.

Also, we are using a sliding item so we can pull in that little remove icon from one side and get rid of positions that we don’t want to have in our list!

Now change your app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Tracking
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <div #map id="map" [hidden]="!user"></div>

  <div *ngIf="user">

    <ion-item>
      <ion-label>User ID: {{ user.uid }}</ion-label>
    </ion-item>

    <ion-button expand="block" (click)="startTracking()" *ngIf="!isTracking">
      <ion-icon name="locate" slot="start"></ion-icon>
      Start Tracking
    </ion-button>

    <ion-button expand="block" (click)="stopTracking()" *ngIf="isTracking">
      <ion-icon name="hand" slot="start"></ion-icon>
      Stop Tracking
    </ion-button>

    <ion-list>
      <ion-item-sliding *ngFor="let pos of locations | async">
        <ion-item>
          <ion-label text-wrap>
            Lat: {{ pos.lat }}
            Lng: {{ pos.lng }}
            <p>
              {{ pos.timestamp | date:'short' }}
            </p>
          </ion-label>
        </ion-item>

        <ion-item-options side="start">
          <ion-item-option color="danger" (click)="deleteLocation(pos)">
            <ion-icon name="trash" slot="icon-only"></ion-icon>
          </ion-item-option>
        </ion-item-options>

      </ion-item-sliding>
    </ion-list>

  </div>

</ion-content>

Right now you wouldn’t see the map as it needs some CSS to be seen. You can start with the following in your app/home/home.page.scss but of course play around with it so it fits your needs!

#map {
  width: 100%;
  height: 300px;
}

Now you can run the app on your browser, you should be able to get your location (yes, Capacitor plugins work directly in your browser!) and you can also fake your location in Chrome to see new points added without leaving the house:

Inside the debugging area click on the 3 dots -> More tools -> Sensors.

capacitor-fake-location-chrome-1

From there you can control your Geolocation and fake it to other places!

Build your Ionic + Capacitor App

If you now want to build the native project, for example iOS, you simply run:

npx cap sync
npx cap open ios

Capacitor works different as said in the beginning – the native projects persists and we only sync our web app files with the native project. This also means, your build is way faster than with Cordova before!

Conclusion

Although he initial task might have sounded intimidating, it’s actually super easy to combine all of these services and technologies. Even within a website and not a real app!

There’s a lot more to say about Capacitor and I’ll talk about it soon. If you want to see anything specific with Capacitor just leave me a comment below.

You can also watch a video version of this article below.

The post Building an Ionic Firebase Location Tracker with Capacitor & Google Maps appeared first on Devdactic.

Getting Started with Angular Material in Ionic 4

$
0
0

When you have built websites with Material design before you might have already used the Angular Material components, but have you thought about using them with Ionic?

Normally the Ionic styling of components and material design items looks pretty good out of the box, but with the Angular Material package we can add many more truly Material Design components including some that don’t even exist within Ionic like this!

ionic-4-angular-material-components

That’s why we will today dive into the usage of the Angular Material package in order to understand how we can use these components inside our Ionic 4 application.

Adding Angular Material with the Angular CLI

First of all we start with a blank app so you can follow every step of the integration. Go ahead by running the below commands:

ionic start devdacticMaterial blank
cd devdacticMaterial
ng add @angular/material

The interesting part here is that we are not simply installing an npm package – we are using the Angular CLI add command which does a lot more than simply installing a package.

From the log in your terminal you will see that many files are changed as well:

UPDATE src/main.ts (389 bytes)
UPDATE src/app/app.module.ts (868 bytes)
UPDATE angular.json (5767 bytes)
UPDATE src/index.html (865 bytes)

That’s the really cool thing about add: It runs scripts for a specific package that will help you to easily integrate the package into your Angular app.

In our case, it will write something to our module, load scripts inside the index.html and also update the information for the Angular build process.

You can see the change for example in your app/app.module.ts:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, BrowserAnimationsModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

The BrowserAnimationsModule was automatically added right there!

Importing all Modules

In order to use the Material components we need to import them into our pages, but actually this can get out of hand if you are using many components.

A better solution (as you can also find in their docs) is to generate an additional module which you can by running:

ionic g module material --flat

In this module file we can then add all the imports we need from Angular Material, and later we only need to include this module file in our pages to get all the imports!

Go ahead and change the just created app/material.module.ts to:

import { NgModule } from "@angular/core";
import {
  MatTableModule,
  MatStepperModule,
  MatButtonModule,
  MatFormFieldModule,
  MatInputModule,
  MatOptionModule,
  MatSelectModule,
  MatIconModule,
  MatPaginatorModule,
  MatSortModule
} from "@angular/material";

@NgModule({
  exports: [
    MatTableModule,
    MatStepperModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule,
    MatIconModule,
    MatOptionModule,
    MatSelectModule,
    MatPaginatorModule,
    MatSortModule
  ]
})
export class MaterialModule {}

Of course this step is optional, but I still recommend it.

To wrap things up, prepare our app/home/home.module.ts so we can use the components in the next step (and also reactive forms):

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { MaterialModule } from '../material.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    MaterialModule,
    ReactiveFormsModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

That’s it for the installation and preparation – actually a very comfortable way for such a big package!

Adding a Simple Material Select Dropdown

To get started let’s pick a very simple component for which we don’t really need any additional logic – a material select!

For most of the components you can look up their markup which might otherwise not be clear upfront as it includes new tags and directives – but of course it might get better once you use it for a longer time!

For now, let’s change the app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Material
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
    
  <mat-form-field>
      <mat-label>Ionites</mat-label>
      <mat-select [(ngModel)]="ionite" name="ionite">
        <mat-option value="Simon">Simon</mat-option>
        <mat-option value="Max">Max</mat-option>
        <mat-option value="Ben">Ben</mat-option>
      </mat-select>
    </mat-form-field>

</ion-content>

This will create a cool component like you can see below!

ionic-material-select

Of course there is also the standard Ionic select, but if you enjoy this type of Material Design components you can see that it’s super easy to integrate. And this was just the start!

Creating an Angular Material Table

We’ve used the nxg-datatable in a previous post (which is now to some degree outdated for Ionic 4) so the Angular Material table component is a great and simple alternative.

To display data in our table we of course need some data upfront. We could keep the data inside a regular array but if we define it with the MatTableDataSource type we can add additional functionality.

For us this means we can add stuff like pagination and sorting to our data source for which we also need to access the ViewChild (which we will implement in our view in a second).

Right now change your app/home/home.page.ts to:

import { Component, OnInit, ViewChild } from "@angular/core";
import { MatPaginator, MatTableDataSource, MatSort } from '@angular/material';

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"]
})
export class HomePage implements OnInit {
  displayedColumns: string[] = ['first_name', 'last_name', 'twitter'];
  dataSource = new MatTableDataSource<any>([
    {
      first_name: 'Max',
      last_name: 'Lynch',
      twitter: 'maxlynch'
    },
    {
      first_name: 'Matt',
      last_name: 'Netkow',
      twitter: 'dotNetkow'
    },
    {
      first_name: 'Ben',
      last_name: 'Sperry',
      twitter: 'benjsperry'
    }
  ]);

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  ngOnInit() {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  }
}

The setup for the table is now a bit more complex than our previous example. So here’s what we are using:

  • mat-table: General directive for the table at the top level
  • matSort: Enables sorting for our table
  • ng-container matColumnDef: Defines how one column looks inlcuding the name displayed in the header row and the actual data
  • mat-sort-header: Add sorting to this column
  • mat-header-row: The definition about the column order, which is actually gathered from our class variable and not how we arrange the ng-template parts before!
  • mat-paginator: Display the Paginator component below the table

This might be a lot in the beginning, just try to understand the basic version first before you dive into more complex topics!

Try to add the below code again to your app/home/home.page.html:

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" matSort>

    <ng-container matColumnDef="first_name">
      <th mat-header-cell *matHeaderCellDef> First Name </th>
      <td mat-cell *matCellDef="let user"> {{user.first_name}} </td>
    </ng-container>

    <ng-container matColumnDef="last_name">
      <th mat-header-cell *matHeaderCellDef> Last Name </th>
      <td mat-cell *matCellDef="let user"> {{user.last_name}} </td>
    </ng-container>

    <ng-container matColumnDef="twitter">
      <th mat-header-cell *matHeaderCellDef mat-sort-header> Twitter </th>
      <td mat-cell *matCellDef="let user"> {{user.twitter}} </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

</table>

<mat-paginator [pageSizeOptions]="[1, 2, 5]" showFirstLastButtons></mat-paginator>

This results in a basic version of a table which might not yet cover the screen, therefore simply add some CSS like:

table {
  width: 100%;
}

Now your result should be a nice table view!

ionic-material-table

Compared to the ngx-datatable this component might not offer as many features as we had back then, but for basic tables with sort, filtering and pagination the component should definitely work great!

Building a Material Wizard with Steps

Finally a component I found by chance but immediately enjoyed the UI – the Stepper.

With this component you can create wizard like flows for signup, orders or whatever form data you need to capture. It will immediately give everything a more professional touch!

Before we add the steps, we also need a form to which we can bind so add the required elements to your app/home/home.page.ts:

import { Component, OnInit } from "@angular/core";
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"]
})
export class HomePage implements OnInit {

  userForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.userForm = this.fb.group({
      name: ['', Validators.required],
      address: ['', Validators.required]
    });
  }
}

Now we can add the stepper component which is built trough different steps in which you capture data. You can finetune how the different steps, labels and icons look though the latter is a bit more complex topic.

Also, this component (or the code below) uses many of the Angular Material components so it looks quite complex in the beginning.

Again, go through it one by one to truly understand what’s going on but it’s basically still just steps with elements, nothing more! And of course some logic to go back and forward between the steps using the right directives like matStepperPrevious or matStepperNext.

Now go ahead and add this somewhere to your app/home/home.page.html:

<mat-vertical-stepper linear="false" #stepper>

  <mat-step [stepControl]="userForm">
    <form [formGroup]="userForm">
      <ng-template matStepLabel>Personal Data</ng-template>
      <mat-form-field>
        <input matInput placeholder="Your Name" formControlName="name" required>
      </mat-form-field>
    </form>
  </mat-step>

  <mat-step [stepControl]="userForm" label="Shipping Info">
    <form [formGroup]="userForm">
      <mat-form-field>
        <input matInput placeholder="Address" formControlName="address" required>
      </mat-form-field>
      <div>
        <button mat-button matStepperPrevious>Back</button>
        <button mat-button matStepperNext>Next</button>
      </div>
    </form>
  </mat-step>
  
  <mat-step label="Confirm Order">
    By clicking finish you accept our Terms & Conditions.
    <div>
      <button mat-button matStepperPrevious>Back</button>
      <button mat-button (click)="stepper.reset()">Reset</button>
    </div>
  </mat-step>

</mat-vertical-stepper>

The result is a pretty cool separation of steps.

ionic-material-stepper

Of course you can also rebuild this behaviour using Ionic components and animations, but compared to this solution things will include more lines of code and additional logic!

What else?

Some of the components inside this package are already present in Ionic like cards, lists or the snackbar (which is called Toast with Ionic). Also, the navigation elements are great within Ionic and you should rely on them as well.

But besides the three here mentioned components there also more utilities that you might want to check out including:

  • Autocomplete: Otherwise only available as additional packages
  • Tree: A very interesting tree component yet problematic to use inside Ionic Apps, also to some degree possible as a simple accordion
  • Progress bar: Can be achieved with a custom component or other packages as well but looks sleek with Angular material as well
  • Tooltip: Not working for mobile apps but if you deploy your Ionic app as a website it might be nice to have

These are just a few of the packages that look interesting. There is also more we could do with the CDK like Drag & drop so if you want to see more on that just leave me a comment!

Conclusion

The Angular Material package is a great way to add material designed components to your Ionic app. Although you can cover most cases with the standard bundled stuff, things like a table or stepper component are not yet present.

The UI of ht elements in here is great and there are many more components to discover inside the Angular Material package so give it a try and let me know which of them you enjoyed as well!

You can also find a video version of this article below.

The post Getting Started with Angular Material in Ionic 4 appeared first on Devdactic.

How to Create Custom Ionic 4 Animations & Transitions

$
0
0

You might have heard that you can customise everything inside your Ionic app.. and this also counts for animations, especially how your components appear!

It’s a topic not many talk about because the basic animations provided from the Ionic team are already great for most apps. Besides that you can use additional animation packages if you want some other cool animations out of the box.

But you can actually also customise the appearance of your modals, toast alert and other components in your app as well!

ionic-4-animations

We will therefore create a custom animation for an alert box and also take a quick look at Angular animations that can help to add some cool effects to the enter event (or other vents as well) of your pages.

Preparing our Animations App

Before we get started, go and setup a new testing app like:

ionic start devdacticAnimation blank
cd devdacticAnimation

ionic g page second
npm i @angular/animations

The additional page and animations package are only used for the second part of the tutorial, so if you just want to change your Ionic alert, toast (…) you don’t need them.

Customising Ionic Component Animations

There is already some information on how to customise the Ionic component animations and their events inside the official documentation, but we will use a bit different structure:

We’ll outsource the additonal code into a file that we can then also reuse in other places. You can take a look at how the standard animation looks by inspecting the code on Github, for example the iOS enter animation for an alert.

Now the syntax isn’t that cool and obvious, and also the Ionic Animations package doesn’t really have any documentation at the time writing this. Therefore, it’s a bit tricky but we can still do it.

Let’s create a new file at app/customAlertEnter.ts and insert a slightly different version of the enter animation:

import { Animation } from '@ionic/core'

export function customAlertEnter(AnimationC: Animation, baseEl: HTMLElement): Promise<Animation> {

    const baseAnimation = new AnimationC();
  
    const backdropAnimation = new AnimationC();
    backdropAnimation.addElement(baseEl.querySelector('ion-backdrop'));
  
    const wrapperAnimation = new AnimationC();
    const wrapperElem = baseEl.querySelector('.alert-wrapper') as HTMLElement;
    wrapperAnimation.addElement(wrapperElem);
  
    wrapperElem.style.top = '0';
  
    backdropAnimation.fromTo('opacity', 0.01, 0.3);
  
    wrapperAnimation.beforeStyles({ 'opacity': 1 });
    wrapperAnimation.fromTo('transform', `translateY(-${baseEl.clientHeight}px)`, 'translateY(0px)')
  
    return Promise.resolve(baseAnimation
      .addElement(baseEl)
      .easing('cubic-bezier(.36, .66, .3, .1, 1)')
      .duration(500)
      .add(wrapperAnimation)
      .add(backdropAnimation));
  }

This code would now move the wrapperElem at the top of the view, which means the wrapperAnimation can later fly this box down the page to the right spot.

Along the way, the backdrop will also be animated to be visible with an opacity of 0.3, and finally all of this happens in a duration of 500ms with a some easing that slows down or speeds up the animation at different points.

Play around with these values later and see how your app reacts!

To se this just created function we now continue with our app/app.module.ts and use it for the right key alertEnter inside the configuration block of our Ionic Module like:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { customAlertEnter } from './customAlertEnter';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, 
    IonicModule.forRoot({
    alertEnter: customAlertEnter
  }), 
  AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

As you can see, the code is a bit cleaner than heaving everything immediately in there.

To call a simple alert, add some buttons to your home/home.page.html including a button for our second example already:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Custom Animations
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-button expand="block" (click)="showAlert()">
    Open Custom Alert
  </ion-button>

  <ion-button expand="block" routerLink="/second">
    Open Second Page
  </ion-button>
</ion-content>

Now you can call the alert from code like always, but I also added another key: enterAnimation.

This is not really needed because we already defined the enter animation in our module.

I just wanted to highlight that you could also only add the custom animation to some components and not at the root level to all of your components!

So go ahead and change your home/home.page.ts to:

import { Component } from '@angular/core';
import { AlertController } from '@ionic/angular';
import { customAlertEnter } from '../customAlertEnter';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  constructor(private alert: AlertController) { }

  async showAlert() {
    const alert = await this.alert.create({
      header: 'My Custom Alert',
      message: 'This is so epic',
      buttons: ['OK'],
      enterAnimation: customAlertEnter
    });

    alert.present();
  }
}

Now you can see your animation in action, and try to play around with values or different use cases to create different animations, like popping up from no size in the middle of the screen for example!

Custom Angular Animations

The second standard way to Animations are Angular Animations that you can write in your code. We’ve talked about this before, and the syntax for them is a bit complicated yet they are still a really powerful tool if you want to influence how the different elements of your page appear.

In order to use the animations, make sure you installed the @angular/animations package like described in the beginning and then also add the BrowserAnimationsModule to your app/app.module.ts imports:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, 
    IonicModule.forRoot(), 
  AppRoutingModule,
  BrowserAnimationsModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now you can define your own animations for every page, so let’s say you want to fade in some part and also fly in some other elements.

The code for this could look like this in your second/second.page.ts:

import { Component, OnInit } from '@angular/core';
import {
  trigger,
  state,
  style,
  animate,
  transition
} from '@angular/animations';

@Component({
  selector: 'app-second',
  templateUrl: './second.page.html',
  styleUrls: ['./second.page.scss'],
  animations: [
    trigger('fadein', [
      state('void', style({ opacity: 0 })),
      transition('void => *', [
        style({ opacity: 0 }),
        animate('900ms ease-out', style({ opacity: 1 }))
      ])
    ]),
    trigger('slidelefttitle', [
      transition('void => *', [
        style({ opacity: 0, transform: 'translateX(150%)' }),
        animate('900ms 300ms ease-out', style({ transform: 'translateX(0%)', opacity: 1 }, ))
      ])
    ])
  ]
})
export class SecondPage implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

We’re not going into the details for all the parts that make up Angular Animations, but there’s also a course on this topic inside the Ionic Academy!

In general we define transitions from a state to another, in our case the initial void state to whatever else, the * wildcard state representing everything else.

The fadein animation starts immediately while the slidelefttitle has a tiny delay of 300ms and starts a bit later. Of course you define many more different animations here for everything you want to animate on your page.

To use the animations, simply apply their trigger to the component that you want to animate like this in your second/second.page.html:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/"></ion-back-button>
    </ion-buttons>
    <ion-title>Second Page</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <ion-card>
    <ion-card-header>
      <ion-card-subtitle @slidelefttitle>Awesome Subtitle</ion-card-subtitle>
      <ion-card-title @slidelefttitle>My animated card</ion-card-title>
    </ion-card-header>
    <ion-card-content @fadein>
      <img src="https://d2hkzae1xmryt1.cloudfront.net/wp-content/uploads/2017/06/simon-dwx.png">
    </ion-card-content>
  </ion-card>

</ion-content>

Now both title fields will fly in from one side while the image inside the cad will only fade in!

Conclusion

Animations inside Ionic apps are not discussed very often, but if you need to change them, be sure you can.

You can create custom animations for the appearance of all Ionic modals and use them for all or just specific elements, or you can use the standard Angular animations for other elements that you want to animate inside your pages.

You can also find a video version of this tutorial below.

The post How to Create Custom Ionic 4 Animations & Transitions appeared first on Devdactic.

Understanding Angular Routing in Ionic Apps

$
0
0

While the Angular Router has been around for quite some time, Ionic developers only started to use it since version 4 (or before if you were into pure Angular as well). Because some of the UI patterns are not that easily transferred to the new way of routing many of you had problems during the migration phase of your apps or simply getting started with v4.

Today we’ll take a look at the most used routing aspects inside our Ionic app and how they actually play together, how we can make different routing possible and how the Angular router actually works!

The first part of this post is actually also in my article about Navigating the Change with Ionic 4 and Angular Router on the official Ionic blog!

The Entry Point of Your App

When you inspect the folders of your app, you’ll find one HomePage at the src/app/home path. This is the page you see on screen (compare it’s HTML with what you see if you don’t trust me), but how is it actually loaded?

To understand this we need to open our app/app-routing.module.ts in which we will find:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
 { path: '', redirectTo: 'home', pathMatch: 'full' },
 { path: 'home', loadChildren: './home/home.module#HomePageModule' },
];

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

This is the first place for routing information in our app and the place where we can add more information about how our app works. Right now, we have two routes defined inside the array.

The first, is actually a simple redirect that will change the empty path ‘’ to the ‘home’ path, so it’s like going to google.com/ and being redirected to google.com/home.

Inside the definition for the home path we can now spot the loadChildren key in which we supply a path to the module file of our home page. This module file holds some information and imports for the page, but you can think of it as the page that gets displayed.

Ok, cool, we now have a router and are loading a page through a path, so how is this connected with actual HTML or the index page of the app?

If you happen to inspect your index.html the only thing you’ll find is:

<body>
 <app-root></app-root>
</body>

The only thing we display is an app-root, which is still not very clear. This app root is replaced by the first real HTML of our app, which is always inside the app/app.component.html:

<ion-app>
 <ion-router-outlet></ion-router-outlet>
</ion-app>

This is the key to understanding how the routing works: The Angular Router will replace router outlets with the resolved information for a path.

This means inside the body, at the top-level, we have this special Ionic router outlet (which is the standard Angular outlet plus some animation extras) wrapped inside a tag for the Ionic app itself. Once we navigate to a certain path, the router will look for a match inside the routes we defined, and display the page inside the right outlet.

We needed this short detour to get a solid understanding about why the things we’ve done work as they do.

Basic Routing Concepts

So router outlets inside our app will be replaced by the Angular Router whenever we find a path match. For all pages that you somehow want to access inside your app, you need to define a path.

The good thing (sometimes, not always) is that when you use the Ionic CLI to create new pages, a new routing entry will be added automatically. You can run inside your project the command to generate new pages like this:

ionic g page list
ionic g page details

You routing inside the app-routing.module.ts now holds the information for a new path:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  { path: 'list', loadChildren: './list/list.module#ListPageModule' },
  { path: 'details', loadChildren: './details/details.module#DetailsPageModule' },
];

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

This means, your app can now show content at these routes:

  • /home: The content of the home.page.html
  • /list: The content of the list.page.html
  • /details: The content of the details.page.html

In order to access one of these pages you need to use the Angular router, and you can navigate to a page both from the HTML code and also from the TS file, and both in different ways:

<!-- Plain String -->
<ion-button routerLink="/list">Open List</ion-button>

<!-- Array with path segments  -->
<ion-button [routerLink]="['/', 'list']">Open List</ion-button>

// Just a string
this.router.navigateByUrl("/list");

// Again an Array
this.router.navigate(["/", "list"]);

All of these formats have the same outcome, they will open the path /list.

And because we told the Angular Router to load the './list/list.module#ListPageModule' when the path matches /list, the exact page will be displayed!

Passing Parameters to a Details Page

Once you need to pass more information to the next page, you need to select a way for your app to pass around data. Basically there are three ways to do so:

  1. Have a service that stores the information, then load the information through that service on the new page
  2. Pass an ID with the URL to the next page, and then make a new request to your API (or maybe your service if you cache responses) in order to get the data for the specific ID of an object
  3. Use the Navigation Extras introduced with Angular 7.2

All of these are legit solutions if done correctly, and you can read more about how to implement them inside my Quick Win about How to Pass Data with Angular Router in Ionic on the Ionic Academy.

Tab Bar Navigation

With the basic routing and a handful of detail pages you basically can’t go wrong. Most of the time all your routing is in one file, and things only get more tricky when you work with child routing.

People especially encountered problems when they didn’t start their app with a tab bar or side menu but want to have other pages upfront. For example they might have a login page and only later navigate to a tabs page, in that case your top most routing could simply look like this:

const routes: Routes = [
  { path: '', redirectTo: 'login', pathMatch: 'full' },
  { path: 'login', loadChildren: './pages/login/login.module#LoginPageModule' },
  { path: 'app', loadChildren: './pages/tabs/tabs.module#TabsPageModule' }
];

The app might start with a login page, and at some point you simply navigate to the inside area which is available at the path /app and loads the actual tabs.module.ts file.

Let’s take a look at the routing information that your tabs page now might contain:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: '',
    component: TabsPage,
    children: [
      { path: 'home', loadChildren: '../home/home.module#HomePageModule' },
      { path: 'news', loadChildren: '../news/news.module#NewsPageModule' },
      { path: 'news/details', loadChildren: '../news-item/news-item.module#NewsItemPageModule' },
    ]
  }
];

Tab bar

To understand what is going on let’s play through a few different cases to where the app might/could navigate:

/app

  1. The router looks up the route for /app and lands inside the routing file of our tabs module – now the first part of our path is resolved
  2. The router now looks for an empty path (”) and the first item that matches is the redirect – now we are looking for the path /home
  3. We go through the second item (as we already “used” the first block of information) and find a children array where we can math the /home part and resolve it to a page!

/app/news

  1. The router looks up the route for /app and lands inside the routing file of our tabs module – now the first part of our path is resolved
  2. The router now looks for a match for /news, and because the redirect only matches if the full path match is correct (that’s what the pathMatch key means) we step over to the next block
  3. We go through the second item and find a children array where we can math the /news part and resolve it to a page!

While the router is looking for the right path, also the TabsPage which holds the actual view elements for the tab bar will be used – and then the final page will be displayed inside a router outlet of the tab bar again!

You can read it up on the Github page of the ion-tabs:

“ion-tabs is a styleless component that works as a router outlet in order to handle navigation.”

These two examples should give you an idea how the Angular router thinks when going through your information:

  • Go through routes from top level to child routes
  • Always use the first match in the array

If you want a full guide on how to implement a tab bar, you can checkout my tab bar guide here.

Side Menu Navigation

The side menu works pretty much the same way like the tabs interface: A parent defines the general structure, and also has a dedicated router outlet area where all the content will be displayed.
side menu

In code the markup for a side menu interface looks abstracted like this:

<ion-menu>
    <ion-header>
      <ion-toolbar>
        <ion-title>Menu</ion-title>
      </ion-toolbar>
      <ion-content>
        Content of the side menu with page links
      </ion-content>
    </ion-header>
</ion-menu>
 
<!-- The place where the content pages will be rendered -->
<ion-router-outlet main></ion-router-outlet>

In terms of routing, there is also no magic behind this layout and a routing file (and this doesn’t have to be the root routing!) could look like this:

const routes: Routes = [ 
  {
    path: 'menu',
    component: MenuPage,
    children: [
      {
        path: 'first',
        loadChildren: '../first/first.module#FirstPageModule'
      },
      {
        path: 'second',
        loadChildren: '../second/second.module#SecondPageModule'
      }
    ]
  },
  {
    path: '',
    redirectTo: '/menu/first',
  }
];

Just like before imagine you are navigating to this module from another page (e.g. login). Your first path component is already resolved and depending on whether more is specified you might step into the first block and resolve a path like /pathafterlogin/menu/first.

If you end in this routing with no more path components left, the redirect strikes again – and brings you again to the first block and the /first path!

To learn how to build a side menu, simply follow my quick win on How to Add A Side Menu to Your Ionic 4 App inside the Ionic Academy!

Combining Navigation Patterns

Sometimes you need both of the before mentioned patterns – but given the current routing system that’s actually no problem at all anymore!
side menu with tabs

You can find my guide on How to Combine Ionic 4 Tabs and Side Menu Navigation here!

Modal Views

Just a quick word on modal pages at the end. Actually, modal pages doesn’t have a lot to do with Angular routing, simply because they don’t have a route!

ionic modal view

If you present a modal from within your app, you will notice that the URL path won’t change at all. This is the correct behaviour because modal views live above the other components of your app.

You can find more information about different overlays inside my article about Passing Data to Ionic Modals, Pages & Popover.

Debugging Routing

Finally just a quick note for everyone still having a hard time with Angular routing:

You can enable a debugging mode right at the app-routing.module.ts file where you provide the router for your root module:

RouterModule.forRoot(routes, { enableTracing: true, preloadingStrategy: PreloadAllModules })

By enabling the tracing you will see a bunch of logs in your console that tell you how the router is going through the information you provided.

Hopefully this will help you find the last bags with the Angular router inside your Ionic app!

You can also find a video version of this article below.

The post Understanding Angular Routing in Ionic Apps appeared first on Devdactic.


Getting Started with Ionic 4 and Socket.io

$
0
0

Working with Sockets has become super easy over the years, and although it might sound intimidating first we’ll learn how to easily use it in combination with Ionic 4!

In this tutorial we will craft a super simple Node server which allows socket connections and also an Ionic app that connects to our server.

ionic-4-socket

We will then be able to send messages to a chatgroup from our app where everyone inside is an anonymous user. You can find more detailed courses inside my Ionic Academy, the learning platform for everything Ionic!

Creating a simple Node Server with Socket.io

Because we need two parts in this tutorial, we start with a simple server. You can create a new node project by running the below commands:

mkdir SocketServer && cd SocketServer
npm init
npm install express socket.io

Now you got a folder with a package.json file, but not really a server yet. Therefore, create a new index.js file and insert:

let app = require('express')();
let server = require('http').createServer(app);
let io = require('socket.io')(server);
 
io.on('connection', (socket) => {

  socket.on('disconnect', function(){
    io.emit('users-changed', {user: socket.username, event: 'left'});   
  });
 
  socket.on('set-name', (name) => {
    socket.username = name;
    io.emit('users-changed', {user: name, event: 'joined'});    
  });
  
  socket.on('send-message', (message) => {
    io.emit('message', {msg: message.text, user: socket.username, createdAt: new Date()});    
  });
});
 
var port = process.env.PORT || 3001;
 
server.listen(port, function(){
   console.log('listening in http://localhost:' + port);
});

This is our basic Socket implementation on the server-side.

All of our functions are wrapped inside the io.on('connection') block, so these will only happen once a client connects to the server. You could also add authentication to the process, but today we want to keep things easy.

We set a function for the events disconnect, set-name and add-message which means whenever our app sends out these events the server reacts.

If we send a new message, we emit that message to everyone connected as a new object with text, the name of the sending user and a date. Also, we set the name of the socket connection if a users send his nickname.

You can imagine storing more information that way like a real user ID of your backend to couple the information closely.

There’s also a tool to test your socket implementation but I haven’t worked that much with it before. Just wanted to mention it here since it can be hard to test and debug it, so check out the Socket.io Tester!

Your node backend with Socket.io is now ready! You can start it by running the command below and you should be able to reach it at http://localhost:3001

node index.js

This backend won’t store the messages, so they are only available to online users. At this point you could plug in your database and store all new messages in there so you have the functionality of a real chat!

Creating the Ionic Socket Chat App

Now we get into the perhaps better known territory for you, the Ionic app. Actually we don’t need a lot, just a blank app to get started and additionally the ngx-socket-io package to create the connection to our server:

ionic start devdacticSocket blank
cd ./devdacticSocket
npm install ngx-socket-io

At the time writing this I also encountered problems with the package, and the easiest way to fix it (as recommended only for other cases as well) is to change your app/polyfills.ts at the end to add one more line:

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
(window as any).global = window;

Now that the problems are gone, we can simply add the information for socket to our app/app.module.ts. You need to supply your server URL and you could add a lot more options in there that you might need in special scenarios later, but for now we don’t have to change a lot and only need the URL from the server we created before, so go ahead and change your app module to:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { SocketIoModule, SocketIoConfig } from 'ngx-socket-io';
const config: SocketIoConfig = { url: 'http://localhost:3001', options: {} };

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    SocketIoModule.forRoot(config)
  ],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now the app is able to make a connection to our server and we simply call a connect() function to establish that connection.

When you add only the connect block, you can also see a new connection on your server if you put in some logs!

But we don’t want to stop there, so here’s what we want and do as well:

  • Create a random name when the app starts
  • Emit the name with the set-name event so the server knows our nickname
  • React to the incoming users-changed events in order to show a toast for users that joined/left the chat
  • Subscribe to the message event just like before to the user change and add new messages to our arry

Because the package returns Observable the implementation is super handy and matches our regular style as well.

Also, sending a new message is as easy as emitting the data to socket – the server will handle the rest and know who sent the information based on the internal socket connection information.

Now go ahead and change your app/home/home.page.ts to:

import { Component, OnInit } from '@angular/core';
import { Socket } from 'ngx-socket-io';
import { ToastController } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  message = '';
  messages = [];
  currentUser = '';

  constructor(private socket: Socket, private toastCtrl: ToastController) { }

  ngOnInit() {
    this.socket.connect();

    let name = `user-${new Date().getTime()}`;
    this.currentUser = name;
    
    this.socket.emit('set-name', name);

    this.socket.fromEvent('users-changed').subscribe(data => {
      let user = data['user'];
      if (data['event'] === 'left') {
        this.showToast('User left: ' + user);
      } else {
        this.showToast('User joined: ' + user);
      }
    });

    this.socket.fromEvent('message').subscribe(message => {
      this.messages.push(message);
    });
  }

  sendMessage() {
    this.socket.emit('send-message', { text: this.message });
    this.message = '';
  }
 
  ionViewWillLeave() {
    this.socket.disconnect();
  }

  async showToast(msg) {
    let toast = await this.toastCtrl.create({
      message: msg,
      position: 'top',
      duration: 2000
    });
    toast.present();
  }
}

Finally it’s a good idea to disconnect once you are done – perhaps not always when you leave the page but maybe when the user logs out again.

Now we need to build our view and most of the following code is copied from one of the Ionic Academy Quick Wins on creating an Elastic Chat view.

We only need to iterate our messages array and have an area for composing the new message at the bottom, so open your home/home.page.html and change it to:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Devdactic Chat
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <ion-grid>
    <ion-text color="medium" text-center>
      <p>You joined the chat as {{ currentUser }}</p>
    </ion-text>
    <ion-row *ngFor="let message of messages">

      <ion-col size="9" *ngIf="message.user !== currentUser" class="message other-message">
        <b>{{ message.user }}</b><br>
        <span>{{ message.msg }}</span>
        <div class="time" text-right><br>{{ message.createdAt | date:'short' }}</div>
      </ion-col>

      <ion-col offset="3" size="9" *ngIf="message.user === currentUser" class="message my-message">
        <b>{{ message.user }}</b><br>
        <span>{{ message.msg }}</span>
        <div class="time" text-right><br>{{ message.createdAt | date:'short' }}</div>
      </ion-col>

    </ion-row>
  </ion-grid>

</ion-content>

<ion-footer>
  <ion-toolbar color="light">
    <ion-row align-items-center>
      <ion-col size="10">
        <ion-textarea auto-grow class="message-input" rows="1" [(ngModel)]="message"></ion-textarea>
      </ion-col>
      <ion-col size="2">
        <ion-button expand="block" fill="clear" color="primary" [disabled]="message === ''" class="msg-btn"
          (click)="sendMessage()">
          <ion-icon name="ios-send" slot="icon-only"></ion-icon>
        </ion-button>
      </ion-col>
    </ion-row>
  </ion-toolbar>
</ion-footer>

Because we have a basic example everything works fine, but if you have a real chat application with a lot of messages it might make sense to also optimise your list performance with a different Ionic component!

To achieve a chat like look we also need a bit of additional CSS so copy the following into your home/home.page.scss:

.message {
    padding: 10px;
    border-radius: 10px;
    margin-bottom: 4px;
    white-space: pre-wrap;
  }
   
  .my-message {
    background: var(--ion-color-tertiary);
    color: #fff;
  }
   
  .other-message {
    background: var(--ion-color-secondary);
    color: #fff;
  }
   
  .time {
    color: #dfdfdf;
    float: right;
    font-size: small;
  }
   
  .message-input {
    margin-top: 0px;
    border: 1px solid var(--ion-color-medium);
    border-radius: 10px;
    background: #fff;
  }
   
  .msg-btn {
    --padding-start: 0.5em;
    --padding-end: 0.5em;
  }

Now you can run your chat and open for example a few more incognito windows so you have different users connected to your socket chat. Then go ahead and enjoy chatting with yourself (forever alone).

Conclusion

Getting started with Socket and Ionic isn’t that hard – creating a basic server and the needed app functionality only takes minutes, and you can expand this example to a full-blown chat application as well!

You can also find a video version of this tutorial below.

The post Getting Started with Ionic 4 and Socket.io appeared first on Devdactic.

Building an Ionic 4 Pokédex with Search, Infinite Scroll & Skeleton Views

$
0
0

While we focus on different detailed aspects of Ionic in most tutorials, today I wanted to offer a more holistic approach that everyone can follow to integrate some of the most common features into an Ionic 4 app.

Therefore we will today dive into the great PokeAPI, a free API that we can use to build a cool Pokédex application!

ionic-4-pokedex

This means we will integrate HTTP requests, infinite loading for Ionic lists, working with RxJS, creating a search bar and adding navigation.

Sounds fun? Let’s do this!

Setting up Our Pokdex

We get started with a blank Ionic app template and only generate one additional page (for the details of a Pokémon) and also a service that will hold most of our logic, so go ahead and run:

ionic start devdacticPokedex blank
cd devdacticPokedex
ionic g page details
ionic g service services/pokemon

In order to make any HTTP requests to a server we have to import the HttpClientModule to our main module, so open your app/app.module.ts and change it to:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now we can also setup the routing logic in advance. Our app has 2 screens, the initial list of Pokémon and a detail view. The CLI has already created a new route for the page we created, but we want to be able to pass an ID to that page so we can simply change our routing to this inside the app/app-routing.module.ts:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: './home/home.module#HomePageModule' },
  { path: 'home/:index', loadChildren: './details/details.module#DetailsPageModule' },
];

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

Now we are able to access the first page at /home and navigate to any details page by calling /home/1 for example to open the details for Pokémon 1.

We’ll later see how to actually retrieve the information from the URL using the Angular router.

If you want to see more possible ways to send data to a details page check out this quick win on routing inside the Ionic Academy!

Finally, if you want to achieve the full Pokédex flair you need a nice red touch. For this, we can easily change the predefined colors inside the theme/variables.scss and change the primary entries to:

--ion-color-primary: #DC0A2C;
  --ion-color-primary-rgb: 220,10,44;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255,255,255;
  --ion-color-primary-shade: #c20927;
  --ion-color-primary-tint: #e02341;

Now we are ready for the more complicated things!

Having Fun with RxJS

Making basic HTTP requests isn’t really a problem. It’s one line of code to make a GET request and see the data.

But in this case with the given API we encounter a few problems:

  • The basic list of Pokémon does not contain information about their image
  • We should not load all data, so we have to work with an offset
  • Because of the offset we need a better way to find out the real index of the Pokémon
  • Some attributes are not formatted as an Array which makes working with the data harder

You could tackle all these issues at the end of the chain in your controller or views, but that’s not how you should approach it.

The best way to overcome these problems is to directly work with the data returned by the HTTP requests inside our service. Here we can simply map() the result to something else and chain different operations inside the pipe() block of the Observable!

This means, in our case we can for example use the index of the Pokémon to construct the URL to the image and store it under a new key “image” (which does not yet exist on the object). You can llokup the URL for the images pretty easy, just change the number at the end for example: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png

Also, in all of the calls we directly calculate the real index of the Pokémon based on the given offset and index in the result array (which only contains 25 objects per call as specified by our limit query param!).

Let’s go ahead by implementing your services/pokemon.service.ts like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class PokemonService {
  baseUrl = 'https://pokeapi.co/api/v2';
  imageUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/';

  constructor(private http: HttpClient) {}

  getPokemon(offset = 0) {
    return this.http
      .get(`${this.baseUrl}/pokemon?offset=${offset}&limit=25`)
      .pipe(
        map(result => {
          return result['results'];
        }),
        map(pokemon => {
          return pokemon.map((poke, index) => {
            poke.image = this.getPokeImage(offset + index + 1);
            poke.pokeIndex = offset + index + 1;
            return poke;
          });
        })
      );
  }

  findPokemon(search) {
    return this.http.get(`${this.baseUrl}/pokemon/${search}`).pipe(
      map(pokemon => {
        pokemon['image'] = this.getPokeImage(pokemon['id']);
        pokemon['pokeIndex'] = pokemon['id'];
        return pokemon;
      })
    );
  }

  getPokeImage(index) {
    return `${this.imageUrl}${index}.png`;
  }

  getPokeDetails(index) {
    return this.http.get(`${this.baseUrl}/pokemon/${index}`).pipe(
      map(poke => {
        let sprites = Object.keys(poke['sprites']);
        poke['images'] = sprites
          .map(spriteKey => poke['sprites'][spriteKey])
          .filter(img => img);
        return poke;
      })
    );
  }
}

All of this mapping will transform the data of the request and once we receive the data in our controller there is really not much to do and we can easily use the properties we added “on the fly“.

Oh and if you wonder why we change the sprites: The sprites in the regular response is not an array. This makes iterating the entries in the view more complicated, so we can easily transform it to an array and at the same time filter() out all the null values!

Building Our Pokédex with Search and Infinite Scroll

I would say from now on things get a lot easier. We can now happily call our service functions and trust that we will receive all the data needed for our views!

So on our first page we load a list of Pokémon, and we keep track of the offset so the calls to the API will look like:

  1. https://pokeapi.co/api/v2/pokemon?offset=0&limit=25
  2. https://pokeapi.co/api/v2/pokemon?offset=25&limit=25
  3. https://pokeapi.co/api/v2/pokemon?offset=50&limit=25

In order to make our view display the items we need to set the whole array, so we can use the spread operator … to fill a new array with the items of two arrays! If you would just concat the data the view wouldn’t reload immediately.

Also, if the function was called using the infinite scroll component (you will see this in the view in a second) we need to take care of finishing the loading animation by calling complete() on the refresher object.

Finally, we also need to handle any search input, so whenever someone changes the data in the search of the view we will make another call to our service to search for a given name or ID. Don’t worry right now – there is a built in debounce time in the Ionic search bar so the function won’t be called after every character if you type quickly!

For now let’s change the home/home.page.ts to:

import { PokemonService } from './../services/pokemon.service';
import { Component, OnInit, ViewChild } from '@angular/core';
import { IonInfiniteScroll } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  offset = 0;
  pokemon = [];

  @ViewChild(IonInfiniteScroll) infinite: IonInfiniteScroll;

  constructor(private pokeService: PokemonService) { }

  ngOnInit()  {
    this.loadPokemon();
  }

  loadPokemon(loadMore = false, event?) {
    if (loadMore) {
      this.offset += 25;
    }

    this.pokeService.getPokemon(this.offset).subscribe(res => {
      this.pokemon = [...this.pokemon, ...res];

      if (event) {
        event.target.complete();
      }

      // Optional
      if (this.offset == 125) {
        this.infinite.disabled = true;
      }
    });
  }

  onSearchChange(e) {
    let value = e.detail.value;

    if (value == '') {
      this.offset = 0;
      this.loadPokemon();
      return;
    }

    this.pokeService.findPokemon(value).subscribe(res => {
      this.pokemon = [res];
    }, err => {
      this.pokemon = [];
    });
  }
}

Now we need the view based on the list of Pokémon we got plus the Ionic search bar and the Ionic infinite scroll component. Both of these component offer a lot of settings but for now we’ll stick to the basics in here.

In the next snippet I also added the Ionic skeleton text whenever the array is empty. This is a great component to indicate progress in your app and make loading times look way faster. You can notice the general behaviour of these skeleton views in basically all Popular apps!

In our list we need to take care of the routing now as well, so every item will have a router link – remember how we set it up in the beginning?

And because we created our custom field pokeIndex we can now make use of it, just like the image link that we can easily set as the source of our thumbnail!

Finally the infinite scroll component can be integrated pretty easily at the top or the end of a list. All we need is to call the function of our class to handle the loading logic, and this will automatically trigger once we get close to the end of the list.

Now go ahead and change your home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic Pokemon
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-searchbar placeholder="Search Pokemon" (ionChange)="onSearchChange($event)"></ion-searchbar>
  <ion-list *ngIf="pokemon.length == 0">
    <ion-item *ngFor="let i of [1,2,3,4,5]">
      <ion-avatar slot="start">
        <ion-skeleton-text animated></ion-skeleton-text>
      </ion-avatar>
      <ion-label class="ion-text-capitalize">
        <h2>
          <ion-skeleton-text animated style="width: 50%"></ion-skeleton-text>
        </h2>
        <p>
          <ion-skeleton-text animated style="width: 20%"></ion-skeleton-text>
        </p>
      </ion-label>
    </ion-item>
  </ion-list>

  <ion-list>
    <ion-item *ngFor="let poke of pokemon;" [routerLink]="poke.pokeIndex">
      <ion-avatar slot="start">
        <img [src]="poke.image" style="background: #F2F2F2;">
      </ion-avatar>
      <ion-label class="ion-text-capitalize">
        <h2>{{ poke.name }}</h2>
        <p>#{{ poke.pokeIndex }}</p>
      </ion-label>
    </ion-item>
  </ion-list>

  <ion-infinite-scroll (ionInfinite)="loadPokemon(true, $event)">
    <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more Pokemon...">
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>

</ion-content>

You should be able to see a nice list of Pokémon now, but the details page is still to do.

Pokedetails with JSON Data

This is a very classic pattern: You pass an ID to a details page to show information about a house, an item from a shop or whatever it might be.

In most cases your API should allow to receive the data like this, if you directly need to pass a whole object to the next page already check out the Quick Win on Angular routing again.

With the Angular router we can now easily retrieve the ID we used in the URL through the ActivatedRoute and then use this information to make a call through our service. See how wonderful all our preparation comes together!

There’s really not much else to the details page, so open the details/details.page.ts and change it to:

import { PokemonService } from './../services/pokemon.service';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-details',
  templateUrl: './details.page.html',
  styleUrls: ['./details.page.scss'],
})
export class DetailsPage implements OnInit {
  details: any;

  slideOpts = {
    autoplay: {
      delay: 1000,
      disableOnInteraction: false
    }
  };

  constructor(private pokeService: PokemonService, private route: ActivatedRoute) { }

  ngOnInit() {
    let index = this.route.snapshot.paramMap.get('index');
    this.pokeService.getPokeDetails(index).subscribe(details => {
      this.details = details;
    });
  }
}

Now we just need to display the details information, and for this I recommend you simply log the value of your object and see which properties you would like to display.

In the following view we also use the Elvis Operator or safe navigation operator ? to access properties without crashing if the object does not yet exist (like in the title). Remember, we load the data from the API which is asynchronous, so when the view is loaded the data is not instant available!

If you don’t want to use the operator all across your page you can also simply wrap your element inside a div with an *ngIf check to make sure you got the data!

What follows then is just looking at JSON data, understanding the types and the path you need to follow to different objects and keys. This is actually where a lot of people go wrong, but you really only need to follow the path and understand if you have an array you can iterate or just plain keys!

So here is an example how your details/details.page.html could look like:

<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="home"></ion-back-button>
    </ion-buttons>
    <ion-title class="ion-text-capitalize">{{ details?.name }}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  
  <div *ngIf="details">
    <ion-slides pager="true" [options]="slideOpts">
      <ion-slide *ngFor="let sprite of details.images">
        <img [src]="sprite" style="height: 250px;">
      </ion-slide>
    </ion-slides>

    <ion-card>
      <ion-card-header>
        <ion-card-title class="ion-text-capitalize">#{{ details.id }} {{ details.name }}
          <ion-chip color="primary" *ngFor="let t of details.types">
            <ion-label>{{ t.type.name }}</ion-label>
          </ion-chip>
        </ion-card-title>
        <ion-card-subtitle class="ion-text-capitalize">{{ details.weight }} lbs</ion-card-subtitle>
      </ion-card-header>

      <ion-card-content>
        <ion-list lines="full">
          <ion-item>
            <ion-label text-wrap><b>Abilities:</b> <span *ngFor="let a of details.abilities; let isLast = last"
                class="ion-text-capitalize">
                {{ a.ability.name }}{{ !isLast ? ',' : '' }}
              </span></ion-label>
          </ion-item>
          <ion-item *ngFor="let s of details.stats" class="ion-text-capitalize">
            <ion-label slot="start"><b>{{ s.stat.name }}:</b></ion-label>
            <ion-label slot="end">
              {{ s.base_stat }}
            </ion-label>
          </ion-item>
        </ion-list>
      </ion-card-content>
    </ion-card>
  </div>

</ion-content>

Feel free to experiment with the different fields and look up what the JSON has to offer.

Oh and before we end this: We’ve also added the Ionic slides component in here which is based on sprites array – the one we composed in our service on the fly!

This gives a nice gallery of images, and you can also pass an options object to the slides (which we created in the details class) where you can specify a lot of settings like in this case autoplay all the Pokémon sprites!

Conclusion

Good planning and preparation pays off. We’ve built a simple Pokédex together and because we did all the hard work in our service, we could focus on creating the views and didn’t have to worry how to transform the data at that end to fit our needs!

This is an approach you should follow when building Ionic (or any) apps, and also make sure you feel comfortable with the JSON data of API responses.

You can also find a video version of this tutorial with more explanation below.

The post Building an Ionic 4 Pokédex with Search, Infinite Scroll & Skeleton Views appeared first on Devdactic.

How to Import & Export CSV Data using Papa Parse with Ionic

$
0
0

Working with CSV data is a common business case, and although a mobile device and screen is not the perfect view for a table, we can still work with the data in an acceptable way!

In this tutorial we will import a CSV file into our Ionic app using the papa parse library or better, the Angular wrapper around it!

ionic-csv-import

We will parse the raw CSV data into objects that we can work with, present it in a table where we can edit all data and finally also build the export functionality for both web and mobile!

Starting our CSV App

In order to build the functionality we start with a blank app and install papa parse, plus two more Cordova plugins:

This might sound strange but it will work, trust me! Now get started by running:

ionic start devdacticCSV blank
cd devdacticCSV
npm install ngx-papaparse@3

# For native file creation
npm install @ionic-native/file
ionic cordova plugin add cordova-plugin-file

# For moving the file to e.g. iCloud
npm install @ionic-native/social-sharing
ionic cordova plugin add cordova-plugin-x-socialsharing

Next we need to prepare our module to load the library and use the providers for the native plugins, therefore change your app/app.module.ts to:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule } from '@angular/common/http';
import { PapaParseModule } from 'ngx-papaparse';
import { File } from '@ionic-native/file/ngx';
import { SocialSharing } from '@ionic-native/social-sharing/ngx';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
  HttpClientModule,
  PapaParseModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    File,
    SocialSharing
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Now finally we need some data to test. Pick a CSV file that you already have or create a file at assets/test.csv and fill it with this dummy data:

Date,"First Name","Last Name","Email","Amount","Currency","InvoiceID","Country"
08.22.2019,"Simon","Grimm","saimon@devdactic.com","50,00","EUR","0001","GER"
08.21.2019,"Simon","Grimm","saimon@devdactic.com","45,00","EUR","0002","GER"
08.19.2019,"Simon","Grimm","saimon@devdactic.com","40,00","EUR","0003","GER"
08.18.2019,"Simon","Grimm","saimon@devdactic.com","35,00","EUR","0004","GER"
08.17.2019,"Simon","Grimm","saimon@devdactic.com","30,00","EUR","0005","GER"
08.16.2019,"Simon","Grimm","saimon@devdactic.com","25,00","EUR","0006","GER"

We assume a standard format, but if you have a different delimiter or any other non standard stuff you can look up the options you need to specify in the papa parse documentation easily.

Working with CSV Data

Now we can get started with the actual import/export of our CSV data. First of all, we need to load the file which is stored locally in our case.

If your app is on a server, the procedure would actually be the same just with a different URL. Just make sure your server has CORS enabled or otherwise you won’t get your file.

We also need to add a special response type to the HTTP request, because otherwise Angular would try to parse it as JSON (which is the default) and you’ll receive an error in that case.

Once you then got the data, you can easily parse it with papa parse into an array with all your data. Inspect the array data and you can see that we can splice the item at the first position as our header row and use the rest as the real data information.

To export the data we can call unparse which is basically the opposite direction. This will create a new CSV file from our array data that we can then write to a local file in our apps data directory. It seems like the Cordova file plugin can’t write to an iCloud folder, so in order to make it available everywhere you can use the social sharing plugin now!

With this plugin you can share or save the file basically wherever you want – give it a try and inspect the location after you shared it!

Go ahead now by changing thehome/home.page.ts to:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Papa } from 'ngx-papaparse';
import { Platform } from '@ionic/angular';
import { File } from '@ionic-native/file/ngx';
import { SocialSharing } from '@ionic-native/social-sharing/ngx';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage {
  csvData: any[] = [];
  headerRow: any[] = [];

  constructor(
    private http: HttpClient,
    private papa: Papa,
    private plt: Platform,
    private file: File,
    private socialSharing: SocialSharing
  ) {
    this.loadCSV();
  }

  private loadCSV() {
    this.http
      .get('./assets/test.csv', {
        responseType: 'text'
      })
      .subscribe(
        data => this.extractData(data),
        err => console.log('something went wrong: ', err)
      );
  }

  private extractData(res) {
    let csvData = res || '';

    this.papa.parse(csvData, {
      complete: parsedData => {
        this.headerRow = parsedData.data.splice(0, 1)[0];
        this.csvData = parsedData.data;
      }
    });
  }

  exportCSV() {
    let csv = this.papa.unparse({
      fields: this.headerRow,
      data: this.csvData
    });

    if (this.plt.is('cordova')) {
      this.file.writeFile(this.file.dataDirectory, 'data.csv', csv, {replace: true}).then( res => {
        this.socialSharing.share(null, null, res.nativeURL, null).then(e =>{
          // Success
        }).catch(e =>{
          console.log('Share failed:', e)
        });
      }, err => {
        console.log('Error: ', err);
      });

    } else {
      // Dummy implementation for Desktop download purpose
      var blob = new Blob([csv]);
      var a = window.document.createElement('a');
      a.href = window.URL.createObjectURL(blob);
      a.download = 'newdata.csv';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    }
  }

  trackByFn(index: any, item: any) {
    return index;
  }
}

There’s also a simple browser implementation that creates a new element and downloads it, but this behaviour is not working inside a real mobile application.

Also, our trackByFn function is used to track changes to our elements. If we don’t use it in our view, we can’t really change our input fields and only edit one character at a time. You’ll see soon where we integrate it.

Our view for the CSV data basically consists of a table with rows and columns that we use inside the ngFor iteration on our data. Here we make use of the before mentioned function to track the changes of the elements in the view!

Also, the index helps to use the right ngModel for each of our cells!

To finally get something we can use on a mobile device we will give the table a bigger width through CSS next, and to make our screen scroll horizontal we can set the scrollX attribute on the ion-content of the view! This is basically the replacement for using ion-scroll in previous versions.

Now change your home/home.page.html to this:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic CSV
    </ion-title>
    <ion-buttons slot="start">
      <ion-button (click)="exportCSV()">
        Export
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content scrollX="true">

  <table border="1" class="data-table">
    <tr>
      <td *ngFor="let header of headerRow" text-center><b>{{ header }}</b></td>
    </tr>
    <tr *ngFor="let row of csvData; let i = index">
      <td *ngFor="let data of row; let j = index; trackBy: trackByFn" class="data-col ">
        <ion-input type="text" [(ngModel)]="csvData[i][j]"></ion-input>
      </td>
    </tr>
  </table>

</ion-content>

Finally we add a bit of CSS so the view looks nice on a mobile device and we can always see which cell we are editing by using the has-focus class that is automatically added to an input once we touch it!

Apply the following changes to the home/home.page.scss to finish the app:

.data-col {
    background: #f0f0f0;
}

.has-focus {
    --background: #fdf696;
}

.data-table {
    min-width: 800px;
}

Now your CSV worker app is ready, working both inside a desktop browser and deployed as a native application!

Conclusion

Working with CSV data inside your Ionic app isn’t that hard with the Papa parse library. The tricky part is as always working with files, but these issues can also be addressed with the right packages!

You can also find a video version of this article below.

The post How to Import & Export CSV Data using Papa Parse with Ionic appeared first on Devdactic.

Building an Ionic Multi App Project with Shared Angular Library

$
0
0

There is not a lot of documentation around the topic of Ionic Multi app projects, but it can be a powerful way to create a white label solution for your clients when you have all the projects in one workspace.

Today is your lucky day as we will do exactly that: Combine the Angular CLI with the Ionic setup for a multi app project and additionally create a shared library that can be used from all of the apps – and everything in one repository!

ionic-multi-app.structure

Make sure you have the Angular CLI installed as we need some of those commands as well.

Creating a Multi App Project Workspace

We could start with an Ionic app and remove some of the files, but it’s actually easier to create a workspace with the Angular CLI for which we can also add a flag to not create any application. This will create just a re barebones project with some files, so start with that:

# Create a workspace
ng new devdacticMulti --createApplication=false
cd ./devdacticMulti

# Create our projects directory
mkdir projects

Because Angular creates new projects and libraries in the projects/ folder we will follow that structure for our endeavour as well. The result for now looks like the image below.

ionic-multi-repo-setup

To prepare the workspace for our Ionic apps, go ahead and create a file at the root of the project called ionic.config.json (which is normally automatically in Ionic projects) and simply fill it with:

{
  "projects": {}
}

Now all the Ionic apps that we generate will be added to the projects key of that file, and we can alter simply select one of them to run by passing a flag. Make sure that you navigate your command line into the projects folder for the next commands as otherwise the apps would be in the wrong place.

cd ./projects
# Create our Ionic Apps
ionic start appOne blank
ionic start appTwo tabs

Now you have a projects folder with 2 Ionic projects, a good start so far. If you were to follow the official guide, it wouldn’t work yet as the projects would not be found.

The reason is that the automatically generated angular.json file contains a generic app keyword, and we have to change it to match the name of our projects.

In order to fix this you need to open both files, here’s an example of the projects/appOne/angular.json file and the replacements:

{
  "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json",
  "version": 1,
  "defaultProject": "appOne",
  "newProjectRoot": "projects",
  "projects": {
    "appOne": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "appOne:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "appOne:build:production"
            }
          }
        },
        "ionic-cordova-build": {
          "builder": "@ionic/angular-toolkit:cordova-build",
          "options": {
            "browserTarget": "appOne:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "appOne:build:production"
            }
          }
        },
        "ionic-cordova-serve": {
          "builder": "@ionic/angular-toolkit:cordova-serve",
          "options": {
            "cordovaBuildTarget": "appOne:ionic-cordova-build",
            "devServerTarget": "appOne:serve"
          },
          "configurations": {
            "production": {
              "cordovaBuildTarget": "appOne:ionic-cordova-build:production",
              "devServerTarget": "appOne:serve:production"
            }
          }
        }
      }
    }
  }
}

I removed a bunch of lines, in general there are 2 places in the beginning where you need to replace “app” by “appOne” (or the name of your project), and then you can search & replace “app:” and replace that with “appOne:” which should be another 12 occurrences in the whole file.

Same procedure for the second project that we added, but I think you know how to do it now. And of course for all other Ionic apps that you add to that workspace again!

A final look at the root ionic.config.json shows that the apps were added:

{
  "projects": {
    "appOne": {
      "name": "appOne",
      "integrations": {},
      "type": "angular",
      "root": "projects/appOne"
    },
    "appTwo": {
      "name": "app",
      "integrations": {},
      "type": "angular",
      "root": "projects/appTwo"
    }
  }
}

Now the Ionic CLI can find and build the right project, so all you have to do to run one of your apps is call the standard serve command and add the --project flag with the name of the project you want to run!

ionic serve --project appOne

Easy as that, 2 Ionic apps in one workspace, both can be served within the same directory.

Enabling Cordova Integration in Ionic Apps

Because the browser preview is not everything, let’s add Cordova to the projects. If you want to see Capacitor as well, just let me know!

The beginning looks promising again, we simply use the flag again but then:

ionic cordova platform add ios --project=appOne
...
...
[OK] Integration cordova added!
[ERROR] Could not find cordova integration in the appOne project.

It ends with a failure, and at this point you can’t build your Cordova app in the multi app project. The problem is that the ionic.config.json file (currently) adds the integrations key to the top-level object, and somehow not to the respective problem.

To fix the problem, open the ionic.config.json and copy the block that was added to the bottom of the file into the according information of the project where you want to add Cordova:

{
  "projects": {
    "appOne": {
      "name": "appOne",
      "integrations": {
        "cordova": {}
      },
      "type": "angular",
      "root": "projects/appOne"
    },
    "appTwo": {
      "name": "app",
      "integrations": {},
      "type": "angular",
      "root": "projects/appTwo"
    }
  },
  "integrations": {
    "cordova": {}
  }
}

Now you can try to run your command once again:

ionic cordova build ios --project=appOne

You will see that it now works, and the platforms/ folder is created inside the projects directoy!

ionic-multi-app-cordova

This also means you can simply have the according ressources like icon and splashcreen in each of the projects and they don’t override each other. No additional magic or copy task needed to configure your client project!

Building a Shared Angular Library

Right now it’s a repository with two separate Ionic apps, but they don’t share any logic or components.

To do this, you can now go ahead and create an Angular library right within that project as well!

ng generate library academyLib --prefix=academy
ng build academyLib --watch

Running the commands will create a new library in your projects folder, and you should always give a prefix to your library in order to know where which components come from, just like all the -ion components.

You always have to run the build command at least once for the library, but if you are working on it, it makes sense to simply watch all changes and it will rebuild immediately on safe.

Actually, the Ionic project will then rebuild itself as well afterwards if you run both the watch and Ionic serve in 2 terminals – I love it when stuff works easy as that!

ionic-shared-angular-library

The standard lib comes with a few files, and it’s enough for testing.

More on building a library with some additional configuration in another post maybe?

Right now you can’t use the library in the Ionic projects as they don’t really know about it, and the import path would be wrong or not work at all.

To fix this new problem, you can open the projects Typescript config, for example the projects/appOne/tsconfig.json and change it to:

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "module": "esnext",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2018",
      "dom"
    ],
    "paths": {
      "academy-lib": [
        "../../dist/academy-lib"
      ],
      "academy-lib/*": [
        "../../dist/academy-lib/*"
      ]
    }
  }
}

We have simply added the path to our shared library in here, just like they were already added to the tsconfig.json at the root of the project.

Now we can easily go ahead and use that library in our appOne project, so open the projects/appOne/src/app/home/home.module.ts and add it like this:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { AcademyLibModule } from 'academy-lib';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    AcademyLibModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

Note that you should use the name academy-lib without any path, which your IDE might want to tell you. If you added the TS config before correctly, this works now!

To finally see it in action, simply use the default component of the lib in the projects/appOne/src/app/home/home.module.html:

<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic App One
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <academy-academyLib></academy-academyLib>

</ion-content>

Now you can make changes to the lib while running watch and Ionic serve, and you get a sense of why this approach can be really powerful!

Feeling Fancy?

If you don’t enjoy how Ionic looks as a website, perhaps you want a standard Angular website? No problemo!

We are anyways in an Angular environment, so you could now go ahead with the Angular CLI and generate a new application and then serve it like this:

ng generate application academyWeb --routing --style scss
ng serve --open --project=academyWeb

Now you would have an Angular application right next to your Ionic apps, living happy and friendly in the same project!

Conclusion

There’s not a lot of information on the topic of Ionic multi app projects, but they can be really powerful if you can make them work for your white label solution or perhaps simply for having the Ionic app and website in one project.

The shared library we used could also be another project, and we could share and use it through NPM as well – if you want to see more about building your custom Ionic library and how to use it, just let me know and I’ll share everything you need to know about it!

You can also find a video version of this article below.

The post Building an Ionic Multi App Project with Shared Angular Library appeared first on Devdactic.

How to Create & Publish an Angular Library with Ionic Components

$
0
0

When your application becomes bigger, separating different elements into a library and reusing them across your projects is almost always necessary.

In the past we’ve seen the setup for an Ionic multi app project with shared library, and today we will take things a step further and focus on the creation of such a library.

We’ll create a library with service and component that can be used, and additionally add a configuration block so we could easily inject values into our library when we include it.

To follow the code you need to have the Angular CLI installed.

In our example we will build a library to make HTTP calls to a WordPress URL, however I only noticed after creating the code that there is already a package called ngx-wordpress, so don’t confuse the two!

Creating a new Angular Library

Since a later version the Angular CLI allows to create a workspace without any real content or project – exactly what we need as a starting point for our new library! So we pass a flag to skip the creation of an application, then we can navigate into the new workspace.

Inside we generate a new library and use a custom prefix wp or whatever you want, and also change the entry file but that’s optional as well. Additionally we already generate the component that we will work on later inside the library and then run the build command which is needed at least once to build the /dist folder!

When you get asked about the router and CSS just pick the defaults, they are not really used anyway.

ng new devdacticLibrary --createApplication=false
cd ./devdacticLibrary
ng generate library ngx-wordpress --prefix=wp --entryFile=ngx-wp
ng g component postCard
ng build

# Create the local NPM link
cd dist/ngx-wordpress
npm link

The last two lines are for testing our library local: You don’t want to build, publish and update a library all the time while testing, and you don’t have to!

You can create a local symlink using NPM that can then be used in the project where you want to integrate the package!

For now we will implement the library and everything related to it, we come to the integration on our Ionic project later.

Library Configuration & Service

You might have seen this with other packages that you include with a forRoot() call in your module, and that’s the behaviour we want to implement. To do so, we need to add a function to the main module of our library that exports some information and becomes a LibConfig object which is a simple interface that we define in there as well.

You could also pass more or other information to your library of course, we will simply pass a URL to it for now.

We as create an InjectionToken with our interface as this is not defined at runtime, we just need it do be injected into our module. We will also inject this LibConfigService in the next step inside a service to retrieve the actual value that was passed to our library!

Now go ahead and change the ngx-wordpress/src/lib/ngx-wordpress.module.ts to:

import { NgxWordpressService } from './ngx-wordpress.service';
import { NgModule, ModuleWithProviders, InjectionToken } from '@angular/core';
import { PostCardComponent } from './post-card/post-card.component';
import { HttpClientModule } from '@angular/common/http';
import { IonicModule } from '@ionic/angular';
import { CommonModule } from '@angular/common';

export interface LibConfig {
  apiUrl: string;
}

export const LibConfigService = new InjectionToken<LibConfig>('LibConfig');

@NgModule({
  declarations: [PostCardComponent],
  imports: [CommonModule, HttpClientModule, IonicModule],
  exports: [PostCardComponent]
})
export class NgxWordpressModule {
  static forRoot(config: LibConfig): ModuleWithProviders {
    return {
      ngModule: NgxWordpressModule,
      providers: [
        NgxWordpressService,
        {
          provide: LibConfigService,
          useValue: config
        }
      ]
    };
  }
}

Make sure that whenever you generate other components or services you add them to the declaration and exports in here, otherwise they won’t be available to the app using your library.

Note: We also import the IonicModule in here because our component will use Ionic elements!

Now let’s implement the service of our library which will make some HTTP requests. In the constructor you can see how we inject the token we defined in the previous step, and you can add a log so you see that you really passed the value from your app to the lib.

Besides that we have simply twi functions, I looked at the great wp-api-angular package for this and simply used the configuration and URL of the library.

Open the ngx-wordpress/src/lib/ngx-wordpress.service.ts and change it to:

import { Injectable, Inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { LibConfigService, LibConfig } from './ngx-wordpress.module';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class NgxWordpressService {
  baseUrl = this.config.apiUrl;

  constructor(@Inject(LibConfigService) private config: LibConfig, private http: HttpClient) {
    console.log('My config: ', config);
  }

  getPostList(options = {}): Observable<any[]> {
    let params = new HttpParams();
    let keys = Object.keys(options);
    
    for (let key of keys) {
      params = params.append(key, options[key]);
    }

    const requestOptions = {
      params: params
    };
    
    return this.http.get<any[]>(`${this.baseUrl}/posts`, requestOptions).pipe(
      map(posts => {
        for (let post of posts) {
          post.media_url = post['_embedded']['wp:featuredmedia'][0]['media_details'].sizes['medium'].source_url;
        }
        return posts;
      })
    );
  }

  getPost(postId: number) {
    return this.http.get(`${this.baseUrl}/posts/${postId}`);
  }
}

We don’t really need to talk about the functionality in detail, you can also add some own functions for testing to see how things work.

Creating the Library Component

Let’s finish the library by implementing a simple component. I basically wanted to provide a card interface for posts like you can see in the previous WordPress Ionic 4 post, so we need to define an input for the component to pass the data to it.

Also, this component has a button and can output a value once you click it – so you can pass data to it, and you even get something back! You can easily define this EventEmitter and you’ll see how to use it once we integrate the library.

The component doesn’t do anything else, so now change your ngx-wordpress/src/lib/post-card/post-card.component.ts to this:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'wp-post-card',
  templateUrl: './post-card.component.html',
  styleUrls: ['./post-card.component.css']
})
export class PostCardComponent implements OnInit {
  @Input('post') post: any;

  @Output()
  details: EventEmitter<number> = new EventEmitter<number>();

  constructor() {}

  ngOnInit() {}

  openDetails() {
    this.details.emit(this.post.id);
  }
}

Finally the view of the component, based on the JSON data that we get back from WordPress (exactly: The WordPress URL you pass to the library).

As said before, we need the Ionic Module because we are using Ionic elements, and because there is an Angular pipe included we also had to import the CommonModule which we did in the first step.

Besides that the component is simply creating some elements based on the data that is (hopefully) passed to it, so go ahead with the ngx-wordpress/src/lib/post-card/post-card.component.html:

<ion-card>
  <ion-card-header>
    <ion-card-title [innerHTML]="post.title.rendered"></ion-card-title>
    <ion-card-subtitle>{{ post.date_gmt | date }}</ion-card-subtitle>
  </ion-card-header>
  <ion-card-content>
    <img [src]="post.media_url">
    <div [innerHTML]="post.excerpt.rendered"></div>
    <ion-button expand="full" fill="clear" (click)="openDetails()" text-right>Read More...</ion-button>
  </ion-card-content>
</ion-card>

Now our library is finished, and you want to see the result!

Testing the Library Local

We don’t want to publish the library yet, as this process takes too long while testing different things.

Let’s start a blank new Ionic app, and then simply run the npm link command inside the apps folder again:

ionic start libIntegration blank
cd ./libIntegration
npm link ngx-wordpress

You won’t see it inside your package.json file, but you should see a statement on the console which shows the path of your symlink.

There’s also a tiny issue with the Angular setup and symlinks, and in order to fix this you can right now open your angular.json inside the Ionic project and add the preserveSymlinks entry at the shown path:

"projects": {
    "app": {
      "architect": {
        "build": {
          "options": {
            "preserveSymlinks": true,

Now with this in place, it’s time to import the library!

Go ahead and open your app/app.module.ts and add the import like:

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

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { NgxWordpressModule } from 'ngx-wordpress';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,
    NgxWordpressModule.forRoot({
    apiUrl: 'https://YOURWORDPRESSDOMAIN.com/wp-json/wp/v2'
  })],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

As you can see, we can pass in our own URL at this point or, if you feel fancy, also use the environment file of your project in order to use dev/prod environments!

Whenever you want to use components from your own library with Ionic you also have to import the library to the module file of your page, in our case it’s the default page at app/home/home.module.ts:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { NgxWordpressModule } from 'ngx-wordpress';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ]),
    NgxWordpressModule
  ],
  declarations: [HomePage]
})
export class HomePageModule {}

At this point we also don’t need the configuration block anymore, just the plain import.

Now we can use both the service and component that we created in the library. First, the service to actually get some data and then the component to render our posts as cards.

Our app/home/home.page.ts has only this one call and also already another openDetails function – we will see how to trigger it in the next step:

import { Component } from '@angular/core';
import { NgxWordpressService } from 'ngx-wordpress';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  posts = [];

  constructor(private wpService: NgxWordpressService) {
    this.wpService.getPostList({'_embed': true}).subscribe(res => {
      this.posts = res;
    });
  }

  openDetails(event) {
    console.log('should open: ', event);
  }
}

We simply call the library service, no further URL needs to be passed to it. This is a perfect way to manage your own API calls and share it across your company, the WordPress stuff is just an example of course!

Now we got an array of posts from WordPress, and we can use the data and pass it to the component that’s contained in our library. Also, we are now attaching our own function to the event emitter that we defined, just like you would do with a click on an ion-button.

The view now becomes super small, so change your app/home/home.page.html to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Devdactic nxg-wp
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>

  <wp-post-card *ngFor="let post of posts" [post]="post" (details)="openDetails($event)"></wp-post-card>

</ion-content>

That’s it, you can now serve the app, and also run the build for the library including watch flag to directly work on your Angular library and see the results with live reload!

Publishing your Library to NPM

Once you are confident that the version of the library is ready to be published, you can naivgate into the build folder of your library and simply run the publish command:

cd dist/ngx-wordpress
npm publish

Make sure that you are logged in to NPM on your command line and that you also specify a decent readme for your package.

Conclusion

We’ve seen that it’s no rocket science to create your own library with a dynamic configuration that can be passed to it. You can add components, directives, pipes, services, everything you want to share with your other applications.

Hopefully this gives you a good starting point for your next project, and if you want even more guidance and help you can become a member of the Ionic Academy and stay on top of the Ionic game at any time!

You can also find a video version of this article below.

The post How to Create & Publish an Angular Library with Ionic Components appeared first on Devdactic.

Viewing all 183 articles
Browse latest View live