-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathconnect-backbone-to-react.js
219 lines (174 loc) · 6.16 KB
/
connect-backbone-to-react.js
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
const hoistStatics = require('hoist-non-react-statics');
const { Component, createElement } = require('react');
const PropTypes = require('prop-types');
const debounceFn = require('lodash.debounce');
const BackboneToReactContext = require('./context');
function getDisplayName(name) {
return `connectBackboneToReact(${name})`;
}
function defaultMapModelsToProps(models) {
return Object.keys(models).reduce((acc, modelKey) => {
const model = models[modelKey];
if (!model) return;
acc[modelKey] = model.toJSON();
return acc;
}, {});
}
module.exports = function connectBackboneToReact(
mapModelsToProps,
options = {}
) {
if (typeof mapModelsToProps !== 'function') {
mapModelsToProps = defaultMapModelsToProps;
}
const {
debounce = false,
events = {},
modelTypes = {},
withRef = false,
} = options;
function getEventNames(modelName) {
let eventNames = events[modelName];
// Allow turning off event handlers by setting events to false.
if (eventNames === false) {
return [];
}
if (!Array.isArray(eventNames)) {
return ['all'];
}
return eventNames;
}
function validateModelTypes(modelsMap) {
return Object.keys(modelTypes).forEach(modelKey => {
const ModelConstructor = modelTypes[modelKey];
const modelInstance = modelsMap[modelKey];
const isInstanceOfModel = modelInstance instanceof ModelConstructor;
if (!isInstanceOfModel) {
throw new Error(`"${modelKey}" model found on modelsMap does not match type required.`);
}
});
}
return function createWrapper(WrappedComponent) {
const wrappedComponentName = WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';
const displayName = getDisplayName(wrappedComponentName);
class ConnectBackboneToReact extends Component {
constructor(props, context) {
super(props, context);
this.componentIsMounted = false;
this.createNewProps = this.createNewProps.bind(this);
this.setWrappedInstance = this.setWrappedInstance.bind(this);
if (debounce) {
const debounceWait = typeof debounce === 'number' ? debounce : 0;
this.createNewProps = debounceFn(this.createNewProps, debounceWait);
}
this.createEventListeners();
}
getModels() {
const models = Object.assign({}, this.context, this.props.models);
validateModelTypes(models);
return models;
}
createEventListeners() {
const models = this.getModels();
Object.keys(models).forEach(mapKey => {
const model = models[mapKey];
// Do not attempt to create event listeners on an undefined model.
if (!model) return;
this.createEventListener(mapKey, model);
});
// Store a reference to the models with event listeners for the next update.
this.prevModels = models;
}
createEventListener(modelName, model) {
getEventNames(modelName).forEach(name => {
model.on(name, this.createNewProps, this);
});
}
removeEventListener(modelName, model) {
getEventNames(modelName).forEach(name => {
model.off(name, this.createNewProps, this);
});
}
createNewProps() {
// Bail out if our component has been unmounted.
// The only case where this flag is encountered is when this component
// is unmounted within an event handler but the 'all' event is still triggered.
// It is covered in a test case.
// Also bails if we haven't yet mounted, to avoid warnings in strict mode.
if (!this.componentIsMounted) {
return;
}
this.forceUpdate();
}
setWrappedInstance(ref) {
this.wrappedInstance = ref;
}
getWrappedInstance() {
if (!withRef) {
throw new Error('getWrappedInstance() requires withRef to be true.');
}
return this.wrappedInstance;
}
componentDidMount() {
this.componentIsMounted = true;
}
componentDidUpdate() {
// add and remove listeners
const models = this.getModels();
const prevModels = this.prevModels;
// Bind event listeners for each model that changed.
Object.keys(Object.assign({}, models, prevModels)).forEach(mapKey => {
const model = models[mapKey];
const prevModel = prevModels[mapKey];
// Do not attempt to create event listeners on an undefined model.
if (!model) {
// Instead, if it was previously defined, remove the old listeners.
if (prevModel) {
this.removeEventListener(mapKey, prevModel);
}
return;
}
if (prevModel === model) return; // Did not change.
this.createEventListener(mapKey, model);
});
// Store a reference to the models with event listeners for the next update.
this.prevModels = models;
}
componentWillUnmount() {
if (debounce) {
this.createNewProps.cancel();
}
Object.keys(this.prevModels).forEach(mapKey => {
const model = this.prevModels[mapKey];
// Do not attempt to remove event listeners on an undefined model.
if (!model) return;
this.removeEventListener(mapKey, model);
});
this.componentIsMounted = false;
}
render() {
const wrappedProps = Object.assign(
{},
mapModelsToProps(this.getModels(), this.props),
this.props
);
// Don't pass through models prop.
wrappedProps.models = undefined;
if (withRef) {
wrappedProps.ref = this.setWrappedInstance;
}
return createElement(WrappedComponent, wrappedProps);
}
}
const propTypes = {
models: PropTypes.object,
};
ConnectBackboneToReact.WrappedComponent = WrappedComponent;
ConnectBackboneToReact.displayName = displayName;
ConnectBackboneToReact.propTypes = propTypes;
ConnectBackboneToReact.contextType = BackboneToReactContext;
return hoistStatics(ConnectBackboneToReact, WrappedComponent);
};
};