-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathCell.jsx
160 lines (144 loc) · 4.95 KB
/
Cell.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Tracker-based reactivity is run here.
// We let minimongo drive cell recalculations by caching cell
// calculations in the `results` field and querying them in an
// autorun (one per cell) which allows arbitrary relationships between
// cells as specified in user cell code.
// XXX NOT FOR PRODUCTION USE
// User code is `eval`ed. This a risk to the user if they trust someone
// else's code, and the user can probably break the app. On the other
// hand, the JS environment and syntax is all there for free. If we put
// functions in the closure with the eval (e.g. `cell()`), the user
// can access that in their formula, and minimongo queries via `cell()`
// or direclty in the cell formula will be reactive.
Cell = React.createClass({
propTypes: {
// cell: React.PropTypes.object.isRequired
},
calculate (formula) {
// Try to run the cell as a js expression, and call it if
// it evaluates to a function.
let result = formula || this.props.cell.formula || "";
// Exit if cell is empty
if (result.trim() === "") {
this.updateResult(null);
return;
}
// These expressions cause infinite recursion
const top = "top";
const window = "window";
// create cell() to reference cells in the spreadsheet
const cell = (col, row) => {
if (row===this.props.cell.row && col === this.props.cell.col){
throw Meteor.Error("cell can't refer to itself.");
}
// Querying the cell data will link Tracker for reactive updates.
// Limit to the result field to `result` to keep Tracker invalidations
// from reacting to formula changing.
const cellData = Cells.findOne({row, col}, {fields: {"result": 1}});
// XXX manually setting isReactive is brittle. User formulas might
// access reactive objects, but they wouldn't trigger this & might
// have mixed reactive behavior (worse that non-reactive).
this.isReactive = true;
return cellData && cellData.result;
};
try {
// eval expression & replace result if no error
result = eval(`(${result})`);
try {
// if expression evals to a function, call it
result = typeof result === "function" ? result() : result;
} catch (e) {
// console.log("fn error")
}
} catch (e) {
// console.log("eval error")
}
if (typeof result === "function" ) {
// must be a function that threw an error, so let's display the formula
// XXX 2nd order functions would display too. Maybe OK?
result = formula || this.props.cell.formula;
}
this.updateResult(result, ! this.isReactive);
return result;
},
showFormula (event) {
this.props.setSelection(this.props.cell);
event.stopPropagation();
},
updateFormula (formula) {
// XXX refactor & move to method
formula = formula.trim();
if (formula !== this.props.cell.formula) {
if (this.props.cell._id){
if (formula) {
let result
this.computation.stop();
this.computation = Tracker.autorun( () => {
result = this.calculate(formula);
});
Cells.update(this.props.cell._id, {$set: {
formula,
result,
isNotReactive: ! this.isReactive
}});
} else {
Cells.remove(this.props.cell._id);
}
} else {
let result;
if (formula) {
// XXX autorun should move...to this.calculate()?
this.computation = Tracker.autorun( () => {
result = this.calculate(formula);
});
Cells.insert({
col: this.props.cell.col,
row: this.props.cell.row,
formula,
result,
isNotReactive: ! this.isReactive
});
}
}
}
this.props.clearSelection();
},
updateResult (result, isNotReactive) {
if (result !== this.props.cell.result) {
Cells.update(this.props.cell._id, {$set: {result, isNotReactive}});
}
},
componentWillMount () {
// isNotReactive memoizes when the formula doesn't contain any
// reactive code - no need to rerun it when we start the app.
// XXX isNotReactive: premature optimization?
if (! this.isNotReactive) {
this.computation = Tracker.autorun( () => {
this.calculate();
});
}
},
componentWillUnmount () {
this.computation && this.computation.stop();
},
shouldComponentUpdate (nextProps, nextState) {
return ! (
this.props.selected === nextProps.selected &&
this.props.cell.result === nextProps.cell.result &&
this.props.cell._id === nextProps.cell._id &&
this.state === nextState
);
},
render() {
return this.props.selected
? <CellFormula
formula={this.props.cell.formula}
updateFormula={this.updateFormula}
clearSelection={this.props.clearSelection}
/>
: <CellResult
result={this.props.cell.result}
showFormula={this.showFormula}
/>;
}
});