Here are a few tips for creating new WebCenter Portal ADF Templates. These tips are for front end developers applying a branded template or who are integrating their own custom Javascript enhancements.
There are 2 approaches widely used – the first option is to use pure ADF for everything – the second and one which I follow is the hybrid approach to use HTML and JSTL tags only for templating; as I feel its easier for web designers to skin and maintain a light weight frontend without the need to learn ADF techniques.
Read on for tips on templating –
Lets Start off with a clean ADF Page Template first –
<?xml version='1.0' encoding='UTF-8'?> <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:af="http://xmlns.oracle.com/adf/faces/rich" xmlns:pe="http://xmlns.oracle.com/adf/pageeditor" xmlns:wcdc="http://xmlns.oracle.com/webcenter/spaces/taglib" xmlns:trh="http://myfaces.apache.org/trinidad/html" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:fn="http://java.sun.com/jsp/jstl/functions" xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"> <af:pageTemplateDef var="attrs"> <af:xmlContent> <component xmlns="http://xmlns.oracle.com/adf/faces/rich/component"> <display-name> Clean Portal Template </display-name> <facet> <facet-name> content </facet-name> <description> Facet for content Composer </description> </facet> </component> </af:xmlContent> <!-- Content Composer Container --> <af:group> <af:facetRef facetName="content"/> </af:group> <!-- xContent Composer Container --> </af:pageTemplateDef> </jsp:root>
The first thing to do is add the files to be included in the generated template <head></head>.
So first lets add a generic CSS file ie global.css this is not the ADF Skin and should not contain any ADF skinning syntax ie af|panelGroupLayout {} or hacks like .af_panelGroupLayout {} or compressed CSS adf classes ie – .xyz {}.
<af:resource type="css" source="//css/global.css"/>
This af:resource tag will put either JavaScript or CSS files based on the attribute type into the DOM <head></head> of the generated template.
If your like me – I like to modularise my CSS files into multiple maintainable files like this –
/*Require JS will compress and pull these files into one*/ @import url("import/normalize.css"); @import url("import/main.css"); @import url("import/bootstrap.css"); @import url("import/psa_components.css"); @import url("import/skin.css"); @import url("import/iscroll.css"); @import url("import/responsive.css"); @import url("import/font-awesome.css"); @import url("import/ie8.css"); @import url("import/cache.css");
So you can see global.css acts as a CSS Module container importing the rest of the files. This allows me to maintain and update the CSS files individually ie Normalise, bootstrap, iscroll etc.
What’s also really useful is that with the requireJS library – when I move the files from DEV to SIT OR Live requireJS will compress and pull all those modules into a single global.css removing the imports improving load times.
Next lets add some base scripts to load first in the head before the rest of the page loads.
<af:resource type="javascript" source="//js/libs/plugins.js"/> <af:resource type="javascript" source="//js/libs/modernizr-2.0.6.min.js"/>
So first lets discuss plugins.js
/********************** * Avoid `console` errors in browsers that lack a console. */ (function() { var method; var noop = function () {}; var methods = [ 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeStamp', 'trace', 'warn' ]; var length = methods.length; var console = (window.console = window.console || {}); while (length--) { method = methods[length]; // Only stub undefined methods. if (!console[method]) { console[method] = noop; } } }()); /************************/
This defines an empty method if the object console is not defined.
For more info on JS console and debugging javascript (Chrome Dev Tools) || (FireFox FireBug)
I often leave console.info, log, error methods in my javascript which makes it easier to debug when doing early development – these echo methods will be stripped out when compressed and moved to SIT/Live with the use of requireJS or will be ignore if console is not defined.
(more to come on requirejs in next post)
– this is common for browser like IE and FireFox without firebug enabled – any console objects found would create a JS error if it the above script was not included.
JS Method Chaining and initialisation.. ..
/********************** * Define FB Base Chain if not defined */ var FB = window.FB || {}; //create Base object FB.Base = FB.Base || (function() { return { //create multi-cast delegate. onPortalInit: function(function1, function2) { return function() { if (function1) { function1(); } if (function2) { function2(); } } }, //used for chaining methods chainPSA: function() {} } })(); /************************/
This is important as I use requireJS library as a module loader at the footer of the template. RequireJS asynchronously loads in my dependency libraries ie Jquery, mustache templates and my own custom JS Libs as-well as providing a great tool for optimising and merging of the libraries into 1 file like the CSS above.
After loading the dependencies the chain method is initialised.
(Why load JS files at the footer and not in the <head></head> with af:resource?
Read this – why it is best practise for scripts to load here by yahoo) ( btw – Modernizr needs to load in the <head></head>)
The chain method above allows me to have multiple portlets that can chain their methods to only initialise once all the dependencies have been loaded on the page. ie. If you imagine an onReady event that only initialises when page has loaded and all scripts in the footer of the page have asynchronously loaded – then and only then initialise all methods from the page or portlets or containers that are wrapped in the chain method that require Jquery for example… Without this if I were to use the Jquery method in the portlet body but initialised the jquery script in the footer the page would send a JS error as the portlet jquery method would be unaware of the Jquery API – as it has not yet loaded.
How to setup inline methods –
in the template or portlet body that can access a library method after it async loads in the footer –
FB.Base.chainPSA = FB.Base.onPortalInit(FB.Base.chainPSA, function() { //JS to initialises after Async load of required libs //ie lets alert the jquery version alert('You are running jQuery Version ' + $.fn.jquery); });
This way you could write out FB.Base.chainPSA multiple times throughout your template to store all the required methods that need to be initiated after the page has loaded..
How to Initialise chainPSA
after all libs have loaded.
//load PSA javascript files if (FB.Base.chainPSA) { FB.Base.chainPSA(); }
So first check that chainPSA exist then execute all methods; that’s all there is too it.
An alternative solution which I often use is to setting a global JS variable flag – this enables me to contain and compress the forum portlet scripts in a single file that reads the configuration and data attributes from the portlet after all the JS dependencies have loaded if my main script in the footer – Once loaded it will then search to see if the variable flag exist and then asynchronously load all the required portlet files and dependencies ie
Portlet contains inline JS or JS script
which will be injected into the head –
<af:resource type=”javascript”>
var WCP = WCP || {}; WCP.Portlet = WCP.Portlet || {}; WCP.Portlet.enableForums = true;
</af:resource>
Footer Script
initiases the following after page load
if (WCP.Portlet.enableForums) { //Async call required forum files. }
Setting up Global reusable variables using JSTL
//Create FB Obj; var FB = FB || {}; //Global Namespace get properties. //http://docs.oracle.com/cd/E25054_01/webcenter.1111/e10149/wcsugappb.htm# FB.Global = { wcApp: { defaultSkin: '${fn:escapeXml(WCAppContext.application.applicationConfig.skin)}', logo: '${fn:escapeXml(WCAppContext.application.applicationConfig.logo)}', resourcePath: '${fn:escapeXml(WCAppContext.spacesResourcesPath)}', requestedSkin: '${fn:escapeXml(requestContext.skinFamily)}', title: '${fn:escapeXml(WCAppContext.application.applicationConfig.title)}', URL: '${fn:escapeXml(WCAppContext.applicationURL)}', webCenterURI: '${fn:escapeXml(WCAppContext.currentWebCenterURI)}' }, spaceInfo: { description: '${fn:escapeXml(spaceContext.currentSpace.GSMetadata.description)}', displayName: '${fn:escapeXml(spaceContext.currentSpace.GSMetadata.displayName)}', keywords: '${fn:escapeXml(spaceContext.currentSpace.metadata.keywords)}', name: '${fn:escapeXml(spaceContext.currentSpaceName)}' }, //custom Fishbowl lib restInfo: { trustServiceToken: '${fb_rtc_bean.trustServiceToken}' }, pageInfo: { createDateString: '${fn:escapeXml(pageDocBean.createDateString)}', createdBy: '${fn:escapeXml(pageDocBean.createdBy)}', lastUpdateDateString: '${fn:escapeXml(pageDocBean.lastUpdateDateString)}', lastUpdatedBy: '${fn:escapeXml(pageDocBean.lastUpdatedBy)}', pageName: '${fn:escapeXml(pageDocBean.name)}', pagePath: '${fn:escapeXml(pageDocBean.pagePath)}', pageTitle: '${fn:escapeXml(pageDocBean.title)}', pageUICSSStyle: '${fn:escapeXml(pageDocBean.UICSSStyle)}' }, userInfo: { businessEmail: '${fn:escapeXml(webCenterProfile[securityContext.userName].businessEmail)}', department: '${fn:escapeXml(webCenterProfile[securityContext.userName].department)}', displayName: '${fn:escapeXml(webCenterProfile[securityContext.userName].displayName)}', employeeNumber: '${fn:escapeXml(webCenterProfile[securityContext.userName].employeeNumber)}', employeeType: '${fn:escapeXml(webCenterProfile[securityContext.userName].employeeType)}', expertise: '${fn:escapeXml(webCenterProfile[securityContext.userName].expertise)}', managerDisplayName: '${fn:escapeXml(webCenterProfile[securityContext.userName].managerDisplayName)}', moderator: '${fn:escapeXml(security.pageContextCommunityModerator)}', organization: '${fn:escapeXml(webCenterProfile[securityContext.userName].organization)}', organizationalUnit: '${fn:escapeXml(webCenterProfile[securityContext.userName].organizationalUnit)}', timeZone: '${fn:escapeXml(webCenterProfile[securityContext.userName].timeZone)}', title: '${fn:escapeXml(webCenterProfile[securityContext.userName].title)}' } };
Sometimes there are values from WebCenter that you wish to use ie Space Name or User Name – the easiest way is to escape these values with JSTL into a javascript Object within the page template. I’ve put a quick example above you can strip it out if you don’t need any values but it makes it easier to pull in values to other JS libs calling the key value pair from the object like this for user display name –
var userDisplayName = FB.Global.userInfo.displayName;
Setting RequireJS to load my dependencies via base bootstrap script
(Read this – For more info on module loading and using requirejs)
<script src="/js/core/config.js"><jsp:text/></script> <script src="/js/libs/requirejs/require.min.js" data-main="bootstrap"><jsp:text/></script>
Now as you can use html in JSF templates and I don’t want my scripts in the head – which af:resource enables I write out the <script> tag. A word or warning and you may have spotted <jsp:text/> this prevents the script tags from being self closed and breaking. This will happen if you are editing the page templates direct at runtime from the browser. This is the same for any empty container ie <div></div> would become <div/> self closing; this is fine with xml but not fine with browser interpreting html mark-up in the DOM..
Also you may want to consider putting this into the login template to pre-load and cache the initial scripts before the portal page loads all of the ADF JS lib dependencies.
<?xml version='1.0' encoding='UTF-8'?> <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:af="http://xmlns.oracle.com/adf/faces/rich" xmlns:pe="http://xmlns.oracle.com/adf/pageeditor" xmlns:wcdc="http://xmlns.oracle.com/webcenter/spaces/taglib" xmlns:trh="http://myfaces.apache.org/trinidad/html" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:fn="http://java.sun.com/jsp/jstl/functions" xmlns:fmt="http://java.sun.com/jsp/jstl/fmt"> <af:pageTemplateDef var="attrs"> <af:xmlContent> <component xmlns="http://xmlns.oracle.com/adf/faces/rich/component"> <display-name> Clean Portal Template </display-name> <facet> <facet-name> content </facet-name> <description> Facet for content Composer </description> </facet> </component> </af:xmlContent> <!-- Put resources into head --> <af:resource type="css" source="//css/global.css"/> <af:resource type="javascript" source="//js/plugins.js"/> <af:resource type="javascript" source="//js/libs/modernizr-2.0.6.min.js"/> <!-- xPut resources into head --> <!-- Define static global FB namespace and vars --> <script> //Create FB Obj; var FB = FB || {}; //Global Namespace get properties. //http://docs.oracle.com/cd/E25054_01/webcenter.1111/e10149/wcsugappb.htm# FB.Global = { wcApp: { defaultSkin: '${fn:escapeXml(WCAppContext.application.applicationConfig.skin)}', logo: '${fn:escapeXml(WCAppContext.application.applicationConfig.logo)}', resourcePath: '${fn:escapeXml(WCAppContext.spacesResourcesPath)}', requestedSkin: '${fn:escapeXml(requestContext.skinFamily)}', title: '${fn:escapeXml(WCAppContext.application.applicationConfig.title)}', URL: '${fn:escapeXml(WCAppContext.applicationURL)}', webCenterURI: '${fn:escapeXml(WCAppContext.currentWebCenterURI)}' }, spaceInfo: { description: '${fn:escapeXml(spaceContext.currentSpace.GSMetadata.description)}', displayName: '${fn:escapeXml(spaceContext.currentSpace.GSMetadata.displayName)}', keywords: '${fn:escapeXml(spaceContext.currentSpace.metadata.keywords)}', name: '${fn:escapeXml(spaceContext.currentSpaceName)}' }, //custom Fishbowl lib restInfo: { trustServiceToken: '${fb_rtc_bean.trustServiceToken}' }, pageInfo: { createDateString: '${fn:escapeXml(pageDocBean.createDateString)}', createdBy: '${fn:escapeXml(pageDocBean.createdBy)}', lastUpdateDateString: '${fn:escapeXml(pageDocBean.lastUpdateDateString)}', lastUpdatedBy: '${fn:escapeXml(pageDocBean.lastUpdatedBy)}', pageName: '${fn:escapeXml(pageDocBean.name)}', pagePath: '${fn:escapeXml(pageDocBean.pagePath)}', pageTitle: '${fn:escapeXml(pageDocBean.title)}', pageUICSSStyle: '${fn:escapeXml(pageDocBean.UICSSStyle)}' }, userInfo: { businessEmail: '${fn:escapeXml(webCenterProfile[securityContext.userName].businessEmail)}', department: '${fn:escapeXml(webCenterProfile[securityContext.userName].department)}', displayName: '${fn:escapeXml(webCenterProfile[securityContext.userName].displayName)}', employeeNumber: '${fn:escapeXml(webCenterProfile[securityContext.userName].employeeNumber)}', employeeType: '${fn:escapeXml(webCenterProfile[securityContext.userName].employeeType)}', expertise: '${fn:escapeXml(webCenterProfile[securityContext.userName].expertise)}', managerDisplayName: '${fn:escapeXml(webCenterProfile[securityContext.userName].managerDisplayName)}', moderator: '${fn:escapeXml(security.pageContextCommunityModerator)}', organization: '${fn:escapeXml(webCenterProfile[securityContext.userName].organization)}', organizationalUnit: '${fn:escapeXml(webCenterProfile[securityContext.userName].organizationalUnit)}', timeZone: '${fn:escapeXml(webCenterProfile[securityContext.userName].timeZone)}', title: '${fn:escapeXml(webCenterProfile[securityContext.userName].title)}' } }; </script> <!-- xDefine static global namespace and vars --> <!-- Content Composer Container --> <af:group> <!-- Add any custom HTML HERE --> <div id="FB-wrapper" class="wrapper"> <af:facetRef facetName="content"/> </div> <!-- xAdd any custom HTML HERE --> </af:group> <!-- Content Container --> <!-- Init RequireJS --> <script src="/js/core/config.js"><jsp:text/></script> <script src="/js/libs/requirejs/require.min.js" data-main="bootstrap"><jsp:text/></script> <!-- xInit RequireJS --> </af:pageTemplateDef> </jsp:root>
** I haven’t added a navigation structure to this template only the Content Composer facet.
Designing for mobile with Responsive Design.
When learning about responsive design the first thing to do is add the following meta viewport tag into the head defining the required content params for mobile ie –
<meta name="viewport" content="width=device-width, initial-scale=1">
Now unfortunately there is no ADF tag to add meta tags into the header and although you could add this html to the <body></body> its not really ideal.
What you should do is add this meta viewport into the Page Style not the Page Template.
This will allow you to add the following trinidad tag to generate the meta tag into the <head></head> of the generated template as the facet metaContainer specifies this region to generate into.
<f:facet name="metaContainer"> <af:group> <trh:meta name="viewport" content="width=device-width, initial-scale=1"/> </af:group> </f:facet>
The metaContainer facet should be held within the <af:document></af:document> tags
You could also add other meta tags ie keywords and add a dynamic value like this –
<trh:meta name="keywords" content="#{bindings.SEO_KEYWORDS}"/>
A word of warning you must make sure you add this to the page style before you create a page. Existing pages will ignore any updates to Page Styles that were used to create a page unlike Page Templates which allow you to tweak and update on the fly.
In the next post I’ll be writing up how to use requireJS properly with WebCenter to Asynchronously load in your libraries, templates and request additional libraries or templates when required by the page.