Snippets

Foreach SortableTableBuilder demo functionality

Created by Arne Vandamme
/*
 * Copyright 2014 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.foreach.across.samples.entity.application.business;

import com.foreach.across.modules.hibernate.business.SettableIdBasedEntity;
import com.foreach.across.modules.hibernate.id.AcrossSequenceGenerator;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;

/**
 * Sample entity representing a partner with a name and url, used for sortable table demo controller.
 *
 * @author Arne Vandamme
 * @since 2.0.0
 */
@Entity
@Table(name = "test_partner")
public class Partner extends SettableIdBasedEntity<Partner>
{
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO, generator = "seq_test_partner_id")
	@GenericGenerator(
			name = "seq_test_partner_id",
			strategy = AcrossSequenceGenerator.STRATEGY,
			parameters = {
					@org.hibernate.annotations.Parameter(name = "sequenceName", value = "seq_test_partner_id"),
					@org.hibernate.annotations.Parameter(name = "allocationSize", value = "1")
			}
	)
	private Long id;

	@NotBlank
	@Length(max = 250)
	@Column(unique = true)
	private String name;

	@NotBlank
	@Length(max = 250)
	@Column(unique = false)
	private String url;

	public Partner() {
	}

	public Partner( String name ) {
		this.name = name;
	}

	public Long getId() {
		return id;
	}

	public void setId( Long id ) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName( String name ) {
		this.name = name;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl( String url ) {
		this.url = url;
	}
}
/*
 * Copyright 2014 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.foreach.across.samples.entity.application.repositories;

import com.foreach.across.modules.hibernate.jpa.repositories.IdBasedEntityJpaRepository;
import com.foreach.across.samples.entity.application.business.Partner;

/**
 * @author Arne Vandamme
 */
public interface PartnerRepository extends IdBasedEntityJpaRepository<Partner>
{
}
/*
 * Copyright 2014 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.foreach.across.samples.entity.application.controllers;

import com.foreach.across.core.annotations.Event;
import com.foreach.across.modules.adminweb.annotations.AdminWebController;
import com.foreach.across.modules.adminweb.menu.AdminMenuEvent;
import com.foreach.across.modules.bootstrapui.elements.BootstrapUiFactory;
import com.foreach.across.modules.bootstrapui.elements.GlyphIcon;
import com.foreach.across.modules.bootstrapui.elements.TableViewElement;
import com.foreach.across.modules.entity.views.EntityViewElementBuilderHelper;
import com.foreach.across.modules.entity.views.bootstrapui.util.SortableTableBuilder;
import com.foreach.across.modules.entity.views.util.EntityViewElementUtils;
import com.foreach.across.modules.web.ui.ViewElement;
import com.foreach.across.modules.web.ui.ViewElementBuilderContext;
import com.foreach.across.modules.web.ui.elements.ContainerViewElement;
import com.foreach.across.modules.web.ui.elements.TextViewElement;
import com.foreach.across.samples.entity.application.business.Partner;
import com.foreach.across.samples.entity.application.repositories.PartnerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static com.foreach.across.modules.bootstrapui.elements.Style.Table.BORDERED;
import static com.foreach.across.modules.bootstrapui.elements.Style.Table.CONDENSED;
import static com.foreach.across.modules.web.ui.elements.support.ContainerViewElementUtils.find;

/**
 * Generates some tables using the {@link SortableTableBuilder}.  Does not include the default javascript
 * for activating client-side support of paging and sorting.  The different tables demonstrate some of the
 * features of the {@link SortableTableBuilder}.
 * <p/>
 * These demo pages expect a Partner entity with a *name* and *url* property.  At least 3 partner entities
 * should exist (with id -1, -2, -3) for this controller to work.
 *
 * @author Arne Vandamme
 * @see SortableTableWithPagingController
 * @since 2.0.0
 */
@AdminWebController
@RequestMapping("/sortableTable")
public class SortableTableSimpleController
{
	private PartnerRepository partnerRepository;
	private EntityViewElementBuilderHelper builderHelper;
	private BootstrapUiFactory bootstrapUiFactory;

	@Autowired
	public SortableTableSimpleController( PartnerRepository partnerRepository,
	                                      EntityViewElementBuilderHelper builderHelper,
	                                      BootstrapUiFactory bootstrapUiFactory ) {
		this.partnerRepository = partnerRepository;
		this.builderHelper = builderHelper;
		this.bootstrapUiFactory = bootstrapUiFactory;
	}

	/**
	 * Register the section in the administration menu.
	 */
	@Event
	@SuppressWarnings("unused")
	protected void registerMenuItems( AdminMenuEvent adminMenu ) {
		adminMenu.builder()
		         .group( "/test", "Functionality demos" ).and()
		         .group( "/test/st", "Sortable tables" ).and()
		         .item( "/test/st/noPaging", "Paging/sorting disabled", "/sortableTable" ).order( 1 )
		         .and()
		         .item( "/test/st/paging", "Paging/sorting enabled", "/sortableTableWithPaging" )
		         .order( 2 );
	}

	/**
	 * Entry point that adds the different tables to the model.
	 * This method does not much more but dispatch to the specific table creation methods.
	 * See the separate methods for more documentation.
	 */
	@RequestMapping(method = RequestMethod.GET)
	public String listSortableTables( Model model, ViewElementBuilderContext builderContext ) {
		List<Partner> partners = partnerRepository.findAll();

		model.addAttribute(
				"message",
				"These tables do not support paging or sorting because the default javascript is not included."
		);

		Map<String, ViewElement> generatedTables = new LinkedHashMap<>();
		generatedTables.put(
				"Minimal table configuration - listing all properties",
				minimalTableOfAllProperties( partners, builderContext )
		);
		generatedTables.put(
				"Table without any results",
				noResultsTable( builderContext )
		);
		generatedTables.put(
				"Table with specific properties and custom styling",
				styledTableWithSelectedProperties( partners, builderContext )
		);
		generatedTables.put(
				"Table with footer and styling added after being generated",
				postProcessedTable( partners, builderContext )
		);
		generatedTables.put(
				"Table with additional cell",
				tableWithCustomCell( partners, builderContext )
		);

		model.addAttribute( "generatedTables", generatedTables );

		return "th/entityModuleTest/sortableTables";
	}

	/**
	 * [1]
	 * Generates a table with only the minimum number of properties of SortableTableBuilder set.
	 * All builders are pre-configured using the EntityConfiguration of our Partner entity.
	 * This is done in the EntityViewElementBuilderHelper class.
	 */
	private ViewElement minimalTableOfAllProperties( List<Partner> partners,
	                                                 ViewElementBuilderContext builderContext ) {
		return builderHelper.createSortableTableBuilder( Partner.class )
		                    .items( partners )              // rows in the table
		                    .properties( "*" )              // columns - render all default properties of Partner
		                    .build( builderContext );       // build the actual TableViewElement (and surrounding containers)
	}

	/**
	 * [2]
	 * Generates a table that does not have any results, because we do not add any items.
	 * The default noResults panel will be generated.
	 */
	private ViewElement noResultsTable( ViewElementBuilderContext builderContext ) {
		return builderHelper.createSortableTableBuilder( Partner.class )
		                    .properties( "*" )
		                    .build( builderContext );
	}

	/**
	 * [3]
	 * Customizes the look and feel of the table using the methods available on the builder.
	 * These cover most of the common use cases like rendering only the table, disabling sorting etc.
	 */
	private ViewElement styledTableWithSelectedProperties( List<Partner> partners,
	                                                       ViewElementBuilderContext builderContext ) {
		return builderHelper.createSortableTableBuilder( Partner.class )
		                    .items( partners )
		                    .properties( "id", "url", "name" )      // specify the properties in order
		                    .tableStyles( CONDENSED, BORDERED )     // add some bootstrap table styles
		                    .tableOnly()                            // only render the table - not the surrounding panel with paging/results information
		                    .noSorting()                            // disable sorting on all properties
		                    .hideResultNumber()                     // hide the result number column
		                    .build( builderContext );
	}

	/**
	 * [4]
	 * Generate the default table but make some changes to the ViewElement after it has been built.
	 * Not necessarily the easiest or even best way to achieve what you want, but it can be done.
	 * This code demonstrates making view elements directly, skipping the BootstrapUiFactory.
	 * <p>
	 * IMPORTANT: SortableTableBuilder uses a ViewElementGenerator for the building of the rows.  That means
	 * the rows itself are actually only built when being rendered.  That means it is not possible to actually
	 * modify the generated rows individually.
	 */
	private ViewElement postProcessedTable( List<Partner> partners, ViewElementBuilderContext builderContext ) {
		ContainerViewElement container = builderHelper.createSortableTableBuilder( Partner.class )
		                                              .items( partners )
		                                              .properties( "*" )
		                                              .build( builderContext );

		// after the table has been generated we receive a container of elements (including the panel),
		// find the actual table element and make some modifications
		find( container, SortableTableBuilder.ELEMENT_TABLE, TableViewElement.class )
				.ifPresent( table -> {
					            // set custom style attribute
					            table.setAttribute( "style", "border: solid 3px red;" );

					            // manually create ViewElement instances
					            TableViewElement.Footer footer = new TableViewElement.Footer();
					            TableViewElement.Row footerRow = new TableViewElement.Row();
					            TableViewElement.Cell footerCell = new TableViewElement.Cell();
					            footerCell.setColumnSpan( 3 );
					            footerCell.addChild( new TextViewElement( "Manually added footer row." ) );
					            footerRow.addChild( footerCell );
					            footer.addChild( footerRow );

					            table.setFooter( footer );
				            }
				);

		return container;
	}

	/**
	 * [5]
	 * Customize the table by adding a column with a link that opens the partner url in a new window.
	 * Illustrates how you can access the entity for which the row is being generated.
	 */
	private ViewElement tableWithCustomCell( List<Partner> partners,
	                                         ViewElementBuilderContext builderContext ) {
		return builderHelper
				.createSortableTableBuilder( Partner.class )
				.items( partners )
				.properties( "*" )
				.headerRowProcessor( ( ctx, element ) -> {
					// add cell to the header
					element.addChild( bootstrapUiFactory.table().heading().build( ctx ) );
				} )
				.valueRowProcessor( ( ctx, element ) -> {
					Partner partner = EntityViewElementUtils.currentEntity( ctx, Partner.class );

					// add cell linking to the url of the partner
					element.addChild(
							bootstrapUiFactory.table().cell().add(
									bootstrapUiFactory.button()
									                  .link( partner.getUrl() )
									                  .icon( new GlyphIcon( GlyphIcon.NEW_WINDOW ) )
									                  .iconOnly()
									                  .attribute( "target", "_blank" )
									                  .text( "Visit partner website" )
							).build( ctx )
					);
				} )
				.build( builderContext );
	}
}
/*
 * Copyright 2014 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.foreach.across.samples.entity.application.controllers;

import com.foreach.across.modules.adminweb.annotations.AdminWebController;
import com.foreach.across.modules.bootstrapui.elements.BootstrapUiFactory;
import com.foreach.across.modules.bootstrapui.elements.TableViewElement;
import com.foreach.across.modules.entity.EntityModule;
import com.foreach.across.modules.entity.views.EntityViewElementBuilderHelper;
import com.foreach.across.modules.entity.views.bootstrapui.util.SortableTableBuilder;
import com.foreach.across.modules.entity.views.util.EntityViewElementUtils;
import com.foreach.across.modules.web.resource.WebResource;
import com.foreach.across.modules.web.resource.WebResourceRegistry;
import com.foreach.across.modules.web.ui.ViewElement;
import com.foreach.across.modules.web.ui.ViewElementBuilderContext;
import com.foreach.across.samples.entity.application.business.Partner;
import com.foreach.across.samples.entity.application.repositories.PartnerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Generates some tables using the {@link SortableTableBuilder} and includes the client-side support
 * for paging and sorting.
 *
 * @author Arne Vandamme
 * @see SortableTableSimpleController
 * @since 2.0.0
 */
@AdminWebController
@RequestMapping("/sortableTableWithPaging")
public class SortableTableWithPagingController
{
	private PartnerRepository partnerRepository;
	private EntityViewElementBuilderHelper builderHelper;
	private BootstrapUiFactory bootstrapUiFactory;

	@Autowired
	public SortableTableWithPagingController( PartnerRepository partnerRepository,
	                                          EntityViewElementBuilderHelper builderHelper,
	                                          BootstrapUiFactory bootstrapUiFactory ) {
		this.partnerRepository = partnerRepository;
		this.builderHelper = builderHelper;
		this.bootstrapUiFactory = bootstrapUiFactory;
	}

	/**
	 * Registers the CSS/Javascript packed with EntityModule to enable paging and sorting support
	 * on all tables on the page.
	 */
	@ModelAttribute
	public void registerWebResources( WebResourceRegistry registry ) {
		registry.addWithKey( WebResource.CSS, EntityModule.NAME, "/css/entity/entity-module.css", WebResource.VIEWS );
		registry.addWithKey(
				WebResource.JAVASCRIPT_PAGE_END, EntityModule.NAME, "/js/entity/entity-module.js", WebResource.VIEWS
		);
	}
	/**
	 * Entry point that adds the different tables to the model.
	 * Fetches the page of partner entities we wish to render, using a page size of 2.
	 * This method does not much more but dispatch to the specific table creation methods.
	 * See the separate methods for more documentation.
	 */
	@RequestMapping(method = RequestMethod.GET)
	public String renderPageTablesWithPageSizeOf2(
			@PageableDefault(size = 2) Pageable pageable,
			Model model,
			ViewElementBuilderContext builderContext
	) {
		Page<Partner> partners = partnerRepository.findAll( pageable );

		model.addAttribute(
				"message",
				"These tables fully support the default sorting and paging coming with the EntityModule."
		);

		Map<String, ViewElement> generatedTables = new LinkedHashMap<>();
		generatedTables.put(
				"Table with checkbox and selected values",
				tableWithCheckbox( partners, builderContext )
		);
		generatedTables.put(
				"Table with specific sortable properties",
				tableWithSpecificSortableProperties( partners, builderContext )
		);

		model.addAttribute( "generatedTables", generatedTables );

		return "th/entityModuleTest/sortableTables";
	}

	/**
	 * [1]
	 * Generates a table with the surrounding panel where both the sort headers and the pager buttons work.
	 * Also adds a checkbox column to the table, with some selected values.
	 */
	private ViewElement tableWithCheckbox( Page<Partner> partners,
	                                       ViewElementBuilderContext builderContext ) {
		// selected partners that should be checked
		Collection<Partner> selectedPartners
				= Arrays.asList( partnerRepository.findOne( -1L ), partnerRepository.findOne( -3L ) );

		return builderHelper.createSortableTableBuilder( Partner.class )
		                    .items( partners )
		                    .properties( "*" )
		                    .hideResultNumber()
		                    .headerRowProcessor( ( builderCtx, row ) -> {
			                    TableViewElement.Cell cell = new TableViewElement.Cell();
			                    cell.setHeading( true );
			                    cell.addChild(
					                    // the checkbox should be unwrapped in order to render correctly
					                    bootstrapUiFactory.checkbox()
					                                      .unwrapped()
					                                      .htmlId( "select-all-partners" )
					                                      .build( builderCtx )
			                    );
			                    row.addFirstChild( cell );
		                    } )
		                    .valueRowProcessor( ( builderCtx, row ) -> {
			                    Partner partner = EntityViewElementUtils.currentEntity( builderCtx, Partner.class );

			                    TableViewElement.Cell cell = new TableViewElement.Cell();
			                    cell.addChild(
					                    bootstrapUiFactory.checkbox()
					                                      .unwrapped()
					                                      .controlName( "partners" )
					                                      .selected( selectedPartners.contains( partner ) )
					                                      .value( partner.getId() )
					                                      .build( builderCtx )
			                    );
			                    row.addFirstChild( cell );
		                    } )
		                    .build( builderContext );
	}

	/**
	 * [2]
	 * Generates the table where only one property can be sorted on.
	 * Note that this table follows the paging and sorting settings of the first table,
	 * but only the single column can be sorted on in the table itself.
	 */
	private ViewElement tableWithSpecificSortableProperties( Page<Partner> partners,
	                                                         ViewElementBuilderContext builderContext ) {
		return builderHelper.createSortableTableBuilder( Partner.class )
		                    .items( partners )
		                    .properties( "name", "url" )
		                    .sortableOn( "name" )           // only allow sorting on the name property
		                    .tableOnly()
		                    .build( builderContext );
	}
}
/*
 * Copyright 2014 the original author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.foreach.across.samples.entity.application.installers;

import com.foreach.across.core.annotations.Installer;
import com.foreach.across.core.annotations.InstallerMethod;
import com.foreach.across.core.installers.InstallerPhase;
import com.foreach.across.samples.entity.application.business.Partner;
import com.foreach.across.samples.entity.application.repositories.PartnerRepository;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Arrays;

/**
 * @author Arne Vandamme
 * @since 2.0.0
 */
@Installer(description = "Installs some test partners", phase = InstallerPhase.AfterModuleBootstrap)
public class TestPartnersInstaller
{
	private PartnerRepository partnerRepository;

	@Autowired
	public TestPartnersInstaller( PartnerRepository partnerRepository ) {
		this.partnerRepository = partnerRepository;
	}

	@InstallerMethod
	public void installPartners() {
		partnerRepository.save(
				Arrays.asList(
						partner( -1, "Google", "http://www.google.com" ),
						partner( -2, "Microsoft", "http://www.microsoft.com" ),
						partner( -3, "Facebook", "http://www.facebook.com" )
				)
		);
	}

	private Partner partner( long id, String name, String url ) {
		Partner partner = new Partner();
		partner.setNewEntityId( id );
		partner.setName( name );
		partner.setUrl( url );

		return partner;
	}
}
<!--
  ~ Copyright 2014 the original author or authors
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~ http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>SortableTable demo page</title>
</head>
<body th:fragment="content">

    <div class="alert alert-info" th:text="${message}">general message</div>

    <div th:each="generatedTable : ${generatedTables}">
        <h2 th:text="${generatedTable.key}">Table description</h2>
        <across:view element="${generatedTable.value}" />
    </div>

</body>
</html>

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.