{"action":"created","issue":{"url":"https://api.github.com/repos/rstudio/leaflet/issues/60","labels_url":"https://api.github.com/repos/rstudio/leaflet/issues/60/labels{/name}","comments_url":"https://api.github.com/repos/rstudio/leaflet/issues/60/comments","events_url":"https://api.github.com/repos/rstudio/leaflet/issues/60/events","html_url":"https://github.com/rstudio/leaflet/pull/60","id":70756414,"number":60,"title":"Shiny interactivity","user":{"login":"jcheng5","id":129551,"avatar_url":"https://avatars.githubusercontent.com/u/129551?v=3","gravatar_id":"","url":"https://api.github.com/users/jcheng5","html_url":"https://github.com/jcheng5","followers_url":"https://api.github.com/users/jcheng5/followers","following_url":"https://api.github.com/users/jcheng5/following{/other_user}","gists_url":"https://api.github.com/users/jcheng5/gists{/gist_id}","starred_url":"https://api.github.com/users/jcheng5/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jcheng5/subscriptions","organizations_url":"https://api.github.com/users/jcheng5/orgs","repos_url":"https://api.github.com/users/jcheng5/repos","events_url":"https://api.github.com/users/jcheng5/events{/privacy}","received_events_url":"https://api.github.com/users/jcheng5/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":7,"created_at":"2015-04-24T18:11:18Z","updated_at":"2015-04-28T17:04:47Z","closed_at":null,"pull_request":{"url":"https://api.github.com/repos/rstudio/leaflet/pulls/60","html_url":"https://github.com/rstudio/leaflet/pull/60","diff_url":"https://github.com/rstudio/leaflet/pull/60.diff","patch_url":"https://github.com/rstudio/leaflet/pull/60.patch"},"body":"The long-awaited resurrection of Shiny interactivity for the new bindings.\r\n\r\n(@wch @yihui No need to review this during your conferences, we can go over it together in person on Monday.)\r\n\r\n## Background\r\n\r\nThe new Leaflet bindings make it quite convenient to create and customize Leaflet map definitions and then render them. Some limited interactivity is available via 1) the `popup` argument, 2) you can use map click events in Shiny to drive other (non-map) behavior, and 3) using `renderLeaflet` you can reactively replace an entire map with an entire new map.\r\n\r\nWhat has been missing is the ability to add/remove features from, and customize bounds on, a running map. (This was possible in the previous-generation binding, using `map$addPolygons` or whatever; in fact, the only way that binding worked was using those kinds of imperative runtime commands.) For example, adding new polygons as new data comes in; removing outliers based on user clicks; driving the map view from one location to another.\r\n\r\n## API Design\r\n\r\nThe new interactivity style aims to restore the power of the old binding, while preserving the ease-of-use that we have aimed for with the new bindings.\r\n\r\nFor the purposes of this explanation, we'll refer to the usual style of Leaflet map creation to be **local**, as in, we're building up a local representation of a map, to be rendered to HTML or sent to Shiny when we're done; and the new capability introduced by this PR as being **remote**, that is, we're issuing commands from R to a map instance in the browser that already exists.\r\n\r\nThis PR introduces the ability for all of the Leaflet layer (e.g. `addPolygons`) and view (e.g. 'fitBounds') functions to operate in both local and remote modes. The local mode has not changed at all. The remote mode we're introducing is invoked when you construct a map not with `leaflet()` but with the new `getMapProxy()` function.\r\n\r\nA small example (which you can run with `example(getMapProxy)`):\r\n\r\n```r\r\nlibrary(shiny)\r\n\r\nui <- fluidPage(\r\n  leafletOutput(\"map1\")\r\n)\r\n\r\nserver <- function(input, output, session) {\r\n  output$map1 <- renderLeaflet({\r\n    # local\r\n    leaflet() %>% addCircleMarkers(\r\n      lng = runif(10),\r\n      lat = runif(10),\r\n      layerId = paste0(\"marker\", 1:10))\r\n  })\r\n  observeEvent(input$map1_marker_click, {\r\n    # remote\r\n    getMapProxy(\"map1\", session) %>%\r\n      removeMarker(input$map1_marker_click$id)\r\n  })\r\n}\r\n\r\nshinyApp(ui, server)\r\n```\r\n\r\nIn this example we're calling `removeMarker` but we could have called any mapping function there. Currently, anything that's valid for local maps is also valid for remote maps (though the `removeXXX`/`clearXXX` functions are pretty sketchy when used for local, see the Note under ?removeTiles).\r\n\r\nI find this symmetry extremely elegant. This approach had occurred to me in the past but I was worried that the implementation would be unwieldy (e.g. two separate code paths for every map function) but the implementation ended up being quite clean and easy as well.\r\n\r\nMy only design-related question is whether the local/remote distinction can be easily grokked by users--feedback welcome!\r\n\r\n## Implementation\r\n\r\n`getMapProxy(mapId, session, data)`: Creates a proxy object for a remote map, the one that has the given mapId in the given session. `data` plays the same role it does in `leaflet()`.\r\n\r\n`appendMapData(map, data, component, ...)`: This non-exported function previously existed, and was the common code path through which all of the layer-adding code passed through; it basically took a method name and arguments, and added them to a map object. (Really, it should be renamed `invokeMapMethod` or something.) This function now works for both local and remote maps; local objects continue to append the method/args to the map, while proxy objects send them directly to the remote map for immediate invocation, using `invokeRemote` (see next).\r\n\r\n`invokeRemote(map, method, args = list())`: Non-exported. Sends a method and arg list to the remote map to be invoked. These are fire-and-forget calls so don't expect a return value from your JS method, you won't get any.\r\n\r\n`dispatch(map, local, remote)`: Non-exported. This is basically a convenient way to say:\r\n```r\r\nif (map.remote)\r\n  local\r\nelse\r\n  remote\r\n```\r\n\r\nMost high-level R map functions should just call `appendMapData` and can be ignorant of the difference between local and remote maps. Those functions that want to customize behavior based on one or the other should call `dispatch` instead; the body of the remote branch will probably involve `invokeRemote`.\r\n\r\n## Open issues\r\n\r\n* I anticipate some discussion about the `getMapProxy` function name. I'm happy to entertain suggestions but let's not let this PR drag on solely because of the function name, as we are sometimes prone to do. :)\r\n\r\n* [x] Automated tests\r\n* [x] Check that no errors occur if getMapProxy is called for a map that doesn't exist\r\n* [x] Allow getMapProxy calls to be deferred until after session flushes outputs\r\n* [x] Better error message when dispatch isn't provided with the appropriate action type\r\n* [x] Change dispatch branches from local/remote to class names"},"comment":{"url":"https://api.github.com/repos/rstudio/leaflet/issues/comments/97138571","html_url":"https://github.com/rstudio/leaflet/pull/60#issuecomment-97138571","issue_url":"https://api.github.com/repos/rstudio/leaflet/issues/60","id":97138571,"user":{"login":"jcheng5","id":129551,"avatar_url":"https://avatars.githubusercontent.com/u/129551?v=3","gravatar_id":"","url":"https://api.github.com/users/jcheng5","html_url":"https://github.com/jcheng5","followers_url":"https://api.github.com/users/jcheng5/followers","following_url":"https://api.github.com/users/jcheng5/following{/other_user}","gists_url":"https://api.github.com/users/jcheng5/gists{/gist_id}","starred_url":"https://api.github.com/users/jcheng5/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jcheng5/subscriptions","organizations_url":"https://api.github.com/users/jcheng5/orgs","repos_url":"https://api.github.com/users/jcheng5/repos","events_url":"https://api.github.com/users/jcheng5/events{/privacy}","received_events_url":"https://api.github.com/users/jcheng5/received_events","type":"User","site_admin":false},"created_at":"2015-04-28T17:04:47Z","updated_at":"2015-04-28T17:04:47Z","body":"All that's left now is finalizing the `getMapProxy` function name. How about `leafletRemote` or `leafletProxy`, for consistency with `leaflet()`?"}}