DOC Document how to enable the preview panel for custom DataObjects. (#10124)

This commit is contained in:
GuySartorelli 2022-02-04 12:50:35 +13:00 committed by GitHub
parent d8499a24d0
commit 8f1c68db42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -7,16 +7,14 @@ summary: How content previews work in the CMS
## Overview
__Deprecated:__
The following documentation regarding JavaScript layouts and Entwine applies to legacy code only.
If you're developing new functionality in React powered sections please refer to
[ReactJS in Silverstripe CMS](./How_Tos/Extend_CMS_Interface.md#reactjs-in-silverstripe).
With the addition of side-by-side editing, the preview has the ability to appear
within the CMS window when editing content in the _Pages_ section of the CMS.
The site is rendered into an iframe. It will update itself whenever the content
is saved, and relevant pages will be loaded for editing when the user navigates
around in the preview.
within the CMS window when editing content in the CMS. This is enabled by default
in the _Pages_ section for `SiteTree` models, but as outlined below can be enabled
in other sections and for other models as well.
Within the preview panel, the site is rendered into an iframe. It will update
itself whenever the content is saved, and relevant pages will be loaded for editing
when the user navigates around in the preview.
The root element for preview is `.cms-preview` which maintains the internal
states necessary for rendering within the entwine properties. It provides
@ -24,22 +22,380 @@ function calls for transitioning between these states and has the ability to
update the appearance of the option selectors.
In terms of backend support, it relies on `SilverStripeNavigator` to be rendered
into the `.cms-edit-form`. _LeftAndMain_ will automatically take care of
generating it as long as the `*_SilverStripeNavigator` template is found -
first segment has to match current _LeftAndMain_-derived class (e.g.
`LeftAndMain_SilverStripeNavigator`).
into the form. _LeftAndMain_ will automatically take care of generating it as long
as the `*_SilverStripeNavigator` template is found - first segment has to match the
current _LeftAndMain_-derived class (e.g. `LeftAndMain_SilverStripeNavigator`).
## PHP
For a DataObject to be previewed using the preview panel there are a few prerequisites:
- The class must implement the `CMSPreviewable` interface
- At least one preview state must be enabled for the class
- There must be some valid URL to use inside the preview panel
### CMSPreviewable
The `CMSPreviewable` interface has three methods: `PreviewLink`, `CMSEditLink`, and
`getMimeType`.
#### PreviewLink
The `PreviewLink` method is what determines the URL used inside the preview panel. If
your `DataObject` is intended to always belong to a page, you might want to preview the
item in the context of where it sits on the page using an anchor. You can also provide
some route specific for previewing this object, for example an action on the ModelAdmin
that is used to manage the object.
#### CMSEditLink
This method exists so that when a user clicks on a link in the preview panel, the CMS
edit form for the page the link leads to can be loaded. Unless your `DataObject` is
[acting like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1)
this will likely not apply, but as this method is mandatory and public we may as well
set it up correctly.
If your object belongs to [a custom ModelAdmin](./01_ModelAdmin.md), the edit URL for the
object is predictable enough to construct and return from this method as you'll see below.
The format for that situation is always the same, with increasing complexity if you're
nesting `GridField`s. For the below examples it is assumed you aren't using nested
`GridField`s.
If your object belongs to a page, you can safely get away with returning `null` or an empty
string, as it won't be used. You can choose to return a valid edit link, but because of the
complexity of the way these links are generated it would be difficult to do so in a general,
reusable way.
#### getMimeType
In ~90% of cases will be 'text/html', but note it is also possible to display (for example)
an inline PDF document in the preview panel.
### Preview states
The preview state(s) you apply to your `DataObject` will depend primarily on whether it uses
the [Versioned](api:SilverStripe\Versioned\Versioned) extension or not.
#### Versioned DataObjects
If your class does use the `Versioned` extension, there are two different states available
to you. It is generally recommended that you enable both, so that content authors can toggle
between viewing the draft and the published content.
To enable the draft preview state, use the `$show_stage_link` configuration variable.
```php
private static $show_stage_link = true;
```
To enable the published preview state, use the `$show_live_link` configuration variable.
```php
private static $show_live_link = true;
```
#### Unversioned DataObjects
If you are not using the `Versioned` extension for your class, there is only one preview
state you can use. This state will always be active once you enable it.
To enable the unversioned preview state, use the `$show_unversioned_preview_link`
configuration variable.
```php
private static $show_unversioned_preview_link = true;
```
### Enabling preview for DataObjects in a ModelAdmin
For this example we will take the `Product` and `MyAdmin` classes from the
[ModelAdmin documentation](./01_ModelAdmin.md).
#### The DataObject implementation
As mentioned above, your `Product` class must implement the `CMSPreviewable` interface.
It also needs at least one preview state enabled. This example assumes we aren't using
the `Versioned` extension.
```php
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
class Product extends DataObject implements CMSPreviewable
{
private static $show_unversioned_preview_link = true;
// ...
public function PreviewLink($action = null)
{
return null;
}
public function CMSEditLink()
{
return null;
}
public function getMimeType()
{
return 'text/html';
}
}
```
We will need to add a new action to the `ModelAdmin` to provide the actual preview itself.
For now, assume that action will be called `cmsPreview`. We can very easily craft a valid
URL using the `Link` method on the `MyAdmin` class.
Note that if you had set up this model to [act like a page](https://www.silverstripe.org/learn/lessons/v4/controller-actions-dataobjects-as-pages-1),
you could simply `return $this->Link($action)`. In that case the new action would not need
to be added to your `ModelAdmin`.
```php
public function PreviewLink($action = null)
{
$admin = MyAdmin::singleton();
return Controller::join_links(
$admin->Link(str_replace('\\', '-', $this->ClassName)),
'cmsPreview',
$this->ID
);
}
```
The `CMSEditLink` is also very easy to build, because the edit link used by `ModelAdmin`s
is predictable.
```php
public function CMSEditLink()
{
$admin = MyAdmin::singleton();
$sanitisedClassname = str_replace('\\', '-', $this->ClassName);
return Controller::join_links(
$admin->Link($sanitisedClassname),
'EditForm/field/',
$sanitisedClassname,
'item',
$this->ID
);
}
```
Let's assume when you display this object on the front end you're just looping through a
list of items and indirectly calling `forTemplate` using the [`$Me` template variable](../01_Templates/01_Syntax.md#me).
This method will be used by the `cmsPreview` action in the `MyAdmin` class to tell the
CMS what to display in the preview panel.
The `forTemplate` method will probably look something like this:
```php
public function forTemplate()
{
// If the template for this DataObject is not an "Include" template, use the appropriate type here e.g. "Layout".
return $this->renderWith(['type' => 'Includes', self::class]);
}
```
#### The ModelAdmin implementation
We need to add the `cmsPreview` action to the `MyAdmin` class, which will output the
content which should be displayed in the preview panel.
Because this is a public method called on a `ModelAdmin`, which will often be executed
in a back-end context using admin themes, it pays to ensure we're loading the front-end
themes whilst rendering out the preview content.
```php
use SilverStripe\Admin\ModelAdmin;
use SilverStripe\View\SSViewer;
class MyAdmin extends ModelAdmin
{
private static $managed_models = [
Product::class,
];
private static $url_segment = 'products';
private static $menu_title = 'Products';
private static $allowed_actions = [
'cmsPreview',
];
private static $url_handlers = [
'$ModelClass/cmsPreview/$ID' => 'cmsPreview',
];
public function cmsPreview()
{
$id = $this->urlParams['ID'];
$obj = $this->modelClass::get_by_id($id);
if (!$obj || !$obj->exists()) {
return $this->httpError(404);
}
// Include use of a front-end theme temporarily.
$oldThemes = SSViewer::get_themes();
SSViewer::set_themes(SSViewer::config()->get('themes'));
$preview = $obj->forTemplate();
// Make sure to set back to backend themes.
SSViewer::set_themes($oldThemes);
return $preview;
}
}
```
### Enabling preview for DataObjects which belong to a page
If the `DataObject` you want to preview belongs to a specific page, for example
through a `has_one` or `has_many` relation, you will most likely want to preview
it in the context of the page it belongs to.
#### The Page implementation
For this example we will assume the `Product` class is `Versioned`.
As discussed above, the `CMSEditLink` method is used to load the correct edit form
in the CMS when you click on a link within the preview panel. This uses the
`x-page-id` and `x-cms-edit-link` meta tags in the head of the page (assuming your
page template calls `$MetaTags` in the `<head>` element). When a page loads,
these meta tags are checked and the appropriate form is loaded.
When rendering a full page in the preview panel to preview a `DataObject` on that
page, the meta tags for that page are present. When a content author toggles between
the draft and published preview states, those meta tags are checked and the page's
edit form would be loaded instead of the `DataObject`'s form. To avoid this
unexpected behaviour, you can include an extra GET parameter in the value returned
by `PreviewLink`. Then in the `MetaTags` method, when the extra parameter is
detected, omit the relevant meta tags.
Note that this is not necessary for unversioned `DataObjects` as they only have
one preview state.
```php
use SilverStripe\Control\Controller;
use SilverStripe\View\Parsers\HTML4Value;
class ProductPage extends Page
{
//...
private static $has_many = [
'Products' => Product::class,
];
public function MetaTags($includeTitle = true)
{
$tags = parent::MetaTags($includeTitle);
if (!Controller::has_curr()) {
return;
}
// If the 'DataObjectPreview' GET parameter is present, remove 'x-page-id' and 'x-cms-edit-link' meta tags.
// This ensures that toggling between draft/published states doesn't revert the CMS to the page's edit form.
$controller = Controller::curr();
$request = $controller->getRequest();
if ($request->getVar('DataObjectPreview') !== null) {
$html = HTML4Value::create($tags);
$xpath = "//meta[@name='x-page-id' or @name='x-cms-edit-link']";
$removeTags = $html->query($xpath);
$body = $html->getBody();
foreach ($removeTags as $tag) {
$body->removeChild($tag);
}
$tags = $html->getContent();
}
return $tags;
}
}
```
#### The DataObject Implementation
Make sure the Versioned `Product` class implements `CMSPreviewable` and enables
the draft and published preview states.
```php
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\ORM\DataObject;
use SilverStripe\Versioned\Versioned;
class Product extends DataObject implements CMSPreviewable
{
private static $show_stage_link = true;
private static $show_live_link = true;
private static $extensions = [
Versioned::class,
];
private static $has_one = [
'ProductPage' => ProductPage::class,
];
// ...
public function PreviewLink($action = null)
{
return null;
}
public function CMSEditLink()
{
return null;
}
public function getMimeType()
{
return 'text/html';
}
}
```
Implement a method which gives you a unique repeatable anchor for each
distinct `Product` object.
```php
/**
* Used to generate the id for the product element in the template.
*/
public function getAnchor()
{
return 'product-' . $this->getUniqueKey();
}
```
For the `PreviewLink`, append the `DataObjectPreview` GET parameter to the
page's frontend URL.
```php
public function PreviewLink($action = null)
{
// Let the page know it's being previewed from a DataObject edit form (see Page::MetaTags())
$action = $action . '?DataObjectPreview=' . mt_rand();
// Scroll the preview straight to where the object sits on the page.
if ($page = $this->ProductPage()) {
$link = $page->Link($action) . '#' . $this->getAnchor();
return $link;
}
return null;
}
```
The CMSEditLink doesn't matter so much for this implementation. It is required
by the `CMSPreviewable` interface so some implementation must be provided, but
you can safely return `null` or an empty string with no repercussions in this
situation.
#### The Page template
In your page template, make sure the anchor is used where you render the objects.
This allows the preview panel to be scrolled automatically to where the object
being edited sits on the page.
```ss
<%-- ... --%>
<% loop $Products %>
<div id="$Anchor">
<%-- ... --%>
</div>
<% end_loop %>
```
## Javascript
### Configuration and Defaults
We use `ss.preview` entwine namespace for all preview-related entwines.
[notice]
Caveat: `SilverStripeNavigator` and `CMSPreviewable` interface currently only
support SiteTree objects that are _Versioned_. They are not general enough for
using on any other DataObject. That pretty much limits the extendability of the
feature.
[/notice]
## Configuration and Defaults
Like most of the CMS, the preview UI is powered by
[jQuery entwine](https://github.com/hafriedlander/jquery.entwine). This means
its defaults are configured through JavaScript, by setting entwine properties.
@ -96,7 +452,7 @@ in the `silverstripe/admin` module.
To understand how layouts are handled in the CMS UI, have a look at the
[CMS Architecture](cms_architecture) guide.
## Enabling preview
### Enabling preview
The frontend decides on the preview being enabled or disabled based on the
presence of the `.cms-previewable` class. If this class is not found the preview
@ -113,7 +469,7 @@ The preview can be affected by calling `enablePreview` and `disablePreview`. You
can check if the preview is active by inspecting the `IsPreviewEnabled` entwine
property.
## Preview states
### Preview states
States are the site stages: _live_, _stage_ etc. Preview states are picked up
from the `SilverStripeNavigator`. You can invoke the state change by calling:
@ -133,7 +489,7 @@ You can get the current state by calling:
$('.cms-preview').entwine('.ss.preview').getCurrentStateName();
```
## Preview sizes
### Preview sizes
This selector defines how the preview iframe is rendered, and try to emulate
different device sizes. The options are hardcoded. The option names map directly
@ -158,7 +514,7 @@ You can find out current size by calling:
$('.cms-preview').entwine('.ss.preview').getCurrentSizeName();
```
## Preview modes
### Preview modes
Preview modes map to the modes supported by the _threeColumnCompressor_ layout
algorithm, see [layout reference](cms_layout) for more details. You
@ -183,7 +539,7 @@ preview is not visible. Currently CMS Actions are a separate area to the preview
option selectors, even if they try to appear as one horizontal bar.
[/notice]
## Preview API
### Preview API
Namespace `ss.preview`, selector `.cms-preview`:
@ -197,6 +553,6 @@ Namespace `ss.preview`, selector `.cms-preview`:
* **disablePreview**: deactivate the preview and switch to the _content_ mode. Preview will re-enable itself when new
previewable content is loaded.
## Related
### Related
* [Reference: Layout](cms_layout)