Repository: holoviz/panel

Issue:
Enable setting the panel.Param name manually.
#### My Pain

Sometimes I have a `param.Parameterized` class with a lot of parameters. For layout reasons I might split them up into multiple `panel.Param`s. I would like to give these seperate names. 

As far as I can see this is not supported by `panel.Param`.

#### Example

As you can see the name is MyComponent instead of the custom one specified.

![image](https://user-images.githubusercontent.com/42288570/81545187-cd6c3200-9378-11ea-9949-53281361e507.png)

```python
import param
import panel as pn

class MyComponent(param.Parameterized):
    selection1 = param.ObjectSelector()
    selection2 = param.ObjectSelector()

component = MyComponent()
app = pn.Column(
    pn.Param(component, parameters=["selection1"], name="Primary Selections"),
    pn.Param(component, parameters=["selection2"], name="Secondary Selections"),
)
app.show()
```

#### Solution

Support this.

#### Workaround

As you can see below the layout is not nice. There is too much space (margin I guess) between things.

![image](https://user-images.githubusercontent.com/42288570/81545496-42d80280-9379-11ea-98b0-f0e68ae8cc06.png)

```python
import param
import panel as pn

class MyComponent(param.Parameterized):
    selection1 = param.ObjectSelector()
    selection2 = param.ObjectSelector()

component = MyComponent()
app = pn.Column(
    pn.pane.Markdown("# Primary Selections"),
    pn.Param(component, parameters=["selection1"], show_name=False),
    pn.pane.Markdown("# Secondary Selections"),
    pn.Param(component, parameters=["selection2"], show_name=False),
)
app.show()
```


Discussion:
It makes sense to support that I guess, I also encountered the same issue and used a similar workaround. I could give it a try since I've been working on `param.py` lately.
@philippjfr Does it make sense to you if I explicitly add a `name` parameter to `panel.Param` ?

It could be:
* set to whatever the user wants even if the keyword argument `parameters` isn't used such as with `pn.Param(MyComponent(name="A name"), name="Another name")` that would display *Another name* as a title
* set to `object.name` (how the title is currently determined) if the user doesn't provide `name`

Note that it would break the following test since the default name would no longer be `ParamXXXXX`:
https://github.com/holoviz/panel/blob/65fb0fba1287ca8bd9ebbbcfd6b918e30742dea3/panel/tests/test_param.py#L103
Yes, I absolutely think that makes sense.

Resolving patch:
diff --git a/panel/param.py b/panel/param.py
--- a/panel/param.py
+++ b/panel/param.py
@@ -94,6 +94,9 @@ class Param(PaneBase):
         usually to update the default Parameter values of the
         underlying parameterized object.""")
 
+    name = param.String(default='', doc="""
+        Title of the pane.""")
+
     parameters = param.List(default=[], doc="""
         If set this serves as a whitelist of parameters to display on
         the supplied Parameterized object.""")
@@ -151,6 +154,8 @@ def __init__(self, object=None, **params):
             object = object.cls if object.self is None else object.self
         if 'parameters' not in params and object is not None:
             params['parameters'] = [p for p in object.param if p != 'name']
+        if object and 'name' not in params:
+            params['name'] = param_name(object.name)
         super(Param, self).__init__(object, **params)
         self._updating = []
 
@@ -172,7 +177,7 @@ def __init__(self, object=None, **params):
                              'type or instance, found %s type.' %
                              type(layout).__name__)
         self.param.watch(self._update_widgets, [
-            'object', 'parameters', 'display_threshold', 'expand_button',
+            'object', 'parameters', 'name', 'display_threshold', 'expand_button',
             'expand', 'expand_layout', 'widgets', 'show_labels', 'show_name'])
         self._update_widgets()
 
@@ -201,7 +206,7 @@ def __repr__(self, depth=0):
     #----------------------------------------------------------------
 
     def _synced_params(self):
-        ignored_params = ['name', 'default_layout']
+        ignored_params = ['default_layout']
         return [p for p in Layoutable.param if p not in ignored_params]
 
     def _update_widgets(self, *events):
@@ -209,14 +214,15 @@ def _update_widgets(self, *events):
         for event in sorted(events, key=lambda x: x.name):
             if event.name == 'object':
                 if isinstance(event.new, param.parameterized.Parameters):
-                    self.object = object.cls if object.self is None else object.self
+                    self.object = event.new.cls if event.new.self is None else event.new.self
                     return
                 if event.new is None:
-                    parameters = None
+                    parameters = []
                 else:
                     parameters = [p for p in event.new.param if p != 'name']
+                    self.name = param_name(event.new.name)
             if event.name == 'parameters':
-                parameters = None if event.new == [] else event.new
+                parameters = [] if event.new == [] else event.new
 
         if parameters != [] and parameters != self.parameters:
             self.parameters = parameters
@@ -491,8 +497,7 @@ def _get_widgets(self):
         if self.expand_layout is Tabs:
             widgets = []
         elif self.show_name:
-            name = param_name(self.object.name)
-            widgets = [('_title', StaticText(value='<b>{0}</b>'.format(name)))]
+            widgets = [('_title', StaticText(value='<b>{0}</b>'.format(self.name)))]
         else:
             widgets = []
         widgets += [(pname, self.widget(pname)) for pname in self._ordered_params]


Test patch:
diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py
--- a/panel/tests/test_param.py
+++ b/panel/tests/test_param.py
@@ -85,6 +85,10 @@ class Test(param.Parameterized):
 
     assert repr(Pane(Test(), parameters=['a'])) == "Param(Test, parameters=['a'])"
 
+    # With a defined name.
+    test_pane = Pane(Test(), parameters=['a'], name='Another')
+    assert repr(test_pane) == "Param(Test, name='Another', parameters=['a'])"
+
 
 def test_get_root(document, comm):
 
@@ -469,6 +473,50 @@ class Test(param.Parameterized):
     assert widget.start == 0
     assert widget.end == 10
 
+    # Check when object is from parameters
+    pane.object = Test().param
+
+    assert len(model.children) == 2
+    title, widget = model.children
+
+    assert isinstance(title, Div)
+    assert title.text == '<b>Test</b>'
+
+    assert isinstance(widget, Slider)
+    assert widget.start == 0
+    assert widget.end == 10
+
+    # Check when object is None
+    pane.object = None
+
+    assert len(model.children) == 0
+
+
+def test_set_name(document, comm):
+    class Test(param.Parameterized):
+        a = param.Number(bounds=(0, 10))
+        b = param.String(default='A')
+
+    pane = Param(Test(), name='First')
+
+    model = pane.get_root(document, comm=comm)
+
+    assert len(model.children) == 3
+    title, slider, text = model.children
+    assert isinstance(title, Div)
+    # Check setting name displays in as a title
+    assert title.text == '<b>First</b>'
+    assert isinstance(slider, Slider)
+    assert isinstance(text, TextInput)
+
+    pane.name = 'Second'
+
+    assert len(model.children) == 3
+    title, _, _ = model.children
+    assert isinstance(title, Div)
+    # Check the title updates with name
+    assert title.text == '<b>Second</b>'
+
 
 def test_set_parameters(document, comm):
     class Test(param.Parameterized):