├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── firebase.json ├── ionic.config.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── blocking-resolver │ │ ├── blocking-resolver.module.ts │ │ ├── blocking-resolver.page.html │ │ ├── blocking-resolver.page.scss │ │ ├── blocking-resolver.page.ts │ │ └── blocking.resolver.ts │ ├── components │ │ ├── components.module.ts │ │ ├── image-shell │ │ │ ├── image-shell.component.html │ │ │ ├── image-shell.component.scss │ │ │ └── image-shell.component.ts │ │ └── text-shell │ │ │ ├── text-shell.component.html │ │ │ ├── text-shell.component.scss │ │ │ └── text-shell.component.ts │ ├── home │ │ ├── home.module.ts │ │ ├── home.page.html │ │ ├── home.page.scss │ │ └── home.page.ts │ ├── non-blocking-resolver │ │ ├── non-blocking-resolver.module.ts │ │ ├── non-blocking-resolver.page.html │ │ ├── non-blocking-resolver.page.scss │ │ ├── non-blocking-resolver.page.ts │ │ └── non-blocking.resolver.ts │ └── progressive-shell-resolver │ │ ├── progressive-shell-resolver.module.ts │ │ ├── progressive-shell-resolver.page.html │ │ ├── progressive-shell-resolver.page.scss │ │ ├── progressive-shell-resolver.page.ts │ │ ├── progressive-shell.resolver.ts │ │ ├── sample-shell.model.ts │ │ ├── shell-elements.scss │ │ └── shell.provider.ts ├── assets │ ├── icon │ │ └── favicon.png │ ├── sample-data │ │ └── page-data.json │ └── shapes.svg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── test.ts ├── theme │ └── variables.scss ├── tsconfig.app.json └── tsconfig.spec.json ├── tsconfig.json └── tslint.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ionic-4-app-shell" 4 | } 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .ionic/ 17 | .sourcemaps/ 18 | .sass-cache/ 19 | .tmp/ 20 | .versions/ 21 | coverage/ 22 | www/ 23 | node_modules/ 24 | tmp/ 25 | temp/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | $RECYCLE.BIN/ 31 | 32 | .DS_Store 33 | Thumbs.db 34 | UserInterfaceState.xcuserstate 35 | 36 | .firebase/ 37 | post-assets/ 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Agustin Haller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Improved UX for Ionic apps with Skeleton Loading Screens 2 | 3 | ### Ionic Free Starter App 4 | UI Skeletons, Ghost Elements, Shell Elements? They are all the same! Think of them as cool content placeholders that are shown where the content will eventually be once it becomes available. 5 | 6 | This repo is part of [Ionic Skeleton Components Tutorial](https://ionicthemes.com/tutorials/about/improved-ux-for-ionic-apps-with-skeleton-loading-screens) where you will learn the importance of adopting the App Shell pattern in your ionic apps. Also, you will learn how to add Skeleton components to your Ionic Angular apps. 7 | 8 | ### Start with Ionic Framework 9 | This post is part of the *Mastering Ionic Framework* series which deep dives into Ionic more advanced stuff. If you are new to Ionic Framwork, I strongly recommend you to first read our [previous introductory ionic 5 tutorial](https://ionicthemes.com/tutorials/about/ionic5-tutorial-migration-and-starter) 10 | 11 | ### Install this Ionic free starter app 12 | ``` 13 | npm install 14 | ``` 15 | 16 | ### Browse the Ionic App 17 | ``` 18 | ionic serve 19 | ``` 20 | 21 | ### Demo 22 | [Try this app](https://ionic-4-app-shell.firebaseapp.com/home). 23 | 24 | ### Free Ionic Examples 25 | Find more Ionic 5 tutorials and freebies in [IonicThemes](https://ionicthemes.com/tutorials). 26 | 27 | ### Get a premium Ionic 5 Starter App 28 | The following skeleton animations are part of our latest [Ionic 5 Full Starter App](https://ionicthemes.com/product/ionic5-full-starter-app). It's an ionic template that you can use to jump start your app development and save yourself hundreds of hours of design and development. 29 | 30 | It also has lots of practical use cases you can use to learn Ionic Framework! 31 | 32 | 33 |
34 |
35 |
36 |
37 |
13 | Notice how the UX degrades when using Blocking Route Resolvers. 14 |
15 |25 | {{ item?.description }} 26 |
27 |21 | Let me explain you the importance of adopting the App Shell pattern in your Ionic apps and show you how to implement it using Ionic 4, Angular 7 and some advanced CSS techniques. 22 |
23 |31 | Angular Route Resolves are a special kind of route guards. They enable us to pre-fetch data from the server before navigating to a route. 32 |
33 |
37 | By design, Angular Route Resolvers won't transition to the page until the resolved Observable
completes.
38 |
40 | Use case: Let's suppose the backend is slow and takes 5 seconds to fetch data and return it to the client. The expected behavior for that scenario is that the page transition will be blocked for 5 seconds until the server sends data back to the client. 41 |
42 |
43 | A minimal improvement would be to show a loader while the resolved Observable
completes.
44 |
46 |
52 | You can avoid waiting for the Observable
to complete and have instant page transitions.
53 |
55 | The trade-off of this approach is that the waiting time gets passed to the page component you are navigating to. 56 |
57 |
58 |
61 | This also means that you will be responsible for unhandled errors that may cause navigating to unavailable pages. 62 |
63 |67 | By showing an app shell layout while loading data, we fix the waiting time issue caused when using non-blocking resolvers. 68 |
69 |
70 |
80 | This is a straightforward approach. We define both shell and view layouts and switch/animate the transition between them when page/business/view data is available. 81 |
82 |83 | <ng-container *ngIf="!routeResolveData"> 84 | <!-- Shell layout here --> 85 | </ng-container> 86 | 87 | <ng-container *ngIf="routeResolveData"> 88 | <!-- View layout here --> 89 | </ng-container> 90 |91 |
95 | This type of shells use the same layout (DOM elements) to present the loading state using the shell model and the real data once it’s available. 96 |
97 |98 | <ion-row> 99 | <ion-col size="4"> 100 | <app-image-shell class="add-spinner" [src]="routeResolveData?.image" [alt]="'Sample Image'"></app-image-shell> 101 | </ion-col> 102 | <ion-col size="8"> 103 | <h3> 104 | <app-text-shell [data]="routeResolveData?.title"></app-text-shell> 105 | </h3> 106 | <p> 107 | <app-text-shell lines="3" [data]="routeResolveData?.description"></app-text-shell> 108 | </p> 109 | </ion-col> 110 | </ion-row> 111 |112 |
120 | This component basically shows a loading indicator while fetching an image source. 121 |
122 |
123 | By listening to the (load)
event attached to the <img/>
element, once the image has loaded, we hide the loader.
124 |
126 | Note: As the [src]
property is empty, the (load)
event won't get triggered and then the component will remain in it's loading state.
127 |
129 | <app-image-shell [src]="" [alt]=""></app-image-shell> 130 |131 | 132 |
141 | This component basically works by wrapping the text node with a loading indicator while you are fetching data. 142 |
143 |144 | While there are empty values the component adds some loading styles and animations. Whereas while there are non empty values, the loading state is removed. 145 |
146 |147 | <app-text-shell [data]="" lines="3"></app-text-shell> 148 |149 |
150 |
160 | Just the masked lines without any animation. 161 |
162 |166 | Note: This approach plays well with use cases that require transparent backgrounds because it doesn't include an animation beneath. 167 |
168 |169 | Just set the masks to transparent and voila. 170 |
171 |178 | This animation works by setting a background gradient beneath some mask elements. 179 |
180 |
184 | Side effect: This solution doesn’t play well if you require the text-shell
to have a transparent background as the masks need a solid color to work properly.
185 |
193 | This animation works by animating the background-size property to achieve a bouncing effect. 194 |
195 |199 | Note: As we don’t use masks, this approach works well with use cases that require transparent backgrounds. 200 |
201 |13 | Non Blocking Route Resolvers provide a better UX. Still the waiting time gets passed to the page component. 14 |
15 |20 | You can show a loading indicator while fetching data from the backend. 21 |
22 |34 | {{ item?.description }} 35 |
36 |13 | By following the App Shell pattern we can achieve awesome UX. 14 |
15 |27 | {{ item?.description }} 28 |
29 |