Skip to content

Instantly share code, notes, and snippets.

@markerikson
Last active January 30, 2020 00:48
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markerikson/fe53bf4c4eb3a040a0007c16e043b322 to your computer and use it in GitHub Desktop.
Save markerikson/fe53bf4c4eb3a040a0007c16e043b322 to your computer and use it in GitHub Desktop.
Backbone/React Interop
import {viewFromComponent} from "viewUtilities";
class MyListComponent extends React.Component {
render() {
// serialized Backbone collection
const {items = [] } this.props;
// do stuff with items
}
}
class MySingleComponent extends React.component {
render() {
// serialized Backbone model, with whatever your real fields are
const {a, b, c} = this.props;
// do stuff with fields
}
}
const MyListView = viewFromComponent(MyListComponent, {
observe : {
// Every time one of these events is triggered, the collection will be serialized
// and the HOC will call this.setState()
collection : "add remove reset",
},
actions : {
addNewItem(newItem) {
// This function runs in the context of the HOC generated by viewFromComponent,
// so `this` is actually the parent of your "real" component. This gives you
// access to the model/collection instance to let you mutate them, while keeping
// your real component unaware of Backbone entirely.
// Yes, this is sorta-kinda mutating "props" in a way, but it's an incremental
// step towards turning this into a component callback or Redux action creator.
this.props.collection.add(newItem);
}
}
});
const MySingleView = viewFromComponent(MySingleComponent, {
observe : {
model : "change"
},
actions : {
setSomeField(newValue) {
this.props.model.set("someField", someValue);
}
}
}, {staticClassProp : 123}, {id : "myViewId", el : "#divToAttachTo"});
const collectionView = new MyListView({collection : someBackboneCollection});
const modelView = new MySingleView({model : someBackboneModel});
// See https://github.com/Workable/backbone.react-bridge for the original implementation.
// I've customized this one in several ways.
const defaultObserveEvents = {
model: "change",
collection: "add remove reset",
};
function defaultGetProps({model, collection, state = {}} = {}) {
if (model) {
Object.assign(state, model.toJSON({computed: true}));
}
if (collection) {
Object.assign(state, {items: collection.toJSON({computed: true})});
}
return state;
}
function extractNewState(options = {}) {
const {getProps = defaultGetProps, props} = options;
const newState = {
...getProps(options),
...props,
};
return newState;
}
class BackboneContainer extends React.Component {
static defaultProps = {
observe: {},
actions: {},
};
constructor(props) {
super();
this.state = extractNewState(props);
const {actions = {}} = props;
this.actions = _.mapValues(actions, action => action.bind(this));
}
componentDidMount() {
const {model, collection, observe} = this.props;
const potentialEventsToSubscribeTo = {...defaultObserveEvents, ...observe};
this.eventsToSubscribeTo = Object.keys(potentialEventsToSubscribeTo).reduce((obj, key) => {
const backboneItem = this.props[key];
if (backboneItem) {
const events = potentialEventsToSubscribeTo[key];
obj[key] = {backboneItem, events};
}
return obj;
}, {});
_.forEach(this.eventsToSubscribeTo, entry => {
const {backboneItem, events} = entry;
this.initListener(backboneItem, events);
});
}
componentWillUnmount() {
_.forEach(this.eventsToSubscribeTo, entry => {
const {backboneItem, events} = entry;
this.destroyListener(backboneItem, events);
});
}
initListener = (entity, events) => {
if (!entity) {
return;
}
if (events instanceof Array) {
events = events.join(" ");
}
entity.on(events, this.updateComponentState);
};
updateComponentState = () => {
const newState = extractNewState(this.props);
this.setState(newState);
};
destroyListener = (entity, events) => {
if (!entity) {
return;
}
entity.off(null, this.updateComponentState);
};
render() {
const {actions = {}} = this;
const {
actions: propActions = {},
observe,
model,
collection,
children,
render = children,
...otherProps
} = this.props;
const childProps = {...actions, ...this.state, ...otherProps};
return renderProps(render, childProps);
}
}
function viewFromComponent(Component, options = {}, classOptions = {}, wrapperOptions = {}) {
const {id, el, className = ""} = wrapperOptions;
const ReactMarionetteView = Backbone.Marionette.ItemView.extend(
{
id,
el,
template: _.template(""),
className: "react-marionette-view " + className,
onRender() {
if (this._reactInternalInstance) {
return false;
}
// Create and render a container component wrapping the given component
//const props = Object.assign({observe : {}, actions : {}}, options, this.options);
const props = {
...options,
...this.options,
render: Component,
};
this._reactInternalInstance = ReactDOM.render(<BackboneContainer {...props} />, this.el);
},
onDestroy() {
this._reactInternalInstance._isMounted = false;
ReactDOM.unmountComponentAtNode(this.el);
},
onClose() {
this._reactInternalInstance._isMounted = false;
ReactDOM.unmountComponentAtNode(this.el);
},
},
classOptions,
);
return ReactMarionetteView;
}
@c0debreaker
Copy link

Thank you so much Mark!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment