Write Once Run Anywhere Is Alive And Well In Modern JavaScript Development
This is Part IV in a series. You can find the previous Parts here I, II,III. In today’s story we will complete our second tab and list out our previously taken photos.
Layout In Ionic
Since this screen has more content let’s learn a bit more about layout in Ionic before continuing. If your page has multiple container elements and requires some strict rules around layout, the Ionic grid system is a built-in easy way of getting that layout. It is very similar to Bootstrap and uses grid, row, and column components for controlling your layout. The column system has up to 12 columns per screen and the columns resize based upon the size of the screen, again like Bootstrap. In addition, the columns have size tiers like small, medium, large, etc. which are based on minimum widths so up to and above a certain width that tier would apply. Note you can even have the grid system use more than 12 columns by changing the — ion-grid-columns CSS variable. There’s a lot more configurability to the grid system and more details can be found here. But for our purposes we’ll use it in a simple form within the new second tab.
You may be wondering if it’s worth it to use the Ionic grid system instead of something more established like Bootstrap or your own classes. However since the Ionic grid system is integrated with their other controls, including themes and styles, I would recommend using their system.
Let’s use this grid system in our new second tab. But first let’s create it without grids to see some of the issues grids solve. Start our second tab by creating a new page inside of the pages folder called Photos.tsx. For now create it as an empty functional component. Since we already know how to modify routes and tabs, based upon the last stories, I’ll just show you the updated App.tsx file that will display the new second tab called photos. This is what it should look like after adding Photos as the second tab. Quick note if you’re curios where the new photos icon comes from, all icons can be found and searched by going to Ionicons (the default icons for Ionic).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Now go back to the new Photos.tsx file and add this code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
If you reload the screen you will see that the text alignment is not centered and on desktop screens it starts all the way to the left.
Now let’s replace this with a grid layout so we can get automatic centering and resizing. Update the Photos.tsx file with this code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
After rebuilding you should see this for desktop screens.
The grid, along with its styling automatically centers itself across different devices. This is what it looks like on mobile devices.
Let’s review this code to understand how this centering is done. The IonGrid is the main container for grid items. The immediate child of an IonGrid must be an IonRow. And it is possible to have multiple IonRow components in an IonGrid. Inside the IonRow is an IonCol, which will contain all of our content. Now in the IonCol you can see we have a size property for medium sized screens, size-md, that we set to 6 columns. If we left it with this property alone, then this column would shift to the left as the total count needs to be 12. However since we also use the offset property, we are telling Ionic to offset this column by 3 from the left, which puts it in the horizontal middle of the screen. Let’s continue to the grid’s content. You’ll notice that the column has a CSS class “ion-text-center”, which centers the text in the IonText component. This is one of many styling and theme related shortcuts, for all Ionic components, that can be used to very quickly get desired styling effects, while maintaining adherence to the core theme. Particularly useful are the ion-padding and ion-margin CSS classes. These classes provide a theme consistent offset for your controls that’s easy to use and ensures the same spacing throughout the entire app. There are dozens of styles that can be used to adjust text, alignment, and spacing. You can see a complete list here.
Before continuing lets run the app on our iOS device or simulator. Normally you need to run Ionic build first and then the other commands that were mentioned in our previous stories, but here’s a faster shortcut.
ionic cap run ios
This single command will build, copy, and then open your XCode project. However note it does not do the sync piece. So if you’ve not done that you’ll need to do it just once, before running this command. Also note in order to sync, your Mac needs to have Cocoapods installed. Now let’s list out the photos we saved previously into the filesystem.
Bugs and Updates
The Ionic team has just released an update to their @ionic/react-hooks package and it now includes hooks for the Filesystem. So we’ll update our Camera code to use those hooks. We’ll also need to update that file as I had an infinite loop bug which was causing the code inside useEffect to run multiple times. My apologies for the issue. So let’s first update the hooks package. I had some trouble getting it to auto increase to version 0.0.6, which is the one with hooks. So I had to first update the package.json file with that version and then run below.
npm install @ionic/react-hooks
Once you do that the code in Camera.tsx should look like this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Starting near the top, as you can see we import the filesystem hook and get useFilesystem. I’ve also created a lastWebPath state object to detect when a new picture has been taken and prevent the infinite looping issue. I’ve also forced the camera source to be CameraSource.Camera so the user will not have to choose which source object to use. Moving down to the bottom you’ll also notice that imageNames is gone now and imagePaths no longer references the photoUrl. This is because on the Photos.tsx page I am now only going to use the file name instead of the path; because converting the imagePaths from string to JSON and back was causing some issues in reading the file paths by the img tag.
Using Ionic Lifecycle Events
The Camera.tsx file is now updated and is using Capacitor to save the new picture names into local storage. Now within our Photos tab, we can use storage and filesystem hooks to retrieve the file paths and display the images into a list. Let’s update the Photos.tsx file like this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
As you can see we have a new hook called useIonViewWillEnter. This lifecycle hook is one of four event hooks that occur on all IonPage components. The others are useIonViewWillLeave, useIonViewDidEnter, and useIonViewDidLeave. Since we want our file data to arrive before the screen completes drawing, we are placing code to retrieve that data into the useIonViewWillEnter handler. Note the React class lifecycle events are still available, like componentDidMount and the others, but you should avoid them because according to Ionic documentation Ionic has taken over lifecycle control and so these events may not occur at the time you expect them to. Let’s continue. Within the useIonViewWillEnter hook we are using the storage and filesystem hooks to retrieve the file names we had saved earlier and get the photo’s path. In case you’ve not used it before, for await is a newer way of doing a for loop using async await. If there are async calls inside the for loop it will wait on that line before continuing. I added this call to make sure that by the time setPhotoItems is called the photos is fully populated with elements. Now once getUri gives us back the photo path we create some list items that we will later feed to a parent IonList component. These IonItems contain an IonAvatar for our image and an IonLabel for our image name. In addition since this is a native mobile app I thought it would look nicer if we could follow some mobile UI paradigms and so I am also using the IonItemSliding component. This component will allow the listed items to slide out to the left and reveal a Delete button. Let’s start the app again with this latest update.
ionic cap run ios
This is what you should see on your device when you slide an item out.
As you can see the IonItemSliding component also accepts an IonItemOptions component which takes the IonItemOption component that will ultimately handle the Delete button click. But before we add the handler for deletion, let’s add a click event handler to show a larger modal view of each image avatar that is clicked. Here’s the updated Photos.tsx file with the modal code and event handlers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Near the top of the Photos functional component you’ll see we have several new state objects currentPhotoUrl, currentFileName, and showModal. These will be used by the IonModal component to display the appropriate information or to toggle the modal window. Now within the useIonViewWillEnter hook we’ve updated the IonItem component to include the attributes data-name and data-path and the onClick event handler. If you look at the onClickSelectPhoto handler you see that we are using those data attributes to set the state objects we created earlier and opening the modal. If you look all the way to the bottom of the Photos file you’ll see the IonModal and related elements. Notice I am using one of the CSS utility classes for alignment and I also have a stylesheet, Photos.css imported, which sets the — height CSS variable of the modal.
#img-modal {--height: 30em;}
Once the modal is opened it uses the properties of the IonItem to display the appropriate file name and photo. Here’s a look at the open modal.
Great now we’re almost done. Let’s create the delete button handler so that users can delete unwanted images. In your Photos.tsx file find the IonItemOption onClick event and replace the dummy function with the name onClickDelete. Let’s also give this component the data-name attribute and pass it the image.fileName. Now In order to create the onClickDelete function we’ll also need to refactor the code inside of useIonViewWillEnter as we need to use that code also in our onClickDelete function. Let me show you the completed code and I’ll go through it.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
So as shown previously we are using local storage on the device in order to store our file names. The code that was retrieving our file names was initially in the useIonViewWillEnter hook. We have now moved it into a new function called setIonListItems. Now inside of onClickDelete we add some code that retrieves the list of file names, but then creates a new list that does not include the selected file to delete. This new file name list is then once again saved into local storage and then again our setIonListItems function is called which resets the IonItem list of files. Try it out yourself on your device.
That’s it a small but functional photo app that works across iOS, Android, and the Web! I’ve had good results with Ionic so I’m going to update my own application, DzHaven, to use Ionic so I can put it in the app store. I might write about it here as well. Here’s the updated code for this series, https://github.com/dharric/MyPhotos. I hope you try Ionic on your next project. Good luck.
Developer Lead DzHaven.com. Where devs help devs and make money.
View more posts
3 thoughts on “Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part IV”
David, this is awesome thanks! I am able to use the camera functionality in web app side with local macbook camera to take a pic, but it is not showing up on the photos tab. Is this because I am not able to access the local storage for the web app version? Or did I mess something up?
Hi tanner did you install the pwa elements? I would assume you did if you can take a pic from a web browser.
When you call your getUri what is in the finalPhotoUri? You need to convert finalPhotoUri.uri, using Camera.convertFileSrc, so that the path becomes a url.
Are you able to set the src attribute of your img element, inside the IonAvatar element, to the url of the photo? The variable name of the url is photoUrl.
David, this is awesome thanks! I am able to use the camera functionality in web app side with local macbook camera to take a pic, but it is not showing up on the photos tab. Is this because I am not able to access the local storage for the web app version? Or did I mess something up?
LikeLiked by 1 person
Hi tanner did you install the pwa elements? I would assume you did if you can take a pic from a web browser.
When you call your getUri what is in the finalPhotoUri? You need to convert finalPhotoUri.uri, using Camera.convertFileSrc, so that the path becomes a url.
Are you able to set the src attribute of your img element, inside the IonAvatar element, to the url of the photo? The variable name of the url is photoUrl.
LikeLike
Sorry for the late reply. Did you figure this out?
LikeLike