├── .gitignore ├── .travis.yml ├── .versions ├── LICENSE ├── README.md ├── lib.js ├── package.js ├── tests.coffee ├── tests_client.coffee ├── tests_client.css └── tests_client.html /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .idea 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | sudo: required 5 | before_install: 6 | - "curl -L http://git.io/ejPSng | /bin/sh" 7 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | babel-compiler@7.3.4 3 | babel-runtime@1.3.0 4 | base64@1.0.11 5 | binary-heap@1.0.11 6 | blaze@2.3.3 7 | blaze-tools@1.0.10 8 | boilerplate-generator@1.6.0 9 | caching-compiler@1.2.1 10 | caching-html-compiler@1.0.7 11 | callback-hook@1.1.0 12 | check@1.3.1 13 | coffeescript@2.4.1 14 | coffeescript-compiler@2.4.1 15 | ddp@1.4.0 16 | ddp-client@2.3.3 17 | ddp-common@1.4.0 18 | ddp-server@2.3.0 19 | deps@1.0.12 20 | diff-sequence@1.1.1 21 | dynamic-import@0.5.1 22 | ecmascript@0.12.7 23 | ecmascript-runtime@0.7.0 24 | ecmascript-runtime-client@0.8.0 25 | ecmascript-runtime-server@0.7.1 26 | ejson@1.1.0 27 | fetch@0.1.1 28 | geojson-utils@1.0.10 29 | html-tools@1.0.11 30 | htmljs@1.0.11 31 | id-map@1.1.0 32 | inter-process-messaging@0.1.0 33 | jquery@1.11.10 34 | local-test:peerlibrary:computed-field@0.10.0 35 | logging@1.1.20 36 | meteor@1.9.3 37 | minimongo@1.4.5 38 | modern-browsers@0.1.4 39 | modules@0.13.0 40 | modules-runtime@0.10.3 41 | mongo@1.6.2 42 | mongo-decimal@0.1.1 43 | mongo-dev-server@1.1.0 44 | mongo-id@1.0.7 45 | npm-mongo@3.1.2 46 | observe-sequence@1.0.16 47 | ordered-dict@1.1.0 48 | peerlibrary:classy-test@0.4.0 49 | peerlibrary:computed-field@0.10.0 50 | promise@0.11.2 51 | random@1.1.0 52 | reactive-var@1.0.11 53 | reload@1.3.0 54 | retry@1.1.0 55 | routepolicy@1.1.0 56 | socket-stream-client@0.2.2 57 | spacebars@1.0.13 58 | spacebars-compiler@1.1.0 59 | templating@1.2.15 60 | templating-compiler@1.2.15 61 | templating-runtime@1.2.15 62 | templating-tools@1.1.1 63 | tinytest@1.1.0 64 | tracker@1.2.0 65 | underscore@1.0.10 66 | webapp@1.7.3 67 | webapp-hashing@1.0.9 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, The PeerLibrary Project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the PeerLibrary Project nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reactively computed field for Meteor 2 | ==================================== 3 | 4 | Reactively computed field for [Meteor](https://meteor.com/) provides an easy way to define a reactive variable 5 | which gets value from another reactive function. This allows you to minimize propagation of a reactive change 6 | because if the reactive function returns the value equal to the previous value, reactively computed field 7 | will not invalidate reactive contexts where it is used. 8 | 9 | ```javascript 10 | var field = new ComputedField(function () { 11 | var sum = 0; 12 | collection.find({}, {fields: {value: 1}}).forEach(function (doc) { 13 | sum += doc.value; 14 | }); 15 | return sum; 16 | }); 17 | 18 | console.log(field()); 19 | ``` 20 | 21 | You get current value by calling the field as a function. A reactive dependency is then registered. 22 | This is useful when you are assigning them to objects because they behave like object methods. You can also access 23 | them in [Blaze Components](https://github.com/peerlibrary/meteor-blaze-components) template by simply doing 24 | `{{field}}` when they are assigned to the component. 25 | 26 | When computed field is created inside a Blaze template (in `onCreated` or `onRendered`) it will automatically detect this 27 | and allow use of `Template.instance()` and `Template.currentData()` inside a function. 28 | 29 | Optionally, you can pass a custom equality function: 30 | 31 | ```javascript 32 | new ComputedField(reactiveFunction, function (a, b) {return a === b}); 33 | ``` 34 | 35 | Adding this package to your [Meteor](http://www.meteor.com/) application adds the `ComputedField` constructor into 36 | the global scope. 37 | 38 | Both client and server side. 39 | 40 | Installation 41 | ------------ 42 | 43 | ``` 44 | meteor add peerlibrary:computed-field 45 | ``` 46 | 47 | Arguments 48 | --------- 49 | 50 | `ComputedField` constructor accepts the following arguments: 51 | 52 | * `reactiveFunction` – a reactive function which should return a computed field's value 53 | * `equalityFunction` – a function to compare the return value to see if it has changed and if the computed field 54 | should be invalidated; by default it is equal to `ReactiveVar._isEqual`, which means that only primitive values are compared 55 | by value, and all other are always different 56 | * `dontStop` – pass `true` to prevent computed field from automatically stopping internal autorun once the field is not used 57 | anymore inside any reactive context (no reactive dependency has been registered on the computed field); sometimes 58 | you do not want autorun to be stopped in this way because you are not using a computed field inside a reactive context at all 59 | 60 | You can pass `dontStop` as a second argument as well, skipping the `equalityFunction`. 61 | 62 | Extra field methods 63 | ------------------- 64 | 65 | The computed field is a function, but it has also two extra methods which you probably do not really need, because 66 | computed field should do the right thing automatically. 67 | 68 | ### `field.stop()` ### 69 | 70 | Internally, computed field creates an autorun which should not run once it is not needed anymore. 71 | 72 | If you create a computed field inside another autorun, then you do not have to worry and computed field's autorun 73 | will be stopped and cleaned automatically every time outside computation gets invalidated. 74 | 75 | If you create a computed field outside an autorun, autorun will be automatically stopped when there will 76 | be no reactive dependencies anymore on the computed field. So the previous example could be written as well as: 77 | 78 | ```javascript 79 | var result = new ComputedField(frequentlyInvalidatedButCheap); 80 | Tracker.autorun(function () { 81 | expensiveComputation(result()); 82 | }); 83 | ``` 84 | 85 | Moreover, if you create a computed field inside a Blaze template (in `onCreated` or `onRendered`) it will automatically 86 | detect this and it will use template's autorun which means that autorun will be stopped automatically when 87 | template instance gets destroyed. 88 | 89 | Despite all this, the `stop()` method is provided for you if you want to explicitly stop and clean the field. Remember, 90 | getting a value again afterwards will start internal autorun again. 91 | 92 | ### `field.flush()` ### 93 | 94 | Sometimes you do not want to wait for global flush to happen to recompute the value. You can call `flush()` on the 95 | field to force immediate recomputation. But the same happens when you access the field value. If the value is 96 | invalidated, it will be automatically first recomputed and then returned. `flush()` is in this case called for you 97 | before returning you the field value. In both cases, calling `flush()` directly or accessing the field value, 98 | recomputation happens only if it is needed. 99 | 100 | Examples 101 | -------- 102 | 103 | Computed field is useful if you want to minimize propagation of reactivity. For example: 104 | 105 | ```javascript 106 | Tracker.autorun(function () { 107 | var result = new ComputedField(frequentlyInvalidatedButCheap); 108 | expensiveComputation(result()); 109 | }); 110 | ``` 111 | 112 | In this example `frequentlyInvalidatedButCheap` is a function which depends on reactive variables which frequently 113 | change, but computing with them is cheap, and resulting value **rarely changes**. On the other hand, 114 | `expensiveComputation` is a function which is expensive and should be called only when `result` value changes. 115 | Example `frequentlyInvalidatedButCheap` could for example be determining if current mouse position is inside a 116 | rectangle on canvas or outside. Every time mouse is moved, it should be recomputed, but result changes only when 117 | mouse moves over the rectangle's border. On the other hand, `expensiveComputation` could be an expensive drawing 118 | operation which draws a rectangle differently if the mouse position is inside or outside of the rectangle. You do 119 | not want to redraw on every mouse position change. 120 | 121 | Even if you create a computed field outside an autorun, autorun will be automatically stopped when there will 122 | be no reactive dependencies anymore on the computed field. So the previous example could be written as well as: 123 | 124 | ```javascript 125 | var result = new ComputedField(frequentlyInvalidatedButCheap); 126 | Tracker.autorun(function () { 127 | expensiveComputation(result()); 128 | }); 129 | ``` 130 | 131 | You can use computed field to attach a field to a [Blaze Component](https://github.com/peerlibrary/meteor-blaze-components): 132 | 133 | ```js 134 | class ExampleComponent extends BlazeComponent { 135 | onCreated() { 136 | super.onCreated(); 137 | this.sum = new ComputedField(() => { 138 | let sum = 0; 139 | collection.find({}, {fields: {value: 1}}).forEach((doc) => { 140 | sum += doc.value; 141 | }); 142 | return sum; 143 | }); 144 | } 145 | } 146 | 147 | ExampleComponent.register('ExampleComponent'); 148 | ``` 149 | 150 | And now you can access this field inside a component without knowing that it will change DOM only when the sum 151 | itself changes, and not at every change of any document: 152 | 153 | ```handlebars 154 | 157 | ``` 158 | 159 | Related projects 160 | ---------------- 161 | 162 | * [meteor-isolate-value](https://github.com/awwx/meteor-isolate-value) – an obsolete package with alternative way of 163 | minimizing reactivity propagation 164 | * [meteor-embox-value](https://github.com/3stack-software/meteor-embox-value) - more or less the same as computed 165 | field, just different implementation, but embox-value does not stop autoruns automatically by default, only when run 166 | lazily; computed field allows you to use `instanceof ComputedField` to determine if a field is a computed field; 167 | embox-value package has special provisioning for better integration with Blaze templates, but computed field does 168 | not need that because of the auto-stopping feature 169 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | export class ComputedField { 2 | constructor(func, equalsFunc, dontStop) { 3 | // To support passing boolean as the second argument. 4 | if (_.isBoolean(equalsFunc)) { 5 | dontStop = equalsFunc; 6 | equalsFunc = null; 7 | } 8 | 9 | let handle = null; 10 | let lastValue = null; 11 | 12 | // TODO: Provide an option to prevent using view's autorun. 13 | // One can wrap code with Blaze._withCurrentView(null, code) to prevent using view's autorun for now. 14 | let autorun; 15 | const currentView = Package.blaze && Package.blaze.Blaze && Package.blaze.Blaze.currentView 16 | if (currentView) { 17 | if (currentView._isInRender) { 18 | // Inside render we cannot use currentView.autorun directly, so we use our own version of it. 19 | // This allows computed fields to be created inside Blaze template helpers, which are called 20 | // the first time inside render. While currentView.autorun is disallowed inside render because 21 | // autorun would be recreated for reach re-render, this is exactly what computed field does 22 | // anyway so it is OK for use to use autorun in this way. 23 | autorun = function (f) { 24 | const templateInstanceFunc = Package.blaze.Blaze.Template._currentTemplateInstanceFunc; 25 | 26 | const comp = Tracker.autorun((c) => { 27 | Package.blaze.Blaze._withCurrentView(currentView, () => { 28 | Package.blaze.Blaze.Template._withTemplateInstanceFunc(templateInstanceFunc, () => { 29 | f.call(currentView, c); 30 | }) 31 | }); 32 | }); 33 | 34 | const stopComputation = () => { 35 | comp.stop(); 36 | }; 37 | currentView.onViewDestroyed(stopComputation); 38 | comp.onStop(() => { 39 | currentView.removeViewDestroyedListener(stopComputation); 40 | }); 41 | 42 | return comp; 43 | }; 44 | 45 | } 46 | else { 47 | autorun = (f) => { 48 | return currentView.autorun(f); 49 | } 50 | } 51 | } 52 | else { 53 | autorun = Tracker.autorun; 54 | } 55 | 56 | const startAutorun = function () { 57 | handle = autorun(function (computation) { 58 | const value = func(); 59 | 60 | if (!lastValue) { 61 | lastValue = new ReactiveVar(value, equalsFunc); 62 | } 63 | else { 64 | lastValue.set(value); 65 | } 66 | 67 | if (!dontStop) { 68 | Tracker.afterFlush(function () { 69 | // If there are no dependents anymore, stop the autorun. We will run 70 | // it again in the getter's flush call if needed. 71 | if (!lastValue.dep.hasDependents()) { 72 | getter.stop(); 73 | } 74 | }); 75 | } 76 | }); 77 | 78 | // If something stops our autorun from the outside, we want to know that and update internal state accordingly. 79 | // This means that if computed field was created inside an autorun, and that autorun is invalided our autorun is 80 | // stopped. But then computed field might be still around and it might be asked again for the value. We want to 81 | // restart our autorun in that case. Instead of trying to recompute the stopped autorun. 82 | if (handle.onStop) { 83 | handle.onStop(() => { 84 | handle = null; 85 | }); 86 | } 87 | else { 88 | // XXX COMPAT WITH METEOR 1.1.0 89 | const originalStop = handle.stop; 90 | handle.stop = function () { 91 | if (handle) { 92 | originalStop.call(handle); 93 | } 94 | handle = null; 95 | }; 96 | } 97 | }; 98 | 99 | startAutorun(); 100 | 101 | const getter = function () { 102 | // We always flush so that you get the most recent value. This is a noop if autorun was not invalidated. 103 | getter.flush(); 104 | return lastValue.get(); 105 | }; 106 | 107 | // We mingle the prototype so that getter instanceof ComputedField is true. 108 | if (Object.setPrototypeOf) { 109 | Object.setPrototypeOf(getter, this.constructor.prototype); 110 | } 111 | else { 112 | getter.__proto__ = this.constructor.prototype; 113 | } 114 | 115 | getter.toString = function() { 116 | return `ComputedField{${this()}}`; 117 | }; 118 | 119 | getter.apply = () => { 120 | return getter(); 121 | }; 122 | 123 | getter.call = () => { 124 | return getter(); 125 | }; 126 | 127 | // If this autorun is nested in the outside autorun it gets stopped automatically when the outside autorun gets 128 | // invalidated, so no need to call destroy. But otherwise you should call destroy when the field is not needed anymore. 129 | getter.stop = function () { 130 | if (handle != null) { 131 | handle.stop(); 132 | } 133 | return handle = null; 134 | }; 135 | 136 | // For tests. 137 | getter._isRunning = () => { 138 | return !!handle; 139 | }; 140 | 141 | // Sometimes you want to force recomputation of the new value before the global Tracker flush is done. 142 | // This is a noop if autorun was not invalidated. 143 | getter.flush = () => { 144 | Tracker.nonreactive(function () { 145 | if (handle) { 146 | handle.flush(); 147 | } 148 | else { 149 | // If there is no autorun, create it now. This will do initial recomputation as well. If there 150 | // will be no dependents after the global flush, autorun will stop (again). 151 | startAutorun(); 152 | } 153 | }) 154 | }; 155 | 156 | return getter; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'peerlibrary:computed-field', 3 | summary: "Reactively computed field for Meteor", 4 | version: '0.10.0', 5 | git: 'https://github.com/peerlibrary/meteor-computed-field.git' 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom('METEOR@1.8.1'); 10 | 11 | // Core dependencies. 12 | api.use([ 13 | 'ecmascript', 14 | 'tracker', 15 | 'reactive-var', 16 | 'underscore' 17 | ]); 18 | 19 | api.use([ 20 | 'blaze@2.3.3' 21 | ], {weak: true}); 22 | 23 | api.export('ComputedField'); 24 | 25 | api.mainModule('lib.js'); 26 | }); 27 | 28 | Package.onTest(function (api) { 29 | api.versionsFrom('METEOR@1.8.1'); 30 | 31 | // Core dependencies. 32 | api.use([ 33 | 'coffeescript@2.4.1', 34 | 'ecmascript', 35 | 'tracker', 36 | 'reactive-var', 37 | 'templating', 38 | 'blaze@2.2.1', 39 | 'spacebars', 40 | 'underscore', 41 | 'jquery' 42 | ]); 43 | 44 | // Internal dependencies. 45 | api.use([ 46 | 'peerlibrary:computed-field' 47 | ]); 48 | 49 | // 3rd party dependencies. 50 | api.use([ 51 | 'peerlibrary:classy-test@0.4.0' 52 | ]); 53 | 54 | api.addFiles([ 55 | 'tests.coffee' 56 | ]); 57 | 58 | api.addFiles([ 59 | 'tests_client.html', 60 | 'tests_client.coffee', 61 | 'tests_client.css' 62 | ], 'client'); 63 | }); 64 | -------------------------------------------------------------------------------- /tests.coffee: -------------------------------------------------------------------------------- 1 | class BasicTestCase extends ClassyTestCase 2 | @testName: 'computed-field - basic' 3 | 4 | testBasic: -> 5 | foo = new ComputedField -> 6 | 42 7 | 8 | @assertEqual foo(), 42 9 | @assertInstanceOf foo, ComputedField 10 | @assertEqual foo.constructor, ComputedField 11 | @assertTrue _.isFunction foo 12 | 13 | @assertEqual foo.apply(), 42 14 | 15 | @assertEqual foo.call(), 42 16 | 17 | @assertEqual "#{foo}", 'ComputedField{42}' 18 | 19 | testReactive: -> 20 | internal = new ReactiveVar 42 21 | 22 | foo = new ComputedField -> 23 | internal.get() 24 | 25 | changes = [] 26 | handle = Tracker.autorun (computation) => 27 | changes.push foo() 28 | 29 | internal.set 43 30 | 31 | Tracker.flush() 32 | 33 | internal.set 44 34 | 35 | Tracker.flush() 36 | 37 | internal.set 44 38 | 39 | Tracker.flush() 40 | 41 | internal.set 43 42 | 43 | Tracker.flush() 44 | 45 | @assertEqual changes, [42, 43, 44, 43] 46 | 47 | handle.stop() 48 | 49 | testNested: -> 50 | internal = new ReactiveVar 42 51 | outside = null 52 | 53 | changes = [] 54 | handle = Tracker.autorun (computation) => 55 | outside = new ComputedField -> 56 | internal.get() 57 | changes.push outside() 58 | 59 | internal.set 43 60 | 61 | Tracker.flush() 62 | 63 | handle.stop() 64 | 65 | Tracker.flush() 66 | 67 | internal.set 44 68 | 69 | Tracker.flush() 70 | 71 | internal.set 45 72 | 73 | # Force reading of the value. 74 | @assertEqual outside(), 45 75 | 76 | Tracker.flush() 77 | 78 | @assertEqual changes, [42, 43] 79 | 80 | outside.stop() 81 | 82 | testDontStop: -> 83 | internal = new ReactiveVar 42 84 | 85 | run = [] 86 | foo = new ComputedField -> 87 | value = internal.get() 88 | run.push value 89 | value 90 | 91 | foo() 92 | 93 | @assertTrue foo._isRunning() 94 | 95 | Tracker.flush() 96 | 97 | @assertFalse foo._isRunning() 98 | 99 | foo() 100 | 101 | @assertTrue foo._isRunning() 102 | 103 | Tracker.flush() 104 | 105 | @assertFalse foo._isRunning() 106 | 107 | foo() 108 | 109 | @assertTrue foo._isRunning() 110 | 111 | @assertEqual run, [42, 42, 42] 112 | 113 | foo.stop() 114 | 115 | run = [] 116 | foo = new ComputedField -> 117 | value = internal.get() 118 | run.push value 119 | value 120 | , 121 | true 122 | 123 | foo() 124 | 125 | @assertTrue foo._isRunning() 126 | 127 | Tracker.flush() 128 | 129 | @assertTrue foo._isRunning() 130 | 131 | foo() 132 | 133 | @assertTrue foo._isRunning() 134 | 135 | Tracker.flush() 136 | 137 | @assertTrue foo._isRunning() 138 | 139 | foo() 140 | 141 | @assertTrue foo._isRunning() 142 | 143 | @assertEqual run, [42] 144 | 145 | foo.stop() 146 | 147 | ClassyTestCase.addTest new BasicTestCase() 148 | -------------------------------------------------------------------------------- /tests_client.coffee: -------------------------------------------------------------------------------- 1 | runs = [] 2 | output = [] 3 | 4 | Template.computedFieldTestTemplate.onCreated -> 5 | internal = new ReactiveVar 42 6 | 7 | @field = new ComputedField => 8 | runs.push true 9 | internal.get() 10 | , 11 | true 12 | 13 | @autorun (computation) => 14 | f = new ComputedField => 15 | Template.currentData()?.foo?() % 10 16 | 17 | output.push f() 18 | 19 | Template.computedFieldTestTemplate.helpers 20 | bar: -> 21 | field = new ComputedField => 22 | foo = Template.currentData()?.foo?() 23 | if _.isNumber foo 24 | foo % 10 25 | else 26 | '' 27 | 28 | field() 29 | 30 | Template.computedFieldTestTemplate.events 31 | 'click .computedFieldTestTemplate': (event) -> 32 | Template.instance().field() 33 | 34 | class TemplateTestCase extends ClassyTestCase 35 | @testName: 'computed-field - template' 36 | 37 | testTemplate: [ 38 | -> 39 | @internal = new ReactiveVar 42 40 | 41 | @foo = new ComputedField => 42 | @internal.get() 43 | 44 | @rendered = Blaze.renderWithData Template.computedFieldTestTemplate, {foo: @foo}, $('body').get(0) 45 | 46 | Tracker.afterFlush @expect() 47 | , 48 | -> 49 | @assertEqual $('.computedFieldTestTemplate').text(), '42|2' 50 | 51 | @internal.set 43 52 | # Field flush happens automatically when using getter. 53 | @assertEqual @foo(), 43 54 | 55 | # There was no global flush yet, so old value is rendered. 56 | @assertEqual $('.computedFieldTestTemplate').text(), '42|2' 57 | 58 | Tracker.afterFlush @expect() 59 | , 60 | -> 61 | # But after global flush we want that the new value is rendered, even if we flushed 62 | # the autorun before the global flush happened (by calling a getter). 63 | @assertEqual $('.computedFieldTestTemplate').text(), '43|3' 64 | 65 | Blaze.remove @rendered 66 | 67 | # Even after all dependencies are removed, autorun is still active. 68 | @assertTrue @foo._isRunning() 69 | 70 | # But after the value changes again and the computation reruns, a new check is made after the global flush. 71 | @internal.set 44 72 | @assertEqual @foo(), 44 73 | 74 | Tracker.afterFlush @expect() 75 | , 76 | -> 77 | # And now the computed field should not be running anymore. 78 | @assertFalse @foo._isRunning() 79 | 80 | @internal.set 45 81 | @assertFalse Tracker.active 82 | @assertEqual @foo(), 45 83 | 84 | Tracker.afterFlush @expect() 85 | , 86 | -> 87 | # Value was updated, but because getter was not called in the reactive context, autorun was stopped again. 88 | @assertFalse @foo._isRunning() 89 | 90 | # But now if we render the template again and register a dependency again. 91 | @rendered = Blaze.renderWithData Template.computedFieldTestTemplate, {foo: @foo}, $('body').get(0) 92 | 93 | Tracker.afterFlush @expect() 94 | , 95 | -> 96 | @assertEqual $('.computedFieldTestTemplate').text(), '45|5' 97 | 98 | # Autorun is running again. 99 | @assertTrue @foo._isRunning() 100 | 101 | Blaze.remove @rendered 102 | 103 | # Still running. There was no value change and no global flush yet. 104 | @assertTrue @foo._isRunning() 105 | 106 | # We can also stop autorun manually. 107 | @foo.stop() 108 | @assertFalse @foo._isRunning() 109 | ] 110 | 111 | testTemplateAutorun: [ 112 | -> 113 | runs = [] 114 | output = [] 115 | 116 | @rendered = Blaze.render Template.computedFieldTestTemplate, $('body').get(0) 117 | 118 | Tracker.afterFlush @expect() 119 | , 120 | -> 121 | @assertEqual $('.computedFieldTestTemplate').text(), '|' 122 | 123 | $('.computedFieldTestTemplate').click() 124 | 125 | Tracker.afterFlush @expect() 126 | , 127 | -> 128 | $('.computedFieldTestTemplate').click() 129 | 130 | Tracker.afterFlush @expect() 131 | , 132 | -> 133 | @assertTrue @rendered.templateInstance().field._isRunning() 134 | 135 | Blaze.remove @rendered 136 | 137 | @assertFalse @rendered.templateInstance().field._isRunning() 138 | 139 | @assertEqual runs.length, 1 140 | @assertEqual output.length, 1 141 | ] 142 | 143 | testTemplateNestedAutorun: [ 144 | -> 145 | output = [] 146 | 147 | @internal = new ReactiveVar 42 148 | 149 | @foo = new ComputedField => 150 | @internal.get() 151 | 152 | @rendered = Blaze.renderWithData Template.computedFieldTestTemplate, {foo: @foo}, $('body').get(0) 153 | 154 | Tracker.afterFlush @expect() 155 | , 156 | -> 157 | @assertEqual $('.computedFieldTestTemplate').text(), '42|2' 158 | 159 | @internal.set 43 160 | 161 | @assertEqual $('.computedFieldTestTemplate').text(), '42|2' 162 | 163 | Tracker.afterFlush @expect() 164 | , 165 | -> 166 | @assertEqual $('.computedFieldTestTemplate').text(), '43|3' 167 | 168 | @internal.set 53 169 | 170 | @assertEqual $('.computedFieldTestTemplate').text(), '43|3' 171 | 172 | Tracker.afterFlush @expect() 173 | , 174 | -> 175 | @assertEqual $('.computedFieldTestTemplate').text(), '53|3' 176 | 177 | Blaze.remove @rendered 178 | 179 | @internal.set 54 180 | 181 | Tracker.afterFlush @expect() 182 | , 183 | -> 184 | # We can also stop autorun manually. 185 | @foo.stop() 186 | 187 | @assertEqual output, [2, 3] 188 | ] 189 | 190 | ClassyTestCase.addTest new TemplateTestCase() 191 | -------------------------------------------------------------------------------- /tests_client.css: -------------------------------------------------------------------------------- 1 | .computedFieldTestTemplate { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /tests_client.html: -------------------------------------------------------------------------------- 1 | 4 | --------------------------------------------------------------------------------