mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move client code to core
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
This commit is contained in:
217
app/client/lib/koUtil.js
Normal file
217
app/client/lib/koUtil.js
Normal file
@@ -0,0 +1,217 @@
|
||||
var _ = require('underscore');
|
||||
var ko = require('knockout');
|
||||
|
||||
/**
|
||||
* This is typed to declare that the observable/computed supports subscribable.fn methods
|
||||
* added in this utility.
|
||||
*/
|
||||
function withKoUtils(obj) {
|
||||
return obj;
|
||||
}
|
||||
exports.withKoUtils = withKoUtils;
|
||||
|
||||
/**
|
||||
* subscribeInit is a convenience method, equivalent to knockout's observable.subscribe(), but
|
||||
* also calls the callback immediately with the observable's current value.
|
||||
*
|
||||
* It is added to the prototype for all observables, as long as this module is included anywhere.
|
||||
*/
|
||||
ko.subscribable.fn.subscribeInit = function(callback, target, event) {
|
||||
var sub = this.subscribe(callback, target, event);
|
||||
callback.call(target, this.peek());
|
||||
return sub;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a named method `assign` to knockout subscribables (including observables) to assign a new
|
||||
* value. This way we can move away from using callable objects for everything, since callable
|
||||
* objects require hacking at prototypes.
|
||||
*/
|
||||
ko.subscribable.fn.assign = function(value) {
|
||||
this(value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method to modify a non-primitive value and assign it back. E.g. if foo() is an
|
||||
* observable whose value is an array, then
|
||||
*
|
||||
* foo.modifyAssign(function(array) { array.push("test"); });
|
||||
*
|
||||
* is one-liner equivalent to:
|
||||
*
|
||||
* var array = foo.peek();
|
||||
* array.push("text");
|
||||
* foo(array);
|
||||
*
|
||||
* Whenever using a non-primitive value, be careful that it's not shared with other code, which
|
||||
* might modify it without any observable subscriptions getting triggered.
|
||||
*/
|
||||
ko.subscribable.fn.modifyAssign = function(modifierFunc) {
|
||||
var value = this.peek();
|
||||
modifierFunc(value);
|
||||
this(value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tells a computed observable which may return non-primitive values (e.g. objects) that it should
|
||||
* only notify subscribers when the computed value is not equal to the last one (using "===").
|
||||
*/
|
||||
ko.subscribable.fn.onlyNotifyUnequal = function() {
|
||||
this.equalityComparer = function(a, b) { return a === b; };
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
let _handlerFunc = (err) => {};
|
||||
let _origKoComputed = ko.computed;
|
||||
|
||||
/**
|
||||
* If setComputedErrorHandler is used, this wrapper catches and swallows errors from the
|
||||
* evaluation of any computed. Any exception gets passed to _handlerFunc, and the computed
|
||||
* evaluates successfully to its previous value (or _handlerFunc may rethrow the error).
|
||||
*/
|
||||
function _wrapComputedRead(readFunc) {
|
||||
return function() {
|
||||
let lastValue;
|
||||
try {
|
||||
return (lastValue = readFunc.call(this));
|
||||
} catch (err) {
|
||||
console.error("ERROR in ko.computed: %s", err);
|
||||
_handlerFunc(err);
|
||||
return lastValue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If called, exceptions thrown while evaluating any ko.computed observable will get passed to
|
||||
* handlerFunc and swallowed. Unless the handlerFunc rethrows them, the computed will evaluate
|
||||
* successfully to its previous value.
|
||||
*
|
||||
* Note that this is merely an attempt to do the best we can to keep going in the face of
|
||||
* application bugs. The returned value is not actually correct, and relying on this incorrect
|
||||
* value may cause even worse bugs elsewhere in the application. It is important that any errors
|
||||
* caught via this mechanism get reported, debugged, and fixed.
|
||||
*/
|
||||
function setComputedErrorHandler(handlerFunc) {
|
||||
_handlerFunc = handlerFunc;
|
||||
|
||||
// Note that ko.pureComputed calls to ko.computed, so doesn't need its own override.
|
||||
ko.computed = function(funcOrOptions, funcTarget, options) {
|
||||
if (typeof funcOrOptions === 'function') {
|
||||
funcOrOptions = _wrapComputedRead(funcOrOptions);
|
||||
} else {
|
||||
funcOrOptions.read = _wrapComputedRead(funcOrOptions.read);
|
||||
}
|
||||
return _origKoComputed(funcOrOptions, funcTarget, options);
|
||||
};
|
||||
}
|
||||
exports.setComputedErrorHandler = setComputedErrorHandler;
|
||||
|
||||
|
||||
/**
|
||||
* Returns an observable which mirrors the passed-in argument, but returns a default value if the
|
||||
* underlying field is falsy. Writes to the returned observable translate directly to writes to the
|
||||
* underlying one. The default may be a function, evaluated as for computed observables,
|
||||
* with optContext as the context.
|
||||
*/
|
||||
function observableWithDefault(obs, defaultOrFunc, optContext) {
|
||||
if (typeof defaultOrFunc !== 'function') {
|
||||
var def = defaultOrFunc;
|
||||
defaultOrFunc = function() { return def; };
|
||||
}
|
||||
return ko.pureComputed({
|
||||
read: function() { return obs() || defaultOrFunc.call(this); },
|
||||
write: function(val) { obs(val); },
|
||||
owner: optContext
|
||||
});
|
||||
}
|
||||
exports.observableWithDefault = observableWithDefault;
|
||||
|
||||
/**
|
||||
* Return an observable which mirrors the passed-in argument, but convert to Number value. Write to
|
||||
* to the returned observable translate to write to the underlying one a Number value.
|
||||
*/
|
||||
function observableNumber(obs) {
|
||||
return ko.pureComputed({
|
||||
read: () => Number(obs()),
|
||||
write: (val) => {
|
||||
obs(Number(val));
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.observableNumber = observableNumber;
|
||||
|
||||
/**
|
||||
* Same interface as ko.computed(), except that it disposes the values it evaluates to. If an
|
||||
* observable is set to values which are created on the fly and need to be disposed (e.g.
|
||||
* components), use foo = computedAutoDispose(...). Whenever the value of foo() changes (and when
|
||||
* foo itself is disposed), the previous value's `dispose` method gets called.
|
||||
*/
|
||||
function computedAutoDispose(optionsOrReadFunc, target, options) {
|
||||
// Note: this isn't quite possible to do as a knockout extender, specifically to get correct the
|
||||
// pure vs non-pure distinction (sometimes the computed must be pure to avoid evaluation;
|
||||
// sometimes it has side-effects and must not be pure).
|
||||
var value = null;
|
||||
function setNewValue(newValue) {
|
||||
if (value && value !== newValue) {
|
||||
ko.ignoreDependencies(value.dispose, value);
|
||||
}
|
||||
value = newValue;
|
||||
return newValue;
|
||||
}
|
||||
|
||||
var origRead;
|
||||
if (typeof optionsOrReadFunc === "object") {
|
||||
// Single-parameter syntax.
|
||||
origRead = optionsOrReadFunc.read;
|
||||
options = _.clone(optionsOrReadFunc);
|
||||
} else {
|
||||
origRead = optionsOrReadFunc;
|
||||
options = _.defaults({ owner: target }, options || {});
|
||||
}
|
||||
options.read = function() {
|
||||
return setNewValue(origRead.call(this));
|
||||
};
|
||||
|
||||
var result = ko.computed(options);
|
||||
var origDispose = result.dispose;
|
||||
result.dispose = function() {
|
||||
setNewValue(null);
|
||||
origDispose.call(result);
|
||||
};
|
||||
return result;
|
||||
}
|
||||
exports.computedAutoDispose = computedAutoDispose;
|
||||
|
||||
|
||||
/**
|
||||
* Helper for building disposable components that depend on a few observables. The callback is
|
||||
* evaluated as for a knockout computed observable, which creates dependencies on any observables
|
||||
* mentioned in it. But the return value of the callback should be a function ("builder"), which
|
||||
* is called to build the resulting value. Observables mentioned in the evaluation of the builder
|
||||
* do NOT create dependencies. In addition, the built value gets disposed automatically when it
|
||||
* changes.
|
||||
*
|
||||
* The optContext argument serves as the context for the callback.
|
||||
*
|
||||
* For example,
|
||||
* var foo = ko.observable();
|
||||
* koUtil.computedBuilder(function() {
|
||||
* return MyComponent.create.bind(MyComponent, foo());
|
||||
* }, this);
|
||||
*
|
||||
* In this case, whenever foo() changes, MyComponent.create(foo()) gets called, and
|
||||
* previously-returned component gets disposed. Observables mentioned during MyComponent's
|
||||
* construction do not trigger its rebuilding (as they would if a plain ko.computed() were used).
|
||||
*/
|
||||
function computedBuilder(callback, optContext) {
|
||||
return computedAutoDispose(function() {
|
||||
var builder = callback.call(optContext);
|
||||
return builder ? ko.ignoreDependencies(builder) : null;
|
||||
}, null, { pure: false });
|
||||
}
|
||||
exports.computedBuilder = computedBuilder;
|
||||
Reference in New Issue
Block a user