« Back

Advanced Web Content Example with AJAX

Staff Blogs February 2, 2011 By Ray Augé Staff

This example demonstrates several advanced features of Liferay's Web Content Management provided when taking full advantage of Liferay's Web Content Template processing engine. It'll demonstrate implementing PHASES of the portlet lifecycle, performing AJAX calls, using Alloy Javascript Library, using Liferay's SearchEngine, converting java objects to JSON for passing back as AJAX response body all from within our templates.

It starts with a structured piece of content we'll call a Widget, defined by the following Web Content Structure:

<root>
  <dynamic-element name='name' type='text' index-type='text' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[Enter plain text only.]]></entry>
			<entry name="label"><![CDATA[Widget Name]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
  <dynamic-element name='description' type='text_box' index-type='text' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[Prefer to use plain text here, at worst use only simple HTML tags like strong, em, etc.]]></entry>
			<entry name="label"><![CDATA[Description]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
  <dynamic-element name='image' type='document_library' index-type='keyword' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[]]></entry>
			<entry name="label"><![CDATA[Widget Image]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
  <dynamic-element name='document' type='document_library' index-type='keyword' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[Link a PDF document preferably.]]></entry>
			<entry name="label"><![CDATA[Widget Document]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
</root>

Make sure that each field of the structure is marked as "Searchable" as either text or token otherwise later on we won't be able to individually access those fields. The template for this structure might look like this (but it's not overly important for this example):

<h3>$name.data</h3>

<p><img style="float: left;" src="$image.data" alt="$name.data"/> $description.data</p>

<a href="$document.data">Spec Sheet</a>

Now given that we have a few pieces of content using this structure, our goal is to create an enhanced view that leverage AJAX in order to be cool... er fullfill our business requirements!

So, we're going to create a structure to act as the settings schema for a simple Web Content app:

<root>
  <dynamic-element name='number-of-items' type='text' index-type='' repeatable='false'>
  <meta-data>
			<entry name="displayAsTooltip"><![CDATA[true]]></entry>
			<entry name="required"><![CDATA[false]]></entry>
			<entry name="instructions"><![CDATA[How many items to retrieve with ajax.]]></entry>
			<entry name="label"><![CDATA[Number of items]]></entry>
			<entry name="predefinedValue"><![CDATA[]]></entry>
		</meta-data>
</dynamic-element>
</root>

Finally, we get to the good part which is the template of the Web Content app. I've implemented this example using Velocity just because it's the one I'm most comfortable with. The template implements both the front end logic as well as the backend that will handle our AJAX request. Remember when writing templates that implement request handling to unckeck "Cacheable".

#set ($ns = $request.portlet-namespace)
#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))
#set ($numberOfItems = $getterUtil.getInteger($number-of-items.data))

#if ($request.lifecycle == 'RESOURCE_PHASE')

	## This phase will handle the ajax request.

#else

	## This phase (default is 'RENDER_PHASE') will handle the view.

#end

Let's look at the view logic and the AJAX call that drives it! It uses Liferay's very own Alloy Javascript Library to perform the AJAx call.

...
#else
	<table class="taglib-search-iterator">
		<thead>
			<tr class="portlet-section-header results-header">
				<th>
					Widgets
				</th>
			</tr>
		</thead>
		<tbody class="${ns}results-container">
			<tr class="portlet-section-body results-row last">
				<td>
					No Widgets
				</td>
			</tr>
		</tbody>
	</table>

	<script type="text/javascript">
	AUI().use(
		'aui-base', 'aui-io',
		function(A) {
			var search = function(eventType) {
				A.io.request(
					'${request.resource-url}',
					{
						dataType: 'json',
						on: {
							success: function(event, id, obj) {
								var instance = this;

								var hits = instance.get('responseData');
								
								var resultsContainer = A.one('.${ns}results-container');
						
								if (!hits && !hits.docs) {
									return;
								}

								resultsContainer.empty();
								
								for (var i = 0; i < hits.docs.length; i++) {
									var doc = hits.docs[i];

									console.log(doc);
								
									var title = doc.fields.map['web_content/name'].value || doc.fields.map.uid.value;
									var description = doc.fields.map['web_content/description'].value;
									var image = doc.fields.map['web_content/image'].value;
									var document = doc.fields.map['web_content/document'].value;
									
									var position = ' portlet-section-body';
									
									if (i % 2 == 1) {
										position = ' portlet-section-alternate alt';
									}
									
									if (i == 0) {
										position += ' first';
									}
									else if (i == hits.length - 1) {
										position += ' last';
									}
		
									resultsContainer.append(
										[
											'<tr class="results-row' + position + '">',
												'<td>',
													'<h3>',
														title,
													'</h3>',
													'<p>',
														'<img style="float: left;" src="',
															image,
															'" alt="',
															name,
															'"/>',
														//description,
													'</p>',
													'<a href="',
														document,
														'">Spec Sheet</a>',
												'</td>',
											'</tr>'
										].join('')
									);
								}
							}
						}
					}
				);
			}

			search();
		}
	);		
	</script>
#end

You'll notice that the bulk of the code lies in javascript processing the results into the table. I'm not the greatest of javascript developers so take my code with a grain of salt.

Also notice that the target of the AJAX call is a url generated from the request and is in fact one which invokes the current portlet in the "RESOURCE_PHASE". If you aren't familiar with portlets, the "RESOURCE_PHASE" is one which allows portlets to return output without the wrappings and trappings of the surrounding portal. Effectively the OutputStream or PrintWriter used by the portlet is not touched or altered in any way by the portal. This allows the portlet to do things like handle AJAX requests, or generally server any type of "static" resource, like images.

Finally, see how the value of each individual structure field is retrieved from the JSON object: doc.fields.map['web_content/name'].value. Each field that is marked as "Searchable" when creating the structure can be retrieved from the SearchEngine result prefixed by web_content/. The prefix exists so that dynamically created fields don't collide with fields of the actual Web Content Article object when indexed.

 

The final step is getting our content! To do that we'll invoke and query Liferay's SearchEngine from within the template and convert the results into JSON format which we will return as the response body of the request.

...
#if ($request.lifecycle == 'RESOURCE_PHASE')
	#set ($portalBeanLocator = $portal.getClass().forName('com.liferay.portal.kernel.bean.PortalBeanLocatorUtil'))

	#set ($searchEngine = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SearchEngineUtil'))
	#set ($queryFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.BooleanQueryFactoryUtil'))
	#set ($sortFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SortFactoryUtil'))
	#set ($jsonFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.json.JSONFactoryUtil'))

	#set ($fullQuery = $queryFactory.create())
	#set ($contextQuery = $queryFactory.create())
	
	#set ($V = $contextQuery.addRequiredTerm('companyId', $companyId))
	#set ($V = $contextQuery.addExactTerm('entryClassName', 'com.liferay.portlet.journal.model.JournalArticle'))
	#set ($V = $contextQuery.addRequiredTerm('groupId', $scopeGroupId))
	#set ($V = $contextQuery.addRequiredTerm('structureId', '10628'))
	#set ($V = $fullQuery.add($contextQuery, 'MUST'))
	
	#set ($sorts = $sortFactory.getDefaultSorts())
	
	#set ($hits = $searchEngine.search($companyId, $fullQuery, $sorts, 0, ))

	$jsonFactory.serialize($hits)
#else
...

What I've done here is create a SearchEngine query which limits the results to a specific structureId. This limitation is only imposed because I chose to limit the logic of the example to only support this one structure. It is entirely possible to query for arbitrary content types (even beyond just Web Content) as long as you are willing to implement the view logic to handle those.

Here's an image of what it might look like.

The complete template follows:

#set ($ns = $request.portlet-namespace)
#set ($companyId = $getterUtil.getLong($request.theme-display.company-id))
#set ($scopeGroupId = $getterUtil.getLong($request.theme-display.scope-group-id))
#set ($numberOfItems = $getterUtil.getInteger($number-of-items.data))

#if ($request.lifecycle == 'RESOURCE_PHASE')
	#set ($portalBeanLocator = $portal.getClass().forName('com.liferay.portal.kernel.bean.PortalBeanLocatorUtil'))

	#set ($searchEngine = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SearchEngineUtil'))
	#set ($queryFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.BooleanQueryFactoryUtil'))
	#set ($sortFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.search.SortFactoryUtil'))
	#set ($jsonFactory = $portalBeanLocator.locate('com.liferay.portal.kernel.json.JSONFactoryUtil'))

	#set ($fullQuery = $queryFactory.create())
	#set ($contextQuery = $queryFactory.create())
	
	#set ($V = $contextQuery.addRequiredTerm('companyId', $companyId))
	#set ($V = $contextQuery.addExactTerm('entryClassName', 'com.liferay.portlet.journal.model.JournalArticle'))
	#set ($V = $contextQuery.addRequiredTerm('groupId', $scopeGroupId))
	#set ($V = $contextQuery.addRequiredTerm('structureId', '10628'))
	#set ($V = $fullQuery.add($contextQuery, 'MUST'))
	
	#set ($sorts = $sortFactory.getDefaultSorts())
	
	#set ($hits = $searchEngine.search($companyId, $fullQuery, $sorts, 0, $numberOfItems))

	$jsonFactory.serialize($hits)
#else
	<table class="taglib-search-iterator">
		<thead>
			<tr class="portlet-section-header results-header">
				<th>
					Widgets
				</th>
			</tr>
		</thead>
		<tbody class="${ns}results-container">
			<tr class="portlet-section-body results-row last">
				<td>
					No Widgets
				</td>
			</tr>
		</tbody>
	</table>

	<script type="text/javascript">
	AUI().use(
		'aui-base', 'aui-io',
		function(A) {
			var search = function(eventType) {
				A.io.request(
					'${request.resource-url}',
					{
						dataType: 'json',
						on: {
							success: function(event, id, obj) {
								var instance = this;

								var hits = instance.get('responseData');
								
								var resultsContainer = A.one('.${ns}results-container');
						
								if (!hits && !hits.docs) {
									return;
								}

								resultsContainer.empty();
								
								for (var i = 0; i < hits.docs.length; i++) {
									var doc = hits.docs[i];

									console.log(doc);
								
									var title = doc.fields.map['web_content/name'].value || doc.fields.map.uid.value;
									var description = doc.fields.map['web_content/description'].value;
									var image = doc.fields.map['web_content/image'].value;
									var document = doc.fields.map['web_content/document'].value;
									
									var position = ' portlet-section-body';
									
									if (i % 2 == 1) {
										position = ' portlet-section-alternate alt';
									}
									
									if (i == 0) {
										position += ' first';
									}
									else if (i == hits.length - 1) {
										position += ' last';
									}
		
									resultsContainer.append(
										[
											'<tr class="results-row' + position + '">',
												'<td>',
													'<h3>',
														title,
													'</h3>',
													'<p>',
														'<img style="float: left;" src="',
															image,
															'" alt="',
															name,
															'"/>',
														//description,
													'</p>',
													'<a href="',
														document,
														'">Spec Sheet</a>',
												'</td>',
											'</tr>'
										].join('')
									);
								}
							}
						}
					}
				);
			}

			search();
		}
	);		
	</script>
#end

Here it is again as XSLT!

<?xml version="1.0"?>

<xsl:stylesheet version="1.0"
	xmlns:BooleanQueryFactoryUtil="xalan://com.liferay.portal.kernel.search.BooleanQueryFactoryUtil"
	xmlns:JSONFactoryUtil="xalan://com.liferay.portal.kernel.json.JSONFactoryUtil"
	xmlns:BooleanQuery="xalan://com.liferay.portal.kernel.search.BooleanQuery"
	xmlns:SearchEngineUtil="xalan://com.liferay.portal.kernel.search.SearchEngineUtil"
	xmlns:SortFactoryUtil="xalan://com.liferay.portal.kernel.search.SortFactoryUtil"
	xmlns:str="http://exslt.org/strings"
	xmlns:xalan="http://xml.apache.org/xalan"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	exclude-result-prefixes="xalan"
	extension-element-prefixes="BooleanQueryFactoryUtil JSONFactoryUtil BooleanQuery SearchEngineUtil SortFactoryUtil str xalan">

	<xsl:output method="text" omit-xml-declaration="yes" />
	
	<xsl:param name="groupId" />

	<xsl:variable name="request" select="/root/request" />
	<xsl:variable name="ns" select="$request/portlet-namespace" />
	<xsl:variable name="companyId" select="$request/theme-display/company-id" />
	<xsl:variable name="scopeGroupId" select="$request/theme-display/scope-group-id" />
	<xsl:variable name="numberOfItems" select="/root/dynamic-element[@name='number-of-items']/dynamic-content" />

	<xsl:template name="out" match="@*|node()">
		<xsl:value-of select="local-name()"/><xsl:text> = </xsl:text>
		<xsl:copy>
			<xsl:apply-templates select="@*|node()"/>
		</xsl:copy>
	</xsl:template>

	<xsl:template match="/">
		<xsl:choose>
			<xsl:when test="$request/lifecycle = 'RESOURCE_PHASE'">
				<xsl:variable name="fullQuery" select="BooleanQueryFactoryUtil:create()" />
				<xsl:variable name="contextQuery" select="BooleanQueryFactoryUtil:create()" />
				<xsl:variable name="void1" select="BooleanQuery:addRequiredTerm($contextQuery, 'companyId', $companyId)" />
				<xsl:variable name="void2" select="BooleanQuery:addExactTerm($contextQuery, 'entryClassName', 'com.liferay.portlet.journal.model.JournalArticle')" />
				<xsl:variable name="void3" select="BooleanQuery:addRequiredTerm($contextQuery, 'groupId', $scopeGroupId)" />
				<xsl:variable name="void4" select="BooleanQuery:addRequiredTerm($contextQuery, 'structureId', '10628')" />
				<xsl:variable name="void5" select="BooleanQuery:add($fullQuery, $contextQuery, 'MUST')" />

				<xsl:variable name="sorts" select="SortFactoryUtil:getDefaultSorts()" />
				
				<xsl:message>
					<xsl:value-of select="$numberOfItems" />
				</xsl:message>
				
				<xsl:variable name="hits" select="SearchEngineUtil:search(number($companyId), $fullQuery, $sorts, number(0), number($numberOfItems))" />
				
				<xsl:value-of select="JSONFactoryUtil:serialize($hits)" />
			</xsl:when>
			<xsl:otherwise>
				<xsl:text disable-output-escaping="yes"><![CDATA[
					<table class="taglib-search-iterator">
						<thead>
							<tr class="portlet-section-header results-header">
								<th>
									Widgets
								</th>
							</tr>
						</thead>
						<tbody class="]]></xsl:text><xsl:value-of select="$ns" /><xsl:text disable-output-escaping="yes"><![CDATA[results-container">
							<tr class="portlet-section-body results-row last">
								<td>
									No Widgets
								</td>
							</tr>
						</tbody>
					</table>]]></xsl:text><xsl:text disable-output-escaping="yes"><![CDATA[
					<script type="text/javascript">
					AUI().use(
						'aui-base', 'aui-io',
						function(A) {
							var search = function(eventType) {
								A.io.request(
									']]></xsl:text><xsl:value-of disable-output-escaping="yes" select="$request/resource-url" /><xsl:text disable-output-escaping="yes"><![CDATA[',
									{
										dataType: 'json',
										on: {
											success: function(event, id, obj) {
												var instance = this;

												var hits = instance.get('responseData');
								
												var resultsContainer = A.one('.]]></xsl:text><xsl:value-of select="$ns" /><xsl:text disable-output-escaping="yes"><![CDATA[results-container');
						
												resultsContainer.empty();
								
												if (!hits && !hits.docs) {
													return;
												}

												for (var i = 0; i < hits.docs.length; i++) {
													var doc = hits.docs[i];

													var title = doc.fields.map['web_content/name'].value || doc.fields.map.uid.value;
													var description = doc.fields.map['web_content/description'].value;
													var image = doc.fields.map['web_content/image'].value;
													var document = doc.fields.map['web_content/document'].value;
									
													var position = ' portlet-section-body';
									
													if (i % 2 == 1) {
														position = ' portlet-section-alternate alt';
													}
									
													if (i == 0) {
														position += ' first';
													}
													else if (i == hits.length - 1) {
														position += ' last';
													}
		
													resultsContainer.append(
														[
															'<tr class="results-row' + position + '">',
																'<td>',
																	'<h3>',
																		title,
																	'</h3>',
																	'<p>',
																		'<img style="float: left;" src="',
																			image,
																			'" alt="',
																			name,
																			'"/>',
																		//description,
																	'</p>',
																	'<a href="',
																		document,
																		'">Spec Sheet</a>',
																'</td>',
															'</tr>'
														].join('')
													);
												}
											}
										}
									}
								);
							}

							search();
						}
					);		
					</script>]]></xsl:text>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>
</xsl:stylesheet>

Here's hoping someone finds this useful!

Threaded Replies Author Date
Nice feature! Thanks a lot, Ray! Jonas Yuan February 2, 2011 1:18 PM
Thanks for getting this up so quickly. So,...... Joseph Toman February 2, 2011 1:51 PM
You can access all the same things from XSL.... Ray Augé February 2, 2011 1:56 PM
Thanks. I had not found any documentation on... Joseph Toman February 2, 2011 2:06 PM
I added the same template implemented as XSLT... Ray Augé February 3, 2011 9:30 AM
Hey, cool! Thanks, that'll save me a lot of... Joseph Toman February 3, 2011 1:56 PM
Note that while you're working with XSL, that... Ray Augé February 3, 2011 2:02 PM
Nice exercise Ray!!! Shagul Khajamohideen February 2, 2011 3:25 PM
Great article .. i will surely try it out ..... Abdelrahman Mohamed El Ghanam February 23, 2011 11:50 AM
[...] Hi Terry, All Liferay's services are... Anonymous June 3, 2011 5:09 AM
On this line of thinking with WCM... Like many... Richard Knight July 15, 2011 3:27 PM
The RSS portlet does exactly this using a... Ray Augé July 15, 2011 3:31 PM
Thanks Ray, you are always so speedy with... Richard Knight July 16, 2011 7:18 AM
One other question, is there anyway to do the... Richard Knight July 16, 2011 7:20 AM
As soon s I get a chance I'll give an example... Ray Augé July 22, 2011 10:43 AM
[...] Internet coverage in Northern Ontario... Anonymous September 28, 2011 8:16 AM
Hey Ray, Have you had a chance to develop any... Richard Knight September 28, 2011 8:07 PM
I was trying really hard to actually get this... Ray Augé September 29, 2011 10:45 AM
[...] Internet coverage in Northern Ontario... Anonymous October 31, 2011 7:50 AM
I am developing a war portlet that should allow... Ismael Ferrer November 3, 2011 11:05 AM
Oh! never include portal-impl.jar! Regarding... Ray Augé November 3, 2011 11:28 AM
[...]... Anonymous November 12, 2011 2:30 PM
Great article, I've learned a lot from it. But... Francisco A Aranda December 7, 2011 5:24 PM
Not sure what I'm doing wrong! I was so hoping... Randy Parsons January 16, 2012 5:05 PM
[...] Jeff just had his 3rd birthray when we... Anonymous January 29, 2012 5:50 AM
[...] Da ich noch immer keine möglichkeit... Anonymous March 26, 2012 6:13 AM
Hi Ray! I am using Liferay 6.1. I am trying to... Vaibhav Mittal July 20, 2012 2:42 AM
Not sure what ime doing wrong about this... my... Marc-Olivier Picard November 28, 2012 8:19 AM
[...] As great as the 6.2 previews were, the... Anonymous November 29, 2013 3:59 PM
Hi Ray, it's amazing topic. But I have a... Truong Mai January 28, 2014 2:26 AM

Nice feature! Thanks a lot, Ray!
Posted on 2/2/11 1:18 PM.
Thanks for getting this up so quickly. So,... If instead of a Velocity template we were working in XSL could we accomplish the same thing? I'm not so much interested in the generic XSL required, but the way an XSL template interacts with the Liferay environment. For instance the Velocity template above depends on a lot of Java objects, are those all available if you use XSL ? If so, how do you get access to them (and their methods)? Thanks again, it was a good talk.
Posted on 2/2/11 1:51 PM.
You can access all the same things from XSL. You just have to know your way around the xalan java language support.
Posted on 2/2/11 1:56 PM in reply to Joseph Toman.
Thanks. I had not found any documentation on this previously, but know that I know Liferay is using Xalan, a number of articles pop up when I do a search on that term. Thanks again.
Posted on 2/2/11 2:06 PM in reply to Ray Augé.
Nice exercise Ray!!!
Posted on 2/2/11 3:25 PM.
I added the same template implemented as XSLT for you!

It's completely functionally equivalent.
Posted on 2/3/11 9:30 AM in reply to Joseph Toman.
Hey, cool! Thanks, that'll save me a lot of head scratching!
Posted on 2/3/11 1:56 PM in reply to Ray Augé.
Note that while you're working with XSL, that there is a small bug in the error processing xsl template which causes your template errors to be swallowed up and never made available.

copy portal-impl.jar!/com/liferay/portlet/journal/dependencies/error.xsl to your WEB-INF/classes folder (with full path) and remove the $companyId variable of the languageUtil:get method.

That will also save you lots of head scratching.
Posted on 2/3/11 2:02 PM in reply to Joseph Toman.
Great article .. i will surely try it out .. thanks ray..
Posted on 2/23/11 11:50 AM.
[...] Hi Terry, All Liferay's services are available to a WCM template through the serviceLocator. Since accessing the full Liferay API is quite a powerful privilege, this is restricted out-of-the-box. In... [...] Read More
Posted on 6/3/11 5:09 AM.
On this line of thinking with WCM...

Like many windows applications having an embedded FileBrowser to pick a file to use in the application, how would you create a custom portlet that uses the existing WCM portlet as a means to create and choose a wcm entry to embed in the custom portlet. Any pointers would be greatly appreciated, since I don't want to recreate the entire functionality of the WCM portlet from scratch.
Posted on 7/15/11 3:27 PM in reply to .
The RSS portlet does exactly this using a special tag so you can embed a custom header and/or footer to it. The tag is <liferay-ui:journal-article /> (see the code of the RSS portlet, it's pretty simple. There is also the example in the configuration.jsp on how to make a selection of a WCM article and store it in a preference.)
Posted on 7/15/11 3:31 PM in reply to Richard Knight.
Thanks Ray, you are always so speedy with responses. I'll take a look.
Thanks Again!
Posted on 7/16/11 7:18 AM in reply to Ray Augé.
One other question, is there anyway to do the same kind of thing with the Document Library or with the Image Library (File Chooser/Viewer) functionality?
Posted on 7/16/11 7:20 AM in reply to Richard Knight.
As soon s I get a chance I'll give an example of this. We don't have any canned examples that I'm aware of, but we should and so I'll make sure to come up with some shortly.
Posted on 7/22/11 10:43 AM in reply to Richard Knight.
[...] Internet coverage in Northern Ontario Forums, IRC, Blog The beauty of XML and XSLT (in 2004) Bits of Liferay's history since 2004, e.g. the Sourceforge Mailinglist Some Features Ray has been involved... [...] Read More
Posted on 9/28/11 8:16 AM.
Hey Ray,

Have you had a chance to develop any examples for as you spoke of above?
....
One other question, is there anyway to do the same kind of thing with the Document Library or with the Image Library (File Chooser/Viewer) functionality?

Thanks,
Richard
Posted on 9/28/11 8:07 PM in reply to .
I was trying really hard to actually get this into 6.1 as prepackaged components that you could just use. Unfortunately I did not get to do it.

That being said, there is a partial component in 6.1 that could probably be re-purposed as a general solution (which is currently dependent on the control panel). That is the Asset Relation chooser (you can find this in the Related Assets section located in the editors of various assets). It let's you choose from any Asset type to create relations and returns reference information. But if it could be re-worked to specify the types available in particular instance (say limited to doclib references), then it could become very useful.

Now, if you're trying to do this in 6.0 or earlier, the best possible examples are those found in the Web Content article editor. You have fields of type Document Library and Image Gallery with associated chooser. These are tied to a particular implementation for that portlet, but they are the best ootb examples that I can think of.

Good luck Richard and my apologies for the very late reply!
Posted on 9/29/11 10:45 AM in reply to Richard Knight.
[...] Internet coverage in Northern Ontario Forums, IRC, Blog The beauty of XML and XSLT (in 2004) Bits of Liferay's history since 2004, e.g. the Sourceforge Mailinglist Some Features Ray has been involved... [...] Read More
Posted on 10/31/11 7:50 AM.
I am developing a war portlet that should allow the user to select a web content and store it in a preference.

I'm taking as a model the RSS portlet as you suggested, but rss/select_journal_article.jspf imports asset_publisher/article_search.jsp that uses ArticleSearch and ArticleDisplayTerms classes wich are in portal-impl.jar.

Is there a way to make that work without including portal-impl.jar in the portlet war?

Thanks!
Posted on 11/3/11 11:05 AM.
Oh! never include portal-impl.jar!

Regarding those two classes, I've been meaning to do something like move this into the portal-service.jar.

While not an ideal solution, for the short term I would simply take those two java classes and pull them into your plugin. I don't think they have any dependencies that are not already public, except from one small exception that could be solved by taking one method from JournalIUtil and pulling into the ArticleSearch class directly.

The long term fix is of course to implement the nice asset choosers we talked about.
Posted on 11/3/11 11:28 AM in reply to Ismael Ferrer.
[...] http://www.liferay.com/web/raymond.auge/blog/-/blogs/advanced-web-content-exampl­e-with-ajax... [...] Read More
Posted on 11/12/11 2:30 PM.
Great article, I've learned a lot from it.
But instead of the webcontent/name attribute, how I do access the webcontent title, not the structure name attribute?
Thanks!
Posted on 12/7/11 5:24 PM.
Not sure what I'm doing wrong! I was so hoping this would be the answer to my issue of displaying multiple web content based on the same Structure. I have followed the instructions and understand most of what is going on, but I never get any content listing, even though I have added two WC using the structure 'WIDGET'. I did change the structureId in the code to what I called mine ('WIDGET') but still get nothing showing up. After refreshing the page the 'No Widgets' does appear briefly but then disappears (yes I did deselect the 'Cacheable' option). I would have thought that if no results are found then this information would continue to display. Is there setting I'm missing? (I have tried in 6.0.10 EE, 6.0.6 CE and 6.1 CE all with same results)
I also tried using the XSLT version as well with the same results. Any suggestions.
Posted on 1/16/12 5:05 PM.
[...] Jeff just had his 3rd birthray when we spoke, so he's in his fourth year now (I stole the term "birthray" from Julio someone in the spanish office - can't remember who it was though, help me to get... [...] Read More
Posted on 1/29/12 5:50 AM.
[...] Da ich noch immer keine möglichkeit gefunden habe, das Portlet mit dem WebContent zu verbinden habe ich es jetzt andersrum versucht. Ich habe den Blogeintrag Advanced Web Content Example with AJAX... [...] Read More
Posted on 3/26/12 6:13 AM.
Hi Ray! I am using Liferay 6.1. I am trying to make a web content using structure and templates. I have choosed lang xsl for templates. I have one textfield and 1 image in structure. But when i preview web content it shows internal server error. From logs i got this:
08:39:42,949 INFO [PortalImpl:4873] Current URL /c/journal/view_article_content?cmd=preview&groupId=19&articleId=14248&version=1­.0 generates exception: com.liferay.portal.kernel.templateparser.TransformException: Unhandled exception

Below is my xsl:
<?xml version="1.0"?>

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<body>
<xsl:value-of disable-output-escaping="yes" select="root/dynamic-element[@name='textarea']/dynamic-content"/>
<br/>
<img>
<xsl:attribute name="src">
<xsl:value-of disable-output-escaping="yes" select="root/dynamic-element[@name='image']/dynamic-content"/>
</xsl:attribute>
</img>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

I have copied portal-impl.jar!/com/liferay/portlet/journal/dependencies/error.xsl to tomcat/weebapps/Root/WEB-INF/classes folder but have not changed anything because 2 things are not clear to me 1.) what to do with full path 2.) not geeting how to remove $companyId variable of the languageUtil:get method because what i can found is only $companyId variable.
Can you shed some light on this?
Posted on 7/20/12 2:42 AM in reply to .
Not sure what ime doing wrong about this... my response data is always null.

I tried return it in a json format in the RESOURCE_PHASE doing someting like
{
"jsonArray": $jsonArray
}

with no luck i removed it and tried with

#set ($V = $jsonArray.put($jsonObject))
$jsonFactory.serialize($jsonArray)

but still no luck. here is my call and alert that tells me the responseData is null.

AUI().use(
"aui-base", "aui-io-plugin", "aui-io-request",
function(A) {
A.io.request(
"${request.resource-url}",
{
data: {
},
dataType: "json",
on: {
success: function(event, id, obj) {
alert(this.get('responseData'));

},
failure: function(event, id, obj) {
alert("ajax call failure ");
}
}
}
);
}
);

any thoughts?
Posted on 11/28/12 8:19 AM.
[...] As great as the 6.2 previews were, the coolest part of the symposium was the chance to meet so many different people. The first night I met up with some guys from Boulder who were working on a... [...] Read More
Posted on 11/29/13 3:59 PM.
Hi Ray, it's amazing topic. But I have a question: I always receive null object "hits" when running this web content. What is the problem?
Posted on 1/28/14 2:26 AM.