Goal: To make JS.patch
and JS.navigate
more interoperable with JS.push
.
Scenario: Imagine making a reusable Phoenix component and you want to allow the user of your component to customize its interactive behavior. The JS
module already gives us good tools for this, but if you want to allow for customized navigation events you have to use a callback function to generate the JS command (as demonstrated by the row_click
attr in the default CoreComponents table.
This is different from push
, which does support values, while patch
and navigate
don’t receive the values from phx-value
or support setting their own values. This makes sense contextually as an event push is different from navigation.
With a slight modification we could allow users of .patch
and .navigate
to opt in to receiving the same phx-value-*
as JS.push
. Then in our component example above, the user of the component would have full control over the behavior without introducing additional complexity via callback functions.
Suggestion:
- Add
:params
as a way to pass query params toJS.patch
andJS.navigate
- Add
:values_as_params
to control whichphx-value-*
should be included as query params. Defaults to false (or empty list?).
Examples:
cmd_1 = JS.patch("/things", values_as_params: true)
~H"""
<!--- a bunch of posts -->
<button phx-click={cmd_1} phx-value-page="2" phx-value-size="10">Next Page</button>
"""
Clicking the button would do a patch to /things?page=2&size=10
.
cmd_2 = JS.patch("/things", values_as_params: [:page])
~H"""
<!--- a bunch of posts -->
<button phx-click={cmd_1} phx-value-page="2" phx-value-size="10">Next Page</button>
"""
Clicking the button would do a patch to /things?page=2
.
cmd_3 = JS.patch("/things", params: %{size: 100}, values_as_params: [:page]
~H"""
<!--- a bunch of posts -->
<button phx-click={cmd_1} phx-value-page="2" phx-value-size="10">Next Page</button>
"""
Goes to /things?page=2&size=100
Using :params
the user of the component can override or introduce their own params.
This would significantly simplify designing data driven components like tables. Users could easily reuse the same components for purely ephemeral uses using JS.push
and more durable ones using JS.patch
without having to change the component code.
I’d be happy to help work on an implementation if anyone else sees value in this.
Alternative approaches:
- Use callbacks functions to generate the JS commands, passing in whatever data is relevant (as mentioned above)
- Use
JS.dispatch
along with a custom event listener inapp.js
to implement the suggested functionality. - We could choose to support custom JS commands instead, something like
JS.custom
where the user would register custom commands similar to how hooks are defined in the socket. (Basically a more streamlined version of alternative 2 above). This approach could also allow further customizations that are out of scope for the core LiveView library like supporting path params in the Javascript commands like/things/:id
Sketch of alternative 3 above:
const commands = {
ParamPatch(view, el, href, opts) {
const finalHref = determineHref(href, opts)
# or a path param version might do
# const finalHref = subsititutePathParams("/things/:id", {id: 5}
view.liveSocket.pushHistoryPatch(finalHref, ... etc)
}
}
let liveSocket = new LiveSocket("/live", Socket, { commands, ...}
JS.custom_command("ParamPatch", opts?)