Repository: bokeh/bokeh

Issue:
[FEATURE] js_link attr indexing
**Related problem**
With a standard `Slider` you can do:
```python
slider.js_link('value', figure.x_range, "start")
```
However if I now want to use a `RangeSlider` which has `value` of type `Tuple[Float, Float]` I cannot currently do:
```python
range_slider.js_link('value[0]', figure.x_range, "start")
```

More generally, I cannot use js_link for subscriptable attributes.

**Possible solution**
A simple solution would be to change the signature of js_link to, as Bryan mentioned in [this topic](https://discourse.bokeh.org/t/rangeslider-js-link-with-a-range-object/4772/5):
```python
def js_link(self, attr, other, other_attr, attr_selector=None):
        # is it possible to check if attr is subscriptable?
        if attr_selector is None:
            cb = CustomJS(args=dict(other=other), code="other.%s = this.%s" % (other_attr, attr))
        else:
            cb = CustomJS(args=dict(other=other), code="other.%s = this.%s[%d]" % (other_attr, attr, attr_selector))
```
Ideally, as mentioned in the comment, we would check if attr is subscriptable using some form of `get_attr(attr, "__getitem__")`. I'm not sure where the actual retrieval of the attr value occurs, though.

**Ideal solution**
Ideally, a user could essentially register a callback (`callable`) to retrieve a value from attr, this would be much more dynamic, the simplest case being `attr_selector = lambda attr: attr`, where it just returns the unsubscripted or altered `attr`. Again, I believe this depends on where the retrieval of the attr value occurs and how to attach the `attr_selector` callable.

**Additional context**
On version: 2.0.0dev6-36-gff2d2f628-dirty



Discussion:
@RyanRio I would only make one change to the "Possible Solution" which is that I think the selector could also reasonably be a string and not just an int (e.g. if the value is a dict/object)

Re: "Ideal Solution" I think at the point of allowing a JS callback code we've moved into being unhelpfully duplicative of `CustomJS`.  The added value of `js_link` over `CustomJS` is precisely that it *doesn't* have any place for user-supplied JS code. Or conversely: if you need JS code to do something, `CustomJS` already exists and is the best (and intended) option. 

Resolving patch:
diff --git a/bokeh/model.py b/bokeh/model.py
--- a/bokeh/model.py
+++ b/bokeh/model.py
@@ -452,7 +452,7 @@ def js_link(self, attr, other, other_attr, attr_selector=None):
 
         from bokeh.models import CustomJS
 
-        selector = f"[{attr_selector!r}]" if attr_selector else ""
+        selector = f"[{attr_selector!r}]" if attr_selector is not None else ""
         cb = CustomJS(args=dict(other=other), code=f"other.{other_attr} = this.{attr}{selector}")
 
         self.js_on_change(attr, cb)


Test patch:
diff --git a/tests/unit/bokeh/test_model.py b/tests/unit/bokeh/test_model.py
--- a/tests/unit/bokeh/test_model.py
+++ b/tests/unit/bokeh/test_model.py
@@ -141,6 +141,20 @@ def test_attr_selector_creates_customjs_int(self) -> None:
         assert cb.args == dict(other=m2)
         assert cb.code == "other.b = this.a[1]"
 
+    def test_attr_selector_creates_customjs_with_zero(self) -> None:
+        m1 = SomeModel()
+        m2 = SomeModel()
+        assert len(m1.js_property_callbacks) == 0
+        m1.js_link('a', m2, 'b', 0)
+        assert len(m1.js_property_callbacks) == 1
+        assert "change:a" in m1.js_property_callbacks
+        cbs = m1.js_property_callbacks["change:a"]
+        assert len(cbs) == 1
+        cb = cbs[0]
+        assert isinstance(cb, CustomJS)
+        assert cb.args == dict(other=m2)
+        assert cb.code == "other.b = this.a[0]"
+
     def test_attr_selector_creates_customjs_str(self) -> None:
         m1 = SomeModel()
         m2 = SomeModel()