|
| 1 | +# SvelteFire |
| 2 | + |
| 3 | +Cybernetically enhanced Firebase apps 💪🔥 |
| 4 | + |
| 5 | +## Basics |
| 6 | + |
| 7 | +- Use Firebase declaratively in Svelte components. |
| 8 | +- Handle complex relational data with simple loading & fallback states. |
| 9 | +- Automatic data disposal to prevent memory/cost leaks, plus enhanced logging. |
| 10 | +- Automatic performance monitoring & Google Analytics. |
| 11 | + |
| 12 | + |
| 13 | +**Psuedo Example** |
| 14 | + |
| 15 | +Handle multiple levels of async relational data (and their loading & fallback states) entirely from the Svelte HTML. |
| 16 | + |
| 17 | + |
| 18 | +```html |
| 19 | +<!-- 1. 🔥 Firebase App --> |
| 20 | +<FirebaseApp {firebase}> |
| 21 | + |
| 22 | + <!-- 2. 😀 Get the current user --> |
| 23 | + <User let:user> |
| 24 | + |
| 25 | + <p>Howdy, {user.uid}</p> |
| 26 | + |
| 27 | + <!-- 3. 📜 Get a Firestore document owned by a user --> |
| 28 | + <Doc path={`posts/${user.uid}`} let:data={post} let:ref={postRef}> |
| 29 | + |
| 30 | + <h2>{post.title}</h2> |
| 31 | + |
| 32 | + <!-- 4. 💬 Get all the comments in its subcollection --> |
| 33 | + <Collection path={postRef.collection('comments')} let:data={comments}> |
| 34 | + {#each comments as comment} |
| 35 | + |
| 36 | + {/each} |
| 37 | + |
| 38 | + |
| 39 | +... |
| 40 | +``` |
| 41 | + |
| 42 | + |
| 43 | +## Quick Start |
| 44 | + |
| 45 | +```bash |
| 46 | +npm install sveltefire firebase |
| 47 | +``` |
| 48 | + |
| 49 | + |
| 50 | +Create a web app from the [Firebase Console](https://console.firebase.google.com/) and grab your credentials. Enable **Anonymous Login** and create a **Firestore** database instance in test mode. |
| 51 | + |
| 52 | + |
| 53 | +Initialize the Firebase app in the `App.svelte` file. |
| 54 | + |
| 55 | +```html |
| 56 | +<script> |
| 57 | + import { FirebaseApp, User, Doc, Collection } from 'sveltefire'; |
| 58 | + |
| 59 | + // Import the Firebase Services you want bundled and call initializeApp |
| 60 | + import firebase from "firebase/app"; |
| 61 | + import 'firebase/firestore'; |
| 62 | + import 'firebase/auth'; |
| 63 | + import 'firebase/performance'; |
| 64 | + import 'firebase/analytics'; |
| 65 | +
|
| 66 | + const firebaseConfig = { |
| 67 | + apiKey: 'api-key', |
| 68 | + authDomain: 'project-id.firebaseapp.com', |
| 69 | + databaseURL: 'https://project-id.firebaseio.com', |
| 70 | + projectId: 'project-id', |
| 71 | + storageBucket: 'project-id.appspot.com', |
| 72 | + messagingSenderId: 'sender-id', |
| 73 | + appId: 'app-id', |
| 74 | + measurementId: 'G-measurement-id', |
| 75 | + } |
| 76 | +
|
| 77 | + firebase.initializeApp(firebaseConfig) |
| 78 | +</script> |
| 79 | +``` |
| 80 | + |
| 81 | +**Full Example** |
| 82 | + |
| 83 | +Start by building an **authenticated realtime CRUD app** . A user can sign-in, create posts, and add comments to that post. Paste this code into your app. |
| 84 | + |
| 85 | +```html |
| 86 | +<!-- 1. 🔥 Firebase App --> |
| 87 | +<FirebaseApp {firebase}> |
| 88 | + |
| 89 | + <!-- 2. 😀 Get the current user --> |
| 90 | + <User let:user let:auth> |
| 91 | + |
| 92 | + <p>Howdy, {user.uid}</p> |
| 93 | + <button on:click={() => auth.signOut()}>Sign Out</button> |
| 94 | + |
| 95 | + <div slot="signed-out"> |
| 96 | + <button on:click={() => auth.signInAnonymously()}>Sign In</button> |
| 97 | + </div> |
| 98 | + |
| 99 | + <!-- 3. 📜 Get a Firestore document owned by a user --> |
| 100 | + <Doc path={`posts/${user.uid}`} let:data={post} let:ref={postRef} log> |
| 101 | + |
| 102 | + <h2>{post.title}</h2> |
| 103 | + |
| 104 | + <span slot="loading">Loading post...</span> |
| 105 | + <span slot="fallback"> |
| 106 | + <p>Demo post not created yet...</p> |
| 107 | + |
| 108 | + <button on:click={() => postRef.set({ title: 'I like Svelte' })}> |
| 109 | + Create it Now |
| 110 | + </button> |
| 111 | + </span> |
| 112 | + |
| 113 | + <!-- 4. 💬 Get all the comments in its subcollection --> |
| 114 | + <Collection |
| 115 | + path={postRef.collection('comments')} |
| 116 | + let:data={comments} |
| 117 | + let:ref={commentsRef} |
| 118 | + log> |
| 119 | + |
| 120 | + {#each comments as comment} |
| 121 | + <p>{comment.text}</p> |
| 122 | + <button on:click={() => comment.ref.delete()}>Delete</button> |
| 123 | + {/each} |
| 124 | + |
| 125 | + <hr /> |
| 126 | + |
| 127 | + <button on:click={() => commentsRef.add({ text: 'Cool!' })}> |
| 128 | + Add Comment |
| 129 | + </button> |
| 130 | + |
| 131 | + <span slot="loading">Loading comments...</span> |
| 132 | + |
| 133 | + </Collection> |
| 134 | + </Doc> |
| 135 | + </User> |
| 136 | +</FirebaseApp> |
| 137 | +``` |
| 138 | + |
| 139 | +Run it on localhost:5000 |
| 140 | + |
| 141 | +``` |
| 142 | +npm run dev |
| 143 | +``` |
| 144 | + |
| 145 | +If you see the error 'openDb' is not exported by node_modules\idb\build\idb.js`, go in the `rollup.config.js` and add this line: |
| 146 | + |
| 147 | +```js |
| 148 | + resolve({ |
| 149 | + ... |
| 150 | + mainFields: ['main', 'module'] /// <-- here |
| 151 | + }), |
| 152 | +``` |
| 153 | + |
| 154 | +## Concepts |
| 155 | + |
| 156 | +SvelteFire allows you to use Firebase data anywhere in the Svelte component without the need to manage async state, promises, or streams. |
| 157 | + |
| 158 | +### Slots |
| 159 | + |
| 160 | +[Slots](https://svelte.dev/tutorial/slots) render different UI templates based on the state of your data. The `loading` state is shown until the first response is received from Firebase. The `fallback` state is shown if there is an error or timeout. |
| 161 | + |
| 162 | +In most cases, state flows from *loading* -> *default*. For errors, non-existent data, or high-latency, state flows from *loading* -> *fallback* `maxWait` default is 10000ms). |
| 163 | + |
| 164 | + |
| 165 | +```html |
| 166 | +<Doc path={'foods/ice-cream'}> |
| 167 | + |
| 168 | + <!-- Default Slot --> |
| 169 | + Data loaded, yay 🍦! |
| 170 | + |
| 171 | + <!-- Only shown when loading --> |
| 172 | + <div slot="loading"> |
| 173 | + Loading... |
| 174 | + </div> |
| 175 | + |
| 176 | + <!-- Shown on error or if nothing loads after maxWait time--> |
| 177 | + <div slot="fallback"> |
| 178 | + whoops! |
| 179 | + </div> |
| 180 | +</Doc> |
| 181 | +``` |
| 182 | + |
| 183 | +You can bypass the loading state entirely by passing a `startWith` prop. |
| 184 | + |
| 185 | +```html |
| 186 | +<Doc path={'foods/ice-cream'} startWith={ {flavor: 'vanilla'} }> |
| 187 | +``` |
| 188 | + |
| 189 | +### Slot Props |
| 190 | + |
| 191 | +[Slot props](https://svelte.dev/tutorial/slot-props) **pass data down** to children the component tree. SvelteFire has done the hard work to expose the data you will need in the UI. For example, `let:data` gives you access to the document data, while `={yourVar}` is the name you use to reference it in your code. The `data` is a plain object for showing data in the UI, while the `ref` is a Firestore `DocumentReference` used to execute writes. |
| 192 | + |
| 193 | + |
| 194 | +```html |
| 195 | +<Doc path={`food/ice-cream`} let:data={icecream} let:ref={docRef}> |
| 196 | + |
| 197 | + {icecream.flavor} yay 🍦! |
| 198 | + |
| 199 | + <button on:click={() => docRef.delete()}>Delete</button> |
| 200 | +</Doc> |
| 201 | +``` |
| 202 | + |
| 203 | + |
| 204 | + |
| 205 | +### Events |
| 206 | + |
| 207 | +[Events](https://svelte.dev/tutorial/component-events) **emit data up** to the parent. You can use components as a mechanism to read documents without actually rendering UI. Also useful for trigging side effects. |
| 208 | + |
| 209 | +```html |
| 210 | +<Doc path={'food/ice-cream'} on:data={(e) => console.log(e.detail.data)} /> |
| 211 | +``` |
| 212 | + |
| 213 | +### Stores |
| 214 | + |
| 215 | +[Stores](https://svelte.dev/tutorial/custom-stores) are used under the hood to manage async data in components. It's an advanced use-case, but they can be used directly in a component script or plain JS. |
| 216 | + |
| 217 | +```js |
| 218 | +<script> |
| 219 | +import { collectionStore } from 'sveltefire'; |
| 220 | + |
| 221 | +const data = collectionStore('things', (ref => ref.orderBy('time') )); |
| 222 | + |
| 223 | +data.subscribe(v => doStuff(v) ) |
| 224 | +</script> |
| 225 | +``` |
| 226 | + |
| 227 | +### Firebase App Context |
| 228 | + |
| 229 | +The Firebase SDK is available via the [Context API](https://svelte.dev/tutorial/context-api) under the key of `firebase`. |
| 230 | + |
| 231 | +```js |
| 232 | +const db = getContext('firebase').firestore(); |
| 233 | +``` |
| 234 | + |
| 235 | +## API |
| 236 | + |
| 237 | +### `<FirebaseApp>` |
| 238 | + |
| 239 | +Sets Firebase app context |
| 240 | + |
| 241 | +Props: |
| 242 | + |
| 243 | +- *firebase (required)* Firebase app |
| 244 | +- *perf* Starts Firebase Performance Monitoring |
| 245 | +- *analytics* Starts Firebase/Google Analytics |
| 246 | + |
| 247 | + |
| 248 | +```html |
| 249 | +<FirebaseApp firebase={firebase} perf analytics> |
| 250 | + <!-- default slot --> |
| 251 | +</FirebaseApp> |
| 252 | +``` |
| 253 | + |
| 254 | + |
| 255 | +### `<User>` |
| 256 | + |
| 257 | +Listens to the current user. |
| 258 | + |
| 259 | +Props: |
| 260 | + |
| 261 | +- *persist* user in `sessionStorage` or `localStorage`. Can prevent flash if user refreshes browser. Default `null`; |
| 262 | + |
| 263 | +Slots: |
| 264 | + |
| 265 | +- *default slot* shown to signed-in user |
| 266 | +- *signed-out* shown to signed-out user |
| 267 | + |
| 268 | +Slot Props & Events: |
| 269 | + |
| 270 | +- *user* current FirebaseUser or `null` |
| 271 | +- *auth* Firebase Auth to call login methods. |
| 272 | + |
| 273 | +```html |
| 274 | +<User persist={sessionStorage} let:user={user} let:auth={auth} on:user> |
| 275 | + {user.uid} |
| 276 | + |
| 277 | + <div slot="signed-out"></div> |
| 278 | +</User> |
| 279 | +``` |
| 280 | + |
| 281 | + |
| 282 | +### `<Doc>` |
| 283 | + |
| 284 | +Retrieves and listens to a Firestore document. |
| 285 | + |
| 286 | +Props: |
| 287 | + |
| 288 | +- *path (required)* - Path to document as `string` OR a DocumentReference i.e `db.doc('path')` |
| 289 | +- *startWith* any value. Bypasses loading state. |
| 290 | +- *maxWait* `number` milliseconds to wait before showing fallback slot if nothing is returned. Default 10000. |
| 291 | +- *log* debugging info to the console. Default `false`. |
| 292 | +- *traceId* `string` name that runs a Firebase Performance trace for latency. |
| 293 | + |
| 294 | +Slots: |
| 295 | + |
| 296 | +- *default slot* shown when document is available. |
| 297 | +- *loading* when waiting for first response. |
| 298 | +- *fallback* when error occurs. |
| 299 | + |
| 300 | + |
| 301 | +Slot Props & Events: |
| 302 | + |
| 303 | +- *data* Document data |
| 304 | +- *ref* DocumentReference for writes |
| 305 | +- *error* current error |
| 306 | + |
| 307 | +```html |
| 308 | +<Doc |
| 309 | + path={'posts/postId'} |
| 310 | + startWith={defaultData} |
| 311 | + log |
| 312 | + traceId={'postRead'} |
| 313 | + let:data={myData} |
| 314 | + let:ref={myRef} |
| 315 | + on:data |
| 316 | + on:ref |
| 317 | +> |
| 318 | + |
| 319 | + |
| 320 | + {post.title} |
| 321 | + |
| 322 | + <span slot="loading">Loading...</span> |
| 323 | + <span slot="fallback">Error...</span> |
| 324 | +</Doc> |
| 325 | +``` |
| 326 | + |
| 327 | + |
| 328 | +### `<Collection>` |
| 329 | + |
| 330 | +Retrieves and listens to a Firestore collection or query. |
| 331 | + |
| 332 | +Props: |
| 333 | + |
| 334 | +- *path (required)* to document as `string` OR `CollectionReference` i.e `db.collection('path')` |
| 335 | +- *query* `function`, i.e (ref) => ref.where('age, '==', 23) |
| 336 | +- *startWith* any value. Bypasses loading state. |
| 337 | +- *maxWait* `number` milliseconds to wait before showing fallback slot if nothing is returned. Default 10000. |
| 338 | +- *log* debugging info to the console. Default `false`. |
| 339 | +- *traceId* `string` name that runs a Firebase Performance trace for latency. |
| 340 | + |
| 341 | +Slots: |
| 342 | + |
| 343 | +- *default slot* shown when document is available. |
| 344 | +- *loading* when waiting for first response. |
| 345 | +- *fallback* when error occurs. |
| 346 | + |
| 347 | + |
| 348 | +Slot Props & Events: |
| 349 | + |
| 350 | +- *data* collection data as array. |
| 351 | +- *ref* CollectionReference for writes |
| 352 | +- *error* current error |
| 353 | + |
| 354 | +```html |
| 355 | +<Collection |
| 356 | + path={'comments'} |
| 357 | + query={ (ref) => ref.orderBy(date).limit(10) } |
| 358 | + traceId={'readComments'} |
| 359 | + log |
| 360 | + let:data={comments} |
| 361 | + let:ref={commentsRef} |
| 362 | + on:data |
| 363 | + on:ref |
| 364 | +> |
| 365 | + |
| 366 | + {#each comments as comment} |
| 367 | + {comment.text} |
| 368 | + {/each} |
| 369 | + |
| 370 | + <div slot="loading">Loading...</div> |
| 371 | + |
| 372 | + <div slot="fallback"> |
| 373 | + Unable to display comments... |
| 374 | + </div> |
| 375 | + |
| 376 | +</Collection> |
| 377 | +``` |
| 378 | + |
| 379 | +Note: Each data item in the collection contains the document data AND fields for the `id` and `ref` (DocumentReference). |
0 commit comments