Skip to content

Commit

Permalink
Add Evented base class (#1959)
Browse files Browse the repository at this point in the history
* Add EventHandler to layer object

In combination with JsCode this makes it easier for users to
add `on` method calls for event handling without extending
Folium itself.

The functionality was inspired by PR #1866 by @yschopfer19.
The PR was not accepted yet, because of concerns with code
duplication. In the approach taken in the current PR, #1866 would
not be necessary anymore, as the requested changes could be added
completely in client code space.

* Make realtime inherit from Layer

* Changes after review comments by conengmo

* Updates after review comments

* Add extra docstring line

* Add Evented class

In Leaflet Evented is the parent class of both `L.Map` and `L.Layer`.
It adds the `on` method which can be used to add event handlers
to a leaflet object.

* Update folium/map.py

Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com>

* Update folium/map.py

Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com>

* As requested in review comment

---------

Co-authored-by: Frank Anema <33519926+Conengmo@users.noreply.github.com>
  • Loading branch information
hansthen and Conengmo committed Jun 2, 2024
1 parent b80e7e9 commit 9fb5f2a
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Utilities
---------------------

.. autoclass:: folium.utilities.JsCode
.. autoclass:: folium.elements.EventHandler


Plugins
Expand Down
76 changes: 76 additions & 0 deletions folium/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from branca.element import CssLink, Element, Figure, JavascriptLink, MacroElement
from jinja2 import Template

from folium.utilities import JsCode


class JSCSSMixin(Element):
"""Render links to external Javascript and CSS resources."""
Expand Down Expand Up @@ -46,6 +48,80 @@ def _add_link(self, name: str, url: str, default_list: List[Tuple[str, str]]):
default_list.append((name, url))


class EventHandler(MacroElement):
'''
Add javascript event handlers.
Examples
--------
>>> import folium
>>> from folium.utilities import JsCode
>>>
>>> m = folium.Map()
>>>
>>> geo_json_data = {
... "type": "FeatureCollection",
... "features": [
... {
... "type": "Feature",
... "geometry": {
... "type": "Polygon",
... "coordinates": [
... [
... [100.0, 0.0],
... [101.0, 0.0],
... [101.0, 1.0],
... [100.0, 1.0],
... [100.0, 0.0],
... ]
... ],
... },
... "properties": {"prop1": {"title": "Somewhere on Sumatra"}},
... }
... ],
... }
>>>
>>> g = folium.GeoJson(geo_json_data).add_to(m)
>>>
>>> highlight = JsCode(
... """
... function highlight(e) {
... e.target.original_color = e.layer.options.color;
... e.target.setStyle({ color: "green" });
... }
... """
... )
>>>
>>> reset = JsCode(
... """
... function reset(e) {
... e.target.setStyle({ color: e.target.original_color });
... }
... """
... )
>>>
>>> g.add_child(EventHandler("mouseover", highlight))
>>> g.add_child(EventHandler("mouseout", reset))
'''

_template = Template(
"""
{% macro script(this, kwargs) %}
{{ this._parent.get_name()}}.on(
{{ this.event|tojson}},
{{ this.handler.js_code }}
);
{% endmacro %}
"""
)

def __init__(self, event: str, handler: JsCode):
super().__init__()
self._name = "EventHandler"
self.event = event
self.handler = handler


class ElementAddToElement(MacroElement):
"""Abstract class to add an element to another element."""

Expand Down
6 changes: 3 additions & 3 deletions folium/folium.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import webbrowser
from typing import Any, List, Optional, Sequence, Union

from branca.element import Element, Figure, MacroElement
from branca.element import Element, Figure
from jinja2 import Template

from folium.elements import JSCSSMixin
from folium.map import FitBounds, Layer
from folium.map import Evented, FitBounds, Layer
from folium.raster_layers import TileLayer
from folium.utilities import (
TypeBounds,
Expand Down Expand Up @@ -79,7 +79,7 @@ def __init__(self, no_touch=False, disable_3d=False):
self.disable_3d = disable_3d


class Map(JSCSSMixin, MacroElement):
class Map(JSCSSMixin, Evented):
"""Create a Map with Folium and Leaflet.js
Generate a base map of given width and height with either default
Expand Down
21 changes: 19 additions & 2 deletions folium/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from branca.element import Element, Figure, Html, MacroElement
from jinja2 import Template

from folium.elements import ElementAddToElement
from folium.elements import ElementAddToElement, EventHandler
from folium.utilities import (
JsCode,
TypeBounds,
TypeJsonValue,
camelize,
Expand All @@ -21,7 +22,23 @@
)


class Layer(MacroElement):
class Evented(MacroElement):
"""The base class for Layer and Map
Adds the `on` method for event handling capabilities.
See https://leafletjs.com/reference.html#evented for
more in depth documentation. Please note that we have
only added the `on(<Object> eventMap)` variant of this
method using python keyword arguments.
"""

def on(self, **event_map: JsCode):
for event_type, handler in event_map.items():
self.add_child(EventHandler(event_type, handler))


class Layer(Evented):
"""An abstract class for everything that is a Layer on the map.
It will be used to define whether an object will be included in
LayerControls.
Expand Down
19 changes: 19 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import folium
from folium import Choropleth, ClickForMarker, GeoJson, Map, Popup
from folium.elements import EventHandler
from folium.utilities import JsCode


@pytest.fixture
Expand Down Expand Up @@ -283,6 +285,23 @@ def test_geojson_empty_features_with_styling():
m.get_root().render()


def test_geojson_event_handler():
"""Test that event handlers are properly generated"""
m = Map()
data = {"type": "FeatureCollection", "features": []}
geojson = GeoJson(data, style_function=lambda x: {}).add_to(m)
fn = JsCode(
"""
function f(e) {
console.log("only for testing")
}
"""
)
geojson.add_child(EventHandler("mouseover", fn))
rendered = m.get_root().render()
assert fn.js_code in rendered


def test_geometry_collection_get_bounds():
"""Assert #1599 is fixed"""
geojson_data = {
Expand Down

0 comments on commit 9fb5f2a

Please sign in to comment.