diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index 0224a73..6c5322b 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -1,12 +1,39 @@
 (function(){
+    const api=window.esp32nmea2k;
+    if (! api) return;
     let isActive=false;
-    window.esp32nmea2k.registerListener((id,data)=>{
-        if (id === 0){
+    const tabName="example";
+    const configName="exampleBDSel";
+    let boatItemName;
+    api.registerListener((id,data)=>{
+        if (id === api.EVENTS.init){
             //data is capabilities
             if (data.testboard) isActive=true;
+            if (isActive){
+                let page=api.addTabPage(tabName,"Example");
+                api.addEl('div','',page,"this is a test tab");
+            }
         }
         if (isActive){
             console.log("exampletask listener",id,data);
+            if (id === api.EVENTS.tab){
+                if (data === tabName){
+                    console.log("example tab activated");
+                }
+            }
+            if (id == api.EVENTS.config){
+                let nextboatItemName=data[configName];
+                console.log("value of "+configName,nextboatItemName);
+                if (nextboatItemName){
+                    api.addUserFormatter(nextboatItemName,"xxx",function(v){
+                        return "X"+v+"X";
+                    })
+                }
+                if (boatItemName !== undefined && boatItemName != nextboatItemName){
+                    api.removeUserFormatter(boatItemName);
+                }
+                boatItemName=nextboatItemName;
+            }
         }
     })
 })();
diff --git a/web/index.css b/web/index.css
index eb3b1ca..f248d97 100644
--- a/web/index.css
+++ b/web/index.css
@@ -22,7 +22,7 @@ body {
   overflow: hidden;
 }
 
-.tabPage{
+#tabPages{
   overflow: auto;
 }
 
@@ -120,6 +120,9 @@ body {
   .hidden{
     display: none !important;
   }
+  .dash.invalid{
+    display: none;
+  }
   #xdrPage .row>.label{
     display: none;
   }
diff --git a/web/index.html b/web/index.html
index e7d784d..0862868 100644
--- a/web/index.html
+++ b/web/index.html
@@ -23,104 +23,106 @@
   <div class="tab" data-page="updatePage">Update</div>
   <div class="tab" data-url="https://github.com/wellenvogel/esp32-nmea2000" data-window="help" id="helpButton">Help</div>
 </div>
-<div id="statusPage" class="tabPage">
-  <div id="statusPageContent">
-    <div class="row">
-      <span class="label">VERSION</span>
-      <span class="value" id="version">---</span>
-      <button class="infoButton" id="converterInfo">?</button>
-    </div>
+<div id="tabPages">
+  <div id="statusPage" class="tabPage">
+    <div id="statusPageContent">
+      <div class="row">
+        <span class="label">VERSION</span>
+        <span class="value" id="version">---</span>
+        <button class="infoButton" id="converterInfo">?</button>
+      </div>
 
-    <div class="row even">
-      <span class="label">Access Point IP</span>
-      <span class="value" id="apIp">---</span>
+      <div class="row even">
+        <span class="label">Access Point IP</span>
+        <span class="value" id="apIp">---</span>
+      </div>
+      <div class="row ">
+        <span class="label">wifi client connected</span>
+        <span class="value" id="wifiConnected">---</span>&nbsp;[<span class="value" id="wifiSSID">---</span>]
+      </div>
+      <div class="row even">
+        <span class="label">wifi client IP</span>
+        <span class="value" id="clientIP">---</span>
+      </div>
+      <div class="row">
+        <span class="label"># clients</span>
+        <span class="value" id="numClients">---</span>
+      </div>
+      <div class="row even">
+        <span class="label">TCP client connected</span>
+        <span class="value" id="clientCon">---</span>
+      </div>
+      <div class="row">
+        <span class="label">TCP client error</span>
+        <span class="value" id="clientErr">---</span>
+      </div>
+      <div class="row even">
+        <span class="label">Free heap</span>
+        <span class="value" id="heap">---</span>
+      </div>
+      <div class="row">
+        <span class="label">NMEA2000 State</span>
+        [<span class="value" id="n2knode">---</span>]&nbsp;
+        <span class="value" id="n2kstate">UNKNOWN</span>
+      </div>
     </div>
-    <div class="row ">
-      <span class="label">wifi client connected</span>
-      <span class="value" id="wifiConnected">---</span>&nbsp;[<span class="value" id="wifiSSID">---</span>]
-    </div>
-    <div class="row even">
-      <span class="label">wifi client IP</span>
-      <span class="value" id="clientIP">---</span>
-    </div>
-    <div class="row">
-      <span class="label"># clients</span>
-      <span class="value" id="numClients">---</span>
-    </div> 
-    <div class="row even">
-      <span class="label">TCP client connected</span>
-      <span class="value" id="clientCon">---</span>
-    </div>
-    <div class="row">
-      <span class="label">TCP client error</span>
-      <span class="value" id="clientErr">---</span>
-    </div> 
-    <div class="row even">
-      <span class="label">Free heap</span>
-      <span class="value" id="heap">---</span>
-    </div>
-    <div class="row">
-      <span class="label">NMEA2000 State</span>
-      [<span class="value" id="n2knode">---</span>]&nbsp;
-      <span class="value" id="n2kstate">UNKNOWN</span>
-    </div> 
+    <button id="reset">Reset</button>
   </div>
-  <button id="reset">Reset</button>
-</div>
-<div class="configForm tabPage hidden" id="configPage" >
-  <div class="buttons">
-    <button id="resetForm">ReloadConfig</button>
-    <button id="forgetPass">ForgetPass</button>
-    <button id="changeConfig">Save&Restart</button>
-    <button id="exportConfig">Export</button>
-    <button id="importConfig">Import</button>
-    <button id="factoryReset">FactoryReset</button>
+  <div class="configForm tabPage hidden" id="configPage">
+    <div class="buttons">
+      <button id="resetForm">ReloadConfig</button>
+      <button id="forgetPass">ForgetPass</button>
+      <button id="changeConfig">Save&Restart</button>
+      <button id="exportConfig">Export</button>
+      <button id="importConfig">Import</button>
+      <button id="factoryReset">FactoryReset</button>
+    </div>
+    <div class="configFormRows">
+
+    </div>
   </div>
-  <div class="configFormRows">
+  <div class="configForm tabPage hidden" id="xdrPage">
+    <div class="buttons">
+      <button id="resetForm">ReloadConfig</button>
+      <button id="changeConfig">Save&Restart</button>
+      <button id="loadUnassigned">Show Unmapped</button>
+      <button id="exportXdr">Export</button>
+      <button id="importXdr">Import</button>
+    </div>
+    <div class="configFormRows">
+
+    </div>
+  </div>
+  <div class="tabPage hidden" id="dashboardPage">
 
   </div>
-</div>
-<div class="configForm tabPage hidden" id="xdrPage" >
-  <div class="buttons">
-    <button id="resetForm">ReloadConfig</button>
-    <button id="changeConfig">Save&Restart</button>
-    <button id="loadUnassigned">Show Unmapped</button>
-    <button id="exportXdr">Export</button>
-    <button id="importXdr">Import</button>
-  </div>
-  <div class="configFormRows">
-
-  </div>
-</div>
-<div class="tabPage hidden" id="dashboardPage">
-
-</div>
-<div class="tabPage hidden" id="updatePage">
-  <div class="row">
-    <span class="label">firmware type</span>
-    <span class="value status-fwtype">---</span>
-  </div>
-  <div class="row">
-    <span class="label">chip type</span>
-    <span class="value status-chipid">---</span>
-  </div>
-  <div class="row">
-    <span class="label">currentVersion</span>
-    <span class="value status-version">---</span>
-  </div>
-  <div class="row">
-    <span class="label">New Firmware</span>
-    <input type="file" name="file1" id="uploadFile">
-  </div>
-  <div class="row">
-    <span class="label"></span>
-    <span id="imageProperties" class="value"></span>
-  </div>
-  <div id="uploadProgress">
-    <div id="uploadDone"></div>
-  </div>
-  <div class="buttons">
-    <button id="uploadBin">Upload</button>
+  <div class="tabPage hidden" id="updatePage">
+    <div class="row">
+      <span class="label">firmware type</span>
+      <span class="value status-fwtype">---</span>
+    </div>
+    <div class="row">
+      <span class="label">chip type</span>
+      <span class="value status-chipid">---</span>
+    </div>
+    <div class="row">
+      <span class="label">currentVersion</span>
+      <span class="value status-version">---</span>
+    </div>
+    <div class="row">
+      <span class="label">New Firmware</span>
+      <input type="file" name="file1" id="uploadFile">
+    </div>
+    <div class="row">
+      <span class="label"></span>
+      <span id="imageProperties" class="value"></span>
+    </div>
+    <div id="uploadProgress">
+      <div id="uploadDone"></div>
+    </div>
+    <div class="buttons">
+      <button id="uploadBin">Upload</button>
+    </div>
   </div>
 </div>
 
diff --git a/web/index.js b/web/index.js
index 1d02096..c0d6ac2 100644
--- a/web/index.js
+++ b/web/index.js
@@ -8,6 +8,7 @@
     let listeners = [];
     let buttonHandlers={};
     let checkers={};
+    let userFormatters={};
     function addEl(type, clazz, parent, text) {
         let el = document.createElement(type);
         if (clazz) {
@@ -19,7 +20,12 @@
             });
         }
         if (text) el.textContent = text;
-        if (parent) parent.appendChild(el);
+        if (parent) {
+            if (typeof(parent) != 'object'){
+                parent=document.querySelector(parent);
+            }
+            if (parent) parent.appendChild(el);
+        }
         return el;
     }
     function forEl(query, callback, base) {
@@ -122,6 +128,7 @@
     function resetForm(ev) {
         getJson("/api/config")
             .then(function (jsonData) {
+                callListeners(api.EVENTS.config,jsonData);
                 for (let k in jsonData) {
                     if (k == "useAdminPass") {
                         needAdminPass = jsonData[k] != 'false';
@@ -1418,16 +1425,15 @@
         }
         let activeTab = document.getElementById(activeName);
         if (!activeTab) return;
-        let all = document.querySelectorAll('.tabPage');
-        for (let i = 0; i < all.length; i++) {
-            all[i].classList.add('hidden');
-        }
-        let tabs = document.querySelectorAll('.tab');
-        for (let i = 0; i < all.length; i++) {
-            tabs[i].classList.remove('active');
-        }
+        forEl('.tabPage',function(pel){
+            pel.classList.add('hidden');
+        });
+        forEl('.tab',function(tel){
+            tel.classList.remove('active');
+        });
         el.classList.add('active');
         activeTab.classList.remove('hidden');
+        callListeners(api.EVENTS.tab,activeName);
     }
     /**
      *
@@ -1602,6 +1608,10 @@
         }
 
 
+    }
+    Object.freeze(valueFormatters);
+    for (let k in valueFormatters){
+        Object.freeze(valueFormatters[k]);
     }
     function resizeFont(el, reset, maxIt) {
         if (maxIt === undefined) maxIt = 10;
@@ -1612,23 +1622,62 @@
             el.style.fontSize = next + "px";
         }
     }
-    function createDashboardItem(name, def, parent) {
-        if (!def.name) return;
-        let frame = addEl('div', 'dash', parent);
-        let title = addEl('span', 'dashTitle', frame, name);
-        let value = addEl('span', 'dashValue', frame);
-        value.setAttribute('id', 'data_' + name);
-        let fmt = valueFormatters[def.format];
-        if (def.format) value.classList.add(def.format);
-        let footer = addEl('div', 'footer', frame);
-        let src = addEl('span', 'source', footer);
-        src.setAttribute('id', 'source_' + name);
+    function getUnit(def,useUser){
+        let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ;
         let u = fmt ? fmt.u : ' ';
         if (!fmt && def.format && def.format.match(/formatXdr/)) {
             u = def.format.replace(/formatXdr:[^:]*:/, '');
         }
-        addEl('span', 'unit', footer, u);
-        return value;
+        return u;
+    }
+    /**
+     * create a dashboard item if it does not exist
+     * @param {*} def 
+     * @param {*} show 
+     * @param {*} parent 
+     * @returns the value div of the dashboard item
+     */
+    function createOrHideDashboardItem(def,show, parent) {
+        if (!def.name) return;
+        let frame=document.getElementById('frame_'+def.name);
+        let build=false;
+        if (frame){
+            if (frame.classList.contains('invalid') && show){
+                build=true;
+                frame.classList.remove('invalid');
+                frame.innerHTML='';
+            }
+        }
+        else{
+            if (! parent) return;
+            frame = addEl('div', 'dash', parent);
+            frame.setAttribute('id','frame_'+def.name);
+            build=true;
+        }
+        if (! show){
+            if (!frame.classList.contains('invalid')){
+                frame.classList.add('invalid');
+                frame.innerHTML='';
+            }
+            return;    
+        }
+        if (build) {
+            let title = addEl('span', 'dashTitle', frame, def.name);
+            let value = addEl('span', 'dashValue', frame);
+            value.setAttribute('id', 'data_' + def.name);
+            if (def.format) value.classList.add(def.format);
+            let footer = addEl('div', 'footer', frame);
+            let src = addEl('span', 'source', footer);
+            src.setAttribute('id', 'source_' + def.name);
+            let u = getUnit(def, true)
+            addEl('span', 'unit', footer, u);
+            callListeners(api.EVENTS.dataItemCreated, frame);
+        }
+        let de = document.getElementById('data_' + def.name);
+        return de;
+    }
+    function hideDashboardItem(name){
+        createOrHideDashboardItem({name:name},false);
     }
     function parseBoatDataLine(line) {
         let rt = {};
@@ -1659,6 +1708,7 @@
     }
     let lastSelectList = [];
     function updateDashboard(data) {
+        callListeners(api.EVENTS.boatData,data);
         let frame = document.getElementById('dashboardPage');
         let showInvalid = true;
         forEl('select[name=showInvalidData]', function (el) {
@@ -1669,24 +1719,17 @@
             let current = parseBoatDataLine(data[n]);
             if (!current.name) continue;
             names[current.name] = true;
-            let de = document.getElementById('data_' + current.name);
-            let isValid = current.valid;
-            if (!de && frame && (isValid || showInvalid)) {
-                de = createDashboardItem(current.name, current, frame);
-            }
-            if (de && (!isValid && !showInvalid)) {
-                de.parentElement.remove();
-                continue;
-            }
+            let show = current.valid||showInvalid;
+            let de=createOrHideDashboardItem(current,show,frame);
             if (de) {
                 let newContent = '----';
                 if (current.valid) {
                     let formatter;
                     if (current.format && current.format != "NULL") {
                         let key = current.format.replace(/^\&/, '');
-                        formatter = valueFormatters[key];
+                        formatter = userFormatters[current.name]|| valueFormatters[key];
                     }
-                    if (formatter) {
+                    if (formatter && formatter.f) {
                         newContent = formatter.f(current.value);
                     }
                     else {
@@ -1711,11 +1754,14 @@
                 src.textContent = sourceName(current.source);
             }
         }
-        console.log("update");
-        forEl('.dashValue', function (el) {
+        //console.log("update");
+        //remove all items that are not send any more
+        //this can only happen if the device restarted
+        //otherwise data items will not go away - they will become invalid
+        forEl('.dash', function (el) {
             let id = el.getAttribute('id');
             if (id) {
-                if (!names[id.replace(/^data_/, '')]) {
+                if (!names[id.replace(/^frame_/, '')]) {
                     el.parentElement.remove();
                 }
             }
@@ -1889,15 +1935,96 @@
             reader.readAsArrayBuffer(slice);
         });
     }
+    function addTabPage(name,label){
+        if (label === undefined) label=name;
+        let tab=addEl('div','tab','#tabs',label);
+        tab.setAttribute('data-page',name);
+        tab.addEventListener('click',function(ev){
+            handleTab(ev.target);
+        })
+        let page=addEl('div','tabPage hidden','#tabPages');
+        page.setAttribute('id',name);
+        return page;
+    }
+    function addUserFormatter(name,unit,formatter){
+        if (unit !== undefined && formatter !== undefined){
+            userFormatters[name]={
+                u:unit,
+                f:formatter
+            }
+        }
+        else{
+            delete userFormatters[name];
+        }
+        hideDashboardItem(name); //will recreate it on next data receive
+    }
     const api= {
         registerListener: function (callback) {
             listeners.push(callback);
         },
+        /**
+         * helper for creating dom elements
+         * parameters:
+         *   type: the element type (e.g. div)
+         *   class: a list of classes separated by space
+         *   parent (opt): a parent element (either a dom element vor a query selector)
+         *   text (opt): the text to be set as textContent 
+         * returns: the newly created element
+         */
         addEl: addEl,
+        /**
+         * iterator helper for a query selector
+         * parameters:
+         *  query: the query selector
+         *  callback: the callback function (will be called with the element as param)
+         *  base (opt): a dome element to be used as the root (defaults to document)
+         */
         forEl: forEl,
+        /**
+         * find the closest parent that has a particular class
+         * parameters:
+         *  element: the element to start with
+         *  class: the class to be searched for
+         * returns: the element or undefined/null
+         */
         closestParent: closestParent,
+        /**
+         * add a new tab
+         * parameters: 
+         *   name - the name of the page
+         *   label (opt): the label for the new page 
+         * returns: the newly created element
+         */
+        addTabPage: addTabPage,
+        /**
+         * add a user defined formatter for a boat data item
+         * parameters:
+         *   name : the boat data item name
+         *   unit: the unit to be displayed
+         *   formatter: the formatter function (must return a string)
+         */
+        addUserFormatter: addUserFormatter,
+        removeUserFormatter: function(name){
+            addUserFormatter(name);
+        },
+        /**
+         * a dict of formatters
+         * each one has 2 members:
+         *   u: the unit
+         *   f: the formatter function
+         */
+        formatters: valueFormatters,
+        /**
+         * parse a line of boat data
+         * the line has name,format,valid,update,source,value
+         */
+        parseBoatDataLine: parseBoatDataLine,
         EVENTS: {
             init: 0, //called when capabilities are loaded, data is capabilities
+            tab: 1, //tab page activated data is the id of the tab page
+            config: 2, //data is the config object
+            boatData: 3, //data is the list of boat Data items
+            dataItemCreated: 4, //data is the frame item of the boat data display
         }
     };
     function callListeners(event,data){