2011-04-22 08:11:14 +02:00
# How to extend the CMS interface #
## Introduction ##
2014-11-07 20:43:57 +01:00
The CMS interface works just like any other part of your website: It consists of
PHP controllers, templates, CSS stylesheets and JavaScript. Because it uses the
same base elements, it is relatively easy to extend.
2013-06-02 03:38:10 +02:00
2014-11-07 20:43:57 +01:00
As an example, we're going to add a permanent "bookmarks" link list to popular pages
into the main CMS menu. A page can be bookmarked by a CMS author through a
2013-06-02 03:38:10 +02:00
simple checkbox.
2011-04-22 08:11:14 +02:00
2012-01-01 18:24:09 +01:00
For a deeper introduction to the inner workings of the CMS, please refer to our
2016-01-14 11:59:53 +01:00
guide on [CMS Architecture ](/developer_guides/customising_the_admin_interface/cms_architecture ).
2012-01-01 18:24:09 +01:00
2011-11-22 11:06:46 +01:00
## Overload a CMS template ##
2011-04-22 08:11:14 +02:00
2014-11-07 20:43:57 +01:00
If you place a template with an identical name into your application template
directory (usually `mysite/templates/` ), it'll take priority over the built-in
2013-06-02 03:38:10 +02:00
one.
2011-04-22 08:11:14 +02:00
2011-11-22 11:06:46 +01:00
CMS templates are inherited based on their controllers, similar to subclasses of
2011-04-22 08:11:14 +02:00
the common `Page` object (a new PHP class `MyPage` will look for a `MyPage.ss` template).
2011-11-22 11:06:46 +01:00
We can use this to create a different base template with `LeftAndMain.ss`
(which corresponds to the `LeftAndMain` PHP controller class).
2014-11-07 20:43:57 +01:00
Copy the template markup of the base implementation at `framework/admin/templates/Includes/LeftAndMain_Menu.ss`
into `mysite/templates/Includes/LeftAndMain_Menu.ss` . It will automatically be picked up by
2013-07-09 22:15:43 +02:00
the CMS logic. Add a new section into the `<ul class="cms-menu-list">`
2014-11-07 20:43:57 +01:00
2011-11-22 11:06:46 +01:00
:::ss
...
2013-07-09 22:15:43 +02:00
< ul class = "cms-menu-list" >
<!-- ... -->
< li class = "bookmarked-link first" >
< a href = "admin/pages/edit/show/1" > Edit "My popular page"< / a >
< / li >
< li class = "bookmarked-link last" >
< a href = "admin/pages/edit/show/99" > Edit "My other page"< / a >
< / li >
< / ul >
2011-11-22 11:06:46 +01:00
...
2014-11-07 20:43:57 +01:00
2013-07-09 22:15:43 +02:00
Refresh the CMS interface with `admin/?flush=all` , and you should see those
2014-11-07 20:43:57 +01:00
hardcoded links underneath the left-hand menu. We'll make these dynamic further down.
2013-07-09 22:15:43 +02:00
2011-11-22 11:06:46 +01:00
## Include custom CSS in the CMS
2011-04-22 08:11:14 +02:00
2014-11-07 20:43:57 +01:00
In order to show the links a bit separated from the other menu entries,
we'll add some CSS, and get it to load
with the CMS interface. Paste the following content into a new file called
2013-06-02 03:38:10 +02:00
`mysite/css/BookmarkedPages.css` :
2011-04-22 08:11:14 +02:00
2011-11-22 11:06:46 +01:00
:::css
2013-07-09 22:15:43 +02:00
.bookmarked-link.first {margin-top: 1em;}
2011-04-22 08:11:14 +02:00
2013-03-21 19:48:54 +01:00
Load the new CSS file into the CMS, by setting the `LeftAndMain.extra_requirements_css`
2015-02-28 01:09:15 +01:00
[configuration value ](../../configuration ).
2013-04-05 15:34:29 +02:00
:::yml
LeftAndMain:
extra_requirements_css:
2015-07-15 16:36:19 +02:00
- mysite/css/BookmarkedPages.css
2011-11-22 11:06:46 +01:00
## Create a "bookmark" flag on pages ##
2014-11-07 20:43:57 +01:00
Now we'll define which pages are actually bookmarked, a flag that is stored in
the database. For this we need to decorate the page record with a
`DataExtension` . Create a new file called `mysite/code/BookmarkedPageExtension.php`
2013-06-02 03:38:10 +02:00
and insert the following code.
2011-11-22 11:06:46 +01:00
:::php
< ?php
2013-06-02 03:38:10 +02:00
2011-11-22 11:06:46 +01:00
class BookmarkedPageExtension extends DataExtension {
2013-06-02 03:38:10 +02:00
private static $db = array(
'IsBookmarked' => 'Boolean'
);
2014-11-07 20:43:57 +01:00
2012-07-15 07:23:10 +02:00
public function updateCMSFields(FieldList $fields) {
2011-11-22 11:06:46 +01:00
$fields->addFieldToTab('Root.Main',
new CheckboxField('IsBookmarked', "Show in CMS bookmarks?")
);
}
}
2011-11-22 11:09:43 +01:00
2015-02-28 01:09:15 +01:00
Enable the extension in your [configuration file ](../../configuration )
2011-11-22 11:06:46 +01:00
2013-03-27 12:06:57 +01:00
:::yml
SiteTree:
extensions:
- BookmarkedPageExtension
2011-11-22 11:09:43 +01:00
2011-11-22 11:06:46 +01:00
In order to add the field to the database, run a `dev/build/?flush=all` .
Refresh the CMS, open a page for editing and you should see the new checkbox.
## Retrieve the list of bookmarks from the database
One piece in the puzzle is still missing: How do we get the list of bookmarked
2014-11-07 20:43:57 +01:00
pages from the database into the template we've already created (with hardcoded
links)? Again, we extend a core class: The main CMS controller called
2013-06-02 03:38:10 +02:00
`LeftAndMain` .
2011-11-22 11:09:43 +01:00
2012-12-04 10:45:23 +01:00
Add the following code to a new file `mysite/code/BookmarkedLeftAndMainExtension.php` ;
2011-11-22 11:09:43 +01:00
2011-11-22 11:06:46 +01:00
:::php
< ?php
2013-06-02 03:38:10 +02:00
2011-11-22 11:06:46 +01:00
class BookmarkedPagesLeftAndMainExtension extends LeftAndMainExtension {
2013-06-02 03:38:10 +02:00
2012-01-30 23:13:42 +01:00
public function BookmarkedPages() {
2012-06-23 00:32:43 +02:00
return Page::get()->filter("IsBookmarked", 1);
2011-11-22 11:06:46 +01:00
}
}
2014-11-07 20:43:57 +01:00
2015-02-28 01:09:15 +01:00
Enable the extension in your [configuration file ](../../configuration )
2011-11-22 11:06:46 +01:00
2013-03-27 12:06:57 +01:00
:::yml
LeftAndMain:
extensions:
- BookmarkedPagesLeftAndMainExtension
2011-11-22 11:06:46 +01:00
As the last step, replace the hardcoded links with our list from the database.
2012-12-04 10:45:23 +01:00
Find the `<ul>` you created earlier in `mysite/admin/templates/LeftAndMain.ss`
2011-11-22 11:06:46 +01:00
and replace it with the following:
:::ss
2013-07-09 22:15:43 +02:00
< ul class = "cms-menu-list" >
<!-- ... -->
2013-04-26 11:48:59 +02:00
< % loop $BookmarkedPages %>
2013-07-09 22:15:43 +02:00
< li class = "bookmarked-link $FirstLast" >
< li > < a href = "admin/pages/edit/show/$ID" > Edit "$Title"< / a > < / li >
< / li >
2012-06-26 17:32:46 +02:00
< % end_loop %>
2011-11-22 11:06:46 +01:00
< / ul >
2011-11-22 11:09:43 +01:00
2012-11-21 22:01:02 +01:00
## Extending the CMS actions
2014-11-07 20:43:57 +01:00
CMS actions follow a principle similar to the CMS fields: they are built in the
backend with the help of `FormFields` and `FormActions` , and the frontend is
2013-06-02 03:38:10 +02:00
responsible for applying a consistent styling.
2012-11-21 22:01:02 +01:00
The following conventions apply:
2014-11-07 20:43:57 +01:00
* New actions can be added by redefining `getCMSActions` , or adding an extension
2013-06-02 03:38:10 +02:00
with `updateCMSActions` .
2014-11-07 20:43:57 +01:00
* It is required the actions are contained in a `FieldSet` (`getCMSActions`
2013-06-02 03:38:10 +02:00
returns this already).
2014-11-07 20:43:57 +01:00
* Standalone buttons are created by adding a top-level `FormAction` (no such
2013-06-02 03:38:10 +02:00
button is added by default).
2014-11-07 20:43:57 +01:00
* Button groups are created by adding a top-level `CompositeField` with
2013-06-02 03:38:10 +02:00
`FormActions` in it.
2012-11-21 22:01:02 +01:00
* A `MajorActions` button group is already provided as a default.
2014-11-07 20:43:57 +01:00
* Drop ups with additional actions that appear as links are created via a
2013-06-02 03:38:10 +02:00
`TabSet` and `Tabs` with `FormActions` inside.
2014-11-07 20:43:57 +01:00
* A `ActionMenus.MoreOptions` tab is already provided as a default and contains
2013-06-02 03:38:10 +02:00
some minor actions.
2014-11-07 20:43:57 +01:00
* You can override the actions completely by providing your own
2013-06-02 03:38:10 +02:00
`getAllCMSFields` .
2012-11-21 22:01:02 +01:00
Let's walk through a couple of examples of adding new CMS actions in `getCMSActions` .
2014-11-07 20:43:57 +01:00
First of all we can add a regular standalone button anywhere in the set. Here
we are inserting it in the front of all other actions. We could also add a
2013-06-02 03:38:10 +02:00
button group (`CompositeField`) in a similar fashion.
2012-11-21 22:01:02 +01:00
:::php
$fields->unshift(FormAction::create('normal', 'Normal button'));
2014-11-07 20:43:57 +01:00
We can affect the existing button group by manipulating the `CompositeField`
2013-06-02 03:38:10 +02:00
already present in the `FieldList` .
2012-11-21 22:01:02 +01:00
:::php
$fields->fieldByName('MajorActions')->push(FormAction::create('grouped', 'New group button'));
2014-11-07 20:43:57 +01:00
Another option is adding actions into the drop-up - best place for placing
2013-06-02 03:38:10 +02:00
infrequently used minor actions.
2012-11-21 22:01:02 +01:00
:::php
$fields->addFieldToTab('ActionMenus.MoreOptions', FormAction::create('minor', 'Minor action'));
2014-11-07 20:43:57 +01:00
We can also easily create new drop-up menus by defining new tabs within the
2013-06-02 03:38:10 +02:00
`TabSet` .
2012-11-21 22:01:02 +01:00
:::php
$fields->addFieldToTab('ActionMenus.MyDropUp', FormAction::create('minor', 'Minor action in a new drop-up'));
< div class = "hint" markdown = '1' >
Empty tabs will be automatically removed from the `FieldList` to prevent clutter.
< / div >
2014-11-07 20:43:57 +01:00
To make the actions more user-friendly you can also use alternating buttons as
2015-02-28 01:09:15 +01:00
detailed in the [CMS Alternating Button ](cms_alternating_button )
2013-06-02 03:38:10 +02:00
how-to.
2012-11-21 22:01:02 +01:00
2016-02-09 00:26:39 +01:00
## ReactJS in SilverStripe
### Requiring React
If you want to use React in a module, you can do so with browserify by simply setting it as external during your build chain. For example, with gulp it would look something like this:
```javascript
gulp.task('build', function () {
browserify({
// Browserify options...
})
.external('react')
.external('silverstripe-component')
.external('react-dom')
.external('react-addons-test-utils')
.external('react-redux')
.external('redux')
.external('redux-thunk')
.pipe(gulp.dest('path/to/dist'));
})
```
Then you can require React in the normal way.
```javascript
import React from 'react';
```
### 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 legacy 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 things 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, { PropTypes, Component } 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) {
// Call a Redux action to update state.
}
}
};
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 using the `getProps` method.
This gives us the flexibility 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) {
// Call a Redux action to update state.
}
}
});
}
});
$('.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));
```
2016-03-09 13:02:28 +01:00
### Implementing handlers
Your newly created buttons need handlers to bind to before they will do anything.
To implement these handlers, you will need to create a `LeftAndMainExtension` and add
applicable controller actions to it:
:::php
class CustomActionsExtension extends LeftAndMainExtension {
2016-03-20 01:08:23 +01:00
private static $allowed_actions = array(
2016-03-09 13:02:28 +01:00
'sampleAction'
);
public function sampleAction()
{
// Create the web
}
}
The extension then needs to be registered:
:::yaml
LeftAndMain:
extensions:
- CustomActionsExtension
You can now use these handlers with your buttons:
:::php
$fields->push(FormAction::create('sampleAction', 'Perform Sample Action'));
2011-11-22 11:06:46 +01:00
## Summary
2011-04-22 08:11:14 +02:00
2016-03-30 02:17:28 +02:00
In a few lines of code, we've customised the look and feel of the CMS.
2013-06-02 03:38:10 +02:00
2011-11-22 11:06:46 +01:00
While this example is only scratching the surface, it includes most building
2012-01-01 18:24:09 +01:00
blocks and concepts for more complex extensions as well.
## Related
2015-02-28 01:09:15 +01:00
* [Reference: CMS Architecture ](../cms_architecture )
* [Reference: Layout ](../cms_layout )
* [Rich Text Editing ](/developer_guides/forms/field_types/htmleditorfield )
* [CMS Alternating Button ](cms_alternating_button )