Search code examples
javascriptjavahtmlspringthymeleaf

How to add html with thymeleaf and bind object or its fields to that html on button click?


This is simple java class that has a list of strings for example:

public class Apple {
    private List<String> listOfStrings;
    // getters setters
}

html with thymeleaf:

<form method="post" th:object="${apple}"> // apple is set through Model.addAttribute(.., ..) in Controller
    <input type="text" th:each="str, iter : *{listOfStrings} th:field="*{listOfStrings[__${iter.index}__]}">
    <button id="add" type="button" onclick="add()>Add</button>
    <button id="remove" type="button" onclick="remove()">Remove</button>
    <input type="submit" value="Save">
<form>

javascript add and remove new input:

const newInputTextString="<input type=\"text\" th:field="?">" // th:field won't be evaluated/bound to apples listOfStrings index

function add() {
    const xhttp = new XMLHttpRequest();
    xhttp.onload = function() {
        document.getElementById("add").insertAdjacentHTML("beforebegin", newInputTextString);
    }
    xhttp.open("GET", window.location.href);
    xhttp.send();
}

function remove() {
    // TODO
}

So far I am only adding html through javascript, but I can not include th: tags there since these tags wont be evaluated and bind to specific object or fields.

Expected functionality:

  • on click of remove button, last input of type text that is bound to specific index of listOfStrings is removed.
  • on click of add button, new input of type text is added and bound to next index of listOfStrings.

In other words I want to make user able to remove and add strings in listOfStrings of that apple object with html buttons.

Feel free to correct my English


Solution

  • th:field essentially just gives you an ID and a NAME attribute on an element. You cannot add a th:field through JS but you can emulate its behavior by manually adding the appropriate ID and NAME attributes. With that said you should make some slight changes in your HTML and JS code.

    You should always have a record of the current total number of strings so you can adjust your string index. For that I've created a div wrapper around all string inputs so we can access it in JS. Afterwards we just read the number of elements inside of it and create a new index out of it.

    You can look the implementation below:

    function createInput(index) {
      var input = document.createElement('input');
      input.setAttribute('type', 'text');
      input.setAttribute('id', `listOfStrings[${index}]`);
      input.setAttribute('name', `listOfStrings[${index}]`);
      input.setAttribute('placeholder', `listOfStrings[${index}]`);
      input.setAttribute('data-index', index);
      input.setAttribute('data-input', '');
      return input;
    }
    
    function addString() {
      var inputsWrapper = document.querySelector('[data-inputs]');
      var inputs = inputsWrapper.querySelectorAll('[data-input]');
      var newIndex = inputs.length;
      var newInput = createInput(newIndex);
      inputsWrapper.append(newInput);
    }
    <form method="post" th:object="${apple}">
      <div style="display: flex; flex-direction: column; max-width: 300px" data-inputs>
        <input 
           type="text"
           placeholder="listOfStrings[0]"
           th:placeholder="${'listOfStrings[__${iter.index}__]'}"
           th:each="str, iter : *{listOfStrings}"
           th:attr="data-index=${__${iter.index}__}" 
           th:field="*{listOfStrings[__${iter.index}__]}"
           data-input />
      </div>
      <button id="add" type="button" onclick="addString()">Add</button>
      <button id="remove" type="button" onclick="remove()">Remove</button>
      <input type="submit" value="Save">
    <form>