ioreskovic
ioreskovic

Reputation: 5709

Tapestry updating DAO from editable grid with AJAX

I'm trying to make the following work in Tapestry.

I have a Dictionary<String, Dictionary<String, Object>> which contains my data.

I'm trying to accomplish that I have 1 drop-down menu (Select component) which contains the keys from outer Dictionary.

When that selection changes, grid should be updated with the keys and values from the selected sub-dictionary.

For example:

Dictionary<String, Dictionary<String, Object>> dictionaries = new Hashtable<String, Dictionary<String, Object>>();

Dictionary<String, Object> dict1 = new Hashtable<String, Object>();
Dictionary<String, Object> dict2 = new Hashtable<String, Object>();

dict1.put("k1", "d1v1");
dict1.put("k2", "d1v2");

dict2.put("k1", "d2v1");
dict2.put("k2", "d2v2");

dictionaries.put("d1", dict1);
dictionaries.put("d2", dict2);

Would anyone would be so kind to give me an example or push me in the right direction?

EDIT:

I managed to get the first part working, with grid contents changing depending on which keys from outer dictionary are selected.

However, I I'm having problems with updating the grid and saving the changes:

I am using my own GridDataSource:

public class EntityDataSource<T> implements GridDataSource {

    private Class<T> persistentClass;

    protected List<T> data;
    protected int count = -1;
    protected int startIndex = 0;


    public EntityDataSource(Class<T> persistentClass, List<T> data) {
        this.persistentClass = persistentClass;
        this.data = data;
    }

    public static final <T> EntityDataSource<T> create(
            Class<T> persistentClass, List<T> data) {
        return new EntityDataSource<T>(persistentClass, data);
    }

    public int getAvailableRows() {
        return this.data.size();
    }

    public void prepare(int startIndex, int endIndex,
            List<SortConstraint> sortConstraints) {
        this.startIndex = startIndex;
        this.data = this.data.subList(startIndex, endIndex + 1);
    }

    public Object getRowValue(int index) {
        return this.data.get(index - this.startIndex);
    }

    @SuppressWarnings("rawtypes")
    public Class getRowType() {
        return this.persistentClass;
    }

}

My grid looks like this:

<t:form t:id="configSelectForm">
    <t:select t:id="storageKeySelecter"
          t:model="storageKeyModel"
          t:value="storageKey"
          zone="configZone" />
</t:form>

<t:zone t:id="configZone" id="configZone">
    <t:form t:id="configReviewForm">
        <table width="100%">
            <t:grid t:source="configurationEntrySource"
                    t:add="update, delete"
                    t:row="configurationEntry"
                    t:mixins="DisableGridSorting"
                    t:include="configKey, configValue"
                    t:encoder="configurationEntryEncoder">
                <p:configValueCell>
                    <input t:id="value" t:type="TextField" t:value="configurationEntry.configValue"
                           t:validate="required" t:context="configurationEntry.configValue" />
                </p:configValueCell>

                <p:updateCell>
                    <t:actionlink t:id="update" zone="configZone" context="[configurationEntry.configKey, configurationEntry.configValue]">Update</t:actionlink>
                </p:updateCell>

                <p:deleteCell>
                    <t:actionlink t:id="delete" zone="configZone" context="configurationEntry.configKey">Delete</t:actionlink>
                </p:deleteCell>

            </t:grid>
        </table>
        <br></br>
        <!-- <input type="submit" value="Update" class="button" /> -->
    </t:form>
</t:zone>

I have managed to make a delete link work, however, I cannot seem to update the value for the arbitrary key.

When I click the link to update, the value from the text field is not the one that is being passed:

public Object onActionFromUpdate(String configKey, String configValue) {

    // my stuff here

    return request.isXHR() ? configZone.getBody() : null;
}

The configValue is the value of the entry that currently exist and not the one I'm trying to change it to.

Is there a way to get that value? My grid has an an arbitrary number of rows.

EDIT2: Even more info provided

This is my TML:

<t:form t:id="configSelectForm">
    <t:select t:id="storageKeySelecter"
          t:model="storageKeyModel"
          t:value="storageKey"
          zone="configZone" />
</t:form>

<br/>

<t:zone t:id="configZone" id="configZone" elementName="div">
    <form t:type="form" t:id="configReviewForm" id="configReviewForm" t:zone="configZone" zone="configZone">
        <table width="100%">
            <t:grid t:source="configurationEntries"
                    t:add="update, delete"
                    t:row="configurationEntry"
                    t:mixins="DisableGridSorting"
                    t:include="configKey, configValue"
                    t:encoder="configurationEntryEncoder" >

                <p:configValueCell>
                    <input t:id="configValueTextField" t:type="TextField" t:value="configurationEntry.configValue"
                           t:validate="required" />
                </p:configValueCell>

                <p:updateCell>
                    <t:actionlink id="update" t:id="update" zone="configZone" t:context="[configurationEntry.configKey, configurationEntry.configValue]">Update</t:actionlink>
                </p:updateCell>

                <p:deleteCell>
                    <t:actionlink id="delete" t:id="delete" zone="configZone" t:context="configurationEntry.configKey">Delete</t:actionlink>
                </p:deleteCell>

            </t:grid>
        </table>
        <br/>
        <input type="submit" value="Update" class="button" zone="configZone"/>
    </form>
</t:zone>

<br/>
<br/>

<t:form t:id="addNewEntryForm">
    <table width="100%">
        <tr>
            <td>
                <input t:id="newEntryKey" t:type="textfield" t:value="newEntry.configKey" 
                       t:validate="required" t:context="newEntry.configKey" />
            </td>

            <td>
                <input t:id="newEntryValue" t:type="textfield" t:value="newEntry.configValue" 
                       t:validate="required" t:context="newEntry.configValue" />
            </td>

            <td>
                <input type="submit" value="Add New Entry" zone="configZone"/>
            </td>
        </tr>
    </table>
</t:form>

This is my JAVA:

public class PropertyConfiguration {

    @Inject
    private BeanModelSource beanModelSource;

    @Component
    private Form configReviewForm;

    @Property
    private List<ConfigurationEntry> configurationEntries;

    private List<ConfigurationEntry> persistentEntries;

    @Property
    private ConfigurationEntry configurationEntry = new ConfigurationEntry("", "");

    @Property
    private ConfigurationEntryEncoder configurationEntryEncoder;

    @InjectComponent
    private Zone configZone;

    @InjectComponent
    private TextField configValueTextField;

    @Inject
    private ConfigurationPersitanceDAO dao;

    private GridDataSource dataSource;

    @Inject
    private Messages messages;

    @Property
    private ConfigurationEntry newEntry;

    @Inject
    private Request request;

    @Property
    @Validate("required")
    @Persist(PersistenceConstants.SESSION)
    private String storageKey;

    private StringSelectModel storageKeysSelectModel;

    public ValueEncoder<ConfigurationEntry> getConfigurationEntryEncoder() {
        initConfigurationEntityEncoder();

        return this.configurationEntryEncoder;
    }

    public BeanModel<ConfigurationEntry> getModel() {
        return beanModelSource.createDisplayModel(ConfigurationEntry.class, messages);
    }

    public SelectModel getStorageKeyModel() {
        if (storageKeysSelectModel == null) {
            storageKeysSelectModel = new StringSelectModel(this.dao.getStorageKeys());
        }

        return storageKeysSelectModel;
    }

    private void initConfigurationEntityEncoder() {
        if (this.configurationEntryEncoder == null) {
            this.configurationEntryEncoder = new ConfigurationEntryEncoder(dao, storageKey);
        }
    }

    private void initZoneData() {
        if (this.storageKey == null) {
            this.storageKey = this.dao.getStorageKeys().get(0);
        }
        initConfigurationEntityEncoder();   
    }

    public Object onActionFromDelete(String configKey) {
        System.out.println("Deleting from: " + storageKey + " entry: " + configKey);
        this.dao.deleteConfigurationEntry(storageKey, configKey);

        return request.isXHR() ? configZone.getBody() : null;
    }

    public Object onActionFromUpdate(String configKey, String configValue) {
        this.dao.updateConfigurationEntry(storageKey, configKey, configValue);

        return request.isXHR() ? configZone.getBody() : null;
    }

    void onActivate(String storageKey) {
        initZoneData();
        this.newEntry = new ConfigurationEntry("", "");
    }

    String onPassivate() {
        this.newEntry = new ConfigurationEntry("", "");
        return this.storageKey;
    }

    Object onRefresh() {
        return request.isXHR() ? configZone.getBody() : null;
    }

    Object onSuccessFromAddNewEntryForm() {
        String configKey = this.newEntry.getConfigKey();
        String configValue = this.newEntry.getConfigValue();

        this.dao.addConfigurationEntry(storageKey, configKey, configValue);

        return request.isXHR() ? configZone.getBody() : null;
    }

    void onValidateFromAddNewEntryForm() {
        return;
    }

    Object onValueChangedFromStorageKeySelecter(String storageKey) {
        this.storageKey = storageKey;
        initConfigurationEntityEncoder();
        this.configurationEntries = wrap(this.dao.getConfiguration(storageKey));
        return configZone.getBody();
    }

    private void updateChangedConfigurations(List<ConfigurationEntry> changedEntries) {
        for (ConfigurationEntry changedEntry : changedEntries) {
            String configKey = changedEntry.getConfigKey();
            String configValue = changedEntry.getConfigValue();

            System.out.println("Updated: [" + storageKey + ":" + configKey + ":" + configValue + "]");

            this.dao.updateConfigurationEntry(storageKey, configKey, configValue);
        }
    }

    void onValidateFromConfigReviewForm() {
        this.persistentEntries = wrap(this.dao.getConfiguration(storageKey));
        List<ConfigurationEntry> tmpList = new ArrayList<ConfigurationEntry>();

        for (ConfigurationEntry newEntry : this.configurationEntries) {
            for (ConfigurationEntry oldEntry : this.persistentEntries) {
                System.out.println("NewEntry: " + newEntry.toString() + " vs. OldEntry: " + oldEntry.toString());
                if (oldEntry.getConfigKey().equals(newEntry.getConfigKey())) {
                    if (!oldEntry.getConfigValue().equals(newEntry.getConfigValue())) {
                        newEntry.setConfigValue(newEntry.getConfigValue().trim());
                        tmpList.add(newEntry);
                    }
                }
            }
        }

        this.persistentEntries = tmpList;
    }

    Object onSuccessFromConfigReviewForm() {
        updateChangedConfigurations(this.persistentEntries);

        return request.isXHR() ? configZone.getBody() : null;
    }

    /**
    *   Wraps dictionary entries in instances of ConfigurationEntry
    */
    private List<ConfigurationEntry> wrap(
        Dictionary<String, Object> rawConfiguration) {
        Set<String> keySet = new TreeSet<String>();
        List<ConfigurationEntry> wrappedEntries = new ArrayList<ConfigurationEntry>();

        Enumeration<String> keys = rawConfiguration.keys();

        while (keys.hasMoreElements()) {
            String key = keys.nextElement();
            keySet.add(key);
        }

        for (String key : keySet) {
            String value = (String) rawConfiguration.get(key);

            ConfigurationEntry entry = new ConfigurationEntry(key, value);

            wrappedEntries.add(entry);
        }

        return wrappedEntries;
    }
}

For some reason, when the "Update" action link is clicked the values passed to the public Object onActionFromUpdate(String configKey, String configValue) aren't the ones I just wrote into the designated textfield, but the ones that were fetched from my database when page was rendered.

For example, if I had these pairs initially:

key1 => value1
key2 => value2
key3 => value3

and I wanted to change value for key2 to "newValue2" the parameters passed to the method are "key2" and "value2" instead of "key2" and "newValue2".

The same problem is the reason the mass-update doesn't work. All the values in configurationEntries are shapshot values from the database instead of the values currently written in my grid.

So, my question would be how I can update my DB from editable grid using AJAX for my concrete example. I've tried many things suggested on SO and rest of internet, but they don't seem to work and I don't know why.

Upvotes: 1

Views: 1918

Answers (2)

sody
sody

Reputation: 3791

To have actual values from inputs you need to submit form. As you use ActionLink the values from its context are used (this values was saved in context during link rendering and can't be changed on client side without hacking).

To have your example working you can use separate form for each row. You will also need some submit component that can work outside the form:

<t:zone t:id="configZone" id="configZone">
  <table width="100%">
    <t:grid t:source="configurationEntrySource"
            t:add="update, delete"
            t:row="configurationEntry"
            t:mixins="DisableGridSorting"
            t:include="configKey, configValue"
            t:encoder="configurationEntryEncoder">
      <p:configValueCell>
        <t:form t:id="configReviewForm" zone="configZone">
          <input t:id="value" t:type="TextField" t:value="configurationEntry.configValue"
                 t:validate="required" t:context="configurationEntry.configValue" />
        </t:form>
      </p:configValueCell>

      <p:updateCell>
        <t:customlinksubmit form="configReviewForm">Update</t:customlinksubmit>
      </p:updateCell>

      <p:deleteCell>
        <t:actionlink t:id="delete" zone="configZone" context="configurationEntry.configKey">Delete</t:actionlink>
      </p:deleteCell>

    </t:grid>
  </table>
  <br/>
</t:zone>

CustomLinkSubmit:

@SupportsInformalParameters
public class CustomLinkSubmit implements ClientElement {

    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.COMPONENT)
    private Form form;

    @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    private SubmitMode mode = SubmitMode.NORMAL;

    @Inject
    private ComponentResources resources;

    @Inject
    private JavaScriptSupport javascriptSupport;

    private String clientId;

    @BeginRender
    void beginRender(MarkupWriter writer) {
        clientId = javascriptSupport.allocateClientId(resources);
        writer.element("span", "id", clientId);
        resources.renderInformalParameters(writer);
    }

    @AfterRender
    void afterRender(MarkupWriter writer) {
        writer.end();

        JSONObject spec = new JSONObject()
                .put("form", form.getClientId())
                .put("clientId", clientId)
                .put("validate", mode == SubmitMode.NORMAL);

        javascriptSupport.addInitializerCall(InitializationPriority.EARLY, "linkSubmit", spec);
    }

    public String getClientId() {
        return clientId;
    }
}

Upvotes: 2

lance-java
lance-java

Reputation: 28016

You need to use the zone parameter.

Take a look at the SelectZoneDemo.tml and SelectZoneDemo.java from the select component's javadoc. It gives an example of updating a zone when a select changes.

For more complex interactions, you might be interested in this

Upvotes: 3

Related Questions