Zaawansowane formularze XForms w projekcie GWT

Nieodłączną częścią każdej aplikacji czy to strony internetowej są formularze. Często w ich skład wchodzi kilka zwykłych pól tekstowych wraz z przyciskiem wysyłającym formularz, ale zdarzają się też bardziej rozbudowane twory. Sam HTML udostępnia większość popularnych typów pól formularzy, jednak obsługę zdarzeń i akcji musimy pisać sami. Ogromnym ułatwieniem są tu możliwości GWT, dostarczające nam takie same kontrolki jak w HTML’u, do których w łatwy sposób można podpinać handlery i obsługiwać wszelkie zdarzenia. Jeśli to za mało, z pomocą przychodzą biblioteki typu GXT czy SmartGWT dokładające olbrzymie ilości własnych elementów formularzy. Niewątpliwą zaletą GWT i bibliotek pomocniczych są szerokie możliwości wczytywania danych do formularzy oraz poszczególnych ich pól (np. listy rozwijanej): dane możemy pobierać poprzez RPC, ładować z wystawionego przez serwlet XML’a lub JSON’a, bądź w jakikolwiek inny sposób możliwy do oprogramowania w GWT. Wydawałoby się, że więcej już nie trzeba!

W praktyce okazuje się, że formularze pisane w GWT mają jedną zasadniczą wadę – nie są tworzone dynamicznie. Z góry określamy, jakie pola znajdą się na stronie i jakie dane zostaną do nich załadowane. Możemy generować dynamicznie kod czystego HTML’a, tracimy tu jednak zalety łatwej w oprogramowaniu obsługi zdarzeń. Możemy też napisać kod, który będzie tworzył dynamicznie formularz w GWT, jednak sami musimy stworzyć potrzebne struktury danych oraz niezbędne parsery i generatory. Jeśli skończy się na formularzach złożonych z pól tekstowych, prostych list wyboru, checkbox’ów, przycisków radio, czy innych podstawowych elementów, to jesteśmy w domu. Gorzej, jeśli dynamicznie chcemy też generować układy elementów formularza (elementy w poziomie lub w pionie, zakładki, kolejne strony formularza). Dokładamy jeszcze do tego obsługę zdarzeń na wszystkich elementach i tworzenie własnych akcji i już mamy robotę na kilka długich tygodni :)

Człowiek leniwy poszuka pewnie gotowych rozwiązań. I słusznie, bo po co tworzyć coś, kto zrobił już kto inny. Szybko okazuje się jednak, że tych rozwiązań nie ma zbyt wiele. Kiedyś napotkałem nawet na pewną bibliotekę w GWT, która z założenia miała robić wszystkie opisane wyżej rzeczy, implementacja była jednak daleka od doskonałości. Rozszerzając poszukiwanie na ogólne standardy opisu formularzy w aplikacjach internetowych trafiłem na standard XForms.

Nie będę się tu długo rozwodził na temat, czym są XForms’y, bo opisów samego standardu można znaleźć wiele (np. Wikipedia). Krótko powiem, że jest to rekomendacja W3C odnośnie budowania nowoczesnych formularzy na stronach webowych. Podczas definiowania standardu starano się wykorzystać jak najwięcej innych gotowych standardów i tak np. reguły walidacji oraz typy danych można określać za pomocą XSD Schema, akcje podpinać do zdarzeń zdefiniowanych przez XMLEvents, a dane z modelu wyciągać za pomocą XPath. Same zaś XForms’y powinny być zagnieżdżone w innym języku, np. HTML’u czy SVG. Ostatnia wersja standardu (1.1) pojawiła się w październiku 2009-go roku, można powiedzieć, że jest jeszcze świeża. Tu pojawia się problem, gdyż przeglądarki internetowe nie implementują jeszcze jej natywnie, nie zapowiada się też, że szybko to się stanie. W ramach powoli powstającego HTML5 pojawić się ma okrojona z wielu możliwości XForms’ów obsługa formularzy pod nazwą WebForms, która i tak będzie czymś o wiele większym, niż zwykłe formularze HTML’owe. Być może zostanie zachowana też pełna kompatybilność z XForms’ami, tak aby co prostsze formularze bez problemy były wyświetlane jako formularze WebForms.

Na dzień dzisiejszy jednak ani WebForms, ani XForms nie są jeszcze zaimplementowane w przeglądarkach, dlatego należy poszukać innych rozwiązań, które przetłumaczą XForms’y na format zrozumiały dla przeglądarki. Jest ich kilka, część z nich działa po stronie serwera (np. betterFORM, Orbeon), a część przetwarza formularze już w przeglądarce. Te drugie mogą występować jako wtyczki do przeglądarki (np. formsPlayer), aplikacje Flash, bądź opierają się na przetwarzaniu w JavaScripcie. Wśród tych ostatnich wartą uwagi jest biblioteka XSLTForms generująca kod HTML i JavaScript za pomocą pliku XSLT. Perełką okazała się natomiast biblioteka EMC Formula XForms Engine, która została napisana i przygotowana do pracy w środowisku GWT!

Domyślasz się pewnie już, co dziś będziemy wykorzystywać. W prostym projekcie chcę Ci pokazać, jak osadzić formularz XForms w aplikacji GWT, jak uzyskiwać informacje o zdarzeniach w formularzu i w jaki sposób pobierać z niego dane. Do tego celu wykorzystamy bibliotekę EMC Formula. Dane o formularzu będą pobierane przez serwlet, łatwo można więc sobie wyobrazić generowanie ich w locie. Pokażę też, że przy zatwierdzaniu formularza można uzyskać informacje o przesyłanych na serwer plikach. Wygląd formularza nieco dopracujemy przy pomocy kilku grafik i stylów CSS. No to do dzieła!

Jeśli przeglądałeś już inne projekty przygotowane przeze mnie, struktura tego będzie bardzo podobna.

Sample.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.4//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.6.4/distro-source/core/src/gwt-module.dtd">
<module rename-to="sample">

  <!-- Inherit the core Web Toolkit stuff.                  -->
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="com.google.gwt.logging.Logging"/>

  <!-- Inherits XForms library                              -->
  <inherits name="com.emc.documentum.xml.xforms.gwt.XForms"/>

  <set-property name="gwt.logging.popupHandler" value="DISABLED"/>

  <set-property name="user.agent" value="ie8,gecko1_8,safari,opera" />

  <!-- Specify the app entry point class.                   -->
  <entry-point class="pl.avd.samples.gwt.xforms.client.SampleModule"/>

</module>

W linii 10-tej został wczytany moduł XForms z dołączonej do projektu biblioteki.

FormsServlet.java
public class FormsServlet extends HttpServlet {

  private Logger LOG = Logger.getLogger(getClass().getName());

  private ServletFileUpload upload;

  public FormsServlet() {
    upload = new ServletFileUpload();
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    resp.setContentType("text/xml");
    resp.setCharacterEncoding("UTF-8");

    InputStream is = getServletContext().getResourceAsStream("/form.xhtml");

    StringWriter writer = new StringWriter();
    IOUtils.copy(is, writer, "UTF-8");

    PrintWriter out = resp.getWriter();
    out.append(writer.toString());
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    LOG.info("form submission");
    LOG.info("has attributes: " + req.getAttributeNames().hasMoreElements());
    LOG.info("has parameters: " + req.getParameterNames().hasMoreElements());
    Set<Entry> set = req.getParameterMap().entrySet();
    for (Entry entry : set) {
      LOG.info(" - param name: " + entry.getKey() + " value: " + entry.getValue());
    }

    if (ServletFileUpload.isMultipartContent(req)) {
      LOG.info("multipart content detected");

      try {
        // Parse the request
        FileItemIterator iter = upload.getItemIterator(req);
        doFileUpload(iter);
      } catch (FileUploadException e) {
              LOG.log(Level.SEVERE, "exception while uploading file", e);
      }
    }
  }

  protected void doFileUpload(FileItemIterator iter) throws UnsupportedEncodingException, FileUploadException, IOException {
    while (iter.hasNext()) {
      FileItemStream item = iter.next();
      InputStream stream = item.openStream();

      String name = item.getFieldName().trim();

      if (name == null || name.length() == 0) {
        /*
         * Pole nie ma nazwy, jest więc omijane.
         */
        LOG.info("skipping field with no name");
        continue;
      }

      if (item.isFormField()) {
        /*
         * Pole jest zwykłym polem formularza, jest więc omijane.
         */
        LOG.info("Form field " + name + " with value " + Streams.asString(stream) + " detected.");
          continue;
      }

      String value = new String(item.getName().getBytes(), "UTF-8");

      if (value == null || value.length() == 0) {
        /*
         * Pole nie ma wartości, jest więc omijane.
         */
        LOG.info("skipping field with no value");
        continue;
      }

      LOG.info("File field " + name + " with file name " + value + " detected.");
    }
  }
}

W metodzie doGet odczytywana jest zawartość pliku form.xhtml, która następnie wysyłana jest jako treść odpowiedzi. Metoda doPost odpowiada za obsługę zatwierdzenia formularza i przechwycenia wysyłanych na serwer plików. Dla ułatwienia niektórych operacji w serwlecie wykorzystano biblioteki pomocnicze commons-io i commons-fileupload.

SampleModule.java
public class SampleModule implements EntryPoint {

  private Logger LOG = Logger.getLogger("SAMPLE");

  public void onModuleLoad() {
    GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() {

      public void onUncaughtException(Throwable caught) {
        LOG.log(Level.SEVERE, "uncaught exception", caught);
      }
    });

    Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {

      public void execute() {
        init();
      }
    });
  }

  private void init() {
    final FlowPanel layout = new FlowPanel();
    layout.add(new Label("Witaj w świecie XForms!"));
    RootPanel.get().add(layout);

    try {
      RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, GWT.getHostPageBaseURL() + "forms");
      rb.sendRequest("", new RequestCallback() {

        public void onResponseReceived(Request request, Response response) {
          LOG.info(" get response: " + response.getText());

          Document document = XMLParser.parse(response.getText());
          XFormsFactory factory = new XFormsFactory();
          XFormsDocument form = factory.create(document);
          layout.add((Widget) form.getXFormsDocumentWidget());

          form.addEventListener(new XFormsEventListener() {

            public void onXFormsEvent(XFormsEvent event) {
              LOG.info("form event: " + event.getNameAsString());
              for (Entry<String, Object> entry : event.getContext().entrySet()) {
                LOG.info(" - context event param name: " + entry.getKey() + " value: " + entry.getValue());
              }
            }
          });

          XFormsModel model = form.getModel();
          LOG.info("model: " + model);
        }

        public void onError(Request request, Throwable exception) {
          LOG.log(Level.SEVERE, "error while getting form", exception);
        }
      });
    } catch (RequestException e) {
      LOG.log(Level.SEVERE, "error while sending request", e);
    }
  }
}

Najważniejsza rzecz dzieje się w implementacji callback’a dla RequestBuilder’a. To tam parsowana jest zawartość odpowiedzi z serwletu, z której następnie tworzony jest dokument formularza. Dokument ten może zostać w prosty sposób dodany do dowolnego panelu GWT, gdyż zawiera w sobie implementację klasy Widget (linia 63). W kolejnych liniach pod formularz został podpięty listener, który rejestruje wszystkie zdarzenia na nim zachodzące. Taki listener może zostać podłączony do dowolnego elementu formularza (np. grupy pól) i będzie wtedy zbierał informacje z elementów podrzędnych tylko dla niego. W dowolnym momencie można też pobrać aktualną definicję modelu (linia 75), a metody API pozwalają także na pobieranie konkretnych instancji danych.

To tyle, jeśli chodzi o obsługę XForms’ów za pomocą GWT. Naprawdę niewiele, a jak zobaczysz poniżej, jest to w zupełności wystarczające aby wyświetlić nawet bardzo zaawansowany formularz.

Teraz przygotujemy mały formularz pokazujący kilka ciekawszych możliwości XForms:

form.xhtml
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/2002/06/xhtml2"
    xmlns:ev="http://www.w3.org/2001/xml-events"
    xmlns:xf="http://www.w3.org/2002/xforms"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">

  <head>
    <xf:model>
      <xf:instance xmlns="">
        <SampleData>
          <Text>blah blah</Text>
          <Password>secret password</Password>
          <Comment>Komentarz
wielolinijkowy</Comment>
          <Range xsi:type="xsd:integer">36</Range>
          <Date>2011-04-21</Date>
          <Time>16:29:00+02:00</Time>
          <Choose>T</Choose>
          <File xsi:type="xsd:anyURI"></File>
          <Country>pl</Country>
        </SampleData>
      </xf:instance>
      <xf:instance id="persons" xmlns="">
        <Persons>
          <Person>
            <Name>Tadeusz</Name>
            <Surname>Abacki</Surname>
            <Gender>M</Gender>
          </Person>
          <Person>
            <Name>Czesław</Name>
            <Surname>Babacki</Surname>
            <Gender>M</Gender>
          </Person>
          <Person>
            <Name>Beata</Name>
            <Surname>Cętocka</Surname>
            <Gender>F</Gender>
          </Person>
        </Persons>
      </xf:instance>

      <xf:bind nodeset="Text" required="true()"/>
      <xf:bind nodeset="Date" type="xsd:date"/>
      <xf:bind nodeset="Time" type="xsd:time"/>

      <xf:submission id="save_data" action="forms" method="post" replace="none"/>
      <xf:submission id="save_people" action="forms" method="post" replace="none" ref="instance('persons')"/>
    </xf:model>
  </head>
  <body>
    <p class="description">Formularz XForms</p>

    <h3>Osoby: (grupa powielana)</h3>
    <xf:group ref="instance('persons')">
      <xf:repeat id="PersonsSet" nodeset="/Persons/Person">
        <p>
          <xf:input ref="Name"><xf:label>Imię</xf:label></xf:input>
          <xf:input ref="Surname"><xf:label>Nazwisko</xf:label></xf:input>

          <xf:select1 ref="Gender">
             <xf:label>Płeć</xf:label>
             <xf:item>
               <xf:label>Mężczyzna</xf:label>
               <xf:value>M</xf:value>
             </xf:item>
             <xf:item>
               <xf:label>Kobieta</xf:label>
               <xf:value>F</xf:value>
             </xf:item>
          </xf:select1>
        </p>
      </xf:repeat>

      <xf:trigger>
        <xf:label>Wstaw nową osobę</xf:label>
        <xf:action ev:event="DOMActivate">
          <xf:insert at="index('PersonsSet')" nodeset="/Persons/Person" position="after"/>
          <xf:setvalue ref="/Persons/Person[index('PersonsSet')]/Name"/>
          <xf:setvalue ref="/Persons/Person[index('PersonsSet')]/Surname"/>
          <xf:setvalue ref="/Persons/Person[index('PersonsSet')]/Gender">M</xf:setvalue>
         </xf:action>
      </xf:trigger>
      <xf:trigger>
        <xf:label>Usuń zaznaczoną osobę</xf:label>
        <xf:delete at="index('PersonsSet')" ev:event="DOMActivate" nodeset="/Persons/Person"/>
      </xf:trigger>
      <xf:output value="index('PersonsSet')">
        <xf:label>Bieżący indeks:</xf:label>
      </xf:output>
    </xf:group>

    <p><xf:submit submission="save_people"><xf:label>Zapisz osoby</xf:label></xf:submit></p>

    <h3>Pola proste</h3>
    <p>
      <xf:input ref="Text">
        <xf:label>Tekst</xf:label>
        <xf:hint>Podpowiedź</xf:hint>
      </xf:input>
    </p>
    <p><xf:secret ref="Password"><xf:label>Hasło</xf:label></xf:secret></p>
    <p><xf:textarea ref="Comment"><xf:label>Komentarz</xf:label></xf:textarea></p>
    <p><xf:range ref="Range" start="0" end="100" step="5"><xf:label>Zakres</xf:label></xf:range></p>

    <xf:trigger appearance="minimal">
      <xf:label>Pola daty</xf:label>
      <xf:toggle case="case-1" ev:event="DOMActivate" />
    </xf:trigger>
    <xf:trigger appearance="minimal">
      <xf:label>Pola wyboru</xf:label>
      <xf:toggle case="case-2" ev:event="DOMActivate" />
    </xf:trigger>
    <xf:trigger appearance="minimal">
      <xf:label>Pola plików</xf:label>
      <xf:toggle case="case-3" ev:event="DOMActivate" />
    </xf:trigger>

    <xf:switch>
      <xf:case id="case-1" selected="true">
        <p><xf:input ref="Date"><xf:label>Data</xf:label></xf:input></p>
        <p><xf:input ref="Time"><xf:label>Czas</xf:label></xf:input></p>
      </xf:case>

      <xf:case id="case-2">
        <p>
          <xf:select1 ref="Choose" appearance="full">
            <xf:label>Wybór (Radio)</xf:label>
            <xf:item>
              <xf:label>Tak</xf:label>
              <xf:value>T</xf:value>
            </xf:item>
            <xf:item>
              <xf:label>Nie</xf:label>
              <xf:value>N</xf:value>
            </xf:item>
          </xf:select1>
        </p>

        <p>
          <xf:select1 ref="Country" selection="open">
            <xf:label>Kraj</xf:label>
            <xf:item>
              <xf:label>Polska</xf:label>
              <xf:value>pl</xf:value>
            </xf:item>
            <xf:item>
              <xf:label>USA</xf:label>
              <xf:value>usa</xf:value>
            </xf:item>
            <xf:item>
              <xf:label>Canada</xf:label>
              <xf:value>can</xf:value>
            </xf:item>
            <xf:item>
              <xf:label>Japan</xf:label>
              <xf:value>jpn</xf:value>
            </xf:item>
            <xf:item>
              <xf:label>Mexico</xf:label>
              <xf:value>mex</xf:value>
            </xf:item>
            <xf:item>
              <xf:label>Inny</xf:label>
              <xf:value>other</xf:value>
            </xf:item>
          </xf:select1>
        </p>
      </xf:case>

      <xf:case id="case-3">
        <p>
          <xf:upload ref="File">
             <xf:label>Plik</xf:label>
             <xf:mediatype>text/xml</xf:mediatype>
           </xf:upload>
         </p>

         <p><xf:output ref="File"><xf:label>Załadowany plik</xf:label></xf:output></p>
      </xf:case>
    </xf:switch>

    <p><xf:submit submission="save_data"><xf:label>Zapisz dane</xf:label></xf:submit></p>
  </body>
</html>

Jeśli przeanalizujesz powyższy kod, powinien stać się dla Ciebie zrozumiały. Ważne, abyś pamiętał o umieszczeniu kodu XForms w ramach pliku XHTML. Zwróć też uwagę na deklaracje przestrzeni nazw – bez nich EMC Formula nie będzie poprawnie parsowało dokumentu XML.
W ramach jednego pliku umieściłem zarówno definicję modelu z dwiema instancjami danych, jak i opis widoku formularza. Nic nie stoi na przeszkodzie, aby to rozdzielić i przy kolejnej instancji podać źródło dokumentu w atrybucie src. W definicji modelu dodatkowo można nadpisać lub dopisać reguły walidacji i typy danych dla poszczególnych elementów instancji danych – możliwości jest tutaj sporo. Sekcję modelu kończą definicje sposobów zatwierdzania formularza – do nich podpina się później reprezentację graficzną w postaci przycisku.
W samym opisie widoku umieściłem grupę powielaną, kilka pól typów prostych oraz kilka sekcji uaktywnianych po naciśnięciu odpowiedniego przycisku.

Po uruchomieniu projektu w przeglądarce formularz prezentuje się tak:
Formularz XForms

W projekcie, którego źródła można pobrać stąd, znajdziesz też style CSS zmieniające wygląd formularza na nieco ładniejszy (zaprezentowany na zrzucie ekranu). Style stworzyłem częściowo wzorując się na demie EMC Formula, jednak postawione ono zostało na nieco starszej wersji biblioteki, która różni się pod względem niektórych nazw klas stylów i nie dało się w całości przenieść tych dostępnych tam.

Zachęcam też do pobrania projektu poprzez repozytorium.

Aby uruchomić projekt, musisz najpierw pobrać bibliotekę EMC Formula i umieścić ją na lokalnym repozytorium maven’a. Użyłem najnowszej obecnie wersji biblioteki z numerem 1.2.1. Nazwy grup i artefaktów użytych w projekcie możesz sprawdzić w pliku pom.xml. Projekt uruchamia się poleceniem mvn gwt:run.

Na koniec dodam, że pokazałem tu tylko niewielką część możliwości XForms’ów. Zachęcam więc do samodzielnego testowania i dzielenia się efektami. Ja również na tym nie poprzestaję i jeśli uda mi się stworzyć coś ciekawszego, z pewnością to zamieszczę.

Jeśli z jakiś względów EMC Formula Ci nie odpowiada, wejdź na poniższą stronę i poszukaj czegoś bardziej spełniającego Twoje potrzeby:

Skomentuj

2 Komentarze.

  1. Tego właśnie szukałem! Świetny artykuł biblioteki której potrzebowałem od dawna. W akcie desperacji pisałem nawet swoje własne implementacje ale już zastanawiam się jak zastosować nowe znalezisko. Bądź pewien, że znalazłeś nowego czytelnika :)

    • Cieszę się, że mogłem jakoś pomóc :) XForms’y faktycznie są bardzo ciekawe i oferują mnóstwo możliwości, a za ich pomocą można pisać wręcz całe aplikacje. Trochę boli fakt, że nie będą pewnie prędko natywnie obsługiwane przez przeglądarki, ale i tak na chwilę obecną nie znalazłem lepszej alternatywy.

Skomentuj


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge