Skip to content

Commit

Permalink
Merge pull request #4 from open-sausages/silverstripe-baseclass
Browse files Browse the repository at this point in the history
Add SilverStripeComponent base class
  • Loading branch information
scott1702 committed Nov 5, 2015
2 parents bd33696 + 0a0bd65 commit aa16f2d
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 12 deletions.
7 changes: 7 additions & 0 deletions docs/en/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# ReactJS Common

ReactJS Common exposes the required libraries and classes for building React components in SilverStripe CMS.

## Contents

- [SilverStripeComponent](silverstripe-component.md): The base class for SilverStripe React components.
181 changes: 181 additions & 0 deletions docs/en/silverstripe-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# SilverStripeComponent

The base class for SilverStripe React components. If you're building React components for the CMS, this is the class you want to extend. `SilverStripeComponent` extends `React.Component` and adds some handy CMS specific behaviour.

## Creating a component

__my-component.js__
```javascript
import SilverStripeComponent from 'silverstripe-component';

class MyComponent extends SilverStripeComponent {

}

export default MyComponent;
```

That's how you create a SilverStripe React component!

## Interfacing with ye olde CMS JavaScript

One of the great things about ReactJS is that it works great with DOM based libraries like jQuery and Entwine. To allow legacy-land scripts to notify your React component about changes, add the following.

__my-component.js__
```javascript
import SilverStripeComponent from 'silverstripe-component';

class MyComponent extends SilverStripeComponent {
componentDidMount() {
super.componentDidMount();
}

componentWillUnmount() {
super.componentWillUnmount();
}
}

export default MyComponent;
```

This is functionally no different from the first example. But it's a good idea to be explicit and add these `super` calls now. You will inevitably add `componentDidMount` and `componentWillUnmount` hooks to your component and it's easy to forget to call `super` then.

So what's going on when we call those? Glad you asked. If you've passed `cmsEvents` into your component's `props`, wonderful things will happen.

Let's take a look at some examples.

### Getting data into a component

Sometimes you'll want to call component methods when this change in legacy-land. For example when a CMS tab changes you might want to update some component state.

__main.js__
```javascript
import $ from 'jquery';
import React from 'react';
import MyComponent from './my-component';

$.entwine('ss', function ($) {
$('.my-component-wrapper').entwine({
getProps: function (props) {
var defaults = {
cmsEvents: {
'cms.tabchanged': function (event, title) {
this.setState({ currentTab: title });
}
}
};

return $.extend(true, defaults, props);
},
onadd: function () {
var props = this.getProps();

React.render(
<MyComponent {...props} />,
this[0]
);
}
});
});
```

__legacy.js__
```javascript
(function ($) {
$.entwine('ss', function ($) {
$('.cms-tab').entwine({
onclick: function () {
$(document).trigger('cms.tabchanged', this.find('.title').text());
}
});
});
}(jQuery));
```

Each key in `props.cmsEvents` gets turned into an event listener by `SilverStripeComponent.componentDidMount`. When a legacy-land script triggers that event on `document`, the associated component callback is invoked, with the component's context bound to it.

All `SilverStripeComponent.componentWillUnmount` does is clean up the event listeners when they're no longer required.

There are a couple of important things to note here:

1. Both files are using the same `ss` namespace.
2. Default properties are defined the a `getProps` method.

This gives us the flexability to add and override event listeners from legacy-land. We're currently updating the current tab's title when `.cms-tab` is clicked. But say we also wanted to highlight the tab. We could do something like this.

__legacy.js__
```javascript
(function ($) {
$.entwine('ss', function ($) {
$('.main .my-component-wrapper').entwine({
getProps: function (props) {
return this._super({
cmsEvents: {
'cms.tabchanged': function (event, title) {
this.setState({
currentTab: title,
selected: true
});
}
}
});
}
});

$('.cms-tab').entwine({
onclick: function () {
$(document).trigger('cms.tabchanged', this.find('.title').text());
}
});
});
}(jQuery));
```

Here we're using Entwine to override the `getProps` method in `main.js`. Note we've made the selector more specific `.main .my-component-wrapper`. The most specific selector comes first in Entwine, so here our new `getProps` gets called, which passes the new callback to the `getProps` method defined in `main.js`.

### Getting data out of a component

There are times you'll want to update things in legacy-land when something changes in you component.

`SilverStripeComponent` has a handly method `_emitCmsEvents` to help with this.

__my-component.js__
```javascript
import SilverStripeComponent from 'silverstripe-component';

class MyComponent extends SilverStripeComponent {
componentDidMount() {
super.componentDidMount();
}

componentWillUnmount() {
super.componentWillUnmount();
}

componentDidUpdate() {
this._emitCmsEvent('my-component.title-changed', this.state.title);
}
}

export default MyComponent;
```

__legacy.js__
```javascript
(function ($) {
$.entwine('ss', function ($) {
$('.cms-tab').entwine({
onmatch: function () {
var self = this;

$(document).on('my-component.title-changed', function (event, title) {
self.find('.title').text(title);
});
},
onunmatch: function () {
$(document).off('my-component.title-changed');
}
});
});
}(jQuery));
```
3 changes: 3 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
var gulp = require('gulp'),
browserify = require('browserify'),
babelify = require('babelify'),
source = require('vinyl-source-stream'),
buffer = require('vinyl-buffer'),
uglify = require('gulp-uglify');

gulp.task('build', function () {

browserify()
.transform(babelify)
.require('react/addons', { expose: 'react' })
.require('flux', { expose: 'flux' })
.require('./public/src/jquery', { expose: 'jquery' })
.require('./public/src/i18n', { expose: 'i18n' })
.require('./public/src/silverstripe-component', { expose: 'silverstripe-component' })
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,26 @@
"react": "^0.13.3"
},
"devDependencies": {
"babel-jest": "^5.3.0",
"babelify": "^6.3.0",
"browserify": "^11.2.0",
"gulp": "^3.9.0",
"gulp-uglify": "^1.4.1",
"jest-cli": "^0.6.1",
"jquery": "^2.1.4",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
"jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"testDirectoryName": "tests/javascript",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react"
],
"bail": true
},
"scripts": {
"build": "gulp"
"build": "gulp",
"test": "jest"
}
}
14 changes: 7 additions & 7 deletions public/dist/bundle.js

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions public/src/silverstripe-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @file Base component which all SilverStripe ReactJS components should extend from.
*/

import React from 'react';
import $ from 'jquery';

class SilverStripeComponent extends React.Component {

/**
* @func componentDidMount
* @desc Bind event listeners which are triggered by legacy-land JavaScript.
* This lets us update the component when something happens in the outside world.
*/
componentDidMount() {
if (typeof this.props.cmsEvents === 'undefined') {
return;
}

// Save some props for later. When we come to unbind these listeners
// there's no guarantee these props will be the same or even present.
this.cmsEvents = this.props.cmsEvents;

for (let cmsEvent in this.cmsEvents) {
$(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this));
}
}

/**
* @func componentWillUnmount
* @desc Unbind the event listeners we added in componentDidMount.
*/
componentWillUnmount() {
for (let cmsEvent in this.cmsEvents) {
$(document).off(cmsEvent);
}
}

/**
* @func _emitCmsEvent
* @param string componentEvent - Namespace component event e.g. 'my-component.title-changed'.
* @param object|string|array|number [data] - Some data to pass with the event.
* @desc Notifies legacy-land something has changed within our component.
*/
_emitCmsEvent(componentEvent, data) {
$(document).trigger(componentEvent, data);
}

}

SilverStripeComponent.propTypes = {
'cmsEvents': React.PropTypes.object
};

export default SilverStripeComponent;
6 changes: 2 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# SilverStripe ReactJS Common

Exposes ReactJS and Flux for use in SilverStripe CMS React Components.

Instead of bundling React a bunch of times, in a bunch of components, include this module and import React from one place.
ReactJS Common exposes the required libraries and classes for building React components in SilverStripe CMS.

## Install

Expand All @@ -14,7 +12,7 @@ $ composer require silverstripe/reactjs-common

Once you have the module installed, React and Flux are available throughout the CMS, and can be accessed through Browserify.

__/yourHipsterComponent/gulpfile.js__
__./my-component/gulpfile.js__

```
Expand Down
Loading

0 comments on commit aa16f2d

Please sign in to comment.