Repository: maxfordham/ipyautoui (language: python)

Issue:
FIX-file-upload-to-dir-following-iterable-update
fixes file(s) upload to dir widgets 


Discussion / hints:

Fix patch:
diff --git a/.env b/.env
index 066de154..ecf2d226 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,5 @@
-PYTHONPATH=${workspaceFolder}/src
 MAPLOCAL_OS_FROM="linux"
 MAPLOCAL_OS_TO="windows"
 MAPLOCAL_FROM=/home
-MAPLOCAL_TO=\\wsl.localhost\20221021\home
+MAPLOCAL_TO=\\wsl.localhost\Ubuntu\home
 MAPLOCAL_SCRIPT_PATH=maplocal_example.py
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 96b3dcb2..f18b7a06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,8 +70,6 @@ target/
 /docs/autoui_mapping.xlsx
 /src/ipyautoui/custom/.ipynb_checkpoints
 /src/ipyautoui/custom/__pycache__
-/src/ipyautoui/autoui.ipynb
-/src/ipyautoui/displayfile.ipynb
 /src/ipyautoui/.ipynb_checkpoints
 /src/ipyautoui/_version.py
 /src/ipyautoui/demo_schemas/.ipynb_checkpoints/
diff --git a/README.md b/README.md
index 7ac919b4..bd1b6cff 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
 
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/maxfordham/ipyautoui/HEAD) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![PyPI version](https://badge.fury.io/py/ipyautoui.svg)](https://badge.fury.io/py/ipyautoui) [![Test Python Package](https://github.com/maxfordham/ipyautoui/actions/workflows/test-python-package.yml/badge.svg)](https://github.com/maxfordham/ipyautoui/actions/workflows/test-python-package.yml)
 
-![](docs/logo.png)
+![](images/logo.png)
 
-## Try in online
+## Try it online!
 
 launch [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/maxfordham/ipyautoui/HEAD).
 
@@ -25,7 +25,7 @@ from ipyautoui import AutoUi, AutoVjsf, AutoDisplay, demo
 
 demo()
 ```
-
+![demo](images/demo.png)
 ipyautoui aims to give you as much as possible out-the-box, whilst also supporting a simple workflow to extend and customise the interface to specific user requirements.
 
 ## Summary of main features
@@ -155,9 +155,10 @@ using VS Code then install the Jupytext extension. In the `.vscode/settings.json
 `"jupyter.notebookFileRoot": "${fileDirname}"` matching with the jupyterlab default. 
 ```
 
-## Packaging (restricted to core-maintainers)
+## Packaging
 
 ```sh
+#  NOTE: restricted to core-maintainers only
 mamba activate hatcher # or conda env with hatch installed
 hatch build  # builds to local folder
 hatch publish  # publishes to pypi
@@ -167,3 +168,4 @@ hatch publish  # publishes to pypi
 
 AutoVjsf is a very thin wrapper where ipyvuetify and vuetify-jsonschema-form are doing all the work.
 Many thanks to @mariobuikhuizen for providing me with the [boiler-plate code](https://github.com/widgetti/ipyvuetify/issues/182).
+Contributions welcome.
diff --git a/binder/environment.yml b/binder/environment.yml
deleted file mode 100644
index 2e7b3ead..00000000
--- a/binder/environment.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: ipyautoui
-# channels priority
-# see https://bioconda.github.io/
-# conda-forge has highest prioriy
-channels:
-  - defaults
-  - conda-forge
-#  - http://10.10.30.125:8000
-dependencies:
-  - python>=3.7
-  - numpy
-  - pandas
-  - jupyterlab
-  - jupyterlab-unfold
-  - xeus-python
-  - ipywidgets
-  - ipyfilechooser
-  - ipydatagrid
-  - ipyvuetify
-  - voila
-  - plotly
-  - halo
-  - jupytext
-  - markdown
-  - nodejs
-  - altair
-  - pydantic>2
-  - pyyaml
-  - openpyxl
-  - black
-  - nb_black
-  - immutables
-  - wcmatch
-  - pytest
-  - pip
-  - pip:
-      - maplocal==0.1.1
-      - jsonref
-      - pydantic-extra-types
diff --git a/docs/README.txt b/docs/README.txt
index e440c9ce..56d20372 100644
--- a/docs/README.txt
+++ b/docs/README.txt
@@ -4,6 +4,7 @@ To build, do the following:
 
 1. create conda environment for docs
 >>> mamba env create -f environment.yml
+>>> mamba activate ipyautoui-docs
 
 2. generate conf.py file from jupyterbook _config.yml
 >>> jupyter-book config sphinx .
diff --git a/docs/_config.yml b/docs/_config.yml
index 7922c843..4110dfc7 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -3,22 +3,13 @@
 
 title: ipyautoui
 author: mfcode
-logo: logo.png
+logo: images/logo.png
 
 # Force re-execution of notebooks on each build.
 # See https://jupyterbook.org/content/execute.html
 execute:
   execute_notebooks: auto
 
-# Define the name of the latex output file for PDF builds
-latex:
-  latex_documents:
-    targetname: book.tex
-
-# Add a bibtex file so that we can create citations
-bibtex_bibfiles:
-  - references.bib
-
 # Information about where the book exists on the web
 repository:
   url: https://github.com/jgunstone/ipyautoui  # Online location of your book
@@ -30,16 +21,15 @@ repository:
 html:
   use_issues_button: true
   use_repository_button: true
-
-
+  favicon: images/favicon.png
 sphinx:
   extra_extensions:
-  - 'sphinx.ext.autodoc'
-  - 'sphinx.ext.napoleon'
-  - 'sphinx.ext.viewcode'
+  # - 'sphinx.ext.autodoc'
+  # - 'sphinx.ext.napoleon'
+  # - 'sphinx.ext.viewcode'
   - 'sphinx.ext.todo'
   - 'sphinx.ext.autosummary'
-  - 'autoapi.extension'
+  # - 'autoapi.extension'
   - 'sphinxcontrib.mermaid'
   #- 'jupyter_sphinx'
   #- 'nbsphinx'
@@ -48,22 +38,22 @@ sphinx:
     # add_module_names: False
     # autosummary_generate: True
     # autoapi
-    autoapi_generate_api_docs: True
-    autoapi_add_toctree_entry: True
-    autoapi_dirs: ['../src']
-    # napoleon
-    napoleon_google_docstring: True
-    napoleon_numpy_docstring:  False
-    napoleon_include_init_with_doc:  False
-    napoleon_include_private_with_doc:  False
-    napoleon_include_special_with_doc:  True
-    napoleon_use_admonition_for_examples:  False
-    napoleon_use_admonition_for_notes:  False
-    napoleon_use_admonition_for_references:  False
-    napoleon_use_ivar:  True
-    napoleon_use_param:  True
-    napoleon_use_rtype:  True
+    # autoapi_generate_api_docs: True
+    # autoapi_add_toctree_entry: True
+    # autoapi_dirs: ['../src']
+    # # napoleon
+    # napoleon_google_docstring: True
+    # napoleon_numpy_docstring:  False
+    # napoleon_include_init_with_doc:  False
+    # napoleon_include_private_with_doc:  False
+    # napoleon_include_special_with_doc:  True
+    # napoleon_use_admonition_for_examples:  False
+    # napoleon_use_admonition_for_notes:  False
+    # napoleon_use_admonition_for_references:  False
+    # napoleon_use_ivar:  True
+    # napoleon_use_param:  True
+    # napoleon_use_rtype:  True
     # show __init__ docstring
     # https://stackoverflow.com/questions/5599254/how-to-use-sphinxs-autodoc-to-document-a-classs-init-self-method
-    autoclass_content:  both
+    # autoclass_content:  both
     # html_static_path: ['_static']
diff --git a/docs/_toc.yml b/docs/_toc.yml
index b72d8426..4257303f 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -9,4 +9,3 @@ chapters:
 - file: autovuetify
 - file: autodisplay
 - file: custom-renderers
-- file: api
diff --git a/docs/autodisplay.ipynb b/docs/autodisplay.ipynb
index 35c89404..8cb5acce 100644
--- a/docs/autodisplay.ipynb
+++ b/docs/autodisplay.ipynb
@@ -1,275 +1,189 @@
 {
-    "cells": [
-        {
-            "cell_type": "markdown",
-            "id": "5119f197-19da-488e-a15f-5c478f55cbef",
-            "metadata": {},
-            "source": [
-                "# AutoDisplay"
-            ]
-        },
-        {
-            "cell_type": "markdown",
-            "id": "03047eb4-67ea-4453-b08b-44296528c8f4",
-            "metadata": {},
-            "source": [
-                "## Render a list of files"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 1,
-            "id": "cd1a6c89-b5c1-4234-99e8-ad4a93036de8",
-            "metadata": {},
-            "outputs": [],
-            "source": [
-                "%run __init__.py\n",
-                "%load_ext lab_black"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 10,
-            "id": "d1307a87-a12c-4f53-8c31-e383eca373e3",
-            "metadata": {},
-            "outputs": [
-                {
-                    "data": {
-                        "application/vnd.jupyter.widget-view+json": {
-                            "model_id": "9c4a68b48cee48ceb3c58ee89c54396e",
-                            "version_major": 2,
-                            "version_minor": 0
-                        },
-                        "text/plain": [
-                            "VBox(children=(VBox(children=(HBox(), HBox())), VBox(children=(DisplayObject(children=(HBox(children=(Valid(va…"
-                        ]
-                    },
-                    "metadata": {},
-                    "output_type": "display_data"
-                }
-            ],
-            "source": [
-                "from ipyautoui.demo_schemas import CoreIpywidgets\n",
-                "from ipyautoui import AutoUi, AutoDisplay\n",
-                "from ipyautoui.autodisplay import DisplayPath\n",
-                "from ipyautoui.constants import load_test_constants\n",
-                "from IPython.display import display\n",
-                "import pathlib\n",
-                "\n",
-                "tests_constants = load_test_constants()\n",
-                "DIR_FILETYPES = load_test_constants().DIR_FILETYPES\n",
-                "paths = list(pathlib.Path(DIR_FILETYPES).glob(\"*.*\"))\n",
-                "ad = AutoDisplay.from_paths(paths, display_showhide=False)\n",
-                "display(ad)"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 12,
-            "id": "7c1ff2ee-ce52-417e-bea1-fe75a728fdcf",
-            "metadata": {},
-            "outputs": [
-                {
-                    "data": {
-                        "application/vnd.jupyter.widget-view+json": {
-                            "model_id": "33e1a4b41ddd46acb87e0148420f63a8",
-                            "version_major": 2,
-                            "version_minor": 0
-                        },
-                        "text/plain": [
-                            "DisplayPath(children=(HBox(children=(Valid(value=True, disabled=True, layout=Layout(height='25px', width='20px…"
-                        ]
-                    },
-                    "execution_count": 12,
-                    "metadata": {},
-                    "output_type": "execute_result"
-                }
-            ],
-            "source": [
-                "d = DisplayPath(paths[0])\n",
-                "d"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 17,
-            "id": "d80cb315-a37e-4d11-9a1c-c815c22aa643",
-            "metadata": {},
-            "outputs": [
-                {
-                    "name": "stdout",
-                    "output_type": "stream",
-                    "text": [
-                        "adsf\n"
-                    ]
-                }
-            ],
-            "source": [
-                "import maplocal\n",
-                "\n",
-                "if maplocal.maplocal_openlocal_exists():\n",
-                "    from maplocal import openlocal as open_path\n",
-                "    from maplocal import maplocal as make_new_path\n",
-                "else:\n",
-                "    print(\"adsf\")"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 19,
-            "id": "5c761bbe-d5d4-4887-bcc4-ebf7104382ce",
-            "metadata": {},
-            "outputs": [
-                {
-                    "data": {
-                        "text/plain": [
-                            "<function maplocal.maplocal.maplocal_openlocal_exists()>"
-                        ]
-                    },
-                    "execution_count": 19,
-                    "metadata": {},
-                    "output_type": "execute_result"
-                }
-            ],
-            "source": [
-                "maplocal.maplocal_openlocal_exists"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 16,
-            "id": "d06f80ed-4567-465d-bc87-3333bb6ed92f",
-            "metadata": {},
-            "outputs": [
-                {
-                    "ename": "FileNotFoundError",
-                    "evalue": "[Errno 2] No such file or directory: 'xdg-open'",
-                    "output_type": "error",
-                    "traceback": [
-                        "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-                        "\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)",
-                        "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43md\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdisplay_actions\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n",
-                        "File \u001b[0;32m~/ipyautoui/src/ipyautoui/_utils.py:37\u001b[0m, in \u001b[0;36mopen_path\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m     34\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01msys\u001b[39;00m\n\u001b[1;32m     36\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m sys\u001b[38;5;241m.\u001b[39mplatform \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlinux\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m---> 37\u001b[0m     \u001b[43msubprocess\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcall\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mxdg-open\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m     38\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m     39\u001b[0m     subprocess\u001b[38;5;241m.\u001b[39mcall([\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mexplorer.exe\u001b[39m\u001b[38;5;124m\"\u001b[39m, path])\n",
-                        "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/subprocess.py:349\u001b[0m, in \u001b[0;36mcall\u001b[0;34m(timeout, *popenargs, **kwargs)\u001b[0m\n\u001b[1;32m    341\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mcall\u001b[39m(\u001b[38;5;241m*\u001b[39mpopenargs, timeout\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m    342\u001b[0m \u001b[38;5;250m    \u001b[39m\u001b[38;5;124;03m\"\"\"Run command with arguments.  Wait for command to complete or\u001b[39;00m\n\u001b[1;32m    343\u001b[0m \u001b[38;5;124;03m    timeout, then return the returncode attribute.\u001b[39;00m\n\u001b[1;32m    344\u001b[0m \n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m    347\u001b[0m \u001b[38;5;124;03m    retcode = call([\"ls\", \"-l\"])\u001b[39;00m\n\u001b[1;32m    348\u001b[0m \u001b[38;5;124;03m    \"\"\"\u001b[39;00m\n\u001b[0;32m--> 349\u001b[0m     \u001b[38;5;28;01mwith\u001b[39;00m \u001b[43mPopen\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mpopenargs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m p:\n\u001b[1;32m    350\u001b[0m         \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m    351\u001b[0m             \u001b[38;5;28;01mreturn\u001b[39;00m p\u001b[38;5;241m.\u001b[39mwait(timeout\u001b[38;5;241m=\u001b[39mtimeout)\n",
-                        "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/subprocess.py:951\u001b[0m, in \u001b[0;36mPopen.__init__\u001b[0;34m(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask)\u001b[0m\n\u001b[1;32m    947\u001b[0m         \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtext_mode:\n\u001b[1;32m    948\u001b[0m             \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstderr \u001b[38;5;241m=\u001b[39m io\u001b[38;5;241m.\u001b[39mTextIOWrapper(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstderr,\n\u001b[1;32m    949\u001b[0m                     encoding\u001b[38;5;241m=\u001b[39mencoding, errors\u001b[38;5;241m=\u001b[39merrors)\n\u001b[0;32m--> 951\u001b[0m     \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_execute_child\u001b[49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexecutable\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpreexec_fn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mclose_fds\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    952\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mpass_fds\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcwd\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43menv\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    953\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mstartupinfo\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcreationflags\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshell\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    954\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mp2cread\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mp2cwrite\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    955\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mc2pread\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mc2pwrite\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    956\u001b[0m \u001b[43m                        \u001b[49m\u001b[43merrread\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43merrwrite\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    957\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mrestore_signals\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    958\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mgid\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgids\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43muid\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mumask\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m    959\u001b[0m \u001b[43m                        \u001b[49m\u001b[43mstart_new_session\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m    960\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m:\n\u001b[1;32m    961\u001b[0m     \u001b[38;5;66;03m# Cleanup if the child failed starting.\u001b[39;00m\n\u001b[1;32m    962\u001b[0m     \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mfilter\u001b[39m(\u001b[38;5;28;01mNone\u001b[39;00m, (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstdin, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstdout, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstderr)):\n",
-                        "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/subprocess.py:1821\u001b[0m, in \u001b[0;36mPopen._execute_child\u001b[0;34m(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session)\u001b[0m\n\u001b[1;32m   1819\u001b[0m     \u001b[38;5;28;01mif\u001b[39;00m errno_num \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m   1820\u001b[0m         err_msg \u001b[38;5;241m=\u001b[39m os\u001b[38;5;241m.\u001b[39mstrerror(errno_num)\n\u001b[0;32m-> 1821\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m child_exception_type(errno_num, err_msg, err_filename)\n\u001b[1;32m   1822\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m child_exception_type(err_msg)\n",
-                        "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'xdg-open'"
-                    ]
-                }
-            ],
-            "source": [
-                "d.display_actions.open_file()"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": null,
-            "id": "dd826619-3193-4bd3-a7fc-6b80e8f8c136",
-            "metadata": {},
-            "outputs": [],
-            "source": [
-                "order=['exists', 'openpreview', 'name', 'openfile', 'openfile']"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 7,
-            "id": "9aaa814d-34bf-40f9-b435-37706d2eb8d9",
-            "metadata": {},
-            "outputs": [
-                {
-                    "data": {
-                        "text/plain": [
-                            "['exists', 'openpreview', 'name', 'openfile', 'openfile']"
-                        ]
-                    },
-                    "execution_count": 7,
-                    "metadata": {},
-                    "output_type": "execute_result"
-                }
-            ],
-            "source": [
-                "list(ad.order) + [\"openfile\"]"
-            ]
-        },
-        {
-            "cell_type": "markdown",
-            "id": "20380660-7a99-42d1-b31a-25f0e0a87401",
-            "metadata": {},
-            "source": [
-                "## Create a custom renderer"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": 9,
-            "id": "7b833bf5-5c24-4e38-8d2e-393558dde75e",
-            "metadata": {},
-            "outputs": [
-                {
-                    "data": {
-                        "application/vnd.jupyter.widget-view+json": {
-                            "model_id": "f5a7194b74a142bba4c2340b8db44071",
-                            "version_major": 2,
-                            "version_minor": 0
-                        },
-                        "text/plain": [
-                            "VBox(children=(VBox(children=(HBox(), HBox())), VBox(children=(DisplayObject(children=(HBox(children=(Valid(va…"
-                        ]
-                    },
-                    "metadata": {},
-                    "output_type": "display_data"
-                }
-            ],
-            "source": [
-                "from ipyautoui.demo_schemas import CoreIpywidgets\n",
-                "\n",
-                "user_file_renderers = AutoUi.create_autodisplay_map(\n",
-                "    ext=\".aui.json\", schema=CoreIpywidgets\n",
-                ")\n",
-                "\n",
-                "test_ui = AutoDisplay.from_paths(\n",
-                "    paths=[tests_constants.PATH_TEST_AUI],\n",
-                "    renderers=user_file_renderers,\n",
-                "    display_showhide=False,\n",
-                ")\n",
-                "\n",
-                "display(test_ui)"
-            ]
-        },
-        {
-            "cell_type": "code",
-            "execution_count": null,
-            "id": "9d7e3a4e-b7f9-4ff6-a9de-bb10e49851a0",
-            "metadata": {},
-            "outputs": [],
-            "source": []
-        }
-    ],
-    "metadata": {
-        "kernelspec": {
-            "display_name": "Python 3 (ipykernel)",
-            "language": "python",
-            "name": "python3"
-        },
-        "language_info": {
-            "codemirror_mode": {
-                "name": "ipython",
-                "version": 3
-            },
-            "file_extension": ".py",
-            "mimetype": "text/x-python",
-            "name": "python",
-            "nbconvert_exporter": "python",
-            "pygments_lexer": "ipython3",
-            "version": "3.9.16"
-        },
-        "vscode": {
-            "interpreter": {
-                "hash": "8ada13db5691318db95c45469f21e1a7752ab0a7ab2e35f607cb2aeb67fe81be"
-            }
-        }
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "5119f197-19da-488e-a15f-5c478f55cbef",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
     },
-    "nbformat": 4,
-    "nbformat_minor": 5
-}
\ No newline at end of file
+    "tags": []
+   },
+   "source": [
+    "# AutoDisplay"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "03047eb4-67ea-4453-b08b-44296528c8f4",
+   "metadata": {},
+   "source": [
+    "## Render a list of files"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "cd1a6c89-b5c1-4234-99e8-ad4a93036de8",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": [
+     "remove-cell"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "%run __init__.py\n",
+    "%load_ext lab_black"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9f7f6255-f439-42f6-8a61-774594a79ea3",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "```{warning} the demo requires a python kernel to run. Try on Binder [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/maxfordham/ipyautoui/HEAD)\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "d1307a87-a12c-4f53-8c31-e383eca373e3",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "36d846a817534b9a868ab45f64390a75",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "VBox(children=(VBox(children=(HBox(), HBox())), VBox(children=(DisplayObject(children=(HBox(children=(Valid(va…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "from ipyautoui.demo_schemas import CoreIpywidgets\n",
+    "from ipyautoui import AutoUi, AutoDisplay\n",
+    "from ipyautoui.autodisplay import DisplayPath\n",
+    "from ipyautoui.constants import load_test_constants\n",
+    "from IPython.display import display\n",
+    "import pathlib\n",
+    "\n",
+    "tests_constants = load_test_constants()\n",
+    "paths = list(load_test_constants().DIR_FILETYPES.glob(\"*.*\"))\n",
+    "ad = AutoDisplay.from_paths(paths, display_showhide=False)\n",
+    "display(ad)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "20380660-7a99-42d1-b31a-25f0e0a87401",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "## Create a custom renderer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "7b833bf5-5c24-4e38-8d2e-393558dde75e",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "6a167136f21a479da9a1e0a984c18dde",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "VBox(children=(VBox(children=(HBox(), HBox())), VBox(children=(DisplayObject(children=(HBox(children=(Valid(va…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "from ipyautoui.demo_schemas import CoreIpywidgets\n",
+    "\n",
+    "user_file_renderers = AutoUi.create_autodisplay_map(\n",
+    "    ext=\".aui.json\", schema=CoreIpywidgets\n",
+    ")\n",
+    "tests_constants.PATH_TEST_AUI.write_text(\n",
+    "    CoreIpywidgets(int_text_req=3, int_text_nullable=None).model_dump_json(indent=4)\n",
+    ")\n",
+    "\n",
+    "test_ui = AutoDisplay.from_paths(\n",
+    "    paths=[tests_constants.PATH_TEST_AUI],\n",
+    "    renderers=user_file_renderers,\n",
+    "    display_showhide=False,\n",
+    ")\n",
+    "\n",
+    "display(test_ui)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9d7e3a4e-b7f9-4ff6-a9de-bb10e49851a0",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.5"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "8ada13db5691318db95c45469f21e1a7752ab0a7ab2e35f607cb2aeb67fe81be"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/autoui.ipynb b/docs/autoui.ipynb
index 114c0217..010b7d59 100644
--- a/docs/autoui.ipynb
+++ b/docs/autoui.ipynb
@@ -5,13 +5,16 @@
    "execution_count": 1,
    "id": "7ba8c00d",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": [
-     "hide-input"
+     "remove-cell"
     ]
    },
    "outputs": [],
    "source": [
-    "# %run ../src/ipyautoui/_dev_sys_path_append.py\n",
     "%run __init__.py\n",
     "%load_ext lab_black"
    ]
@@ -39,6 +42,10 @@
    "execution_count": 2,
    "id": "c8630081",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": []
    },
    "outputs": [],
@@ -93,12 +100,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "0c6d2681401949ceb0ae6eba55edd69d",
+       "model_id": "32d97fb0ac0d4cfcbbde76972186e761",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=False, button_style='success', disabled=True, icon…"
+       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=True, button_style='danger', disabled=True, icon='…"
       ]
      },
      "metadata": {},
@@ -111,12 +118,19 @@
     "import pathlib\n",
     "\n",
     "\n",
-    "class AutoUiExample(BaseModel):\n",
+    "class Sub(BaseModel):\n",
+    "    a: str = \"a\"\n",
+    "    b: int = 1\n",
+    "\n",
+    "\n",
+    "class Example(BaseModel):\n",
     "    text: str = Field(default=\"Test\", description=\"This description is very important\")\n",
+    "    inty: int = 1\n",
+    "    sub: Sub\n",
     "\n",
     "\n",
     "data = {\"text\": \"this is a value\"}\n",
-    "ui = AutoUi(schema=AutoUiExample, path=pathlib.Path(\"test.ui.json\"), show_savebuttonbar=False)\n",
+    "ui = AutoUi(schema=Example, path=pathlib.Path(\"test.ui.json\"), show_savebuttonbar=False)\n",
     "display(ui)"
    ]
   },
@@ -186,12 +200,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "4cab6d1d6ebb43d5b920340d5cfb30cc",
+       "model_id": "ccf8e2bbc0534517b0aabb5e25ebfdc6",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "AutoRenderer(children=(SaveButtonBar(children=(ToggleButton(value=False, button_style='success', disabled=True…"
+       "AutoRenderer(children=(SaveButtonBar(children=(ToggleButton(value=True, button_style='danger', disabled=True, …"
       ]
      },
      "metadata": {},
@@ -208,6 +222,7 @@
     "def test_action():\n",
     "    print(\"done\")\n",
     "\n",
+    "\n",
     "ui_simple.savebuttonbar.fns_onsave_add_action(test_action)\n",
     "ui_simple.show_savebuttonbar = True\n",
     "display(ui_simple)"
@@ -215,7 +230,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 8,
    "id": "1d546f3e-1ce8-413e-96c5-50e3cfc3de49",
    "metadata": {},
    "outputs": [],
@@ -223,6 +238,7 @@
     "import typing as ty\n",
     "import ipywidgets as w\n",
     "\n",
+    "\n",
     "class DataFrameCols(BaseModel):\n",
     "    string: str = Field(\"string\", aui_column_width=100)\n",
     "    integer: int = Field(1, aui_column_width=80)\n",
@@ -238,19 +254,19 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 9,
    "id": "01ff66c3-36e3-42b5-a0b1-942991b84b35",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "7d6e8e68091b4b92a4058c95c59e2ffd",
+       "model_id": "b4d7bc3dc5564192afb76e63b53fb7bb",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=False, button_style='success', disabled=True, icon…"
+       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=True, button_style='danger', disabled=True, icon='…"
       ]
      },
      "metadata": {},
@@ -264,7 +280,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 10,
    "id": "213647db-a6ee-42e5-8afa-19a5336fe070",
    "metadata": {},
    "outputs": [],
@@ -287,42 +303,117 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 11,
    "id": "128c4dcd-33f0-4ea6-811b-577fb99a729d",
    "metadata": {
     "tags": []
    },
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "FileUploadToDir still needs fixing following updates to `iterable.py`. DO NOT USE.\n",
+      "/home/jovyan/micromamba/envs/ipyautoui-dev/lib/python3.11/site-packages/pydantic/_internal/_config.py:317: UserWarning: Valid config keys have changed in V2:\n",
+      "* 'schema_extra' has been renamed to 'json_schema_extra'\n",
+      "  warnings.warn(message, UserWarning)\n"
+     ]
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "52f1cb5e768c4bc5bfba0b50328b32c3",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=True, button_style='danger', disabled=True, icon='…"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
     "from pydantic import BaseModel, Field\n",
     "from ipyautoui.custom.fileupload import AutoUploadPaths\n",
     "from ipyautoui.autoobject import AutoObject\n",
     "import pathlib\n",
     "\n",
+    "\n",
     "class Test(BaseModel):\n",
-    "    paths: list[pathlib.Path] = Field(autoui=\"ipyautoui.custom.fileupload.AutoUploadPaths\")\n",
+    "    paths: list[pathlib.Path] = Field(\n",
+    "        autoui=\"ipyautoui.custom.fileupload.AutoUploadPaths\"\n",
+    "    )\n",
     "    a: str\n",
     "\n",
     "    class Config:\n",
     "        schema_extra = {\n",
-    "            'nested_widgets': ['ipyautoui.custom.fileupload.AutoUploadPaths']\n",
+    "            \"nested_widgets\": [\"ipyautoui.custom.fileupload.AutoUploadPaths\"]\n",
     "        }\n",
+    "\n",
+    "\n",
     "aui = AutoUi(Test)  # , nested_widgets=[AutoUploadPaths]\n",
     "aui"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "29b39d9c-2a35-4c05-894c-3943b9166ca5",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "2534c2c57173404c801d071cb60dd9f6",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=True, button_style='danger', disabled=True, icon='…"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from ipyautoui import AutoUi\n",
+    "from ipyautoui.demo_schemas import NestedEditableGrid\n",
+    "\n",
+    "ui = AutoUi(NestedEditableGrid)\n",
+    "ui"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6db190e6-2c5c-45a3-b014-c9522ca8f5a9",
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3.9 (XPython)",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
-   "name": "xpython"
+   "name": "python3"
   },
   "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
    "file_extension": ".py",
    "mimetype": "text/x-python",
    "name": "python",
-   "version": "3.9.16"
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.5"
   },
   "vscode": {
    "interpreter": {
diff --git a/docs/autovuetify.ipynb b/docs/autovuetify.ipynb
index d8839963..13f140e2 100644
--- a/docs/autovuetify.ipynb
+++ b/docs/autovuetify.ipynb
@@ -5,13 +5,17 @@
    "execution_count": 1,
    "id": "7ba8c00d",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": [
      "remove-input"
     ]
    },
    "outputs": [],
    "source": [
-    "%run ../src/ipyautoui/__init__.py\n",
+    "%run __init__.py\n",
     "%load_ext lab_black"
    ]
   },
@@ -28,7 +32,13 @@
   {
    "cell_type": "markdown",
    "id": "aa67ab0e",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "source": [
     "`AutoVjsf` works in exactly the same way as `AutoUi`, but instead of using `ipywidgets` to render the JSON schema it uses [ipyvuetify](https://github.com/widgetti/ipyvuetify) and [vuetify-jsonschema-form](https://github.com/koumoul-dev/vuetify-jsonschema-form). \n",
     "\n"
@@ -37,7 +47,13 @@
   {
    "cell_type": "markdown",
    "id": "79c6f3ec-327d-423f-b244-872d5b1b4b15",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "source": [
     ":::{admonition} [vuetify-jsonschema-form documentation](https://koumoul-dev.github.io/vuetify-jsonschema-form/latest/) is awesome! \n",
     ":class: tip\n",
@@ -52,6 +68,10 @@
    "execution_count": 2,
    "id": "d5f79a9d-3ff6-40d6-bcb3-ca7258575662",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": [
      "hide-input"
     ]
@@ -64,7 +84,7 @@
        "        <iframe\n",
        "            width=\"600\"\n",
        "            height=\"500\"\n",
-       "            src=\"https://videos.koumoul.com/videos/embed/dd75bf3b-8830-4296-be8b-ec2286b76da3?sandbox=allow-same-origin+allow-scripts+allow-popups&frameborder=0\"\n",
+       "            src=\"https://videos.koumoul.com/videos/embed/29d12ba2-f694-4659-8027-e9386692d8b5?sandbox=allow-same-origin+allow-scripts+allow-popups&frameborder=0\"\n",
        "            frameborder=\"0\"\n",
        "            allowfullscreen\n",
        "            \n",
@@ -72,7 +92,7 @@
        "        "
       ],
       "text/plain": [
-       "<IPython.lib.display.IFrame at 0x7f839077e4f0>"
+       "<IPython.lib.display.IFrame at 0x7f86343a0e90>"
       ]
      },
      "execution_count": 2,
@@ -88,36 +108,35 @@
     "    height=\"500\",\n",
     "    sandbox=\"allow-same-origin allow-scripts allow-popups\",\n",
     "    frameborder=\"0\",\n",
-    "    src=\"https://videos.koumoul.com/videos/embed/dd75bf3b-8830-4296-be8b-ec2286b76da3\",\n",
+    "    src=\"https://videos.koumoul.com/videos/embed/29d12ba2-f694-4659-8027-e9386692d8b5\",\n",
     ")"
    ]
   },
   {
    "cell_type": "markdown",
    "id": "b5dd1ae4-6a9e-4409-bea5-0fde7e8e97d5",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "source": [
     "```{note} vjsf uses __\"-\"__ in the schema keys for specifying formatting (e.g. \"x-display\")...\n",
     "when you're using pydantic to make the schema, __\"-\"__ cannot be used field names, use __\"_\"__ instead (e.g. \"x_display\") and AutoVjsf does the conversion. \n",
     "```"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "id": "3de4e57c-36b0-4ac9-a2e1-650bd7ebdd0c",
-   "metadata": {},
-   "source": [
-    "```{warning} the code below won't output correctly in HTML \n",
-    "Try running with Binder! \n",
-    "TODO: make it work with binder\n",
-    "```"
-   ]
-  },
   {
    "cell_type": "code",
    "execution_count": 3,
    "id": "c8630081",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": []
    },
    "outputs": [],
@@ -155,30 +174,25 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 4,
    "id": "b0cb559e",
    "metadata": {
     "tags": []
    },
    "outputs": [
     {
-     "ename": "TraitError",
-     "evalue": "The 'fns_onsave' trait of an AutoVjsf instance expected a list, not the NoneType None.",
-     "output_type": "error",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mTraitError\u001b[0m                                Traceback (most recent call last)",
-      "Cell \u001b[0;32mIn[6], line 8\u001b[0m\n\u001b[1;32m      5\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpathlib\u001b[39;00m\n\u001b[1;32m      7\u001b[0m value \u001b[38;5;241m=\u001b[39m {\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtext\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mthis is a value\u001b[39m\u001b[38;5;124m\"\u001b[39m}\n\u001b[0;32m----> 8\u001b[0m ui \u001b[38;5;241m=\u001b[39m \u001b[43mAutoVjsf\u001b[49m\u001b[43m(\u001b[49m\u001b[43mschema\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mAutoUiExample\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mvalue\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpathlib\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mPath\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtest.json\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m      9\u001b[0m display(ui)\n",
-      "File \u001b[0;32m~/ipyautoui/src/ipyautoui/autovjsf.py:109\u001b[0m, in \u001b[0;36mAutoVjsf.__init__\u001b[0;34m(self, schema, value, path, fns_onsave, show_raw, show_title)\u001b[0m\n\u001b[1;32m    107\u001b[0m value \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_value(value, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpath)\n\u001b[1;32m    108\u001b[0m \u001b[38;5;66;03m# list of actions to be called on save\u001b[39;00m\n\u001b[0;32m--> 109\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfns_onsave \u001b[38;5;241m=\u001b[39m fns_onsave\n\u001b[1;32m    110\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m value \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m    111\u001b[0m     \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mvui \u001b[38;5;241m=\u001b[39m Vjsf(schema\u001b[38;5;241m=\u001b[39mschema)\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:729\u001b[0m, in \u001b[0;36mTraitType.__set__\u001b[0;34m(self, obj, value)\u001b[0m\n\u001b[1;32m    727\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m TraitError(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mThe \u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m trait is read-only.\u001b[39m\u001b[38;5;124m'\u001b[39m \u001b[38;5;241m%\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mname)\n\u001b[1;32m    728\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 729\u001b[0m     \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mset\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:3007\u001b[0m, in \u001b[0;36mList.set\u001b[0;34m(self, obj, value)\u001b[0m\n\u001b[1;32m   3005\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mset(obj, [value])\n\u001b[1;32m   3006\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 3007\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mset\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:703\u001b[0m, in \u001b[0;36mTraitType.set\u001b[0;34m(self, obj, value)\u001b[0m\n\u001b[1;32m    702\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mset\u001b[39m(\u001b[38;5;28mself\u001b[39m, obj, value):\n\u001b[0;32m--> 703\u001b[0m     new_value \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_validate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m    704\u001b[0m     \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m    705\u001b[0m         old_value \u001b[38;5;241m=\u001b[39m obj\u001b[38;5;241m.\u001b[39m_trait_values[\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mname]\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:735\u001b[0m, in \u001b[0;36mTraitType._validate\u001b[0;34m(self, obj, value)\u001b[0m\n\u001b[1;32m    733\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m value\n\u001b[1;32m    734\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mvalidate\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m--> 735\u001b[0m     value \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m    736\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m obj\u001b[38;5;241m.\u001b[39m_cross_validation_lock \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mFalse\u001b[39;00m:\n\u001b[1;32m    737\u001b[0m     value \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_cross_validate(obj, value)\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:2852\u001b[0m, in \u001b[0;36mContainer.validate\u001b[0;34m(self, obj, value)\u001b[0m\n\u001b[1;32m   2850\u001b[0m     \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mklass \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m   2851\u001b[0m     value \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mklass(value)  \u001b[38;5;66;03m# type:ignore[operator]\u001b[39;00m\n\u001b[0;32m-> 2852\u001b[0m value \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   2853\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m value \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m   2854\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m value\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:2147\u001b[0m, in \u001b[0;36mInstance.validate\u001b[0;34m(self, obj, value)\u001b[0m\n\u001b[1;32m   2145\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m value\n\u001b[1;32m   2146\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 2147\u001b[0m     \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43merror\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m)\u001b[49m\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/traitlets/traitlets.py:841\u001b[0m, in \u001b[0;36mTraitType.error\u001b[0;34m(self, obj, value, error, info)\u001b[0m\n\u001b[1;32m    835\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m    836\u001b[0m     e \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m trait expected \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m, not \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m.\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\n\u001b[1;32m    837\u001b[0m         \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mname,\n\u001b[1;32m    838\u001b[0m         \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39minfo(),\n\u001b[1;32m    839\u001b[0m         describe(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mthe\u001b[39m\u001b[38;5;124m\"\u001b[39m, value),\n\u001b[1;32m    840\u001b[0m     )\n\u001b[0;32m--> 841\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m TraitError(e)\n",
-      "\u001b[0;31mTraitError\u001b[0m: The 'fns_onsave' trait of an AutoVjsf instance expected a list, not the NoneType None."
-     ]
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a7de6fb0d1654c2e95c2891e94415fb4",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "AutoVjsf(children=(SaveButtonBar(children=(ToggleButton(value=False, button_style='success', disabled=True, ic…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
     }
    ],
    "source": [
@@ -186,6 +200,7 @@
     "class AutoUiExample(BaseModel):\n",
     "    text: str = Field(default=\"Test\", description=\"This description is very important\")\n",
     "\n",
+    "\n",
     "import pathlib\n",
     "\n",
     "value = {\"text\": \"this is a value\"}\n",
@@ -213,7 +228,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 5,
    "id": "ce2e69e0",
    "metadata": {},
    "outputs": [
@@ -234,19 +249,32 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 6,
    "id": "b4c3a453",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "pydantic validation failed\n"
+     ]
+    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "638a2f1ba51842d1ae807436547fde98",
+       "model_id": "053235a48ace4e5bba351f9a042113de",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "AutoRenderer(children=(VBox(children=(HBox(children=(SaveButtonBar(children=(ToggleButton(value=False, button_…"
+       "AutoRenderer(children=(SaveButtonBar(children=(ToggleButton(value=False, button_style='success', disabled=True…"
       ]
      },
      "metadata": {},
@@ -274,7 +302,13 @@
   {
    "cell_type": "markdown",
    "id": "a59a323d",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "source": [
     "Let's look at a complete pydantic model producing all of the possible widgets. \n",
     "within `import ipyautoui.demo_schemas` there is a class called `CoreIpywidgets` that outlines what is possible. \n",
@@ -283,84 +317,37 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
-   "id": "11f51204",
+   "execution_count": 7,
+   "id": "7d401137-1716-444c-ac97-6afe4289d092",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": []
    },
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "1563fdab8fb64d348f5d55dfef5a6d1f",
+       "model_id": "42f15a0bd8624d35addfcf5607971857",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "Tab(children=(AutoRenderer(children=(VBox(children=(HBox(children=(SaveButtonBar(children=(ToggleButton(value=…"
+       "AutoVjsf(children=(SaveButtonBar(children=(ToggleButton(value=False, button_style='success', disabled=True, ic…"
       ]
      },
+     "execution_count": 7,
      "metadata": {},
-     "output_type": "display_data"
+     "output_type": "execute_result"
     }
    ],
    "source": [
-    "from ipyautoui.demo_schemas import CoreIpywidgets  # the schema shown in the file above\n",
-    "import ipywidgets as w\n",
-    "from ipyautoui._utils import display_python_module\n",
-    "from IPython.display import Markdown\n",
-    "\n",
-    "DIR_EXAMPLE = pathlib.Path(\".\")\n",
-    "\n",
-    "save_path = DIR_EXAMPLE / \"test.complexaui.json\"\n",
+    "from ipyautoui.demo_schemas import CoreIpywidgets\n",
     "\n",
-    "AutoVjsfRenderer = AutoVjsf.create_autoui_renderer(\n",
-    "    schema=CoreIpywidgets, fn_onsave=lambda: print(\"saved\")\n",
-    ")\n",
-    "ui_complex = AutoVjsfRenderer(path=save_path)\n",
-    "ui_complex.file(path=save_path)\n",
-    "s_py = display_python_module(CoreIpywidgets.__file__, show=False, return_str=True)\n",
-    "out_py = w.Output()\n",
-    "\n",
-    "\n",
-    "s_js = f\"\"\"\n",
-    "```json\n",
-    "{json.dumps(ui_complex.value, indent=4)}\n",
-    "```\n",
-    "\"\"\"\n",
-    "out_json = w.Output()\n",
-    "\n",
-    "\n",
-    "tab = w.Tab([ui_complex, out_py, out_json])\n",
-    "titles = {0: \"AutoUi\", 1: \"pydantic-model\", 2: \"autoui value\"}\n",
-    "\n",
-    "\n",
-    "with out_json:\n",
-    "    display(Markdown(s_js))\n",
-    "with out_py:\n",
-    "    display(Markdown(s_py))\n",
-    "tab._titles = titles\n",
-    "tab\n",
-    "\n",
-    "\n",
-    "# ^ uncommemt-"
+    "AutoVjsf(CoreIpywidgets)"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "dce6a3cb-f3e8-4ac4-986b-880ea5a5378f",
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "565c33d0-56c5-43e9-9216-48fac3448f33",
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
@@ -379,7 +366,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.17"
+   "version": "3.11.5"
   },
   "vscode": {
    "interpreter": {
diff --git a/docs/conf.py b/docs/conf.py
index 20b88ecc..4c8dc204 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -4,20 +4,15 @@
 # re-generate this one.
 ###############################################################################
 author = 'mfcode'
-autoapi_add_toctree_entry = True
-autoapi_dirs = ['../src']
-autoapi_generate_api_docs = True
-autoclass_content = 'both'
-bibtex_bibfiles = ['references.bib']
 comments_config = {'hypothesis': False, 'utterances': False}
 copyright = '2022'
 exclude_patterns = ['**.ipynb_checkpoints', '.DS_Store', 'Thumbs.db', '_build']
-extensions = ['sphinx_togglebutton', 'sphinx_copybutton', 'myst_nb', 'jupyter_book', 'sphinx_thebe', 'sphinx_comments', 'sphinx_external_toc', 'sphinx.ext.intersphinx', 'sphinx_design', 'sphinx_book_theme', 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'autoapi.extension', 'sphinxcontrib.mermaid', 'sphinxcontrib.bibtex', 'sphinx_jupyterbook_latex']
+extensions = ['sphinx_togglebutton', 'sphinx_copybutton', 'myst_nb', 'jupyter_book', 'sphinx_thebe', 'sphinx_comments', 'sphinx_external_toc', 'sphinx.ext.intersphinx', 'sphinx_design', 'sphinx_book_theme', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinxcontrib.mermaid', 'sphinx_jupyterbook_latex']
 external_toc_exclude_missing = False
 external_toc_path = '_toc.yml'
 html_baseurl = ''
-html_favicon = ''
-html_logo = 'logo.png'
+html_favicon = 'images/favicon.png'
+html_logo = 'images/logo.png'
 html_sourcelink_suffix = ''
 html_theme = 'sphinx_book_theme'
 html_theme_options = {'search_bar_text': 'Search this book...', 'launch_buttons': {'notebook_interface': 'classic', 'binderhub_url': '', 'jupyterhub_url': '', 'thebe': False, 'colab_url': ''}, 'path_to_docs': 'docs', 'repository_url': 'https://github.com/jgunstone/ipyautoui', 'repository_branch': 'main', 'extra_footer': '', 'home_page_in_toc': True, 'announcement': '', 'analytics': {'google_analytics_id': ''}, 'use_repository_button': True, 'use_edit_page_button': False, 'use_issues_button': True}
@@ -25,17 +20,6 @@
 latex_engine = 'pdflatex'
 myst_enable_extensions = ['colon_fence', 'dollarmath', 'linkify', 'substitution', 'tasklist']
 myst_url_schemes = ['mailto', 'http', 'https']
-napoleon_google_docstring = True
-napoleon_include_init_with_doc = False
-napoleon_include_private_with_doc = False
-napoleon_include_special_with_doc = True
-napoleon_numpy_docstring = False
-napoleon_use_admonition_for_examples = False
-napoleon_use_admonition_for_notes = False
-napoleon_use_admonition_for_references = False
-napoleon_use_ivar = True
-napoleon_use_param = True
-napoleon_use_rtype = True
 nb_execution_allow_errors = False
 nb_execution_cache_path = ''
 nb_execution_excludepatterns = []
diff --git a/docs/crop_logo.py b/docs/crop_logo.py
index 7f6d2743..b57d04ab 100644
--- a/docs/crop_logo.py
+++ b/docs/crop_logo.py
@@ -6,7 +6,7 @@
 """
 
 from PIL import Image
-
+import shutil
 
 def convertImage(img):
     img = img.convert("RGBA")
@@ -21,8 +21,17 @@ def convertImage(img):
     return img
 
 
-img = Image.open("logo.png")
+img = Image.open("images/logo.png")
 img = convertImage(img)
 img.getbbox()
 im2 = img.crop(img.getbbox())
-im2.save("logo.png")
+im2.save("images/logo.png")
+shutil.copyfile("images/logo.png", "../images/logo.png")
+
+img = Image.open("images/favicon.png")
+img = convertImage(img)
+img.getbbox()
+im2 = img.crop(img.getbbox())
+im2.save("images/favicon.png")
+
+
diff --git a/docs/custom-renderers.ipynb b/docs/custom-renderers.ipynb
index 39961d22..058b3537 100644
--- a/docs/custom-renderers.ipynb
+++ b/docs/custom-renderers.ipynb
@@ -13,7 +13,8 @@
    "id": "bae51901-15d8-4e53-bf6f-f639593712c2",
    "metadata": {},
    "source": [
-    "As described in [AutoVjsf](autovuetify.ipynb), AutoVjsf can be customised "
+    "AutoUi is built using ipywidgets. It can be easily customised and extended... see ruleset in demo... \n",
+    "More TBC"
    ]
   },
   {
diff --git a/docs/demo.ipynb b/docs/demo.ipynb
index 4a3bdd3b..f3c96053 100644
--- a/docs/demo.ipynb
+++ b/docs/demo.ipynb
@@ -3,7 +3,13 @@
   {
    "cell_type": "markdown",
    "id": "c4c141ea-22f2-4d5c-bf7e-c2708a0d39e3",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "source": [
     "# demo"
    ]
@@ -12,18 +18,45 @@
    "cell_type": "code",
    "execution_count": 1,
    "id": "bee6d082-9055-412f-ae7e-a8a3647d4d93",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": [
+     "remove-cell"
+    ]
+   },
    "outputs": [],
    "source": [
     "%run __init__.py\n",
     "%load_ext lab_black"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "9f6d86e9-33e7-4109-8b06-87a66573860d",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "```{warning} the demo requires a python kernel to run. Try on Binder [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/maxfordham/ipyautoui/HEAD)\n",
+    "```"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": 2,
    "id": "8d15de17-8959-458f-84e5-f2d51eabc8bc",
    "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
     "tags": []
    },
    "outputs": [
diff --git a/docs/editgrid.ipynb b/docs/editgrid.ipynb
index 921a20cd..345dba5f 100644
--- a/docs/editgrid.ipynb
+++ b/docs/editgrid.ipynb
@@ -11,12 +11,36 @@
   {
    "cell_type": "code",
    "execution_count": 1,
+   "id": "d033ad47-b6b3-46b1-84ef-8a7a9c40fe18",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": [
+     "remove-cell"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "%run __init__.py"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
    "id": "dcd2cbe9-ec3e-47d2-b41c-391b606cdf42",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "outputs": [],
    "source": [
     "import typing as ty\n",
-    "from pydantic import BaseModel, Field\n",
+    "from pydantic import BaseModel, Field, RootModel\n",
     "from ipyautoui import AutoUi\n",
     "\n",
     "\n",
@@ -29,27 +53,32 @@
     "\n",
     "class TestDataFrame(BaseModel):\n",
     "    \"\"\"a description of TestDataFrame\"\"\"\n",
-    "\n",
-    "    dataframe: ty.List[DataFrameCols] = Field(\n",
-    "        default_factory=lambda: [], format=\"dataframe\"\n",
-    "    )"
+    "    a: str\n",
+    "    df: ty.List[DataFrameCols] = Field([DataFrameCols().model_dump(mode=\"json\")], format=\"dataframe\")\n",
+    "       "
    ]
   },
   {
    "cell_type": "code",
    "execution_count": 3,
    "id": "880ed5c0-6c79-47e4-8f0e-1cfcd74a63f0",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "68aa7eff731d4b8ca3d440ba89fa2d1c",
+       "model_id": "d4691db8f51b4cc28f9511821ee32998",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "AutoUi(children=(VBox(children=(HBox(), HBox(children=(ToggleButton(value=False, icon='code', layout=Layout(di…"
+       "AutoUi(children=(SaveButtonBar(children=(ToggleButton(value=True, button_style='danger', disabled=True, icon='…"
       ]
      },
      "metadata": {},
@@ -63,13 +92,50 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
-   "id": "20410d98-5451-42d6-bba7-601e15d26d72",
+   "execution_count": 8,
+   "id": "5c4ac8e3-0518-40cf-92e7-0a7845c6bc96",
    "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a4faf1322c844ef891df466d30c623d6",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "EditGrid(children=(HTML(value=''), CrudButtonBar(children=(ToggleButton(value=False, button_style='success', i…"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from ipyautoui.custom.editgrid import EditGrid\n",
+    "class TestDataFrame(RootModel):\n",
+    "    \"\"\"a description of TestDataFrame\"\"\"\n",
+    "\n",
+    "    root: ty.List[DataFrameCols] = Field(format=\"dataframe\")\n",
+    "auto_grid = EditGrid(schema=TestDataFrame)\n",
+    "auto_grid"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "20410d98-5451-42d6-bba7-601e15d26d72",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "outputs": [],
    "source": [
-    "auto_grid.value = {\n",
-    "    \"dataframe\": [\n",
+    "auto_grid.value =  [\n",
     "        {\n",
     "            \"string\": \"important string\",\n",
     "            \"integer\": 1,\n",
@@ -80,9 +146,61 @@
     "        {\"string\": \"evening\", \"integer\": 5, \"floater\": 3.14, \"something_else\": 235},\n",
     "        {\"string\": \"morning\", \"integer\": 5, \"floater\": 3.14, \"something_else\": 12},\n",
     "        {\"string\": \"number\", \"integer\": 3, \"floater\": 3.14, \"something_else\": 123},\n",
-    "    ]\n",
-    "}"
+    "    ]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "3426e3da-306c-4a28-a848-a86fe7aa06e0",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "<class '__main__.Sub'>\n",
+      "<class '__main__.Sub'>\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Row Validation (WIP)\n",
+    "\n",
+    "from pydantic import RootModel\n",
+    "\n",
+    "\n",
+    "class Sub(BaseModel):\n",
+    "    a: str = \"a\"\n",
+    "    b: int = 1\n",
+    "\n",
+    "\n",
+    "class Table(RootModel):\n",
+    "    root: list[Sub]\n",
+    "\n",
+    "\n",
+    "class NestedTable(BaseModel):\n",
+    "    table: list[Sub]\n",
+    "\n",
+    "\n",
+    "# for simple root tables it is simple to extract the pydantic model for the row\n",
+    "RowSchema = Table.__pydantic_core_schema__[\"schema\"][\"items_schema\"][\"cls\"]\n",
+    "print(RowSchema)\n",
+    "\n",
+    "# more difficult for nested rows\n",
+    "NestedRowSchema = NestedTable.__pydantic_core_schema__[\"schema\"][\"fields\"][\"table\"][\n",
+    "    \"schema\"\n",
+    "][\"items_schema\"][\"cls\"]\n",
+    "print(NestedRowSchema)"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6e3efbd4-602c-42ee-95ae-72e07cb25b17",
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {
@@ -101,7 +219,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.17"
+   "version": "3.11.5"
   }
  },
  "nbformat": 4,
diff --git a/docs/environment.yml b/docs/environment.yml
index 9db43fa7..443a5407 100644
--- a/docs/environment.yml
+++ b/docs/environment.yml
@@ -7,15 +7,13 @@ name: ipyautoui-docs
 # -
 # - needs everything that ipyautoui needs + everything for docs
 # - in future when packaged could make ipyautoui a dependency
+#   - in CI just make sure packaging happens first
 channels:
-  - defaults
   - conda-forge
 dependencies:
   - python>=3.7
-  - numpy
   - pandas
   - jupyterlab>4
-  - xeus-python
   - ipywidgets
   - ipyfilechooser
   - ipydatagrid
@@ -28,6 +26,7 @@ dependencies:
   - nodejs
   - altair
   - pydantic>2
+  - pydantic-settings
   - stringcase
   - pyyaml
   - pillow
@@ -47,11 +46,7 @@ dependencies:
   - pytest
   - pip
   - pip:
-      - maplocal==0.1.1
-      - pytest-sphinx
-      - jsonref
-      - pydantic-extra-types
-#  - xlsxwriter
-#  - xlsxtemplater
-#  - mf_file_utilities
-#  - mfom
+    - maplocal==0.1.1
+    - pytest-sphinx
+    - jsonref
+    - pydantic-extra-types
diff --git a/docs/images/demo.png b/docs/images/demo.png
new file mode 100644
index 00000000..774f0e5c
Binary files /dev/null and b/docs/images/demo.png differ
diff --git a/docs/logo.3dm b/docs/images/favicon.3dm
similarity index 91%
rename from docs/logo.3dm
rename to docs/images/favicon.3dm
index f051c4c8..56110613 100644
Binary files a/docs/logo.3dm and b/docs/images/favicon.3dm differ
diff --git a/docs/images/favicon.png b/docs/images/favicon.png
new file mode 100644
index 00000000..d3a42b29
Binary files /dev/null and b/docs/images/favicon.png differ
diff --git a/docs/images/logo.3dm b/docs/images/logo.3dm
new file mode 100644
index 00000000..e2c6dff1
Binary files /dev/null and b/docs/images/logo.3dm differ
diff --git a/docs/logo.png b/docs/images/logo.png
similarity index 100%
rename from docs/logo.png
rename to docs/images/logo.png
diff --git a/docs/images/logo.svg b/docs/images/logo.svg
new file mode 100644
index 00000000..67ae1f76
--- /dev/null
+++ b/docs/images/logo.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" width="8.4pt" height="8.4pt" viewBox="0 0 8.4 8.4" overflow="visible" xmlns="http://www.w3.org/2000/svg" />
\ No newline at end of file
diff --git a/docs/validation.ipynb b/docs/validation.ipynb
index 31a3eba9..bfcba923 100644
--- a/docs/validation.ipynb
+++ b/docs/validation.ipynb
@@ -48,7 +48,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 1,
    "id": "3a6be2ea-a222-4b35-b4ca-4e391781e3c2",
    "metadata": {
     "tags": []
@@ -61,8 +61,8 @@
      "traceback": [
       "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
       "\u001b[0;31mValidationError\u001b[0m                           Traceback (most recent call last)",
-      "Cell \u001b[0;32mIn[3], line 15\u001b[0m\n\u001b[1;32m     12\u001b[0m \u001b[38;5;66;03m# If no exception is raised by validate(), the instance is valid.\u001b[39;00m\n\u001b[1;32m     13\u001b[0m validate(instance\u001b[38;5;241m=\u001b[39m{\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mname\u001b[39m\u001b[38;5;124m\"\u001b[39m : \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEggs\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprice\u001b[39m\u001b[38;5;124m\"\u001b[39m : \u001b[38;5;241m34.99\u001b[39m}, schema\u001b[38;5;241m=\u001b[39mschema)\n\u001b[0;32m---> 15\u001b[0m \u001b[43mvalidate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m     16\u001b[0m \u001b[43m    \u001b[49m\u001b[43minstance\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m{\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mname\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m \u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mEggs\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mprice\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m \u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mInvalid\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mschema\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mschema\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m     17\u001b[0m \u001b[43m)\u001b[49m\n",
-      "File \u001b[0;32m~/mambaforge/envs/ipyautoui-dev/lib/python3.9/site-packages/jsonschema/validators.py:1121\u001b[0m, in \u001b[0;36mvalidate\u001b[0;34m(instance, schema, cls, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1119\u001b[0m error \u001b[38;5;241m=\u001b[39m exceptions\u001b[38;5;241m.\u001b[39mbest_match(validator\u001b[38;5;241m.\u001b[39miter_errors(instance))\n\u001b[1;32m   1120\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m error \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m-> 1121\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m error\n",
+      "Cell \u001b[0;32mIn[1], line 15\u001b[0m\n\u001b[1;32m     12\u001b[0m \u001b[38;5;66;03m# If no exception is raised by validate(), the instance is valid.\u001b[39;00m\n\u001b[1;32m     13\u001b[0m validate(instance\u001b[38;5;241m=\u001b[39m{\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mname\u001b[39m\u001b[38;5;124m\"\u001b[39m : \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEggs\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprice\u001b[39m\u001b[38;5;124m\"\u001b[39m : \u001b[38;5;241m34.99\u001b[39m}, schema\u001b[38;5;241m=\u001b[39mschema)\n\u001b[0;32m---> 15\u001b[0m validate(\n\u001b[1;32m     16\u001b[0m     instance\u001b[38;5;241m=\u001b[39m{\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mname\u001b[39m\u001b[38;5;124m\"\u001b[39m : \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mEggs\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprice\u001b[39m\u001b[38;5;124m\"\u001b[39m : \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid\u001b[39m\u001b[38;5;124m\"\u001b[39m}, schema\u001b[38;5;241m=\u001b[39mschema,\n\u001b[1;32m     17\u001b[0m )\n",
+      "File \u001b[0;32m~/micromamba/envs/ipyautoui-dev/lib/python3.11/site-packages/jsonschema/validators.py:1121\u001b[0m, in \u001b[0;36mvalidate\u001b[0;34m(instance, schema, cls, *args, **kwargs)\u001b[0m\n\u001b[1;32m   1119\u001b[0m error \u001b[38;5;241m=\u001b[39m exceptions\u001b[38;5;241m.\u001b[39mbest_match(validator\u001b[38;5;241m.\u001b[39miter_errors(instance))\n\u001b[1;32m   1120\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m error \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m-> 1121\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m error\n",
       "\u001b[0;31mValidationError\u001b[0m: 'Invalid' is not of type 'number'\n\nFailed validating 'type' in schema['properties']['price']:\n    {'type': 'number'}\n\nOn instance['price']:\n    'Invalid'"
      ]
     }
@@ -94,259 +94,36 @@
    "metadata": {
     "tags": []
    },
-   "outputs": [],
-   "source": [
-    "schema = {\n",
-    "    \"items\": {\n",
-    "        \"anyOf\": [{\"type\": \"string\", \"maxLength\": 2}, {\"type\": \"integer\", \"minimum\": 5}]\n",
-    "    }\n",
-    "}\n",
-    "instance = [{}, 3, \"foo\"]\n",
-    "v = Draft202012Validator(schema)\n",
-    "errors = sorted(v.iter_errors(instance), key=lambda e: e.path)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "id": "e39f8063-acb2-43aa-86a9-4a08d271550e",
-   "metadata": {
-    "tags": []
-   },
    "outputs": [
     {
      "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "c52aa3b1d93f41bd880c8e150c5441cf",
-       "version_major": 2,
-       "version_minor": 0
-      },
       "text/plain": [
-       "HBox(children=(Button(disabled=True, icon='check', layout=Layout(height='25px', width='44px'), style=ButtonSty…"
+       "[<ValidationError: '{} is not valid under any of the given schemas'>,\n",
+       " <ValidationError: '3 is not valid under any of the given schemas'>,\n",
+       " <ValidationError: \"'foo' is not valid under any of the given schemas\">]"
       ]
      },
-     "execution_count": 6,
+     "execution_count": 4,
      "metadata": {},
      "output_type": "execute_result"
     }
    ],
-   "source": [
-    "from ipyautoui.constants import TRUE_BUTTON_KWARGS, BUTTON_WIDTH_MIN, BUTTON_HEIGHT_MIN\n",
-    "import ipywidgets as w\n",
-    "\n",
-    "BLANK_BUTTON_KWARGS = dict(\n",
-    "    icon=\"times\",\n",
-    "    style={\"button_color\": \"red\"},\n",
-    "    layout={\"width\": BUTTON_WIDTH_MIN, \"height\": BUTTON_HEIGHT_MIN},\n",
-    "    disabled=True,\n",
-    ")\n",
-    "\n",
-    "BLANK_BUTTON_KWARGS = dict(\n",
-    "    icon=\"check\",\n",
-    "    style={\"button_color\": \"white\"},\n",
-    "    layout={\"width\": BUTTON_WIDTH_MIN, \"height\": BUTTON_HEIGHT_MIN},\n",
-    "    disabled=True,\n",
-    ")\n",
-    "w.HBox([w.Button(**BLANK_BUTTON_KWARGS), w.Text(\"asdfasdf\")])"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 7,
-   "id": "97a51b55-ddea-4af0-a37f-59f99a2553e0",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "[0, 'type'], {} is not of type 'string'\n",
-      "[1, 'type'], {} is not of type 'integer'\n",
-      "[0, 'type'], 3 is not of type 'string'\n",
-      "[1, 'minimum'], 3 is less than the minimum of 5\n",
-      "[0, 'maxLength'], 'foo' is too long\n",
-      "[1, 'type'], 'foo' is not of type 'integer'\n"
-     ]
-    }
-   ],
-   "source": [
-    "for error in errors:\n",
-    "    for suberror in sorted(error.context, key=lambda e: e.schema_path):\n",
-    "        print(list(suberror.schema_path), suberror.message, sep=\", \")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 8,
-   "id": "acb5746f-6be6-4630-a8e7-0ded0bd00ff2",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "'spam' is not of type 'number'\n",
-      "'spam' is not one of [1, 2, 3]\n",
-      "['spam', 2] is too short\n"
-     ]
-    }
-   ],
    "source": [
     "schema = {\n",
-    "    \"type\": \"array\",\n",
-    "    \"items\": {\"type\": \"number\", \"enum\": [1, 2, 3]},\n",
-    "    \"minItems\": 3,\n",
+    "    \"items\": {\n",
+    "        \"anyOf\": [{\"type\": \"string\", \"maxLength\": 2}, {\"type\": \"integer\", \"minimum\": 5}]\n",
+    "    }\n",
     "}\n",
-    "instance = [\"spam\", 2]\n",
-    "\n",
+    "instance = [{}, 3, \"foo\"]\n",
     "v = Draft202012Validator(schema)\n",
-    "for error in sorted(v.iter_errors([\"spam\", 2]), key=str):\n",
-    "    print(error.message)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "id": "ff3af45b-6046-4357-adfc-80d1bf28be34",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [],
-   "source": [
-    "tree = ErrorTree(v.iter_errors(instance))"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 17,
-   "id": "4081eded-f153-4846-a14d-2dd91a5d660b",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "username='scolvin' password1='zxcvbn' password2='zxcvbn'\n",
-      "1 validation error for UserModel\n",
-      "  Value error, passwords do not match [type=value_error, input_value={'username': 'scolvin', '... 'password2': 'zxcvbn2'}, input_type=dict]\n",
-      "    For further information visit https://errors.pydantic.dev/2.1/v/value_error\n",
-      "1 validation error for UserModel\n",
-      "  Assertion failed, card_number should not be included [type=assertion_error, input_value={'username': 'scolvin', '..., 'card_number': '1234'}, input_type=dict]\n",
-      "    For further information visit https://errors.pydantic.dev/2.1/v/assertion_error\n"
-     ]
-    }
-   ],
-   "source": [
-    "# REF: https://docs.pydantic.dev/latest/concepts/validators/#model-validators\n",
-    "\n",
-    "from typing import Any\n",
-    "from pydantic import BaseModel, ValidationError, model_validator\n",
-    "\n",
-    "\n",
-    "class UserModel(BaseModel):\n",
-    "    username: str\n",
-    "    password1: str\n",
-    "    password2: str\n",
-    "\n",
-    "    @model_validator(mode=\"before\")\n",
-    "    @classmethod\n",
-    "    def check_card_number_omitted(cls, data: Any) -> Any:\n",
-    "        if isinstance(data, dict):\n",
-    "            assert \"card_number\" not in data, \"card_number should not be included\"\n",
-    "        return data\n",
-    "\n",
-    "    @model_validator(mode=\"after\")\n",
-    "    def check_passwords_match(self) -> \"UserModel\":\n",
-    "        pw1 = self.password1\n",
-    "        pw2 = self.password2\n",
-    "        if pw1 is not None and pw2 is not None and pw1 != pw2:\n",
-    "            raise ValueError(\"passwords do not match\")\n",
-    "        return self\n",
-    "\n",
-    "\n",
-    "print(UserModel(username=\"scolvin\", password1=\"zxcvbn\", password2=\"zxcvbn\"))\n",
-    "# > username='scolvin' password1='zxcvbn' password2='zxcvbn'\n",
-    "try:\n",
-    "    UserModel(username=\"scolvin\", password1=\"zxcvbn\", password2=\"zxcvbn2\")\n",
-    "except ValidationError as e:\n",
-    "    print(e)\n",
-    "    \"\"\"\n",
-    "    1 validation error for UserModel\n",
-    "      Value error, passwords do not match [type=value_error, input_value={'username': 'scolvin', '... 'password2': 'zxcvbn2'}, input_type=dict]\n",
-    "    \"\"\"\n",
-    "\n",
-    "try:\n",
-    "    UserModel(\n",
-    "        username=\"scolvin\",\n",
-    "        password1=\"zxcvbn\",\n",
-    "        password2=\"zxcvbn\",\n",
-    "        card_number=\"1234\",\n",
-    "    )\n",
-    "except ValidationError as e:\n",
-    "    print(e)\n",
-    "    \"\"\"\n",
-    "    1 validation error for UserModel\n",
-    "      Assertion failed, card_number should not be included\n",
-    "    assert 'card_number' not in {'card_number': '1234', 'password1': 'zxcvbn', 'password2': 'zxcvbn', 'username': 'scolvin'} [type=assertion_error, input_value={'username': 'scolvin', '..., 'card_number': '1234'}, input_type=dict]\n",
-    "    \"\"\""
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 18,
-   "id": "2ade04b5-4d59-417f-b355-25b25e4c7fae",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "c25751b0679149d6a15975148ba21392",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "VBox(children=(HBox(children=(Text(value=''), Label(value='schedule name in plain english (with spaces)'))), H…"
-      ]
-     },
-     "execution_count": 18,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "import stringcase as sc\n",
-    "\n",
-    "name = w.Text()\n",
-    "name_ = w.Text(disabled=True)\n",
-    "vbox = w.VBox(\n",
-    "    [\n",
-    "        w.HBox([name, w.Label(\"schedule name in plain english (with spaces)\")]),\n",
-    "        w.HBox([name_, w.Label(\"schedule name for machines\")]),\n",
-    "    ]\n",
-    ")\n",
-    "\n",
-    "\n",
-    "def on_name_change(on_change):\n",
-    "    name_.value = sc.pascalcase(sc.snakecase(name.value.lower())).strip(\"_\")\n",
-    "\n",
-    "\n",
-    "name.observe(on_name_change, \"value\")\n",
-    "\n",
-    "vbox"
+    "errors = sorted(v.iter_errors(instance), key=lambda e: e.path)\n",
+    "errors"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "02c53e29-c7aa-4a31-97b4-c3774ad1b595",
+   "id": "b29c36bb-aba6-49c8-9185-08f876332da7",
    "metadata": {},
    "outputs": [],
    "source": []
@@ -368,7 +145,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.17"
+   "version": "3.11.5"
   }
  },
  "nbformat": 4,
diff --git a/environment.yml b/environment.yml
index 2c023380..27e9684d 100644
--- a/environment.yml
+++ b/environment.yml
@@ -10,6 +10,7 @@ dependencies:
   - numpy
   - pandas
   - jupyterlab>=4.0.0,<5.0.0a0
+  - jupyterlab-unfold
   - jupyterlab-lsp
   - python-lsp-server
   - ipywidgets>8
@@ -36,7 +37,8 @@ dependencies:
   - stringcase
   - pip
   - pip:
-    - maplocal==0.1.1
+    - maplocal==0.2.1
     - pydantic-extra-types
     - pytest-examples
-    - jsonref
\ No newline at end of file
+    - jsonref
+    - -e .
\ No newline at end of file
diff --git a/images/demo.png b/images/demo.png
new file mode 100644
index 00000000..774f0e5c
Binary files /dev/null and b/images/demo.png differ
diff --git a/images/logo.png b/images/logo.png
new file mode 100644
index 00000000..d9e1d473
Binary files /dev/null and b/images/logo.png differ
diff --git a/pyproject.toml b/pyproject.toml
index 0e08f161..37c1a005 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ dependencies = [
     "PyYAML",
     "stringcase",
     "wcmatch",
-    "maplocal==0.1.1",
+    "maplocal==0.2.1",
 ]
 
 [project.urls]
diff --git a/src/ipyautoui/_dev_sys_path_append.py b/src/ipyautoui/_dev_maplocal_params.py
similarity index 54%
rename from src/ipyautoui/_dev_sys_path_append.py
rename to src/ipyautoui/_dev_maplocal_params.py
index 75cf4503..e4c33d2b 100644
--- a/src/ipyautoui/_dev_sys_path_append.py
+++ b/src/ipyautoui/_dev_maplocal_params.py
@@ -1,21 +1,15 @@
 """this is only used when running the python files as jupyter notebooks
-in the dev environment. It is called with a `%run _dev_sys_path_append.py`
+in the dev environment. It is called with a `%run _dev_maplocal_params.py`
 magic command so is not run when the module is imported.
 """
 from dotenv import dotenv_values
 import pathlib
 import os
-import sys
 
 rootdir = pathlib.Path(__file__).parents[2]
 config = dotenv_values(str(rootdir / ".env"))
-config["PYTHONPATH"] = str(rootdir / config["PYTHONPATH"].replace("/",""))
 config["MAPLOCAL_SCRIPT_PATH"] = str(rootdir / config["MAPLOCAL_SCRIPT_PATH"])
 
 for k, v in config.items():
     if v is not None:
-        os.environ[k] = v
-
-sys.path.append(config["PYTHONPATH"])
-# ^ don't know why we have to do this as i think adding to 
-#   to PYTHONPATH should deal with this... but alas...
\ No newline at end of file
+        os.environ[k] = v
\ No newline at end of file
diff --git a/src/ipyautoui/_utils.py b/src/ipyautoui/_utils.py
index 9cb52a64..5a112eeb 100644
--- a/src/ipyautoui/_utils.py
+++ b/src/ipyautoui/_utils.py
@@ -9,7 +9,7 @@
 import ipywidgets as w
 from IPython.display import display, Markdown
 import codecs
-from pydantic import BaseModel, field_validator, Field, FieldValidationInfo
+from pydantic import BaseModel, field_validator, Field, ValidationInfo
 import typing as ty
 import importlib.util
 import inspect
@@ -282,7 +282,7 @@ class PyObj(BaseModel):
 
     @field_validator("module_name")
     @classmethod
-    def _module_name(cls, v, info: FieldValidationInfo):
+    def _module_name(cls, v, info: ValidationInfo):
         return info.data["path"].stem
 
 
@@ -422,3 +422,43 @@ def get_user():
         from getpass import getuser
 
         return getuser()
+
+def type_as_json(value):
+    if isinstance(value, str):
+        return "string"
+    elif isinstance(value, int):
+        return "integer"
+    elif isinstance(value, float):
+        return "number"
+    elif isinstance(value, bool):
+        return "number"
+    elif isinstance(value, list):
+        return "array"
+    elif isinstance(value, dict):
+        return "object" + "-" + "".join(value.keys())
+    elif isinstance(value, None):
+        return "null"
+    else:
+        raise ValueError(
+            "value must be: string, integer, number, array, object or None"
+        )
+        
+def json_as_type(s):
+    if s == "string":
+        return str
+    elif s == "integer":
+        return int
+    elif s == "number":
+        return float
+    elif s == "boolean":
+        return bool
+    elif s == "array":
+        return list
+    elif s == "object":
+        return dict
+    elif s == "null":
+        return None
+    else:
+        raise ValueError(
+            "value must be: string, integer, number, array, object or None"
+        )
\ No newline at end of file
diff --git a/src/ipyautoui/autoanyof.py b/src/ipyautoui/autoanyof.py
index 5430c50b..0ebe0341 100644
--- a/src/ipyautoui/autoanyof.py
+++ b/src/ipyautoui/autoanyof.py
@@ -13,34 +13,13 @@
 #     name: python3
 # ---
 
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 
 import ipywidgets as w
 import traitlets as tr
 import typing as ty
-
-
-# +
-def value_type_as_json(value):
-    if isinstance(value, str):
-        return "string"
-    elif isinstance(value, int):
-        return "integer"
-    elif isinstance(value, float):
-        return "number"
-    elif isinstance(value, bool):
-        return "number"
-    elif isinstance(value, list):
-        return "array"
-    elif isinstance(value, dict):
-        return "object" + "-" + "".join(value.keys())
-    elif isinstance(value, None):
-        return "null"
-    else:
-        raise ValueError(
-            "value must be: string, integer, number, array, object or None"
-        )
+from ipyautoui._utils import type_as_json
 
 
 def get_anyOf_type(l):
@@ -54,8 +33,6 @@ def get_anyOf_type(l):
         raise ValueError(f"could not find widget type:\n{l}")
 
 
-# -
-
 class AnyOf(w.HBox):
     allOf = tr.List(allow_none=True, default_value=None)
     anyOf = tr.List(
@@ -72,7 +49,7 @@ def value(self):
 
     @value.setter
     def value(self, value):
-        t_in = value_type_as_json(value)
+        t_in = type_as_json(value)
         ti = None
         try:
             ti = self.map_type_title[t_in]
@@ -211,3 +188,5 @@ class RootSimple(RootModel):
     ui = AnyOf(**sch)
     display(sch)
     display(ui)
+
+
diff --git a/src/ipyautoui/autobox.py b/src/ipyautoui/autobox.py
index c25be837..d9367b1a 100644
--- a/src/ipyautoui/autobox.py
+++ b/src/ipyautoui/autobox.py
@@ -15,7 +15,7 @@
 
 """create a simple row item for a form. contains some simple automated layout features
 """
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
@@ -72,7 +72,7 @@
     w.VBox([self.html_title, w.HBox([SPACER, self.get_tgl]), self.widget])
 ]
 
-f6 = lambda self: [w.VBox([self.html_title,w.HBox([self.get_tgl ]), self.widget])]
+f6 = lambda self: [w.VBox([self.html_title, w.HBox([self.get_tgl]), self.widget])]
 
 f7 = lambda self: [
     w.VBox(
@@ -110,7 +110,9 @@ class Nest:
     @property
     def get_tgl(self):
         if not hasattr(self, "tgl"):
-            self.tgl = w.ToggleButton(description="show", layout={"width": "300px"})
+            self.tgl = w.ToggleButton(
+                description="show", layout={"width": "300px"}, tooltip="open/close"
+            )
             self._init_controls_Nest()
         return self.tgl
 
@@ -128,7 +130,11 @@ class AutoBox(w.VBox, Nest, TitleDescription):
     nested = tr.Bool(default_value=False)
     align_horizontal = tr.Bool(default_value=True).tag(sync=True)
     hide = tr.Bool(default_value=True).tag(sync=True)
-    widget = tr.Any(default_value=w.ToggleButton(layout={"width": "600px"}))
+    widget = tr.Any(
+        default_value=w.ToggleButton(
+            tooltip="placeholder...", layout={"width": "600px"}
+        )
+    )
     indent = tr.Bool(default_value=False)
 
     @tr.observe("nested")
@@ -160,8 +166,8 @@ def _hide(self, on_change):
 
     def __init__(self, **kwargs):
         self._update_title_description()
-        # with self.hold_trait_notifications():
-        super().__init__(**kwargs)
+        super().__init__(**{k: v for k, v in kwargs.items() if k != "tooltip"})
+        #  ^ tooltip issue: https://github.com/jupyter-widgets/ipywidgets/issues/3860
         self.format_box()
 
     @property
@@ -200,4 +206,20 @@ def format_box(self):
 if __name__ == "__main__":
     (bx.align_horizontal, bx.nested, bx.indent) = False, False, False  # f8
 
+if __name__ == "__main__":
+    di = {
+        "title": "Uniclass System Description",
+        "description": "System Description that matches a code within the Uniclass Ss tables. https://uniclass.thenbs.com/taxon/ss",
+        "tooltip": "Created and used by the Autodesk Classification Manager for Revit",
+    }
+    bx1 = AutoBox(**di)
+    display(bx1)
 
+if __name__ == "__main__":
+    di = {
+        "title": "title",
+        "description": "Description ...",
+        "tooltip": "tooltip ...",
+    }
+    display(w.VBox(**di))
+    #  ^ tooltip issue: https://github.com/jupyter-widgets/ipywidgets/issues/3860
diff --git a/src/ipyautoui/autodisplay.py b/src/ipyautoui/autodisplay.py
index 63f6afa0..ec462920 100644
--- a/src/ipyautoui/autodisplay.py
+++ b/src/ipyautoui/autodisplay.py
@@ -8,7 +8,7 @@
 #       extension: .py
 #       format_name: percent
 #       format_version: '1.3'
-#       jupytext_version: 1.14.0
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
@@ -36,9 +36,8 @@
         display(d)
 
 """
-# %run _dev_sys_path_append.py
+# # %run _dev_maplocal_params.py
 # %load_ext lab_black
-
 # %%
 import pathlib
 import functools
@@ -108,7 +107,7 @@ def get_renderers(
         return dict(DEFAULT_FILE_RENDERERS)
 
 
-from pydantic import field_validator, FieldValidationInfo
+from pydantic import field_validator, ValidationInfo, Field
 
 
 # %%
@@ -116,20 +115,20 @@ class DisplayObjectActions(BaseModel):
     """base object with callables for creating a display object"""
 
     renderers: dict[str, ty.Callable] = dict(DEFAULT_FILE_RENDERERS)
-    path: ty.Union[str, pathlib.Path, HttpUrl, ty.Callable]
-    ext: ty.Optional[str] = None
-    name: ty.Optional[str] = None
-    check_exists: ty.Optional[ty.Callable] = None
-    renderer: ty.Optional[ty.Callable] = None
-    check_date_modified: ty.Optional[ty.Callable] = None
+    path: ty.Optional[ty.Union[str, pathlib.Path, HttpUrl, ty.Callable]]
+    ext: ty.Optional[str] = Field(None, validate_default=True)
+    name: ty.Optional[str] = Field(None, validate_default=True)
+    check_exists: ty.Optional[ty.Callable] = Field(None, validate_default=True)
+    renderer: ty.Optional[ty.Callable] = Field(None, validate_default=True)
+    check_date_modified: ty.Optional[ty.Callable] = Field(None, validate_default=True)
 
     @field_validator("renderer")
     @classmethod
-    def _renderer(cls, v: ty.Callable, info: FieldValidationInfo):
+    def _renderer(cls, v: ty.Callable, info: ValidationInfo):
         if v is None:
             ext = info.data["ext"]
             map_ = info.data["renderers"]
-            if ext in map_.keys():
+            if ext is not None and ext in map_.keys():
                 fn = functools.partial(map_[ext], info.data["path"])
             else:
                 fn = lambda: w.HTML("File renderer not found")
@@ -155,23 +154,30 @@ class DisplayFromPath(DisplayObjectActions):
     @field_validator("path")
     @classmethod
     def _path(cls, v):
-        return pathlib.Path(v)
+        if v is not None:
+            return pathlib.Path(v)
+        else:
+            return v
 
     @field_validator("path_new")
     @classmethod
-    def _path_new(cls, v, info: FieldValidationInfo):
-        return make_new_path(info.data["path"].absolute())
+    def _path_new(cls, v, info: ValidationInfo):
+        p = info.data["path"]
+        if p is not None:
+            return make_new_path(p.absolute())
+        else:
+            return p
 
     @field_validator("name")
     @classmethod
-    def _name(cls, v, info: FieldValidationInfo):
+    def _name(cls, v, info: ValidationInfo):
         if info.data["path"] is not None:
             v = info.data["path"].name
         return v
 
     @field_validator("ext")
     @classmethod
-    def _ext(cls, v, info: FieldValidationInfo):
+    def _ext(cls, v, info: ValidationInfo):
         p = info.data["path"]
         rs = info.data["renderers"]
         if p is not None:
@@ -183,13 +189,16 @@ def _ext(cls, v, info: FieldValidationInfo):
 
     @field_validator("check_exists")
     @classmethod
-    def _check_exists(cls, v, info: FieldValidationInfo):
-        fn = functools.partial(check_exists, info.data["path"])
+    def _check_exists(cls, v, info: ValidationInfo):
+        p = info.data["path"]
+        fn = lambda: None
+        if p is not None:
+            fn = functools.partial(check_exists, p)
         return fn
 
     @field_validator("check_date_modified")
     @classmethod
-    def _check_date_modified(cls, v, info: FieldValidationInfo):
+    def _check_date_modified(cls, v, info: ValidationInfo):
         p = info.data["path"]
         if p is not None:
             return functools.partial(st_mtime_string, p)
@@ -198,7 +207,7 @@ def _check_date_modified(cls, v, info: FieldValidationInfo):
 
     @field_validator("open_file")
     @classmethod
-    def _open_file(cls, v, info: FieldValidationInfo):
+    def _open_file(cls, v, info: ValidationInfo):
         p = info.data["path"]
         if p is not None:
             return functools.partial(open_path, p)
@@ -207,7 +216,7 @@ def _open_file(cls, v, info: FieldValidationInfo):
 
     @field_validator("open_folder")
     @classmethod
-    def _open_folder(cls, v, info: FieldValidationInfo):
+    def _open_folder(cls, v, info: ValidationInfo):
         p = info.data["path"]
         if not p.is_dir():
             p = p.parent
@@ -245,13 +254,13 @@ class DisplayFromRequest(DisplayObjectActions):
 
     @field_validator("check_exists")
     @classmethod
-    def _check_exists(cls, v, info: FieldValidationInfo):
+    def _check_exists(cls, v, info: ValidationInfo):
         fn = functools.partial(url_ok, info.data["path"])
         return fn
 
     @field_validator("name")
     @classmethod
-    def _name(cls, v, info: FieldValidationInfo):
+    def _name(cls, v, info: ValidationInfo):
         return info.data["path"].path
 
 
@@ -274,13 +283,13 @@ class DisplayFromCallable(DisplayObjectActions):
 
     @field_validator("check_exists")
     @classmethod
-    def _check_exists(cls, v, info: FieldValidationInfo):
+    def _check_exists(cls, v, info: ValidationInfo):
         fn = functools.partial(check_callable, info.data["path"])
         return fn
 
     @field_validator("name")
     @classmethod
-    def _name(cls, v, info: FieldValidationInfo):
+    def _name(cls, v, info: ValidationInfo):
         return info.data["path"].__name__
 
 
@@ -574,6 +583,7 @@ def get_catfact():
 
 # %%
 if __name__ == "__main__":
+    # FIXME:
     ext = ".json"
     dobj = DisplayCallable(value=get_catfact, ext=ext)
     display(dobj)
@@ -644,7 +654,7 @@ class AutoDisplay(tr.HasTraits):
 
     @tr.observe("order")
     def _observe_order(self, change):
-        self._update_bx_bar(change["new"])
+        [d._update_bx_bar(change["new"]) for d in self.display_objects]
 
     """
     displays the contents of a file in the notebook.
@@ -1008,6 +1018,7 @@ def _update_files(self):
     display(ad)
 # %%
 if __name__ == "__main__":
+    # FIXME
     ad.order = ORDER_DEFAULT
 
 # %%
@@ -1041,7 +1052,7 @@ def _update_files(self):
 
     display(test_ui)
 
-# %%
+# %% editable=true slideshow={"slide_type": ""}
 if __name__ == "__main__":
     from ipyautoui.demo_schemas import CoreIpywidgets
     from pydantic import parse_obj_as
diff --git a/src/ipyautoui/autodisplay_renderers.py b/src/ipyautoui/autodisplay_renderers.py
index d8d0b8e8..4006784e 100644
--- a/src/ipyautoui/autodisplay_renderers.py
+++ b/src/ipyautoui/autodisplay_renderers.py
@@ -18,7 +18,7 @@
 displayfile is used to display certain types of files.
 The module lets us preview a data retrieved from: file, request or callable (< TODO)
 """
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 # +
 import os
@@ -67,9 +67,15 @@ def getbytes(path: ty.Union[pathlib.Path, HttpUrl, ty.Callable]) -> ty.ByteStrin
     """common function for read bytes from: a request, file or callable
     NOTE: if a callable the data must be returned as bytes
     """
+    def is_url(p):
+        try: 
+            HttpUrl(p)
+            return True
+        except:
+            return False
     if isinstance(path, pathlib.Path):
         return path.read_bytes()
-    elif isinstance(path, HttpUrl):
+    elif is_url(path):
         return requests.get(path).content
     elif isinstance(path, ty.Callable):
         return path()
diff --git a/src/ipyautoui/autoform.py b/src/ipyautoui/autoform.py
index c7df557c..0577ee6c 100644
--- a/src/ipyautoui/autoform.py
+++ b/src/ipyautoui/autoform.py
@@ -1,3 +1,6 @@
+# %run _dev_maplocal_params.py
+"""layout attributes of a form box
+"""
 # +
 import ipywidgets as w
 import traitlets as tr
diff --git a/src/ipyautoui/automapschema.py b/src/ipyautoui/automapschema.py
index f2b0249b..bfaeb873 100644
--- a/src/ipyautoui/automapschema.py
+++ b/src/ipyautoui/automapschema.py
@@ -14,7 +14,7 @@
 # ---
 
 # +
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 
 import typing as ty
@@ -51,6 +51,9 @@ def _init_model_schema(
     schema = {k: v for k, v in schema.items() if k != "$defs"}
     return model, schema
 
+def pydantic_validate(model, value):
+    return model.model_validate(value).model_dump(mode="json")
+
 
 # def is_Nullable(di: dict) -> bool:
 #     """
@@ -113,12 +116,9 @@ def is_allowed_type(di: dict) -> bool:
 
 def flatten_allOf(di: dict) -> dict:
     if "allOf" in di.keys():
-        kw = "allOf"
-        types = di[kw]
-        for _ in types:
+        for _ in di["allOf"]:
             di = {**di, **_}
-        del di[kw]
-        return di
+        return {k:v for k,v in di.items() if k != "allOf"}
     else:
         return di
 
@@ -127,7 +127,7 @@ def handle_null_or_unknown_types(fn, di: dict) -> tuple[bool, bool]:
     if "anyOf" in di:
         return is_Nullable(fn, di)
     elif "allOf" in di:  # do something...
-        logger.info(
+        logger.debug(
             f"allOf not properly implemented. it is flattened and re-evaluated.\nGiven : {str(di)}"
         )
         return fn(flatten_allOf(di))
@@ -740,7 +740,35 @@ def is_Textarea(
         return True, allow_none
     else:
         return False, allow_none
+    
+def is_enum_array(di: dict) -> (bool, int):
+    is_enum = lambda di: True if "enum" in di.keys() else False
+    
+    if not "items" in di.keys():
+        return False, 0
+    if is_enum(di["items"]):
+        return True, len(di["items"]["enum"])
+    else:
+        if "allOf" in di["items"]:
+            di["items"] = flatten_allOf(di["items"])
+            if is_enum(di["items"]):
+                return True, len(di["items"]["enum"])
+            else:
+                return False, 0
+        else:
+            return False, 0
+        
+def is_select_multiple(di:dict) ->( bool, int):
+    t = di["type"]
+    if "autoui" in di.keys():
+        return False, 0
+    if t != "array":
+        return False, 0
+    if not "items" in di.keys():
+        return False, 0
+    return is_enum_array(di)
 
+SELECT_MULTIPLE_MAX_ITEMS = 10
 
 def is_SelectMultiple(
     di: dict, allow_none=False, checked_nullable=False
@@ -748,21 +776,52 @@ def is_SelectMultiple(
     """
     ```py
     from ipyautoui.automapschema import is_SelectMultiple
-    di = {"title": "Dropdown", "enum": [1,2], "type": "array"}
+    di = {"title": "SelectMultiple", "items": {"enum": [1,2]}, "type": "array"}
+    print(is_SelectMultiple(di))
+    #> (True, False)
+    di = {"title": "SelectMultiple", "items": {"allOf":[{"enum": [1,2]}]}, "type": "array"}
     print(is_SelectMultiple(di))
     #> (True, False)
     ```
     """
     if "type" not in di.keys() and not checked_nullable:
         return handle_null_or_unknown_types(is_SelectMultiple, di)
-    t = di["type"]
-    if "autoui" in di.keys():
-        return False, allow_none
-    if not "enum" in di.keys():
+    is_enum, enum_len = is_select_multiple(di)
+    if not is_enum:
         return False, allow_none
-    if t != "array":
+    else:
+        if enum_len < SELECT_MULTIPLE_MAX_ITEMS:
+            return True, allow_none
+        else:
+            return False, allow_none
+    
+def is_TagsInput(
+    di: dict, allow_none=False, checked_nullable=False
+) -> tuple[bool, bool]:
+    """
+    ```py
+    from ipyautoui.automapschema import is_TagsInput
+    di = {"title": "TagsInput", "items": {"enum": [1,2]}, "type": "array"}
+    print(is_TagsInput(di))
+    #> (False, False)
+    di = {"title": "TagsInput", "items": {"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}, "type": "array"}
+    print(is_TagsInput(di))
+    #> (True, False)
+    di = {"title": "TagsInput", "items": {"allOf":[{"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}]}, "type": "array"}
+    print(is_TagsInput(di))
+    #> (True, False)
+    ```
+    """
+    if "type" not in di.keys() and not checked_nullable:
+        return handle_null_or_unknown_types(is_TagsInput, di)
+    is_enum, enum_len = is_select_multiple(di)
+    if not is_enum:
         return False, allow_none
-    return True, allow_none
+    else:
+        if enum_len >= SELECT_MULTIPLE_MAX_ITEMS:
+            return True, allow_none
+        else:
+            return False, allow_none
 
 
 def is_Checkbox(
@@ -858,23 +917,23 @@ def is_Array(di: dict, allow_none=False, checked_nullable=False) -> tuple[bool,
         return False, allow_none
     if is_range(di):
         return False, allow_none
-    if "enum" in di.keys():
+    if is_enum_array(di)[0]:
         return False, allow_none  # as this is picked up from SelectMultiple
     if is_DataFrame(di, checked_nullable=checked_nullable)[0]:
         return False, allow_none
     return True, allow_none
 
 
-def update_keys(di, di_map=MAP_JSONSCHEMA_TO_IPYWIDGET):
-    update_key = lambda key, di_map: di_map[key] if key in di_map.keys() else key
-    return {update_key(k, di_map): v for k, v in di.items()}
+def update_keys(di, map_keys=MAP_JSONSCHEMA_TO_IPYWIDGET):
+    update_key = lambda key, map_keys: map_keys[key] if key in map_keys.keys() else key
+    return {update_key(k, map_keys): v for k, v in di.items()}
 
 
 def remove_title_and_description(di):
     return {k: v for k, v in di.items() if k != "description" and k != "title"}
 
 
-def create_widget_caller(schema, calling=None, remove_title=True):
+def create_widget_caller(schema, calling=None, remove_title=True, map_keys=MAP_JSONSCHEMA_TO_IPYWIDGET):
     """
     creates a "caller" object from the schema.
     this renames schema keys as follows to match ipywidgets:
@@ -900,13 +959,20 @@ def create_widget_caller(schema, calling=None, remove_title=True):
             initialised like ```calling(**caller)```
 
     """
-    caller = update_keys(schema)
+    caller = update_keys(schema, map_keys=map_keys)
     if remove_title:
         caller = remove_title_and_description(caller)
     if calling is not None:
         caller = remove_non_present_kwargs(calling, caller)
     return caller
 
+def tags_widget_caller(schema, calling=None, remove_title=True):
+    map_keys = dict(MAP_JSONSCHEMA_TO_IPYWIDGET) | {"enum": "allowed_tags"}
+    return create_widget_caller(schema, calling=calling, remove_title=remove_title, map_keys=map_keys)
+
+def flatten_items(di: dict, fn=None) -> dict:
+    di = {**di, **{k:v for k, v in di["items"].items() if k!="type"}}
+    return {k:v for k,v in di.items() if k!="items"}
 
 def flatten_type_and_nullable(di: dict, fn=None) -> dict:
     if "type" in di.keys():
@@ -919,23 +985,23 @@ def flatten_type_and_nullable(di: dict, fn=None) -> dict:
         else:
             raise ValueError("currently must have anyOf or allOf or type in schema")
         types = di[kw]
-        n = "null" in [l.get("type") for l in types]
-        t = [l for l in types if l.get("type") != "null"]  # [0]  # get non-null type
+        is_nullable = "null" in [l.get("type") for l in types]
+        types_non_null = [l for l in types if l.get("type") != "null"]  # get non-null types
 
-        def get_enum(t):
-            li = [_["enum"] for _ in t if "enum" in _.keys()]
+        def get_enum(types_non_null):
+            li = [_["enum"] for _ in types_non_null if "enum" in _.keys()]
             if len(li) == 0:
                 return None
             else:
                 return li[0]
 
-        e = get_enum(t)
-        if len(t) > 1 and e is not None:
-            di["examples"] = e
-        for _ in t:
+        enum = get_enum(types_non_null)
+        if len(types_non_null) > 1 and enum is not None:
+            di["examples"] = enum
+        for _ in types_non_null:
             di = {**di, **_}
         del di[kw]
-        return {**di, **{"nullable": n}}
+        return {**di, **{"nullable": is_nullable}}
 
 
 def add_schema_key(di: dict, wi=None) -> dict:
@@ -1117,6 +1183,22 @@ def get_widgets_map(di_update=None):
             "SelectMultiple": WidgetMapper(
                 fn_filt=is_SelectMultiple,
                 widget=w.SelectMultiple,
+                li_fn_modify=[
+                    flatten_type_and_nullable,
+                    flatten_items,
+                    create_widget_caller,
+                    dropdown_drop_null_value,
+                ],
+            ),
+            "TagsInput": WidgetMapper(
+                fn_filt=is_TagsInput,
+                widget=w.TagsInput,
+                li_fn_modify=[
+                    flatten_type_and_nullable,
+                    flatten_items,
+                    tags_widget_caller,
+                    dropdown_drop_null_value,
+                ],
             ),
             "Color": WidgetMapper(
                 fn_filt=is_Color,
@@ -1235,7 +1317,8 @@ def get_widget(di, k, widgets_map):
             return WidgetCaller(schema_=di, autoui=wi, allow_none=allow_none)
 
 
-def get_widget(di):
+def get_widget(di, **kwargs):
+    di = di | kwargs
     caller = map_widget(di)
     return widgetcaller(caller)  # TODO: add passing of value
 
diff --git a/src/ipyautoui/autoobject.py b/src/ipyautoui/autoobject.py
index 969bdc7f..92107b69 100644
--- a/src/ipyautoui/autoobject.py
+++ b/src/ipyautoui/autoobject.py
@@ -6,7 +6,7 @@
 #       extension: .py
 #       format_name: light
 #       format_version: '1.5'
-#       jupytext_version: 1.15.0
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
@@ -16,7 +16,7 @@
 # +
 """AutoObject - create a 
 """
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 
 import logging
@@ -34,6 +34,8 @@
 from ipyautoui.autoform import AutoObjectFormLayout
 from jsonref import replace_refs
 from pydantic import BaseModel
+from ipyautoui.watch_validate import WatchValidate
+import contextlib
 
 logger = logging.getLogger(__name__)
 
@@ -62,7 +64,7 @@ def _get_value_trait(obj_with_traits):
         )
 
 
-class AutoObject(w.VBox):
+class AutoObject(w.VBox, WatchValidate):
     """creates an ipywidgets form from a json-schema or pydantic model.
     datatype must be "object"
 
@@ -101,7 +103,7 @@ class AutoObject(w.VBox):
     type = tr.Unicode(default_value="object")
     allOf = tr.List(allow_none=True, default_value=None)
     properties = tr.Dict()
-    _value = tr.Dict(allow_none=True)
+    _value = tr.Dict(allow_none=True)  # NOTE: value setter and getter in `WatchValidate`
     fdir = tr.Instance(klass=pathlib.PurePath, default_value=None, allow_none=True)
     align_horizontal = tr.Bool(default_value=True)
     nested_widgets = tr.List()
@@ -307,30 +309,10 @@ def __init__(
             self.value = kwargs["value"]
         else:
             self._value = self.di_widgets_value
-
-    @classmethod
-    def from_schema(
-        cls, schema: ty.Union[ty.Type[BaseModel], dict], value: dict = None
-    ):
-        if isinstance(schema, type):
-            if issubclass(schema, BaseModel):
-                model = schema
-                schema = replace_refs(schema.model_json_schema())
-        elif isinstance(schema, dict):
-            model = None
-            if "$defs" in schema.keys():
-                try:
-                    schema = replace_refs(schema)
-                except ValueError as e:
-                    logger.warning(f"replace_refs error: \n{e}")
-                    pass
-        else:
-            raise ValueError("schema must be a jsonschema or pydantic model")
-        if value is not None:
-            schema["value"] = value
-        ui = cls(**schema)
-        ui.model = model
-        return ui
+        self._post_init(**kwargs)
+            
+    def _post_init(self, **kwargs):
+        pass
 
     def _open_nested(self):
         for r in self.di_boxes.values():
@@ -349,21 +331,6 @@ def default_order(self):
         except:
             None
 
-    @property
-    def value(self):
-        return self._value
-
-    @value.setter
-    def value(self, value):
-        """this is for setting the value via the API"""
-        if value is None:
-            pass
-        else:
-            with self.hold_trait_notifications():
-                self._value = value
-                if hasattr(self, "di_widgets"):
-                    self._update_widgets_from_value()
-
     @property
     def json(self):
         if self.model is not None:
@@ -404,7 +371,7 @@ def _init_controls(self):
     def set_watcher(self, key, widget, watch):
         logger.debug(f"{watch} trait found for: {key}")
         widget.observe(
-            functools.partial(self._watch_change, key=key, watch=watch), watch
+            self._watch_validate_change, watch  # NOTE: `_watch_validate_change` in WatchValidate
         )
 
     def _init_watch_widgets(self):
@@ -414,26 +381,24 @@ def _init_watch_widgets(self):
                     self.set_watcher(k, v, watch)
                     break  # if `_value` is found don't look for `value`
 
-    def _watch_change(self, change, key=None, watch="value"):
-        self._value = self.di_widgets_value
-        if hasattr(self, "savebuttonbar"):
-            self.savebuttonbar.unsaved_changes = True
-        # NOTE: it is required to set the whole "_value" otherwise
-        #       traitlets doesn't register the change.
 
     def _update_widgets_from_value(self):
-        for k, v in self.value.items():
-            if k in self.di_widgets.keys():
-                if v is None and not isinstance(self.di_widgets[k], Nullable):
-                    v = _get_value_trait(self.di_widgets[k]).default()
-                try:
-                    self.di_widgets[k].value = v
-                except tr.TraitError as err:
-                    logging.warning(err)
-            else:
-                logging.critical(
-                    f"no widget created for {k}, with value {str(v)}. fix this in the schema!"
-                )
+        with self.silence_autoui_traits():
+            for k, v in self.value.items():
+                if k in self.di_widgets.keys():
+                    if v is None and not isinstance(self.di_widgets[k], Nullable):
+                        v = _get_value_trait(self.di_widgets[k]).default()
+                    try:
+                        self.di_widgets[k].value = v
+                    except tr.TraitError as err:
+                        logging.warning(err)
+                else:
+                    logging.critical(
+                        f"no widget created for {k}, with value {str(v)}. fix this in the schema!"
+                    )
+                    
+    def _get_value(self):
+        return self.di_widgets_value
 
     @property
     def di_widgets_value(self):  # used to set _value
@@ -441,7 +406,6 @@ def di_widgets_value(self):  # used to set _value
         return {k: get_value(v) for k, v in self.di_widgets.items()}
 
 
-# +
 class AutoObjectForm(AutoObject, AutoObjectFormLayout):
     def __init__(self, **kwargs):
         super().__init__(
@@ -463,8 +427,6 @@ def display_showraw(self):  # NOTE: this overwritten this in AutoObjectForm
         return self.json
 
 
-# -
-
 if __name__ == "__main__":
     from ipyautoui.demo_schemas import CoreIpywidgets
 
diff --git a/src/ipyautoui/autoui.py b/src/ipyautoui/autoui.py
index c8230b59..9a07a189 100644
--- a/src/ipyautoui/autoui.py
+++ b/src/ipyautoui/autoui.py
@@ -6,7 +6,7 @@
 #       extension: .py
 #       format_name: light
 #       format_version: '1.5'
-#       jupytext_version: 1.15.0
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
@@ -27,12 +27,12 @@
         from ipyautoui.constants import DISPLAY_AUTOUI_SCHEMA_EXAMPLE
         DISPLAY_AUTOUI_SCHEMA_EXAMPLE()
 """
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 
 import pathlib
 from IPython.display import display
-from pydantic import BaseModel
+from pydantic import BaseModel, ValidationError
 import json
 import traitlets as tr
 import typing as ty
@@ -43,6 +43,8 @@
 from ipyautoui.automapschema import map_widget, widgetcaller, _init_model_schema
 import ipywidgets as w
 from pydantic import BaseModel
+from ipyautoui.automapschema import pydantic_validate
+from IPython.display import clear_output
 
 logger = logging.getLogger(__name__)
 
@@ -193,6 +195,8 @@ def create_autodisplay_map(
 
 
 # +
+# TODO: should autoui be a function that returns the appropriate widget?
+#       this would solve the nested traits issue...
 class AutoUi(w.VBox, AutoObjectFormLayout, AutoUiFileMethods, AutoRenderMethods):
 
     """extends AutoObject and AutoUiCommonMethods to create an
@@ -233,11 +237,24 @@ class AutoUi(w.VBox, AutoObjectFormLayout, AutoUiFileMethods, AutoRenderMethods)
             using schema kwargs this is remembered when re-enabled. Defaults to False.
 
     """
-
+    error = tr.Unicode(default_value=None, allow_none=True)
     schema = tr.Dict()
     model = tr.Type(klass=BaseModel, default_value=None, allow_none=True)
     _value = tr.Any()  # TODO: update trait type on schema change
-
+    
+    @tr.observe("error")
+    def _error(self, on_change):
+        if self.error is None:
+            with self.out_error:
+                clear_output()
+            self.out_error.layout.display = "None"
+            
+        else:
+            self.out_error.layout.display = ""
+            with self.out_error:
+                clear_output()
+                print(self.error)
+            
     @property
     def value(self):
         return self._value
@@ -276,7 +293,7 @@ def __init__(
             **kwargs: passed to AutoObject. see attributes for details.
         """
         kwargs["model"], kwargs["schema"] = _init_model_schema(schema)
-
+        self.out_error = w.Output()
         super().__init__(
             **kwargs,
         )
@@ -288,6 +305,7 @@ def __init__(
             self.savebuttonbar,
             self.hbx_title,
             self.html_description,
+            self.out_error,
             self.autowidget,
             self.vbx_showraw,
         ]
@@ -315,7 +333,24 @@ def _init_watch_widget(self):
 
     def _watch_change(self, on_change):
         if on_change["new"] != self._value:
-            self._value = self.autowidget.value
+            v = self.autowidget.value
+            if self.model is not None:
+                try:
+                    v_ = pydantic_validate(self.model, v)
+                    self.error = None
+                except ValidationError as e:
+                    self.error = str(e)
+                    v_ = v
+                if v_ != v:
+                    try:
+                        with self.autowidget.silence_autoui_traits():
+                            self.value = v_
+                    except:
+                        pass
+                else:
+                    self._value = v_
+            else:
+                self._value = v
             if hasattr(self, "savebuttonbar"):
                 self.savebuttonbar.unsaved_changes = True
             # NOTE: it is required to set the whole "_value" otherwise
@@ -371,8 +406,6 @@ def summarize_di_callers(obj: AutoUi):  # NOTE: mainly used for demo
     # aui.show_savebuttonbar = False
     display(aui)
 
-# +
-
 # -
 
 if __name__ == "__main__":
diff --git a/src/ipyautoui/autovjsf.py b/src/ipyautoui/autovjsf.py
index 4154280f..dbbe7950 100644
--- a/src/ipyautoui/autovjsf.py
+++ b/src/ipyautoui/autovjsf.py
@@ -14,7 +14,7 @@
 # ---
 
 # +
-# %run _dev_sys_path_append.py
+# %run _dev_maplocal_params.py
 # %load_ext lab_black
 
 import pathlib
diff --git a/src/ipyautoui/basemodel.py b/src/ipyautoui/basemodel.py
index eeb58674..c0b91dd0 100644
--- a/src/ipyautoui/basemodel.py
+++ b/src/ipyautoui/basemodel.py
@@ -21,12 +21,4 @@ def file(self: Type[BaseModel], path: pathlib.Path, **json_kwargs):
     path.write_text(self.model_dump_json(**json_kwargs), encoding="utf-8")
 
 
-class BaseModel(BaseModel):
-    # TODO[pydantic]: The following keys were removed: `json_encoders`.
-    # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
-    model_config = ConfigDict(
-        json_encoders={pathlib.PurePosixPath: str}, arbitrary_types_allowed=True
-    )
-
-
 setattr(BaseModel, "file", file)
diff --git a/src/ipyautoui/custom/__init__.py b/src/ipyautoui/custom/__init__.py
index cfbc061d..c3045e49 100644
--- a/src/ipyautoui/custom/__init__.py
+++ b/src/ipyautoui/custom/__init__.py
@@ -3,3 +3,4 @@
 from ipyautoui.custom.iterable import AutoArray, Array
 from ipyautoui.custom.loadproject import LoadProject
 from ipyautoui.custom.buttonbars import SaveButtonBar
+from ipyautoui.custom.fileupload import FileUploadToDir, FilesUploadToDir
\ No newline at end of file
diff --git a/src/ipyautoui/custom/autogrid.py b/src/ipyautoui/custom/autogrid.py
index 8d240388..a68e1514 100644
--- a/src/ipyautoui/custom/autogrid.py
+++ b/src/ipyautoui/custom/autogrid.py
@@ -20,7 +20,7 @@
 contains methods for validation, coercion, and default values. 
 
 defines AutoGrid, a datagrid generated from a jsonschema."""
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 import typing as ty
diff --git a/src/ipyautoui/custom/buttonbars.py b/src/ipyautoui/custom/buttonbars.py
index 93307e1c..86d41ad5 100644
--- a/src/ipyautoui/custom/buttonbars.py
+++ b/src/ipyautoui/custom/buttonbars.py
@@ -14,7 +14,7 @@
 # ---
 
 # +
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 import ipywidgets as w
diff --git a/src/ipyautoui/custom/date_string.py b/src/ipyautoui/custom/date_string.py
index 9fa7af61..cf7ffd0f 100644
--- a/src/ipyautoui/custom/date_string.py
+++ b/src/ipyautoui/custom/date_string.py
@@ -11,7 +11,8 @@ class DatePickerString(w.DatePicker):
 
     @tr.observe("value")
     def _update_value_string(self, on_change):
-        self._value = self.value.strftime(self.strftime_format)
+        if self.value is not None:
+            self._value = self.value.strftime(self.strftime_format)
 
     @tr.validate("value")
     def _validate_value(self, proposal):
@@ -39,4 +40,5 @@ def _validate_value(self, proposal):
 
     @tr.observe("value")  # convert datetime to string
     def _update_value_string(self, on_change):
-        self._value = self.value.strftime(self.strftime_format)
+        if self.value is not None:
+            self._value = self.value.strftime(self.strftime_format)
diff --git a/src/ipyautoui/custom/decision_branch.py b/src/ipyautoui/custom/decision_branch.py
index fc288291..2e13903f 100644
--- a/src/ipyautoui/custom/decision_branch.py
+++ b/src/ipyautoui/custom/decision_branch.py
@@ -20,7 +20,7 @@
 a UI element that loads a folder for data caching, whilst storing a record of folders in use
 """
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 #
 # %load_ext lab_black
 from pydantic import ConfigDict, BaseModel
diff --git a/src/ipyautoui/custom/editgrid.py b/src/ipyautoui/custom/editgrid.py
index df6a0c4f..41d9b2bf 100644
--- a/src/ipyautoui/custom/editgrid.py
+++ b/src/ipyautoui/custom/editgrid.py
@@ -8,7 +8,7 @@
 #       extension: .py
 #       format_name: light
 #       format_version: '1.5'
-#       jupytext_version: 1.15.0
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
@@ -17,7 +17,7 @@
 
 # +
 """General widget for editing data"""
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 # TODO: move editgrid.py to root ?
 
@@ -66,6 +66,7 @@ class DataHandler(BaseModel):
 
 
 if __name__ == "__main__":
+
     class TestModel(BaseModel):
         string: str = Field("string", title="Important String")
         integer: int = Field(40, title="Integer of somesort")
@@ -77,7 +78,7 @@ def test_save():
     def test_revert():
         print("Reverted.")
 
-    ui = AutoObjectForm.from_schema(TestModel)
+    ui = AutoObjectForm.from_pydantic_model(TestModel)
     display(ui)
 
 if __name__ == "__main__":
@@ -229,12 +230,12 @@ def _bn_copy(self, onclick):
 if __name__ == "__main__":
     display(UiCopy())
 
-
 # +
 # TODO: refactor how the datahandler works...
 # TODO: add a test for the datahandler...
 
 
+# from ipyautoui.watch_validate import WatchValidate
 class EditGrid(w.VBox, TitleDescription):
     _value = tr.Tuple()  # using a tuple to guarantee no accidental mutation
     warn_on_delete = tr.Bool()
@@ -300,7 +301,7 @@ def __init__(
         description: str = None,
         show_title: bool = True,
         **kwargs,
-    ):
+    ):  # TODO: use **kwargs to pass attributes to EditGrid as in AutoObject and AutoArray
         self.html_title = w.HTML()
         self.description = description
         self.title = title
@@ -318,13 +319,13 @@ def __init__(
 
         self._init_form()
         if ui_add is None:
-            self.ui_add = AutoObjectForm.from_schema(self.row_schema)
+            self.ui_add = AutoObjectForm.from_jsonschema(self.row_schema)
         else:
-            self.ui_add = ui_add.from_schema(self.row_schema)
+            self.ui_add = ui_add.from_jsonschema(self.row_schema)
         if ui_edit is None:
-            self.ui_edit = AutoObjectForm.from_schema(self.row_schema)
+            self.ui_edit = AutoObjectForm.from_jsonschema(self.row_schema)
         else:
-            self.ui_edit = ui_edit.from_schema(self.row_schema)
+            self.ui_edit = ui_edit.from_jsonschema(self.row_schema)
         if ui_delete is None:
             self.ui_delete = UiDelete()
         else:
@@ -628,17 +629,22 @@ def _delete(self):
     AUTO_GRID_DEFAULT_VALUE = AUTO_GRID_DEFAULT_VALUE * 4
 
     class DataFrameCols(BaseModel):
-        string: str = Field("string", json_schema_extra=dict(column_width=400, section="a"))
+        string: str = Field(
+            "string", json_schema_extra=dict(column_width=400, section="a")
+        )
         integer: int = Field(1, json_schema_extra=dict(column_width=80, section="a"))
-        floater: float = Field(None, json_schema_extra=dict(column_width=70, section="b"))
+        floater: float = Field(
+            None, json_schema_extra=dict(column_width=70, section="b")
+        )
 
     class TestDataFrame(RootModel):
         """a description of TestDataFrame"""
 
         root: ty.List[DataFrameCols] = Field(
             default=AUTO_GRID_DEFAULT_VALUE,
-            json_schema_extra=dict(format="dataframe",
-                datagrid_index_name=("section", "title")),
+            json_schema_extra=dict(
+                format="dataframe", datagrid_index_name=("section", "title")
+            ),
         )
 
     title = "The Wonderful Edit Grid Application"
@@ -743,3 +749,5 @@ class TestDataFrame(RootModel):
 
 if __name__ == "__main__":
     editgrid.transposed = True
+
+
diff --git a/src/ipyautoui/custom/filechooser.py b/src/ipyautoui/custom/filechooser.py
index 3255ec8b..93848288 100644
--- a/src/ipyautoui/custom/filechooser.py
+++ b/src/ipyautoui/custom/filechooser.py
@@ -14,7 +14,7 @@
 # ---
 
 """wrapper for ipyfilechooster.FileChooser"""
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 import pathlib
diff --git a/src/ipyautoui/custom/filesindir.py b/src/ipyautoui/custom/filesindir.py
index f8184b7b..de783043 100644
--- a/src/ipyautoui/custom/filesindir.py
+++ b/src/ipyautoui/custom/filesindir.py
@@ -14,7 +14,7 @@
 #     name: python3
 # ---
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 #
 # %load_ext lab_black
 
diff --git a/src/ipyautoui/custom/fileupload.py b/src/ipyautoui/custom/fileupload.py
index d6e9ab43..4575049f 100644
--- a/src/ipyautoui/custom/fileupload.py
+++ b/src/ipyautoui/custom/fileupload.py
@@ -8,7 +8,7 @@
 #       extension: .py
 #       format_name: light
 #       format_version: '1.5'
-#       jupytext_version: 1.15.0
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
@@ -17,13 +17,13 @@
 
 """file upload wrapper"""
 # %load_ext lab_black
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 
 # +
 import ipywidgets as w
 from markdown import markdown
 from IPython.display import display, clear_output
-from pydantic import BaseModel, field_validator, Field
+from pydantic import BaseModel, field_validator, Field, ValidationInfo
 import pathlib
 import typing as ty
 import stringcase
@@ -33,19 +33,16 @@
 import logging
 from ipyautoui.constants import DELETE_BUTTON_KWARGS
 from ipyautoui._utils import getuser
-from ipyautoui.autodisplay import DisplayObject, DisplayPath
+from ipyautoui.autodisplay import DisplayObject, DisplayPath, ORDER_DEFAULT
 from ipyautoui.custom.iterable import Array
 from ipyautoui.autodisplay_renderers import render_file
 from ipyautoui.env import Env
+from ipyautoui.constants import DELETE_BUTTON_KWARGS
+from ipyautoui.autodisplay import ORDER_NOTPATH
 
 IPYAUTOUI_ROOTDIR = Env().IPYAUTOUI_ROOTDIR
-IS_IPYWIDGETS8 = (lambda: True if "8" in w.__version__ else False)()
 logger = logging.getLogger(__name__)
 
-logger.warning(
-    "FileUploadToDir still needs fixing following updates to `iterable.py`. DO NOT USE."
-)
-
 
 # -
 
@@ -53,10 +50,12 @@
 class File(BaseModel):
     name: str
     fdir: pathlib.Path = pathlib.Path(".")
-    path: pathlib.Path = None
+    path: pathlib.Path = Field(pathlib.Path("overide.me"), validate_default=True)
 
     @field_validator("path")
-    def _path(cls, v, values):
+    @classmethod
+    def _path(cls, v, info: ValidationInfo):
+        values = info.data
         return values["fdir"] / values["name"]
 
 
@@ -64,13 +63,7 @@ def _path(cls, v, values):
 def read_file_upload_item(di: dict, fdir=pathlib.Path("."), added_by=None):
     if added_by is None:
         added_by = getuser()
-    if IS_IPYWIDGETS8:
-        _ = di
-    else:
-        _ = di["metadata"]
-    _["fdir"] = fdir
-    _["added_by"] = added_by
-    return File(**_)
+    return File(**di | dict(fdir=fdir, added_by=added_by))
 
 
 def add_file(upld_item, fdir=pathlib.Path(".")):
@@ -79,7 +72,7 @@ def add_file(upld_item, fdir=pathlib.Path(".")):
     return f
 
 
-def add_files_ipywidgets8(upld_value, fdir=pathlib.Path(".")):
+def add_files_to_dir(upld_value, fdir=pathlib.Path(".")):
     di = {}
     for l in upld_value:
         f = add_file(l, fdir=fdir)
@@ -87,35 +80,111 @@ def add_files_ipywidgets8(upld_value, fdir=pathlib.Path(".")):
     return [v.path for v in di.values()]
 
 
+# +
 def add_files(upld_value, fdir=pathlib.Path(".")):
     if not pathlib.Path(fdir).exists():
         pathlib.Path(fdir).mkdir(exist_ok=True)
-    return add_files_ipywidgets8(upld_value, fdir=fdir)
+    return add_files_to_dir(upld_value, fdir=fdir)
 
 
-# +
+class FileUploadToDir(w.VBox):
+    _value = tr.Unicode(default_value=None, allow_none=True)
+    fdir = tr.Instance(klass=pathlib.PurePath, default_value=pathlib.Path("."))
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        if pathlib.Path(value).is_file():
+            self.add_file([pathlib.Path(value)])
+        else:
+            pass
+
+    @property
+    def path(self):
+        if self.value is None:
+            return None
+        else:
+            return pathlib.Path(self.value)
+
+    def __init__(self, **kwargs):
+        self.upld = w.FileUpload()
+        self.bn_delete = w.Button(**DELETE_BUTTON_KWARGS)
+        self._show_bn_delete("")
+        self.fdisplay = DisplayPath(value=None, order=("exists", "openpreview", "name"))
+        self._init_controls()
+        super().__init__(**kwargs)
+        if "value" in kwargs:
+            self.value = kwargs["value"]
+        self.children = [
+            w.HBox([self.bn_delete, self.upld, self.fdisplay]),
+            self.fdisplay.bx_out,
+        ]
+        self.fdisplay.children = [self.fdisplay.bx_bar]
+
+    def _init_controls(self):
+        self.upld.observe(self._upld, "value")
+        self.observe(self._show_bn_delete, "_value")
+        self.bn_delete.on_click(self._bn_delete)
+
+    def _upld(self, on_change):
+        paths = add_files(self.upld.value, fdir=self.fdir)
+        self.add_file(paths)
+        self.upld.value = ()
+
+    def _bn_delete(self, on_click):
+        self.path.unlink()
+        self._value = None
+        self.fdisplay.value = None
+
+    def _show_bn_delete(self, on_change):
+        if self.value is None:
+            self.bn_delete.layout.display = "None"
+        else:
+            self.bn_delete.layout.display = ""
+
+    def add_file(self, paths: list[str]):
+        if len(paths) > 1:
+            raise ValueError("asdf")
+        elif len(paths) == 0:
+            return
+        else:
+            if self.path is not None:
+                self.path.unlink(missing_ok=True)
+            self.fdisplay.value = str(paths[0])
+            self._value = str(paths[0])
+
+
+if __name__ == "__main__":
+    fupld = FileUploadToDir(value="IMG_0688.jpg")
+    display(fupld)
+
+
+# -
+
 class FilesUploadToDir(Array):
-    def __init__(
-        self,
-        value=None,
-        fdir=pathlib.Path("."),
-        kwargs_display_path: ty.Optional[dict] = None,
-        **kwargs,
-    ):
-        super().__init__(
-            add_remove_controls="remove_only",
-            show_hash=None,
-        )
-        coerce_none = lambda v: {} if v is None else v
-        self.kwargs_display_path = coerce_none(kwargs_display_path)
-        self.bx_boxes.layout = {"border": "solid LightCyan 2px"}
-        self.fdir = fdir
-        self.upld = w.FileUpload(**kwargs)
-        self.children = [self.upld] + list(self.children)
+    fdir = tr.Instance(klass=pathlib.PurePath, default_value=pathlib.Path("."))
+    kwargs_display_path = tr.Dict(default_value={}, allow_none=False)
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+    def _post_init(self, **kwargs):
+        self.fn_remove = self.fn_remove_file
+        self.add_remove_controls = "remove_only"
+        self.show_hash = None
+        value = kwargs.get("value")
         if value is not None:
             self.add_files(value)
+        kwargs_display_path = kwargs.get("kwargs_display_path")
+        self.kwargs_display_path = (lambda v: {} if v is None else v)(
+            kwargs_display_path
+        )
+        self.upld = w.FileUpload(**dict(multiple=True))
+        self.children = [self.upld] + list(self.children)
         self._init_controls_FilesUploadToDir()
-        self.fn_remove = self.fn_remove_file
 
     def _init_controls_FilesUploadToDir(self):
         self.upld.observe(self._upld, "value")
@@ -127,10 +196,14 @@ def _upld(self, on_change):
 
     def add_files(self, paths: list[str]):
         for p in paths:
-            self.add_row(widget=DisplayPath(str(p), **self.kwargs_display_path))
-
-    def fn_remove_file(self, key=None):
-        p = pathlib.Path(self.map_key_value[key])
+            self.add_row(
+                widget=DisplayPath(
+                    str(p), **self.kwargs_display_path | dict(order=ORDER_NOTPATH)
+                )
+            )
+
+    def fn_remove_file(self, bx=None):
+        p = pathlib.Path(bx.widget.value)
         p.unlink(missing_ok=True)
 
     @property
@@ -139,35 +212,19 @@ def value(self):
 
     @value.setter
     def value(self, value):
-        self.items = []
+        self.boxes = []
         self.add_files(value)
 
 
-class AutoUploadPaths(FilesUploadToDir):
-    def __init__(
-        self,
-        schema=None,
-        value=None,
-        fdir=pathlib.Path("."),
-        kwargs_display_path: ty.Optional[dict] = None,
-        **kwargs,
-    ):
-        super().__init__(
-            value=value, fdir=fdir, kwargs_display_path=kwargs_display_path, **kwargs
-        )
-
-
-# -
-
 if __name__ == "__main__":
     p = pathlib.Path()
-    p_ = list(IPYAUTOUI_ROOTDIR.parents)[2] / "docs" / "logo.png"
-    upld = FilesUploadToDir([str(p_)])
+    p_ = list(IPYAUTOUI_ROOTDIR.parents)[2] / "docs" / "images" / "logo.png"
+    upld = FilesUploadToDir(value=[str(p_)])
     display(upld)
     # test
 
 if __name__ == "__main__":
-    upld.value = ["EquipmentReferences-MaxFordhamStandard.pdf", "GenesisCroixDeFer.jpg"]
+    upld.value = ["__init__.py", "../automapschema.yaml"]
 
 if __name__ == "__main__":
     from pydantic import BaseModel, Field
@@ -179,79 +236,9 @@ class Test(BaseModel):
         paths: list[pathlib.Path] = Field(
             title="A longish title about something",
             description="with a rambling description as well...",
-            json_schema_extra=dict(autoui="__main__.AutoUploadPaths"),
+            json_schema_extra=dict(autoui="__main__.FilesUploadToDir"),
         )
 
-    value = {"string": "string", "paths": ["bdns.csv"]}
+    value = {"string": "string", "paths": ["__init__.py"]}
     aui = AutoUi(Test, value=value, nested_widgets=[AutoUploadPaths])
     display(aui)
-
-
-class AutoUploadPathsValueString(w.VBox):
-    _value = tr.Unicode()
-
-    def __init__(
-        self,
-        schema=None,
-        value=None,
-        fdir=pathlib.Path("."),
-        kwargs_display_path: ty.Optional[dict] = None,
-        **kwargs,
-    ):
-        super().__init__()
-        self.upld = AutoUploadPaths(
-            schema=None,
-            value=None,
-            fdir=fdir,
-            kwargs_display_path=kwargs_display_path,
-            **kwargs,
-        )
-        self.children = [self.upld]
-        self._init_controls()
-        self.value = value
-
-    def _init_controls(self):
-        self.upld.observe(self._update_value, "_value")
-
-    def _update_value(self, on_change):
-        self._value = json.dumps(self.upld.value)
-
-    @property
-    def value(self):
-        return self._value
-
-    @value.setter
-    def value(self, value):
-        if value is not None:
-            self.upld.value = json.loads(value)
-
-
-if __name__ == "__main__":
-    from pydantic import BaseModel, Field
-    from ipyautoui.custom.fileupload import AutoUploadPaths
-    from ipyautoui import AutoUi
-
-    class Test(BaseModel):
-        paths: list[pathlib.Path] = Field(
-            json_schema_extra=dict(autoui="__main__.AutoUploadPathsValueString")
-        )
-
-    aui = AutoUi(Test)
-    display(aui)
-
-if __name__ == "__main__":
-    upld = AutoUploadPathsValueString(
-        fdir=pathlib.Path("/mnt/c/engDev/git_mf"),
-    )
-    display(upld)
-
-if __name__ == "__main__":
-    upld.value = (
-        '["EquipmentReferences-MaxFordhamStandard.pdf", "GenesisCroixDeFer.jpg"]'
-    )
-
-if __name__ == "__main__":
-    aui = AutoUploadPathsValueString(
-        value='["EquipmentReferences-MaxFordhamStandard.pdf", "GenesisCroixDeFer.jpg"]'
-    )
-    display(aui)
diff --git a/src/ipyautoui/custom/halo_decorator.py b/src/ipyautoui/custom/halo_decorator.py
index 24d46619..6ad42b7c 100644
--- a/src/ipyautoui/custom/halo_decorator.py
+++ b/src/ipyautoui/custom/halo_decorator.py
@@ -13,7 +13,7 @@
 #     name: python3
 # ---
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
diff --git a/src/ipyautoui/custom/iterable.py b/src/ipyautoui/custom/iterable.py
index 724ae86b..54e25477 100644
--- a/src/ipyautoui/custom/iterable.py
+++ b/src/ipyautoui/custom/iterable.py
@@ -21,7 +21,7 @@
 This item is used for the AutoObject `array`. 
 """
 # TODO: move iterable.py to root
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
@@ -48,7 +48,7 @@
 import inspect
 from ipyautoui.automapschema import from_schema_method, get_widget
 from jsonref import replace_refs
-
+from ipyautoui.watch_validate import WatchValidate
 
 logger = logging.getLogger(__name__)
 BOX = frozenmap({True: w.HBox, False: w.VBox})
@@ -141,15 +141,15 @@ def set_children(self):
 # -
 
 
-class Array(w.VBox):
-    _value = tr.List()
+class Array(w.VBox, WatchValidate):
+    _value = tr.List()  # NOTE: value setter and getter in `WatchValidate`
     fn_add = tr.Callable(
         default_value=lambda **kwargs: w.ToggleButton(
             description=(
                 "add test "
                 + "".join(random.choices(string.ascii_uppercase + string.digits, k=4))
             )
-        )
+        ),
     )
     fn_remove = tr.Callable(
         default_value=lambda box: print(f"on remove {str(box.index)}")
@@ -162,18 +162,21 @@ class Array(w.VBox):
     max_items = tr.Int(default_value=None, allow_none=True)
     type = tr.Unicode(default_value="array")
 
-    @property
-    def value(self):
-        return self._value
-
-    @value.setter
-    def value(self, value: ty.List):
-        self.boxes = []
-        self.bx_boxes.children = []
-        [self.add_row() for v in value]
-        for n, v in enumerate(value):
-            self.boxes[n].widget.value = v
-        self._update_value("onchange")
+    def _update_widgets_from_value(self):
+        diff = len(self.value) - len(self.boxes)
+        if diff < 0:
+            self.boxes = self.boxes[0 : len(self.value)]
+        elif diff > 0:
+            for n in range(0, diff):
+                self.add_row()
+        for n, v in enumerate(self.value):
+            try:
+                self.li_widgets[n].value = v
+            except:
+                raise ValueError(
+                    f"value (len={len(self.value)} and li_widgets (len={len(self.li_widgets)}) must be same length"
+                )
+        self._update_boxes()
 
     @tr.validate("type")
     def _type(self, proposal):
@@ -185,7 +188,10 @@ def _type(self, proposal):
     @tr.observe("length")
     def _length(self, on_change):
         if self.length == 0:
-            self.display_bn_add_from_zero(True)
+            if self.add_remove_controls == ItemControl.append_only or self.add_remove_controls == ItemControl.append_only:
+                self.display_bn_add_from_zero(True)
+            else:
+                pass
         else:
             if self.add_remove_controls == ItemControl.append_only:
                 self.display_bn_add_from_zero(True)
@@ -198,6 +204,7 @@ def _valid_fn_remove(self, proposal):
             raise ValueError(
                 "fn_remove must have 1no arg == item box. the item box that is being deleted. i.e. `self.fn_remove(bx)"
             )
+        return proposal["value"]
 
     @tr.observe("add_remove_controls")
     def _add_remove_controls(self, on_change):
@@ -216,6 +223,10 @@ def _add_remove_controls(self, on_change):
     def _align_horizontal(self, on_change):
         flip(self.bx_boxes, self.align_horizontal)
 
+    @property 
+    def map_key_value(self):
+        return {b.key: (lambda w: w.value if hasattr(w, "value") or hasattr(w, "_value") else None)(b.widget.value) for b in self.boxes}
+
     def display_bn_add_from_zero(self, display: bool):
         if display:
             self.bn_add_from_zero.layout.display = ""
@@ -240,6 +251,10 @@ def __init__(self, **kwargs):
         self._update_boxes()
         self._align_horizontal("on_change")
         self.layout.border = "1px solid #00a3e0"
+        self._post_init(**kwargs)
+
+    def _post_init(self, **kwargs):
+        pass # NOTE: this can be overwritten to provide customisation
 
     def _init_controls(self):
         [self._init_row_controls(key=i.key) for i in self.boxes]
@@ -262,7 +277,8 @@ def _init_row_controls(self, key=None):
         widget = self._get_attribute(key, "widget")
         for watch in ["_value", "value"]:
             if widget.has_trait(watch):
-                widget.observe(self._update_value, names=watch)
+                # widget.observe(self._update_value, names=watch)
+                widget.observe(self._watch_validate_change, names=watch)
                 break  # if `_value` is found don't look for `value`
             else:
                 logging.info(
@@ -277,9 +293,14 @@ def _sort_boxes(self):
         for n, s in enumerate(sort):
             s.index = n
         self.boxes = sort
+        self.li_widgets = [bx.widget for bx in self.boxes]
 
-    def _update_value(self, on_change):
-        self._value = [bx.widget.value for bx in self.boxes]
+    def _get_value(self):
+        return [bx.widget.value for bx in self.boxes]
+
+    # def _update_value(self, on_change):
+    #     if not self._silent:
+    #         self._value = [bx.widget.value for bx in self.boxes]
 
     def _update_boxes(self):
         self.bx_boxes.children = self.boxes
@@ -335,7 +356,8 @@ def add_row(self, key=None, new_key=None, add_kwargs=None, widget=None):
         self._sort_boxes()  # update map
         self._init_row_controls(bx.key)  # init controls
         self._update_boxes()
-        self._update_value("")
+        # self._update_value("")
+        self._watch_validate_change("")
 
     def _remove_rows(self, onclick, key=None):
         self.remove_row(key=key)
@@ -357,7 +379,9 @@ def remove_row(self, key=None, fn_onremove=None):
         self.boxes.pop(n)
         self._sort_boxes()
         self._update_boxes()
-        self._update_value("")
+        # self._update_value("")
+        self._watch_validate_change("")
+
 
 
 class AutoArray(Array):
@@ -368,21 +392,6 @@ class AutoArray(Array):
     #       : adds tuple functionality
     #       : maybe this should be a different widget all together?
 
-    @property
-    def value(self):
-        return self._value
-
-    @value.setter
-    def value(
-        self, value: ty.List
-    ):  # TODO: should be able to have this in parent `Array` ?
-        self.boxes = []
-        self.bx_boxes.children = []
-        [self.add_row() for v in value]
-        for n, v in enumerate(value):
-            self.boxes[n].widget.value = v
-        self._update_value("onchange")
-
     @tr.observe("allOf")
     def _allOf(self, on_change):
         if self.allOf is not None and len(self.allOf) == 1:
@@ -410,6 +419,8 @@ def __init__(self, **kwargs):
 class AutoArrayForm(AutoArray, TitleDescription):
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
+        if "value" in kwargs.keys():
+            self.value = kwargs["value"]
         self._update_title_description()
         self.children = [self.html_title, self.bn_add_from_zero, self.bx_boxes]
 
diff --git a/src/ipyautoui/custom/loadproject.py b/src/ipyautoui/custom/loadproject.py
index ce9f276a..e697f780 100644
--- a/src/ipyautoui/custom/loadproject.py
+++ b/src/ipyautoui/custom/loadproject.py
@@ -14,7 +14,7 @@
 
 # %%
 """generic iterable object."""
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 #
 # %load_ext lab_black
 
diff --git a/src/ipyautoui/custom/markdown_widget.py b/src/ipyautoui/custom/markdown_widget.py
index 7095239f..963a4af3 100644
--- a/src/ipyautoui/custom/markdown_widget.py
+++ b/src/ipyautoui/custom/markdown_widget.py
@@ -6,7 +6,7 @@
 #       extension: .py
 #       format_name: light
 #       format_version: '1.5'
-#       jupytext_version: 1.14.0
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
@@ -14,8 +14,7 @@
 # ---
 
 """simple markdown widget"""
-# %run ../_dev_sys_path_append.py
-#
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
@@ -149,13 +148,13 @@ class MarkdownWidget(w.VBox):
 
     _value = tr.Unicode(allow_none=True)  # default=""
 
-    def __init__(self, value=None):
-        self._init_form()
+    def __init__(self, **kwargs):
+        self._init_form( **kwargs)
         self._init_controls()
-        if value is None:
-            pass
-        else:
-            self.value = value
+        v = kwargs.get("value")
+        if v is None:
+            v=""
+        self.value = v
 
     @property
     def value(self):
@@ -166,8 +165,8 @@ def value(self, value):
         self._value = value
         self.text.value = value
 
-    def _init_form(self):
-        super().__init__()
+    def _init_form(self,  **kwargs):
+        super().__init__( **{k:v for k, v in kwargs.items() if k != "tooltip"})
         self.text = w.Textarea(layout={"width": "400px", "height": "300px"})
         self.rendered = w.Output()
         self.example_text = w.Textarea(
diff --git a/src/ipyautoui/custom/multi_toggle_buttons.py b/src/ipyautoui/custom/multi_toggle_buttons.py
index f5ac501a..3a609081 100644
--- a/src/ipyautoui/custom/multi_toggle_buttons.py
+++ b/src/ipyautoui/custom/multi_toggle_buttons.py
@@ -19,7 +19,7 @@
 # https://github.com/stas-prokopiev/ipywidgets_toggle_buttons
 """
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
diff --git a/src/ipyautoui/custom/outputlogging.py b/src/ipyautoui/custom/outputlogging.py
index 31e4ee23..8fc30e32 100644
--- a/src/ipyautoui/custom/outputlogging.py
+++ b/src/ipyautoui/custom/outputlogging.py
@@ -15,7 +15,7 @@
 
 # +
 """simple set-up of outputting logging messages to widgets output."""
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 #
 # %run ../__init__.py
 # %load_ext lab_black
diff --git a/src/ipyautoui/custom/selectandclick.py b/src/ipyautoui/custom/selectandclick.py
index 97e2cae0..e545cf2e 100644
--- a/src/ipyautoui/custom/selectandclick.py
+++ b/src/ipyautoui/custom/selectandclick.py
@@ -6,14 +6,14 @@
 #       extension: .py
 #       format_name: light
 #       format_version: '1.5'
-#       jupytext_version: 1.14.7
+#       jupytext_version: 1.15.2
 #   kernelspec:
 #     display_name: Python 3 (ipykernel)
 #     language: python
 #     name: python3
 # ---
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
@@ -310,20 +310,3 @@ def __init__(self, **kwargs):
 if __name__ == "__main__":
     ui.align_horizontal = False
     ui.align_left = False
-
-# # +
-# import random
-# import string
-
-
-# def get_random_string(length):
-#     # choose from all lowercase letter
-#     letters = string.ascii_lowercase
-#     return "".join(random.choice(letters) for i in range(length))
-
-
-# li = [get_random_string(8) for l in range(0, 10)]
-# w.TagsInput(allowed_tags=li)
-# # -
-
-# w.ToggleButtons(options=li)
diff --git a/src/ipyautoui/custom/selectdir.py b/src/ipyautoui/custom/selectdir.py
index 64dd4363..68948f33 100644
--- a/src/ipyautoui/custom/selectdir.py
+++ b/src/ipyautoui/custom/selectdir.py
@@ -14,7 +14,7 @@
 # ---
 
 # +
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 from IPython.display import display
diff --git a/src/ipyautoui/custom/showhide.py b/src/ipyautoui/custom/showhide.py
index f4f8ade2..bf33ea5a 100644
--- a/src/ipyautoui/custom/showhide.py
+++ b/src/ipyautoui/custom/showhide.py
@@ -13,7 +13,7 @@
 #     name: python3
 # ---
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # # %run __init__.py
 # # %run ../__init__.py
 # %load_ext lab_black
diff --git a/src/ipyautoui/custom/showopenurl.py b/src/ipyautoui/custom/showopenurl.py
index bf65f68e..bc5b5019 100644
--- a/src/ipyautoui/custom/showopenurl.py
+++ b/src/ipyautoui/custom/showopenurl.py
@@ -13,7 +13,7 @@
 #     name: python3
 # ---
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 from IPython.display import IFrame, Javascript, clear_output, display
diff --git a/src/ipyautoui/custom/title_description.py b/src/ipyautoui/custom/title_description.py
index c69271d7..04b28162 100644
--- a/src/ipyautoui/custom/title_description.py
+++ b/src/ipyautoui/custom/title_description.py
@@ -16,7 +16,7 @@
 # ---
 
 """generic support for observed title and description"""
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 import traitlets as tr
diff --git a/src/ipyautoui/custom/urlimagelink.py b/src/ipyautoui/custom/urlimagelink.py
index 2d305f06..c0fa7069 100644
--- a/src/ipyautoui/custom/urlimagelink.py
+++ b/src/ipyautoui/custom/urlimagelink.py
@@ -14,7 +14,7 @@
 # ---
 
 # +
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 from IPython.display import display, HTML, clear_output
diff --git a/src/ipyautoui/custom/widgetcaller_error.py b/src/ipyautoui/custom/widgetcaller_error.py
index 9f5f86b1..ea1342d0 100644
--- a/src/ipyautoui/custom/widgetcaller_error.py
+++ b/src/ipyautoui/custom/widgetcaller_error.py
@@ -19,7 +19,7 @@
 # %%
 """widget caller error"""
 # %load_ext lab_black
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %run ../__init__.py
 
 # %%
diff --git a/src/ipyautoui/custom/workingdir.py b/src/ipyautoui/custom/workingdir.py
index d3c6616a..19a52f62 100644
--- a/src/ipyautoui/custom/workingdir.py
+++ b/src/ipyautoui/custom/workingdir.py
@@ -18,7 +18,7 @@
 a UI element that loads a folder for data caching, whilst storing a record of folders in use
 """
 
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # +
diff --git a/src/ipyautoui/demo.py b/src/ipyautoui/demo.py
index bd765623..5238dd3e 100644
--- a/src/ipyautoui/demo.py
+++ b/src/ipyautoui/demo.py
@@ -27,10 +27,12 @@ def get_order():
     li = [l.split("import ")[1] for l in li]
     return li
 
-pycall = """# copy code below into your notebook to try demo
+pycall = """# copy the code below into your notebook to try the demo
+
 from ipyautoui import AutoUi
 from ipyautoui.demo_schemas import {name}
-AutoUi({name})
+ui = AutoUi({name})
+ui
 """
 
 
diff --git a/src/ipyautoui/demo_schemas/__init__.py b/src/ipyautoui/demo_schemas/__init__.py
index 37f5fe2b..737b3df8 100644
--- a/src/ipyautoui/demo_schemas/__init__.py
+++ b/src/ipyautoui/demo_schemas/__init__.py
@@ -5,7 +5,9 @@
 from ipyautoui.demo_schemas.override_ipywidgets import OverrideIpywidgets
 from ipyautoui.demo_schemas.complex_serialization import ComplexSerialisation
 from ipyautoui.demo_schemas.nested import Nested
+from ipyautoui.demo_schemas.null_and_required import NullAndRequired
 from ipyautoui.demo_schemas.editable_datagrid import EditableGrid
+from ipyautoui.demo_schemas.multiindex_editable_grid import MultiIndexEditableGrid
 from ipyautoui.demo_schemas.nested_editable_datagrid import NestedEditableGrid
 from ipyautoui.demo_schemas.root_array import RootArray
 from ipyautoui.demo_schemas.root_simple import RootSimple
@@ -16,3 +18,6 @@
 from ipyautoui.demo_schemas.recursive_array import RecursiveArray
 from ipyautoui.demo_schemas.recursive_object import RecursiveObject
 from ipyautoui.demo_schemas.ruleset import ScheduleRuleSet
+from ipyautoui.demo_schemas.pydantic_validation import PydanticValidation
+from ipyautoui.demo_schemas.pydantic_validation_list import PydanticValidationList
+from ipyautoui.demo_schemas.pydantic_validation_error import PydanticValidationError
\ No newline at end of file
diff --git a/src/ipyautoui/demo_schemas/complex_serialization.py b/src/ipyautoui/demo_schemas/complex_serialization.py
index 21ca1974..79aedc4d 100644
--- a/src/ipyautoui/demo_schemas/complex_serialization.py
+++ b/src/ipyautoui/demo_schemas/complex_serialization.py
@@ -10,14 +10,17 @@ class ComplexSerialisation(BaseModel):
     """all of these types need to be serialised to json and parsed back to objects upon reading..."""
 
     file_chooser: pathlib.Path = pathlib.Path(".")
+    file_upload_to_dir: pathlib.Path = Field(
+        description="uploaded files automatically get dumped in cwd or fdir if defined for the AutoUi",
+        json_schema_extra=dict(autoui="ipyautoui.custom.FileUploadToDir"))
     date_picker_string: ty.Optional[
         date
     ] = (
         date.today()
-    )  # TODO: serialisation / parsing round trip not working... # TODO: fix this!
+    )
     naive_datetime_picker_string: ty.Optional[
         datetime
-    ] = datetime.now()  # TODO: update with ipywidgets-v8 # TODO: fix this!
+    ] = datetime.now()
     color_picker_ipywidgets: Color = "#f5f595"
     markdown_widget: str = Field(json_schema_extra=dict(format="markdown"))
     any_of: ty.Union[conint(ge=0, le=3), str] = Field(
diff --git a/src/ipyautoui/demo_schemas/core_ipywidgets.py b/src/ipyautoui/demo_schemas/core_ipywidgets.py
index 71672eb3..264b12dc 100644
--- a/src/ipyautoui/demo_schemas/core_ipywidgets.py
+++ b/src/ipyautoui/demo_schemas/core_ipywidgets.py
@@ -25,6 +25,24 @@ class Number(IntEnum):
     ONE = 1
     TWO = 2
     THREE = 3
+    
+class LongEnum(str, Enum):
+    a = "a"
+    b = "b"
+    c = "c"
+    d = "d"
+    e = "e"
+    f = "f"
+    g = "g"
+    h = "h"
+    i = "i"
+    j = "j"
+    k = "k"
+    l = "l"
+    m = "m"
+    n = "n"
+    o = "o"
+    p = "p"
 
 
 class CoreIpywidgets(BaseModel):
@@ -70,5 +88,9 @@ class CoreIpywidgets(BaseModel):
     textarea: Annotated[str, StringConstraints(min_length=0, max_length=800)] = Field(
         "long text " * 50, description="long text field"
     )
+    select_multiple: ty.List[FruitEnum] = Field((FruitEnum.apple,), description="a short select multiple list")
+    select_multiple_nullable: ty.Optional[ty.List[FruitEnum]] = Field(None, description="a short nullable select multiple list")
+    tags_input: ty.List[LongEnum] = Field((LongEnum.a,), description="a long select multiple list")
+    tags_input_nullable: ty.Optional[ty.List[LongEnum]] = Field((LongEnum.a,), description="a long nullable select multiple list")
 
     model_config = ConfigDict(json_schema_extra=dict(show_raw=True))
diff --git a/src/ipyautoui/demo_schemas/editable_datagrid.py b/src/ipyautoui/demo_schemas/editable_datagrid.py
index 471d811b..b0005268 100644
--- a/src/ipyautoui/demo_schemas/editable_datagrid.py
+++ b/src/ipyautoui/demo_schemas/editable_datagrid.py
@@ -9,10 +9,6 @@
         "floater": 3.14,
         "something_else": 324,
     },
-    # {"string": "update", "integer": 4, "floater": 3.12344, "something_else": 123},
-    # {"string": "evening", "integer": 5, "floater": 3.14, "something_else": 235},
-    # {"string": "morning", "integer": 5, "floater": 3.14, "something_else": 12},
-    # {"string": "number", "integer": 3, "floater": 3.14, "something_else": 123},
 ]
 
 
diff --git a/src/ipyautoui/demo_schemas/multiindex_editable_grid.py b/src/ipyautoui/demo_schemas/multiindex_editable_grid.py
new file mode 100644
index 00000000..74c58c43
--- /dev/null
+++ b/src/ipyautoui/demo_schemas/multiindex_editable_grid.py
@@ -0,0 +1,32 @@
+from ipyautoui.basemodel import BaseModel
+from pydantic import Field, RootModel, ConfigDict
+import typing as ty
+
+DATAGRID_TEST_VALUE = [
+    {
+        "string": "how long",
+        "integer": 1,
+        "floater": 3.14,
+        "something_else": 324,
+    },
+]
+
+
+class DataFrameCols(BaseModel):
+    string: str = Field("string", title=("string", "string"), json_schema_extra=dict(column_width=200))
+    integer: int = Field(1, title=("number", "integer"))
+    floater: float = Field(3.1415,title=("number", "integer"), json_schema_extra=dict(column_width=70))
+    something_else: float = Field(324,title=("number", "something_else"), json_schema_extra=dict(column_width=100))
+
+
+class MultiIndexEditableGrid(RootModel):
+    root: ty.List[DataFrameCols]
+    model_config = ConfigDict(
+        json_schema_extra=dict(
+            default=DATAGRID_TEST_VALUE,
+            # default_factory=lambda: DATAGRID_TEST_VALUE, # TODO: AutoUi isn't getting data when set using default_factory. make this work!
+            format="DataFrame",
+            warn_on_delete=True,  # TODO: this isn't being passed
+            global_decimal_places=2,
+        )
+    )
diff --git a/src/ipyautoui/demo_schemas/nested.py b/src/ipyautoui/demo_schemas/nested.py
index 8d43e4b1..6f9b62e6 100644
--- a/src/ipyautoui/demo_schemas/nested.py
+++ b/src/ipyautoui/demo_schemas/nested.py
@@ -1,6 +1,25 @@
 from pydantic import Field, constr
 from ipyautoui.basemodel import BaseModel
 from typing_extensions import Annotated
+from enum import Enum
+
+class LongEnum(str, Enum):
+    a = "a"
+    b = "b"
+    c = "c"
+    d = "d"
+    e = "e"
+    f = "f"
+    g = "g"
+    h = "h"
+    i = "i"
+    j = "j"
+    k = "k"
+    l = "l"
+    m = "m"
+    n = "n"
+    o = "o"
+    p = "p"
 
 
 class NestedObject(BaseModel):
@@ -9,6 +28,7 @@ class NestedObject(BaseModel):
     string1: str = Field(default="adsf", description="a description about my string")
     int_slider1: Annotated[int, Field(ge=0, le=3)] = 2
     int_text1: int = 1
+    tags: list[LongEnum] = (LongEnum.a, )
 
 
 class RecursiveNest(BaseModel):
diff --git a/src/ipyautoui/demo_schemas/null_and_required.py b/src/ipyautoui/demo_schemas/null_and_required.py
index cebe0aac..1cb2f602 100644
--- a/src/ipyautoui/demo_schemas/null_and_required.py
+++ b/src/ipyautoui/demo_schemas/null_and_required.py
@@ -6,8 +6,6 @@
 from enum import Enum
 from pydantic import StringConstraints, Field, conint, confloat, ConfigDict
 from ipyautoui.basemodel import BaseModel
-import pandas as pd
-from pathlib import PurePosixPath
 from typing_extensions import Annotated
 
 
@@ -20,10 +18,10 @@ class NullAndRequired(BaseModel):
     # https://docs.pydantic.dev/latest/migration/#required-optional-and-nullable-fields
     f1: str = Field(description="Required, cannot be None")
     f2: ty.Optional[ty.Union[TestEnum, str]] = Field(
-        "a", description="Not required, can be None, is 'abc' by default"
+        "a", description="Not required, can be None, is 'a' by default"
     )
     f3: ty.Union[TestEnum, str] = Field(
-        "a", description="Not required, cannot be None, is 'abc' by default"
+        "a", description="Not required, cannot be None, is 'a' by default"
     )
     f4: ty.Optional[str] = Field(description="Required, can be None")  #
     f5: ty.Optional[str] = Field(
diff --git a/src/ipyautoui/demo_schemas/pydantic_validation.py b/src/ipyautoui/demo_schemas/pydantic_validation.py
new file mode 100644
index 00000000..e499a94a
--- /dev/null
+++ b/src/ipyautoui/demo_schemas/pydantic_validation.py
@@ -0,0 +1,13 @@
+from pydantic import BaseModel, field_validator, ValidationInfo, Field
+
+class PydanticValidation(BaseModel):
+    a: int = 1
+    b: int = 3
+    c: int = Field(0, description=" = A + B", json_schema_extra=dict(disabled=True))
+
+    @field_validator('c')
+    @classmethod
+    def _c(cls, v: int, info: ValidationInfo) -> int:
+        v = info.data.get('a') + info.data.get('b')
+        return v
+    
\ No newline at end of file
diff --git a/src/ipyautoui/demo_schemas/pydantic_validation_error.py b/src/ipyautoui/demo_schemas/pydantic_validation_error.py
new file mode 100644
index 00000000..e85c3db2
--- /dev/null
+++ b/src/ipyautoui/demo_schemas/pydantic_validation_error.py
@@ -0,0 +1,14 @@
+from pydantic import BaseModel, field_validator, ValidationInfo, Field
+
+
+class PydanticValidationError(BaseModel):
+    a: int = 1
+    b: int = 3
+    c: int = Field(0, description=" = A + B")
+
+    @field_validator('c')
+    @classmethod
+    def _c(cls, v: int, info: ValidationInfo) -> int:
+        if v !=  info.data.get('a') + info.data.get('b'):
+            raise ValueError("C must equal A + B")
+        return v
\ No newline at end of file
diff --git a/src/ipyautoui/demo_schemas/pydantic_validation_list.py b/src/ipyautoui/demo_schemas/pydantic_validation_list.py
new file mode 100644
index 00000000..a09fba49
--- /dev/null
+++ b/src/ipyautoui/demo_schemas/pydantic_validation_list.py
@@ -0,0 +1,14 @@
+from pydantic import BaseModel, field_validator, Field
+
+class PydanticValidationList(BaseModel):
+    a: list[str] = Field(description="a list of strings that must contain 'asdf' as the first item.\n If not, it will be added by pydantic validation")
+    b: str
+
+    @field_validator("a")
+    @classmethod
+    def add_val(cls, v: list) -> list:
+        if len(v) == 0:
+            v = ["asdf"] + v
+        if v[0] != "asdf":
+            v[0] = "asdf"
+        return v
diff --git a/src/ipyautoui/demo_schemas/ruleset.py b/src/ipyautoui/demo_schemas/ruleset.py
index 2f34f53b..2e42e1bf 100644
--- a/src/ipyautoui/demo_schemas/ruleset.py
+++ b/src/ipyautoui/demo_schemas/ruleset.py
@@ -15,17 +15,48 @@
 # ---
 
 # %%
-# %run ../_dev_sys_path_append.py
+# %run ../_dev_maplocal_params.py
 # %load_ext lab_black
 
 # %%
 from enum import Enum
-from pydantic import ConfigDict, BaseModel, Field, RootModel
+from pydantic import (
+    ConfigDict,
+    BaseModel,
+    Field,
+    RootModel,
+    ValidationInfo,
+    field_validator,
+    ValidationError,
+)
 from ipyautoui import AutoUi
-from ipyautoui.autoobject import AutoObject
+from ipyautoui.autoobject import AutoObjectForm
 from ipyautoui._utils import html_link
 import typing as ty
 from enum import Enum
+from jsonref import replace_refs
+
+
+# %%
+def get_property_names():
+    return {
+        "Abbreviation": 1,
+        "AirflowDensity": 11,
+        "Classification.Uniclass.Pr.Description": 6,
+        "Classification.Uniclass.Pr.Number": 5,
+        "Classification.Uniclass.Ss.Description": 8,
+        "Classification.Uniclass.Ss.Number": 7,
+        "InstanceReference": 4,
+        "Notes": 10,
+        "OverallLength": 9,
+        "PipingPressure": 12,
+        "Symbol": 2,
+        "TypeReference": 3,
+    }
+
+
+PR_CLASSIFICATION = "Classification.Uniclass.Pr.Number"
+SS_CLASSIFICATION = "Classification.Uniclass.Ss.Number"
 
 
 # %%
@@ -89,9 +120,12 @@ class CategoriesEnum(RootModel):
 
 
 class Rule(BaseModel):
-    categories: list[CategoriesEnum] = Field(  # RevitCategoriesEnum
+    categories: list[
+        CategoriesEnum
+    ] = Field(  # BUG: this doesn't work if it is ty.Optional ...
         title="Categories",  # TODO: this is pydantic bug (should generate title from field name)
         description="Revit MEP categories to filter by (i.e. revit object must belong to categories defined here). If empty, all categories are included.",
+        json_schema_extra=dict(allow_duplicates=False),
     )
     parameter: str = Field(
         description="name of schedule parameter against which to apply filter rule",
@@ -109,10 +143,33 @@ class Rule(BaseModel):
             "ab",
         ],  # get_uniclass_product_codes() + get_uniclass_system_codes(),
     )
-    model_config = ConfigDict(json_schema_extra=dict(autoui="ipyautoui.demo_schemas.ruleset.rule_ui"))
+    model_config = ConfigDict(
+        json_schema_extra=dict(autoui="ipyautoui.demo_schemas.ruleset.rule_ui")
+    )
     # NOTE: can use "__main__.rule_ui" if you import it into the location you want to use it
 
 
+class RulePatch(Rule):
+    parameter_id: ty.Optional[int]
+
+    @field_validator("parameter_id")
+    @classmethod
+    def _c(cls, v: int, info: ValidationInfo) -> int:
+        try:
+            v = get_property_names()[info.data.get("parameter")]
+        except:
+            v = None
+            # raise ValidationError("could not find that parameter in the database...")
+        return v
+
+    # model_config = ConfigDict(
+    #     json_schema_extra=dict(
+    #         autoui="ipyautoui.demo_schemas.ruleset.rule_ui",
+    #         order=["categories", "parameter", "operator", "value"],
+    #     )
+    # )
+
+
 URL_REVIT_FILTERS = "https://help.autodesk.com/view/RVT/2023/ENU/?guid=GUID-400FD74B-00E0-4573-B3AC-3965E65CBBDB"
 DI_UNICLASS_PR = {
     "Pr_15": "Pr_15 - Preparatory products",
@@ -131,10 +188,6 @@ class Rule(BaseModel):
 UniclassSystems.__doc__ = "A list of valid Uniclass System codes"
 
 
-def get_property_names():  # TODO: overwrite this
-    return ["Product Classification", "System Classification"]
-
-
 def get_uniclass_product_codes():
     return UniclassProducts._member_names_
 
@@ -144,19 +197,22 @@ def get_uniclass_system_codes():
 
 
 def get_value_kwargs(property_name):
-    if property_name == "Product Classification":
+    if property_name == PR_CLASSIFICATION:
         return {"ensure_option": True, "options": get_uniclass_product_codes()}
-    elif property_name == "System Classification":
+    elif property_name == SS_CLASSIFICATION:
         return {"ensure_option": True, "options": get_uniclass_system_codes()}
     else:
         return {"ensure_option": False, "options": []}
 
 
-class RuleUi(AutoObject):  # RuleUi extends AutoObject allowing customisation
+class RuleUi(AutoObjectForm):  # RuleUi extends AutoObjectForm allowing customisation
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
-        self.di_widgets["parameter"].options = get_property_names()
+
+    def _post_init(self, **kwargs):
+        self.di_widgets["parameter"].options = list(get_property_names().keys())
         self._init_RuleUi_controls()
+        self.order = ["categories", "parameter", "operator", "value"]
 
     def _init_RuleUi_controls(self):
         self.di_widgets["parameter"].observe(self._update_rule_value, "value")
@@ -174,7 +230,7 @@ def _update_rule_value(self, on_change):
 
 
 def rule_ui(value=None, **kwargs):
-    ui = RuleUi.from_schema(Rule)
+    ui = RuleUi.from_pydantic_model(RulePatch)
     ui.open_nested = True
     if value is not None:
         ui.value = value
@@ -185,8 +241,10 @@ def rule_ui(value=None, **kwargs):
 
 
 class ScheduleRuleSet(BaseModel):
-    set_type: RuleSetType = Field(default=RuleSetType.AND, disabled=True)
-    rule_sets: ty.List[ty.Union[Rule, ScheduleRuleSet]] = Field(
+    set_type: RuleSetType = Field(
+        default=RuleSetType.AND, autoui="ipywidgets.ToggleButtons"
+    )
+    rule_sets: ty.List[ty.Union[RulePatch, ScheduleRuleSet]] = Field(
         description="""
 rules return a boolean for the logical evaluation defined below for every item within the categories defined
 """
@@ -210,7 +268,18 @@ class ScheduleRuleSet(BaseModel):
     from IPython.display import display
 
     # m, s = _init_model_schema(ObjSet)
-    ui = AutoObject.from_schema(ScheduleRuleSet)
+    ui = AutoObjectForm.from_pydantic_model(ScheduleRuleSet)
     display(ui)
 
 # %%
+if __name__ == "__main__":
+    import yaml
+    from ipyautoui.autodisplay_renderers import preview_yaml_string
+
+    display(preview_yaml_string(yaml.dump(ui.value)))
+
+# %%
+if __name__ == "__main__":
+    display(ui.value)
+
+# %%
diff --git a/src/ipyautoui/watch_validate.py b/src/ipyautoui/watch_validate.py
new file mode 100644
index 00000000..57233340
--- /dev/null
+++ b/src/ipyautoui/watch_validate.py
@@ -0,0 +1,160 @@
+import traitlets as tr
+import typing as ty
+import ipywidgets as w
+from IPython.display import clear_output
+import contextlib
+from pydantic import BaseModel, RootModel, ValidationError
+from jsonref import replace_refs
+import logging
+logger = logging.getLogger(__name__)
+
+def pydantic_validate(model, value):
+    return model.model_validate(value).model_dump(mode="json")
+
+class _WatchSilent(tr.HasTraits):  # TODO: contains context manager for silencing traits
+    pass
+
+class WatchValidate(tr.HasTraits):  # TODO: _WatchValidate
+    error = tr.Unicode(default_value=None, allow_none=True)
+    schema = tr.Dict(default_value=None, allow_none=True)
+    model = tr.Type(klass=BaseModel, default_value=None, allow_none=True)
+    # show_validation = tr.Bool(default_value=True)
+    _value = tr.Any()  # TODO: update trait type on schema change
+    _silent = tr.Bool(default_value=False)
+
+    @contextlib.contextmanager
+    def silence_autoui_traits(self):
+        self._silent = True
+        yield
+        self._silent = False
+
+    @tr.observe("error")
+    def _error(self, on_change):
+        if self.error is None:
+            with self.out_error:
+                clear_output()
+            self.out_error.layout = w.Layout(border=None, display="None")
+            self.is_valid.value = True
+        else:
+            self.out_error.layout = w.Layout(border='2px solid red', display="")
+            with self.out_error:
+                clear_output()
+                print(self.error)
+            self.is_valid.value = False
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value: ty.Any):
+        if value != self._value:
+            with self.hold_trait_notifications():
+                # these means that change events will be squashed
+                # and trigger after all widgets have changed
+                self._value = value
+                # NOTE: it is required to set the whole "_value" otherwise
+                #       traitlets doesn't register the change.
+                with self.silence_autoui_traits():
+                    self._update_widgets_from_value()
+
+    def _validate_value(self, v):
+        if self.model is not None:
+            try:
+                v_ = pydantic_validate(self.model, v)
+                self.error = None
+            except ValidationError as e:
+                self.error = str(e)
+                v_ = v
+            if v_ != v:
+                try:
+                    with self.silence_autoui_traits():
+                        # silence trait notications to avoid infinite loop
+                        # and push validated value back to widgets
+                        self.value = v_
+                except Exception as e:
+                    self.value = v_
+            else:
+                self._value = v_
+        else:
+            self._value = v
+
+    def _watch_validate_change(self, on_change):
+        # NOTE: this method is intended for container widgets and is called
+        #       when any child widget changes. that is why the _get_value()
+        #       method required. it gets the value from all child widgets.
+
+        # TODO: add log of on_change...
+        if not self._silent:
+            # NOTE: this code only run when triggered by a change in a UI
+            #       when value is forced in by the value setter it does not run
+            v = self._get_value()
+            if v != self._value:
+                self._validate_value(v)
+                if hasattr(self, "savebuttonbar"):
+                    self.savebuttonbar.unsaved_changes = True
+
+    @classmethod
+    def from_jsonschema(cls, schema: dict, value: ty.Any = None, **kwargs):
+        if not isinstance(schema, dict):
+            raise ValueError(f"schema must be a dict of type jsonschema, not {type(schema)}")
+        else:
+            model = None
+            if "$defs" in schema.keys():
+                try:
+                    schema = replace_refs(schema)
+                    schema = {k: v for k, v in schema.items() if k != "$defs"}
+                except ValueError as e:
+                    logger.warning(f"replace_refs error: \n{e}")
+                    pass
+        if value is not None:
+            schema["value"] = value
+        schema = {**schema, **kwargs}
+        ui = cls(**schema)
+        ui.schema = schema
+        return ui
+
+    @classmethod
+    def from_pydantic_model(cls, model: ty.Type[BaseModel], value: ty.Any = None, **kwargs):
+        if not issubclass(model, BaseModel) or issubclass(model, RootModel):
+            raise ValueError(f"schema must be a pydantic model, not {type(model)}")
+        else:
+            schema = replace_refs(model.model_json_schema())
+            schema = {k: v for k, v in schema.items() if k != "$defs"}
+        if value is not None:
+            schema["value"] = value
+        schema = {**schema, **kwargs}
+        ui = cls(**schema)
+        ui.model = model
+        ui.schema = schema
+        ui._init_validation_error()
+        ui._validate_value(ui.value)
+        return ui
+
+    def _init_validation_error(self):
+        if self.model is not None:
+            self.out_error = w.Output()
+            self.is_valid = w.Valid(value=True)
+            self.vbx_error = w.VBox([self.is_valid, self.out_error])
+            self.children = [self.vbx_error] + list(self.children)
+
+    # -----------------------------------------------------------
+    # implement these methods in your class that uses validation:
+    # i.e. AutoObject, AutoArray, EditGrid, etc.
+    # ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+
+    def _update_widgets_from_value(self):
+        # NOTE: fn name requried by WatchValidate base class
+        with self.silence_autoui_traits():
+            pass  # NOTE: implement this method in your class
+
+    def _get_value(self, **kwargs):
+        # NOTE: fn name requried by WatchValidate base class
+        pass  # NOTE: implement this method in your class
+
+    def _init_watcher(self):
+        # NOTE: implement a method in your class
+        #       it must call `_watch_validate_change` on change
+        #       of any child widget. `_init_watcher` name is not
+        #       required by this base class.
+        pass


Test patch:
diff --git a/docs/test.complexaui.json b/docs/test.complexaui.json
index 19044298..1d526ed3 100644
--- a/docs/test.complexaui.json
+++ b/docs/test.complexaui.json
@@ -1,24 +1,3 @@
 {
-    "text": "adsf",
-    "int_slider": 2,
-    "int_text": 1,
-    "int_range_slider": [
-        0,
-        3
-    ],
-    "float_slider": 2.2,
-    "float_text": 2.2,
-    "float_text_locked": 2.2,
-    "float_range_slider": [
-        0.0,
-        2.2
-    ],
-    "checkbox": true,
-    "dropdown": "male",
-    "dropdown_edge_case": "female",
-    "dropdown_simple": "asd",
-    "combobox": "asd",
-    "text_short": "short text",
-    "text_area": "long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text ",
-    "auto_markdown": "\nSee details here: [__commonmark__](https://commonmark.org/help/)\n\nor press the question mark button. \n"
+    "text": "this is a value"
 }
\ No newline at end of file
diff --git a/docs/test.simpleaui.json b/docs/test.simpleaui.json
index bd7eed1c..1d526ed3 100644
--- a/docs/test.simpleaui.json
+++ b/docs/test.simpleaui.json
@@ -1,3 +1,3 @@
 {
-    "text": "Test"
+    "text": "this is a value"
 }
\ No newline at end of file
diff --git a/tests/custom/test_fileupload.py b/tests/custom/test_fileupload.py
index ff8d9835..2b394da3 100644
--- a/tests/custom/test_fileupload.py
+++ b/tests/custom/test_fileupload.py
@@ -6,5 +6,5 @@
 
 class TestFileUi:
     def test_file_ui(self):
-        f = FilesUploadToDir(["test_buttonbars.py"])
+        f = FilesUploadToDir(value=["test_buttonbars.py"])
         assert f.value == ["test_buttonbars.py"]
diff --git a/tests/test_autogrid.py b/tests/test_autogrid.py
index c90737d2..bcd5ef2d 100644
--- a/tests/test_autogrid.py
+++ b/tests/test_autogrid.py
@@ -124,6 +124,51 @@ class TestGridSchema(RootModel):
                 ),
             )
         )
+        
+    @pytest.mark.skip(
+        reason="TODO: fix this. issue about retrieving title from anyOf schema"
+    )
+    def test_nullable_multiindex(self):
+        class TestProperties(BaseModel):
+            string: ty.Optional[str] = Field(column_width=100, section="a")
+            floater: ty.Optional[float] = Field(
+                1.5, column_width=70, global_decimal_places=3, section="b"
+            )
+            inty: int = Field(1, section="b")
+
+        class TestGridSchema(RootModel):
+            """no default"""
+
+            root: ty.List[TestProperties] = Field(
+                [TestProperties(string="string").model_dump()],
+                format="dataframe",
+                datagrid_index_name=("section", "title"),
+            )
+
+        model, schema = _init_model_schema(TestGridSchema)
+        gridschema = GridSchema(schema)
+
+        assert gridschema.is_multiindex == True
+        assert gridschema.index.equals(
+            pd.MultiIndex.from_tuples(
+                [("a", "String"), ("b", "Floater"), ("b", "Inty")],
+                names=("section", "title"),
+            )
+        )
+        assert gridschema._get_default_data() == [
+            {"string": "string", "floater": 1.5, "inty": 1}
+        ]
+        assert gridschema.default_row == {"floater": 1.5, "inty": 1, "string": None}
+        # TODO: this doesn't make that much sense ans string=None is not allowed...
+        assert gridschema.get_default_dataframe().equals(
+            pd.DataFrame(
+                [{("a", "String"): "string", ("b", "Floater"): 1.5, ("b", "Inty"): 1}],
+                columns=pd.MultiIndex.from_tuples(
+                    [("a", "String"), ("b", "Floater"), ("b", "Inty")],
+                    names=("section", "title"),
+                ),
+            )
+        )
 
     def test_get_field_names_from_properties(self):
         class TestProperties(BaseModel):
diff --git a/tests/test_automapschema.py b/tests/test_automapschema.py
index 803f8c0e..cf594350 100644
--- a/tests/test_automapschema.py
+++ b/tests/test_automapschema.py
@@ -8,6 +8,7 @@
     ArrayObjectDataframe,
     CoreIpywidgets,
     ComplexSerialisation,
+    RootEnum
 )
 import typing as ty
 from typing_extensions import Annotated
diff --git a/tests/test_autoobject.py b/tests/test_autoobject.py
index 30c28568..af829ddd 100644
--- a/tests/test_autoobject.py
+++ b/tests/test_autoobject.py
@@ -1,4 +1,4 @@
-from pydantic import BaseModel, Field, RootModel
+from pydantic import BaseModel, Field, RootModel, ConfigDict
 from ipyautoui.autoobject import AutoObject
 import pytest
 import stringcase
@@ -10,21 +10,21 @@ class TestAutoObject:
     def test_root(self):
         with pytest.raises(ValueError) as e:
             ExampleRoot = RootModel[str]
-            ui = AutoObject.from_schema(ExampleRoot)
+            ui = AutoObject.from_pydantic_model(ExampleRoot)
             assert e == "properties must be in kwargs"
 
     def test_simple(self):
         class ExampleSchema(BaseModel):
             text: str = Field(default="Test", description="This test is important")
 
-        ui = AutoObject.from_schema(ExampleSchema)
+        ui = AutoObject.from_pydantic_model(ExampleSchema)
         assert ui.value == {"text": "Test"}
         print("done")
 
 
 class TestAutoObjectRowOrder:
     def test_order(self):
-        ui = AutoObject.from_schema(CoreIpywidgets)
+        ui = AutoObject.from_pydantic_model(CoreIpywidgets)
 
         ui.order_can_hide_rows = False
         # ui.order =
@@ -42,6 +42,19 @@ def test_order(self):
         ui.order_can_hide_rows = False
         assert ui.order == ui.default_order
         assert len(ui.vbx_main.children) == len(ui.default_order)
+        
+
+    def test_order_on_init(self):
+        class ExampleSchema(BaseModel):
+            a: str = Field(default="Test", description="This test is important")
+            b: str = Field(default="Test1", description="This test is important too")
+            model_config = ConfigDict(json_schema_extra=dict(order=["a"]))
+            
+        ui = AutoObject.from_pydantic_model(ExampleSchema)
+        assert ui.value == {"a": "Test", "b": "Test1"}
+        assert len(ui.vbx_main.children) == 1
+
+        
 
 class TestAnyOf:
     def test_recursive_anyof(self):
@@ -61,6 +74,6 @@ class ObjSet(BaseModel):
             op_type: RuleSetType
             obj_set: list[ty.Union[Obj, ObjSet]]
 
-        ui = AutoObject.from_schema(ObjSet)
+        ui = AutoObject.from_pydantic_model(ObjSet)
         assert "anyOf" in ui.di_callers['obj_set'].kwargs["items"]
 
diff --git a/tests/test_autoui.py b/tests/test_autoui.py
index 271e3602..a307b57a 100644
--- a/tests/test_autoui.py
+++ b/tests/test_autoui.py
@@ -12,8 +12,10 @@
 from ipyautoui import AutoUi
 from ipyautoui.demo_schemas import CoreIpywidgets
 from ipyautoui.basemodel import file
-import json
 import pytest
+from pydantic import field_validator, BaseModel
+from enum import Enum
+
 
 DIR_TEST_DATA = DIR_TESTS / "testdata"
 DIR_TEST_DATA.mkdir(parents=True, exist_ok=True)
@@ -30,6 +32,10 @@
 file(changed, PATH_TEST_AUTO_READ_FILE)
 
 
+
+    
+
+
 class TestAutoUi:
     @pytest.mark.skip(
         reason="this fails, as widgets auto sets the value...? TODO: resovle."
@@ -52,7 +58,83 @@ def test_pass_kwargs(self):
         )
         assert ui.savebuttonbar.layout.display == "None"
         print("done")
+        
+    def test_pydantic_validation(self):
+        
+        class Test(BaseModel):
+            a: str
+            b: str
+
+            @field_validator("a")
+            @classmethod
+            def name_must(cls, v: str) -> str:
+                return "asdf"
+            
+        ui = AutoUi(Test)
+        ui.autowidget.di_widgets["a"].value = "my val"
+        assert ui.value == {"a": "asdf", "b":""}
+        ui.autowidget.di_widgets["b"].value = "my val"
+        assert ui.value == {"a": "asdf", "b":"my val"}
+        
+    def test_pydantic_validation_list(self):
+        
+        class TestList(BaseModel):
+            a: list[str]
+            b: str
+
+            @field_validator("a")
+            @classmethod
+            def add_val(cls, v: str) -> str:
+                if "asdf" not in v:
+                    v = ["asdf"] + v
+                return v
+        ui = AutoUi(TestList)
+        ui.autowidget.di_widgets["a"].value = []
+        v = ui.value
+        assert v == {"a": ["asdf"], "b":""}
+        ui.autowidget.di_widgets["a"].li_widgets[0].value = "a"
+        v = ui.value
+        assert v == {"a": ["asdf","a"], "b":""}
+        ui.autowidget.di_widgets["b"].value = "my val"
+        v = ui.value
+        assert ui.value == {"a": ["asdf","a"], "b":"my val"}
+        
+        
+    def test_pydantic_validation_list_enums(self):
+        class RoleEnum(Enum):
+            director = "Director in Charge"
+            lead_crm = "Client Relationship Management (CRM) Lead"
+        
+        class Obj(BaseModel):
+            c: RoleEnum = RoleEnum.director
+            d: int =0
+        
+        class TestListEnums(BaseModel):
+            a: list[Obj]
+            b: str
+
+            @field_validator("a")
+            @classmethod
+            def _document_role(cls, v):
+                li = [_.c for _ in v]
+                if RoleEnum.director not in li:
+                    v = [Obj(**{"a": RoleEnum.director})] + v
+                return v
+            
+        ui = AutoUi(TestListEnums)
+        
+        # ui.autowidget.di_widgets["a"].value = [Obj()]
+        # v = ui.value
+        # assert v == {"a": [{"c": "Director in Charge", "d":0}], "b":""}
+        ui.value = {"a": [{"c": "Client Relationship Management (CRM) Lead", "d":0}], "b":""}
+        v = ui.value
+        assert v["a"][1]["c"] == "Client Relationship Management (CRM) Lead"
+        assert v == {"a": [{"c": "Director in Charge", "d":0}, {"c": "Client Relationship Management (CRM) Lead", "d":0}], "b":""}
+
+
 
     # def test_display_file(self):
     #     fpths = list(pathlib.Path(DIR_FILETYPES).glob("*"))
     #     d0 = DisplayFile(fpths[0])
+
+
diff --git a/tests/test_demo_schemas.py b/tests/test_demo_schemas.py
index 9d58ef5b..c5e4582a 100644
--- a/tests/test_demo_schemas.py
+++ b/tests/test_demo_schemas.py
@@ -4,11 +4,13 @@
     CoreIpywidgets,
     ArrayObjectDataframe,
     OverrideIpywidgets,
+    EditableGrid,
+    ScheduleRuleSet,
+    RootEnum
     # ScheduleRuleSet,
 )
 from ipyautoui import AutoUi
 
-
 # NOTE: refer also to test_automapschema.py to ensure mappings are correct
 def assert_no_error_widget_in_autoui(autoui):
     for k, wi in autoui.autowidget.di_widgets.items():
@@ -38,7 +40,19 @@ def test_OverrideIpywidgets():
 
 
 def test_ScheduleRuleSet():
-    from ipyautoui.demo_schemas.ruleset import ScheduleRuleSet
 
     autoui = AutoUi(ScheduleRuleSet)
     assert_no_error_widget_in_autoui(autoui)
+    
+def test_EditableGrid():
+    
+    autoui = AutoUi(EditableGrid)
+    assert "EditGrid" in str(autoui.autowidget)
+    
+def test_RootEnum():
+    from ipyautoui.demo_schemas import RootEnum
+    
+    autoui = AutoUi(RootEnum)
+    assert "SelectMultiple" in str(autoui.autowidget)
+    # autoui.add_row()
+    # assert "SelectMultiple" in str(autoui.li_widgets[0])
\ No newline at end of file
diff --git a/tests/test_maplocal_openfile.py b/tests/test_maplocal_openfile.py
index 134f7c2f..6c366737 100644
--- a/tests/test_maplocal_openfile.py
+++ b/tests/test_maplocal_openfile.py
@@ -1,26 +1,28 @@
-# import pathlib
-# import typing as ty
+import pathlib
+import typing as ty
 
-# # NOTE: this requires user specific configuration of the .env file to work
-# from maplocal import maplocal, openlocal
-# from maplocal.maplocal import MAPENV
+# NOTE: this requires user specific configuration of the .env file to work
+from maplocal import maplocal, openlocal, maplocal_openlocal_exists
+from maplocal.maplocal import MAPENV
 
+if maplocal_openlocal_exists():
+    class TestMAPENV:
+        def test_MAPENV(self):
+            assert MAPENV.MAPLOCAL_FROM == pathlib.PurePosixPath("/home")
+            assert MAPENV.MAPLOCAL_TO == pathlib.PureWindowsPath(
+                "//wsl.localhost/Ubuntu/home"
+            )
 
-# class TestMAPENV:
-#     def test_MAPENV(self):
-#         assert MAPENV.MAPLOCAL_FROM == pathlib.PurePosixPath("/home")
-#         assert MAPENV.MAPLOCAL_TO == pathlib.PureWindowsPath(
-#             "//wsl.localhost/20221021/home"
-#         )
 
+    class TestEnv:
+        # @pytest.mark.usefixtures("mock_env_user")
+        def test_dev_env(self):
 
-# class TestEnv:
-#     # @pytest.mark.usefixtures("mock_env_user")
-#     def test_dev_env(self):
-
-#         assert isinstance(MAPENV.openpath, ty.Callable)
-#         p = pathlib.Path(__file__).resolve()
-#         assert maplocal(p) == pathlib.PureWindowsPath(
-#             "//wsl.localhost/20221021/home/jovyan/ipyautoui/tests/test_maplocal_openfile.py"
-#         )
-#         openlocal(p)
+            assert isinstance(MAPENV.openpath, ty.Callable)
+            p = pathlib.Path(__file__).resolve()
+            assert maplocal(p) == pathlib.PureWindowsPath(
+                "//wsl.localhost/Ubuntu/home/jovyan/ipyautoui/tests/test_maplocal_openfile.py"
+            )
+            openlocal(p)
+else:
+    pass
\ No newline at end of file
diff --git a/tests/testdata/test.aui.json b/tests/testdata/test.aui.json
index 481a12d2..09edab5d 100644
--- a/tests/testdata/test.aui.json
+++ b/tests/testdata/test.aui.json
@@ -1,72 +1,33 @@
 {
-    "string": "adsf",
+    "int_slider_req": 2,
+    "int_slider_nullable": null,
     "int_slider": 2,
-    "int_text": 1,
+    "int_text_req": 3,
+    "int_text_nullable": null,
     "int_range_slider": [
         0,
         3
     ],
+    "int_range_slider_disabled": [
+        0,
+        3
+    ],
     "float_slider": 2.2,
     "float_text": 2.2,
+    "float_text_locked": 2.2,
     "float_range_slider": [
-        0,
+        0.0,
         2.2
     ],
     "checkbox": true,
     "dropdown": null,
+    "dropdown_int": 1,
+    "dropdown_int_optional": 1,
+    "combobox": "apple",
+    "combobox1": "apple",
+    "dropdown_edge_case": "apple",
     "dropdown_simple": "asd",
-    "combobox": "asd",
-    "text": "short text",
-    "text_area": "long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text ",
-    "markdown": "\nSee details here: [__commonmark__](https://commonmark.org/help/)\n\nor press the question mark button. \n",
-    "complex_serialisation": null,
-    "select_multiple_non_constrained": [
-        "male",
-        "female"
-    ],
-    "select_multiple_from_list": [
-        "male",
-        "female"
-    ],
-    "select_multiple_search": [
-        "male",
-        "female"
-    ],
-    "array": [],
-    "objects_array": [],
-    "run_name": "000-lean-description",
-    "datagrid": [
-        {
-            "string": "important string",
-            "integer": 1,
-            "floater": 3.14,
-            "something_else": 324
-        },
-        {
-            "string": "update",
-            "integer": 4,
-            "floater": 3.12344,
-            "something_else": 123
-        },
-        {
-            "string": "evening",
-            "integer": 5,
-            "floater": 3.14,
-            "something_else": 235
-        },
-        {
-            "string": "morning",
-            "integer": 5,
-            "floater": 3.14,
-            "something_else": 12
-        },
-        {
-            "string": "number",
-            "integer": 3,
-            "floater": 3.14,
-            "something_else": 123
-        }
-    ],
-    "nested": null,
-    "recursive_nest": null
+    "text": "adsf",
+    "text_short": "short text",
+    "textarea": "long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text "
 }
\ No newline at end of file
diff --git a/tests/testdata/test_auto_read_file.json b/tests/testdata/test_auto_read_file.json
index e80f68d5..ab7161cf 100644
--- a/tests/testdata/test_auto_read_file.json
+++ b/tests/testdata/test_auto_read_file.json
@@ -29,5 +29,15 @@
     "dropdown_simple": "asd",
     "text": "adsf",
     "text_short": "short text",
-    "textarea": "long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text "
+    "textarea": "long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text long text ",
+    "select_multiple": [
+        "apple"
+    ],
+    "select_multiple_nullable": null,
+    "tags_input": [
+        "a"
+    ],
+    "tags_input_nullable": [
+        "a"
+    ]
 }
\ No newline at end of file