├── .babelrc ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── fiery-vue.js └── types │ └── index.d.ts ├── example └── index.html ├── package.json ├── src └── index.ts ├── tests ├── reactive.ts └── util.ts ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": ["html"], 4 | "env": { 5 | "amd": true 6 | }, 7 | "rules": { 8 | "no-new": 0 9 | } 10 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: clickermonkey 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | custom: # Replace with a single custom sponsorship URL 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | npm-debug.log 4 | package-lock.json 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | nguage: node_js 2 | node_js: 3 | - 6.10.0 4 | script: node_modules/karma/bin/karma start karma.conf.js --single-run 5 | before_install: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | before_script: 9 | - npm install 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Philip Diffenderfer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Fiery Data 4 |

5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 | ## fiery-vue 13 | 14 | Vue.js binding for Google Firebase Cloud Firestore. 15 | 16 | Relies on [fiery-data](https://github.com/fiery-data/fiery-data) - you can go there to see more advanced examples 17 | 18 | #### Features 19 | - Documents [example](#documents) 20 | - Collections (stored as array or map) [example](#collections) 21 | - Queries (stored as array or map) [example](#queries) 22 | - Streams (stored as array or map) [example](#streams) 23 | - Pagination [example](#pagination) 24 | - Real-time or once [example](#real-time-or-once) 25 | - Data or computed properties [example](#data-or-computed) 26 | - Adding, updating, sync, removing, remove field [example](#adding-updating-overwriting-removing) 27 | - Sub-collections (with cascading deletions!) [example](#sub-collections) 28 | - Return instances of a class [example](#return-instances-of-a-class) 29 | - Add active record methods (sync, update, remove, clear, getChanges) [example](#active-record) 30 | - Control over what properties are sent on save [example](#save-fields) 31 | - Encode & decode properties [example](#encode--decode-properties) 32 | - Adding the key and exists to the document [example](#adding-key-and-exists-to-object) 33 | - Sharing, extending, defining, and global options [example](#sharing-extending-defining-and-global-options) 34 | - Callbacks (error, success, missing, remove) [example](#callbacks) 35 | - Custom binding / unbinding [example](#binding-and-unbinding) 36 | 37 | **Contents** 38 | - [Dependencies](#dependencies) 39 | - [Installation](#installation) 40 | - [Usage](#usage) 41 | 42 | ### Dependencies 43 | 44 | - fiery-data: ^0.0.7 45 | - Firebase ^5.0.0 (a runtime dependency only, since you are passing the references) 46 | - Vue: ^1.0.28 (not an actual dependency, since you are calling `Vue.use` yourself) 47 | 48 | ### Installation 49 | 50 | #### npm 51 | 52 | Installation via npm : `npm install --save fiery-vue` 53 | 54 | ### Usage 55 | 56 | ```javascript 57 | import Vue from 'vue' 58 | import FieryVue from 'fiery-vue' 59 | import firebase from 'firebase' 60 | 61 | require('firebase/firestore') 62 | 63 | Vue.use(FieryVue) 64 | 65 | const app = firebase.initializeApp({ ... }) 66 | const fs = firebase.firestore(app); 67 | 68 | new Vue({ 69 | el: '#app', 70 | fiery: true, // required to add this.$fiery to this component 71 | data() { 72 | return { 73 | todos: this.$fiery(fs.collection('todos')) // live collection, 74 | ford: this.$fiery(fs.collection('cars').doc('ford')), // live document 75 | role: 'admin' 76 | } 77 | }, 78 | computed: { 79 | // Updated when role changes 80 | personsWithRole() { 81 | const { role } = this 82 | const options = { 83 | query: q => q.where('role', '==', role), 84 | type: Person 85 | } 86 | return this.$fiery(fs.collection('persons'), options, 'personsWithRole') 87 | } 88 | } 89 | }) 90 | ``` 91 | 92 | Each object will contain a `.uid` property. This helps identify what firestore 93 | database the document is stored in, the collection, and with which options. 94 | 95 | ```json 96 | { 97 | ".uid": "1///1///todos/-Jtjl482BaXBCI7brMT8", 98 | "name": "Star fiery-vue", 99 | "done": true 100 | } 101 | ``` 102 | 103 | ### Documents 104 | 105 | ```javascript 106 | new Vue({ 107 | inject: ['currentUserId'], 108 | fiery: true, // required to add this.$fiery to this component 109 | data() { 110 | const $fiery = this.$fiery 111 | return { 112 | settings: $fiery(fs.collection('settings').doc('system')), 113 | currentUser: $fiery(fs.collection('users').doc(this.currentUserId)) // not reactive, but is updated real-time 114 | } 115 | } 116 | }) 117 | ``` 118 | 119 | ### Collections 120 | 121 | ```javascript 122 | new Vue({ 123 | fiery: true, // required to add this.$fiery to this component 124 | data() { 125 | const $fiery = this.$fiery 126 | return { 127 | cars: $fiery(fs.collection('cars')) // real-time array 128 | carMap: $fiery(fs.collection('cars'), {map: true}) // real-time map: carMap[id] = car 129 | } 130 | } 131 | }) 132 | ``` 133 | 134 | ### Queries 135 | 136 | ```javascript 137 | new Vue({ 138 | inject: ['currentUserId'], 139 | fiery: true, // required to add this.$fiery to this component 140 | data() { 141 | const $fiery = this.$fiery 142 | return { 143 | currentCars: $fiery(fs.collection('cars'), { // real-time array 144 | query: (cars) => cars.where('created_by', '==', this.currentUserId) 145 | }) 146 | currentCarMap: $fiery(fs.collection('cars'), { // real-time map: currentCarMap[id] = car 147 | query: (cars) => cars.where('created_by', '==', this.currentUserId), 148 | map: true 149 | }) 150 | } 151 | } 152 | }) 153 | ``` 154 | 155 | ### Streams 156 | 157 | A stream is an ordered collection of documents where the first N are fetched, and any newly created/updated documents that should be placed in the collection 158 | are added. You can look back further in the stream using `more`. A use case for 159 | streams are a message channel. When the stream is first loaded N documents are 160 | read. As new messages are created they are added to the beginning of the collection. If the user wishes to see older messages they simply have to call 161 | `more` on the stream to load M more. The `once` property does not work on streams, they are real-time only. 162 | 163 | You MUST have an orderBy clause on the query option and `stream` must be `true`. 164 | 165 | ```javascript 166 | new Vue({ 167 | fiery: true, // required to add this.$fiery to this component 168 | data() { 169 | const $fiery = this.$fiery 170 | return { 171 | // streams are always real-time, but can be an array or map 172 | messages: $fiery( 173 | fs.collection('messages'), { 174 | query: q => q.orderBy('created_at', 'desc'), 175 | stream: true, 176 | streamInitial: 25, // initial number of documents to load 177 | streamMore: 10 // documents to load when more is called without a count 178 | }) 179 | } 180 | }, 181 | methods: { 182 | loadMore() { 183 | // load 10 more 184 | this.$fiery.more(this.messages) 185 | }, 186 | loadManyMore() { 187 | // load count more 188 | $fiery.more(messages, 100) 189 | } 190 | } 191 | }) 192 | ``` 193 | 194 | ### Pagination 195 | 196 | ```javascript 197 | new Vue({ 198 | data() { 199 | return { 200 | make: 'Honda', 201 | limit: 10 202 | } 203 | }, 204 | computed: { 205 | carsOptions() { 206 | const { make, limit } = this // we have to reference these here for this to work 207 | return { 208 | query: cars => cars.where('make', '==', make).orderBy('created_at').limit(limit), 209 | // required for prev() - orderBy's must be in reverse 210 | queryReverse: cars => cars.where('make', '==', make).orderBy('created_at', 'desc').limit(limit) 211 | } 212 | }, 213 | cars() { 214 | return this.$fiery(fs.collection('cars'), this.carsOptions, 'cars') 215 | }, 216 | carsPager() { 217 | return this.$fiery.pager(this.cars) 218 | } 219 | }, 220 | methods: { 221 | next() { 222 | this.carsPager.next() // next 10 please, returns a promise which resolves when they're fetched 223 | 224 | // this.carsPager.index // which page we're on 225 | // this.carsPager.hasNext() // typically returns true since we don't really know - unless cars is empty 226 | // this.carsPager.next() // executes the query again but on the next 10 results. index++ 227 | // this.carsPager.hasPrev() // looks at pager.index to determines if there's a previous page 228 | // this.carsPager.prev() // executes the query again but on the previous 10 results. index-- 229 | } 230 | } 231 | }) 232 | ``` 233 | 234 | ### Real-time or once 235 | 236 | ```javascript 237 | new Vue({ 238 | inject: ['currentUserId'], 239 | fiery: true, // required to add this.$fiery to this component 240 | data() { 241 | const $fiery = this.$fiery 242 | return { 243 | // real-time is default, all you need to do is specify once: true to disable it 244 | cars: $fiery(fs.collection('cars'), {once: true}), // array populated once 245 | currentUser: $fiery(fs.collection('users').doc(this.currentUserId), {once: true}), // current user populated once 246 | } 247 | } 248 | }) 249 | ``` 250 | 251 | ### Data or computed 252 | 253 | For computed properties the third parameter to `$fiery` is required (it's best to just use the name of the property) 254 | 255 | ```javascript 256 | new Vue({ 257 | inject: ['currentUserId'], 258 | fiery: true, // required to add this.$fiery to this component 259 | data() { 260 | // data examples above 261 | return { 262 | limit: 25, 263 | status: 'unfinished' 264 | } 265 | }, 266 | computed: { 267 | currentUser() { 268 | const { currentUserId } = this; 269 | const options = {} 270 | return this.$fiery(fs.collection('users').doc(currentUserId), options, 'currentUser') // reactive and real-time 271 | }, 272 | todos() { 273 | // For computed results you need to get the dependent variables early so they are properly tracked. 274 | // The query/queryReversed callback may not be called immediately, so they must be pulled out. 275 | const { currentUserId, status, limit } = this; 276 | const options = { 277 | query: todos => todos 278 | .where('created_by', '==', currentUserId) 279 | .where('status', '==', status) 280 | .limit(limit) 281 | } 282 | return this.$fiery(fs.collection('todos'), options, 'todos') // reactive and real-time 283 | } 284 | } 285 | }) 286 | ``` 287 | 288 | ### Adding, updating, overwriting, removing 289 | 290 | ```javascript 291 | new Vue({ 292 | inject: ['currentUserId'], 293 | fiery: true, // required to add this.$fiery to this component 294 | data() { 295 | return { 296 | todos: this.$fiery(fs.collection('todos')) 297 | } 298 | }, 299 | computed: { 300 | currentUser() { 301 | const { currentUserId } = this; 302 | return this.$fiery(fs.collection('users').doc(currentUserId), {}, 'currentUser') 303 | } 304 | }, 305 | methods: { 306 | addTodo() { // COLLECTIONS STORED IN $fires 307 | this.$fires.todos.add({ 308 | name: 'Like fiery-vue', 309 | done: true 310 | }) 311 | // OR 312 | const savedTodo = this.$fiery.create(this.todos, { // you can pass this.todos or 'todos' 313 | name: 'Love fiery-vue', 314 | done: false 315 | }) 316 | }, 317 | updateUser() { 318 | this.$fiery.update(this.currentUser) 319 | }, 320 | updateUserEmailOnly() { 321 | this.$fiery.update(this.currentUser, ['email']) 322 | }, 323 | updateAny(data) { // any document can be passed, ex: this.todos[1], this.currentUser 324 | this.$fiery.update(data) 325 | }, 326 | overwrite(data) { // only fields present on data will exist on sync 327 | this.$fiery.sync(data) 328 | }, 329 | remove(data) { 330 | this.$fiery.remove(data) // removes sub collections as well 331 | this.$fiery.remove(data, true) // preserves sub collections 332 | }, 333 | removeName(todo) { 334 | this.$fiery.clear(data, 'name') // can also specify an array of props or sub collections 335 | } 336 | } 337 | }) 338 | ``` 339 | 340 | ### Sub-collections 341 | 342 | You can pass the same options to sub, nesting as deep as you want! 343 | 344 | ```javascript 345 | new Vue({ 346 | fiery: true, // required to add this.$fiery to this component 347 | data() { 348 | return { 349 | // this.todos[todoIndex].children[childIndex] 350 | todos: this.$fiery(fs.collection('todos'), { 351 | sub: { 352 | children: { // creates an array or map on each todo object: todo.children[] 353 | // once, map, etc 354 | query: children => children.orderBy('updated_at') 355 | } 356 | } 357 | }) 358 | } 359 | }, 360 | methods: { 361 | addChild(parent) { 362 | this.$fiery.ref(parent).collection('children').add( { /* values */ } ) 363 | // OR 364 | this.$fiery.ref(parent, 'children').add( { /* values */ } ) 365 | // OR 366 | const savedChild = this.$fiery.createSub(parent, 'children', { /* values */ } ) 367 | // OR 368 | const unsavedChild = this.$fiery.buildSub(parent, 'children', { /* values */ } ) 369 | }, 370 | clearChildren(parent) { 371 | this.$fiery.clear(parent, 'children') // clear the sub collection 372 | } 373 | } 374 | }) 375 | ``` 376 | 377 | ### Return instances of a class 378 | 379 | ```javascript 380 | function Todo() { 381 | 382 | } 383 | Todo.prototype = { 384 | markDone (byUser) { 385 | this.done = true 386 | this.updated_at = Date.now() 387 | this.updated_by = byUser.id 388 | } 389 | } 390 | 391 | new Vue({ 392 | fiery: true, // required to add this.$fiery to this component 393 | data() { 394 | return { 395 | // this.todos[todoIndex] instanceof Todo 396 | todos: this.$fiery(fs.collection('todos'), { 397 | type: Todo, 398 | // OR you can specify newDocument and do custom loading (useful for polymorphic data) 399 | newDocument: function(initialData) { 400 | var instance = new Todo() 401 | instance.callSomeMethod() 402 | return instance 403 | } 404 | }) 405 | } 406 | } 407 | }) 408 | ``` 409 | 410 | ### Active Record 411 | 412 | ```javascript 413 | // can be used with type, doesn't have to be 414 | function Todo() { 415 | 416 | } 417 | Todo.prototype = { 418 | markDone (byUser) { 419 | this.done = true 420 | this.updated_at = Date.now() 421 | this.updated_by = byUser.id 422 | this.$update() // injected 423 | } 424 | } 425 | 426 | new Vue({ 427 | fiery: true, // required to add this.$fiery to this component 428 | data() { 429 | return { 430 | todos: this.$fiery(fs.collection('todos'), { 431 | type: Todo, 432 | record: true 433 | // $sync, $update, $remove, $ref, $clear, $getChanges, $build, $create are functions added to every Todo instance 434 | }), 435 | todosCustom: this.$fiery(fs.collection('todos'), { 436 | record: true, 437 | recordOptions: { // which methods do you want added to every object, and with what method names? 438 | sync: 'sync', 439 | update: 'save', 440 | remove: 'destroy' 441 | // we don't want $ref, $clear, or $getChanges 442 | } 443 | }) 444 | } 445 | }, 446 | methods: { 447 | updateTodoAt(index) { 448 | // same as this.$fiery.update(this.todos[index]) 449 | this.todos[index].$update() 450 | }, 451 | saveTodoCustomAt(index) { 452 | // same as this.$fiery.update(this.todosCustom[index]) 453 | this.todosCustom[index].save() 454 | }, 455 | done(todo) { 456 | todo.markDone(this.currentUser) // assuming currentUser exists 457 | }, 458 | getChanges(todo) { 459 | // exclude array to compare entire document 460 | todo.$getChanges(['name', 'done']).then((changes) => { 461 | // changes.changed, changes.remote, changes.local 462 | }) 463 | } 464 | } 465 | }) 466 | ``` 467 | 468 | ### Save fields 469 | 470 | ```javascript 471 | new Vue({ 472 | fiery: true, // required to add this.$fiery to this component 473 | data() { 474 | return { 475 | todos: this.$fiery(fs.collection('todos'), { 476 | include: ['name', 'done'], // if specified, we ONLY send these fields on sync/update 477 | exclude: ['hidden'] // if specified here, will not be sent on sync/update 478 | }), 479 | } 480 | }, 481 | methods: { 482 | save(todo) { 483 | this.$fiery.update(todo) // sends name and done as configured above 484 | }, 485 | saveDone(todo) { 486 | this.$fiery.update(todo, ['done']) // only send this value if it exists 487 | }, 488 | saveOverride(todo) { 489 | this.$fiery.update(todo, ['hidden']) // ignores exclude and include when specified 490 | } 491 | } 492 | }) 493 | ``` 494 | 495 | ### Encode & decode properties 496 | 497 | ```javascript 498 | new Vue({ 499 | fiery: true, // required to add this.$fiery to this component 500 | data() { 501 | return { 502 | todos: this.$fiery(fs.collection('todos'), { 503 | // convert server values to local values 504 | decoders: { 505 | status(remoteValue, remoteData) { 506 | return remoteValue === 1 ? 'done' : (remoteValue === 2 ? 'started' : 'not started') 507 | } 508 | }, 509 | // convert local values to server values 510 | encoders: { 511 | status(localValue, localData) { 512 | return localValue === 'done' ? 1 : (localeValue === 'started' ? 2 : 0) 513 | } 514 | }, 515 | // optionally instead of individual decoders you can specify a function 516 | decode(remoteData) { 517 | // do some decoding, maybe do something special 518 | return remoteData 519 | } 520 | }) 521 | } 522 | } 523 | }) 524 | ``` 525 | 526 | ### Adding key and exists to object 527 | 528 | ```javascript 529 | new Vue({ 530 | fiery: true, // required to add this.$fiery to this component 531 | data() { 532 | return { 533 | todos: this.$fiery(fs.collection('todos'), {key: 'id', propExists: 'exists', exclude: ['id', 'exists']}) // must be excluded manually from saving if include is not specified 534 | } 535 | }, 536 | methods: { 537 | log(todo) { 538 | // todo.id exists now 539 | console.log(todo) 540 | } 541 | } 542 | }) 543 | ``` 544 | 545 | ### Sharing, extending, defining, and global options 546 | 547 | ```javascript 548 | // ==== Sharing ==== 549 | let Todo = { 550 | shared: true, // necessary for non-global or defined options that are used multiple times 551 | include: ['name', 'done', 'done_at'] 552 | } 553 | 554 | // ==== Extending ==== 555 | let TodoWithChildren = { 556 | shared: true 557 | extends: Todo, 558 | sub: { 559 | children: Todo 560 | } 561 | } 562 | 563 | // ==== Defining ==== 564 | FieryVue.define('post', { 565 | // shared is not necessary here 566 | include: ['title', 'content', 'tags'] 567 | }) 568 | 569 | // or multiple 570 | FieryVue.define({ 571 | comment: { 572 | include: ['author', 'content', 'posted_at', 'status'], 573 | sub: { 574 | replies: 'comment' // we can reference options by name now, even circularly 575 | } 576 | }, 577 | images: { 578 | include: ['url', 'tags', 'updated_at', 'title'] 579 | } 580 | }) 581 | 582 | // ==== Global ==== 583 | FieryVue.setGlobalOptions({ 584 | // lets make everything active record 585 | record: true, 586 | recordOptions: { 587 | update: 'save', // object.save(fields?) 588 | sync: 'sync', // object.sync(fields?) 589 | remove: 'remove', // object.remove() 590 | clear: 'clear', // object.clear(fields) 591 | create: 'create', // object.create(sub, initial?) 592 | build: 'build', // object.build(sub, initial?) 593 | ref: 'doc', // object.doc().collection('subcollection') 594 | getChanges: 'changes' // object.changes((changes, remote, local) => {}) 595 | } 596 | }) 597 | 598 | new Vue({ 599 | fiery: true, // required to add this.$fiery to this component 600 | data() { 601 | return { 602 | comments: this.$fiery(fs.collection('comment'), 'comment') // you can pass a named or Shared 603 | } 604 | } 605 | }) 606 | ``` 607 | 608 | ### Callbacks 609 | 610 | ```javascript 611 | new Vue({ 612 | fiery: true, // required to add this.$fiery to this component 613 | data() { 614 | return { 615 | todos: this.$fiery(fs.collection('todos'), { 616 | onSuccess: (todos) => {}, 617 | onError: (message) => {}, 618 | onRemove: () => {}, 619 | onMissing: () => {} // occurs for documents 620 | }) 621 | } 622 | } 623 | }) 624 | ``` 625 | 626 | ### Binding and Unbinding 627 | 628 | ```javascript 629 | new Vue({ 630 | fiery: true, // required to add this.$fiery to this component 631 | methods: { 632 | bindTodos() { 633 | this.todos = this.$fiery(fs.collection('todos')) 634 | }, 635 | unbindTodos() { 636 | this.$fiery.free(this.todos) 637 | } 638 | } 639 | }) 640 | ``` 641 | 642 | ## LICENSE 643 | [MIT](https://opensource.org/licenses/MIT) 644 | -------------------------------------------------------------------------------- /dist/fiery-vue.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FieryVue=t():e.FieryVue=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.plugin=t.destroy=t.link=t.init=void 0;var n=r(1);function o(){var e=this;this.$options.fiery&&(this.$fiery=n.default({removeNamed:function(t){e[t]=null},setProperty:function(t,r,n){e.$set(t,r,n)},removeProperty:function(t,r){e.$delete(t,r)}}),this.$fires=this.$fiery.sources)}function a(){this.$fiery&&this.$fiery.linkSources(this)}function i(){this.$fiery&&this.$fiery.destroy()}t.init=o,t.link=a,t.destroy=i,t.plugin={mergeOptions:n.mergeOptions,mergeStrategy:n.mergeStrategy,define:n.define,setGlobalOptions:n.setGlobalOptions,getOptions:n.getOptions,stats:n.stats,callbacks:n.callbacks,getCacheForData:n.getCacheForData,destroyGlobalCache:n.destroyGlobalCache,constants:n.constants,install:function(e){e.config.optionMergeStrategies.fiery=function(e,t){return e||t},e.mixin({beforeCreate:o,created:a,beforeDestroy:i})}},t.default=t.plugin},function(e,t,r){e.exports=function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=12)}([function(e,t,r){"use strict";function n(e){return"[object Object]"===Object.prototype.toString.call(e)}function o(e){return"function"==typeof e}function a(e){return"string"==typeof e}function i(e){return e&&e instanceof Array}function s(e){return e&&e instanceof Date}function c(e){return void 0!==e}function u(e){return"number"==typeof e&&isFinite(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.isObject=n,t.isFunction=o,t.isString=a,t.isArray=i,t.isDate=s,t.isDefined=c,t.isNumber=u,t.parseDate=function(e){return s(e)?e:e&&o(e.toDate)?e.toDate():u(e)?new Date(e):e},t.coalesce=function(e,t){return c(e)?e:t},t.isCollectionSource=function(e){return!!e.where},t.getFields=function(e,t){return e?a(e)?[e]:e:t},t.forEach=function(e,t){if(i(e)){for(var r=0;r0)for(var c in t.sub)l(t,c)||(s.callbacks.onSubDestroy(t.data,c,t),o.closeEntry(t.sub[c],!0))}}function l(e,t){for(var r=e.entries,n=0;n=0;o--){var a=n[o];a.instance===t&&f(a,e)}r&&e.uses<=0&&g(e)}}function g(e){for(var r=e.entries,n=0;n=0;o--)r[o]in n?r.splice(o,1):n[r[o]]=!0;return r}},exclude:function(e,r){var n=t.mergeStrategy.concat(e,r);if(!n&&e&&r){var o={},i=a.isArray(r),s=a.isArray(e);return a.forEach(r,(function(e,t){return e?o[i?e:t]=!0:0})),a.forEach(e,(function(e,t){return e?o[s?e:t]=!0:0})),o}return n}},t.mergeOptions={extends:t.mergeStrategy.ignore,id:t.mergeStrategy.ignore,parent:t.mergeStrategy.ignore,shared:t.mergeStrategy.ignore,vm:t.mergeStrategy.ignore,key:t.mergeStrategy.replace,query:t.mergeStrategy.replace,map:t.mergeStrategy.replace,once:t.mergeStrategy.replace,stream:t.mergeStrategy.replace,streamInitial:t.mergeStrategy.replace,streamMore:t.mergeStrategy.replace,type:t.mergeStrategy.replace,nullifyMissing:t.mergeStrategy.replace,newDocument:t.mergeStrategy.replace,newCollection:t.mergeStrategy.replace,decode:t.mergeStrategy.replace,decoders:t.mergeStrategy.shallow,encoders:t.mergeStrategy.shallow,record:t.mergeStrategy.replace,recordOptions:t.mergeStrategy.replace,recordFunctions:t.mergeStrategy.replace,events:t.mergeStrategy.replace,eventsOptions:t.mergeStrategy.replace,triggerEvent:t.mergeStrategy.replace,propValue:t.mergeStrategy.replace,propExists:t.mergeStrategy.replace,propParent:t.mergeStrategy.replace,onceOptions:t.mergeStrategy.replace,liveOptions:t.mergeStrategy.replace,include:t.mergeStrategy.concat,exclude:t.mergeStrategy.exclude,timestamps:t.mergeStrategy.concat,onError:t.mergeStrategy.replace,onSuccess:t.mergeStrategy.replace,onMissing:t.mergeStrategy.replace,onRemove:t.mergeStrategy.replace,onMutate:t.mergeStrategy.replace,onPromise:t.mergeStrategy.replace,sub:t.mergeStrategy.shallow}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(14),o=r(10),a=r(11),i=r(17);function s(e){return(e.source.where?e.options.stream?i.default:e.options.map?o.default:a.default:n.default)(e)}t.factory=s,t.default=s},function(e,t,r){"use strict";var n=this&&this.__awaiter||function(e,t,r,n){return new(r||(r=Promise))((function(o,a){function i(e){try{c(n.next(e))}catch(e){a(e)}}function s(e){try{c(n.throw(e))}catch(e){a(e)}}function c(e){e.done?o(e.value):new r((function(t){t(e.value)})).then(i,s)}c((n=n.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(a){return function(s){return function(a){if(r)throw new TypeError("Generator is already executing.");for(;i;)try{if(r=1,n&&(o=2&a[0]?n.return:a[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,a[1])).done)return o;switch(n=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,n=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&(c.stats.updates++,c.stats.writes++,p.push(f.update(g))),u.callbacks.onClear(e,o),Promise.all(p)}return u.callbacks.onInvalidOperation(e,"clear"),Promise.reject("The given data is out of scope and cannot be operated on.")},t.getChanges=function(e,t,r){return n(this,void 0,void 0,(function(){var n,f,l,d,p;return o(this,(function(o){return(n=s.getCacheForData(e))&&n.ref?(f=i.isFunction(t)?void 0:i.getFields(t),l=(f?r:t)||i.isEqual,d=n.firstEntry.options,p=a.encodeData(e,d,f),c.stats.reads++,u.callbacks.onGetChanges(e,n,f),[2,n.ref.get().then((function(e){var t=a.parseDocument(e,d),r={},n={},o=!1;for(var i in p){var s=t[i],c=p[i];l(s,c)||(o=!0,r[i]=s,n[i]=c)}return Promise.resolve({changed:o,remote:r,local:n})}))]):(u.callbacks.onInvalidOperation(e,"getChanges"),[2,Promise.reject("The given data is out of scope and cannot be operated on.")])}))}))},t.ref=function(e,t){var r=s.getCacheForData(e);if(r&&r.ref){var n=r.ref;return t?n.collection(t):n}throw"The given data is out of scope and cannot be referenced."},t.create=function(e,t){var r=this.build(e,t);return r&&this.sync(r),r},t.createSub=function(e,t,r){var n=this.buildSub(e,t,r);return n&&this.sync(n),n},t.build=function(e,t){var r=this.entryFor(e);if(r)return d(r.source,r,t);throw"Cannot build "+e+NaN},t.buildSub=function(e,t,r){var n=s.getCacheForData(e);if(n&&n.ref&&t in n.sub){var o=n.sub[t];return d(n.ref.collection(t),o,r)}throw"Cannot build in the sub collection "+t+NaN},t.buildFromCollection=d},function(e,t,r){"use strict";var n=this&&this.__assign||Object.assign||function(e){for(var t,r=1,n=arguments.length;rt&&e.splice(t,e.length-t)}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(5),o=r(1),a=r(2),i=r(3);function s(e){var t=e.source,r=e.options,n=o.getCacheForReference(e,t,!0),i=e.target,s=!1,u=function(){},f=function(){},l=function(t){return r.onMutate((function(){return c(n,e,t),n.data})),s=!t.exists,u(n.data),n.data};return i&&i!==n.data&&o.removeDataFromEntry(e,i),e.target=n.data,a.stats.queries++,r.once?e.promise=t.get(r.onceOptions).then(l).catch(r.onError):(e.promise=new Promise((function(e,t){u=e,f=t})),e.off=t.onSnapshot(r.liveOptions,l,(function(e){f(e),r.onError(e)}))),r.onPromise(e.promise),s&&r.nullifyMissing?null:e.target}function c(e,t,r){var a=t.options,s=t.instance.system;r.exists?(n.refreshData(e,r,t),a.onSuccess(e.data),i.callbacks.onDocumentUpdate(e.data,t)):(a.propExists&&s.setProperty(e.data,a.propExists,!1),null===e.exists?(i.callbacks.onDocumentMissing(e.data,t),a.triggerEvent(e.data,"missing")):a.triggerEvent(e.data,"remove"),e.exists=!1,a.nullifyMissing&&(o.destroyCache(e),t.name&&s.removeNamed(t.name)))}t.factory=s,t.handleDocumentUpdate=c,t.default=s},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.stores={keyNext:0,map:{},idToKey:{}},t.getStoreKey=function(e){var r=e.firestore,n=r.app.name,o=t.stores.idToKey[n];return o||(o=++t.stores.keyNext,t.stores.map[o]=r,t.stores.idToKey[n]=o),o}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(0);t.getPager=function(e){var t;return{index:0,hasQuery:function(){return!(!e.query||!e.requery)},hasData:function(){var t=e.target;if(n.isArray(t))return t.length>0;if(n.isObject(t))for(var r in t)return!0;return!1},hasNext:function(){return this.hasQuery()&&this.hasData()},hasPrev:function(){return this.hasQuery()&&this.index>0},next:function(){var r=e.query,n=e.requery,o=e.last,a=e.first,i=e.off;return r&&n&&o&&this.hasData()&&(i&&i(),delete e.off,delete e.last,this.index++,t=a,n(r.startAfter(o)),e.promise)?e.promise:Promise.reject("The pager could not execute next")},prev:function(){var r=e.query,n=e.requery,o=e.first,a=e.off,i=e.options;return r&&n&&(o||t)&&this.index>0&&(a&&a(),delete e.off,this.index--,o&&i.queryReverse?(delete e.first,i.queryReverse(e.source).startAfter(o).get(i.onceOptions).then((function(e){var t=e.docs[e.docs.length-1];n(r.startAt(t))}))):(n(r.startAt(t)),t=void 0),e.promise)?e.promise:Promise.reject("The pager could not execute prev")}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(4),o=r(2),a=r(11),i=r(10);function s(e){var t=e.options;if(!t.query)throw"query is required for streaming";if(!t.streamInitial)throw"streamInitial is required for streaming";var r=t.query(e.source);return e.requery=function(r){var n=t.map?i.getInitialHandler(e):a.getInitialHandler(e),s=t.streamInitial;e.target||(e.target=t.newCollection()),o.stats.queries++,e.hasMore=!0,e.promise=r.limit(s).get(t.onceOptions).then((function(t){o.stats.queries++,e.hasMore=t.size>=s,n(t),c(e,n,r)})).catch(t.onError)},e.more=function(s){var u=s||t.streamMore;if(!u||u<0)throw"streamMore is required for streaming";if(!e.last||!e.hasMore)return Promise.reject("There are no more results to load.");var f=t.map?i.getInitialHandler(e):a.getInitialHandler(e);return r.startAfter(e.last).limit(u).get(t.onceOptions).then((function(t){o.stats.queries++,e.hasMore=t.size>=u,n.updatePointers(e,t),c(e,f,r)})).catch(t.onError)},e.requery(e.query=r),e.target}function c(e,t,r){var n=e.options,o=function(){},s=function(){};e.promise=new Promise((function(e,t){o=e,s=t}));var c=n.map?i.getLiveHandler(e,t,o):a.getLiveHandler(e,t,o);e.off&&e.off(),e.last&&(r=r.endAt(e.last)),e.off=r.onSnapshot(n.liveOptions,c,(function(e){s(e),n.onError(e)})),n.onPromise(e.promise)}t.factory=s,t.default=s}])}]).default})); -------------------------------------------------------------------------------- /dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { define, setGlobalOptions, destroyGlobalCache, getCacheForData, getOptions, FieryInstance, FierySources } from 'fiery-data'; 2 | export interface FieryVue { 3 | $fiery: FieryInstance; 4 | $fires: FierySources; 5 | [prop: string]: any; 6 | $delete: (object: any, key: string | number) => any; 7 | $set: (object: any, key: string | number, value?: any) => any; 8 | } 9 | export declare function init(this: FieryVue): void; 10 | export declare function link(this: FieryVue): void; 11 | export declare function destroy(this: FieryVue): void; 12 | export declare const plugin: { 13 | mergeOptions: import("fiery-data").FieryMergeStrategies; 14 | mergeStrategy: import("fiery-data").FieryMergeStrategies; 15 | define: typeof define; 16 | setGlobalOptions: typeof setGlobalOptions; 17 | getOptions: typeof getOptions; 18 | stats: { 19 | queries: number; 20 | reads: number; 21 | deletes: number; 22 | updates: number; 23 | sets: number; 24 | writes: number; 25 | }; 26 | callbacks: { 27 | onInvalidOperation(_data: import("fiery-data").FieryData, _operation: string): void; 28 | onUpdate(_data: import("fiery-data").FieryData, _values: import("fiery-data").FieryData, _cache: import("fiery-data").FieryCacheEntry): void; 29 | onSet(_data: import("fiery-data").FieryData, _values: import("fiery-data").FieryData, _cache: import("fiery-data").FieryCacheEntry): void; 30 | onDelete(_data: import("fiery-data").FieryData, _cache: import("fiery-data").FieryCacheEntry): void; 31 | onClear(_data: import("fiery-data").FieryData, _props: string[]): void; 32 | onGetChanges(_data: import("fiery-data").FieryData, _cache: import("fiery-data").FieryCacheEntry, _fields?: string[] | undefined): void; 33 | onRefresh(_data: import("fiery-data").FieryData, _cachedOnly?: boolean | undefined): void; 34 | onBuild(_data: import("fiery-data").FieryData, _cache: import("fiery-data").FieryCacheEntry): void; 35 | onCacheCreate(_cache: import("fiery-data").FieryCacheEntry): void; 36 | onCacheDestroy(_cache: import("fiery-data").FieryCacheEntry): void; 37 | onSubCreate(_data: import("fiery-data").FieryData, _sub: string, _cache: import("fiery-data").FieryCacheEntry): void; 38 | onSubDestroy(_data: import("fiery-data").FieryData, _sub: string, _cache: import("fiery-data").FieryCacheEntry): void; 39 | onCollectionAdd(_data: import("fiery-data").FieryData, _target: import("fiery-data").FieryTarget, _entry: import("fiery-data").FieryEntry): void; 40 | onCollectionRemove(_data: import("fiery-data").FieryData, _target: import("fiery-data").FieryTarget, _entry: import("fiery-data").FieryEntry): void; 41 | onCollectionModify(_data: import("fiery-data").FieryData, _target: import("fiery-data").FieryTarget, _entry: import("fiery-data").FieryEntry): void; 42 | onCollectionChanged(_target: import("fiery-data").FieryTarget, _entry: import("fiery-data").FieryEntry): void; 43 | onDocumentUpdate(_data: import("fiery-data").FieryData, _entry: import("fiery-data").FieryEntry): void; 44 | onDocumentMissing(_data: import("fiery-data").FieryData, _entry: import("fiery-data").FieryEntry): void; 45 | onInstanceCreate(_instance: FieryInstance): void; 46 | onInstanceDestroy(_instance: FieryInstance): void; 47 | }; 48 | getCacheForData: typeof getCacheForData; 49 | destroyGlobalCache: typeof destroyGlobalCache; 50 | constants: { 51 | PROP_VALUE: string; 52 | PROP_UID: string; 53 | UID_SEPARATOR: string; 54 | ENTRY_SEPARATOR: string; 55 | PATH_SEPARATOR: string; 56 | RECORD_OPTIONS: { 57 | refresh: string; 58 | sync: string; 59 | update: string; 60 | save: string; 61 | remove: string; 62 | ref: string; 63 | clear: string; 64 | build: string; 65 | create: string; 66 | getChanges: string; 67 | }; 68 | EVENTS_OPTIONS: { 69 | create: string; 70 | missing: string; 71 | update: string; 72 | remove: string; 73 | destroy: string; 74 | }; 75 | }; 76 | install(Vue: any): void; 77 | }; 78 | export default plugin; 79 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 36 | 37 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fiery-vue", 3 | "version": "0.10.2", 4 | "author": "Philip Diffenderfer", 5 | "description": "A Typescript/JS library for Vue and Google Firebase Firestore", 6 | "license": "MIT", 7 | "main": "dist/fiery-vue.js", 8 | "typings": "dist/types/index.d.ts", 9 | "scripts": { 10 | "dev": "webpack --mode development", 11 | "build": "webpack --mode production", 12 | "test": "mocha -r ts-node/register tests/**/*.ts" 13 | }, 14 | "keywords": [ 15 | "fiery", 16 | "firebase", 17 | "firestore", 18 | "cloud", 19 | "cloud firestore", 20 | "realtime", 21 | "vue", 22 | "fiery", 23 | "fiery-data" 24 | ], 25 | "dependencies": { 26 | "fiery-data": "1.0.0" 27 | }, 28 | "devDependencies": { 29 | "@types/chai": "^4.1.4", 30 | "@types/mocha": "^5.2.5", 31 | "@types/node": "^10.7.1", 32 | "@types/webpack": "^4.4.10", 33 | "chai": "^4.1.2", 34 | "cross-env": "^3.0.0", 35 | "css-loader": "^0.25.0", 36 | "fiery-firebase-memory": "0.2.1", 37 | "file-loader": "^0.9.0", 38 | "mocha": "^5.2.0", 39 | "nyc": "^12.0.2", 40 | "ts-loader": "^4.0.0", 41 | "ts-node": "^7.0.1", 42 | "typescript": "^3.1.0-dev.20180825", 43 | "typings": "^2.1.1", 44 | "vue": "^2.5.17", 45 | "webpack": "^4.16.5", 46 | "webpack-cli": "^3.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import getInstance, { 3 | constants, define, setGlobalOptions, mergeStrategy, mergeOptions, stats, callbacks, destroyGlobalCache, getCacheForData, destroyCache, getOptions, FieryInstance, FierySources 4 | } from 'fiery-data' 5 | 6 | 7 | export interface FieryVue 8 | { 9 | $fiery: FieryInstance 10 | 11 | $fires: FierySources 12 | 13 | [prop: string]: any 14 | 15 | $delete: (object: any, key: string | number) => any 16 | 17 | $set: (object: any, key: string | number, value?: any) => any 18 | } 19 | 20 | export function init (this: FieryVue) 21 | { 22 | if (!this.$options.fiery) 23 | { 24 | return 25 | } 26 | 27 | this.$fiery = getInstance( 28 | { 29 | removeNamed: (name: string) => 30 | { 31 | this[name] = null 32 | }, 33 | setProperty: (target: any, property: string, value: any) => 34 | { 35 | this.$set(target, property, value) 36 | }, 37 | removeProperty: (target: any, property: string) => 38 | { 39 | this.$delete(target, property) 40 | } 41 | }) 42 | 43 | this.$fires = this.$fiery.sources 44 | } 45 | 46 | export function link (this: FieryVue) 47 | { 48 | if (this.$fiery) 49 | { 50 | this.$fiery.linkSources(this) 51 | } 52 | } 53 | 54 | export function destroy (this: FieryVue) 55 | { 56 | if (this.$fiery) 57 | { 58 | this.$fiery.destroy() 59 | } 60 | } 61 | 62 | export const plugin = 63 | { 64 | mergeOptions, 65 | 66 | mergeStrategy, 67 | 68 | define, 69 | 70 | setGlobalOptions, 71 | 72 | getOptions, 73 | 74 | stats, 75 | 76 | callbacks, 77 | 78 | getCacheForData, 79 | 80 | destroyGlobalCache, 81 | 82 | constants, 83 | 84 | install (Vue: any) 85 | { 86 | Vue.config.optionMergeStrategies.fiery = (a, b) => { 87 | return a || b 88 | } 89 | 90 | Vue.mixin({ 91 | beforeCreate: init, 92 | created: link, 93 | beforeDestroy: destroy 94 | }) 95 | } 96 | } 97 | 98 | export default plugin 99 | -------------------------------------------------------------------------------- /tests/reactive.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// 4 | /// 5 | 6 | import { getStore, getStored } from './util' 7 | import { expect } from 'chai' 8 | import FieryVue from '../src' 9 | import * as Vue from 'vue' 10 | 11 | describe('reactive', function() 12 | { 13 | const VueTest: any = Vue 14 | 15 | before(function() { 16 | VueTest.use(FieryVue) 17 | }) 18 | 19 | it('recomputes', function() 20 | { 21 | const fs = getStore('reactive recomputes', { 22 | 'todos/1': { name: 'T1', done: false }, 23 | 'todos/2': { name: 'T2', done: true }, 24 | 'todos/3': { name: 'T3', done: false }, 25 | 'todos/4': { name: 'T4', done: true }, 26 | 'todos/5': { name: 'T5', done: true } 27 | }) 28 | 29 | class Todo { 30 | name: string = '' 31 | done: boolean = false 32 | } 33 | 34 | const TodoOptions = { 35 | shared: true, 36 | type: Todo, 37 | include: ['name', 'done'] 38 | } 39 | 40 | const vm = new VueTest({ 41 | fiery: true, 42 | beforeCreate() { 43 | this.recomputed = 0 44 | }, 45 | data() { 46 | return { 47 | done: true 48 | } 49 | }, 50 | computed: { 51 | results(): Todo[] { 52 | this.recomputed++ 53 | const options = { 54 | extends: TodoOptions, 55 | query: q => q.where('done', '==', this.done) 56 | } 57 | return this.$fiery(fs.collection('todos'), options, 'results') 58 | } 59 | } 60 | }) 61 | 62 | expect(vm.recomputed).to.equal(0) 63 | expect(vm.results.length).to.equal(3) 64 | expect(vm.recomputed).to.equal(1) 65 | 66 | vm.done = false 67 | 68 | expect(vm.results.length).to.equal(2) 69 | expect(vm.recomputed).to.equal(2) 70 | 71 | vm.done = true 72 | 73 | expect(vm.results.length).to.equal(3) 74 | expect(vm.recomputed).to.equal(3) 75 | 76 | vm.$destroy() 77 | 78 | expect(vm.recomputed).to.equal(3) 79 | }) 80 | 81 | }) 82 | -------------------------------------------------------------------------------- /tests/util.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// 4 | 5 | import * as firebase from 'firebase' 6 | import firebasemock from 'fiery-firebase-memory' 7 | 8 | 9 | export function getStore (name: string, map?: any): firebase.firestore.Firestore 10 | { 11 | const app = firebasemock.initializeApp({}, name) 12 | const fs = firebasemock.firestore(app) 13 | 14 | if (map) 15 | { 16 | for (var path in map) 17 | { 18 | const data: any = fs.dataAt(path, true) 19 | const copy: any = map[path] 20 | 21 | for (var prop in copy) 22 | { 23 | if (copy.hasOwnProperty(prop)) 24 | { 25 | data[prop] = copy[prop] 26 | } 27 | } 28 | } 29 | } 30 | 31 | return (fs) as firebase.firestore.Firestore 32 | } 33 | 34 | export function getStored (fs: firebase.firestore.Firestore, data: any): any 35 | { 36 | const uid: string = data['.uid'] 37 | 38 | return (fs)._docs[uid.substring(uid.indexOf('///') + 3)] 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "es6" 9 | ], 10 | "experimentalDecorators": true, 11 | "noImplicitAny": false, 12 | "noImplicitThis": false, 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "allowSyntheticDefaultImports": true, 17 | "allowJs": false, 18 | "sourceMap": true, 19 | "declaration": true, 20 | "declarationDir": "types", 21 | "outDir": "dist" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/ 11 | } 12 | ] 13 | }, 14 | resolve: { 15 | extensions: [ '.ts' ] 16 | }, 17 | output: { 18 | filename: 'fiery-vue.js', 19 | path: path.resolve(__dirname, 'dist'), 20 | library: 'FieryVue', 21 | libraryTarget: 'umd', 22 | libraryExport: 'default' 23 | } 24 | }; 25 | --------------------------------------------------------------------------------