-
Notifications
You must be signed in to change notification settings - Fork 1
Page type handling
As part of the new CMS backend system, resource handling for page types has been altered. Although backwards compatibility is available for traditional views, it's highly recommended to change views to use this new setup.
A backend view for a page type should no longer consist of server-side generated HTML and JavaScript. Instead a JSON string is available on the server to define resources and a reference to the JavaScript class that handles the functionality of this page type.
By doing this, large performance gains can be achieved both in reduced server CPU and bandwidth usage, as well as reducing delays and bandwidth usage on the client. Because per page type, these resources only need to be loaded once and is from there on cached client-side by the browser.
To use the new page types system, you create a new folder called pagetypes
within your component and a folder with the name of your page type (for each page type). Inside this folder only one file is required: definition.json
. This JSON file defines the resources that are needed and the name of the PageType class as defined by one of the JavaScript files included.
From there on, the PageType class defines which tabs are present, which templates they use and what to do when the page is saved. Finally, a server-side script should be present that can be used to save the entered information in the database (using REST is recommended for this).
To clarify, this is what an example folder structure of the text
component would look like.
text
|
+-pagetypes
| |
| +-text
| |
| +-definition.json
| +-TextController.js
+-Json.php
The definition file defines the following things:
- What is the controller for this page type?
- Which JavaScript files are required?
- Which
jQuery.tmpl()
template files are required?
An example definition.json
file looks like this.
{
"javascript": [
"TextController.js"
],
"templates": {
"contentTab": "contentTab.tmpl.php",
"settingsTab": "settingsTab.tmpl.php"
},
"controller": "cmsBackend.text.TextController"
}
This attribute provides the name of the controller class that handles this page type's client-side scripting.
It's relative to the window
global variable and can be namespaced through dots.
The example above would try to do the following:
var controller = new window.cmsBackend.text.TextController(/* arguments */);
This attribute is an array of JavaScript files relative to this page type's folder.
The JavaScript file defined in this example would resolve as:
http://mysite.com/site/components/text/pagetypes/text/TextController.js
This is an object defining which templates will be available to the controller class.
The keys will be used for referencing inside the controller. The values are relative URL's to this page type's folder.
The template named contentTab
will resolve to the contents of the file:
http://mysite.com/site/components/text/pagetypes/text/contents-tab.tmpl.html
The controller defined in the definition.json
handles the client-side scripting for this page type.
There's a class available using the jsFramework
plug-in called PageType
from which you can inherit.
This handles the majority of commonly used functionality for you and allows you to create a controller for your page type with ease.
An example is the TextController.js
file:
(function($, exports){
//Create a new page type controller called TextController.
var TextController = PageType.sub({
//Define the tabs to be created.
tabs: {
'Content': 'contentTab',
'Text settings': 'settingsTab'
},
//Define the elements for jsFramework to keep track of.
elements: {
'contentForm': 'form.text-contentTab-form',
'settingsForm': 'form.text-settingsTab-form'
},
//Retrieve input data (from the server probably).
getData: function(pageId){
//TODO: retrieve input data from the server based on the page ID
//Example data.
return {foo: 'bar'};
},
//When rendering of the tab templates has been done, do some final things.
afterRender: function(){
//Turn the form on the content tab into a REST form.
this.contentForm.restForm();
//TODO: do things like initialize WYSIWYG editor.
},
//Saves the data currently present in the different tabs controlled by this controller.
save: function(pageId){
//Trigger submit on the content form.
this.contentForm.trigger('submit');
//TODO: use a custom way of submitting the settings tab's data to the server.
}
});
//Export the namespaced class.
TextController.exportTo(exports, 'cmsBackend.text.TextController');
})(jQuery, window);
You can see here that using the templates to create tabs is as simple as giving the tab a title and refer to the templates you defined in the definition.json
file.
It may seem slightly redundant in this example, however templates defined inside the definition.json
file are automatically cached client-side by the CMS
and it also allows for a short notation inside the templates themselves, when nesting templates (using the {{tmpl}}
tag). We'll get to that in the templating chapter.
The save
method is required and is given the page ID to be used for saving as it's only argument.
The counterpart to this is the getData
method. Also with the only argument being page ID, it's responsible for retrieving the data you need from the server.
In this case though the ID can also be empty, meaning a new page is still to be created.
The afterRender
method is optional, but very useful. For example when applying a WYSIWYG editor or an image uploader.
Note that while you certainly could, it's not recommended to use afterRender
for binding events.
PageType
inherits from Controller
from the jsFramework
plug-in, so you can define the elements
and events
attributes to do this in a way that's easier to read and maintain.
Retrieving the data from the templates is the responsibility of this class to implement for the sake of saving.
A quick way to do this would be to use the restForm
function (from the jquery_rest
plug-in) in the afterRender
method and triggering a submit
event in the save
method.
The reason this is not automatically done for you is so that you can implement rich JavaScript-based editors without having to create a <form>
element to hold the end results.
The templates provided in the definition.json
files are processed on the server first, however the server should never insert data into the templates.
A template is loaded only once on the server and from there on cached on the client.
The processing in PHP is done so that the templates can be translated into the administrator's preferred language before sending to the client.
The client in turn should insert the actual data, using jQuery.tmpl()
.
Here is an example template contentTab.tmpl.php
.
<?php namespace components\text; if(!defined('TX')) die('No direct access.'); ?>
<form method="PUT" action="?rest=text/page_text/" class="text-contentTab-form">
<p class="example">
<!--
Example of how to use input data.
Remember the 'getData' method from TextController.js?
It returns {foo: "bar"}, this has been stored in the 'data' variable.
-->
${data.foo}
<!-- Gives: bar -->
</p>
{{each(language_id, language) languages}}
<div class="multilingual-section" data-language-id="${language_id}">
<?php
//Shorten the "Title in " translations.
$title_in = __($component, 'Title', true).' '.__('IN_LANGUAGE_NAME', true, 'lowercase').' ';
?>
<input type="text" class="title" name="info[${language_id}][title]"
placeholder="<?php echo $title_in; ?>${language.title}" value="${data.info[language_id]['title']}" />
<textarea name="info[${language_id}][text]" class="text editor">
${data.info[language_id]['text']}
</textarea>
</div>
{{/each}}
</form>
What you can see here is that on the PHP end, it only prevents direct access (like all templates we create) and it translates some labels using the __()
helper function.
In PHP the following variables are available:
-
$component
- The name of the current component. Required for component specific translations. -
$pagetype
- The name of the pagetype that this template is being used for. - All global helper functions, such as
tx()
and__()
.
Every time the page type is used on the client it will supply a large set of data for you to use in jQuery.tmpl()
.
The data retrieved by the getData
method is among this, but on top of that there is runtime data available on the client also being passed onto the templating engine.
Here is a breakdown of this data structure as a JavaScript object (and comments).
{
/**
* The ID of the page. For example; 1.
*/
"page_id": 1
/**
* The data that you retrieve from the server using the getData method.
*/
"data": {/* the result of getData */},
/**
* The languages that are enabled in the CMS.
* This is exceptionally useful when you want your component to support multi-language features.
*/
"languages": {
/**
* The format of this is <language_id>: <language_object>
* So this is language ID 1.
*/
"1": {
/**
* The code for the locale. In format <language_code>-<COUNTRY_CODE>.
*/
"code": "en-GB",
/**
* The shortcode for the language.
*/
"shortcode": "EN",
/**
* The title of this language, translated in the language of the current user.
* Don't use this for switch or if-statements. As it will change when the user changes language.
*/
"title": "British English"
},
/**
* Another example for Dutch.
*/
"2": {
"code": "nl-NL",
"shortcode": "NL",
"title": "Dutch"
}
},
/**
* A reference to each template defined in the definition.json file.
* Very useful if you want to nest your templates, break your template into smaller sections,
* or want to make your template recursive.
*/
"templates": {
/**
* This is the jQuery object of the templates you defined.
* You can use them directly inside a template with the {{tmpl}} tag.
* For instance: {{tmpl templates.contentTab}}
*/
"contentTab": $("<form method= ... /form>"),
"settingsTab": $("...")
}
}
You might have noticed in the template we used an {{each}}
statement for the languages and made a <div>
element with the multilingual-section
class.
The PageType
controller class automatically detects this type of structure to make implementing multilingual components easier.
In the backend there are tabs created for each of the languages in the languages
attribute.
When using the multilingual section setup as in the example, these will be made functional automatically.
Language tabs example.
Clicking one of the other tabs would switch to the corresponding multilingual-section
div for that language.
This is why the data-language-id
attribute is set on the div. Clicking the "Dutch" tab in this case would switch to the following div.
<div class="multilingual-section" data-language-id="2">
<input type="text" class="title" name="info[2][title]"
placeholder="Title in Dutch" value="" />
<textarea name="info[2][text]" class="text editor">
</textarea>
</div>
TODO:
- Give server-side examples to complete the case