Creating custom content types
In your own application or plugins you can add new content types that will be editable by your users and displayed in the repository along with the standard content types.
These custom types can have full control over how a request is handled (e.g. issue a redirect, like WcmExternalLink does) or delegate to established rendering mechanisms within Weceem. You might have it call out to a PDF generator for example, or render some data using an alternative presentation technology e.g. wiki rendering or Freemarker. They can also specify how their properties should be edited in the repository, and hook into events.
To create a new content type, you just need to create a new Grails domain class that extends the WcmContent class and follow a few simple conventions. You can create a basic template for a new content type using the create-content-class script:
grails create-content-class com.mycompany.Product
This will create the domain class under grails-app/domain/, with some default placeholder code. Below is an example of the contentions.
import org.weceem.util.ContentUtils
import org.weceem.content.WcmContent
class PressRelease extends WcmContent {
// Set to false if this content node should never actually be rendered
// i.e. Groovy Script nodes cannot be rendered directly.
static standaloneContent = false
String getMimeType() { "text/html" }
String content
/**
* Must be overriden by content types that can represent their content as text.
* Used for search results and versioning
*/
public String getContentAsText() { ContentUtils.htmlToText(content) }
/**
* Should be overriden by content types that can represent their content as HTML.
* Used for wcm:content tag (content rendering)
*/
public String getContentAsHTML() { content }
static constraints = {
content(nullable: false, maxSize: 65536)
}
static editors = {
content(editor:'HtmlCode')
}
static transients = WcmContent.transients
}
The above declares a new type which, when created, will immediately become editable in the repository when you run your application. There are several conventions that apply, described in more detail below.
Basic considerations
Your descendent class has certain conventions it must honour for correct operation:
-
the Grails "transients" property must be populated with the sum of the superclass transients and any new transient properties you add (should not be necessary for newer Grails versions)
-
the "searchable" property convention must be defined as per Grails Searchable plugin, to add any fields that should be indexed for searching
Controlling the editing of your content type
The "editors" closure property is used, like Grails constraints closure, to customize behaviour for the properties of the domain class - but in this case how they are treated in the Weceem editor.
For each field of your content domain class you can define the order in which they appear in the editor - defined by the order you list them in the editors closure.
You can also set the editor used for each property. These have names like HtmlCode, ReadOnly, CssCode, GroovyCode etc. You can also define your own editors if the supplied editors are not suitable. See the section on [customizing content editors].
Some properties are for internal use only, for those you can set hidden:true.
Finally, you can push your properties into the "Extras" editor group that is displayed separately by setting "group" to "extra":
static editors = {
isbn(editor:'ISBNEditor')
genre(editor:'GenrePopup', group:'extra')
}
Displaying content as text or HTML
For various reasons such as search results and versioning, Weceem needs to be able to get some kind of representation of your custom content as both plain text and HTML. If your needs are different from the defaults, you can override the methods for getContentAsText and getContentAsHTML. The default behaviour is:
/**
* Must be overriden by content types that can represent their content as text.
* Used for search results and versioning
*/
String getContentAsText() { "" }
/**
* Should be overriden by content types that can represent their content as HTML.
* Used for wcm:content tag (content rendering)
*/
String getContentAsHTML() { contentAsText ? contentAsText.encodeAsHTML() : '' }
So as a minimum you should override getContentAsText() - but if you have a reasonable HTML representation, e.g. for HTML, Wiki or other richly formatted content, you should also implement getContentAsHTML().
Defining the mime type
Weceem's content controller will ask the content for its MIME type when building the response. In many content types this is a constant value and is not persisted, but in types such as WcmContentFile this is persisted so that the type can be overriden by the user.
To return the correct MIME type, just implement the getter:
String getMimeType() { "text/html" }
This non-static value is read whenever the content is to be served. This means that you can determine it on a per-node basis, which is how the WcmContentFile type specifies the mime types of downloadable files.
Customizing the rendering of your content
Weceem delegates the rendering of content to the content node itself. If there is no rendering implementation provided, it will simply render the node as text to the client. This mechanism is used by the built in content types to render themselves appropriately. Some types render HTML decorated by a template, and others issue redirects.
There are two conventions that control the custom rendering of content:
-
The static "standaloneContent" property is set to false if the content type is not intended for direct rendering. This prevents node types that are used by other types from being rendered when their URI is requested. A good example of this is the built in template and widget types, which make no sense being rendered on their own.
-
The static "handleRequest" Closure property provides the custom implementation of rendering if you require it.
The handleRequest closure is executed so that it delegates to Weceem's content controller. This means that you can use all the regular Grails dynamic controller methods and properties (render, redirect, response, request, session) as well as some custom methods to render Weceem content nodes using the normal GSP and templating mechanisms.
Here's an example of how WcmHTMLContent renders its regular HTML or GSP code:
static handleRequest = { content ->
if (content.allowGSP) {
renderGSPContent(content)
} else {
renderContent(content)
}
}
This implementation calls WcmContentController's renderContent method if the content is HTML, which will render the content inside a Template if the node has one. Alternatively if the content is classed as GSP content, it calls the controller's renderGSPContent method which renders the content as GSP code, inside a template if one is found for the node.
Another example is the External Link type which redirects the browser to the target URL of the node:
static handleRequest = { content ->
redirect(url:content.url)
}
This implementation uses the standard Grails "redirect" method to redirect to the URL specified in the content's "url" property.
Interacting with content events
Since Weceem 1.0-M2, content nodes can hook into common events, such as deletion and updates. To do this your content class needs to implement methods for any events it wants to support. None of these are mandatory, and you implement only those you need. The available event methods are defined in
WeceemDomainEvents.groovy
The event methods are:
boolean contentShouldBeCreated(WcmContent parentNodeOrNull)
This method allows the content to veto whether or not it can be created. This is called on the newly populated but not saved content node, and is passed the parent node that it would be attached to, which can be null if it is root level content.
void contentDidGetCreated()
This is called after the content node has been created and saved.
boolean contentShouldBeDeleted()
This allows the content type to veto whether or not the node can or should be deleted. For example it may not be possible to delete a content node if it has children.
void contentWillBeDeleted()
This event is triggered just before the node is deleted.
void contentDidGetDeleted()
This event is triggered immediately after the node is deleted.
void contentDidChangeTitle(String previousTitle)
This event is called when the title property of a node has changed.
void contentDidGetUpdated()
This event is triggered after a node has been edited and the new values saved.
boolean contentShouldMove(WcmContent targetParent)
This event is called to allow it to veto being moved to parent nodes that are not compatible. Returning false will prevent the user moving the node (changing its parent).
void contentDidMove()
This is called after the content's parent has been changed and saved.
boolean contentShouldAcceptChildren()
This event is called to find out if the current node can accept child nodes.
boolean contentShouldAcceptChild(WcmContent newChild)
This event is called to find out if the current node can accept the new child node. There might be per-node reasons for vetoing new children!
You can also hook into many events outside of the domain classes themselves, using the Event Service.
Customizing the icon shown in the repository
You can supply an icon for your custom content types, that will be used in the admin UI of Weceem, e.g. in the repository view. To do this, you define a static "icon" property which contains the arguments you would pass to Grails g:resource tag to create a link to the resource:
static icon = [
plugin: "weceem",
dir: "_weceem/images/weceem/content-icons",
file: "widget-32.png"
]
If you are placing your custom content class in a plugin of your own, it is vital to include the "plugin" attribute and value.
Content versioning
In order to participate in Weceem's content versioning system, you need to add any properties you wish to have serialized to the getVersioningProperties() implementation of your class. This function returns a map of values that are stored in the content revision.
These are simple values that must be expressable as strings, and by necessity you may refer to objects not available at the time the user views your revision later, so all complex object references that need to be versioned should be coerced to some kind of text representation.
For a real example, here's the code from WcmHTMLContent:
Map getVersioningProperties() {
def r = super.getVersioningProperties() + [
menuTitle:menuTitle,
htmlTitle:htmlTitle,
keywords:keywords,
template:template?.ident() // A string representation of template object
]
return r
}
In almost all cases you will want to call the super method to ensure all inherited properties are correctly versioned.
Controlling parentage
[This mechanism is currently under review]