Columns can be moved and validated individually. Sheet is a spreadsheet-like in-cell editing component based on Handsontable by Handsoncode.
Sheet supports movable columns and rows with sorting and filtering. Drag a column header to reorder; sort and filter elements follow the column, and columns without them (like Icon) can be moved freely.
Cells and columns can be validated individually as well as marked required: converters, range validators, JavaScript validators (onvalidate) and Faces validators.
<div class="card">
<h5>Movable Columns</h5>
<p>Sheet supports <strong>movable columns and rows</strong> with sorting and filtering. Drag a column header to reorder; sort and filter elements follow the column, and columns without them (like Icon) can be moved freely.</p>
<h:form id="formMovableColumns">
<p:messages id="messages" showSummary="true" showDetail="true"/>
<p:importEnum type="org.primefaces.extensions.showcase.model.sheet.AssetType" var="AssetType"/>
<p:importEnum type="org.primefaces.extensions.showcase.model.sheet.PlatformType" var="PlatformType"/>
<pe:sheet id="sheet" widgetVar="sheetMovableWidget" value="#{sheetController.assets}" var="row"
height="400" rowKey="#{row.assetId}" showRowHeaders="true"
resizableCols="true" resizableRows="true"
movableCols="true" movableRows="true"
filteredValue="#{sheetController.filteredAssets}"
sortBy="#{row.assetId}" sortOrder="ascending">
<p:ajax event="change" listener="#{sheetController.cellChangeEvent}" update="messages"/>
<f:facet name="header">
<h:outputText value="Assets (drag columns to reorder)"/>
</f:facet>
<f:facet name="footer">
<h:outputText id="footer" value="#{sheetController.filteredAssets.size()} Records"/>
</f:facet>
<pe:sheetcolumn headerText="Id" readOnly="true" value="#{row.assetId}"
sortBy="#{row.assetId}" filterBy="#{row.assetId}" colWidth="150"/>
<pe:sheetcolumn headerText="Icon" readOnly="true" styleClass="text-center"
value="#{row.icon}" colWidth="80"/>
<pe:sheetcolumn headerText="Type" value="#{row.assetType}" colWidth="100"
sortBy="#{row.assetType}" filterBy="#{row.assetType}"
filterOptions="#{sheetOptionProducer.assetTypes}"
colType="dropdown" selectItems="#{AssetType.ALL_VALUES}"/>
<pe:sheetcolumn headerText="Platform" value="#{row.platform}" colWidth="100"
sortBy="#{row.platform}" filterBy="#{row.platform}"
filterOptions="#{sheetOptionProducer.platformTypes}"
colType="autocomplete" selectItems="#{PlatformType.ALL_VALUES}"
autoCompleteStrict="false" autoCompleteVisibleRows="4"/>
<pe:sheetcolumn headerText="Arch" value="#{row.platformArch}" readOnly="true"
colWidth="100" sortBy="#{row.platformArch}"
filterBy="#{row.platformArch}"
filterOptions="#{sheetOptionProducer.archTypes}"/>
<pe:sheetcolumn headerText="Active" value="#{row.active}" colWidth="60" colType="checkbox"/>
<pe:sheetcolumn headerText="Price" value="#{row.purchasePrice}" colWidth="100"
styleClass="htRight" colType="numeric"
numericLocale="de-DE" numericPattern="$0,0.00" required="true"
requiredMessage="Price is required!"/>
<pe:sheetcolumn headerText="Date" value="#{row.purchaseDate}" colWidth="110"
sortBy="#{row.purchaseDate}" styleClass="htCenter"
colType="date" dateFormat="YYYY-MM-DD">
<f:convertDateTime type="date" pattern="yyyy-MM-dd"/>
</pe:sheetcolumn>
</pe:sheet>
</h:form>
</div>
<div class="card">
<h5>Validation</h5>
<p>Cells and columns can be validated individually as well as marked required: converters, range validators, JavaScript validators (<code>onvalidate</code>) and Faces validators.</p>
<h:form id="formValidation">
<p:messages id="messages" showSummary="true" showDetail="true"/>
<pe:sheet id="sheet" value="#{sheetAjaxController.assets}" var="row" height="400" rowKey="#{row.assetId}"
showRowHeaders="true" sortBy="#{row.assetId}" sortOrder="ascending" width="1000"
widgetVar="sheetValidationWidget">
<p:ajax event="change" listener="#{sheetAjaxController.cellChangeEvent}" update="messages"/>
<f:facet name="header">
<h:outputText value="Assets"/>
</f:facet>
<pe:sheetcolumn headerText="Id (readOnly)" readOnly="true" value="#{row.assetId}" colWidth="150"/>
<pe:sheetcolumn headerText="Required" value="#{row.hostName}" colWidth="140"
required="true" requiredMessage="This field is required!"/>
<pe:sheetcolumn id="colBigDecimal" headerText="BigDecimal(1-10)" value="#{row.purchasePrice}" colWidth="100" styleClass="htRight">
<f:converter converterId="jakarta.faces.BigDecimal"/>
<f:validateDoubleRange minimum="1.7" maximum="10.5" for="sheet"/>
</pe:sheetcolumn>
<pe:sheetcolumn headerText="Integer" value="#{row.value1}" colWidth="100" styleClass="htRight" onvalidate="numeric">
<f:converter converterId="jakarta.faces.Integer"/>
</pe:sheetcolumn>
<pe:sheetcolumn headerText="Regex" value="#{row.value2}" colWidth="100" styleClass="htRight" onvalidate="myRegex">
<f:converter converterId="jakarta.faces.Integer"/>
</pe:sheetcolumn>
<pe:sheetcolumn headerText="JS Exactly 6" value="#{row.value3}" colWidth="100" styleClass="htRight" onvalidate="validateEquals6">
<f:converter converterId="jakarta.faces.Integer"/>
</pe:sheetcolumn>
<pe:sheetcolumn headerText="Faces Exactly 5" value="#{row.value4}" colWidth="100" styleClass="htRight"
validator="#{sheetAjaxController.validateExactly5}">
<f:converter converterId="jakarta.faces.Integer"/>
</pe:sheetcolumn>
<f:facet name="footer">
<h:outputText value="#{sheetAjaxController.assets.size()} Records"/>
</f:facet>
</pe:sheet>
</h:form>
<script type="text/javascript">
var myRegex = /^\d+$/; // regex only digits
// function must return callback(bool)
function validateEquals6(value, callback) {
if (value != 6) {
callback(false);
} else {
callback(true);
}
}
</script>
</div>
package org.primefaces.extensions.showcase.controller.sheet;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.primefaces.extensions.component.sheet.Sheet;
import org.primefaces.extensions.event.SheetEvent;
import org.primefaces.extensions.model.sheet.SheetUpdate;
import org.primefaces.extensions.showcase.model.sheet.Asset;
import org.primefaces.extensions.showcase.model.sheet.AssetType;
import org.primefaces.extensions.showcase.model.sheet.PlatformArchType;
import org.primefaces.extensions.showcase.model.sheet.PlatformType;
/**
* {@link Sheet} Controller.
*
* @author Melloware [email protected]
*/
@Named
@ViewScoped
public class SheetController implements Serializable {
@Serial
private static final long serialVersionUID = 20120224L;
private List<Asset> assets = new ArrayList<>();
private List<Asset> filteredAssets = new ArrayList<>();
public SheetController() {
addAssets(40, PlatformType.Linux, PlatformArchType.AMD64, AssetType.SERVER);
addAssets(50, PlatformType.Windows, PlatformArchType.I386, AssetType.DESKTOP);
addAssets(60, PlatformType.Mac, PlatformArchType.OTHER, AssetType.LAPTOP);
addAssets(5, null, null, AssetType.PRINTER);
}
public void cellChangeEvent(final SheetEvent event) {
final Sheet sheet = event.getSheet();
final List<SheetUpdate> updates = sheet.getUpdates();
// A SheetUpdate exists for each column updated, the same row may
// appear more than once. For this reason we will track those we already
// persisted
final HashSet<Asset> processed = new HashSet<Asset>();
int rowUpdates = 0;
for (final SheetUpdate update : updates) {
final Asset asset = (Asset) update.getRowData();
if (processed.contains(asset)) {
continue;
}
System.out.println("Asset " + asset.getAssetId() + " updated.");
rowUpdates++;
}
sheet.commitUpdates();
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Update Success", rowUpdates + " rows updated"));
}
private void addAssets(final int count, final PlatformType platform, final PlatformArchType arch,
final AssetType type) {
for (int i = 0; i < count; i++) {
final Asset asset = new Asset();
asset.setAssetId(RandomUtils.secure().randomLong());
asset.setActive(RandomUtils.secure().randomBoolean());
asset.setPlatform(platform);
asset.setPlatformArch(arch);
asset.setHostName(type.toString().toLowerCase() + i + ".example.lan");
asset.setAssetType(type);
asset.setIcon("<i class='fas fa-info-circle' style='color:cornflowerblue' />");
asset.setPurchaseDate(new Date());
asset.setPurchaseTime(new Date());
asset.setValue1(RandomUtils.secure().randomInt(1, 1000));
asset.setPassword(RandomStringUtils.secure().nextAlphabetic(6));
asset.setPurchasePrice(
BigDecimal.valueOf(RandomUtils.secure().randomDouble(1.11, 999.99) * (RandomUtils.secure().randomBoolean() ? -1 : 1))
.setScale(2, RoundingMode.HALF_UP));
getAssets().add(asset);
}
}
public List<Asset> getAssets() {
return assets;
}
public void setAssets(final List<Asset> assets) {
this.assets = assets;
}
public List<Asset> getFilteredAssets() {
return filteredAssets;
}
public void setFilteredAssets(final List<Asset> filteredAssets) {
this.filteredAssets = filteredAssets;
}
}
package org.primefaces.extensions.showcase.model.sheet;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
public class Asset implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long assetId;
private Date purchaseDate;
private Date purchaseTime;
private String hostName;
private String description;
private String icon;
private BigDecimal purchasePrice;
private PlatformType platform;
private PlatformArchType platformArch;
private AssetType assetType;
private Boolean active;
private String comment;
private String password;
private Date lastUpdated;
private String patchLevel;
private String allocatedTo;
private String location;
private Integer value1;
private Integer value2;
private Integer value3;
private Integer value4;
private Integer value5;
private Integer value6;
private Integer value7;
private Integer value8;
private Integer value9;
private Integer value10;
public Long getAssetId() {
return assetId;
}
public void setAssetId(final Long assetId) {
this.assetId = assetId;
}
public Date getPurchaseDate() {
return purchaseDate;
}
public void setPurchaseDate(final Date purchaseDate) {
this.purchaseDate = purchaseDate;
}
public String getHostName() {
return hostName;
}
public void setHostName(final String hostName) {
this.hostName = hostName;
}
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
public String getIcon() {
return icon;
}
public void setIcon(final String icon) {
this.icon = icon;
}
public BigDecimal getPurchasePrice() {
return purchasePrice;
}
public void setPurchasePrice(final BigDecimal purchasePrice) {
this.purchasePrice = purchasePrice;
}
public PlatformType getPlatform() {
return platform;
}
public void setPlatform(final PlatformType platform) {
this.platform = platform;
}
public PlatformArchType getPlatformArch() {
return platformArch;
}
public void setPlatformArch(final PlatformArchType platformArch) {
this.platformArch = platformArch;
}
public AssetType getAssetType() {
return assetType;
}
public void setAssetType(final AssetType assetType) {
this.assetType = assetType;
}
public String getComment() {
return comment;
}
public void setComment(final String comment) {
this.comment = comment;
}
public Date getLastUpdated() {
return lastUpdated;
}
public void setLastUpdated(final Date lastUpdated) {
this.lastUpdated = lastUpdated;
}
public String getPatchLevel() {
return patchLevel;
}
public void setPatchLevel(final String patchLevel) {
this.patchLevel = patchLevel;
}
public String getAllocatedTo() {
return allocatedTo;
}
public void setAllocatedTo(final String allocatedTo) {
this.allocatedTo = allocatedTo;
}
public String getLocation() {
return location;
}
public void setLocation(final String location) {
this.location = location;
}
public Integer getValue1() {
return value1;
}
public void setValue1(final Integer value1) {
this.value1 = value1;
}
public Integer getValue2() {
return value2;
}
public void setValue2(final Integer value2) {
this.value2 = value2;
}
public Integer getValue3() {
return value3;
}
public void setValue3(final Integer value3) {
this.value3 = value3;
}
public Integer getValue4() {
return value4;
}
public void setValue4(final Integer value4) {
this.value4 = value4;
}
public Integer getValue5() {
return value5;
}
public void setValue5(final Integer value5) {
this.value5 = value5;
}
public Integer getValue6() {
return value6;
}
public void setValue6(final Integer value6) {
this.value6 = value6;
}
public Integer getValue7() {
return value7;
}
public void setValue7(final Integer value7) {
this.value7 = value7;
}
public Integer getValue8() {
return value8;
}
public void setValue8(final Integer value8) {
this.value8 = value8;
}
public Integer getValue9() {
return value9;
}
public void setValue9(final Integer value9) {
this.value9 = value9;
}
public Integer getValue10() {
return value10;
}
public void setValue10(final Integer value10) {
this.value10 = value10;
}
public String getPassword() {
return password;
}
public void setPassword(final String password) {
this.password = password;
}
public Boolean getActive() {
return active;
}
public void setActive(final Boolean active) {
this.active = active;
}
public Date getPurchaseTime() {
return purchaseTime;
}
public void setPurchaseTime(final Date purchaseTime) {
this.purchaseTime = purchaseTime;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (assetId == null ? 0 : assetId.hashCode());
result = prime * result + (hostName == null ? 0 : hostName.hashCode());
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Asset other = (Asset) obj;
if (assetId == null) {
if (other.assetId != null) {
return false;
}
}
else if (!assetId.equals(other.assetId)) {
return false;
}
if (hostName == null) {
return other.hostName == null;
}
else {
return hostName.equals(other.hostName);
}
}
public Integer getTotal() {
return (value1 == null ? 0 : value1) +
(value2 == null ? 0 : value2) +
(value3 == null ? 0 : value3) +
(value4 == null ? 0 : value4) +
(value5 == null ? 0 : value5) +
(value6 == null ? 0 : value6) +
(value7 == null ? 0 : value7) +
(value8 == null ? 0 : value8) +
(value9 == null ? 0 : value9) +
(value10 == null ? 0 : value10);
}
}
package org.primefaces.extensions.showcase.model.sheet;
public enum PlatformType {
Windows, Linux, Mac
}
package org.primefaces.extensions.showcase.model.sheet;
public enum PlatformArchType {
I386, AMD64, OTHER
}
package org.primefaces.extensions.showcase.model.sheet;
public enum AssetType {
DESKTOP, SERVER, LAPTOP, PRINTER, OTHER
}
package org.primefaces.extensions.showcase.controller.sheet;
import java.util.*;
import jakarta.annotation.*;
import jakarta.enterprise.context.*;
import jakarta.faces.model.*;
import jakarta.inject.*;
import org.primefaces.extensions.showcase.model.sheet.*;
@Named
@ApplicationScoped
public class SheetOptionProducer {
private List<SelectItem> assetTypes;
private List<SelectItem> platformTypes;
private List<SelectItem> archTypes;
@PostConstruct
private void init() {
assetTypes = createEnumList(AssetType.values());
platformTypes = createEnumList(PlatformType.values());
archTypes = createEnumList(PlatformArchType.values());
}
private static <T extends Enum<?>> List<SelectItem> createEnumList(final T[] values) {
final List<SelectItem> result = new ArrayList<>();
result.add(new SelectItem("", "All"));
for (final T value : values) {
result.add(new SelectItem(value, value.name()));
}
return result;
}
public List<SelectItem> getAssetTypes() {
return assetTypes;
}
public List<SelectItem> getPlatformTypes() {
return platformTypes;
}
public List<SelectItem> getArchTypes() {
return archTypes;
}
}
package org.primefaces.extensions.showcase.controller.sheet;
import java.io.Serial;
import java.util.List;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.ValidatorException;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import org.apache.commons.collections4.IterableUtils;
import org.primefaces.extensions.component.sheet.Sheet;
import org.primefaces.extensions.event.SheetEvent;
import org.primefaces.extensions.model.sheet.SheetUpdate;
import org.primefaces.extensions.showcase.model.sheet.Asset;
/**
* {@link Sheet} Ajax Controller.
*
* @author Melloware [email protected]
*/
@Named
@ViewScoped
public class SheetAjaxController extends SheetController {
@Serial
private static final long serialVersionUID = 20120224L;
private Asset selectedAsset;
/**
* Ajax callback from the Sheet component when a cell value is changed.
*/
@Override
public void cellChangeEvent(final SheetEvent event) {
final Sheet sheet = event.getSheet();
final List<SheetUpdate> updates = sheet.getUpdates();
// only show 1 update
SheetUpdate sheetUpdate = IterableUtils.first(updates);
final Long id = (Long) sheetUpdate.getRowKey();
final Object oldValue = sheetUpdate.getOldValue();
final Object newValue = sheetUpdate.getNewValue();
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Update Success",
String.format("Asset %s updated. Old Value = %s, New Value = %s", id, oldValue, newValue)));
sheet.commitUpdates();
}
/**
* Ajax callback from the Sheet component when a column is selected.
*/
public static void columnSelectEvent(final SheetEvent event) {
final Sheet sheet = event.getSheet();
final int column = sheet.getSelectedColumn() + 1;
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Column Selected", String.format("Column %d selected.", column)));
}
/**
* Ajax callback from the Sheet component when a row is selected.
*/
public void rowSelectEvent(final SheetEvent event) {
selectedAsset = (Asset) event.getRowData();
final int row = event.getRowIndex() + 1;
final String hostName = (selectedAsset != null) ? selectedAsset.getHostName() : "N/A";
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Row Selected",
String.format("Row %d selected. Host: %s", row, hostName)));
}
public Asset getSelectedAsset() {
return selectedAsset;
}
public static void validateExactly5(final FacesContext context, final UIComponent comp, final Object value) {
if (context == null || comp == null) {
return;
}
final Integer integer = (Integer) value;
if (integer.intValue() != 5) {
final FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Error",
"Value must only be 5 exactly!");
throw new ValidatorException(message);
}
}
}