├── .editorconfig ├── .gitignore ├── .prettierrc ├── config.xml ├── ionic.config.json ├── package-lock.json ├── package.json ├── readme.md ├── resources ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png ├── icon.png ├── ios │ ├── icon │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-60.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ ├── icon-small@3x.png │ │ ├── icon.png │ │ └── icon@2x.png │ └── splash │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default@2x~iphone.png │ │ └── Default~iphone.png └── splash.png ├── src ├── actions │ └── text-item.action.ts ├── app │ ├── app.component.ts │ ├── app.html │ ├── app.module.ts │ ├── auth │ │ ├── auth.action.ts │ │ ├── auth.effect.ts │ │ ├── auth.reducer.ts │ │ ├── auth.selector.ts │ │ └── auth.service.ts │ └── main.ts ├── assets │ ├── icon │ │ └── favicon.ico │ ├── manifest.json │ └── service-worker.js ├── components │ ├── error │ │ ├── error.component.html │ │ └── error.component.ts │ └── example-list │ │ ├── example-list.component.html │ │ └── example-list.component.ts ├── effects │ └── text-item.effect.ts ├── gadget │ ├── gadget.actions.ts │ ├── gadget.data.service.ts │ ├── gadget.effect.ts │ ├── gadget.model.ts │ ├── gadget.reducer.ts │ ├── gadget.service.ts │ ├── modals │ │ └── gadget-detail │ │ │ ├── gadget-detail.modal.html │ │ │ ├── gadget-detail.modal.scss │ │ │ └── gadget-detail.modal.ts │ └── pages │ │ └── gadget-list │ │ ├── gadget-list.page.html │ │ ├── gadget-list.page.scss │ │ └── gadget-list.page.ts ├── gizmo │ ├── gizmo.actions.ts │ ├── gizmo.data.service.ts │ ├── gizmo.effect.ts │ ├── gizmo.model.ts │ ├── gizmo.reducer.ts │ ├── gizmo.service.ts │ ├── modals │ │ └── gizmo-detail │ │ │ ├── gizmo-detail.modal.html │ │ │ ├── gizmo-detail.modal.scss │ │ │ └── gizmo-detail.modal.ts │ └── pages │ │ └── gizmo-list │ │ ├── gizmo-list.page.html │ │ ├── gizmo-list.page.scss │ │ └── gizmo-list.page.ts ├── index.html ├── manifest.json ├── models │ ├── index.ts │ └── textItem.ts ├── pages │ ├── home │ │ ├── home.page.html │ │ └── home.page.ts │ ├── login │ │ ├── login.page.html │ │ ├── login.page.ts │ │ └── login.scss │ ├── page1 │ │ ├── page1.html │ │ ├── page1.scss │ │ └── page1.ts │ ├── page2 │ │ ├── page2.html │ │ ├── page2.scss │ │ └── page2.ts │ ├── realtime-database │ │ ├── realtime-database.page.html │ │ ├── realtime-database.page.scss │ │ └── realtime-database.page.ts │ └── signup │ │ ├── signup.page.html │ │ └── signup.page.ts ├── reducers │ ├── collection.ts │ └── index.ts ├── service-worker.js ├── theme │ └── variables.scss └── widget │ ├── modals │ └── widget-detail │ │ ├── widget-detail.modal.html │ │ ├── widget-detail.modal.scss │ │ └── widget-detail.modal.ts │ ├── pages │ └── widget-list │ │ ├── widget-list.page.html │ │ ├── widget-list.page.scss │ │ └── widget-list.page.ts │ ├── widget.actions.ts │ ├── widget.data.service.ts │ ├── widget.effect.ts │ ├── widget.model.ts │ ├── widget.reducer.ts │ └── widget.service.ts ├── tim-readme.md ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.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 | .sass-cache/ 17 | .sourcemaps 18 | .tmp/ 19 | .versions/ 20 | coverage/ 21 | dist/ 22 | node_modules/ 23 | tmp/ 24 | temp/ 25 | hooks/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | www/ 31 | $RECYCLE.BIN/ 32 | 33 | .DS_Store 34 | Thumbs.db 35 | UserInterfaceState.xcuserstate 36 | 37 | my-firebase-app-config.ts 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | my-side-menu 4 | An awesome Ionic/Cordova app. 5 | Ionic Framework Team 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-side-menu", 3 | "app_id": "", 4 | "type": "ionic-angular", 5 | "integrations": {} 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-ionic-angularfire", 3 | "author": "tja", 4 | "repository": "https://github.com/tja4472/ngrx-ionic-angularfire", 5 | "private": true, 6 | "scripts": { 7 | "clean": "ionic-app-scripts clean", 8 | "build": "ionic-app-scripts build", 9 | "lint": "ionic-app-scripts lint", 10 | "ionic:build": "ionic-app-scripts build", 11 | "ionic:serve": "ionic-app-scripts serve", 12 | "prettier:base": "prettier \"./{e2e,src}/**/*.{scss,ts}\"", 13 | "prettier:list": "npm run prettier:base -- --list-different", 14 | "prettier:write": "npm run prettier:base -- --write" 15 | }, 16 | "dependencies": { 17 | "@angular/common": "5.0.0", 18 | "@angular/compiler": "5.0.0", 19 | "@angular/compiler-cli": "5.0.0", 20 | "@angular/core": "5.0.0", 21 | "@angular/forms": "5.0.0", 22 | "@angular/http": "5.0.0", 23 | "@angular/platform-browser": "5.0.0", 24 | "@angular/platform-browser-dynamic": "5.0.0", 25 | "@ionic-native/core": "4.0.0", 26 | "@ionic-native/splash-screen": "4.0.0", 27 | "@ionic-native/status-bar": "4.0.0", 28 | "@ionic/storage": "2.1.3", 29 | "@ngrx/effects": "5.1.0", 30 | "@ngrx/entity": "5.1.0", 31 | "@ngrx/store": "5.1.0", 32 | "@ngrx/store-devtools": "5.1.0", 33 | "angularfire2": "5.0.0-rc.6", 34 | "firebase": "4.9.0", 35 | "ionic-angular": "3.9.2", 36 | "ionicons": "3.0.0", 37 | "rxjs": "5.5.2", 38 | "sw-toolbox": "3.6.0", 39 | "zone.js": "0.8.18" 40 | }, 41 | "devDependencies": { 42 | "@ionic/app-scripts": "3.1.8", 43 | "prettier": "1.10.2", 44 | "ngrx-store-freeze": "0.2.1", 45 | "typescript": "2.4.2" 46 | }, 47 | "description": "my-side-menu: An Ionic project", 48 | "cordovaPlugins": [ 49 | "cordova-plugin-device", 50 | "cordova-plugin-console", 51 | "cordova-plugin-whitelist", 52 | "cordova-plugin-splashscreen", 53 | "cordova-plugin-statusbar", 54 | "ionic-plugin-keyboard" 55 | ], 56 | "cordovaPlatforms": [] 57 | } 58 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Gadget 2 | 3 | * Firebase Realtime Database 4 | * [@ngrx/entity](https://github.com/ngrx/platform/tree/master/docs/entity) 5 | * [AngularFireList - valueChanges()](https://github.com/angular/angularfire2/blob/master/docs/rtdb/lists.md#valuechanges) returns sorted records 6 | 7 | # Gizmo 8 | 9 | * Firebase Cloud Firestore 10 | * [@ngrx/entity](https://github.com/ngrx/platform/tree/master/docs/entity) 11 | * [AngularFirestoreCollection - stateChanges()](https://github.com/angular/angularfire2/blob/master/docs/firestore/collections.md#snapshotchanges) 12 | * [EntityAdapter](https://github.com/ngrx/platform/blob/master/docs/entity/adapter.md) sorts records 13 | 14 | # Widget 15 | 16 | * Firebase Cloud Firestore 17 | * [@ngrx/entity](https://github.com/ngrx/platform/tree/master/docs/entity) 18 | * [AngularFirestoreCollection - valueChanges()](https://github.com/angular/angularfire2/blob/master/docs/firestore/collections.md#valuechanges) returns sorted records 19 | 20 | ### my-firebase-app-config.ts 21 | 22 | ```typescript 23 | import { FirebaseAppConfig } from 'angularfire2'; 24 | 25 | export const MyFirebaseAppConfig: Readonly = { 26 | apiKey: 'xxxxxx', 27 | authDomain: 'xxxxxx', 28 | databaseURL: 'xxxxxx', 29 | messagingSenderId: 'xxxxxx', 30 | projectId: 'xxxxxx', 31 | storageBucket: 'xxxxxx', 32 | }; 33 | ``` 34 | 35 | ## State 36 | 37 | * auth 38 | * displayName 39 | * email 40 | * error 41 | * hasChecked 42 | * isAuthenticated 43 | * isAuthenticating 44 | * userId 45 | * collection 46 | * loaded 47 | * loading 48 | * textItems 49 | * 0 50 | * description 51 | * title 52 | * 1 53 | * description 54 | * title 55 | * gadget 56 | * ids 57 | * 0: "aa" 58 | * 1: "bb" 59 | * entities 60 | * aa 61 | * description 62 | * id 63 | * name 64 | * bb 65 | * description 66 | * id 67 | * name 68 | * selectedWidgetId 69 | * wgizmo 70 | * ids 71 | * 0: "aa" 72 | * 1: "bb" 73 | * entities 74 | * aa 75 | * description 76 | * id 77 | * name 78 | * bb 79 | * description 80 | * id 81 | * name 82 | * selectedGizmoId 83 | * widget 84 | * ids 85 | * 0: "aa" 86 | * 1: "bb" 87 | * entities 88 | * aa 89 | * description 90 | * id 91 | * name 92 | * bb 93 | * description 94 | * id 95 | * name 96 | * selectedWidgetId 97 | 98 | ## ngrx 4.1.1 99 | 100 | ### Actions 101 | 102 | Using [example-app/book.ts](https://github.com/ngrx/platform/blob/master/example-app/app/books/actions/book.ts) as pattern. 103 | 104 | [Action doc](https://github.com/ngrx/platform/blob/master/docs/store/actions.md) 105 | 106 | ### Reducers 107 | 108 | https://github.com/ngrx/platform/blob/master/example-app/app/auth/reducers/auth.ts 109 | 110 | https://github.com/ngrx/platform/blob/master/docs/entity/adapter.md#createentityadapter 111 | -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/resources/splash.png -------------------------------------------------------------------------------- /src/actions/text-item.action.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | // tslint:disable:no-empty 3 | import { Action } from '@ngrx/store'; 4 | 5 | import { TextItem } from '../models'; 6 | 7 | export enum TextItemActionTypes { 8 | LoadCollection = '[TextItem] Load Collection', 9 | LoadCollectionSuccess = '[TextItem] Load Collection Success', 10 | } 11 | 12 | export class LoadCollectionAction implements Action { 13 | public readonly type = TextItemActionTypes.LoadCollection; 14 | 15 | constructor() {} 16 | } 17 | 18 | export class LoadCollectionSuccessAction implements Action { 19 | public readonly type = TextItemActionTypes.LoadCollectionSuccess; 20 | 21 | constructor(public payload: TextItem[]) {} 22 | } 23 | 24 | export type TextItemActions = 25 | | LoadCollectionAction 26 | | LoadCollectionSuccessAction; 27 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | 3 | import { AngularFireAuth } from 'angularfire2/auth'; 4 | 5 | import { StatusBar } from '@ionic-native/status-bar'; 6 | import { select, Store } from '@ngrx/store'; 7 | import { AlertController, MenuController, Nav, Platform } from 'ionic-angular'; 8 | import { concatMap, filter } from 'rxjs/operators'; 9 | 10 | import * as FromAuthSelector from '../app/auth/auth.selector'; 11 | import { GadgetListPage } from '../gadget/pages/gadget-list/gadget-list.page'; 12 | import { GizmoListPage } from '../gizmo/pages/gizmo-list/gizmo-list.page'; 13 | import { HomePage } from '../pages/home/home.page'; 14 | import { LoginPage } from '../pages/login/login.page'; 15 | import { Page1 } from '../pages/page1/page1'; 16 | import { Page2 } from '../pages/page2/page2'; 17 | import { RealtimeDatabasePage } from '../pages/realtime-database/realtime-database.page'; 18 | import { SignupPage } from '../pages/signup/signup.page'; 19 | import * as FromRoot from '../reducers'; 20 | import { WidgetListPage } from '../widget/pages/widget-list/widget-list.page'; 21 | import * as AuthActions from './auth/auth.action'; 22 | 23 | // Should be import * as LoginActions from '../actions/login.action'; 24 | // See: https://gitter.im/ngrx/effects?at=57f3a2cbd45d7f0f52601422 25 | // 26 | // Do not import from 'firebase' as you'd lose the tree shaking benefits 27 | // Add the RxJS Observable operators we need in this app. 28 | export interface PageInterface { 29 | title: string; 30 | component: any; 31 | icon: string; 32 | doSignOut?: boolean; 33 | } 34 | 35 | @Component({ 36 | templateUrl: 'app.html', 37 | }) 38 | export class MyApp { 39 | @ViewChild(Nav) public nav: Nav; 40 | 41 | public rootPage: any; 42 | 43 | public viewAppPages: PageInterface[] = [ 44 | { title: 'Page One', component: Page1, icon: 'calendar' }, 45 | { title: 'Page Two', component: Page2, icon: 'calendar' }, 46 | ]; 47 | 48 | public viewSignedInPages: PageInterface[] = [ 49 | { title: 'Page Home', component: HomePage, icon: 'calendar' }, 50 | { 51 | component: GadgetListPage, 52 | icon: 'calendar', 53 | title: 'Gadgets(Realtime Database)', 54 | }, 55 | { 56 | component: GizmoListPage, 57 | icon: 'calendar', 58 | title: 'Gizmos(Cloud Firestore)', 59 | }, 60 | { 61 | component: WidgetListPage, 62 | icon: 'calendar', 63 | title: 'Widgets(Cloud Firestore)', 64 | }, 65 | { 66 | component: RealtimeDatabasePage, 67 | icon: 'calendar', 68 | title: 'Realtime Database', 69 | }, 70 | { title: 'Sign Out', component: Page1, icon: 'log-out', doSignOut: true }, 71 | ]; 72 | 73 | public viewSignedOutPages: PageInterface[] = [ 74 | { title: 'Page Login', component: LoginPage, icon: 'log-in' }, 75 | { title: 'Page Signup', component: SignupPage, icon: 'person-add' }, 76 | ]; 77 | 78 | public loginState$: any; 79 | 80 | private readonly signedInMenuId = 'signedInMenu'; 81 | private readonly signedOutMenuId = 'signedOutMenu'; 82 | 83 | constructor( 84 | public afAuth: AngularFireAuth, 85 | public alertController: AlertController, 86 | public menuController: MenuController, 87 | public platform: Platform, 88 | public statusBar: StatusBar, 89 | private store: Store, 90 | ) { 91 | // 92 | console.log('MyApp:constructor'); 93 | this.initializeApp(); 94 | 95 | this.loginState$ = this.store.select(FromAuthSelector.getAuthState); 96 | } 97 | 98 | public initializeApp() { 99 | this.setStartPage(); 100 | 101 | this.platform.ready().then(() => { 102 | console.log('platform.ready'); 103 | // Okay, so the platform is ready and our plugins are available. 104 | // Here you can do any higher level native things you might need. 105 | this.statusBar.styleDefault(); 106 | }); 107 | 108 | /* 109 | this.subscription = 110 | this.store 111 | .let(FromRoot.getLoginState) 112 | .subscribe(loginState => { 113 | // Triggered when loginState changes. 114 | // i.e. when user logs in or logs out. 115 | console.log('loginState>', loginState); 116 | console.log('loginState.isAuthorized>', loginState.isAuthenticated); 117 | // this.enableMenu(loginState.isAuthenticated); 118 | 119 | /* 120 | if (loginState.isAuthorized) { 121 | this.rootPage = HomePage; 122 | } 123 | else { 124 | this.rootPage = LoginPage; 125 | } 126 | * / 127 | }); 128 | */ 129 | } 130 | 131 | public viewIsActive(page: PageInterface) { 132 | if ( 133 | this.nav.getActive() && 134 | this.nav.getActive().component === page.component 135 | ) { 136 | return 'primary'; 137 | } 138 | return; 139 | } 140 | 141 | public viewOpenPage(page: PageInterface) { 142 | console.log('viewOpenPage'); 143 | 144 | // Reset the content nav to have just this page 145 | // we wouldn't want the back button to show in this scenario 146 | // this.nav.setRoot(page.component); 147 | 148 | // if (page.title === 'Logout') { 149 | if (page.doSignOut) { 150 | // Give the menu time to close before changing to logged out 151 | setTimeout(() => { 152 | // this.userData.logout(); 153 | this.store.dispatch(new AuthActions.SignOut()); 154 | // this.afAuth.auth.signOut(); 155 | }, 1000); 156 | } else { 157 | this.nav.setRoot(page.component); 158 | } 159 | } 160 | 161 | private enableMenu(signedIn: boolean): void { 162 | // 163 | if (!this.menuController.get(this.signedInMenuId)) { 164 | console.error( 165 | `enableMenu() *** WARNING: Menu not found>`, 166 | this.signedInMenuId, 167 | ); 168 | } 169 | 170 | if (!this.menuController.get(this.signedOutMenuId)) { 171 | console.error( 172 | `enableMenu() *** WARNING: Menu not found>`, 173 | this.signedOutMenuId, 174 | ); 175 | } 176 | 177 | this.menuController.enable(signedIn, this.signedInMenuId); 178 | this.menuController.enable(!signedIn, this.signedOutMenuId); 179 | } 180 | 181 | private setStartPage(): void { 182 | // 183 | this.store 184 | .pipe( 185 | select(FromAuthSelector.getHasDoneFirstCheck), 186 | filter((hasDoneFirstCheck) => hasDoneFirstCheck), 187 | concatMap(() => 188 | this.store.pipe(select(FromAuthSelector.getIsAuthenticated)), 189 | ), 190 | filter((isAuthenticated) => isAuthenticated), 191 | ) 192 | .subscribe(() => { 193 | // const emailVerified = authState.emailVerified; 194 | const emailVerified = true; 195 | 196 | if (emailVerified) { 197 | this.enableMenu(true); 198 | this.nav.setRoot(HomePage); 199 | } else { 200 | this.showEmailVerifiedAlert(); 201 | this.enableMenu(false); 202 | this.nav.setRoot(Page1); 203 | } 204 | }); 205 | 206 | this.store 207 | .pipe( 208 | select(FromAuthSelector.getHasDoneFirstCheck), 209 | filter((hasDoneFirstCheck) => hasDoneFirstCheck), 210 | concatMap(() => 211 | this.store.pipe(select(FromAuthSelector.getIsAuthenticated)), 212 | ), 213 | filter((isAuthenticated) => !isAuthenticated), 214 | ) 215 | .subscribe(() => { 216 | this.enableMenu(false); 217 | this.nav.setRoot(Page1); 218 | }); 219 | } 220 | 221 | private showEmailVerifiedAlert() { 222 | // 223 | const confirm = this.alertController.create({ 224 | buttons: [ 225 | { 226 | handler: () => { 227 | console.log('Cancel clicked'); 228 | }, 229 | text: 'Cancel', 230 | }, 231 | { 232 | handler: () => { 233 | console.log('Verify clicked'); 234 | this.store.dispatch(new AuthActions.SendEmailVerification()); 235 | }, 236 | text: 'Verify', 237 | }, 238 | ], 239 | message: 'You need to verify your email address to sign in.', 240 | title: 'Verify Email?', 241 | }); 242 | confirm.present(); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Menu(signed in) 8 |

{{(loginState$ | async).displayName}}

9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | App Pages 17 | 18 | 27 | 28 | 29 | 30 | 31 | Signed In Pages 32 | 33 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | Menu(signed out) 54 |

{{(loginState$ | async).displayName}}

55 |
56 |
57 |
58 | 59 | 60 | 61 | 62 | App Pages 63 | 64 | 73 | 74 | 75 | 76 | 77 | Signed Out Pages 78 | 79 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AngularFireModule } from 'angularfire2'; 5 | import { AngularFireAuthModule } from 'angularfire2/auth'; 6 | import { AngularFireDatabaseModule } from 'angularfire2/database'; 7 | import { AngularFirestoreModule } from 'angularfire2/firestore'; 8 | 9 | import { StatusBar } from '@ionic-native/status-bar'; 10 | import { EffectsModule } from '@ngrx/effects'; 11 | import { StoreModule } from '@ngrx/store'; 12 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 13 | import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular'; 14 | 15 | import { Error } from '../components/error/error.component'; 16 | import { ExampleList } from '../components/example-list/example-list.component'; 17 | import { TextItemEffects } from '../effects/text-item.effect'; 18 | import { GadgetDataService } from '../gadget/gadget.data.service'; 19 | import { GadgetEffects } from '../gadget/gadget.effect'; 20 | import { GadgetService } from '../gadget/gadget.service'; 21 | import { GadgetDetailModal } from '../gadget/modals/gadget-detail/gadget-detail.modal'; 22 | import { GadgetListPage } from '../gadget/pages/gadget-list/gadget-list.page'; 23 | import { GizmoDataService } from '../gizmo/gizmo.data.service'; 24 | import { GizmoEffects } from '../gizmo/gizmo.effect'; 25 | import { GizmoService } from '../gizmo/gizmo.service'; 26 | import { GizmoDetailModal } from '../gizmo/modals/gizmo-detail/gizmo-detail.modal'; 27 | import { GizmoListPage } from '../gizmo/pages/gizmo-list/gizmo-list.page'; 28 | import { HomePage } from '../pages/home/home.page'; 29 | import { LoginPage } from '../pages/login/login.page'; 30 | import { Page1 } from '../pages/page1/page1'; 31 | import { Page2 } from '../pages/page2/page2'; 32 | import { RealtimeDatabasePage } from '../pages/realtime-database/realtime-database.page'; 33 | import { SignupPage } from '../pages/signup/signup.page'; 34 | import { metaReducers, reducers } from '../reducers'; 35 | import { WidgetDetailModal } from '../widget/modals/widget-detail/widget-detail.modal'; 36 | import { WidgetListPage } from '../widget/pages/widget-list/widget-list.page'; 37 | import { WidgetDataService } from '../widget/widget.data.service'; 38 | import { WidgetEffects } from '../widget/widget.effect'; 39 | import { WidgetService } from '../widget/widget.service'; 40 | import { MyApp } from './app.component'; 41 | import { AuthEffects } from './auth/auth.effect'; 42 | import { AuthService } from './auth/auth.service'; 43 | import { MyFirebaseAppConfig } from './my-firebase-app-config'; 44 | 45 | @NgModule({ 46 | declarations: [ 47 | WidgetDetailModal, 48 | WidgetListPage, 49 | GadgetDetailModal, 50 | GadgetListPage, 51 | GizmoDetailModal, 52 | GizmoListPage, 53 | Error, 54 | ExampleList, 55 | MyApp, 56 | Page1, 57 | Page2, 58 | HomePage, 59 | LoginPage, 60 | RealtimeDatabasePage, 61 | SignupPage, 62 | ], 63 | imports: [ 64 | BrowserModule, 65 | IonicModule.forRoot(MyApp), 66 | AngularFireModule.initializeApp(MyFirebaseAppConfig), 67 | AngularFireAuthModule, 68 | AngularFireDatabaseModule, 69 | AngularFirestoreModule.enablePersistence(), 70 | StoreModule.forRoot(reducers, { metaReducers }), 71 | StoreDevtoolsModule.instrument(), 72 | EffectsModule.forRoot([ 73 | AuthEffects, 74 | TextItemEffects, 75 | WidgetEffects, 76 | GadgetEffects, 77 | GizmoEffects, 78 | ]), 79 | ], 80 | // tslint:disable-next-line:object-literal-sort-keys 81 | bootstrap: [IonicApp], 82 | entryComponents: [ 83 | GadgetDetailModal, 84 | GadgetListPage, 85 | GizmoDetailModal, 86 | GizmoListPage, 87 | WidgetDetailModal, 88 | WidgetListPage, 89 | MyApp, 90 | Page1, 91 | Page2, 92 | HomePage, 93 | LoginPage, 94 | RealtimeDatabasePage, 95 | SignupPage, 96 | ], 97 | providers: [ 98 | AuthService, 99 | StatusBar, 100 | { provide: ErrorHandler, useClass: IonicErrorHandler }, 101 | GadgetDataService, 102 | GadgetService, 103 | GizmoDataService, 104 | GizmoService, 105 | WidgetDataService, 106 | WidgetService, 107 | ], 108 | }) 109 | export class AppModule {} 110 | -------------------------------------------------------------------------------- /src/app/auth/auth.action.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { Action } from '@ngrx/store'; 3 | 4 | export enum AuthActionTypes { 5 | CREATE_USER_WITH_EMAIL_AND_PASSWORD = '[Auth] Create User With Email and Password', 6 | CREATE_USER_WITH_EMAIL_AND_PASSWORD_FAILURE = '[Auth] Create User With Email and Password Failure', 7 | CREATE_USER_WITH_EMAIL_AND_PASSWORD_SUCCESS = '[Auth] Create User With Email and Password Success', 8 | EMAIL_AUTHENTICATION = '[Auth] Email Authentication', 9 | EMAIL_AUTHENTICATION_FAILURE = '[Auth] Email Authentication Failure', 10 | EMAIL_AUTHENTICATION_SUCCESS = '[Auth] Email Authentication Success', 11 | LISTEN_FOR_AUTH = '[Auth] Listen For Auth', 12 | LISTEN_FOR_AUTH_FAILURE = '[Auth] Listen For Auth Failure', 13 | LISTEN_FOR_AUTH_NO_USER = '[Auth] Listen For Auth No User', 14 | LISTEN_FOR_AUTH_SUCCESS = '[Auth] Listen For Auth Success', 15 | SEND_EMAIL_VERIFICATION = '[Auth] Send Email Verification', 16 | SEND_EMAIL_VERIFICATION_FAILURE = '[Auth] Send Email Verification Failure', 17 | SEND_EMAIL_VERIFICATION_SUCCESS = '[Auth] Send Email Verification Success', 18 | SEND_PASSWORD_RESET_EMAIL = '[Auth] Send Password Reset Email', 19 | SEND_PASSWORD_RESET_EMAIL_FAILURE = '[Auth] Send Password Reset Email Failure', 20 | SEND_PASSWORD_RESET_EMAIL_SUCCESS = '[Auth] Send Password Reset Email Success', 21 | SIGN_OUT = '[Auth] Sign Out', 22 | SIGN_OUT_FAILURE = '[Auth] Sign Out Failure', 23 | SIGN_OUT_SUCCESS = '[Auth] Sign Out Success', 24 | UPDATE_PASSWORD = '[Auth] Update Password', 25 | UPDATE_PASSWORD_FAILURE = '[Auth] Update Password Failure', 26 | UPDATE_PASSWORD_SUCCESS = '[Auth] Update Password Success', 27 | } 28 | 29 | export class CreateUserWithEmailAndPassword implements Action { 30 | public readonly type = AuthActionTypes.CREATE_USER_WITH_EMAIL_AND_PASSWORD; 31 | 32 | constructor( 33 | public payload: { 34 | email: string; 35 | password: string; 36 | }, 37 | ) {} 38 | } 39 | 40 | export class CreateUserWithEmailAndPasswordFailure implements Action { 41 | public readonly type = AuthActionTypes.CREATE_USER_WITH_EMAIL_AND_PASSWORD_FAILURE; 42 | 43 | constructor(public payload: { error: any }) {} 44 | } 45 | 46 | export class CreateUserWithEmailAndPasswordSuccess implements Action { 47 | public readonly type = AuthActionTypes.CREATE_USER_WITH_EMAIL_AND_PASSWORD_SUCCESS; 48 | } 49 | 50 | export class EmailAuthentication implements Action { 51 | public readonly type = AuthActionTypes.EMAIL_AUTHENTICATION; 52 | 53 | constructor( 54 | public payload: { 55 | userName: string; 56 | password: string; 57 | }, 58 | ) {} 59 | } 60 | 61 | export class EmailAuthenticationFailure implements Action { 62 | public readonly type = AuthActionTypes.EMAIL_AUTHENTICATION_FAILURE; 63 | 64 | constructor(public payload: { error: any }) {} 65 | } 66 | 67 | export class EmailAuthenticationSuccess implements Action { 68 | public readonly type = AuthActionTypes.EMAIL_AUTHENTICATION_SUCCESS; 69 | } 70 | 71 | export class ListenForAuth implements Action { 72 | public readonly type = AuthActionTypes.LISTEN_FOR_AUTH; 73 | } 74 | 75 | export class ListenForAuthFailure implements Action { 76 | public readonly type = AuthActionTypes.LISTEN_FOR_AUTH_FAILURE; 77 | 78 | constructor(public payload: any) {} 79 | } 80 | 81 | export class ListenForAuthNoUser implements Action { 82 | public readonly type = AuthActionTypes.LISTEN_FOR_AUTH_NO_USER; 83 | } 84 | 85 | export class ListenForAuthSuccess implements Action { 86 | public readonly type = AuthActionTypes.LISTEN_FOR_AUTH_SUCCESS; 87 | 88 | constructor( 89 | public payload: { 90 | signedInUser: { 91 | displayName: string | null; 92 | email: string | null; 93 | emailVerified: boolean; 94 | userId: string; 95 | }; 96 | }, 97 | ) {} 98 | } 99 | 100 | export class SendEmailVerification implements Action { 101 | public readonly type = AuthActionTypes.SEND_EMAIL_VERIFICATION; 102 | } 103 | 104 | export class SendEmailVerificationFailure implements Action { 105 | public readonly type = AuthActionTypes.SEND_EMAIL_VERIFICATION_FAILURE; 106 | 107 | constructor(public payload: { error: any }) {} 108 | } 109 | 110 | export class SendEmailVerificationSuccess implements Action { 111 | public readonly type = AuthActionTypes.SEND_EMAIL_VERIFICATION_SUCCESS; 112 | } 113 | 114 | export class SendPasswordResetEmail implements Action { 115 | public readonly type = AuthActionTypes.SEND_PASSWORD_RESET_EMAIL; 116 | 117 | constructor(public payload: { email: string }) {} 118 | } 119 | 120 | export class SendPasswordResetEmailFailure implements Action { 121 | public readonly type = AuthActionTypes.SEND_PASSWORD_RESET_EMAIL_FAILURE; 122 | 123 | constructor(public payload: { error: any }) {} 124 | } 125 | 126 | export class SendPasswordResetEmailSuccess implements Action { 127 | public readonly type = AuthActionTypes.SEND_PASSWORD_RESET_EMAIL_SUCCESS; 128 | } 129 | 130 | export class SignOut implements Action { 131 | public readonly type = AuthActionTypes.SIGN_OUT; 132 | } 133 | 134 | export class SignOutFailure implements Action { 135 | public readonly type = AuthActionTypes.SIGN_OUT_FAILURE; 136 | 137 | constructor(public payload: { error: any }) {} 138 | } 139 | 140 | export class SignOutSuccess implements Action { 141 | public readonly type = AuthActionTypes.SIGN_OUT_SUCCESS; 142 | } 143 | 144 | export class UpdatePassword implements Action { 145 | public readonly type = AuthActionTypes.UPDATE_PASSWORD; 146 | 147 | constructor(public payload: { password: string }) {} 148 | } 149 | 150 | export class UpdatePasswordFailure implements Action { 151 | public readonly type = AuthActionTypes.UPDATE_PASSWORD_FAILURE; 152 | 153 | constructor(public payload: { error: any }) {} 154 | } 155 | 156 | export class UpdatePasswordSuccess implements Action { 157 | public readonly type = AuthActionTypes.UPDATE_PASSWORD_SUCCESS; 158 | } 159 | 160 | export type AuthActions = 161 | | ListenForAuth 162 | | ListenForAuthNoUser 163 | | ListenForAuthSuccess 164 | | CreateUserWithEmailAndPassword 165 | | CreateUserWithEmailAndPasswordFailure 166 | // CreateUserSuccessAction | 167 | | EmailAuthentication 168 | | EmailAuthenticationFailure 169 | | EmailAuthenticationSuccess 170 | | SignOut 171 | | SignOutFailure 172 | | SignOutSuccess; 173 | -------------------------------------------------------------------------------- /src/app/auth/auth.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { defer } from 'rxjs/observable/defer'; 5 | import { of } from 'rxjs/observable/of'; 6 | import { catchError, concatMap, map, switchMap, tap } from 'rxjs/operators'; 7 | import { 8 | AuthActionTypes, 9 | CreateUserWithEmailAndPassword, 10 | CreateUserWithEmailAndPasswordFailure, 11 | CreateUserWithEmailAndPasswordSuccess, 12 | EmailAuthentication, 13 | EmailAuthenticationFailure, 14 | EmailAuthenticationSuccess, 15 | ListenForAuth, 16 | ListenForAuthFailure, 17 | ListenForAuthNoUser, 18 | ListenForAuthSuccess, 19 | SendEmailVerification, 20 | SendEmailVerificationFailure, 21 | SendEmailVerificationSuccess, 22 | SendPasswordResetEmail, 23 | SendPasswordResetEmailFailure, 24 | SendPasswordResetEmailSuccess, 25 | SignOutFailure, 26 | SignOutSuccess, 27 | UpdatePassword, 28 | UpdatePasswordFailure, 29 | UpdatePasswordSuccess, 30 | } from './auth.action'; 31 | import { AuthService } from './auth.service'; 32 | 33 | @Injectable() 34 | export class AuthEffects { 35 | constructor( 36 | private readonly actions$: Actions, 37 | private readonly authService: AuthService, 38 | ) {} 39 | 40 | // tslint:disable-next-line:member-ordering 41 | @Effect() 42 | public ListenForAuth$ = this.actions$.pipe( 43 | ofType(AuthActionTypes.LISTEN_FOR_AUTH), 44 | tap(() => console.log('ListenForAuth$')), 45 | switchMap(() => 46 | this.authService.authState$().pipe( 47 | map((firebaseUser) => { 48 | if (firebaseUser) { 49 | return new ListenForAuthSuccess({ 50 | signedInUser: { 51 | displayName: firebaseUser.displayName, 52 | email: firebaseUser.email, 53 | emailVerified: firebaseUser.emailVerified, 54 | userId: firebaseUser.uid, 55 | }, 56 | }); 57 | } else { 58 | return new ListenForAuthNoUser(); 59 | } 60 | }), 61 | catchError((error: any) => of(new ListenForAuthFailure(error))), 62 | ), 63 | ), 64 | ); 65 | 66 | // tslint:disable-next-line:member-ordering 67 | @Effect() 68 | public signOut$ = this.actions$ 69 | .ofType(AuthActionTypes.SIGN_OUT) 70 | .switchMap(() => 71 | this.authService 72 | .signOut() 73 | .pipe( 74 | map(() => new SignOutSuccess()), 75 | catchError((error: any) => of(new SignOutFailure(error))), 76 | ), 77 | ); 78 | 79 | // tslint:disable-next-line:member-ordering 80 | @Effect() 81 | public emailAuthentication$ = this.actions$.pipe( 82 | ofType(AuthActionTypes.EMAIL_AUTHENTICATION), 83 | map((action: EmailAuthentication) => action.payload), 84 | concatMap((payload) => 85 | this.authService 86 | .signInWithEmailAndPassword(payload.userName, payload.password) 87 | .then(() => new EmailAuthenticationSuccess()) 88 | .catch((error: any) => new EmailAuthenticationFailure({ error })), 89 | ), 90 | ); 91 | 92 | // tslint:disable-next-line:member-ordering 93 | @Effect() 94 | public createUserWithEmailAndPassword$ = this.actions$.pipe( 95 | ofType( 96 | AuthActionTypes.CREATE_USER_WITH_EMAIL_AND_PASSWORD, 97 | ), 98 | map((action) => action.payload), 99 | concatMap((payload) => 100 | this.authService 101 | .createUserWithEmailAndPassword(payload.email, payload.password) 102 | .pipe( 103 | map(() => new CreateUserWithEmailAndPasswordSuccess()), 104 | catchError((error: any) => 105 | of(new CreateUserWithEmailAndPasswordFailure({ error })), 106 | ), 107 | ), 108 | ), 109 | ); 110 | 111 | // tslint:disable-next-line:member-ordering 112 | @Effect() 113 | public sendEmailVerification$ = this.actions$.pipe( 114 | ofType(AuthActionTypes.SEND_EMAIL_VERIFICATION), 115 | concatMap(() => 116 | this.authService 117 | .sendEmailVerification() 118 | .pipe( 119 | map(() => new SendEmailVerificationSuccess()), 120 | catchError((error: any) => 121 | of(new SendEmailVerificationFailure({ error })), 122 | ), 123 | ), 124 | ), 125 | ); 126 | 127 | // tslint:disable-next-line:member-ordering 128 | @Effect() 129 | public sendPasswordResetEmail$ = this.actions$.pipe( 130 | ofType(AuthActionTypes.SEND_PASSWORD_RESET_EMAIL), 131 | map((action) => action.payload), 132 | concatMap((payload) => 133 | this.authService 134 | .sendPasswordResetEmail(payload.email) 135 | .pipe( 136 | map(() => new SendPasswordResetEmailSuccess()), 137 | catchError((error: any) => 138 | of(new SendPasswordResetEmailFailure({ error })), 139 | ), 140 | ), 141 | ), 142 | ); 143 | 144 | // tslint:disable-next-line:member-ordering 145 | @Effect() 146 | public updatePassword$ = this.actions$.pipe( 147 | ofType(AuthActionTypes.UPDATE_PASSWORD), 148 | map((action) => action.payload), 149 | concatMap((payload) => 150 | this.authService 151 | .updatePassword(payload.password) 152 | .pipe( 153 | map(() => new UpdatePasswordSuccess()), 154 | catchError((error: any) => of(new UpdatePasswordFailure({ error }))), 155 | ), 156 | ), 157 | ); 158 | // Should be your last effect 159 | // tslint:disable-next-line:member-ordering 160 | @Effect() 161 | public init$: Observable = defer(() => { 162 | return of(new ListenForAuth()); 163 | }); 164 | } 165 | -------------------------------------------------------------------------------- /src/app/auth/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import { AuthActions, AuthActionTypes } from './auth.action'; 2 | 3 | export interface AuthState { 4 | displayName: string; 5 | email: string | null; 6 | emailVerified: boolean; 7 | hasDoneFirstCheck: boolean; 8 | isAuthenticated: boolean; 9 | isAuthenticating: boolean; 10 | error: any; 11 | userId: string; 12 | } 13 | 14 | const initialState: AuthState = { 15 | displayName: '', 16 | email: '', 17 | emailVerified: false, 18 | error: null, 19 | hasDoneFirstCheck: false, 20 | isAuthenticated: false, 21 | isAuthenticating: false, 22 | userId: '', 23 | }; 24 | 25 | export function reducer(state = initialState, action: AuthActions): AuthState { 26 | switch (action.type) { 27 | case AuthActionTypes.LISTEN_FOR_AUTH_SUCCESS: { 28 | return { 29 | ...state, 30 | displayName: makeDisplayName(action.payload.signedInUser), 31 | email: action.payload.signedInUser.email, 32 | emailVerified: action.payload.signedInUser.emailVerified, 33 | hasDoneFirstCheck: true, 34 | isAuthenticated: true, 35 | isAuthenticating: false, 36 | userId: action.payload.signedInUser.userId, 37 | }; 38 | } 39 | 40 | case AuthActionTypes.LISTEN_FOR_AUTH_NO_USER: { 41 | return { 42 | ...state, 43 | displayName: 'Not Signed In', 44 | email: '', 45 | hasDoneFirstCheck: true, 46 | isAuthenticated: false, 47 | isAuthenticating: false, 48 | userId: '', 49 | }; 50 | } 51 | case AuthActionTypes.CREATE_USER_WITH_EMAIL_AND_PASSWORD: 52 | case AuthActionTypes.EMAIL_AUTHENTICATION: { 53 | return { 54 | ...state, 55 | error: null, 56 | isAuthenticated: false, 57 | isAuthenticating: true, 58 | }; 59 | } 60 | 61 | case AuthActionTypes.EMAIL_AUTHENTICATION_FAILURE: { 62 | return { 63 | ...state, 64 | error: action.payload.error, 65 | // isAuthenticating: false, 66 | }; 67 | } 68 | 69 | case AuthActionTypes.CREATE_USER_WITH_EMAIL_AND_PASSWORD_FAILURE: { 70 | return { 71 | ...state, 72 | error: action.payload, 73 | isAuthenticating: false, 74 | }; 75 | } 76 | 77 | default: { 78 | return state; 79 | } 80 | } 81 | } 82 | 83 | function makeDisplayName(user: { 84 | displayName: string | null; 85 | email: string | null; 86 | userId: string; 87 | }) { 88 | // 89 | const defaultDisplayName = ''; 90 | 91 | if (user.displayName) { 92 | return user.displayName; 93 | } 94 | 95 | if (user.email) { 96 | return user.email; 97 | } 98 | return defaultDisplayName; 99 | } 100 | 101 | export const getDisplayName = (state: AuthState) => state.displayName; 102 | export const getHasDoneFirstCheck = (state: AuthState) => state.hasDoneFirstCheck; 103 | export const getEmailVerified = (state: AuthState) => state.emailVerified; 104 | export const getError = (state: AuthState) => state.error; 105 | export const getIsAuthenticated = (state: AuthState) => state.isAuthenticated; 106 | export const getIsAuthenticating = (state: AuthState) => state.isAuthenticating; 107 | export const getUserId = (state: AuthState) => state.userId; 108 | -------------------------------------------------------------------------------- /src/app/auth/auth.selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@ngrx/store'; 2 | 3 | import * as FromRootReducer from '../../reducers'; 4 | import * as fromAuth from './auth.reducer'; 5 | 6 | export const getAuthState = (state: FromRootReducer.State) => state.auth; 7 | 8 | export const getDisplayName = createSelector( 9 | getAuthState, 10 | fromAuth.getDisplayName, 11 | ); 12 | 13 | export const getHasDoneFirstCheck = createSelector( 14 | getAuthState, 15 | fromAuth.getHasDoneFirstCheck, 16 | ); 17 | 18 | export const getError = createSelector(getAuthState, fromAuth.getError); 19 | 20 | export const getEmailVerified = createSelector( 21 | getAuthState, 22 | fromAuth.getEmailVerified, 23 | ); 24 | 25 | export const getIsAuthenticated = createSelector( 26 | getAuthState, 27 | fromAuth.getIsAuthenticated, 28 | ); 29 | 30 | export const getIsAuthenticating = createSelector( 31 | getAuthState, 32 | fromAuth.getIsAuthenticating, 33 | ); 34 | 35 | export const getUserId = createSelector(getAuthState, fromAuth.getUserId); 36 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFireAuth } from 'angularfire2/auth'; 3 | 4 | import { Observable } from 'rxjs/Observable'; 5 | import { empty } from 'rxjs/observable/empty'; 6 | 7 | @Injectable() 8 | export class AuthService { 9 | constructor(private readonly auth$: AngularFireAuth) {} 10 | 11 | public authState$() { 12 | return this.auth$.authState; 13 | } 14 | 15 | public createUserWithEmailAndPassword(email: string, password: string) { 16 | return Observable.fromPromise( 17 | this.auth$.auth.createUserWithEmailAndPassword(email, password), 18 | ); 19 | } 20 | 21 | public sendEmailVerification() { 22 | if (this.auth$.auth.currentUser) { 23 | return Observable.fromPromise( 24 | this.auth$.auth.currentUser.sendEmailVerification(), 25 | ); 26 | } else { 27 | return empty(); 28 | } 29 | } 30 | 31 | public sendPasswordResetEmail(email: string) { 32 | return Observable.fromPromise( 33 | this.auth$.auth.sendPasswordResetEmail(email), 34 | ); 35 | } 36 | 37 | public signInWithEmailAndPassword(email: string, password: string) { 38 | return this.auth$.auth.signInWithEmailAndPassword(email, password); 39 | } 40 | 41 | public signOut() { 42 | return Observable.fromPromise(this.auth$.auth.signOut()); 43 | } 44 | 45 | public updatePassword(password: string) { 46 | if (this.auth$.auth.currentUser) { 47 | return Observable.fromPromise( 48 | this.auth$.auth.currentUser.updatePassword(password), 49 | ); 50 | } else { 51 | return empty(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tja4472/ngrx-ionic-angularfire/3ab67d8c0e8abc7a5235511bfae7b7fd96bb3a0d/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic", 3 | "short_name": "Ionic", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/assets/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('activate', function (event) { 2 | 3 | }); 4 | 5 | self.addEventListener('fetch', function (event) { 6 | 7 | }); 8 | 9 | self.addEventListener('push', function (event) { 10 | 11 | }); -------------------------------------------------------------------------------- /src/components/error/error.component.html: -------------------------------------------------------------------------------- 1 |
2 |
Error Component
3 |

{{error | json}}

4 |
5 | -------------------------------------------------------------------------------- /src/components/error/error.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | export type ErrorInput = any; 4 | 5 | @Component({ 6 | changeDetection: ChangeDetectionStrategy.OnPush, 7 | selector: 'error', 8 | templateUrl: 'error.component.html', 9 | }) 10 | export class Error { 11 | @Input() public error: ErrorInput; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/example-list/example-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | ##example-list## 3 | Loading... 4 | 9 |
10 | -------------------------------------------------------------------------------- /src/components/example-list/example-list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | import { TextItem } from '../../models'; 4 | 5 | export type IsFetchingInput = boolean; 6 | export type PostsInput = TextItem[]; 7 | 8 | @Component({ 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | selector: 'example-list', 11 | templateUrl: 'example-list.component.html', 12 | }) 13 | export class ExampleList { 14 | @Input() public posts: PostsInput; 15 | @Input() public isFetching: IsFetchingInput; 16 | } 17 | -------------------------------------------------------------------------------- /src/effects/text-item.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { AngularFireDatabase } from 'angularfire2/database'; 4 | 5 | import { Actions, Effect, ofType } from '@ngrx/effects'; 6 | import { Store } from '@ngrx/store'; 7 | 8 | import { 9 | filter, 10 | map, 11 | switchMap, 12 | tap, 13 | withLatestFrom, 14 | } from 'rxjs/operators'; 15 | 16 | import { 17 | LoadCollectionSuccessAction, 18 | TextItemActionTypes, 19 | } from '../actions/text-item.action'; 20 | import { TextItem } from '../models'; 21 | import { State } from '../reducers'; 22 | 23 | @Injectable() 24 | export class TextItemEffects { 25 | constructor( 26 | private actions$: Actions, 27 | private state$: Store, 28 | public afDb: AngularFireDatabase, 29 | ) {} 30 | 31 | // tslint:disable-next-line:member-ordering 32 | @Effect() 33 | public loadCollection$ = this.actions$.pipe( 34 | ofType(TextItemActionTypes.LoadCollection), 35 | // This will cause the effect to run once immediately on startup 36 | // .startWith(new LoadCollectionAction()) 37 | tap((x) => { 38 | console.log('Effect:loadCollection$:A', x); 39 | }), 40 | withLatestFrom(this.state$), 41 | // tslint:disable-next-line:no-unused-variable 42 | filter(([, state]) => (state as State).auth.isAuthenticated), 43 | // Watch database node and get TextItems. 44 | switchMap(() => this.afDb.list('/textItems').valueChanges()), 45 | tap((x) => { 46 | console.log('Effect:loadCollection$:B', x); 47 | }), 48 | map((textItems: TextItem[]) => new LoadCollectionSuccessAction(textItems)), 49 | ); 50 | } 51 | 52 | // .withLatestFrom(this.store.select('masterGain')) 53 | // .withLatestFrom(this.state$, (action, state) => state.aaa) 54 | 55 | /* 56 | @Effect() redirectAfterLogin$ = this.actions$ 57 | .ofType(AuthActions.REDIRECT_AFTER_LOGIN) 58 | .withLatestFrom(this.store.let(appSelectors.getAuthRedirectUrl())) 59 | .do(([action, url]) => router.navigateByUrl.next(url)) 60 | .mapTo(this.authActions.resetRedirectAfterLogin()) 61 | */ 62 | 63 | /* 64 | @Injectable() 65 | export class UserService implements OnDestroy { 66 | // our subscription(s) to @ngrx/effects 67 | private subscription: Subscription; 68 | 69 | constructor(private router: Router, private actions$: Actions, private store$: Store) { 70 | this.subscription = mergeEffects(this).subscribe(store$); 71 | } 72 | 73 | @Effect({dispatch: false}) usr_connect$ = this.actions$ 74 | .ofType(USR_CONNECT) 75 | .withLatestFrom(this.store$) 76 | .do(([action, state]) => {console.log(state)}) 77 | .do(() => this.router.navigate(['/petals-cockpit'])); 78 | 79 | ngOnDestroy() { 80 | this.subscription.unsubscribe(); 81 | } 82 | } 83 | */ 84 | 85 | // http://stackoverflow.com/search?q=%5Bngrx%5D+withlatestfrom 86 | // https://github.com/teropa/harmonics-explorer/blob/master/src/app/services/audio.service.ts 87 | 88 | /* 89 | constructor( 90 | private actions$: Actions, 91 | private store: Store 92 | ) {} 93 | 94 | @Effect() bar$ = this.actions$ 95 | .ofType(ActionTypes.FOO) 96 | .withLatestFrom(this.store, (action, state) => state.user.isCool) 97 | .distinctUntilChanged() 98 | .filter(x => x) 99 | .map(() => ({ type: ActionTypes.BAR })); 100 | */ 101 | 102 | /* 103 | @Effect() update$ = this.action$ 104 | .ofType('UPDATE_NOTE_TEXT', 'UPDATE_NOTE_POSITION', 'ADD_NOTE') 105 | .withLatestFrom(this.store.select('notes')) 106 | .mergeMap(notes => Observable.from(notes)) 107 | .filter((note:Note) => {return (note.dirty==true)}) 108 | .switchMap((updatedNote:Note) => this.notesDataService.addOrUpdateNote(updatedNote) 109 | .map((responseNote:Note) => ({ type: "UPDATE_NOTE_FROM_SERVER", payload: { note: responseNote } })) 110 | .catch(() => Observable.of({ type: "UPDATE_FAILED" })) 111 | ) 112 | */ 113 | -------------------------------------------------------------------------------- /src/gadget/gadget.actions.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { Action } from '@ngrx/store'; 3 | import { Gadget } from './gadget.model'; 4 | 5 | export enum GadgetActionTypes { 6 | // 7 | DATABASE_LISTEN_FOR_DATA_START = '[Gadget] (Database) Listen For Data - Start', 8 | DATABASE_LISTEN_FOR_DATA_START_ERROR = '[Gadget] (Database) Listen For Data - Start - Error', 9 | DATABASE_LISTEN_FOR_DATA_STOP = '[Gadget] (Database) Listen For Data - Stop', 10 | DELETE_ITEM = '[Gadget] Delete Item', 11 | LOAD_SUCCESS = '[Gadget] Load Success', 12 | UPSERT_ITEM = '[Gadget] Upsert item', 13 | UPSERT_ITEM_ERROR = '[Gadget] Upsert Item - Error ', 14 | UPSERT_ITEM_SUCCESS = '[Gadget] Upsert Item - Success', 15 | } 16 | 17 | export class DatabaseListenForDataStart implements Action { 18 | public readonly type = GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_START; 19 | 20 | constructor( 21 | public payload: { 22 | userId: string; 23 | }, 24 | ) {} 25 | } 26 | 27 | export class DatabaseListenForDataStartError implements Action { 28 | public readonly type = GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_START_ERROR; 29 | 30 | constructor( 31 | public payload: { 32 | error: { 33 | code: string; 34 | message: string; 35 | name: string; 36 | }; 37 | }, 38 | ) {} 39 | } 40 | 41 | export class DatabaseListenForDataStop implements Action { 42 | public readonly type = GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_STOP; 43 | } 44 | 45 | export class DeleteItem implements Action { 46 | public readonly type = GadgetActionTypes.DELETE_ITEM; 47 | 48 | constructor(public payload: { id: string; userId: string }) {} 49 | } 50 | 51 | export class LoadSuccess implements Action { 52 | public readonly type = GadgetActionTypes.LOAD_SUCCESS; 53 | 54 | constructor(public payload: { items: Gadget[] }) {} 55 | } 56 | 57 | export class UpsertItem implements Action { 58 | public readonly type = GadgetActionTypes.UPSERT_ITEM; 59 | 60 | constructor(public payload: { item: Gadget; userId: string }) {} 61 | } 62 | 63 | export class UpsertItemError implements Action { 64 | public readonly type = GadgetActionTypes.UPSERT_ITEM_ERROR; 65 | 66 | constructor( 67 | public payload: { 68 | error: { 69 | code: string; 70 | message: string; 71 | name: string; 72 | }; 73 | }, 74 | ) {} 75 | } 76 | 77 | export class UpsertItemSuccess implements Action { 78 | public readonly type = GadgetActionTypes.UPSERT_ITEM_SUCCESS; 79 | } 80 | 81 | export type GadgetActions = 82 | | DeleteItem 83 | | LoadSuccess 84 | | DatabaseListenForDataStart 85 | | DatabaseListenForDataStartError 86 | | DatabaseListenForDataStop 87 | | UpsertItem 88 | | UpsertItemError 89 | | UpsertItemSuccess; 90 | -------------------------------------------------------------------------------- /src/gadget/gadget.data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFireDatabase } from 'angularfire2/database'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { Gadget } from './gadget.model'; 6 | 7 | const DATA_COLLECTION = 'gadgets'; 8 | const USERS_COLLECTION = 'users'; 9 | 10 | interface FirestoreDoc { 11 | id: string; 12 | description: string; 13 | name: string; 14 | sysDateCreatedOn?: string; 15 | sysDateUpdatedOn?: string; 16 | } 17 | 18 | @Injectable() 19 | export class GadgetDataService { 20 | constructor(public readonly afs: AngularFireDatabase) { 21 | // console.log('GadgetDataService:constructor'); 22 | } 23 | 24 | public getData$(userId: string): Observable { 25 | // 26 | return this.angularFireList(userId) 27 | .valueChanges() 28 | .map((items) => 29 | items.map((item) => { 30 | return this.fromFirestoreDoc(item); 31 | }), 32 | ); 33 | } 34 | 35 | public deleteItem(id: string, userId: string): void { 36 | this.angularFireList(userId).remove(id); 37 | } 38 | 39 | public upsertItem(item: Gadget, userId: string): Promise { 40 | // 41 | userId = userId; 42 | 43 | if (item.id === '') { 44 | return this.createItem(item, userId); 45 | } else { 46 | return this.updateItem(item, userId); 47 | } 48 | } 49 | 50 | private createItem(item: Gadget, userId: string): Promise { 51 | // 52 | const doc = this.toFirestoreDoc(item); 53 | const dateNow = Date().toString(); 54 | const recordToSet: FirestoreDoc = { 55 | ...doc, 56 | sysDateCreatedOn: dateNow, 57 | sysDateUpdatedOn: dateNow, 58 | }; 59 | 60 | return new Promise((resolve, reject) => { 61 | this.angularFireList(userId) 62 | .push(recordToSet) 63 | .then( 64 | (reference) => { 65 | const values: Partial = { 66 | // id: reference.key != null ? reference.key : '', 67 | id: reference.key!, 68 | }; 69 | 70 | reference.update(values); 71 | resolve(); 72 | }, 73 | (error) => reject(error), 74 | ); 75 | }); 76 | } 77 | 78 | private updateItem(item: Gadget, userId: string): Promise { 79 | // 80 | const doc = this.toFirestoreDoc(item); 81 | const dateNow = Date().toString(); 82 | const recordToUpdate: FirestoreDoc = { 83 | ...doc, 84 | sysDateUpdatedOn: dateNow, 85 | }; 86 | 87 | return this.angularFireList(userId).update(doc.id, recordToUpdate); 88 | } 89 | 90 | private angularFireList(userId: string) { 91 | // 92 | const pathOrRef = USERS_COLLECTION + '/' + userId + '/' + DATA_COLLECTION; 93 | 94 | return this.afs.list(pathOrRef, (ref) => 95 | ref.orderByChild('name'), 96 | ); 97 | /* 98 | return this.afs 99 | .collection(USERS_COLLECTION) 100 | .doc(userId) 101 | .collection(DATA_COLLECTION, (ref) => 102 | ref.orderBy('name', 'asc'), 103 | ); 104 | */ 105 | /* 106 | return this.afs.collection(DATA_COLLECTION, (ref) => 107 | ref.orderBy('name', 'asc'), 108 | ); 109 | */ 110 | } 111 | 112 | private toFirestoreDoc(item: Gadget): FirestoreDoc { 113 | // 114 | const result: FirestoreDoc = { 115 | description: item.description, 116 | id: item.id, 117 | name: item.name, 118 | }; 119 | 120 | // console.log('toFirebaseTodo>', result); 121 | return result; 122 | } 123 | 124 | private fromFirestoreDoc(x: FirestoreDoc): Gadget { 125 | // 126 | // console.log('TodoDataService:fromFirebaseTodo>', x); 127 | 128 | // This copies extra fields. 129 | // const result: Gadget = { ...x }; 130 | 131 | const result: Gadget = { 132 | description: x.description, 133 | id: x.id, 134 | name: x.name, 135 | }; 136 | 137 | // console.log('TodoDataService:fromFirebaseTodo:result>', result); 138 | 139 | return result; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/gadget/gadget.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect, ofType } from '@ngrx/effects'; 4 | import { Store } from '@ngrx/store'; 5 | import { empty } from 'rxjs/observable/empty'; 6 | 7 | import * as FromRootReducer from '../reducers'; 8 | import { 9 | DatabaseListenForDataStart, 10 | DatabaseListenForDataStartError, 11 | DatabaseListenForDataStop, 12 | DeleteItem, 13 | GadgetActionTypes, 14 | LoadSuccess, 15 | UpsertItem, 16 | UpsertItemError, 17 | UpsertItemSuccess, 18 | } from './gadget.actions'; 19 | import { GadgetDataService } from './gadget.data.service'; 20 | import { Gadget } from './gadget.model'; 21 | 22 | import { fromPromise } from 'rxjs/observable/fromPromise'; 23 | import { of } from 'rxjs/observable/of'; 24 | import { 25 | catchError, 26 | map, 27 | switchMap, 28 | tap, 29 | } from 'rxjs/operators'; 30 | 31 | @Injectable() 32 | export class GadgetEffects { 33 | constructor( 34 | private actions$: Actions, 35 | private store$: Store, 36 | private dataService: GadgetDataService, 37 | ) {} 38 | 39 | // tslint:disable-next-line:member-ordering 40 | @Effect({ dispatch: false }) 41 | public deleteItem$ = this.actions$.pipe( 42 | ofType(GadgetActionTypes.DELETE_ITEM), 43 | map((action: DeleteItem) => action.payload), 44 | tap((payload) => { 45 | console.log('Effect:deleteItem$:A', payload); 46 | this.dataService.deleteItem(payload.id, payload.userId); 47 | }), 48 | ); 49 | 50 | // tslint:disable-next-line:member-ordering 51 | @Effect({ dispatch: false }) 52 | public listenForData$ = this.actions$.pipe( 53 | ofType( 54 | GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_START, 55 | GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_STOP, 56 | ), 57 | tap(() => { 58 | console.log('Effect:listenForData$:A'); 59 | }), 60 | switchMap((action) => { 61 | console.log('Effect:listenForData$:action>', action); 62 | switch (action.type) { 63 | case GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_START: { 64 | return this.dataService.getData$(action.payload.userId).pipe( 65 | map((items: Gadget[]) => { 66 | this.store$.dispatch(new LoadSuccess({ items })); 67 | }), 68 | catchError((error) => { 69 | this.store$.dispatch( 70 | new DatabaseListenForDataStartError({ 71 | error: this.handleFirebaseError(error), 72 | }), 73 | ); 74 | // Pass on to higher level. 75 | // throw error; 76 | return empty(); 77 | }), 78 | ); 79 | } 80 | 81 | default: { 82 | return empty(); 83 | } 84 | } 85 | }), 86 | tap((x) => { 87 | console.log('Effect:listenForData$:B', x); 88 | }), 89 | ); 90 | 91 | // tslint:disable-next-line:member-ordering 92 | @Effect() 93 | public upsertItem$ = this.actions$.pipe( 94 | ofType(GadgetActionTypes.UPSERT_ITEM), 95 | map((action) => action.payload), 96 | switchMap((payload) => { 97 | return fromPromise( 98 | this.dataService.upsertItem(payload.item, payload.userId), 99 | ).pipe( 100 | map(() => new UpsertItemSuccess()), 101 | catchError((error) => 102 | of( 103 | new UpsertItemError({ 104 | error: this.handleFirebaseError(error), 105 | }), 106 | ), 107 | ), 108 | ); 109 | }), 110 | ); 111 | 112 | private handleFirebaseError(firebaseError: any) { 113 | // 114 | return { 115 | code: firebaseError.code, 116 | message: firebaseError.message, 117 | name: firebaseError.name, 118 | }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/gadget/gadget.model.ts: -------------------------------------------------------------------------------- 1 | // https://redux.js.org/docs/faq/OrganizingState.html#organizing-state-non-serializable 2 | /* 3 | Can I put functions, promises, or other non-serializable items in my store 4 | state? It is highly recommended that you only put plain serializable objects, 5 | arrays, and primitives into your store. It's technically possible to insert 6 | non-serializable items into the store, but doing so can break the ability to 7 | persist and rehydrate the contents of a store, as well as interfere with 8 | time-travel debugging. 9 | 10 | If you are okay with things like persistence and time-travel debugging 11 | potentially not working as intended, then you are totally welcome to put 12 | non-serializable items into your Redux store. Ultimately, it's your application, 13 | and how you implement it is up to you. As with many other things about Redux, 14 | just be sure you understand what tradeoffs are involved. 15 | */ 16 | // https://stackoverflow.com/questions/43181516/getting-model-instance-from-ngrx-store-select/43185931#43185931 17 | /* 18 | But: It is generally not recommended to have Class-Instances in the store, there 19 | are a few rules of thumb: 20 | 21 | The store-content should serializable without any major modifications (=> just 22 | use Object and Primitives) ngrx (and rxjs in general) are relying heavily on 23 | functional programming patterns, so mixing it Object Oriented paradigms is not 24 | recommended. 25 | */ 26 | export interface Gadget { 27 | readonly description: string; 28 | readonly id: string; 29 | readonly name: string; 30 | } 31 | 32 | export function newGadget(): Gadget { 33 | return { description: '', id: '', name: '' }; 34 | } 35 | -------------------------------------------------------------------------------- /src/gadget/gadget.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 2 | 3 | import { GadgetActions, GadgetActionTypes } from './gadget.actions'; 4 | import { Gadget } from './gadget.model'; 5 | 6 | export interface State extends EntityState { 7 | // additional entities state properties 8 | loaded: boolean; 9 | loading: boolean; 10 | selectedGadgetId: string; 11 | } 12 | 13 | export const adapter: EntityAdapter = createEntityAdapter(); 14 | 15 | export const initialState: State = adapter.getInitialState({ 16 | // additional entity state properties 17 | loaded: false, 18 | loading: false, 19 | selectedGadgetId: '', 20 | }); 21 | 22 | export function reducer(state = initialState, action: GadgetActions): State { 23 | switch (action.type) { 24 | case GadgetActionTypes.DATABASE_LISTEN_FOR_DATA_STOP: { 25 | return adapter.removeAll({ 26 | ...state, 27 | loaded: false, 28 | loading: false, 29 | selectedGadgetId: '', 30 | }); 31 | } 32 | 33 | case GadgetActionTypes.LOAD_SUCCESS: { 34 | return adapter.addAll(action.payload.items, state); 35 | } 36 | 37 | default: { 38 | return state; 39 | } 40 | } 41 | } 42 | 43 | export const getSelectedGadgetId = (state: State) => state.selectedGadgetId; 44 | 45 | export const { 46 | // select the array of gadget ids 47 | selectIds: selectGadgetIds, 48 | 49 | // select the dictionary of gadget entities 50 | selectEntities: selectGadgetEntities, 51 | 52 | // select the array of gadgets 53 | selectAll: selectAllGadgets, 54 | 55 | // select the total gadget count 56 | selectTotal: selectGadgetTotal, 57 | } = adapter.getSelectors(); 58 | 59 | export const getLoaded = (state: State) => state.loaded; 60 | export const getLoading = (state: State) => state.loading; 61 | -------------------------------------------------------------------------------- /src/gadget/gadget.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { select, Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { filter, take } from 'rxjs/operators'; 6 | 7 | import * as FromAuthSelector from '../app/auth/auth.selector'; 8 | import * as FromRootReducer from '../reducers'; 9 | import { 10 | DatabaseListenForDataStart, 11 | DatabaseListenForDataStop, 12 | DeleteItem, 13 | UpsertItem, 14 | } from './gadget.actions'; 15 | import { Gadget } from './gadget.model'; 16 | 17 | @Injectable() 18 | export class GadgetService { 19 | // 20 | private init$ = this.store.pipe( 21 | select(FromAuthSelector.getUserId), 22 | filter((userId) => userId !== ''), 23 | ); 24 | 25 | constructor(private store: Store) {} 26 | 27 | public getData$(): Observable> { 28 | // 29 | return this.store.pipe(select(FromRootReducer.selectAllGadgets)); 30 | } 31 | 32 | public ListenForDataStart(): void { 33 | // 34 | this.init$.pipe(take(1)).subscribe((userId) => { 35 | this.store.dispatch(new DatabaseListenForDataStart({ userId })); 36 | }); 37 | } 38 | 39 | public ListenForDataStop(): void { 40 | // 41 | this.store.dispatch(new DatabaseListenForDataStop()); 42 | } 43 | 44 | public deleteItem(item: Gadget) { 45 | // 46 | this.init$.pipe(take(1)).subscribe((userId) => { 47 | this.store.dispatch(new DeleteItem({ id: item.id, userId })); 48 | }); 49 | } 50 | 51 | public upsertItem(item: Gadget) { 52 | // 53 | this.init$.pipe(take(1)).subscribe((userId) => { 54 | this.store.dispatch(new UpsertItem({ item, userId })); 55 | }); 56 | } 57 | 58 | public isLoaded(): Observable { 59 | // 60 | return this.store.pipe(select(FromRootReducer.getGadgetLoaded)); 61 | } 62 | 63 | public isLoading(): Observable { 64 | return this.store.pipe(select(FromRootReducer.getGadgetLoading)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/gadget/modals/gadget-detail/gadget-detail.modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | Gadget 11 | 12 | 18 | 19 | 20 | 21 | 22 |
24 | 25 | 26 | Name 28 | 30 | 31 | 32 | 33 | 34 | Description 36 | 38 | 39 | 40 | 41 |
42 |
-------------------------------------------------------------------------------- /src/gadget/modals/gadget-detail/gadget-detail.modal.scss: -------------------------------------------------------------------------------- 1 | tja-modal-gadget-detail { 2 | } 3 | -------------------------------------------------------------------------------- /src/gadget/modals/gadget-detail/gadget-detail.modal.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { NavParams, ViewController } from 'ionic-angular'; 5 | 6 | import { Gadget, newGadget } from '../../gadget.model'; 7 | 8 | interface ModalInput { 9 | item?: Gadget; 10 | } 11 | 12 | export function getModalInput(item: Gadget | undefined) { 13 | // 14 | const modalInput: ModalInput = { item }; 15 | 16 | return { 17 | modalInput, 18 | }; 19 | } 20 | 21 | export interface ModalResult { 22 | save: boolean; 23 | item?: Gadget; 24 | } 25 | 26 | interface FormModel { 27 | description: any; 28 | name: any; 29 | } 30 | 31 | @Component({ 32 | selector: 'tja-modal-gadget-detail', 33 | templateUrl: 'gadget-detail.modal.html', 34 | }) 35 | export class GadgetDetailModal { 36 | // Called from view. 37 | public viewForm: FormGroup; 38 | 39 | public get viewCanSave(): boolean { 40 | return !(this.viewForm.dirty && this.viewForm.valid); 41 | } 42 | 43 | private readonly CLASS_NAME = 'GadgetDetailModal'; 44 | 45 | private dataModel: Gadget; 46 | 47 | constructor( 48 | public formBuilder: FormBuilder, 49 | public navParams: NavParams, 50 | public viewController: ViewController, 51 | ) { 52 | console.log(`%s:constructor`, this.CLASS_NAME); 53 | 54 | const modalInput: ModalInput = this.getModalInput(); 55 | 56 | if (modalInput.item === undefined) { 57 | // new item. 58 | this.dataModel = newGadget(); 59 | } else { 60 | // navParams passes by reference. 61 | this.dataModel = { ...modalInput.item }; 62 | } 63 | } 64 | 65 | public ngOnInit() { 66 | console.log('###%s:ngOnInit>', this.CLASS_NAME); 67 | 68 | this.viewForm = this.formBuilder.group({ 69 | description: [this.dataModel.description], 70 | name: [this.dataModel.name, Validators.required], 71 | }); 72 | } 73 | 74 | public viewCancel() { 75 | console.log('viewCancel>'); 76 | const result: ModalResult = { save: false }; 77 | 78 | this.viewController.dismiss(result); 79 | } 80 | 81 | public viewSave() { 82 | console.log('viewSave>'); 83 | 84 | if (!this.viewForm.valid) { 85 | return; 86 | } 87 | 88 | const saveItem = this.prepareSaveItem(); 89 | const result: ModalResult = { save: true, item: saveItem }; 90 | this.viewController.dismiss(result); 91 | } 92 | 93 | private getModalInput(): ModalInput { 94 | // 95 | return this.navParams.get('modalInput'); 96 | } 97 | 98 | private prepareSaveItem(): Gadget { 99 | const formModel: FormModel = this.viewForm.value; 100 | 101 | const saveItem: Gadget = { 102 | ...this.dataModel, 103 | description: formModel.description, 104 | name: formModel.name, 105 | }; 106 | 107 | return saveItem; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/gadget/pages/gadget-list/gadget-list.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Gadgets(Realtime Database) 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | IsLoading: {{viewIsLoading$ | async}} 22 |
23 |
24 | IsLoaded: {{viewIsLoaded$ | async}} 25 |
26 | 27 | 28 | 29 |
{{item.name}}
30 |

{{item.description}}

31 | 33 | 36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /src/gadget/pages/gadget-list/gadget-list.page.scss: -------------------------------------------------------------------------------- 1 | tja-page-gadget-list { 2 | } 3 | -------------------------------------------------------------------------------- /src/gadget/pages/gadget-list/gadget-list.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { ModalController } from 'ionic-angular'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { Gadget } from '../../gadget.model'; 7 | import { GadgetService } from '../../gadget.service'; 8 | import { 9 | GadgetDetailModal, 10 | getModalInput, 11 | ModalResult, 12 | } from '../../modals/gadget-detail/gadget-detail.modal'; 13 | 14 | // import { Store } from '@ngrx/store'; 15 | 16 | // import * as fromRoot from '../../reducers'; 17 | @Component({ 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | selector: 'tja-page-gadget-list', 20 | templateUrl: 'gadget-list.page.html', 21 | }) 22 | export class GadgetListPage { 23 | public viewData$: Observable>; 24 | public viewIsLoaded$: Observable; 25 | public viewIsLoading$: Observable; 26 | 27 | constructor( 28 | public modalCtrl: ModalController, 29 | private readonly gadgetService: GadgetService, 30 | ) { 31 | this.viewData$ = gadgetService.getData$(); 32 | this.viewIsLoaded$ = gadgetService.isLoaded(); 33 | this.viewIsLoading$ = gadgetService.isLoading(); 34 | } 35 | 36 | public ionViewDidLoad() { 37 | // 38 | this.gadgetService.ListenForDataStart(); 39 | } 40 | 41 | public ionViewWillUnload() { 42 | // 43 | this.gadgetService.ListenForDataStop(); 44 | } 45 | 46 | public viewAdd(): void { 47 | console.log('viewAdd'); 48 | this.showModal(); 49 | } 50 | 51 | public viewDelete(item: Gadget): void { 52 | console.log('viewDelete>', item); 53 | this.gadgetService.deleteItem(item); 54 | } 55 | 56 | public viewEdit(item: any): void { 57 | console.log('viewEdit>', item); 58 | this.showModal(item); 59 | } 60 | 61 | private showModal(item?: Gadget) { 62 | // 63 | const modalInput = getModalInput(item); 64 | 65 | const modal = this.modalCtrl.create(GadgetDetailModal, modalInput); 66 | 67 | modal.onDidDismiss((data: ModalResult | null) => { 68 | console.log('onDidDismiss>', data); 69 | 70 | if (data === null) { 71 | console.log('onDidDismiss:NULL'); 72 | return; 73 | } 74 | 75 | if (data.save && data.item) { 76 | this.gadgetService.upsertItem(data.item); 77 | } 78 | }); 79 | 80 | modal.present(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/gizmo/gizmo.actions.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { Update } from '@ngrx/entity'; 3 | import { Action } from '@ngrx/store'; 4 | 5 | import { Gizmo } from './gizmo.model'; 6 | 7 | export enum GizmoActionTypes { 8 | // 9 | DATABASE_DELETE_ITEM = '[Gizmo] (Database) Delete Item', 10 | DATABASE_LISTEN_FOR_ADDED_ITEMS = '[Gizmo] (Database) Listen For Added Items', 11 | DATABASE_LISTEN_FOR_ADDED_ITEMS_ERROR = '[Gizmo] (Database) Listen For Added Items - Error', 12 | DATABASE_LISTEN_FOR_DATA_START = '[Gizmo] (Database) Listen For Data - Start', 13 | DATABASE_LISTEN_FOR_DATA_STOP = '[Gizmo] (Database) Listen For Data - Stop', 14 | DATABASE_LISTEN_FOR_MODIFIED_ITEMS = '[Gizmo] (Database) Listen For Modified Items', 15 | DATABASE_LISTEN_FOR_MODIFIED_ITEMS_ERROR = '[Gizmo] (Database) Listen For Modified Items - Error', 16 | DATABASE_LISTEN_FOR_REMOVED_ITEMS = '[Gizmo] (Database) Listen For Removed Items', 17 | DATABASE_LISTEN_FOR_REMOVED_ITEMS_ERROR = '[Gizmo] (Database) Listen For Removed Items - Error', 18 | DATABASE_UPSERT_ITEM = '[Gizmo] (Database) Upsert Item', 19 | DATABASE_UPSERT_ITEM_ERROR = '[Gizmo] (Database) Upsert Item - Error ', 20 | DATABASE_UPSERT_ITEM_SUCCESS = '[Gizmo] (Database) Upsert Item - Success', 21 | // 22 | STORE_ADD_ITEMS = '[Gizmo] (Store) Add Items', 23 | STORE_DELETE_ITEMS = '[Gizmo] (Store) Delete Items', 24 | STORE_UPDATE_ITEMS = '[Gizmo] (Store) Update Items', 25 | } 26 | 27 | export class DatabaseDeleteItem implements Action { 28 | public readonly type = GizmoActionTypes.DATABASE_DELETE_ITEM; 29 | 30 | constructor(public payload: { id: string; userId: string }) {} 31 | } 32 | 33 | export class DatabaseListenForAddedItems implements Action { 34 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_ADDED_ITEMS; 35 | 36 | constructor( 37 | public payload: { 38 | userId: string; 39 | }, 40 | ) {} 41 | } 42 | 43 | export class DatabaseListenForAddedItemsError implements Action { 44 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_ADDED_ITEMS_ERROR; 45 | 46 | constructor( 47 | public payload: { 48 | error: { 49 | code: string; 50 | message: string; 51 | name: string; 52 | }; 53 | }, 54 | ) {} 55 | } 56 | 57 | export class DatabaseListenForDataStart implements Action { 58 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_START; 59 | 60 | constructor( 61 | public payload: { 62 | userId: string; 63 | }, 64 | ) {} 65 | } 66 | 67 | export class DatabaseListenForDataStop implements Action { 68 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP; 69 | } 70 | 71 | export class DatabaseListenForModifiedItems implements Action { 72 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_MODIFIED_ITEMS; 73 | 74 | constructor( 75 | public payload: { 76 | userId: string; 77 | }, 78 | ) {} 79 | } 80 | 81 | export class DatabaseListenForModifiedItemsError implements Action { 82 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_MODIFIED_ITEMS_ERROR; 83 | 84 | constructor( 85 | public payload: { 86 | error: { 87 | code: string; 88 | message: string; 89 | name: string; 90 | }; 91 | }, 92 | ) {} 93 | } 94 | 95 | export class DatabaseListenForRemovedItems implements Action { 96 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_REMOVED_ITEMS; 97 | 98 | constructor( 99 | public payload: { 100 | userId: string; 101 | }, 102 | ) {} 103 | } 104 | 105 | export class DatabaseListenForRemovedItemsError implements Action { 106 | public readonly type = GizmoActionTypes.DATABASE_LISTEN_FOR_REMOVED_ITEMS_ERROR; 107 | 108 | constructor( 109 | public payload: { 110 | error: { 111 | code: string; 112 | message: string; 113 | name: string; 114 | }; 115 | }, 116 | ) {} 117 | } 118 | 119 | export class DatabaseUpsertItem implements Action { 120 | public readonly type = GizmoActionTypes.DATABASE_UPSERT_ITEM; 121 | 122 | constructor( 123 | public payload: { 124 | item: Gizmo; 125 | userId: string; 126 | }, 127 | ) {} 128 | } 129 | 130 | export class DatabaseUpsertItemError implements Action { 131 | public readonly type = GizmoActionTypes.DATABASE_UPSERT_ITEM_ERROR; 132 | 133 | constructor( 134 | public payload: { 135 | error: { 136 | code: string; 137 | message: string; 138 | name: string; 139 | }; 140 | }, 141 | ) {} 142 | } 143 | 144 | export class DatabaseUpsertItemSuccess implements Action { 145 | public readonly type = GizmoActionTypes.DATABASE_UPSERT_ITEM_SUCCESS; 146 | } 147 | 148 | export class StoreAddItems implements Action { 149 | public readonly type = GizmoActionTypes.STORE_ADD_ITEMS; 150 | 151 | constructor(public payload: { gizmos: Gizmo[] }) {} 152 | } 153 | 154 | export class StoreDeleteItems implements Action { 155 | public readonly type = GizmoActionTypes.STORE_DELETE_ITEMS; 156 | 157 | constructor(public payload: { ids: string[] }) {} 158 | } 159 | 160 | export class StoreUpdateItems implements Action { 161 | public readonly type = GizmoActionTypes.STORE_UPDATE_ITEMS; 162 | 163 | constructor( 164 | // public payload: { gizmos: Array<{ id: string; changes: Gizmo }> }, 165 | public payload: { items: Array> }, 166 | ) {} 167 | } 168 | 169 | export type GizmoActions = 170 | | DatabaseDeleteItem 171 | | DatabaseListenForAddedItems 172 | | DatabaseListenForAddedItemsError 173 | | DatabaseListenForDataStart 174 | | DatabaseListenForDataStop 175 | | DatabaseListenForModifiedItems 176 | | DatabaseListenForRemovedItems 177 | | DatabaseUpsertItem 178 | | StoreAddItems 179 | | StoreUpdateItems 180 | | StoreDeleteItems; 181 | -------------------------------------------------------------------------------- /src/gizmo/gizmo.data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFirestore } from 'angularfire2/firestore'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { Gizmo } from './gizmo.model'; 6 | 7 | const DATA_COLLECTION = 'gizmos'; 8 | const USERS_COLLECTION = 'users'; 9 | 10 | interface FirestoreDoc { 11 | id: string; 12 | description: string; 13 | name: string; 14 | sysDateCreatedOn?: string; 15 | sysDateUpdatedOn?: string; 16 | } 17 | 18 | @Injectable() 19 | export class GizmoDataService { 20 | constructor(private readonly afs: AngularFirestore) { 21 | // console.log('GizmoDataService:constructor'); 22 | } 23 | 24 | public ListenForAdded$(userId: string): Observable { 25 | // 26 | return this.firestoreCollection(userId) 27 | .stateChanges(['added']) 28 | .map((actions) => 29 | actions.map((a) => { 30 | const data = a.payload.doc.data() as FirestoreDoc; 31 | return this.fromFirestoreDoc(data); 32 | }), 33 | ); 34 | } 35 | 36 | public ListenForModified$(userId: string): Observable { 37 | // 38 | return this.firestoreCollection(userId) 39 | .stateChanges(['modified']) 40 | .map((actions) => 41 | actions.map((a) => { 42 | const data = a.payload.doc.data() as FirestoreDoc; 43 | return this.fromFirestoreDoc(data); 44 | }), 45 | ); 46 | } 47 | 48 | public ListenForRemoved$(userId: string): Observable { 49 | // 50 | return this.firestoreCollection(userId) 51 | .stateChanges(['removed']) 52 | .map((actions) => 53 | actions.map((a) => { 54 | const data = a.payload.doc.data() as FirestoreDoc; 55 | return this.fromFirestoreDoc(data); 56 | }), 57 | ); 58 | } 59 | 60 | public deleteItem(id: string, userId: string): void { 61 | this.firestoreCollection(userId) 62 | .doc(id) 63 | .delete(); 64 | } 65 | 66 | public upsertItem(item: Gizmo, userId: string): Promise { 67 | // 68 | if (item.id === '') { 69 | return this.createItem(item, userId); 70 | } else { 71 | return this.updateItem(item, userId); 72 | } 73 | } 74 | 75 | private createItem(item: Gizmo, userId: string): Promise { 76 | // 77 | const doc = this.toFirestoreDoc(item); 78 | const dateNow = Date().toString(); 79 | doc.id = this.afs.createId(); 80 | const recordToSet: FirestoreDoc = { 81 | ...doc, 82 | sysDateCreatedOn: dateNow, 83 | sysDateUpdatedOn: dateNow, 84 | }; 85 | 86 | return this.firestoreCollection(userId) 87 | .doc(recordToSet.id) 88 | .set(recordToSet); 89 | } 90 | 91 | private updateItem(item: Gizmo, userId: string): Promise { 92 | // 93 | const doc = this.toFirestoreDoc(item); 94 | const dateNow = Date().toString(); 95 | const recordToUpdate: FirestoreDoc = { 96 | ...doc, 97 | sysDateUpdatedOn: dateNow, 98 | }; 99 | 100 | return this.firestoreCollection(userId) 101 | .doc(doc.id) 102 | .update(recordToUpdate); 103 | } 104 | 105 | private firestoreCollection(userId: string) { 106 | // 107 | /**/ 108 | return this.afs 109 | .collection(USERS_COLLECTION) 110 | .doc(userId) 111 | .collection(DATA_COLLECTION); 112 | /**/ 113 | // return this.afs.collection(DATA_COLLECTION); 114 | } 115 | 116 | private toFirestoreDoc(item: Gizmo): FirestoreDoc { 117 | // 118 | const result: FirestoreDoc = { 119 | description: item.description, 120 | id: item.id, 121 | name: item.name, 122 | }; 123 | 124 | // console.log('toFirebaseTodo>', result); 125 | return result; 126 | } 127 | 128 | private fromFirestoreDoc(x: FirestoreDoc): Gizmo { 129 | // 130 | // console.log('TodoDataService:fromFirebaseTodo>', x); 131 | 132 | // This copies extra fields. 133 | // const result: Gizmo = { ...x }; 134 | 135 | const result: Gizmo = { 136 | description: x.description, 137 | id: x.id, 138 | name: x.name, 139 | }; 140 | 141 | // console.log('fromFirestoreDoc:result>', result); 142 | 143 | return result; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/gizmo/gizmo.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect, ofType } from '@ngrx/effects'; 4 | import { Store } from '@ngrx/store'; 5 | import { empty } from 'rxjs/observable/empty'; 6 | 7 | import * as FromRootReducer from '../reducers'; 8 | import { 9 | DatabaseDeleteItem, 10 | DatabaseListenForAddedItems, 11 | DatabaseListenForAddedItemsError, 12 | DatabaseListenForDataStart, 13 | DatabaseListenForDataStop, 14 | DatabaseListenForModifiedItems, 15 | DatabaseListenForModifiedItemsError, 16 | DatabaseListenForRemovedItems, 17 | DatabaseListenForRemovedItemsError, 18 | DatabaseUpsertItem, 19 | DatabaseUpsertItemError, 20 | DatabaseUpsertItemSuccess, 21 | GizmoActionTypes, 22 | StoreAddItems, 23 | StoreDeleteItems, 24 | StoreUpdateItems, 25 | } from './gizmo.actions'; 26 | import { GizmoDataService } from './gizmo.data.service'; 27 | import { Gizmo } from './gizmo.model'; 28 | 29 | import { fromPromise } from 'rxjs/observable/fromPromise'; 30 | import { of } from 'rxjs/observable/of'; 31 | import { 32 | catchError, 33 | map, 34 | switchMap, 35 | tap, 36 | } from 'rxjs/operators'; 37 | 38 | @Injectable() 39 | export class GizmoEffects { 40 | constructor( 41 | private actions$: Actions, 42 | private store$: Store, 43 | private dataService: GizmoDataService, 44 | ) {} 45 | 46 | // tslint:disable-next-line:member-ordering 47 | @Effect({ dispatch: false }) 48 | public deleteItem$ = this.actions$.pipe( 49 | ofType(GizmoActionTypes.DATABASE_DELETE_ITEM), 50 | map((action) => action.payload), 51 | tap((payload) => { 52 | console.log('Effect:deleteItem$:A', payload); 53 | this.dataService.deleteItem(payload.id, payload.userId); 54 | }), 55 | ); 56 | 57 | // tslint:disable-next-line:member-ordering 58 | @Effect({ dispatch: false }) 59 | public listenForAddedItems$ = this.actions$.pipe( 60 | ofType( 61 | GizmoActionTypes.DATABASE_LISTEN_FOR_ADDED_ITEMS, 62 | GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP, 63 | ), 64 | switchMap((action) => { 65 | switch (action.type) { 66 | case GizmoActionTypes.DATABASE_LISTEN_FOR_ADDED_ITEMS: { 67 | return this.dataService.ListenForAdded$(action.payload.userId).pipe( 68 | map((items: Gizmo[]) => { 69 | this.store$.dispatch(new StoreAddItems({ gizmos: items })); 70 | }), 71 | catchError((error) => { 72 | this.store$.dispatch( 73 | new DatabaseListenForAddedItemsError({ 74 | error: this.handleFirebaseError(error), 75 | }), 76 | ); 77 | // Pass on to higher level. 78 | // throw error; 79 | return empty(); 80 | }), 81 | ); 82 | } 83 | 84 | case GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP: { 85 | console.log('listenForAddedItems.DATABASE_LISTEN_FOR_DATA_STOP'); 86 | return empty(); 87 | } 88 | 89 | default: { 90 | return empty(); 91 | } 92 | } 93 | }), 94 | ); 95 | 96 | // tslint:disable-next-line:member-ordering 97 | @Effect() 98 | public listenForData$ = this.actions$.pipe( 99 | ofType(GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_START), 100 | tap(() => { 101 | console.log('Effect:listenForData$:A'); 102 | }), 103 | switchMap((action: DatabaseListenForDataStart) => { 104 | console.log('Effect:listenForData$:action>', action); 105 | return [ 106 | new DatabaseListenForAddedItems({ userId: action.payload.userId }), 107 | new DatabaseListenForModifiedItems({ userId: action.payload.userId }), 108 | new DatabaseListenForRemovedItems({ userId: action.payload.userId }), 109 | ]; 110 | }), 111 | ); 112 | 113 | // tslint:disable-next-line:member-ordering 114 | @Effect({ dispatch: false }) 115 | public listenForModifiedItems$ = this.actions$.pipe( 116 | ofType( 117 | GizmoActionTypes.DATABASE_LISTEN_FOR_MODIFIED_ITEMS, 118 | GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP, 119 | ), 120 | tap(() => { 121 | console.log('Effect:listenForModifiedItems$:A'); 122 | }), 123 | switchMap((action) => { 124 | switch (action.type) { 125 | case GizmoActionTypes.DATABASE_LISTEN_FOR_MODIFIED_ITEMS: { 126 | // return this.dataService.ListenForModified$(action.payload.userId); 127 | return this.dataService 128 | .ListenForModified$(action.payload.userId) 129 | .pipe( 130 | map((items: Gizmo[]) => { 131 | return items.map((item) => { 132 | return { 133 | changes: item, 134 | id: item.id, 135 | }; 136 | }); 137 | }), 138 | map((qq) => { 139 | this.store$.dispatch(new StoreUpdateItems({ items: qq })); 140 | }), 141 | catchError((error) => { 142 | this.store$.dispatch( 143 | new DatabaseListenForModifiedItemsError({ 144 | error: this.handleFirebaseError(error), 145 | }), 146 | ); 147 | // Pass on to higher level. 148 | // throw error; 149 | return empty(); 150 | }), 151 | ); 152 | } 153 | 154 | case GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP: { 155 | console.log('listenForModifiedItems.DATABASE_LISTEN_FOR_DATA_STOP'); 156 | return empty(); 157 | } 158 | 159 | default: { 160 | return empty(); 161 | } 162 | } 163 | }), 164 | ); 165 | 166 | // tslint:disable-next-line:member-ordering 167 | @Effect({ dispatch: false }) 168 | public listenForRemovedItems$ = this.actions$.pipe( 169 | ofType( 170 | GizmoActionTypes.DATABASE_LISTEN_FOR_REMOVED_ITEMS, 171 | GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP, 172 | ), 173 | tap(() => { 174 | console.log('Effect:listenForRemovedItems$:A'); 175 | }), 176 | switchMap((action) => { 177 | switch (action.type) { 178 | case GizmoActionTypes.DATABASE_LISTEN_FOR_REMOVED_ITEMS: { 179 | // return this.dataService.ListenForRemoved$(action.payload.userId); 180 | return this.dataService.ListenForRemoved$(action.payload.userId).pipe( 181 | map((items: Gizmo[]) => items.map((a) => a.id)), 182 | map((ids) => { 183 | this.store$.dispatch(new StoreDeleteItems({ ids })); 184 | }), 185 | catchError((error) => { 186 | this.store$.dispatch( 187 | new DatabaseListenForRemovedItemsError({ 188 | error: this.handleFirebaseError(error), 189 | }), 190 | ); 191 | // Pass on to higher level. 192 | // throw error; 193 | return empty(); 194 | }), 195 | ); 196 | } 197 | 198 | case GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP: { 199 | console.log('listenForRemovedItems.DATABASE_LISTEN_FOR_DATA_STOP'); 200 | return empty(); 201 | } 202 | 203 | default: { 204 | return empty(); 205 | } 206 | } 207 | }), 208 | tap((x) => { 209 | console.log('Effect:listenForRemovedItems$:B', x); 210 | }), 211 | // map((items: Gizmo[]) => items.map((a) => a.id)), 212 | // map((ids) => new StoreDeleteItems({ ids })), 213 | ); 214 | 215 | // tslint:disable-next-line:member-ordering 216 | @Effect() 217 | public databaseUpsertItem$ = this.actions$.pipe( 218 | ofType(GizmoActionTypes.DATABASE_UPSERT_ITEM), 219 | map((action: DatabaseUpsertItem) => action.payload), 220 | switchMap((payload) => { 221 | return fromPromise( 222 | this.dataService.upsertItem(payload.item, payload.userId), 223 | ).pipe( 224 | map(() => new DatabaseUpsertItemSuccess()), 225 | catchError((error) => 226 | of( 227 | new DatabaseUpsertItemError({ 228 | error: this.handleFirebaseError(error), 229 | }), 230 | ), 231 | ), 232 | ); 233 | }), 234 | ); 235 | 236 | private handleFirebaseError(firebaseError: any) { 237 | // 238 | return { 239 | code: firebaseError.code, 240 | message: firebaseError.message, 241 | name: firebaseError.name, 242 | }; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/gizmo/gizmo.model.ts: -------------------------------------------------------------------------------- 1 | // https://redux.js.org/docs/faq/OrganizingState.html#organizing-state-non-serializable 2 | /* 3 | Can I put functions, promises, or other non-serializable items in my store 4 | state? It is highly recommended that you only put plain serializable objects, 5 | arrays, and primitives into your store. It's technically possible to insert 6 | non-serializable items into the store, but doing so can break the ability to 7 | persist and rehydrate the contents of a store, as well as interfere with 8 | time-travel debugging. 9 | 10 | If you are okay with things like persistence and time-travel debugging 11 | potentially not working as intended, then you are totally welcome to put 12 | non-serializable items into your Redux store. Ultimately, it's your application, 13 | and how you implement it is up to you. As with many other things about Redux, 14 | just be sure you understand what tradeoffs are involved. 15 | */ 16 | // https://stackoverflow.com/questions/43181516/getting-model-instance-from-ngrx-store-select/43185931#43185931 17 | /* 18 | But: It is generally not recommended to have Class-Instances in the store, there 19 | are a few rules of thumb: 20 | 21 | The store-content should serializable without any major modifications (=> just 22 | use Object and Primitives) ngrx (and rxjs in general) are relying heavily on 23 | functional programming patterns, so mixing it Object Oriented paradigms is not 24 | recommended. 25 | */ 26 | export interface Gizmo { 27 | readonly description: string; 28 | readonly id: string; 29 | readonly name: string; 30 | } 31 | 32 | export function newGizmo(): Gizmo { 33 | return { description: '', id: '', name: '' }; 34 | } 35 | -------------------------------------------------------------------------------- /src/gizmo/gizmo.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 2 | 3 | import { GizmoActions, GizmoActionTypes } from './gizmo.actions'; 4 | import { Gizmo } from './gizmo.model'; 5 | 6 | export interface State extends EntityState { 7 | // additional entities state properties 8 | loaded: boolean; 9 | loading: boolean; 10 | selectedGizmoId: string; 11 | } 12 | 13 | export function sortByName(a: Gizmo, b: Gizmo): number { 14 | return a.name.localeCompare(b.name); 15 | } 16 | 17 | export const adapter: EntityAdapter = createEntityAdapter({ 18 | sortComparer: sortByName, 19 | }); 20 | 21 | export const initialState: State = adapter.getInitialState({ 22 | // additional entity state properties 23 | loaded: false, 24 | loading: false, 25 | selectedGizmoId: '', 26 | }); 27 | 28 | export function reducer(state = initialState, action: GizmoActions): State { 29 | switch (action.type) { 30 | case GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_START: { 31 | return { 32 | ...state, 33 | loading: true, 34 | }; 35 | } 36 | 37 | case GizmoActionTypes.DATABASE_LISTEN_FOR_DATA_STOP: { 38 | return adapter.removeAll({ 39 | ...state, 40 | loaded: false, 41 | loading: false, 42 | selectedGizmoId: '', 43 | }); 44 | } 45 | 46 | /* 47 | case GizmoActionTypes.ADD_GIZMO: { 48 | return adapter.addOne(action.payload.gizmo, state); 49 | } 50 | 51 | case GizmoActionTypes.ADD_GIZMOS: { 52 | return adapter.addMany(action.payload.gizmos, state); 53 | } 54 | 55 | case GizmoActionTypes.UPDATE_GIZMO: { 56 | return adapter.updateOne(action.payload.gizmo, state); 57 | } 58 | */ 59 | case GizmoActionTypes.STORE_ADD_ITEMS: { 60 | return { 61 | ...adapter.addMany(action.payload.gizmos, state), 62 | loaded: true, 63 | loading: false, 64 | }; 65 | } 66 | 67 | case GizmoActionTypes.STORE_DELETE_ITEMS: { 68 | return adapter.removeMany(action.payload.ids, state); 69 | } 70 | 71 | case GizmoActionTypes.STORE_UPDATE_ITEMS: { 72 | return adapter.updateMany(action.payload.items, state); 73 | } 74 | /* 75 | case GizmoActionTypes.DELETE_GIZMO: { 76 | return adapter.removeOne(action.payload.id, state); 77 | } 78 | 79 | case GizmoActionTypes.LOAD_GIZMOS: { 80 | return adapter.addAll(action.payload.gizmos, state); 81 | } 82 | 83 | case GizmoActionTypes.CLEAR_GIZMOS: { 84 | return adapter.removeAll({ ...state, selectedGizmoId: '' }); 85 | } 86 | */ 87 | default: { 88 | return state; 89 | } 90 | } 91 | } 92 | 93 | export const getSelectedGizmoId = (state: State) => state.selectedGizmoId; 94 | 95 | export const { 96 | // select the array of gizmo ids 97 | selectIds: selectGizmoIds, 98 | 99 | // select the dictionary of gizmo entities 100 | selectEntities: selectGizmoEntities, 101 | 102 | // select the array of gizmos 103 | selectAll: selectAllGizmos, 104 | 105 | // select the total gizmo count 106 | selectTotal: selectGizmoTotal, 107 | } = adapter.getSelectors(); 108 | 109 | export const getLoaded = (state: State) => state.loaded; 110 | export const getLoading = (state: State) => state.loading; 111 | -------------------------------------------------------------------------------- /src/gizmo/gizmo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { select, Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { filter, take } from 'rxjs/operators'; 6 | 7 | import * as FromAuthSelector from '../app/auth/auth.selector'; 8 | import * as FromRootReducer from '../reducers'; 9 | import { 10 | DatabaseDeleteItem, 11 | DatabaseListenForDataStart, 12 | DatabaseListenForDataStop, 13 | DatabaseUpsertItem, 14 | } from './gizmo.actions'; 15 | import { Gizmo } from './gizmo.model'; 16 | 17 | @Injectable() 18 | export class GizmoService { 19 | // 20 | private init$ = this.store.pipe( 21 | select(FromAuthSelector.getUserId), 22 | filter((userId) => userId !== ''), 23 | ); 24 | 25 | constructor(private store: Store) {} 26 | 27 | public getData$(): Observable> { 28 | // 29 | return this.store.pipe(select(FromRootReducer.selectAllGizmos)); 30 | } 31 | 32 | public ListenForDataStart(): void { 33 | // 34 | this.init$.pipe(take(1)).subscribe((userId) => { 35 | this.store.dispatch(new DatabaseListenForDataStart({ userId })); 36 | }); 37 | } 38 | 39 | public ListenForDataStop(): void { 40 | // 41 | this.store.dispatch(new DatabaseListenForDataStop()); 42 | } 43 | 44 | public deleteItem(item: Gizmo) { 45 | // 46 | this.init$.pipe(take(1)).subscribe((userId) => { 47 | this.store.dispatch(new DatabaseDeleteItem({ id: item.id, userId })); 48 | }); 49 | } 50 | 51 | /* 52 | Best practice is to provide the user as part of the payload as mentioned 53 | instead of selecting it from the state in the effect. This keeps the Effect 54 | pure and easier to test. You can also write a selector that composes the two 55 | pieces of data together for your action. 56 | https://github.com/ngrx/platform/issues/496#issuecomment-337781385 57 | */ 58 | public upsert(item: Gizmo) { 59 | // 60 | this.init$.pipe(take(1)).subscribe((userId) => { 61 | this.store.dispatch(new DatabaseUpsertItem({ item, userId })); 62 | }); 63 | } 64 | 65 | public isLoaded(): Observable { 66 | // 67 | return this.store.pipe(select(FromRootReducer.getGizmoLoaded)); 68 | } 69 | 70 | public isLoading(): Observable { 71 | // 72 | return this.store.pipe(select(FromRootReducer.getGizmoLoading)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/gizmo/modals/gizmo-detail/gizmo-detail.modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | Gizmo 11 | 12 | 18 | 19 | 20 | 21 | 22 |
24 | 25 | 26 | Name 28 | 30 | 31 | 32 | 33 | 34 | Description 36 | 38 | 39 | 40 | 41 |
42 |
-------------------------------------------------------------------------------- /src/gizmo/modals/gizmo-detail/gizmo-detail.modal.scss: -------------------------------------------------------------------------------- 1 | tja-modal-gizmo-detail { 2 | } 3 | -------------------------------------------------------------------------------- /src/gizmo/modals/gizmo-detail/gizmo-detail.modal.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { NavParams, ViewController } from 'ionic-angular'; 5 | 6 | import { Gizmo, newGizmo } from '../../gizmo.model'; 7 | 8 | interface ModalInput { 9 | item?: Gizmo; 10 | } 11 | 12 | export function getModalInput(item: Gizmo | undefined) { 13 | // 14 | const modalInput: ModalInput = { item }; 15 | 16 | return { 17 | modalInput, 18 | }; 19 | } 20 | 21 | export interface ModalResult { 22 | save: boolean; 23 | item?: Gizmo; 24 | } 25 | 26 | interface FormModel { 27 | description: any; 28 | name: any; 29 | } 30 | 31 | @Component({ 32 | selector: 'tja-modal-gizmo-detail', 33 | templateUrl: 'gizmo-detail.modal.html', 34 | }) 35 | export class GizmoDetailModal { 36 | // Called from view. 37 | public viewForm: FormGroup; 38 | 39 | public get viewCanSave(): boolean { 40 | return !(this.viewForm.dirty && this.viewForm.valid); 41 | } 42 | 43 | private readonly CLASS_NAME = 'GizmoDetailModal'; 44 | 45 | private dataModel: Gizmo; 46 | 47 | constructor( 48 | public formBuilder: FormBuilder, 49 | public navParams: NavParams, 50 | public viewController: ViewController, 51 | ) { 52 | console.log(`%s:constructor`, this.CLASS_NAME); 53 | 54 | const modalInput: ModalInput = this.getModalInput(); 55 | 56 | if (modalInput.item === undefined) { 57 | // new item. 58 | this.dataModel = newGizmo(); 59 | } else { 60 | // navParams passes by reference. 61 | this.dataModel = { ...modalInput.item }; 62 | } 63 | } 64 | 65 | public ngOnInit() { 66 | console.log('###%s:ngOnInit>', this.CLASS_NAME); 67 | // console.log('this.todo.isNew()>', this.todo.isNew()); 68 | /* 69 | if (this.todo.$key === undefined) { 70 | this.isEditing = false; 71 | } 72 | */ 73 | this.viewForm = this.formBuilder.group({ 74 | description: [this.dataModel.description], 75 | name: [this.dataModel.name, Validators.required], 76 | }); 77 | } 78 | 79 | public viewCancel() { 80 | console.log('viewCancel>'); 81 | const result: ModalResult = { save: false }; 82 | 83 | this.viewController.dismiss(result); 84 | } 85 | 86 | public viewSave() { 87 | console.log('viewSave>'); 88 | 89 | if (!this.viewForm.valid) { 90 | return; 91 | } 92 | 93 | const saveItem = this.prepareSaveItem(); 94 | const result: ModalResult = { save: true, item: saveItem }; 95 | this.viewController.dismiss(result); 96 | } 97 | 98 | private getModalInput(): ModalInput { 99 | // 100 | return this.navParams.get('modalInput'); 101 | } 102 | 103 | private prepareSaveItem(): Gizmo { 104 | const formModel: FormModel = this.viewForm.value; 105 | 106 | const saveItem: Gizmo = { 107 | ...this.dataModel, 108 | description: formModel.description, 109 | name: formModel.name, 110 | }; 111 | 112 | return saveItem; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/gizmo/pages/gizmo-list/gizmo-list.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Gizmos(Cloud Firestore) 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | IsLoading: {{viewIsLoading$ | async}} 22 |
23 |
24 | IsLoaded: {{viewIsLoaded$ | async}} 25 |
26 | 27 | 28 | 29 |
{{item.name}}
30 |

{{item.description}}

31 | 33 | 36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /src/gizmo/pages/gizmo-list/gizmo-list.page.scss: -------------------------------------------------------------------------------- 1 | tja-page-gizmo-list { 2 | } 3 | -------------------------------------------------------------------------------- /src/gizmo/pages/gizmo-list/gizmo-list.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { ModalController } from 'ionic-angular'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { Gizmo } from '../../gizmo.model'; 7 | import { GizmoService } from '../../gizmo.service'; 8 | import { 9 | getModalInput, 10 | GizmoDetailModal, 11 | ModalResult, 12 | } from '../../modals/gizmo-detail/gizmo-detail.modal'; 13 | 14 | // import { Store } from '@ngrx/store'; 15 | 16 | // import * as fromRoot from '../../reducers'; 17 | @Component({ 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | selector: 'tja-page-gizmo-list', 20 | templateUrl: 'gizmo-list.page.html', 21 | }) 22 | export class GizmoListPage { 23 | public viewData$: Observable>; 24 | public viewIsLoaded$: Observable; 25 | public viewIsLoading$: Observable; 26 | constructor( 27 | public modalCtrl: ModalController, 28 | private readonly gizmoService: GizmoService, 29 | ) { 30 | this.viewData$ = gizmoService.getData$(); 31 | this.viewIsLoaded$ = gizmoService.isLoaded(); 32 | this.viewIsLoading$ = gizmoService.isLoading(); 33 | } 34 | 35 | public ionViewDidLoad() { 36 | // 37 | this.gizmoService.ListenForDataStart(); 38 | } 39 | 40 | public ionViewWillUnload() { 41 | // 42 | this.gizmoService.ListenForDataStop(); 43 | } 44 | 45 | public viewAdd(): void { 46 | console.log('viewAdd'); 47 | this.showModal(); 48 | } 49 | 50 | public viewDelete(item: Gizmo): void { 51 | console.log('viewDelete>', item); 52 | this.gizmoService.deleteItem(item); 53 | } 54 | 55 | public viewEdit(item: any): void { 56 | console.log('viewEdit>', item); 57 | this.showModal(item); 58 | } 59 | 60 | private showModal(item?: Gizmo) { 61 | // 62 | const modalInput = getModalInput(item); 63 | 64 | const modal = this.modalCtrl.create(GizmoDetailModal, modalInput); 65 | 66 | modal.onDidDismiss((data: ModalResult | null) => { 67 | console.log('onDidDismiss>', data); 68 | 69 | if (data === null) { 70 | console.log('onDidDismiss:NULL'); 71 | return; 72 | } 73 | 74 | if (data.save && data.item) { 75 | this.gizmoService.upsert(data.item); 76 | } 77 | }); 78 | 79 | modal.present(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Ionic App 8 | 10 | 12 | 14 | 15 | 18 | 20 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic", 3 | "short_name": "Ionic", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { TextItem } from './textItem'; 2 | -------------------------------------------------------------------------------- /src/models/textItem.ts: -------------------------------------------------------------------------------- 1 | export interface TextItem { 2 | $key: string; 3 | description: string; 4 | timestamp: number; 5 | title: string; 6 | user: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Home Page 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/pages/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | import { Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { Subscription } from 'rxjs/Subscription'; 6 | 7 | import * as textItemActions from '../../actions/text-item.action'; 8 | import { 9 | IsFetchingInput, 10 | PostsInput, 11 | } from '../../components/example-list/example-list.component'; 12 | import { TextItemEffects } from '../../effects/text-item.effect'; 13 | import * as fromRoot from '../../reducers'; 14 | 15 | @Component({ 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | providers: [TextItemEffects], 18 | templateUrl: 'home.page.html', 19 | }) 20 | export class HomePage implements OnInit { 21 | public posts$: Observable; 22 | public isFetching$: Observable; 23 | 24 | public effectsSubscription: Subscription; 25 | 26 | constructor( 27 | // private textItemEffects: TextItemEffects, 28 | private store: Store, 29 | ) { 30 | // 31 | console.log('HomePage:constructor'); 32 | this.isFetching$ = store.select(fromRoot.getCollectionLoading); 33 | this.posts$ = store.select(fromRoot.getCollectionTextItems); 34 | 35 | // this.effectsSubscription = textItemEffects.loadCollection$.subscribe(store); 36 | } 37 | 38 | public ngOnInit() { 39 | this.store.dispatch(new textItemActions.LoadCollectionAction()); 40 | // this.isFetching$ = Observable.of(false); 41 | // this.posts$ = this.af.database.list('/textItems') 42 | // .do(v => {console.log('posts>', v)}); 43 | } 44 | 45 | public doSearch(ev: any) { 46 | // 47 | // set val to the value of the ev target 48 | console.log('doSearch'); 49 | const val = ev.target.value; 50 | 51 | // if the value is an empty string don't filter the items 52 | if (val && val.trim() !== '') { 53 | // this.items = this.items.filter((item) => { 54 | // return (item.toLowerCase().indexOf(val.toLowerCase()) > -1); 55 | // }) 56 | } 57 | } 58 | 59 | public ionViewDidLoad() { 60 | console.log('ionViewDidLoad'); 61 | // this.effectsSubscription.unsubscribe(); 62 | } 63 | 64 | public ionViewDidUnload() { 65 | console.log('ionViewDidUnload'); 66 | // this.effectsSubscription.unsubscribe(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/pages/login/login.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | Login 9 | 10 | 11 | 12 | 13 | 14 |
16 | 17 | Email 19 | 24 | 25 | 26 | 27 |

This field is invalid!

28 |

This field is required!

29 |

This field is required! AAAA

30 | 31 | 36 | 37 | Password 39 | 42 | 43 | 44 | 45 |

This field is required!

46 | 51 | 52 | 53 | 56 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 80 | 81 | 82 |
83 |
84 | 85 |
Login State
86 |

87 | {{loginState$ | async | json}} 88 |

89 | 90 | 91 | 92 |
-------------------------------------------------------------------------------- /src/pages/login/login.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { Store } from '@ngrx/store'; 5 | import { NavController } from 'ionic-angular'; 6 | 7 | import * as authActions from '../../app/auth/auth.action'; 8 | import * as FromAuthSelector from '../../app/auth/auth.selector'; 9 | import * as fromRoot from '../../reducers'; 10 | import { SignupPage } from '../signup/signup.page'; 11 | 12 | @Component({ 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | templateUrl: 'login.page.html', 15 | }) 16 | export class LoginPage { 17 | // login: { username?: string, password?: string } = {}; 18 | public submitted = false; 19 | public loginForm: FormGroup; 20 | 21 | public loginState$: any; 22 | 23 | constructor( 24 | private formBuilder: FormBuilder, 25 | private nav: NavController, 26 | private store: Store, 27 | ) { 28 | // 29 | this.loginState$ = this.store.select(FromAuthSelector.getAuthState); 30 | 31 | this.loginForm = this.formBuilder.group({ 32 | password: ['', Validators.required], 33 | username: ['', Validators.required], 34 | }); 35 | } 36 | 37 | /* 38 | ionViewDidLoad() { 39 | // 40 | this.loginForm = this.formBuilder.group({ 41 | username: ['', Validators.required], 42 | password: ['', Validators.required], 43 | }); 44 | } 45 | */ 46 | 47 | public logForm() { 48 | console.log(this.loginForm.value); 49 | console.log('loginForm>', this.loginForm); 50 | 51 | this.submitted = true; 52 | 53 | if (this.loginForm.valid) { 54 | this.store.dispatch( 55 | new authActions.EmailAuthentication({ 56 | password: this.loginForm.value.password, 57 | userName: this.loginForm.value.username, 58 | }), 59 | ); 60 | } 61 | } 62 | 63 | public onLogin() { 64 | this.submitted = true; 65 | 66 | if (this.loginForm.valid) { 67 | this.store.dispatch( 68 | new authActions.EmailAuthentication({ 69 | password: this.loginForm.value.password, 70 | userName: this.loginForm.value.username, 71 | }), 72 | ); 73 | } 74 | } 75 | 76 | public onSignup() { 77 | this.nav.push(SignupPage); 78 | } 79 | 80 | public viewChangePasswordClick() { 81 | console.log('viewChangePasswordClick'); 82 | if (this.loginForm.valid) { 83 | this.store.dispatch( 84 | new authActions.UpdatePassword({ 85 | password: this.loginForm.value.password, 86 | }), 87 | ); 88 | } 89 | } 90 | 91 | public viewResetPasswordClick() { 92 | console.log('viewResetPasswordClick'); 93 | if (this.loginForm.valid) { 94 | this.store.dispatch( 95 | new authActions.SendPasswordResetEmail({ 96 | email: this.loginForm.value.username, 97 | }), 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/pages/login/login.scss: -------------------------------------------------------------------------------- 1 | .login-page .logo { 2 | padding: 20px 0; 3 | min-height: 200px; 4 | text-align: center; 5 | } 6 | 7 | .login-page .logo img { 8 | max-width: 150px; 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/page1/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Page One 7 | 8 | 9 | 10 | 11 |

Ionic Menu Starter

12 | 13 |

14 | If you get lost, the docs will show you the way. 15 |

16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /src/pages/page1/page1.scss: -------------------------------------------------------------------------------- 1 | page1-page { 2 | } 3 | -------------------------------------------------------------------------------- /src/pages/page1/page1.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { NavController } from 'ionic-angular'; 4 | 5 | @Component({ 6 | selector: 'page-page1', 7 | templateUrl: 'page1.html', 8 | }) 9 | export class Page1 { 10 | constructor(public navCtrl: NavController) { 11 | // 12 | console.log('Page1:constructor'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/page2/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Page Two 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 |
21 | You navigated here from {{selectedItem.title}} 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/pages/page2/page2.scss: -------------------------------------------------------------------------------- 1 | .page2-page { 2 | } 3 | -------------------------------------------------------------------------------- /src/pages/page2/page2.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { NavController, NavParams } from 'ionic-angular'; 4 | 5 | export interface ItemInterface { 6 | title: string; 7 | note: string; 8 | icon: string; 9 | } 10 | 11 | @Component({ 12 | templateUrl: 'page2.html', 13 | }) 14 | export class Page2 { 15 | public selectedItem: any; 16 | public icons: string[]; 17 | public items: Array<{ title: string; note: string; icon: string }>; 18 | 19 | constructor(public navCtrl: NavController, public navParams: NavParams) { 20 | // If we navigated to this page, we will have an item available as a nav param 21 | this.selectedItem = navParams.get('item'); 22 | 23 | // Let's populate this page with some filler content for funzies 24 | this.icons = [ 25 | 'flask', 26 | 'wifi', 27 | 'beer', 28 | 'football', 29 | 'basketball', 30 | 'paper-plane', 31 | 'american-football', 32 | 'boat', 33 | 'bluetooth', 34 | 'build', 35 | ]; 36 | 37 | this.items = []; 38 | for (let i = 1; i < 11; i++) { 39 | this.items.push({ 40 | icon: this.icons[Math.floor(Math.random() * this.icons.length)], 41 | note: 'This is item #' + i, 42 | title: 'Item ' + i, 43 | }); 44 | } 45 | } 46 | 47 | public itemTapped(item: ItemInterface) { 48 | // That's right, we're pushing to ourselves! 49 | this.navCtrl.push(Page2, { 50 | item, 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/realtime-database/realtime-database.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Realtime Database 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
{{item.title}}
20 |

{{item.description}}

21 | 23 | 26 |
27 |
28 |
29 |
-------------------------------------------------------------------------------- /src/pages/realtime-database/realtime-database.page.scss: -------------------------------------------------------------------------------- 1 | realtime-database1-page { 2 | } 3 | -------------------------------------------------------------------------------- /src/pages/realtime-database/realtime-database.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | import { Store } from '@ngrx/store'; 4 | 5 | import * as fromRoot from '../../reducers'; 6 | 7 | @Component({ 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | selector: 'page-realtime-database', 10 | templateUrl: 'realtime-database.page.html', 11 | }) 12 | export class RealtimeDatabasePage implements OnInit { 13 | public textItem$: any; 14 | public isFetching$: any; 15 | 16 | constructor( 17 | // private textItemEffects: TextItemEffects, 18 | private store: Store, 19 | ) { 20 | this.isFetching$ = this.store.select(fromRoot.getCollectionLoading); 21 | this.textItem$ = this.store.select(fromRoot.getCollectionTextItems); 22 | 23 | // this.effectsSubscription = textItemEffects.loadCollection$.subscribe(store); 24 | } 25 | 26 | public ngOnInit() { 27 | // 28 | } 29 | 30 | public viewAdd(): void { 31 | console.log('viewAdd'); 32 | } 33 | 34 | public viewDelete(item: any): void { 35 | console.log('viewDelete>', item); 36 | } 37 | 38 | public viewEdit(item: any): void { 39 | console.log('viewEdit>', item); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/signup/signup.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Signup 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/signup/signup.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { Store } from '@ngrx/store'; 5 | 6 | import * as authActions from '../../app/auth/auth.action'; 7 | import * as FromAuthSelector from '../../app/auth/auth.selector'; 8 | import * as fromRoot from '../../reducers'; 9 | 10 | @Component({ 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | templateUrl: 'signup.page.html', 13 | }) 14 | export class SignupPage { 15 | // signup: { username?: string, password?: string } = {}; 16 | public submitted = false; 17 | public loginForm: FormGroup; 18 | 19 | public loginState$: any; 20 | 21 | constructor( 22 | private formBuilder: FormBuilder, 23 | private store: Store, 24 | ) { 25 | // 26 | this.loginState$ = this.store.select(FromAuthSelector.getAuthState); 27 | 28 | this.loginForm = this.formBuilder.group({ 29 | password: ['', Validators.required], 30 | username: ['', Validators.required], 31 | }); 32 | } 33 | 34 | /* 35 | ionViewDidLoad() { 36 | // 37 | this.loginForm = this.formBuilder.group({ 38 | username: ['', Validators.required], 39 | password: ['', Validators.required], 40 | }); 41 | } 42 | */ 43 | 44 | public logForm() { 45 | console.log(this.loginForm.value); 46 | console.log('loginForm>', this.loginForm); 47 | 48 | this.submitted = true; 49 | 50 | if (this.loginForm.valid) { 51 | this.store.dispatch( 52 | new authActions.CreateUserWithEmailAndPassword({ 53 | email: this.loginForm.value.username, 54 | password: this.loginForm.value.password, 55 | }), 56 | ); 57 | } 58 | } 59 | /* 60 | onSignup(form) { 61 | this.submitted = true; 62 | 63 | if (form.valid) { 64 | this.store.dispatch( 65 | this.authActions.createUser( 66 | this.signup.username, 67 | this.signup.password)); 68 | } 69 | } 70 | */ 71 | } 72 | -------------------------------------------------------------------------------- /src/reducers/collection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TextItemActions, 3 | TextItemActionTypes, 4 | } from '../actions/text-item.action'; 5 | import { TextItem } from '../models'; 6 | 7 | export interface State { 8 | loaded: boolean; 9 | loading: boolean; 10 | textItems: TextItem[]; 11 | } 12 | 13 | const initialState: State = { 14 | loaded: false, 15 | loading: false, 16 | textItems: [], 17 | }; 18 | 19 | export function reducer(state = initialState, action: TextItemActions): State { 20 | switch (action.type) { 21 | case TextItemActionTypes.LoadCollection: { 22 | return { 23 | ...state, 24 | loading: true, 25 | }; 26 | } 27 | 28 | case TextItemActionTypes.LoadCollectionSuccess: { 29 | const books: TextItem[] = action.payload; 30 | 31 | return { 32 | loaded: true, 33 | loading: false, 34 | textItems: books.map((book) => book), 35 | }; 36 | } 37 | 38 | default: { 39 | return state; 40 | } 41 | } 42 | } 43 | 44 | export const getLoaded = (state: State) => state.loaded; 45 | export const getLoading = (state: State) => state.loading; 46 | export const getTextItems = (state: State) => state.textItems; 47 | -------------------------------------------------------------------------------- /src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap, createSelector, MetaReducer } from '@ngrx/store'; 2 | import { storeFreeze } from 'ngrx-store-freeze'; 3 | 4 | import * as fromAuth from '../app/auth/auth.reducer'; 5 | import * as fromGadget from '../gadget/gadget.reducer'; 6 | import * as fromGizmo from '../gizmo/gizmo.reducer'; 7 | import * as fromWidget from '../widget/widget.reducer'; 8 | import * as fromCollection from './collection'; 9 | 10 | export interface State { 11 | collection: fromCollection.State; 12 | auth: fromAuth.AuthState; 13 | gadget: fromGadget.State; 14 | gizmo: fromGizmo.State; 15 | widget: fromWidget.State; 16 | } 17 | 18 | export const reducers: ActionReducerMap = { 19 | auth: fromAuth.reducer, 20 | collection: fromCollection.reducer, 21 | gadget: fromGadget.reducer, 22 | gizmo: fromGizmo.reducer, 23 | widget: fromWidget.reducer, 24 | }; 25 | 26 | export const metaReducers: Array> = [storeFreeze]; 27 | 28 | // const developmentReducer: ActionReducer = compose(storeFreeze, storeLogger(), combineReducers)(reducers); 29 | // const productionReducer: ActionReducer = combineReducers(reducers); 30 | 31 | /* 32 | Don't know where PROD is set. 33 | 34 | export function reducer(state: any, action: any) { 35 | if (PROD) { 36 | return productionReducer(state, action); 37 | } 38 | else { 39 | return developmentReducer(state, action); 40 | } 41 | } 42 | */ 43 | /*export function reducer(state: any, action: any) { 44 | return developmentReducer(state, action); 45 | } 46 | */ 47 | export const getCollectionState = (state: State) => state.collection; 48 | 49 | export const getCollectionLoaded = createSelector( 50 | getCollectionState, 51 | fromCollection.getLoaded, 52 | ); 53 | export const getCollectionLoading = createSelector( 54 | getCollectionState, 55 | fromCollection.getLoading, 56 | ); 57 | export const getCollectionTextItems = createSelector( 58 | getCollectionState, 59 | fromCollection.getTextItems, 60 | ); 61 | // 62 | /* 63 | export const getAuthState = (state: State) => state.auth; 64 | 65 | export const getAuthDisplayName = createSelector( 66 | getAuthState, 67 | fromAuth.getDisplayName, 68 | ); 69 | export const getAuthError = createSelector(getAuthState, fromAuth.getError); 70 | export const getAuthIsAuthenticated = createSelector( 71 | getAuthState, 72 | fromAuth.getIsAuthenticated, 73 | ); 74 | export const getAuthIsAuthenticating = createSelector( 75 | getAuthState, 76 | fromAuth.getIsAuthenticating, 77 | ); 78 | export const getAuthUserId = createSelector(getAuthState, fromAuth.getUserId); 79 | */ 80 | // 81 | export const getGizmoState = (state: State) => state.gizmo; 82 | 83 | export const getGizmoLoaded = createSelector( 84 | getGizmoState, 85 | fromGizmo.getLoaded, 86 | ); 87 | export const getGizmoLoading = createSelector( 88 | getGizmoState, 89 | fromGizmo.getLoading, 90 | ); 91 | 92 | export const selectGizmoIds = createSelector( 93 | getGizmoState, 94 | fromGizmo.selectGizmoIds, 95 | ); 96 | export const selectGizmoEntities = createSelector( 97 | getGizmoState, 98 | fromGizmo.selectGizmoEntities, 99 | ); 100 | export const selectAllGizmos = createSelector( 101 | getGizmoState, 102 | fromGizmo.selectAllGizmos, 103 | ); 104 | export const selectGizmoTotal = createSelector( 105 | getGizmoState, 106 | fromGizmo.selectGizmoTotal, 107 | ); 108 | export const selectCurrentGizmoId = createSelector( 109 | getGizmoState, 110 | fromGizmo.getSelectedGizmoId, 111 | ); 112 | 113 | export const selectCurrentGizmo = createSelector( 114 | selectGizmoEntities, 115 | selectCurrentGizmoId, 116 | (gizmoEntities, gizmoId) => gizmoEntities[gizmoId], 117 | ); 118 | // 119 | export const getWidgetState = (state: State) => state.widget; 120 | 121 | export const getWidgetLoaded = createSelector( 122 | getWidgetState, 123 | fromWidget.getLoaded, 124 | ); 125 | export const getWidgetLoading = createSelector( 126 | getWidgetState, 127 | fromWidget.getLoading, 128 | ); 129 | 130 | export const selectWidgetIds = createSelector( 131 | getWidgetState, 132 | fromWidget.selectWidgetIds, 133 | ); 134 | export const selectWidgetEntities = createSelector( 135 | getWidgetState, 136 | fromWidget.selectWidgetEntities, 137 | ); 138 | export const selectAllWidgets = createSelector( 139 | getWidgetState, 140 | fromWidget.selectAllWidgets, 141 | ); 142 | export const selectWidgetTotal = createSelector( 143 | getWidgetState, 144 | fromWidget.selectWidgetTotal, 145 | ); 146 | export const selectCurrentWidgetId = createSelector( 147 | getWidgetState, 148 | fromWidget.getSelectedWidgetId, 149 | ); 150 | 151 | export const selectCurrentWidget = createSelector( 152 | selectWidgetEntities, 153 | selectCurrentWidgetId, 154 | (widgetEntities, widgetId) => widgetEntities[widgetId], 155 | ); 156 | //#region Gadget selectors 157 | export const getGadgetState = (state: State) => state.gadget; 158 | 159 | export const getGadgetLoaded = createSelector( 160 | getGadgetState, 161 | fromGadget.getLoaded, 162 | ); 163 | export const getGadgetLoading = createSelector( 164 | getGadgetState, 165 | fromGadget.getLoading, 166 | ); 167 | 168 | export const selectGadgetIds = createSelector( 169 | getGadgetState, 170 | fromGadget.selectGadgetIds, 171 | ); 172 | export const selectGadgetEntities = createSelector( 173 | getGadgetState, 174 | fromGadget.selectGadgetEntities, 175 | ); 176 | export const selectAllGadgets = createSelector( 177 | getGadgetState, 178 | fromGadget.selectAllGadgets, 179 | ); 180 | export const selectGadgetTotal = createSelector( 181 | getGadgetState, 182 | fromGadget.selectGadgetTotal, 183 | ); 184 | export const selectCurrentGadgetId = createSelector( 185 | getGadgetState, 186 | fromGadget.getSelectedGadgetId, 187 | ); 188 | 189 | export const selectCurrentGadget = createSelector( 190 | selectGadgetEntities, 191 | selectCurrentGadgetId, 192 | (widgetEntities, widgetId) => widgetEntities[widgetId], 193 | ); 194 | //#endregion 195 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | // tick this to make the cache invalidate and update 2 | const CACHE_VERSION = 1; 3 | const CURRENT_CACHES = { 4 | 'read-through': 'read-through-cache-v' + CACHE_VERSION 5 | }; 6 | 7 | self.addEventListener('activate', (event) => { 8 | // Delete all caches that aren't named in CURRENT_CACHES. 9 | // While there is only one cache in this example, the same logic will handle the case where 10 | // there are multiple versioned caches. 11 | const expectedCacheNames = Object.keys(CURRENT_CACHES).map((key) => { 12 | return CURRENT_CACHES[key]; 13 | }); 14 | 15 | event.waitUntil( 16 | caches.keys().then((cacheNames) => { 17 | return Promise.all( 18 | cacheNames.map((cacheName) => { 19 | if (expectedCacheNames.indexOf(cacheName) === -1) { 20 | // If this cache name isn't present in the array of "expected" cache names, then delete it. 21 | console.log('Deleting out of date cache:', cacheName); 22 | return caches.delete(cacheName); 23 | } 24 | }) 25 | ); 26 | }) 27 | ); 28 | }); 29 | 30 | // This sample illustrates an aggressive approach to caching, in which every valid response is 31 | // cached and every request is first checked against the cache. 32 | // This may not be an appropriate approach if your web application makes requests for 33 | // arbitrary URLs as part of its normal operation (e.g. a RSS client or a news aggregator), 34 | // as the cache could end up containing large responses that might not end up ever being accessed. 35 | // Other approaches, like selectively caching based on response headers or only caching 36 | // responses served from a specific domain, might be more appropriate for those use cases. 37 | self.addEventListener('fetch', (event) => { 38 | 39 | event.respondWith( 40 | caches.open(CURRENT_CACHES['read-through']).then((cache) => { 41 | return cache.match(event.request).then((response) => { 42 | if (response) { 43 | // If there is an entry in the cache for event.request, then response will be defined 44 | // and we can just return it. 45 | 46 | return response; 47 | } 48 | 49 | // Otherwise, if there is no entry in the cache for event.request, response will be 50 | // undefined, and we need to fetch() the resource. 51 | console.log(' No response for %s found in cache. ' + 52 | 'About to fetch from network...', event.request.url); 53 | 54 | // We call .clone() on the request since we might use it in the call to cache.put() later on. 55 | // Both fetch() and cache.put() "consume" the request, so we need to make a copy. 56 | // (see https://fetch.spec.whatwg.org/#dom-request-clone) 57 | return fetch(event.request.clone()).then((response) => { 58 | 59 | // Optional: add in extra conditions here, e.g. response.type == 'basic' to only cache 60 | // responses from the same domain. See https://fetch.spec.whatwg.org/#concept-response-type 61 | if (response.status < 400 && response.type === 'basic') { 62 | // We need to call .clone() on the response object to save a copy of it to the cache. 63 | // (https://fetch.spec.whatwg.org/#dom-request-clone) 64 | cache.put(event.request, response.clone()); 65 | } 66 | 67 | // Return the original response object, which will be used to fulfill the resource request. 68 | return response; 69 | }); 70 | }).catch((error) => { 71 | // This catch() will handle exceptions that arise from the match() or fetch() operations. 72 | // Note that a HTTP error response (e.g. 404) will NOT trigger an exception. 73 | // It will return a normal response object that has the appropriate error code set. 74 | console.error(' Read-through caching failed:', error); 75 | 76 | throw error; 77 | }); 78 | }) 79 | ); 80 | }); -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/v2/theming/ 3 | @import 'ionic.globals'; 4 | 5 | // Shared Variables 6 | // -------------------------------------------------- 7 | // To customize the look and feel of this app, you can override 8 | // the Sass variables found in Ionic's source scss files. 9 | // To view all the possible Ionic variables, see: 10 | // http://ionicframework.com/docs/v2/theming/overriding-ionic-variables/ 11 | 12 | $text-color: #000; 13 | $background-color: #fff; 14 | 15 | // Named Color Variables 16 | // -------------------------------------------------- 17 | // Named colors makes it easy to reuse colors on various components. 18 | // It's highly recommended to change the default colors 19 | // to match your app's branding. Ionic uses a Sass map of 20 | // colors so you can add, rename and remove colors as needed. 21 | // The "primary" color is the only required color in the map. 22 | 23 | $colors: ( 24 | primary: #387ef5, 25 | secondary: #32db64, 26 | danger: #f53d3d, 27 | light: #f4f4f4, 28 | dark: #222, 29 | favorite: #69bb7b 30 | ); 31 | 32 | // App Theme 33 | // -------------------------------------------------- 34 | // Ionic apps can have different themes applied, which can 35 | // then be future customized. This import comes last 36 | // so that the above variables are used and Ionic's 37 | // default are overridden. 38 | 39 | @import 'ionic.theme.default'; 40 | 41 | // Ionicons 42 | // -------------------------------------------------- 43 | // The premium icon font for Ionic. For more info, please see: 44 | // http://ionicframework.com/docs/v2/ionicons/ 45 | 46 | $ionicons-font-path: '../assets/fonts'; 47 | @import 'ionicons'; 48 | -------------------------------------------------------------------------------- /src/widget/modals/widget-detail/widget-detail.modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | Widget 11 | 12 | 18 | 19 | 20 | 21 | 22 |
24 | 25 | 26 | Name 28 | 30 | 31 | 32 | 33 | 34 | Description 36 | 38 | 39 | 40 | 41 |
42 |
-------------------------------------------------------------------------------- /src/widget/modals/widget-detail/widget-detail.modal.scss: -------------------------------------------------------------------------------- 1 | tja-modal-widget-detail { 2 | } 3 | -------------------------------------------------------------------------------- /src/widget/modals/widget-detail/widget-detail.modal.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | import { NavParams, ViewController } from 'ionic-angular'; 5 | 6 | import { newWidget, Widget } from '../../widget.model'; 7 | 8 | interface ModalInput { 9 | item?: Widget; 10 | } 11 | 12 | export function getModalInput(item: Widget | undefined) { 13 | // 14 | const modalInput: ModalInput = { item }; 15 | 16 | return { 17 | modalInput, 18 | }; 19 | } 20 | 21 | export interface ModalResult { 22 | save: boolean; 23 | item?: Widget; 24 | } 25 | 26 | interface FormModel { 27 | description: any; 28 | name: any; 29 | } 30 | 31 | @Component({ 32 | selector: 'tja-modal-widget-detail', 33 | templateUrl: 'widget-detail.modal.html', 34 | }) 35 | export class WidgetDetailModal { 36 | // Called from view. 37 | // public viewForm: any; 38 | public viewForm: FormGroup; 39 | 40 | public get viewCanSave(): boolean { 41 | return !(this.viewForm.dirty && this.viewForm.valid); 42 | } 43 | 44 | private readonly CLASS_NAME = 'WidgetDetailModal'; 45 | 46 | private dataModel: Widget; 47 | 48 | constructor( 49 | public formBuilder: FormBuilder, 50 | public navParams: NavParams, 51 | public viewController: ViewController, 52 | ) { 53 | console.log(`%s:constructor`, this.CLASS_NAME); 54 | 55 | console.log('navParams>', navParams); 56 | console.log('navParams.data>', navParams.data); 57 | console.log('navParams.get("item")>', navParams.get('item')); 58 | console.log('navParams.get("modalInput")>', navParams.get('modalInput')); 59 | 60 | // const modalInput: ModalInput = navParams.get('modalInput'); 61 | const modalInput: ModalInput = this.getModalInput(); 62 | 63 | // const paramItem: Widget = navParams.get('item'); 64 | // const paramItem: Widget | undefined = modalInput.item; 65 | 66 | if (modalInput.item === undefined) { 67 | // new item. 68 | this.dataModel = newWidget(); 69 | } else { 70 | // navParams passes by reference. 71 | this.dataModel = { ...modalInput.item }; 72 | } 73 | 74 | console.log('}}}}}}modalInput.item >', modalInput.item ); 75 | console.log('}}}}}}this.dataModel>', this.dataModel); 76 | } 77 | 78 | public ngOnInit() { 79 | console.log('###%s:ngOnInit>', this.CLASS_NAME); 80 | 81 | this.viewForm = this.formBuilder.group({ 82 | description: [this.dataModel.description], 83 | name: [this.dataModel.name, Validators.required], 84 | } as FormModel); 85 | 86 | /* 87 | this.viewForm.setValue({ 88 | description: this.dataModel.description, 89 | name: this.dataModel.name, 90 | } as FormModel); 91 | */ 92 | } 93 | 94 | public viewCancel() { 95 | console.log('viewCancel>'); 96 | const result: ModalResult = { save: false }; 97 | 98 | this.viewController.dismiss(result); 99 | } 100 | 101 | public viewSave() { 102 | console.log('viewSave>'); 103 | 104 | if (!this.viewForm.valid) { 105 | return; 106 | } 107 | 108 | console.log('this.todoForm.value>', this.viewForm.value); 109 | 110 | const saveItem = this.prepareSaveItem(); 111 | console.log('saveItem>', saveItem); 112 | const result: ModalResult = { save: true, item: saveItem }; 113 | this.viewController.dismiss(result); 114 | } 115 | 116 | private getModalInput(): ModalInput { 117 | // 118 | return this.navParams.get('modalInput'); 119 | } 120 | 121 | private prepareSaveItem(): Widget { 122 | const formModel: FormModel = this.viewForm.value; 123 | 124 | const saveItem: Widget = { 125 | ...this.dataModel, 126 | description: formModel.description, 127 | name: formModel.name, 128 | }; 129 | 130 | return saveItem; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/widget/pages/widget-list/widget-list.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Widgets(Cloud Firestore) 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | IsLoading: {{viewIsLoading$ | async}} 22 |
23 |
24 | IsLoaded: {{viewIsLoaded$ | async}} 25 |
26 | 27 | 28 | 29 |
{{item.name}}
30 |

{{item.description}}

31 | 33 | 36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /src/widget/pages/widget-list/widget-list.page.scss: -------------------------------------------------------------------------------- 1 | tja-page-widget-list { 2 | } 3 | -------------------------------------------------------------------------------- /src/widget/pages/widget-list/widget-list.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { ModalController } from 'ionic-angular'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | import { 7 | getModalInput, 8 | ModalResult, 9 | WidgetDetailModal, 10 | } from '../../modals/widget-detail/widget-detail.modal'; 11 | import { Widget } from '../../widget.model'; 12 | import { WidgetService } from '../../widget.service'; 13 | 14 | // import { Store } from '@ngrx/store'; 15 | 16 | // import * as fromRoot from '../../reducers'; 17 | @Component({ 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | selector: 'tja-page-widget-list', 20 | templateUrl: 'widget-list.page.html', 21 | }) 22 | export class WidgetListPage { 23 | public viewData$: Observable>; 24 | public viewIsLoaded$: Observable; 25 | public viewIsLoading$: Observable; 26 | 27 | constructor( 28 | public modalCtrl: ModalController, 29 | private readonly widgetService: WidgetService, 30 | ) { 31 | this.viewData$ = widgetService.getData$(); 32 | this.viewIsLoaded$ = widgetService.isLoaded(); 33 | this.viewIsLoading$ = widgetService.isLoading(); 34 | } 35 | 36 | public ionViewDidLoad() { 37 | // 38 | this.widgetService.ListenForDataStart(); 39 | } 40 | 41 | public ionViewWillUnload() { 42 | // 43 | this.widgetService.ListenForDataStop(); 44 | } 45 | 46 | public viewAdd(): void { 47 | console.log('viewAdd'); 48 | this.showModal(); 49 | } 50 | 51 | public viewDelete(item: Widget): void { 52 | console.log('viewDelete>', item); 53 | this.widgetService.deleteItem(item); 54 | } 55 | 56 | public viewEdit(item: any): void { 57 | console.log('viewEdit>', item); 58 | this.showModal(item); 59 | } 60 | 61 | private showModal(item?: Widget) { 62 | // 63 | // const modalInput: ModalInput = { item }; 64 | const modalInput = getModalInput(item); 65 | 66 | const modal = this.modalCtrl.create(WidgetDetailModal, modalInput); 67 | 68 | modal.onDidDismiss((data: ModalResult | null) => { 69 | console.log('onDidDismiss>', data); 70 | 71 | if (data === null) { 72 | console.log('onDidDismiss:NULL'); 73 | return; 74 | } 75 | 76 | if (data.save && data.item) { 77 | this.widgetService.upsertItem(data.item); 78 | } 79 | }); 80 | 81 | modal.present(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/widget/widget.actions.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-classes-per-file 2 | import { Action } from '@ngrx/store'; 3 | import { Widget } from './widget.model'; 4 | 5 | export enum WidgetActionTypes { 6 | // 7 | DATABASE_LISTEN_FOR_DATA_START = '[Widget] (Database) Listen For Data - Start', 8 | DATABASE_LISTEN_FOR_DATA_START_ERROR = '[Widget] (Database) Listen For Data - Start - Error', 9 | DATABASE_LISTEN_FOR_DATA_STOP = '[Widget] (Database) Listen For Data - Stop', 10 | DELETE_ITEM = '[Widget] Delete Item', 11 | LOAD_SUCCESS = '[Widget] Load Success', 12 | UPSERT_ITEM = '[Widget] Upsert item', 13 | UPSERT_ITEM_ERROR = '[Widget] Upsert Item - Error ', 14 | UPSERT_ITEM_SUCCESS = '[Widget] Upsert Item - Success', 15 | } 16 | 17 | export class DatabaseListenForDataStart implements Action { 18 | public readonly type = WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_START; 19 | 20 | constructor( 21 | public payload: { 22 | userId: string; 23 | }, 24 | ) {} 25 | } 26 | 27 | export class DatabaseListenForDataStartError implements Action { 28 | public readonly type = WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_START_ERROR; 29 | 30 | constructor( 31 | public payload: { 32 | error: { 33 | code: string; 34 | message: string; 35 | name: string; 36 | }; 37 | }, 38 | ) {} 39 | } 40 | 41 | export class DatabaseListenForDataStop implements Action { 42 | public readonly type = WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_STOP; 43 | } 44 | 45 | export class DeleteItem implements Action { 46 | public readonly type = WidgetActionTypes.DELETE_ITEM; 47 | 48 | constructor(public payload: { id: string; userId: string }) {} 49 | } 50 | 51 | export class LoadSuccess implements Action { 52 | public readonly type = WidgetActionTypes.LOAD_SUCCESS; 53 | 54 | constructor(public payload: { items: Widget[] }) {} 55 | } 56 | 57 | export class UpsertItem implements Action { 58 | public readonly type = WidgetActionTypes.UPSERT_ITEM; 59 | 60 | constructor(public payload: { item: Widget; userId: string }) {} 61 | } 62 | 63 | export class UpsertItemError implements Action { 64 | public readonly type = WidgetActionTypes.UPSERT_ITEM_ERROR; 65 | 66 | constructor( 67 | public payload: { 68 | error: { 69 | code: string; 70 | message: string; 71 | name: string; 72 | }; 73 | }, 74 | ) {} 75 | } 76 | 77 | export class UpsertItemSuccess implements Action { 78 | public readonly type = WidgetActionTypes.UPSERT_ITEM_SUCCESS; 79 | } 80 | 81 | export type WidgetActions = 82 | | DeleteItem 83 | | LoadSuccess 84 | | DatabaseListenForDataStart 85 | | DatabaseListenForDataStartError 86 | | DatabaseListenForDataStop 87 | | UpsertItem 88 | | UpsertItemError 89 | | UpsertItemSuccess; 90 | -------------------------------------------------------------------------------- /src/widget/widget.data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AngularFirestore } from 'angularfire2/firestore'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | import { Widget } from './widget.model'; 6 | 7 | const DATA_COLLECTION = 'widgets'; 8 | const USERS_COLLECTION = 'users'; 9 | 10 | interface FirestoreDoc { 11 | id: string; 12 | description: string; 13 | name: string; 14 | sysDateCreatedOn?: string; 15 | sysDateUpdatedOn?: string; 16 | } 17 | 18 | @Injectable() 19 | export class WidgetDataService { 20 | constructor(public readonly afs: AngularFirestore) { 21 | // console.log('WidgetDataService:constructor'); 22 | } 23 | 24 | public getData$(userId: string): Observable { 25 | // 26 | return this.firestoreCollection(userId) 27 | .valueChanges() 28 | .map((items) => 29 | items.map((item) => { 30 | return this.fromFirestoreDoc(item); 31 | }), 32 | ); 33 | } 34 | 35 | public deleteItem(id: string, userId: string): void { 36 | this.firestoreCollection(userId) 37 | .doc(id) 38 | .delete(); 39 | } 40 | 41 | public upsertItem(item: Widget, userId: string): Promise { 42 | // 43 | if (item.id === '') { 44 | return this.createItem(item, userId); 45 | } else { 46 | return this.updateItem(item, userId); 47 | } 48 | } 49 | 50 | private createItem(item: Widget, userId: string): Promise { 51 | // 52 | const doc = this.toFirestoreDoc(item); 53 | const dateNow = Date().toString(); 54 | doc.id = this.afs.createId(); 55 | const recordToSet: FirestoreDoc = { 56 | ...doc, 57 | sysDateCreatedOn: dateNow, 58 | sysDateUpdatedOn: dateNow, 59 | }; 60 | 61 | return this.firestoreCollection(userId) 62 | .doc(recordToSet.id) 63 | .set(recordToSet); 64 | } 65 | 66 | private updateItem(item: Widget, userId: string): Promise { 67 | // 68 | const doc = this.toFirestoreDoc(item); 69 | const dateNow = Date().toString(); 70 | const recordToUpdate: FirestoreDoc = { 71 | ...doc, 72 | sysDateUpdatedOn: dateNow, 73 | }; 74 | 75 | return this.firestoreCollection(userId) 76 | .doc(doc.id) 77 | .update(recordToUpdate); 78 | } 79 | 80 | private firestoreCollection(userId: string) { 81 | // 82 | return this.afs 83 | .collection(USERS_COLLECTION) 84 | .doc(userId) 85 | .collection(DATA_COLLECTION, (ref) => 86 | ref.orderBy('name', 'asc'), 87 | ); 88 | 89 | /* 90 | return this.afs.collection(DATA_COLLECTION, (ref) => 91 | ref.orderBy('name', 'asc'), 92 | ); 93 | */ 94 | } 95 | 96 | private toFirestoreDoc(item: Widget): FirestoreDoc { 97 | // 98 | const result: FirestoreDoc = { 99 | description: item.description, 100 | id: item.id, 101 | name: item.name, 102 | }; 103 | 104 | // console.log('toFirebaseTodo>', result); 105 | return result; 106 | } 107 | 108 | private fromFirestoreDoc(x: FirestoreDoc): Widget { 109 | // 110 | // console.log('TodoDataService:fromFirebaseTodo>', x); 111 | 112 | // This copies extra fields. 113 | // const result: Widget = { ...x }; 114 | 115 | const result: Widget = { 116 | description: x.description, 117 | id: x.id, 118 | name: x.name, 119 | }; 120 | 121 | // console.log('TodoDataService:fromFirebaseTodo:result>', result); 122 | 123 | return result; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/widget/widget.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Actions, Effect, ofType } from '@ngrx/effects'; 4 | import { Store } from '@ngrx/store'; 5 | import { empty } from 'rxjs/observable/empty'; 6 | 7 | import * as FromRootReducer from '../reducers'; 8 | import { 9 | DatabaseListenForDataStart, 10 | DatabaseListenForDataStartError, 11 | DatabaseListenForDataStop, 12 | DeleteItem, 13 | LoadSuccess, 14 | UpsertItem, 15 | UpsertItemError, 16 | UpsertItemSuccess, 17 | WidgetActionTypes, 18 | } from './widget.actions'; 19 | import { WidgetDataService } from './widget.data.service'; 20 | import { Widget } from './widget.model'; 21 | 22 | import { fromPromise } from 'rxjs/observable/fromPromise'; 23 | import { of } from 'rxjs/observable/of'; 24 | import { 25 | catchError, 26 | map, 27 | switchMap, 28 | tap, 29 | } from 'rxjs/operators'; 30 | 31 | @Injectable() 32 | export class WidgetEffects { 33 | constructor( 34 | private actions$: Actions, 35 | private store$: Store, 36 | private dataService: WidgetDataService, 37 | ) {} 38 | 39 | // tslint:disable-next-line:member-ordering 40 | @Effect({ dispatch: false }) 41 | public deleteItem$ = this.actions$.pipe( 42 | ofType(WidgetActionTypes.DELETE_ITEM), 43 | map((action: DeleteItem) => action.payload), 44 | tap((payload) => { 45 | console.log('Effect:deleteItem$:A', payload); 46 | this.dataService.deleteItem(payload.id, payload.userId); 47 | }), 48 | ); 49 | 50 | // tslint:disable-next-line:member-ordering 51 | @Effect({ dispatch: false }) 52 | public listenForData$ = this.actions$.pipe( 53 | ofType( 54 | WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_START, 55 | WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_STOP, 56 | ), 57 | tap(() => { 58 | console.log('Effect:listenForData$:A'); 59 | }), 60 | switchMap((action) => { 61 | console.log('Effect:listenForData$:action>', action); 62 | switch (action.type) { 63 | case WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_START: { 64 | return this.dataService.getData$(action.payload.userId).pipe( 65 | map((items: Widget[]) => { 66 | this.store$.dispatch(new LoadSuccess({ items })); 67 | }), 68 | catchError((error) => { 69 | this.store$.dispatch( 70 | new DatabaseListenForDataStartError({ 71 | error: this.handleFirebaseError(error), 72 | }), 73 | ); 74 | // Pass on to higher level. 75 | // throw error; 76 | return empty(); 77 | }), 78 | ); 79 | } 80 | 81 | default: { 82 | return empty(); 83 | } 84 | } 85 | }), 86 | tap((x) => { 87 | console.log('Effect:listenForData$:B', x); 88 | }), 89 | ); 90 | 91 | // tslint:disable-next-line:member-ordering 92 | @Effect() 93 | public upsertItem$ = this.actions$.pipe( 94 | ofType(WidgetActionTypes.UPSERT_ITEM), 95 | map((action) => action.payload), 96 | switchMap((payload) => { 97 | return fromPromise( 98 | this.dataService.upsertItem(payload.item, payload.userId), 99 | ).pipe( 100 | map(() => new UpsertItemSuccess()), 101 | catchError((error) => 102 | of( 103 | new UpsertItemError({ 104 | error: this.handleFirebaseError(error), 105 | }), 106 | ), 107 | ), 108 | ); 109 | }), 110 | ); 111 | 112 | private handleFirebaseError(firebaseError: any) { 113 | // 114 | return { 115 | code: firebaseError.code, 116 | message: firebaseError.message, 117 | name: firebaseError.name, 118 | }; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/widget/widget.model.ts: -------------------------------------------------------------------------------- 1 | // https://redux.js.org/docs/faq/OrganizingState.html#organizing-state-non-serializable 2 | /* 3 | Can I put functions, promises, or other non-serializable items in my store 4 | state? It is highly recommended that you only put plain serializable objects, 5 | arrays, and primitives into your store. It's technically possible to insert 6 | non-serializable items into the store, but doing so can break the ability to 7 | persist and rehydrate the contents of a store, as well as interfere with 8 | time-travel debugging. 9 | 10 | If you are okay with things like persistence and time-travel debugging 11 | potentially not working as intended, then you are totally welcome to put 12 | non-serializable items into your Redux store. Ultimately, it's your application, 13 | and how you implement it is up to you. As with many other things about Redux, 14 | just be sure you understand what tradeoffs are involved. 15 | */ 16 | // https://stackoverflow.com/questions/43181516/getting-model-instance-from-ngrx-store-select/43185931#43185931 17 | /* 18 | But: It is generally not recommended to have Class-Instances in the store, there 19 | are a few rules of thumb: 20 | 21 | The store-content should serializable without any major modifications (=> just 22 | use Object and Primitives) ngrx (and rxjs in general) are relying heavily on 23 | functional programming patterns, so mixing it Object Oriented paradigms is not 24 | recommended. 25 | */ 26 | export interface Widget { 27 | readonly description: string; 28 | readonly id: string; 29 | readonly name: string; 30 | } 31 | 32 | export function newWidget(): Widget { 33 | return { description: '', id: '', name: '' }; 34 | } 35 | -------------------------------------------------------------------------------- /src/widget/widget.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 2 | 3 | import { WidgetActions, WidgetActionTypes } from './widget.actions'; 4 | import { Widget } from './widget.model'; 5 | 6 | export interface State extends EntityState { 7 | // additional entities state properties 8 | loaded: boolean; 9 | loading: boolean; 10 | selectedWidgetId: string; 11 | } 12 | 13 | export const adapter: EntityAdapter = createEntityAdapter(); 14 | 15 | export const initialState: State = adapter.getInitialState({ 16 | // additional entity state properties 17 | loaded: false, 18 | loading: false, 19 | selectedWidgetId: '', 20 | }); 21 | 22 | export function reducer(state = initialState, action: WidgetActions): State { 23 | switch (action.type) { 24 | case WidgetActionTypes.DATABASE_LISTEN_FOR_DATA_STOP: { 25 | return adapter.removeAll({ 26 | ...state, 27 | loaded: false, 28 | loading: false, 29 | selectedWidgetId: '', 30 | }); 31 | } 32 | 33 | case WidgetActionTypes.LOAD_SUCCESS: { 34 | return adapter.addAll(action.payload.items, state); 35 | } 36 | 37 | default: { 38 | return state; 39 | } 40 | } 41 | } 42 | 43 | export const getSelectedWidgetId = (state: State) => state.selectedWidgetId; 44 | 45 | export const { 46 | // select the array of widget ids 47 | selectIds: selectWidgetIds, 48 | 49 | // select the dictionary of widget entities 50 | selectEntities: selectWidgetEntities, 51 | 52 | // select the array of widgets 53 | selectAll: selectAllWidgets, 54 | 55 | // select the total widget count 56 | selectTotal: selectWidgetTotal, 57 | } = adapter.getSelectors(); 58 | 59 | export const getLoaded = (state: State) => state.loaded; 60 | export const getLoading = (state: State) => state.loading; 61 | -------------------------------------------------------------------------------- /src/widget/widget.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { select, Store } from '@ngrx/store'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { filter, take } from 'rxjs/operators'; 6 | 7 | import * as FromAuthSelector from '../app/auth/auth.selector'; 8 | import * as FromRootReducer from '../reducers'; 9 | import { 10 | DatabaseListenForDataStart, 11 | DatabaseListenForDataStop, 12 | DeleteItem, 13 | UpsertItem, 14 | } from './widget.actions'; 15 | import { Widget } from './widget.model'; 16 | 17 | @Injectable() 18 | export class WidgetService { 19 | // 20 | private init$ = this.store.pipe( 21 | select(FromAuthSelector.getUserId), 22 | filter((userId) => userId !== ''), 23 | ); 24 | 25 | constructor(private store: Store) {} 26 | 27 | public getData$(): Observable> { 28 | // 29 | return this.store.pipe(select(FromRootReducer.selectAllWidgets)); 30 | } 31 | 32 | public ListenForDataStart(): void { 33 | // 34 | this.init$.pipe(take(1)).subscribe((userId) => { 35 | this.store.dispatch(new DatabaseListenForDataStart({ userId })); 36 | }); 37 | } 38 | 39 | public ListenForDataStop(): void { 40 | // 41 | this.store.dispatch(new DatabaseListenForDataStop()); 42 | } 43 | 44 | public deleteItem(item: Widget) { 45 | // 46 | this.init$.pipe(take(1)).subscribe((userId) => { 47 | this.store.dispatch(new DeleteItem({ id: item.id, userId })); 48 | }); 49 | } 50 | 51 | public upsertItem(item: Widget) { 52 | // 53 | this.init$.pipe(take(1)).subscribe((userId) => { 54 | this.store.dispatch(new UpsertItem({ item, userId })); 55 | }); 56 | } 57 | 58 | public isLoaded(): Observable { 59 | // 60 | return this.store.pipe(select(FromRootReducer.getWidgetLoaded)); 61 | } 62 | 63 | public isLoading(): Observable { 64 | // 65 | return this.store.pipe(select(FromRootReducer.getWidgetLoading)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tim-readme.md: -------------------------------------------------------------------------------- 1 | Latest release 2 | npm install -g ionic 3 | 4 | Started using sidemenu template 5 | ionic start my-side-menu sidemenu --v2 6 | 7 | # Custom config files not working on Windows 10. 8 | https://github.com/driftyco/ionic-app-scripts 9 | 10 | So need to copy config/af-rollup.config.js over node_modules/@ionic/app-scripts/config/rollup.config.js. 11 | 12 | ### firebase-browser.js does not export initializeApp 13 | https://github.com/angular/angularfire2/issues/565 14 | 15 | 16 | https://github.com/angular/angularfire2/issues/578 17 | 18 | 19 | https://github.com/angular/angularfire2/issues/545#issuecomment-248712121 20 | 21 | 22 | import named doesn't work with re-exported contents 23 | https://github.com/rollup/rollup-plugin-commonjs/issues/35 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "strict": true, 10 | "lib": [ 11 | "dom", 12 | "es2015" 13 | ], 14 | "module": "es2015", 15 | "moduleResolution": "node", 16 | "sourceMap": true, 17 | "target": "es5" 18 | }, 19 | "include": [ 20 | "src/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ], 25 | "compileOnSave": false, 26 | "atom": { 27 | "rewriteTsconfig": false 28 | } 29 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "rules": { 6 | // "tslint:recommended" overrides 7 | // ------------------------------ 8 | "interface-name": [true, "never-prefix"], 9 | "no-console": [false], 10 | "quotemark": [true, "single"] 11 | }, 12 | "rulesDirectory": [ 13 | "node_modules/tslint-eslint-rules/dist/rules" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------