package org.activityinfo.ui.client.component.form;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.Widget;
import org.activityinfo.core.client.ResourceLocator;
import org.activityinfo.i18n.shared.I18N;
import org.activityinfo.model.form.*;
import org.activityinfo.model.resource.Resource;
import org.activityinfo.model.resource.ResourceId;
import org.activityinfo.model.type.FieldValue;
import org.activityinfo.promise.Promise;
import org.activityinfo.ui.client.component.form.event.FieldMessageEvent;
import org.activityinfo.ui.client.component.form.field.FormFieldWidget;
import org.activityinfo.ui.client.component.form.field.FormFieldWidgetFactory;
import org.activityinfo.ui.client.widget.DisplayWidget;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Displays a simple view of the form, where users can edit instances
*/
public class SimpleFormPanel implements DisplayWidget<FormInstance> {
private final FieldContainerFactory containerFactory;
private final FormFieldWidgetFactory widgetFactory;
private final FlowPanel panel;
private final ScrollPanel scrollPanel;
private final boolean withScroll;
private final EventBus eventBus = new SimpleEventBus();
private final Map<ResourceId, FieldContainer> containers = Maps.newHashMap();
/**
* The original, unmodified instance
*/
private Resource instance;
/**
* A new version of the instance, being updated by the user
*/
private FormInstance workingInstance;
private FormClass formClass;
private ResourceLocator locator;
private RelevanceHandler relevanceHandler;
// validation form class is used to refer to "top-level" form class.
// For example "Properties panel" renders current type-formClass but in order to validate expression we need
// reference to formClass that is currently editing on FormDesigner.
// it can be null.
private FormClass validationFormClass = null;
public SimpleFormPanel(ResourceLocator locator, FieldContainerFactory containerFactory,
FormFieldWidgetFactory widgetFactory) {
this(locator, containerFactory, widgetFactory, true);
}
public SimpleFormPanel(ResourceLocator locator, FieldContainerFactory containerFactory,
FormFieldWidgetFactory widgetFactory, boolean withScroll) {
FormPanelStyles.INSTANCE.ensureInjected();
this.locator = locator;
this.containerFactory = containerFactory;
this.widgetFactory = widgetFactory;
this.withScroll = withScroll;
this.relevanceHandler = new RelevanceHandler(this);
panel = new FlowPanel();
panel.setStyleName(FormPanelStyles.INSTANCE.formPanel());
scrollPanel = new ScrollPanel(panel);
bindEvents();
}
private void bindEvents() {
eventBus.addHandler(FieldMessageEvent.TYPE, new FieldMessageEvent.Handler() {
@Override
public void handle(FieldMessageEvent event) {
showFieldMessage(event);
}
});
}
private void showFieldMessage(FieldMessageEvent event) {
FieldContainer container = containers.get(event.getFieldId());
if (event.isClearMessage()) {
container.setValid();
} else {
container.setInvalid(event.getMessage());
}
}
public FormInstance getInstance() {
return workingInstance;
}
@Override
public Promise<Void> show(final FormInstance instance) {
return show(instance.asResource());
}
public Promise<Void> show(final Resource instance) {
this.instance = instance;
return locator.getFormClass(instance.getResourceId("classId")).join(new Function<FormClass, Promise<Void>>() {
@Nullable
@Override
public Promise<Void> apply(@Nullable FormClass formClass) {
return buildForm(formClass);
}
}).join(new Function<Void, Promise<Void>>() {
@Nullable
@Override
public Promise<Void> apply(@Nullable Void input) {
return setValue(instance);
}
});
}
private Promise<Void> buildForm(final FormClass formClass) {
this.formClass = formClass;
this.relevanceHandler.formClassChanged();
try {
return createWidgets().then(new Function<Void, Void>() {
@Nullable
@Override
public Void apply(@Nullable Void input) {
addFormElements(formClass, 0);
return null;
}
});
} catch (Throwable caught) {
return Promise.rejected(caught);
}
}
private Promise<Void> createWidgets() {
final String resourceId = instance.getId().asString();
return Promise.forEach(formClass.getFields(), new Function<FormField, Promise<Void>>() {
@Override
public Promise<Void> apply(final FormField field) {
if (!field.isVisible()) {
return Promise.resolved(null); // we have join inside forEach, must return promise
} else {
return widgetFactory.createWidget(resourceId, formClass, field, new ValueUpdater<FieldValue>() {
@Override
public void update(FieldValue value) {
onFieldUpdated(field, value);
}
}, validationFormClass, eventBus).then(new Function<FormFieldWidget, Void>() {
@Override
public Void apply(@Nullable FormFieldWidget widget) {
containers.put(field.getId(), containerFactory.createContainer(field, widget, 4));
return null;
}
});
}
}
});
}
public Promise<Void> setValue(Resource instance) {
this.instance = instance;
this.workingInstance = FormInstance.fromResource(instance);
List<Promise<Void>> tasks = Lists.newArrayList();
for (FieldContainer container : containers.values()) {
FormField field = container.getField();
FieldValue value = workingInstance.get(field.getId(), field.getType());
if(value != null && value.getTypeClass() == field.getType().getTypeClass()) {
tasks.add(container.getFieldWidget().setValue(value));
} else {
container.getFieldWidget().clearValue();
}
container.setValid();
}
return Promise.waitAll(tasks).then(new Function<Void, Void>() {
@Nullable
@Override
public Void apply(@Nullable Void input) {
relevanceHandler.onValueChange(); // invoke relevance handler once values are set
return null;
}
});
}
private void addFormElements(FormElementContainer container, int depth) {
for (FormElement element : container.getElements()) {
if (element instanceof FormSection) {
panel.add(createHeader(depth, ((FormSection) element)));
addFormElements((FormElementContainer) element, depth + 1);
} else if (element instanceof FormField) {
FormField formField = (FormField) element;
if (formField.isVisible()) {
panel.add(containers.get(formField.getId()));
}
}
}
}
public void onFieldUpdated(FormField field, FieldValue newValue) {
if (!Objects.equals(workingInstance.get(field.getId()), newValue)) {
workingInstance.set(field.getId(), newValue);
relevanceHandler.onValueChange(); // skip handler must be applied after workingInstance is updated
}
validateField(containers.get(field.getId()));
}
private boolean validateField(FieldContainer container) {
FormField field = container.getField();
FieldValue value = getCurrentValue(field);
if (value != null && value.getTypeClass() != field.getType().getTypeClass()) {
value = null;
}
if (field.isRequired() && value == null && field.isVisible()) { // if field is not visible user doesn't have chance to fix it
container.setInvalid(I18N.CONSTANTS.requiredFieldMessage());
return false;
} else {
container.setValid();
return true;
}
}
public boolean validate() {
boolean valid = true;
for (FieldContainer container : this.containers.values()) {
if (!validateField(container)) {
valid = false;
}
}
return valid;
}
private FieldValue getCurrentValue(FormField field) {
return workingInstance.get(field.getId());
}
private Widget createHeader(int depth, FormSection section) {
StringBuilder html = new StringBuilder();
String hn = "h" + (3 + depth);
html.append("<").append(hn).append(">")
.append(SafeHtmlUtils.htmlEscape(section.getLabel()))
.append("</").append(hn).append(">");
return new HTML(html.toString());
}
@Override
public Widget asWidget() {
return withScroll ? scrollPanel : panel;
}
public FormClass getFormClass() {
return formClass;
}
public FieldContainer getFieldContainer(ResourceId fieldId) {
return containers.get(fieldId);
}
public ResourceLocator getLocator() {
return locator;
}
public Map<ResourceId, FieldContainer> getContainers() {
return containers;
}
public void setValidationFormClass(FormClass validationFormClass) {
this.validationFormClass = validationFormClass;
}
public FormClass getValidationFormClass() {
return validationFormClass;
}
}