Repository: holoviz/panel

Issue:
Prepending panel to Template does not work.
#### ALL software version info

Panel 0.10.1

#### Description of expected behavior and the observed behavior

When `main` of a Template is a List I would expect to be able to prepend items to the list.

I need this functionality to enable a work flow where every application in our site automatically gets wrapped with a generic header (=Card with title, info and links). So we would develop our application via a Template and fill in main. But then we would prepend the info header automatically

![image](https://user-images.githubusercontent.com/42288570/97838301-2beaeb00-1ce0-11eb-89e2-7826e5e98481.png)

#### Complete, minimal, self-contained example code that reproduces the issue

```
import panel as pn

def _create_app():
    view = pn.pane.Markdown("App View")
    app=pn.template.MaterialTemplate()
    app.main[:]=[view] # There might be a list of elements

    # Something done automatically for all apps in the site
    info_card = pn.pane.Markdown("Info Card")
    app.main.insert(0, info_card)
    return app

def test_app():
    app = _create_app()
    main_items = [item[0] for item in app._render_items.values() if "main" in item[1]]

    assert main_items[0].object=="Info Card"
    assert main_items[1].object=="App View"

if __name__.startswith("bokeh"):
    _create_app().servable()
```

#### Screenshots or screencasts of the bug in action

**`python -m panel serve 'scripts\issue_render_items.py' --dev`**

![image](https://user-images.githubusercontent.com/42288570/97838698-0ad6ca00-1ce1-11eb-8eef-99ccc550ea72.png)


**`pytest 'scripts\issue_render_items.py'**

```
$ pytest 'scripts\issue_render_items.py'
Test session starts (platform: win32, Python 3.7.6, pytest 5.4.2, pytest-sugar 0.9.3)
rootdir: C:\repos\private\awesome-panel, inifile: pytest.ini
plugins: cov-2.8.1, mock-3.1.0, sugar-0.9.3
collecting ... 

――――――――――――――――――――――――――――――――――――――― test_app ―――――――――――――――――――――――――――――――――――――――

    def test_app():
        app = _create_app()
        main_items = [item[0] for item in app._render_items.values() if "main" in item[1]]

>       assert main_items[0].object=="Info Card"
E       AssertionError: assert 'App View' == 'Info Card'
E         - Info Card
E         + App View

scripts\issue_render_items.py:17: AssertionError

 scripts/issue_render_items.py ⨯                                          100% ██████████
=============================== short test summary info ================================
FAILED scripts/issue_render_items.py::test_app - AssertionError: assert 'App View' == ...

Results (1.47s):
       1 failed
         - scripts/issue_render_items.py:13 test_app
```



Discussion:

Resolving patch:
diff --git a/panel/template/base.py b/panel/template/base.py
--- a/panel/template/base.py
+++ b/panel/template/base.py
@@ -8,6 +8,7 @@
 import sys
 import uuid
 
+from collections import OrderedDict
 from functools import partial
 
 import param
@@ -68,7 +69,7 @@ def __init__(self, template=None, items=None, nb_template=None, **params):
         if isinstance(nb_template, string_types):
             nb_template = _Template(nb_template)
         self.nb_template = nb_template or template
-        self._render_items = {}
+        self._render_items = OrderedDict()
         self._render_variables = {}
         self._server = None
         self._layout = self._build_layout()
@@ -524,16 +525,18 @@ def _update_render_items(self, event):
         old = event.old if isinstance(event.old, list) else event.old.values()
         for obj in old:
             ref = str(id(obj))
-            if obj not in event.new and ref in self._render_items:
+            if ref in self._render_items:
                 del self._render_items[ref]
 
         labels = {}
         new = event.new if isinstance(event.new, list) else event.new.values()
         for obj in new:
             ref = str(id(obj))
-            labels[ref] = 'Content' if obj.name.startswith(type(obj).__name__) else obj.name
-            if ref not in self._render_items:
-                self._render_items[ref] = (obj, [tag])
+            if obj.name.startswith(type(obj).__name__):
+                labels[ref] = 'Content'
+            else:
+                labels[ref] = obj.name
+            self._render_items[ref] = (obj, [tag])
         tags = [tags for _, tags in self._render_items.values()]
         self._render_variables['nav'] = any('nav' in ts for ts in tags)
         self._render_variables['header'] = any('header' in ts for ts in tags)


Test patch:
diff --git a/panel/tests/test_template.py b/panel/tests/test_template.py
--- a/panel/tests/test_template.py
+++ b/panel/tests/test_template.py
@@ -19,13 +19,17 @@
 
 from panel.layout import Row
 from panel.pane import HoloViews, Markdown
-from panel.template import Template, BootstrapTemplate, GoldenTemplate, MaterialTemplate, ReactTemplate, VanillaTemplate
+from panel.template import (
+    BootstrapTemplate, GoldenTemplate, MaterialTemplate, ReactTemplate,
+    Template, VanillaTemplate
+)
 from panel.template.base import BasicTemplate
 from panel.widgets import FloatSlider
 
 from .util import hv_available
 
 TEMPLATES = [BootstrapTemplate, GoldenTemplate, MaterialTemplate, ReactTemplate, VanillaTemplate]
+LIST_TEMPLATES = [item for item in TEMPLATES if item is not ReactTemplate]
 
 template = """
 {% extends base %}
@@ -165,6 +169,25 @@ def test_react_template(document, comm):
                {'h': 4, 'i': '2', 'w': 6, 'x': 6, 'y': 0}]
     }
 
+@pytest.mark.parametrize(["template_class"], [(t,) for t in LIST_TEMPLATES])
+def test_list_template_insert_order(template_class):
+    template = template_class()
+
+    template.main.append(1)
+
+    template.main.insert(0, 0)
+
+    template.main.extend([2, 3])
+
+    objs = list(template._render_items.values())[2:]
+    ((obj1, tag1), (obj2, tag2), (obj3, tag3), (obj4, tag4)) = objs
+
+    assert tag1 == tag2 == tag3 == tag4 == ['main']
+    assert obj1.object == 0
+    assert obj2.object == 1
+    assert obj3.object == 2
+    assert obj4.object == 3
+
 @pytest.mark.parametrize(["template_class"], [(item,) for item in TEMPLATES])
 def test_constructor(template_class):
     item = Markdown("Hello World")
@@ -176,4 +199,4 @@ def test_constructor_grid_spec():
     item = Markdown("Hello World")
     grid = GridSpec(ncols=12)
     grid[0:2, 3:4]=item
-    ReactTemplate(main=grid)
\ No newline at end of file
+    ReactTemplate(main=grid)