Skip to content

Latest commit

 

History

History
167 lines (113 loc) · 7.2 KB

03.md

File metadata and controls

167 lines (113 loc) · 7.2 KB
title image
Components/Mixins
../images/components.jpg

Components are much like pages in that they combine templates and a Java file, but have parameters instead of an activation context. Components may implement a specific feature or combine various other components into one integrated component. For example, the Grid component is made up of various subcomponents such as pagers, rows and cells. The GridPager provides navigation, GridRows handles each row and GridCell handles each cell. The Grid component organizes all the subcomponents so they can work together.

Mixins provide a way to augment a component without extending it. Some mixins such as Autocomplete are tightly coupled to a component. Others such as DiscardBody can be applied to almost any component. Mixins function much like a component but have no template and cannot be used by themselves. They are useful for adding client-side Javascript or server-side business logic. Mixins can be applied to components in various ways, either up front in the template or behind the scenes. Generally, use components when markup is needed or to combine existing components. Use mixins to alter/extend the functionality of existing components.

The booking app needs the ability to display a list of pending bookings and a list of completed bookings. This could be done by just putting a Grid on a page and running the query, but pages are no place for business logic because it cannot be reused. Creating components is simple enough, consolidating the business logic into one place and allowing various pages to display the same kind of data.

The Booking component combines the grid and output components to create a list of bookings with a header. The inputs are a user, title and some of the grid inputs. The tml file just contains the component markup; the parameters will be added in the Java file. The grid accepts arbitrary parameters to allow customization of the grid.

Tapestry calls these informal parameters so this component will also need to accept informal parameters. The @Component annotation tells Tapestry to inject the component object into the page and allows passing parameters to it. In this case, all the informal parameters are passed to the grid and the row and rowsPerPage parameters are exposed to the booking component. Tapestry will match the name of the field to the component in the tml, or the id can be specified to avoid name conflicts.

The output component accepts an object and some kind of formatter. In this case it will be a Message formatter, so the value needs to be an Object[].

The new bookings component acts very much like a grid but hides how the objects are fetched.

Bookings.java

@SupportsInformalParameters
public class Bookings {

	@Inject
	@Property
	UserService userService;

	@Parameter("userService.currentUser")
	User user;

	@Parameter(name="title",defaultPrefix="literal")
	String titleString;

	@Property
	List<Booking> bookings;

	@Component(parameters={"value=prop:value","format=prop:format"})
	Output title;

	@Component(parameters={"source=prop:bookings"},publishParameters="rowsPerPage,add,row",id="bookings",inheritInformalParameters=true)
	Grid bookingGrid;

	@Inject
	DAO dao;

	@SetupRender
	void setupRender() {
		bookings = dao.query(Booking.class, " from Booking where user = :user", "user",user);
	}

	public Format getFormat() {
		return new MessageFormat("{0}");
	}

	public Object[] getValue() {
		Object[] values = new Object[1];
		values[0] = titleString;
		return values;
	}
}

Booking.tml

<t:container
  xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd"
  xmlns:p="tapestry:parameter">

  <h2><t:output t:id="title"/></h2>
  <t:grid t:id="bookings" />

</t:container>

Next would be a component to show in progress bookings. This one will be exactly the same except for the query to populate the list. It would be nice to reuse the shell and the obvious way to do that would be to create an abstract class with an abstract getList method. This works OK until the query needs different arguments. Mixins provide a better way to separate the UI from the business logic. Instead of creating an abstract class create a Listing interface with a setGridDateSource method and a Listing component that implements it create a mixin called PendingBookingsByUser which takes User as a parameter. The component will handle the UI and the mixin will handle the query. The mixin goes perfectly with the simple DAO and provides checked arguments instead of the name/value pair of the DAO. The mixin can be applied to any component that implements the Listing interface.

PendingBookingsByUser.java

package com.trsvax.hotelbooking.mixins;

public class PendingBookingsByUser {

	@Parameter
	User user;

	@Inject
	Listing listing;

	@Inject
	DAO dao;

	@SetupRender
	void setupRender() {
		listing.setListing(dao.query(Booking.class, "from Booking where user = :user", "user",user));
	}

}

Listing.java

package com.trsvax.hotelbooking.components;

@SupportsInformalParameters
public class Listing implements com.trsvax.hotelbooking.Listing {

	@Parameter(name="title",defaultPrefix="literal")
	String titleString;

	@Property
	Object listings;

	@Component(parameters={"value=prop:value","format=prop:format"})
	Output title;

	@Component(parameters={"source=prop:bookings"},publishParameters="rowsPerPage,add,row",id="bookings",inheritInformalParameters=true)
	Grid bookingGrid;


	public Format getFormat() {
		return new MessageFormat("{0}");
	}

	public Object[] getValue() {
		Object[] values = new Object[1];
		values[0] = titleString;
		return values;
	}

	@Override
	public void setListing(Object listings) {
		this.listings = listings;
	}

}

With Tapestry it's often best to write the code you want and then make it work. In this case it's nice to just inject the Listing rather than finding it. To make that work just create a service that implements listing, finds the container and sets the object. This simplifies the code and makes it easier to test. The mixin is not concerned where the Listing component comes from or even if it is a listing component. Injection just provides an interface with setListing.

ListingImpl.java

package com.trsvax.hotelbooking.services;


public class ListingImpl implements Listing {

	private final ComponentResources componentResources;

	public ListingImpl(ComponentResources componentResources) {
		this.componentResources = componentResources;
	}

	@Override
	public void setListing(Object listings) {
		((Listing)componentResources.getContainer()).setListing(listings);
	}

}

Now it's possible to create different kinds of Listing components and add specific mixins to retrieve the data.