Auto-width Bootstrap Column XPages Controls
I’ve been stuck working with OneUI Version 3 for the past couple of years, due to a regretful decision made at the beginning of my major project. OneUI was better than nothing but very frustrating at times.
Finally, I have moved on to my next project and I am now using bootstrap (version 3)
A common task when laying out a page using bootstrap is to divide sections up into rows and columns, and use the appropriate css styles to do so. I’m going to assume you are familiar with bootstrap’s grid system, and if not I recommend you hearing it from the horse’s mouth instead of me trying to explain it again.
I like creating XspLibrary controls, so I decided I would go ahead and create row and column controls. It could be seen as a little overkill and you can easily live without it, but I see the Bootfaces project did it, so I took inspiration from their container, row and column controls.
I then also added the ability for the columns to have their widths automatically distributed. Auto-widths is a feature that is coming with bootstrap 4, but for now we need to do it ourselves!
Example XPages Markup
The following examples end up looking like this: (Note: the blue border is just added for demo purposes to show the outline of the columns)
Here is a simple example of 2-column layout with equal width columns
1 2 3 4 |
<gb:row id="row1"> <gb:column id="column1">These two columns</gb:column> <gb:column id="column2">are Equal Width</gb:column> </gb:row> |
Notice above how I haven’t specified any column widths? The columns will all become equal width, so in the example above, they will both be 6 columns wide (12 divided by 2 columns).
If there have 3 columns, then they would each automatically be 4 columns wide (12 divided by 3 columns).
1 2 3 4 5 |
<gb:row id="row5"> <gb:column id="column3">These three </gb:column> <gb:column id="column10">columns are also</gb:column> <gb:column id="column11">Equal Width</gb:column> </gb:row> |
Specifying a column width
If you want a column to be a specifc width, you use the span property to specify the width for that column, the remaining columns will receive their fair share of whatever is left over.
1 2 3 4 5 |
<gb:row id="row2"> <gb:column id="column4">These columns have</gb:column> <gb:column id="column5" span="6">the middle with span="6"</gb:column> <gb:column id="column6">and the outer columns are auto width</gb:column> </gb:row> |
Specifying alignment
I’ve also added an align property so I can quickly align the contents.
1 2 3 4 5 |
<gb:row id="row4"> <gb:column id="column7" align="left">This is align left (default)</gb:column> <gb:column id="column8" align="center">This is align middle</gb:column> <gb:column id="column9" align="right">This is align left</gb:column> </gb:row> |
Full Row column
If you want to use the entire row, then you can just leave the column definition out entirely and the contents will automatically be wrapped in a col-md-12 column
1 2 3 |
<gb:row id="row3"> If you want a Full Row, then just don't include columns </gb:row> |
Using style and styleClass
You can still use style and styleClass properties as you normally would.
Container Control
If you are using one of the extension library bootstrap applicationLayouts, then you won’t need to use the container control because it is already covered by that. But, If you are rolling your own you can use the container layout which has a fluid property to control whether is is a bootstrap fluid layout or not.
1 2 3 4 5 |
<gb:container id="container1" fluid="true"> <!-- rows, columns, your content --> </gb:container> |
Overview of creating the controls
Here is what we are going to do:
- Create the Container UIComponent
- Create the Column UIComponent
- Create the Row UIComponent
- Provide Control Definitions via an xsp-config file so that designer knows how to use them
- Program in the auto-width algorithm
Notice we are not going to create a renderer. We will be extending the panel control, and the existing panel renderer will be used automatically.
For each component we are just going to extend the existing <xp:panel> control to re-use all the existing functionality, and just add a few extra properties.
Creating the Container Component
The container component is simply an extension of the panel control (UIPanelEx) with an additional property ‘fluid’. We add the getters and setters for the fluid property, and make sure it is included in the restore and save state operations.
We also override the getStyleClass method so that it will always concatenate ‘container’ or ‘container-fluid’ depending on the fluid property.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
package com.gregorbyte.xsp.component.bootstrap; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import com.gregorbyte.xsp.util.GregorbyteUtil; import com.ibm.xsp.component.UIPanelEx; public class UIContainer extends UIPanelEx { public Boolean fluid; public UIContainer() { } @Override public String getStyleClass() { String parent = super.getStyleClass(); if (isFluid()) { return GregorbyteUtil.concatStyleClasses("container-fluid", parent); } else { return GregorbyteUtil.concatStyleClasses("container", parent); } } public Boolean isFluid() { if (this.fluid != null) { return this.fluid; } ValueBinding vb = getValueBinding("fluid"); if (vb != null) { return (Boolean) vb.getValue(getFacesContext()); } return false; } public void setFluid(Boolean fluid) { this.fluid = fluid; } @Override public void restoreState(FacesContext context, Object state) { Object[] values = (Object[]) state; super.restoreState(context, values[0]); fluid = (Boolean) values[1]; } @Override public Object saveState(FacesContext context) { Object[] values = new Object[2]; values[0] = super.saveState(context); values[1] = fluid; return values; } } |
Creating the Column Component
The column component is a little more complex. We are also going to extend the UIPanelEx control but we will also add some control properties
- span – This will be an integer that can be set to specify the column span of the bootstrap grid
- align – This will be a String which can be ‘left’ (Default), ‘center’ or ‘right’. This will then add the necessary bootstrap css class to perform aligning of the contents
I am also going to add a property guessSpan which is not exposed to domino designer and will only be used to store our ‘guesstimate’ column span when calculating the auto widths.
Then, I create a private method ‘getEffectiveSpan’ which will check if a column span has been explicitly set with the span property and if so it will use that. If a span was not set, it will use the ‘guessSpan’ that was guessed via the algorithm
Then we override the ‘getStyleClass’ method, so that when the renderer comes along and asks for the styleClasses, we can inject some styleClasses of our own.
We are using the very handy method from Extension Library’s ExtLibUtil, which allows us to easily concatenate styles. I have copied the method into my own Util class so that there will be no dependency on the Extension Library.
First we ask for any styles that have been explicitly set. Then we concatenate our column style from the bootstrap grid system e.g. “col-md-8”, and then we concatenate an alignment style if one was set on the column.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
package com.gregorbyte.xsp.component.bootstrap; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import com.gregorbyte.xsp.util.GregorbyteUtil; import com.ibm.commons.util.StringUtil; import com.ibm.xsp.component.UIPanelEx; public class UIColumn extends UIPanelEx { private Integer guessSpan = null; // Defaults for Medium Columns, use styleClass for specific layouts private Integer span = null; private String align = null; public UIColumn() { super(); } private Integer getEffectiveSpan() { if (getSpan() != null) { return getSpan(); } else if (getGuessSpan() != null) { return getGuessSpan(); } return 12; } @Override public String getStyleClass() { String styleClass = super.getStyleClass(); styleClass = GregorbyteUtil.concatStyleClasses(styleClass, "col-md-" + getEffectiveSpan()); String align = getAlign(); if (StringUtil.equals(align, "left")) { styleClass = GregorbyteUtil.concatStyleClasses(styleClass, "text-left"); } else if (StringUtil.equals(align, "right")) { styleClass = GregorbyteUtil.concatStyleClasses(styleClass, "text-right"); } else if (StringUtil.equals(align, "center")) { styleClass = GregorbyteUtil.concatStyleClasses(styleClass, "text-center"); } return styleClass; } public Integer getGuessSpan() { return guessSpan; } public void setGuessSpan(Integer guessSpan) { this.guessSpan = guessSpan; } public Integer getSpan() { if (this.span != null) { return this.span; } ValueBinding vb = getValueBinding("span"); if (vb != null) { return (Integer) vb.getValue(getFacesContext()); } return null; } public void setSpan(Integer span) { this.span = span; } public String getAlign() { if (this.align != null) { return this.align; } ValueBinding vb = getValueBinding("align"); if (vb != null) { return (String) vb.getValue(getFacesContext()); } return null; } public void setAlign(String align) { this.align = align; } @Override public void restoreState(FacesContext context, Object state) { Object[] values = (Object[]) state; super.restoreState(context, values[0]); guessSpan = (Integer) values[1]; span = (Integer) values[2]; align = (String) values[3]; } @Override public Object saveState(FacesContext context) { Object[] values = new Object[4]; values[0] = super.saveState(context); values[1] = guessSpan; values[2] = span; values[3] = align; return values; } } |
Creating the Row Component
The row component is also an extension of the panel control. But we are going to add some extra behaviour to it. Just before the Row’s children (the columns) are Rendered we are going to calculate the necessary spans for any columns that did not have a span. If we can’t find any columns as children, then we will assume that we are supposed to wrap all the contents into a single column and therefore we will render a start and end tag for a <div class=”col-md-12″>
Programming the auto-width algorithm
The auto-width algorithm does the following
- Finds the total span of columns that have been explicitly allocated using the span property
- Figures out the remaining span available (e.g. 12 – allocated)
- Figures out the average span to allocate to remaining columns without a span
- Allocates the ‘guessed’ span to the remaining columns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
package com.gregorbyte.xsp.component.bootstrap; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.faces.FacesException; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import com.gregorbyte.xsp.util.GregorbyteUtil; import com.ibm.xsp.component.UIPanelEx; import com.ibm.xsp.util.TypedUtil; public class UIRow extends UIPanelEx { private Boolean autoColumn = false; public UIRow() { super(); } @Override public String getStyleClass() { String parent = super.getStyleClass(); return GregorbyteUtil.concatStyleClasses("row", parent); } @Override public void encodeBegin(FacesContext context) throws IOException { super.encodeBegin(context); calculateColumnSpans(context); if (autoColumn) { context.getResponseWriter().startElement("div", null); context.getResponseWriter().writeAttribute("class", "col-md-12", null); } } @Override public void encodeEnd(FacesContext context) throws IOException { if (autoColumn) { context.getResponseWriter().endElement("div"); } super.encodeEnd(context); } public void calculateColumnSpans(FacesContext context) throws FacesException { List<UIComponent> kids = TypedUtil.getChildren(this); List<UIColumn> cols = new ArrayList<UIColumn>(); int totalMediumSpecified = 0; for (UIComponent comp : kids) { if (comp instanceof UIColumn) { UIColumn col = (UIColumn) comp; if (col.getSpan() != null) { totalMediumSpecified += col.getSpan(); } else { cols.add(col); } } } // If No Columns we will render autoColumn = (cols.size() == 0); if (autoColumn) return; int remaining = 12 - totalMediumSpecified; Integer colGuess = remaining / cols.size(); if (colGuess != null) { for (UIColumn uiColumn : cols) { uiColumn.setGuessSpan(colGuess); } } } @Override public void restoreState(FacesContext context, Object state) { Object[] values = (Object[]) state; super.restoreState(context, values[0]); autoColumn = (Boolean) values[1]; } @Override public Object saveState(FacesContext context) { Object[] values = new Object[2]; values[0] = super.saveState(context); values[1] = autoColumn; return values; } } |
Provide Component Definition via xsp-config file
The xsp-config file tells Domino Designer everything it needs to know to include it on an XPage so we need to specify the properties that can be set.
The file starts with namespace definition to say ‘all component definitions in this file are under this namespace’ and then it specifies the 2 components.
We provide the description, tagname, display name, corresponding java class (so it knows how to instantiate it) a component type (usually just the same as the class).
Then we specify the properties that can be set. You can see for the row there is no extra properties, but for the column we have added the span and align properties and have provided combo options Left, Center and Right for the align property. For the container component, we have specified the fluid property.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
<?xml version="1.0" encoding="UTF-8"?> <faces-config> <faces-config-extension> <namespace-uri>http://www.gregorbyte.com/xsp/</namespace-uri> <default-prefix>gb</default-prefix> </faces-config-extension> <component> <description>Row Container for Bootstrap Columns </description> <display-name>Bootstrap Row</display-name> <component-type>com.gregorbyte.xsp.bootstrap.Row </component-type> <component-class>com.gregorbyte.xsp.component.bootstrap.UIRow </component-class> <component-extension> <base-component-type>com.ibm.xsp.Panel </base-component-type> <component-family>javax.faces.Panel </component-family> <tag-name>row</tag-name> <designer-extension> <in-palette>true</in-palette> <category>Gregorbyte Library</category> </designer-extension> </component-extension> </component> <component> <description>Bootstrap Columns </description> <display-name>Bootstrap Column</display-name> <component-type>com.gregorbyte.xsp.bootstrap.Column </component-type> <component-class>com.gregorbyte.xsp.component.bootstrap.UIColumn </component-class> <property> <description>Adds an Alignment class to the column </description> <display-name>Alignment</display-name> <property-name>align</property-name> <property-class>java.lang.String</property-class> <property-extension> <designer-extension> <editor>com.ibm.workplace.designer.property.editors.comboParameterEditor</editor> <editor-parameter> left center right </editor-parameter> <category>basics</category> </designer-extension> </property-extension> </property> <property> <description>Number of Columns to Span (Medium Screen size). Leave blank for auto calculation</description> <display-name>Columns to Span</display-name> <property-name>span</property-name> <property-class>java.lang.Integer</property-class> <property-extension> <allow-run-time-binding>true</allow-run-time-binding> <designer-extension> <category>basics</category> </designer-extension> </property-extension> </property> <component-extension> <base-component-type>com.ibm.xsp.Panel </base-component-type> <component-family>javax.faces.Panel </component-family> <tag-name>column</tag-name> <designer-extension> <in-palette>true</in-palette> <category>Gregorbyte Library</category> </designer-extension> </component-extension> </component> <component> <description>Container for Bootstrap Layout </description> <display-name>Bootstrap Container</display-name> <component-type>com.gregorbyte.xsp.bootstrap.Container </component-type> <component-class>com.gregorbyte.xsp.component.bootstrap.UIContainer </component-class> <property> <description>Use Fluid Layout </description> <display-name>Use Fluid Layout</display-name> <property-name>fluid</property-name> <property-class>java.lang.Boolean</property-class> <property-extension> <allow-run-time-binding>true</allow-run-time-binding> <designer-extension> <category>display</category> </designer-extension> </property-extension> </property> <component-extension> <base-component-type>com.ibm.xsp.Panel </base-component-type> <component-family>javax.faces.Panel </component-family> <tag-name>container</tag-name> <designer-extension> <in-palette>true</in-palette> <category>Gregorbyte Library</category> </designer-extension> </component-extension> </component> </faces-config> |
Installing
If you would like to use these controls, you have 2 choices!
- Install the latest version of my GregorbyteXspLibrary to your Designer Client and Domino Server using the same method as Extension Library Installation
- Scavenge the necessary files from my Github repository and include it your own NSF
To Scavenge the necessary files from my Github Repo:
- You will need to place these java files in your java design elements:
- /com.gregorbyte.xsp.controls/src/com/gregorbyte/xsp/component/bootstrap/UIColumn.java
- /com.gregorbyte.xsp.controls/src/com/gregorbyte/xsp/component/bootstrap/UIRow.java
- /com.gregorbyte.xsp.controls/src/com/gregorbyte/xsp/component/bootstrap/UIContainer.java
- /com.gregorbyte.xsp.core/src/com/gregorbyte/xsp/util/GregorbyteUtil.java
- You will need to place this xsp-config file in the WEB-INF directory
- /com.gregorbyte.xsp.controls/src/com/gregorbyte/xsp/config/gregorbyte-bootstrap.xsp-config
Conclusion
You can easily get the job done without these controls, but so far I have really enjoyed having them in my palette.
I’ve only just started using these controls, so I expect to improve on them. Maybe by adding the ‘small’, ‘medium’, ‘large’, ‘extraLarge’ properties to the columns (but for now I can use styleClass=”col-sm-4″ etc.)
I’m also keen to take a look at other parts of the bootfaces project and see if any of the ideas might be effective in Xpages.
If you end up using these please let me know! I’d love to hear any suggestions and as always if any problems then please leave a comment or post an issue on my GregorbyteXspLibrary project.