decaporg / decaporg/decap-cms

API Overhaul: Preview Templating

Open
#1,041 16 comments 27 reactions 0 assignees View on GitHub
pinned
Dominant language
JavaScript
Stars
19.4k
Forks
3.1k
Avg merge
1d 14h
Merged PRs (30d)
9

Description

Let's make custom preview templating better.

This issue outlines a potential approach for improving the custom preview authoring experience through documentation driven development. Rough documentation will be drawn up right in the comments describing an ideal custom preview interface, and POC's (proof of concept) validating these proposals can be linked as we go.

**Quick Links:**
- [POC Branch](https://github.com/netlify/netlify-cms/compare/api-register-preview-template)
- [Deploy Preview](https://api-register-preview-template--cms-demo.netlify.com/)

### Current
Currently, registering a preview template looks like this:

```js
var PostPreview = createClass({
render: function() {
var entry = this.props.entry;
var image = entry.getIn(['data', 'image']);
var bg = image && this.props.getAsset(image);
return h('div', {},
h('div', {className: "cover"},
h('h1', {}, entry.getIn(['data', 'title'])),
bg ? h('img', {src: bg.toString()}) : null
),
h('p', {},
h('small', {}, "Written " + entry.getIn(['data', 'date']))
),
h('div', {"className": "text"}, this.props.widgetFor('body'))
);
}
});
```

It's a generally effective templating system, but with three primary drawbacks:

* It's written to work with React's `createElement` method, which isn't a newfangled interface by any means, but may be foreign to a lot of folks.
* Lots of cruft.
* It's hampered by the awkward `widgetFor`/`widgetsFor` interface for retrieving widgets and entry data.

### Proposed
Ideally, developers could provide a simple mustache/handlebars template - here's the above example rewritten in handlebars:

```js
var postPreviewTemplate = `



{{title}}


{{#image}}

{{/image}}


Written {{date}}



{{widgetFor "body"}}


`;
```

This style of templating should prove far more approachable, simpler for troubleshooting, and clearer in purpose. Handlebars helper functions would be created to apply necessary functionality, for example, our use of `getAsset` in the template above.

### widgetFor / widgetsFor
As mentioned before, the `widgetFor` / `widgetsFor` methods for getting values from shallow and deep fields, respectively, are confusing to use. `widgetFor` accepts a field name and returns a React component with the field's value wrapped in the preview component, ready for rendering. `widgetsFor` accepts an array of keys for accessing a nested field, but instead of returning a React component, it returns an object with keys "widget" and "data", for accessing the component or just the raw value respectively.

Instead, `widgetFor` should handle both jobs, accepting either a field name string, or else an array of nested field names for deep retrieval. It should always return a React component, as raw values, including nested ones, are already available on the `entry` object.

### Current
An implementation from the example project that uses both `widgetFor` and `widgetsFor`:

```js
var GeneralPreview = createClass({
render: function() {
var entry = this.props.entry;
var title = entry.getIn(['data', 'site_title']);
var posts = entry.getIn(['data', 'posts']);
var thumb = posts && posts.get('thumb');

return h('div', {},
h('h1', {}, title),
h('dl', {},
h('dt', {}, 'Posts on Frontpage'),
h('dd', {}, this.props.widgetFor(['posts', 'front_limit']) || 0),

h('dt', {}, 'Default Author'),
h('dd', {}, this.props.widgetsFor('posts').getIn(['data', 'author']) || 'None'),

h('dt', {}, 'Default Thumbnail'),
h('dd', {}, thumb && h('img', {src: this.props.getAsset(thumb).toString()}))
)
);
}
});
```

### Proposed
Here's what the above template would look like with the proposed removal of `widgetsFor`:

```js
var generalPreviewTemplate = `


{{ site_title }}



{{#posts}}
Posts on Frontpage

{{widgetFor "front_limit"}}

Default Author

{{author}}

Default Thumbnail


{{#thumb}}{{/thumb}}
{{/posts}}


`;
```

### Proof of Concept
A branch can be referenced here:
https://github.com/netlify/netlify-cms/compare/api-register-preview-template

Deploy preview here:
https://api-register-preview-template--cms-demo.netlify.com

The first example above is working in the POC - the preview for Post entries is created using a handlebars template in `example/index.html`. The second example has not yet been implemented.

# Documentation

### Customizing the Preview Pane

The preview pane shows raw, roughly formatted content by default, but you can
register templates and styles so that the preview matches what will appear when
the content is published. Netlify CMS has a few options for doing this.

#### `registerPreviewTemplate`

The `registerPreviewTemplate` registry method accepts a name, a template string,
a data provider function, and an optional [template
parser](#register-template-parser) name.

| param | required | type | default | description |
| --------------- | -------- | ------------------------- | -------------------------------------------------- | ----------------------------------------------- |
| name | yes | string | n/a | Used to reference the template in configuration |
| template | yes | React component or string | n/a | The raw template |
| dataProvider | - | function | n/a | Accepts raw entry data and returns prepared template data |
| parserName | - | string | "handlebars" if template is a string, otherwise "" | The name of a registered template parser |

Each example below, given a `title` field value of "My First Post", will output:

```html

My First Post


```

**Example using Handlebars**

Netlify CMS ships with a Handlebars template parser that is registered and used
by default for any string templates.

```js
/**
* With ES6 + modules and Webpack
* Use [raw-loader](https://github.com/webpack-contrib/raw-loader) to import template text via Webpack.
*/
import { registerPreviewTemplate } from 'netlify-cms'
import postTemplate from './post-template.hbs' // handlebars template, contains "

{{title}}

"

registerPreviewTemplate("post", postTemplate)

/**
* With ES5
* Use `CMS` global to access registry methods.
*/
var postTemplate = "

{{title}}

"
CMS.registerPreviewTemplate("post", postTemplate)
```

**Example using a React Component**

Template parsers output a React component which the CMS uses directly, but you
can also bypass templating and create the React component yourself for tighter
control.

Note: field values are accessed by the template component via the raw `entry`
prop, which is an [Immutable.js
Map](https://facebook.github.io/immutable-js/docs/#/Map), where each field value
is stored by name under the `data` property. For example, accessing the title
field value on the `entry` prop looks like: `entry.getIn(['data', 'title'])`.

```js
/**
* With ES6 + modules, JSX, and Webpack
*/
import React from 'react'
import { registerPreviewTemplate } from 'netlify-cms'

export class PostTemplate extends React.Component {
render() {
return

{this.props.entry.getIn(['data', 'title'])}


}
}

registerPreviewTemplate("post", PostTemplate)

/**
* With ES5
* Use `CMS` global to access registry methods.
* Use `createClass` global to access [create-react-class](https://www.npmjs.com/package/create-react-class).
* Use `h` global to access [React.createElement](https://reactjs.org/docs/react-api.html#createelement).
*/
var PostTemplate = createClass({
render: function() {
return h('h1', {}, this.props.entry.getIn(['data', 'title']))
}
})

CMS.registerPreviewTemplate("post", PostTemplate)
```

**Example using a custom data provider**

When reusing a production template, the data object expected by the template
will often be different from the one passed into the template parser by Netlify
CMS. To address this, you can pass in a data provider function that receives
the data object provided by Netlify CMS and returns an object that will work
with your template. The received value will be an Immutable Map, and the
function must return an Immutable Map.

Note that the data provider function doesn't receive all of the props that are
passed to the template parser, just the `data` prop, which contains the entry
values.

```js
/**
* With ES6 + modules and Webpack
* Use [raw-loader](https://github.com/webpack-contrib/raw-loader) to import template text via Webpack.
*/
import { registerPreviewTemplate } from 'netlify-cms'
import postTemplate from './post-template.hbs' // handlebars template, contains "

{{post.title}}

"

const providePostData = data => data.setIn(['post', 'title'], data.get('title'))

registerPreviewTemplate("post", postTemplate, providePostData)

/**
* With ES5
* Use `CMS` global to access registry methods.
* Uses an inline template since site templates shouldn't be available in production.
*/
var postTemplate = "

{{post.title}}

"

var providePostData = function(data) {
return data.setIn(['post', 'title'], data.get('title'))
}

CMS.registerPreviewTemplate("post", postTemplate, providePostData)
```

#### `registerTemplateParser`

The `registerTemplateParser` registry method accepts a name and a parsing
function.

| param | required | type | default | description |
| ------- | -------- | -------- | ------- | ------------------------------------------------------------------------ |
| name | yes | string | n/a | Used to reference the parser when registering templates |
| parser | - | function | n/a | Accepts a string template, template data, and options hash; returns HTML |
| options | - | object | `{}` | Passed through to parser function |

**Example**

We'll create a simplified [EJS](http://ejs.co/) template parser to demonstrate.
Note that a real parser would need to provide a few helpers for a complete
integration (docs TBA).

```js
// ES6
import ejs from 'ejs'
import { registerTemplateParser } from 'netlify-cms'

const ejsParser = (template, data, opts) => {
return ejs.render(template, data, opts)
}

registerTemplateParser('ejs', ejsParser)
```

Given template `

<%= title %>

` and data `{ title: 'My First Post' }`,
this parser would output: `

My First Post

`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.