Merge remote-tracking branch 'upstream/master'
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeIndentRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeIndentRenderer.java
index ce63cae..5e64307 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeIndentRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeIndentRenderer.java
@@ -54,9 +54,7 @@
     }
 
     final boolean folder = node.isFolder();
-    final int level = node.getLevel();
     final boolean showJunctions = treeIndent.isShowJunctions();
-    final boolean showRootJunction = data.isShowRootJunction();
     final boolean expanded = folder && data.getExpandedState().isExpanded(node.getPath());
 
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
@@ -70,7 +68,7 @@
         treeIndent.getCustomClass());
 
     // encode tree junction
-    if (!showJunctions || !showRootJunction && level == 0) {
+    if (!showJunctions) {
       return;
     }
     writer.startElement(HtmlElements.I);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
index 8d65b5b..2569227 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
@@ -33,6 +33,7 @@
 import org.apache.myfaces.tobago.renderkit.RendererBase;
 import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
 import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
 import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
@@ -84,7 +85,7 @@
 
       // select
       if (data.getSelectable() != Selectable.none) { // selection
-         String selected = requestParameterMap.get(
+        String selected = requestParameterMap.get(
             clientId + ComponentUtils.SUB_SEPARATOR + AbstractUIData.SUFFIX_SELECTED);
 // todo        JsonUtils.decodeIntegerArray()StringArray()
         selected = selected.replaceAll("\\[", ";");
@@ -128,7 +129,9 @@
     Markup markup = Markup.NULL;
     final TreePath path = node.getPath();
     final SelectedState selectedState = data.getSelectedState();
-    if (data instanceof AbstractUITree && selectedState.isSelected(path)) {
+    final boolean selected = data instanceof AbstractUITree && selectedState.isSelected(path);
+
+    if (selected) {
       markup = markup.add(Markup.SELECTED);
     }
     if (folder) {
@@ -149,7 +152,7 @@
       writer.writeAttribute(HtmlAttributes.SELECTED, selectedState.isAncestorOfSelected(path));
       writer.writeAttribute(DataAttributes.ROW_INDEX, data.getRowIndex());
     } else {
-      writer.startElement(HtmlElements.DIV);
+      writer.startElement(HtmlElements.TOBAGO_TREE_NODE);
 
       // div id
       writer.writeIdAttribute(clientId);
@@ -158,16 +161,20 @@
       final boolean hidden = !dataRendersRowContainer && !visible;
 
       writer.writeClassAttribute(
-          TobagoClass.TREE_NODE,
+          null,
           TobagoClass.TREE_NODE.createMarkup(markup),
           hidden ? BootstrapClass.D_NONE : null,
           node.getCustomClass());
+      writer.writeAttribute(CustomAttributes.SELECTED, selected);
+      writer.writeAttribute(CustomAttributes.EXPANDABLE, folder);
+      writer.writeAttribute(CustomAttributes.INDEX, data.getRowIndex());
       HtmlRendererUtils.writeDataAttributes(facesContext, writer, node);
       if (parentId != null) {
         // TODO: replace with
         // todo writer.writeIdAttribute(parentId + SUB_SEPARATOR + AbstractUITree.SUFFIX_PARENT);
         // todo like in TreeListboxRenderer
         writer.writeAttribute(DataAttributes.TREE_PARENT, parentId, false);
+        writer.writeAttribute(CustomAttributes.PARENT, parentId, false);
       }
       writer.writeAttribute(DataAttributes.LEVEL, data.isShowRoot() ? node.getLevel() : node.getLevel() - 1);
     }
@@ -189,7 +196,7 @@
       }
       writer.endElement(HtmlElements.OPTION);
     } else {
-      writer.endElement(HtmlElements.DIV);
+      writer.endElement(HtmlElements.TOBAGO_TREE_NODE);
     }
   }
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeRenderer.java
index 382cfa5..81daec0 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeRenderer.java
@@ -33,6 +33,7 @@
 import org.apache.myfaces.tobago.model.TreePath;
 import org.apache.myfaces.tobago.renderkit.RendererBase;
 import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
 import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
@@ -95,18 +96,18 @@
       return;
     }
 
-    writer.startElement(HtmlElements.DIV);
+    writer.startElement(HtmlElements.TOBAGO_TREE);
     writer.writeIdAttribute(clientId);
     writer.writeClassAttribute(
-        TobagoClass.TREE,
-        TobagoClass.TREE.createMarkup(markup),
-        tree.getCustomClass());
+        tree.getCustomClass(),
+        TobagoClass.TREE.createMarkup(markup));
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, tree);
     writer.writeAttribute(DataAttributes.SCROLL_PANEL, Boolean.TRUE.toString(), false);
 
     final Selectable selectable = tree.getSelectable();
     if (selectable.isSupportedByTree()) {
       writer.writeAttribute(DataAttributes.SELECTABLE, selectable.name(), false);
+      writer.writeAttribute(CustomAttributes.SELECTABLE, selectable.name(), false);
     }
 
     final SelectedState selectedState = tree.getSelectedState();
@@ -168,6 +169,6 @@
     writer.writeAttribute(DataAttributes.SCROLL_POSITION, Boolean.TRUE.toString(), false);
     writer.endElement(HtmlElements.INPUT);
 
-    writer.endElement(HtmlElements.DIV);
+    writer.endElement(HtmlElements.TOBAGO_TREE);
   }
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeSelectRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeSelectRenderer.java
index 54b7dfd..6c6321b 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeSelectRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeSelectRenderer.java
@@ -51,6 +51,8 @@
   @Override
   public void decode(final FacesContext facesContext, final UIComponent component) {
 
+    // TODO do we need this?
+
     final AbstractUITreeSelect select = (AbstractUITreeSelect) component;
     final AbstractUITreeNodeBase node = ComponentUtils.findAncestor(select, AbstractUITreeNodeBase.class);
     final AbstractUIData data = ComponentUtils.findAncestor(node, AbstractUIData.class);
@@ -105,12 +107,11 @@
     final boolean folder = data.isFolder();
     final Selectable selectable = data.getSelectable();
 
-    writer.startElement(HtmlElements.SPAN);
+    writer.startElement(HtmlElements.TOBAGO_TREE_SELECT);
     final Markup markup = treeSelect.getMarkup();
     writer.writeClassAttribute(
-        TobagoClass.TREE_SELECT,
-        TobagoClass.TREE_SELECT.createMarkup(markup),
-        treeSelect.getCustomClass());
+        treeSelect.getCustomClass(),
+        TobagoClass.TREE_SELECT.createMarkup(markup));
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, treeSelect);
 
     if (treeSelect.isShowCheckbox()
@@ -149,7 +150,7 @@
       writer.endElement(HtmlElements.LABEL);
     }
 
-    writer.endElement(HtmlElements.SPAN);
+    writer.endElement(HtmlElements.TOBAGO_TREE_SELECT);
   }
 
   private String getClientIdWithoutRowIndex(final AbstractUIData data, final String id) {
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java
index 4e7568b..ccf4d3e 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java
@@ -163,7 +163,8 @@
     writer.writeAttribute(CustomAttributes.RENDER, command.getRender(), false);
     writer.writeAttribute(CustomAttributes.OMIT, command.getOmit());
     writer.writeAttribute(CustomAttributes.CONFIRMATION, command.getConfirmation(), true);
-    writer.writeAttribute(CustomAttributes.DECOUPLED, command.getTransition() != null ? command.getTransition() : false);
+    writer.writeAttribute(CustomAttributes.DECOUPLED,
+        command.getTransition() != null ? command.getTransition() : false);
     final Collapse collapse = command.getCollapse();
     if (collapse != null) {
       writer.writeAttribute(CustomAttributes.COLLAPSE_ACTION, collapse.getAction().name(), false);
@@ -219,14 +220,14 @@
   }
 
   private static ClientBehaviorContext getClientBehaviorContext(final FacesContext facesContext,
-                                                                final ClientBehaviorHolder clientBehaviorHolder, final String eventName) {
+      final ClientBehaviorHolder clientBehaviorHolder, final String eventName) {
     UIComponent component = (UIComponent) clientBehaviorHolder;
     return ClientBehaviorContext.createClientBehaviorContext(facesContext, component, eventName,
         component.getClientId(facesContext), null);
   }
 
   private static CommandMap getCommandMap(final FacesContext facesContext,
-                                          final ClientBehaviorContext clientBehaviorContext, final ClientBehavior clientBehavior) {
+      final ClientBehaviorContext clientBehaviorContext, final ClientBehavior clientBehavior) {
     if (clientBehavior instanceof ClientBehaviorBase) {
       String type = ((ClientBehaviorBase) clientBehavior).getRendererType();
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
index af6aade..4a975ce 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
@@ -31,6 +31,7 @@
    * <f:ajax> attribute
    */
   EXECUTE("execute"),
+  EXPANDABLE("expandable"),
   FOCUS_ID("focus-id"),
   /**
    * The index of the tab inside the tab group.
@@ -41,6 +42,9 @@
   MIN_CHARS("min-chars"),
   OMIT("omit"),
   ORIENTATION("orientation"),
+  PARENT("parent"),
+  SELECTABLE("selectable"),
+  SELECTED("selected"),
   /**
    * <f:ajax> attribute
    */
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index bb8cdb6..8e22447 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -139,7 +139,9 @@
 
   /**
    * The selectable attribute e. g. for trees.
+   * @deprecated since 5.0.0, please use {@link CustomAttributes#SELECTABLE}
    */
+  @Deprecated
   SELECTABLE("data-tobago-selectable"),
 
   /**
@@ -174,7 +176,9 @@
 
   /**
    * Id of the parent node in a tree node.
+   * @deprecated since 5.0.0, please use {@link CustomAttributes#PARENT}
    */
+  @Deprecated
   TREE_PARENT("data-tobago-tree-parent"),
 
   /**
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
index da03315..9e6e660 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/HtmlElements.java
@@ -144,7 +144,10 @@
   TOBAGO_SUGGEST("tobago-suggest"),
   TOBAGO_TAB("tobago-tab"),
   TOBAGO_TAB_CONTENT("tobago-tab-content"),
-  TOBAGO_TAB_GROUP("tobago-tab-group");
+  TOBAGO_TAB_GROUP("tobago-tab-group"),
+  TOBAGO_TREE("tobago-tree"),
+  TOBAGO_TREE_NODE("tobago-tree-node"),
+  TOBAGO_TREE_SELECT("tobago-tree-select");
 
   private final String value;
   private final boolean voidElement;
diff --git a/tobago-core/src/main/resources/scss/_tobago.scss b/tobago-core/src/main/resources/scss/_tobago.scss
index 4a4090b..6b972be 100644
--- a/tobago-core/src/main/resources/scss/_tobago.scss
+++ b/tobago-core/src/main/resources/scss/_tobago.scss
@@ -1526,9 +1526,19 @@
   margin-left: 7rem;
 }
 
-@for $i from 0 through 20 {
-  .tobago-treeNode[data-tobago-level='#{$i}'] {
-    margin-left: #{$i}rem;
+tobago-tree {
+  tobago-tree-node {
+    display: block;
+
+    @for $i from 0 through 20 {
+      &[data-tobago-level='#{$i}'] {
+        margin-left: #{$i}rem;
+      }
+    }
+
+    tobago-tree-select {
+      display: inline;
+    }
   }
 }
 
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectableUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectableUnitTest.java
index e26ce79..dbed202 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectableUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/model/SelectableUnitTest.java
@@ -20,12 +20,56 @@
 package org.apache.myfaces.tobago.model;
 
 import org.apache.myfaces.tobago.util.EnumUnitTest;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
 public class SelectableUnitTest extends EnumUnitTest {
 
   @Test
   public void testNames() throws IllegalAccessException, NoSuchFieldException {
     testNames(Selectable.class);
   }
+
+  @Test
+  public void testTypeScript() throws IOException {
+    final Path path = Paths.get("").toAbsolutePath().getParent().resolve(
+        Paths.get("tobago-theme", "tobago-theme-standard", "src", "main", "npm", "ts", "tobago-selectable.ts"));
+
+    final List<String> words = getWords(path);
+
+    for (Selectable selectable : Selectable.values()) {
+      Assertions.assertTrue(words.contains(selectable.name()),
+          selectable.name() + " should be found in tobago-selectable.ts");
+    }
+  }
+
+  private List<String> getWords(final Path path) throws IOException {
+    List<String> words = new ArrayList<>();
+
+    final String fileContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
+
+
+    StringBuilder stringBuilder = new StringBuilder();
+
+    for (char c : fileContent.toCharArray()) {
+      if (('0' <= c && c <= '9')
+          || ('A' <= c && c <= 'Z')
+          || ('a' <= c && c <= 'z')) {
+        stringBuilder.append(c);
+      } else {
+        words.add(stringBuilder.toString());
+        stringBuilder = new StringBuilder();
+      }
+    }
+
+    return words;
+  }
 }
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.test.js b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.test.js
new file mode 100644
index 0000000..de1a714
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.test.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {testFrameQuerySelectorFn} from "/script/tobago-test.js";
+import {TobagoTestTool} from "/tobago/test/tobago-test-tool.js";
+
+QUnit.test("execute both 'Action 1' and 'Action 2' two times", function (assert) {
+  let action1Fn = testFrameQuerySelectorFn("#page\\:mainForm\\:tree\\:2\\:actionCommand");
+  let action2Fn = testFrameQuerySelectorFn("#page\\:mainForm\\:tree\\:3\\:actionCommand");
+  let actionCount1Fn = testFrameQuerySelectorFn("#page\\:mainForm\\:actionCount1 .tobago-out");
+  let actionCount2Fn = testFrameQuerySelectorFn("#page\\:mainForm\\:actionCount2 .tobago-out");
+
+  const counterBeforeTestResult1 = Number(actionCount1Fn().textContent);
+  const counterBeforeTestResult2 = Number(actionCount2Fn().textContent);
+
+  let TTT = new TobagoTestTool(assert);
+  TTT.action(function () {
+    action1Fn().dispatchEvent(new Event('click'));
+  });
+  TTT.waitForResponse();
+  TTT.asserts(1, function () {
+    assert.equal(Number(actionCount1Fn().textContent), counterBeforeTestResult1 + 1);
+  });
+  TTT.action(function () {
+    action2Fn().dispatchEvent(new Event('click'));
+  });
+  TTT.waitForResponse();
+  TTT.asserts(1, function () {
+    assert.equal(Number(actionCount2Fn().textContent), counterBeforeTestResult2 + 1);
+  });
+  TTT.action(function () {
+    action1Fn().dispatchEvent(new Event('click'));
+  });
+  TTT.waitForResponse();
+  TTT.asserts(1, function () {
+    assert.equal(Number(actionCount1Fn().textContent), counterBeforeTestResult1 + 2);
+  });
+  TTT.action(function () {
+    action2Fn().dispatchEvent(new Event('click'));
+  });
+  TTT.waitForResponse();
+  TTT.asserts(1, function () {
+    assert.equal(Number(actionCount2Fn().textContent), counterBeforeTestResult2 + 2);
+  });
+  TTT.startTest();
+});
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.xhtml
index 205ce10..f7d8477 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/00-command/Tree_Command_Types.xhtml
@@ -35,16 +35,18 @@
       Like the parent nodes suggest,
       the entries in 'Actions' run an action with the <code>action</code> attribute.
       And the entries in 'Links' are links to an external website using the <code>link</code> attribute.</p>
-    <tc:tree value="#{treeCommandTypesController.sample}" var="node">
-      <tc:treeNode>
+    <tc:tree id="tree" value="#{treeCommandTypesController.sample}" var="node">
+      <tc:treeNode id="treeNode">
         <tc:treeIndent showJunctions="false"/>
         <tc:treeLabel value="#{node.name}" rendered="#{node.childCount > 0}"/>
-        <tc:treeCommand label="#{node.name}" rendered="#{node.childCount == 0}"
-                        action="#{treeCommandTypesController.increaseActionCount(node.action)}" link="#{node.url}"/>
+        <tc:treeCommand id="actionCommand" label="#{node.name}" rendered="#{node.childCount == 0 and empty node.url}"
+                        action="#{treeCommandTypesController.increaseActionCount(node.action)}"/>
+        <tc:treeCommand label="#{node.name}" rendered="#{node.childCount == 0 and not empty node.url}"
+                        link="#{node.url}"/>
       </tc:treeNode>
     </tc:tree>
 
-    <tc:out label="Action 1 Count" value="#{treeCommandTypesController.actionOneCount}"/>
-    <tc:out label="Action 2 Count" value="#{treeCommandTypesController.actionTwoCount}"/>
+    <tc:out id="actionCount1" label="Action 1 Count" value="#{treeCommandTypesController.actionOneCount}"/>
+    <tc:out id="actionCount2" label="Action 2 Count" value="#{treeCommandTypesController.actionTwoCount}"/>
   </tc:section>
 </ui:composition>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/01-select/Tree_Select.test.js b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/01-select/Tree_Select.test.js
index 7b638ed..6592f4c 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/01-select/Tree_Select.test.js
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/090-tree/01-select/Tree_Select.test.js
@@ -24,7 +24,7 @@
   let outputFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectedNodesOutput span");
   let selectableNoneFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:0");
   let selectableSingleFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:1");
-  let inputFn = testFrameQuerySelectorFn(".tobago-treeSelect input");
+  let inputFn = testFrameQuerySelectorFn("tobago-tree-select input");
 
   let TTT = new TobagoTestTool(assert);
   TTT.action(function () {
@@ -68,7 +68,7 @@
   let outputFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectedNodesOutput span");
   let selectableNoneFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:0");
   let selectableSingleLeafOnlyFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:2");
-  let inputFn = testFrameQuerySelectorFn(".tobago-treeSelect input");
+  let inputFn = testFrameQuerySelectorFn("tobago-tree-select input");
 
   let TTT = new TobagoTestTool(assert);
   TTT.action(function () {
@@ -112,7 +112,7 @@
   let outputFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectedNodesOutput span");
   let selectableNoneFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:0");
   let selectableMultiFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:3");
-  let inputFn = testFrameQuerySelectorFn(".tobago-treeSelect input");
+  let inputFn = testFrameQuerySelectorFn("tobago-tree-select input");
 
   let TTT = new TobagoTestTool(assert);
   TTT.action(function () {
@@ -164,7 +164,7 @@
   let outputFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectedNodesOutput span");
   let selectableNoneFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:0");
   let selectableMultiLeafOnlyFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:4");
-  let inputFn = testFrameQuerySelectorFn(".tobago-treeSelect input");
+  let inputFn = testFrameQuerySelectorFn("tobago-tree-select input");
 
   let TTT = new TobagoTestTool(assert);
   TTT.action(function () {
@@ -217,7 +217,7 @@
   let outputFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectedNodesOutput span");
   let selectableNoneFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:0");
   let selectableMultiCascadeFn = testFrameQuerySelectorFn("#page\\:mainForm\\:selectable\\:\\:5");
-  let inputFn = testFrameQuerySelectorFn(".tobago-treeSelect input");
+  let inputFn = testFrameQuerySelectorFn("tobago-tree-select input");
 
   let TTT = new TobagoTestTool(assert);
   TTT.action(function () {
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-selectable.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-selectable.ts
new file mode 100644
index 0000000..16db2e2
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-selectable.ts
@@ -0,0 +1,11 @@
+export enum Selectable {
+  none, // Not selectable.
+  multi, // Multi selection possible. No other limitations.
+  single, // Only one item is selectable.
+  singleOrNone, // Only one of no item is selectable.
+  multiLeafOnly, // Only leafs are selectable.
+  singleLeafOnly, // Only one item is selectable and it must be a leaf.
+  sibling, // Only siblings are selectable.
+  siblingLeafOnly, // Only siblings are selectable and they have to be leafs.
+  multiCascade // Multi selection possible. When (de)selecting an item, the subtree will also be (un)selected.
+}
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree.ts
index 0988f47..016f7ac 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-tree.ts
@@ -15,223 +15,269 @@
  * limitations under the License.
  */
 
-import {Listener, Phase} from "./tobago-listener";
-import {DomUtils} from "./tobago-utils";
+import {Selectable} from "./tobago-selectable";
 
-class Tree {
+export class Tree extends HTMLElement {
 
-  static toggleNode = function (event: MouseEvent) {
-    const element = event.currentTarget as HTMLElement;
-    const node: HTMLDivElement = element.closest(".tobago-treeNode") as HTMLDivElement;
-    const data = node.closest(".tobago-tree, .tobago-sheet") as HTMLElement;
-    const expanded = data.querySelector(
-        ":scope > .tobago-tree-expanded, :scope > .tobago-sheet-expanded") as HTMLInputElement;
-    const togglesIcon = node.querySelectorAll(".tobago-treeNode-toggle i") as NodeListOf<HTMLElement>;
-    const togglesImage = node.querySelectorAll(".tobago-treeNode-toggle img") as NodeListOf<HTMLImageElement>;
-    const rowIndex = Tree.rowIndex(node);
-    if (Tree.isExpanded(node, expanded)) {
-      Tree.hideChildren(node);
-      for (const icon of togglesIcon) {
-        icon.classList.remove(icon.dataset["tobagoOpen"]);
-        icon.classList.add(icon.dataset["tobagoClosed"]);
-      }
-      for (const image of togglesImage) {
-        let src = image.dataset["tobagoClosed"];
-        if (!src) { // use the open icon if there is no closed icon
-          src = image.dataset["tobagoOpen"];
-        }
-        image.setAttribute("src", src);
-      }
-      const set = Tree.getSet(expanded);
-      set.delete(rowIndex);
-      Tree.setSet(expanded, set);
-      node.classList.remove("tobago-treeNode-markup-expanded");
+  constructor() {
+    super();
+  }
+
+  connectedCallback() {
+  };
+
+  get isSheet(): boolean {
+    // TODO if sheet is implemented as custom element use:
+    // return this.tagName === "TOBAGO-SHEET";
+    return this.classList.contains("tobago-sheet");
+  }
+
+  clearSelectedNodes(): void {
+    this.hiddenInputSelected.value = "[]"; //empty set
+  }
+
+  addSelectedNode(selectedNode: number): void {
+    const selectedNodes = new Set(JSON.parse(this.hiddenInputSelected.value));
+    selectedNodes.add(selectedNode);
+    this.hiddenInputSelected.value = JSON.stringify(Array.from(selectedNodes));
+  }
+
+  deleteSelectedNode(selectedNode: number): void {
+    const selectedNodes = new Set(JSON.parse(this.hiddenInputSelected.value));
+    selectedNodes.delete(selectedNode);
+    this.hiddenInputSelected.value = JSON.stringify(Array.from(selectedNodes));
+  }
+
+  private get hiddenInputSelected(): HTMLInputElement {
+    if (this.isSheet) {
+      return this.querySelector(":scope > .tobago-sheet-selected");
     } else {
-      const reload = Tree.showChildren(node, expanded);
-      Tree.setSet(expanded, Tree.getSet(expanded).add(rowIndex));
-      if (reload) {
-        jsf.ajax.request(
-            node.id,
-            event,
-            {
-              //"javax.faces.behavior.event": "click",
-              execute: data.id,
-              render: data.id
-            });
-      } else {
-        for (const icon of togglesIcon) {
-          icon.classList.remove(icon.dataset["tobagoClosed"]);
-          icon.classList.add(icon.dataset["tobagoOpen"]);
-        }
-        for (const image of togglesImage) {
-          let src = image.dataset["tobagoOpen"];
-          if (!src) { // use the open icon if there is no closed icon
-            src = image.dataset["tobagoClosed"];
-          }
-          image.setAttribute("src", src);
-        }
-        node.classList.add("tobago-treeNode-markup-expanded");
-      }
+      return this.querySelector(":scope > .tobago-tree-selected");
     }
-  };
+  }
 
-  /**
-   * Hide all children of the node recursively.
-   * @param node A HTMLElement as a node of the tree.
-   */
-  static hideChildren = function (node: HTMLElement): void {
-    for (const child of Tree.findTreeChildren(node)) {
-      if (Tree.isInSheet(node)) {
-        child.parentElement.parentElement.classList.add("d-none");
-      } else {
-        child.classList.add("d-none");
-      }
-      Tree.hideChildren(child);
-    }
-  };
+  clearExpandedNodes(): void {
+    this.hiddenInputExpanded.value = "[]"; //empty set
+  }
 
-  /**
-   * Show the children of the node recursively, there parents are expanded.
-   * @param node A HTMLElement as a node of the tree.
-   * @param expanded The hidden field which contains the expanded state.
-   * @return is reload needed (to get all nodes from the server)
-   */
-  static showChildren = function (node: HTMLElement, expanded: HTMLInputElement): boolean {
-    const children = Tree.findTreeChildren(node);
-    if (children.length === 0) {
-      return true;
+  addExpandedNode(expandedNode: number): void {
+    const expandedNodes = new Set(JSON.parse(this.hiddenInputExpanded.value));
+    expandedNodes.add(expandedNode);
+    this.hiddenInputExpanded.value = JSON.stringify(Array.from(expandedNodes));
+  }
+
+  deleteExpandedNode(expandedNode: number): void {
+    const expandedNodes = new Set(JSON.parse(this.hiddenInputExpanded.value));
+    expandedNodes.delete(expandedNode);
+    this.hiddenInputExpanded.value = JSON.stringify(Array.from(expandedNodes));
+  }
+
+  get expandedNodes(): Set<number> {
+    return new Set(JSON.parse(this.hiddenInputExpanded.value));
+  }
+
+  private get hiddenInputExpanded(): HTMLInputElement {
+    if (this.isSheet) {
+      return this.querySelector(":scope > .tobago-sheet-expanded");
+    } else {
+      return this.querySelector(":scope > .tobago-tree-expanded");
     }
-    for (const child of children) {
-      if (Tree.isInSheet(node)) {
-        child.parentElement.parentElement.classList.remove("d-none");
-      } else {
-        child.classList.remove("d-none");
-      }
-      if (Tree.isExpanded(child, expanded)) {
-        const reload = Tree.showChildren(child, expanded);
-        if (reload) {
-          return true;
-        }
+  }
+
+  get selectable(): Selectable {
+    return Selectable[this.getAttribute("selectable")];
+  }
+}
+
+export class TreeNode extends HTMLElement {
+
+  constructor() {
+    super();
+  }
+
+  connectedCallback() {
+    if (this.isExpandable() && this.toggles !== null) {
+      this.toggles.forEach(element => element.addEventListener("click", this.toggleNode.bind(this)));
+    }
+  }
+
+  get tree(): Tree {
+    return this.closest("tobago-tree") as Tree; //TODO how to detect tobago-sheet?
+  }
+
+  isExpandable(): boolean {
+    return this.getAttribute("expandable") === "expandable";
+  }
+
+  isExpanded(): boolean {
+    for (const expandedNodeIndex of this.tree.expandedNodes) {
+      if (expandedNodeIndex === this.index) {
+        return true;
       }
     }
     return false;
-  };
-
-  static commandFocus = function (event: FocusEvent) {
-    const command = event.currentTarget as HTMLElement;
-    const node = command.parentElement;
-    const tree = node.closest(".tobago-tree");
-    const selected = tree.querySelector(".tobago-tree-selected") as HTMLInputElement;
-    selected.value = String(Tree.rowIndex(node));
-    for (const otherNode of tree.querySelectorAll(".tobago-treeNode-markup-selected")) {
-      if (otherNode !== node) {
-        otherNode.classList.remove("tobago-treeNode-markup-selected");
-      }
-    }
-    node.classList.add("tobago-treeNode-markup-selected");
-  };
-
-  static init = function (element: HTMLElement) {
-
-    for (const toggle of DomUtils.selfOrQuerySelectorAll(element, ".tobago-treeNode-markup-folder .tobago-treeNode-toggle")) {
-      toggle.addEventListener("click", Tree.toggleNode);
-    }
-
-    // selected for treeNode
-    for (const command of DomUtils.selfOrQuerySelectorAll(element, ".tobago-treeCommand")) {
-      command.addEventListener("focus", Tree.commandFocus);
-    }
-
-    for (const e of DomUtils.selfOrQuerySelectorAll(element, ".tobago-sheet, .tobago-tree")) {
-      const sheetOrTree: HTMLDivElement = e as HTMLDivElement;
-
-      // init selected field
-      const hiddenInputSelected: HTMLInputElement = sheetOrTree.querySelector(":scope > .tobago-sheet-selected, :scope > .tobago-tree-selected");
-      if (hiddenInputSelected) {
-        const value = new Set<number>();
-        for (const selected of sheetOrTree.querySelectorAll(".tobago-treeNode-markup-selected")) {
-          value.add(Tree.rowIndex(selected));
-        }
-        Tree.setSet(hiddenInputSelected, value);
-      }
-
-      // selected for treeSelect
-      for (const select of sheetOrTree.querySelectorAll(".tobago-treeSelect > input") as NodeListOf<HTMLInputElement>) {
-        let value: Set<number>;
-        // todo may use an class attribute for this value
-        if (select.type === "radio") {
-          value = new Set();
-          value.add(Tree.rowIndex(select));
-        } else if (select.checked) {
-          value = Tree.getSet(hiddenInputSelected);
-          value.add(Tree.rowIndex(select));
-        } else {
-          value = Tree.getSet(hiddenInputSelected);
-          value.delete(Tree.rowIndex(select));
-        }
-        Tree.setSet(hiddenInputSelected, value);
-      }
-
-      // init expanded field
-      const hiddenInputExpanded: HTMLInputElement = sheetOrTree.querySelector(":scope > .tobago-sheet-expanded, :scope > .tobago-tree-expanded");
-      if (hiddenInputExpanded) {
-        const value = new Set<number>();
-        for (const expanded of sheetOrTree.querySelectorAll(".tobago-treeNode-markup-expanded")) {
-          value.add(Tree.rowIndex(expanded));
-        }
-        Tree.setSet(hiddenInputExpanded, value);
-      }
-
-      // init tree selection for multiCascade
-      if (sheetOrTree.dataset.tobagoSelectable === "multiCascade") {
-        for (const treeNode of sheetOrTree.querySelectorAll(".tobago-treeNode") as NodeListOf<HTMLDivElement>) {
-
-          const checkbox: HTMLInputElement = treeNode.querySelector(".tobago-treeSelect input[type=checkbox]");
-          checkbox.addEventListener("change", function (event: Event) {
-            for (const childTreeNode of Tree.findTreeChildren(treeNode)) {
-              const childCheckbox: HTMLInputElement = childTreeNode.querySelector(".tobago-treeSelect input[type=checkbox]");
-              childCheckbox.checked = checkbox.checked;
-
-              const event = document.createEvent('HTMLEvents');
-              event.initEvent('change', true, false);
-              childCheckbox.dispatchEvent(event);
-            }
-          });
-        }
-      }
-    }
-  };
-
-  static getSet(element: HTMLInputElement): Set<number> {
-    return new Set(JSON.parse(element.value));
   }
 
-  static setSet(element, set: Set<number>) {
-    return element.value = JSON.stringify(Array.from(set));
-  }
-
-  static isExpanded = function (node: Element, expanded: HTMLInputElement) {
-    const rowIndex = Tree.rowIndex(node);
-    return Tree.getSet(expanded).has(rowIndex);
-  };
-
-  static rowIndex = function (node: Element): number { // todo: use attribute data-tobago-row-index
-    return parseInt(node.id.replace(/.+\:(\d+)(\:\w+)+/, '$1'));
-  };
-
-  static findTreeChildren = function (treeNode: HTMLElement): NodeListOf<HTMLDivElement> {
-    if (Tree.isInSheet(treeNode)) {
-      return treeNode.closest("tbody")
-          .querySelectorAll(".tobago-sheet-row[data-tobago-tree-parent='" + treeNode.id + "'] .tobago-treeNode");
+  get treeChildNodes(): NodeListOf<TreeNode> {
+    if (this.tree.isSheet) {
+      return this.closest("tbody").querySelectorAll("tobago-tree-node[parent='" + this.id + "']");
     } else {
-      return treeNode.parentElement.querySelectorAll(".tobago-treeNode[data-tobago-tree-parent='" + treeNode.id + "']");
+      return this.parentElement.querySelectorAll("tobago-tree-node[parent='" + this.id + "']");
     }
-  };
+  }
 
-  static isInSheet = function (node: Element) {
-    return node.parentElement.tagName === "TD";
-  };
+  get toggles(): NodeListOf<HTMLSpanElement> {
+    return this.querySelectorAll(".tobago-treeNode-toggle");
+  }
+
+  get icons(): NodeListOf<HTMLElement> {
+    return this.querySelectorAll(".tobago-treeNode-toggle i");
+  }
+
+  get images(): NodeListOf<HTMLImageElement> {
+    return this.querySelectorAll(".tobago-treeNode-toggle img");
+  }
+
+  get index(): number {
+    return Number(this.getAttribute("index"));
+  }
+
+  toggleNode(event: MouseEvent): void {
+    if (this.isExpanded()) {
+      for (const icon of this.icons) {
+        icon.classList.remove(icon.dataset.tobagoOpen);
+        icon.classList.add(icon.dataset.tobagoClosed);
+      }
+      for (const image of this.images) {
+        if (image.dataset.tobagoClosed) {
+          image.src = image.dataset.tobagoClosed;
+        } else {
+          image.src = image.dataset.tobagoOpen;
+        }
+      }
+
+      this.tree.deleteExpandedNode(this.index);
+      this.classList.remove("tobago-treeNode-markup-expanded");
+
+      this.hideNodes(this.treeChildNodes);
+    } else {
+      for (const icon of this.icons) {
+        icon.classList.remove(icon.dataset.tobagoClosed);
+        icon.classList.add(icon.dataset.tobagoOpen);
+      }
+      for (const image of this.images) {
+        if (image.dataset.tobagoOpen) {
+          image.src = image.dataset.tobagoOpen;
+        } else {
+          image.src = image.dataset.tobagoClosed;
+        }
+      }
+
+      this.tree.addExpandedNode(this.index);
+      this.classList.add("tobago-treeNode-markup-expanded");
+
+      if (this.treeChildNodes.length === 0) {
+        jsf.ajax.request(
+            this.id,
+            event,
+            {
+              //"javax.faces.behavior.event": "click",
+              execute: this.tree.id,
+              render: this.tree.id
+            });
+      } else {
+        this.showNodes(this.treeChildNodes);
+      }
+    }
+  }
+
+  hideNodes(treeChildNodes: NodeListOf<TreeNode>): void {
+    for (const treeChildNode of treeChildNodes) {
+
+      if (treeChildNode.tree.isSheet) {
+        treeChildNode.closest("tobago-sheet-row").classList.add("d-none");
+      } else {
+        treeChildNode.classList.add("d-none");
+      }
+
+      this.hideNodes(treeChildNode.treeChildNodes);
+    }
+  }
+
+  showNodes(treeChildNodes: NodeListOf<TreeNode>) {
+    for (const treeChildNode of treeChildNodes) {
+
+      if (treeChildNode.tree.isSheet) {
+        treeChildNode.closest("tobago-sheet-row").classList.remove("d-none");
+      } else {
+        treeChildNode.classList.remove("d-none");
+      }
+
+      this.showNodes(treeChildNode.treeChildNodes);
+    }
+  }
 }
 
-Listener.register(Tree.init, Phase.DOCUMENT_READY);
-Listener.register(Tree.init, Phase.AFTER_UPDATE);
+export class TreeSelect extends HTMLElement {
+
+  constructor() {
+    super();
+  }
+
+  connectedCallback() {
+    this.input.addEventListener("change", this.select.bind(this));
+
+    if (this.tree.selectable === Selectable.multiCascade) {
+      this.input.addEventListener("change", this.selectChildren.bind(this));
+    }
+  }
+
+  get tree(): Tree {
+    return this.closest("tobago-tree") as Tree;
+  }
+
+  get treeNode(): TreeNode {
+    return this.closest("tobago-tree-node") as TreeNode;
+  }
+
+  get input(): HTMLInputElement {
+    return this.querySelector("input");
+  }
+
+  select(event: Event): void {
+    switch (this.input.type) {
+      case "radio":
+        this.tree.clearSelectedNodes();
+        this.tree.addSelectedNode(this.treeNode.index);
+        break;
+      case "checkbox":
+        if (this.input.checked) {
+          this.tree.addSelectedNode(this.treeNode.index);
+        } else {
+          this.tree.deleteSelectedNode(this.treeNode.index);
+        }
+        break;
+    }
+  }
+
+  selectChildren(event: Event): void {
+    for (const treeChildNode of this.treeNode.treeChildNodes) {
+      const child: TreeSelect = treeChildNode.querySelector(":scope > tobago-tree-select");
+      child.input.checked = this.input.checked;
+
+      if (this.input.checked) {
+        this.tree.addSelectedNode(child.treeNode.index);
+      } else {
+        this.tree.deleteSelectedNode(child.treeNode.index);
+      }
+
+      child.input.dispatchEvent(new Event("change", {bubbles: true}));
+    }
+  }
+}
+
+document.addEventListener("DOMContentLoaded", function (event) {
+  window.customElements.define("tobago-tree-select", TreeSelect);
+  window.customElements.define("tobago-tree-node", TreeNode);
+  window.customElements.define("tobago-tree", Tree);
+});