{{sum}}
156 | 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 | 2 |