Here in autocomplete I get the Product name as expected.
I want to do some calculation based on the product selected. But in doCalculation function i'm getting id
instead of 'price'. So calculation not working as expected.
Suppose if i change String idExpression = "#{price}";
then calculation works as expected but Order not saved. Since getting error as below
Failed to convert property value of type [java.lang.String] to required type [com.myapp.domain.Product] for property product; nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@javax.persistence.OneToOne
@io.springlets.format.EntityFormat com.myapp.domain.Product] for value 2500; nested exception is java.lang.IllegalStateException: Parsers are not allowed to return null: io.springlets.format.EntityParser@2201ba1c
So I want to get the price for calculation at the same time the save feature should not be broken. For now either 1st or 2nd is working for me.
ProductsCollectionThymeleafController.java
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, name = "select2", value = "/s2")
@ResponseBody
public ResponseEntity<Select2DataSupport<Product>> select2(GlobalSearch search, Pageable pageable,
Locale locale) {
Page<Product> products = getProductService().findAll(search, pageable);
String idExpression = "#{id}";
Select2DataSupport<Product> select2Data =
new Select2DataWithConversion<Product>(products, idExpression, getConversionService());
return ResponseEntity.ok(select2Data);
}
OrderCollectionThymeleafController.java
@PostMapping(name = "create")
public ModelAndView create(@Valid @ModelAttribute Order order, BindingResult result,
Model model) {
if (result.hasErrors()) {
populateForm(model);
return new ModelAndView("/order/create");
}
Order newOrder = getOrderService().save(order);
UriComponents showURI = getItemLink().to(OrderItemThymeleafLinkFactory.SHOW)
.with("order", newOrder.getId()).toUri();
return new ModelAndView("redirect:" + showURI.toUriString());
}
orderview.html
<form class="form-horizontal validate" method="POST" data-th-object="${order}" data-th-action="@{${collectionLink.to('create').with('order', order.id)}}">
<fieldset id="containerFields">
<div class="form-group has-error has-feedback" data-z="3c00987d" id="servicio-product-field" data-th-classappend="${#fields.hasErrors('product')}? 'has-error has-feedback'" data-th-class="form-group" data-th-with="collectionLink=${@linkBuilder.of('ProductsCollectionThymeleafController')}">
<label for="product" class="col-md-3 control-label" data-th-text="#{label_servicio_product}">Product</label>
<div class="col-md-6">
<!-- Select2 -->
<select data-th-field="*{product}" onChange="doCalculation()" class="form-control dropdown-select-ajax" data-allow-clear="true" data-data-ajax--url="${collectionLink.to('select2')}" data-ajax--cache="true" data-ajax--delay="250" data-ajax--data-type="json" data-data-placeholder="#{info_select_an_option}">
<option data-th-unless="*{product} == null" data-th-value="*{product.id}" data-th-text="*{{product}}" selected="selected">Product</option>
</select>
<span data-th-classappend="${#fields.hasErrors('product')}? 'glyphicon glyphicon-remove form-control-feedback'" class="glyphicon glyphicon-remove form-control-feedback" data-th-if="${#fields.hasErrors('product')}" aria-hidden="true"></span>
<span id="product-error" class="help-block" data-th-if="${#fields.hasErrors('product')}" data-th-errors="*{product}">Error message.</span>
</div>
</div>
<script>
function doCalculation() {
var price = document.getElementById("product").value;
alert("price: " + price);
// Do some calculation
}
doCalculation();
</script>
</fieldset>
</form>
Product.java
@RooJavaBean
@RooToString
@RooJpaEntity
@RooEquals(isJpaEntity = true)
@Entity
@EntityFormat
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String productName;
@Min(1L)
@NumberFormat
private Integer price;
@OneToOne(fetch = FetchType.LAZY)
@EntityFormat
private Order order;
public static final String ITERABLE_TO_ADD_CANT_BE_NULL_MESSAGE = "The given Iterable of items to add can't be null!";
public static final String ITERABLE_TO_REMOVE_CANT_BE_NULL_MESSAGE = "The given Iterable of items to add can't be null!";
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getPrice() {
return this.price;
}
public void setPrice(Integer price) {
this.price = price;
}
public String getProductName() {
return this.productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public Order getOrder() {
return this.order;
}
public void setOrder(Order order) {
this.order= order;
}
}
Order.java
@RooJavaBean
@RooToString
@RooJpaEntity
@RooEquals(isJpaEntity = true)
@Entity
@EntityFormat
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
private Integer version;
@OneToOne(cascade = { javax.persistence.CascadeType.MERGE,
javax.persistence.CascadeType.PERSIST }, fetch = FetchType.LAZY, mappedBy = "order")
@RooJpaRelation(type = JpaRelationType.AGGREGATION)
@EntityFormat
private Product product;
public static final String ITERABLE_TO_ADD_CANT_BE_NULL_MESSAGE = "The given Iterable of items to add can't be null!";
public static final String ITERABLE_TO_REMOVE_CANT_BE_NULL_MESSAGE = "The given Iterable of items to add can't be null!";
/**
* This `equals` implementation is specific for JPA entities and uses the
* entity identifier for it, following the article in
* https://vladmihalcea.com/2016/06/06/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
*
* @param obj
* @return Boolean
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
// instanceof is false if the instance is null
if (!(obj instanceof Order)) {
return false;
}
return getId() != null && Objects.equals(getId(), ((Order) obj).getId());
}
/**
* This `hashCode` implementation is specific for JPA entities and uses a
* fixed `int` value to be able to identify the entity in collections after
* a new id is assigned to the entity, following the article in
* https://vladmihalcea.com/2016/06/06/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
*
* @return Integer
*/
public int hashCode() {
return 31;
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getVersion() {
return this.version;
}
public void setVersion(Integer version) {
this.version = version;
}
public Product getProduct() {
return this.product;
}
public void setProduct(Product product) {
this.product = product;
}
public void addToProduct(Product product) {
if (product == null) {
removeFromProduct();
} else {
this.product = product;
product.setOrder(this);
}
}
public void removeFromProduct() {
if (this.product != null) {
product.setOrder(null);
}
this.product = null;
}
}
By default, the Select2DataWithConversion
data type only returns the identifier that will be set as value
attribute of the option
element and the representation of the object (in your case the product name) as the text
attribute of the option
element.
That is the minimum info that the select2 component needs to be constructed.
https://select2.org/data-sources/formats
However, as you described in your answer, it's really common to need more info in your Select2 component. For that reason, we overloaded the constructor of Select2DataWithConversion
including a boolean parameter to return the entire information of the object.
Check this overloaded constructor here:
So, you just need to change your ProductsCollectionThymeleafController.java to use it like:
Select2DataSupport<Product> select2Data = new Select2DataWithConversion<Product>(products, idExpression, getConversionService(), true);
Now that yor select2 component is going to receive extra information, you need to store it in a data-*
attribute of your select2 option during the option creation. To do that use the templateSelection
function that offers the select2 component.
https://select2.org/programmatic-control/retrieving-selections#using-a-jquery-selector
Now, your doCalculation
should obtain the selected option and after that, the data-price
attribute.
<script>
function doCalculation() {
var price = $('#product').find(':selected').data('price');
alert("price: " + price);
//Do some calculation
}
doCalculation();
</script>
And that's all!
EDIT: I've just create the following project where you could find your desired behaviour: https://github.com/jcagarcia/proofs/tree/master/select2-with-extra-info Just check the necessary changes in the following commit: https://github.com/jcagarcia/proofs/commit/105c18f7ad0da4d1e2089fbf71d4f27ccdb60689
Hope it helps,