Table of Contents |
---|
Using the SearchPanel
When data is stored in a database we want to be able to search for records. Searching is so common that DomUI has a special component that helps with creating database search screens: the SearchPanel. The Search Panel works on database objects, i.e. entity classes that are defined in Hibernate or JPA, or even with plain JDBC accessed objects (using DomUI's generic database layer). This means that working with the panel you stay inside the Java world.
...
This makes it very easy to create your own defaults for SearchPanel controls: just create a factory, register it and make it return the correct score when you recognise a property you'd want to handle.
Example: creating your own component and builder
In this example we are going to make a screen to search inside the Tracks table.
We will use the EnumSetInput control to select zero to one Genre's from the database, then limit the search to those genres selected. The completed thing looks like this:
We will start with the page's code:
Code Block |
---|
@Override public void createContent() throws Exception {
ContentPanel cp = new ContentPanel();
add(cp);
SearchPanel<Track> lf = new SearchPanel<>(Track.class);
cp.add(lf);
lf.setClicked(a -> search(lf.getCriteria()));
//-- For Genre we will use a new control
EnumSetInput<Genre> genreC = new EnumSetInput<>(Genre.class);
List<Genre> genreList = getSharedContext().query(QCriteria.create(Genre.class));
genreC.setData(genreList);
Set<Genre> def = new HashSet<>();
def.add(genreList.get(0));
def.add(genreList.get(1));
lf.add().property("genre").defaultValue(def).control(new EnumSetQueryBuilder<>("genre"), genreC);
lf.add().property("name").control();
lf.add().property("album").control(); // Allow searching for a total
} |
We add the usual panel, then we prepare the EnumSetInput:
- We read all Genre records from the database in genreList
- We prepare a default (just for show) of the 1st two genres returned
- Then we add the control for the Genre to the SearchPanel.
The EnumSetInput returns a Set<Genre> for all of the selected items. This set of course is not understood by any of the existing query builders, so we made one ourself: the EnumSetQueryBuilder:
Code Block |
---|
public class EnumSetQueryBuilder<V> implements ILookupQueryBuilder<Set<V>> {
private final String m_propertyName;
public EnumSetQueryBuilder(String propertyName) {
m_propertyName = propertyName;
}
@Nonnull @Override public <T> LookupQueryBuilderResult appendCriteria(@Nonnull QCriteria<T> criteria, @Nullable Set<V> lookupValue) {
if(lookupValue == null || lookupValue.isEmpty())
return LookupQueryBuilderResult.EMPTY;
QRestrictor<T> or = criteria.or();
lookupValue.forEach(value -> or.eq(m_propertyName, value));
return LookupQueryBuilderResult.VALID;
}
} |
An instance of this class gets connected to the search item. It works as follows:
- The class gets instantiated with the property name to search for, which in this case will be "genre" inside the Track entity.
- When it is time to create the query the appendCriteria call gets called. It:
- Checks whether data was actually there. If not it returns the EMPTY indicator. This indicator is used when searching is only allowed with at least one clause filled in.
- For complex data you would also check the input here and throw a ValidationException if the data was wrong.
- We can now create the query: we add an or of all values in the set.
Generating the form
The search panel generates a form containing the labels and controls that are added. By default it generates a simple vertical form where each label/control pair are on a single line. How can we influence the form's layout? For that we need to know how the form gets generated.
The SearchPanel uses an ISearchFormBuilder implementation as the thing that actually builds the form:
Code Block |
---|
public interface ISearchFormBuilder {
/** Defines the target node for the form to be built. */
void setTarget(NodeContainer target) throws Exception;
void append(SearchControlLine<?> it) throws Exception;
void finish() throws Exception;
} |
All actions that create the form based UI are delegated to this interface. If you do nothing then SearchPanel uses the default implementation of this thing: DefaultSearchFormBuilder. This implementation is very basic:
Code Block |
---|
public class DefaultSearchFormBuilder implements ISearchFormBuilder {
@Nullable
private FormBuilder m_builder;
private Div m_target;
@Override public void setTarget(NodeContainer target) throws Exception {
Div root = m_target = new Div("ui-dfsb-panel");
target.add(root);
Div d = new Div("ui-dfsb-part");
root.add(d);
m_builder = new FormBuilder(d);
}
@Override public void append(SearchControlLine<?> it) throws Exception {
NodeContainer label = it.getLabel();
if(null != label)
fb().label(label);
IControl<?> control = it.getControl();
fb().control(control);
}
public void addBreak() {
NodeContainer target = requireNonNull(m_target);
Div d = new Div("ui-dfsb-part");
target.add(d);
m_builder = new FormBuilder(d);
}
@Override public void finish() throws Exception {
m_builder = null;
}
@Nonnull
public FormBuilder fb() {
return requireNonNull(m_builder);
}
} |
It just uses a normal FormBuilder to create the form, and it has an extra "method" to allow the form to be split into multiple columns: the "addBreak" method.
Controlling how the form looks can be done by creating your own implementation of ISearchFormBuilder. You can even make it the default layout by calling:
Code Block |
---|
SearchPanel.setDefaultSearchFormBuilder(Supplier<ISearchFormBuilder> factory) |
so that the supplier returns your factory.
To interact with your factory you can add special "actions" to the SearchForm's definition. For example, to use that "addBreak()" method we would code the following:
Code Block |
---|
SearchPanel sp = new SearchPanel(Invoice.class);
DefaultSearchFormBuilder bld = new DefaultSearchFormBuilder();
sp.setFormBuilder(bld); // Ensure that the DefaultFormBuilder is used
//-- add first two props
lf.add().property("type").control(typeC, new EnumSetQueryBuilder<>(Definition.pTYPE));
lf.add().property("type").control();
lf.add().action(() -> bld.addBreak());
lf.add().property("stage").control(); |
The action will be executed in the order of adding the controls.
Warning |
---|
You could of course also cast lf.getFormBuilder() to DefaultSearchFormBuilder instead of creating an instance yourself. This however will cause your code to break if the default ever changes. |