Xpages: 'RichText' field on web and saving with java bean - xpages

I have a requirement to allow users to enter 'RichText' in a field in an Xpages form on the web. It is just rich text (bold, size, color) no attachments/links/pictures. And no need to edit it on the client.
I have googled this and put together something that kind of is close, but doesn't work. I am only trying to get a minimal working example right now.
Used some info in this link and in another one I cannot find right now.
Any help would be greatly appreciated.
Xpage:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xe="http://www.ibm.com/xsp/coreex">
<xp:messages id="messages1"></xp:messages>
<xp:panel id="pnlAll">
<xp:this.data>
<xe:objectData saveObject="#{javascript:docModel.save()}"
var="docModel">
<xe:this.createObject><![CDATA[#{javascript:var docModel = new com.scoular.model.Doc();
var unid = sessionScope.get("key");
if (unid != null) {
docModel.loadByUnid(unid);
sessionScope.put("key","");
viewScope.put("readOnly","Yes");
} else {
docModel.create();
viewScope.put("readOnly","No");
}
return docModel;}]]></xe:this.createObject>
</xe:objectData>
</xp:this.data>
<xp:inputRichText id="inputRichText1"
value="#{docModel.body}">
</xp:inputRichText>
<xp:button value="Save"
id="button1"
type="button"
styleClass="btn-primary">
<xp:eventHandler event="onclick" submit="true"
refreshMode="complete" save="true">
<xp:this.action><![CDATA[#{javascript:if (docModel.save() == true) {
}}]]></xp:this.action>
</xp:eventHandler>
</xp:button>
</xp:panel>
</xp:view>
Java:
package com.scoular.model;
import java.io.Serializable;
import java.util.Date;
import org.openntf.domino.Database;
import lotus.domino.MIMEEntity;
import lotus.domino.Stream;
import org.openntf.domino.DateTime;
import org.openntf.domino.Document;
import org.openntf.domino.Session;
import org.openntf.domino.utils.Factory;
import org.openntf.domino.xsp.XspOpenLogUtil;
public class Doc implements Serializable {
private static final long serialVersionUID = -5867831497684227875L;
private com.ibm.xsp.http.MimeMultipart body;
// Common Fields
private String unid;
private Boolean newNote;
private DateTime crtDte;
private String crtUsr;
public Doc() {
}
public void create() {
try {
newNote = true;
Session session = Factory.getSession();
Date date = new Date();
crtDte = session.createDateTime(date);
crtUsr = session.getEffectiveUserName();
} catch (Exception e) {
XspOpenLogUtil.logError(e);
}
}
public void loadByUnid(String unid) {
try {
Session session = Factory.getSession();
String DataDBpath = session.getCurrentDatabase().getServer() + "!!" + "scoApps\\Spectrum\\cashmarkData.nsf";
Database DataDB = session.getDatabase(DataDBpath);
Document doc = DataDB.getDocumentByUNID(unid);
if (null == doc) {
System.out.println("Document not found");
} else {
loadValues(doc);
}
} catch (Exception e) {
XspOpenLogUtil.logError(e);
}
}
public void loadValues(Document doc) {
try {
// common fields
newNote = false;
unid = doc.getUniversalID();
crtDte = doc.getItemValue("checkInDate", DateTime.class);
crtUsr = doc.getItemValueString("crtUsr");
// custom fields
} catch (Exception e) {
XspOpenLogUtil.logError(e);
}
}
public boolean save() {
boolean tmpSave = true;
try {
Document doc = null;
Session session = Factory.getSession();
String DataDBpath = session.getCurrentDatabase().getServer() + "!!" + "scoApps\\Spectrum\\cashmarkData.nsf";
Database DataDB = session.getDatabase(DataDBpath);
if (newNote) {
doc = DataDB.createDocument();
doc.put("form", "doc");
} else {
doc = DataDB.getDocumentByUNID(unid);
}
//Create the body as a MIME entity
session.setConvertMIME(false); // Do not convert MIME to RT MIMEEntity body = doc.createMIMEEntity("body");
Stream stream = session.createStream();
stream.writeText("<ul><li>hello</li><li>world</li></ul>Google");
//body.setContentFromText(stream, "text/html;charset=UTF-8", MIMEEntity.ENC_IDENTITY_7BIT);
stream.close();
doc.save();
} catch (Exception e) {
XspOpenLogUtil.logError(e);
}
return tmpSave;
}
// Getters and Setters for common fields
public String getUnid() {
return unid;
}
public void setUnid(String unid) {
this.unid = unid;
}
public Boolean getNewNote() {
return newNote;
}
public void setNewNote(Boolean newNote) {
this.newNote = newNote;
}
public DateTime getCrtDte() {
return crtDte;
}
public void setCrtDte(DateTime crtDte) {
this.crtDte = crtDte;
}
public String getCrtUsr() {
return crtUsr;
}
public void setCrtUsr(String crtUsr) {
this.crtUsr = crtUsr;
}
public com.ibm.xsp.http.MimeMultipart getBody() {
return body;
}
public void setbody(com.ibm.xsp.http.MimeMultipart body) {
this.body = body;
}
}

To save your MimeMultipart to a MimeEntity field give this a try:
String fieldName = "yourFieldName";
if (doc.hasItem(fieldName))
doc.removeItem(fieldName);
Stream stream = session.createStream();
stream.writeText(body.getHTML());
MIMEEntity mimeEnt = doc.createMIMEEntity(fieldName);
mimeEnt.setContentFromText(stream,
"text/html;charset=UTF-8", MIMEntity.ENC_NONE);
stream.close();
stream.recycle();
Also you may want to remove some CKEditor plugins by adding a dojoAttribute called 'removePlugins'
<xp:inputRichText id="inputRichText1">
<xp:this.dojoAttributes>
<xp:dojoAttribute name="removePlugins"
value="ibmxspimage,smiley,ibmsametimeemoticons"></xp:dojoAttribute>
</xp:this.dojoAttributes>
</xp:inputRichText>
This will remove the toolbar buttons that allow the user to choose emoticons / upload images (which will cause an error)
Note however this will not prevent users from trying to paste Images into the CKEditor, to prevent that you need to make/obtain a custom CKEditor plugin that prevents pasting images both via a 'Data URL' (clipboard paste) or via 'Href' e.g. web address based images.

Related

Xamarin QLPreviewController + NavigationPage broken on iOS 10

After updating the device to iOS 10, QLPreviewController stopped to display correctly the documents. It shows the white screen.
I have extracted the sample scenario from the app.
It contains single page with two buttons that should load two different documents:
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:QuickLookIOS10Test"
x:Class="QuickLookIOS10Test.QuickLookIOS10TestPage">
<StackLayout Orientation="Vertical">
<Button Text="Load first doc" Clicked="OnLoadFirstClicked"/>
<Button Text="Load second doc" Clicked="OnLoadSecondClicked"/>
<Button Text="Navigate forward" Clicked="OnForwardClicked"/>
<local:QLDocumentView
x:Name="DocumentView"
BackgroundColor="Silver"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/>
</StackLayout>
</ContentPage>
where:
public class QLDocumentView : View
{
public static readonly BindableProperty FilePathProperty =
BindableProperty.Create(nameof(FilePath), typeof(string), typeof(QLDocumentView), null);
public string FilePath
{
get { return (string)GetValue(FilePathProperty); }
set { SetValue(FilePathProperty, value); }
}
}
There is a custom renderer involved:
public class QLDocumentViewRenderer : ViewRenderer<QLDocumentView, UIView>
{
private QLPreviewController controller;
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
{
//This is a fix to prevent incorrect scaling after rotating from portrait to landscape.
//No idea why does this work :( Bug #101639
return new SizeRequest(Size.Zero, Size.Zero);
}
protected override void OnElementChanged(ElementChangedEventArgs<QLDocumentView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
controller = new QLPreviewController();
SetNativeControl(controller.View);
}
RefreshView();
}
protected override void OnElementPropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == QLDocumentView.FilePathProperty.PropertyName)
{
RefreshView();
}
}
private void RefreshView()
{
DisposeDataSource();
if (Element?.FilePath != null)
{
controller.DataSource = new DocumentQLPreviewControllerDataSource(Element.FilePath);
}
controller.ReloadData();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
DisposeDataSource();
DisposeController();
}
}
private void DisposeDataSource()
{
var dataSource = controller.DataSource;
controller.DataSource = null;
dataSource?.Dispose();
}
private void DisposeController()
{
controller?.Dispose();
controller = null;
}
private class DocumentQLPreviewControllerDataSource : QLPreviewControllerDataSource
{
private readonly string fileName;
public DocumentQLPreviewControllerDataSource(string fileName)
{
this.fileName = fileName;
}
public override nint PreviewItemCount(QLPreviewController controller)
{
return 1;
}
public override IQLPreviewItem GetPreviewItem(QLPreviewController controller, nint index)
{
NSUrl url = NSUrl.FromFilename(fileName);
return new QlItem(url);
}
private sealed class QlItem : QLPreviewItem
{
private readonly NSUrl itemUrl;
public QlItem(NSUrl uri)
{
itemUrl = uri;
}
public override string ItemTitle { get { return string.Empty; } }
public override NSUrl ItemUrl { get { return itemUrl; } }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
this.itemUrl?.Dispose();
}
}
}
}
}
If the application setups the main page like below:
MainPage = new NavigationPage(new QuickLookIOS10TestPage());
it does work on iOS 9.3 but not on iOS 10. If I remove NavigationPage:
MainPage = new QuickLookIOS10TestPage();
it works on both iOS versions.
The code behind for button clicks just sets the FilePath property of the control.
Sample app demonstrating the problem
Xamarin Forms 2.3.2.127
Xamarin Studio 6.1.1 (build 15)
I've faced with the same problem. It looks like something was changed or even broken in QuickLook in iOS10, but the solution is quite simple:
public class PdfViewerControlRenderer : ViewRenderer<PdfViewerControl, UIView>
{
private readonly bool IsOniOS10;
private UIViewController _controller;
private QLPreviewController _qlPreviewController;
public PdfViewerControlRenderer()
{
IsOniOS10 = UIDevice.CurrentDevice.CheckSystemVersion(10, 0);
}
protected override void OnElementChanged(ElementChangedEventArgs<PdfViewerControl> e)
{
if (e.NewElement != null)
{
_controller = new UIViewController();
_qlPreviewController = new QLPreviewController();
//...
// Set QuickLook datasource here
//...
if (!IsOniOS10)
{
_controller.AddChildViewController(_qlPreviewController);
_controller.View.AddSubview(_qlPreviewController.View);
_qlPreviewController.DidMoveToParentViewController(_controller);
}
SetNativeControl(_controller.View);
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
_controller.View.Frame = Bounds;
_controller.View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
_qlPreviewController.View.Frame = Bounds;
if (IsOniOS10)
{
_controller.View.AddSubview(_qlPreviewController.View);
_qlPreviewController.DidMoveToParentViewController(_controller);
}
}
}
Result:

Xpages and EL: Syntax for retrieving HashMap Values

I have a cacheBean written in Java. I am successfully pulling out Vectors using EL, but I have a HashMap and when I try to access a value I throw an error.
My cacheBean is:
package com.scoular.cache;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Vector;
import org.openntf.domino.utils.Factory;
import org.openntf.domino.Database;
import org.openntf.domino.Session;
import org.openntf.domino.View;
import org.openntf.domino.ViewEntry;
import org.openntf.domino.ViewNavigator;
public class PCConfig implements Serializable {
private static final long serialVersionUID = 1L;
private Database thisDB;
private Database compDirDB;
public Database PCDataDB;
public HashMap<Integer, String> status = new HashMap<Integer, String>();
public static Vector<Object> geoLocations = new Vector<Object>();
public static Vector<Object> models = new Vector<Object>();
// #SuppressWarnings("unchecked")
private void initConfigData() {
try {
getStatus();
getGeoLocations();
getModels();
} catch (Exception e) {
e.printStackTrace();
}
}
public PCConfig() {
// initialize application config
System.out.println("Starting CacheBean");
initConfigData();
System.out.println("Ending CacheBean");
}
public static void setModels(Vector<Object> models) {
PCConfig.models = models;
}
public void getStatus() {
status.put(1, "In Inventory");
status.put(2, "Being Built");
status.put(3, "In Production");
status.put(4, "Aquiring PC");
status.put(5, "Decommissioning");
}
public Vector<Object> getGeoLocations() {
if (PCConfig.geoLocations == null || PCConfig.geoLocations.isEmpty()) {
try {
Session session = Factory.getSession();
thisDB = session.getCurrentDatabase();
compDirDB = session.getDatabase(thisDB.getServer(), "compdir.nsf", false);
View geoView = compDirDB.getView("xpGeoLocationsByName");
for (ViewEntry ce : geoView.getAllEntries()) {
Vector<Object> rowVal = ce.getColumnValues();
geoLocations.addElement(rowVal.elementAt(0));
}
} catch (Exception e) {
e.printStackTrace();
}
}
return geoLocations;
}
public Vector<Object> getModels() {
if (PCConfig.models == null || PCConfig.models.isEmpty()) {
try {
Session session = Factory.getSession();
thisDB = session.getCurrentDatabase();
PCDataDB = session.getDatabase(thisDB.getServer(), "scoApps\\PC\\PCData.nsf", false);
ViewNavigator vn = PCDataDB.getView("dbLookupModels").createViewNav();
ViewEntry entry = vn.getFirstDocument();
while (entry != null) {
Vector<Object> thisCat = entry.getColumnValues();
if (entry.isCategory()) {
String thisCatString = thisCat.elementAt(0).toString();
models.addElement(thisCatString);
}
entry = vn.getNext(entry);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return models;
}
}
and the code to grab the a value is:
<xp:text escape="true" id="computedField2">
<xp:this.value><![CDATA[#{PCConfig.status[0]}]]></xp:this.value></xp:text>
Your method getStatus() has to return the HashMap.
public HashMap<Integer, String> getStatus() {
...
return status;
}
In addition, #{PCConfig.status[0]} tries to read the value for key 0. There is no key 0 in your HashMap status though...
As far I know you should do as follow
#{javascript:PCConfig.status.get(1)}

Restricting file upload if image isn't attached

I have a jsf form that takes an email address, brief description, and image from the user. The else statement below is completely ignored when file==null. If a picture isn't uploaded I want an error telling the user they have to upload a image. How do I accomplish this?
import org.primefaces.model.UploadedFile;
private UploadedFile file;
public UploadedFile getFile() {
return file;
}
public void setFile(UploadedFile file) {
this.file = file;
}
public String upload() {
if (file!=null ) {
try {
System.out.println(file.getFileName());
InputStream fin2 = file.getInputstream();
Connection con = DataConnect.getConnection();
PreparedStatement pre = con.prepareStatement("insert into PICTURE_TABLE (EMAIL_ADDRESS,PRICE,ITEM_DESC,PICTURES,DATE) values(?,?,?,?,?)");
pre.setString(1, email);
pre.setString(2, price);
pre.setString(3,itemDesc);
pre.setBinaryStream(4, fin2, file.getSize());
pre.setString(5,time);
pre.executeUpdate();
System.out.println("Inserting Successfully!");
}
catch (Exception e) {
System.out.println("Exception-File Upload." + e.getMessage());
return "imageFail";
}
}
else{
FacesMessage msg = new FacesMessage("Please select image!!");
FacesContext.getCurrentInstance().addMessage(null, msg);
}
this is the jsf tag I'm using.
<h:form enctype="multipart/form-data"

Download file, filename doesn't work in portlet

I set filename in the HttpServletResponse header but when I download it has not this filename
FileInputStream fis = new FileInputStream(fileDocumento);
PortletResponse portletResponse=(PortletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
HttpServletResponse res = PortalUtil.getHttpServletResponse(portletResponse);
res.setHeader("Content-Disposition", "attachment; filename=schedaObiettivoTAC_.docx");
res.setHeader("Content-Transfer-Encoding", "binary");
res.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
res.flushBuffer();
OutputStream out=res.getOutputStream();
out.write(IOUtils.toByteArray(fis));
out.close();
fis.close();
You should not use the HttpServletResponse. Instead you should create a custom Resource and ResourceHandler:
CustomResourceHandler.java:
public final class CustomResourceHandler extends ResourceHandlerWrapper {
// Public Constants
public static final String LIBRARY_NAME = "exampleLib";
public static final String RESOURCE_NAME = "exampleName";
// Private Data Members
private ResourceHandler wrappedResourceHandler;
public CustomResourceHandler(ResourceHandler resourceHandler) {
this.wrappedResourceHandler = resourceHandler;
}
#Override
public Resource createResource(String resourceName, String libraryName) {
if (LIBRARY_NAME.equals(libraryName)) {
if (RESOURCE_NAME.equals(resourceName)) {
return new CustomResource(libraryName, resourceName,
"exampleFileName.txt", "Example Content");
}
else {
return super.createResource(resourceName, libraryName);
}
}
else {
return super.createResource(resourceName, libraryName);
}
}
#Override
public ResourceHandler getWrapped() {
return wrappedResourceHandler;
}
#Override
public boolean libraryExists(String libraryName) {
if (LIBRARY_NAME.equals(libraryName)) {
return true;
}
else {
return super.libraryExists(libraryName);
}
}
/* package-private */ static final class CustomResource extends Resource {
private final String content;
private final Map<String, String> responseHeaders;
private final String requestPath;
private final URL url;
public CustomResource(String libraryName, String resourceName,
String fileName, String content) {
super.setLibraryName(libraryName);
super.setResourceName(resourceName);
super.setContentType("text/plain");
Map<String, String> responseHeaders = new HashMap<String, String>();
responseHeaders.put("Content-Disposition",
"attachment; filename=" + fileName + ";");
this.responseHeaders = Collections.unmodifiableMap(responseHeaders);
StringBuilder sb = new StringBuilder();
sb.append(ResourceHandler.RESOURCE_IDENTIFIER);
sb.append("/");
sb.append(super.getResourceName());
sb.append("?ln=");
sb.append(super.getLibraryName());
this.requestPath = sb.toString();
URL url;
try {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
url = new URL(externalContext.encodeResourceURL(this.requestPath));
}
catch (MalformedURLException e) {
url = null;
}
this.url = url;
this.content = content;
}
#Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(
content.getBytes(StandardCharsets.UTF_8));
}
#Override
public String getRequestPath() {
return requestPath;
}
#Override
public Map<String, String> getResponseHeaders() {
return responseHeaders;
}
#Override
public URL getURL() {
return url;
}
#Override
public boolean userAgentNeedsUpdate(FacesContext facesContext) {
// Return false if the content cannot change dynamically.
return true;
}
}
}
WEB-INF/faces-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
<application>
<!-- ... -->
<resource-handler>custom.resource.handler.CustomResourceHandler</resource-handler>
</application>
<!-- ... -->
</faces-config>
Here's some example code for downloading the resource:
<h:outputLink target="_blank" value="#{bean.getDownloadURL(facesContext)}">
<h:outputText value="download" />
</h:outputLink>
public String getDownloadURL(FacesContext facesContext) {
return facesContext.getApplication().getResourceHandler()
.createResource(CustomResourceHandler.RESOURCE_NAME,
CustomResourceHandler.LIBRARY_NAME)
.getURL().toString();
}
You can also look at the Liferay Faces JSF Export PDF for a full portlet example to download/export a file.
If you are in the action or render phase of your portlet (what I would guess for a JSF portlet): there is a ResponseWrapper which will suppress any header. I'm using this method to find the response for sending binary data during the JSF lifecycle:
public static HttpServletResponse getHttpServletResponse() {
final FacesContext facesContext = FacesContext.getCurrentInstance();
if (facesContext == null) {
throw new IllegalStateException("Not inside a JSF request");
}
final Object responseObject = facesContext.getExternalContext().getResponse();
if (responseObject instanceof HttpServletResponse) {
return (HttpServletResponse) responseObject;
}
if (responseObject instanceof PortletResponse) {
// Use Liferays util to find the real response
HttpServletResponse response = PortalUtil.getHttpServletResponse((PortletResponse) responseObject);
// Find the outer most response (setting the headers would have no effect, as we are included)
while (response instanceof ServletResponseWrapper) {
final ServletResponse servletResponse = ((ServletResponseWrapper) response).getResponse();
if (!(servletResponse instanceof HttpServletResponse)) {
break;
}
response = (HttpServletResponse) servletResponse;
}
return response;
}
throw new IllegalStateException("Unknown type of response object: " + responseObject);
}

How can i make a field in XPages to be like a Names normal field?

I want to create a new field in XPages to do the same thing like a Names field from a form.
TY
Use the Extension Library's Name picker, and the DOJO form control's name text box.
The name picker grabs from the directory and the name field displays the names.
Here is an example.
<xe:namePicker id="namePicker1"
for="djextNameTextBox1">
<xe:this.dataProvider>
<xe:dominoNABNamePicker groups="false"
nameList="peopleByLastName">
</xe:dominoNABNamePicker>
</xe:this.dataProvider>
</xe:namePicker>
<xe:djextNameTextBox id="djextNameTextBox1"
value="#{document1.<FIELDNAME>}">
</xe:djextNameTextBox>
You can use this simple SSJS code snippet to create Names field in your document.
var n:NotesItem = doc.replaceItemValue("Field1", "Value1");
n.setNames(true);
How about to use a java bean background?
Create a java bean called NameConverter...
package converters;
import java.util.ArrayList;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import lotus.domino.Name;
import lotus.domino.NotesException;
import com.ibm.xsp.model.domino.DominoUtils;
public class NameConverter implements Converter {
private Name createName(String name) {
Name n = null;
try {
n = DominoUtils.getCurrentSession().createName(name);
} catch (NotesException e) {
e.printStackTrace();
}
return n;
}
public Object getAsObject(FacesContext context, UIComponent component, String value) {
String[] names = value.split(",");
Name name = null;
ArrayList<String> tmpNames = new ArrayList<String>();
for (int i = 0; i <= names.length - 1; i++) {
name = this.createName(names[i].trim());
try {
tmpNames.add(name.getAbbreviated());
} catch (NotesException e) {
e.printStackTrace();
}
}
return tmpNames.toString().replace("[", "").replace("]", "");
}
public String getAsString(FacesContext context, UIComponent component, Object value) {
String[] names = value.toString().split(",");
Name name = null;
ArrayList<String> tmpNames = new ArrayList<String>();
for (int i = 0; i <= names.length - 1; i++) {
name = this.createName(names[i].trim());
try {
tmpNames.add(name.getAbbreviated());
} catch (NotesException e) {
e.printStackTrace();
}
}
return tmpNames.toString().replace("[", "").replace("]", "");
}
}
Register your converter in faces-config.xml...
<converter>
<converter-id>nameConverter</converter-id>
<converter-class>converters.NameConverter</converter-class>
</converter>
And then create a field with a simple converter in your xPage...
<xp:inputText id="userName" style="width:300px">
<xp:this.converter>
<xp:converter converterId="nameConverter" />
</xp:this.converter>
</xp:inputText>
Simple as hell and perfectly scalable.
Enjoy, JiKra
custom converter is the best solution:
getAsObject:#Name("[Cannonicalize]",#Explode(vlaue)) - the saved value
getAsString:#Implode(#Name("[Abbreviate]",value),",") - value in web

Resources