-
Notifications
You must be signed in to change notification settings - Fork 423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pass data attributes as action method argument #249
Conversation
@jaan @sstephenson Whats your opinion on the pull request? It's small but a very usable improvement |
I'd like to see a complete params concept encapsulate this. So: <div data-controller="items" data-items-base-url="https://api.stimulus.org/upvote/">
<button data-action="items#upvote" data-items-upvote-id-param='1'></button>
<button data-action="items#upvote" data-items-upvote-id-param='2'></button>
<button data-action="items#upvote" data-items-upvote-id-param='3'></button>
</div> // Call signature is event, params
upvote(e, { id }) {
fetch(this.baseUrl + id, { method: "POST" })
} This way params are separated from any other type of data-* on the action element, they're namespaced so different actions can have different params, and we have a concept that's encapsulated fully. |
Actually, I'd rather still even just make the event part of the params, so you can chose whether or not to destruct it: // Doesn't need the event
upvote({ id }) {
fetch(this.baseUrl + id, { method: "POST" })
}
// Does need it event
upvote({ id, event }) {
fetch(this.baseUrl + id, { method: "POST" })
event.preventDefault()
} |
This would break backwards compatibility |
I started implementing and testing with this namespacing approach. I agree that it does fully encapsulate the params but it could become very verbose to write if we have several controllers needing the same parameter This same example with the <div data-controller="controller1 controller2" data-items-base-url="https://api.stimulus.org/upvote/">
<button data-action="controller1#upvote controller2#doSomething"
data-controller1-upvote-id-param='1'
data-controller2-upvote-id-param='1'></button>
<button data-action="controller1#upvote controller2#doSomething"
data-controller1-upvote-id-param='2'
data-controller2-upvote-id-param='2'></button>
</div> The I think that the type of parameters that we need to pass to the functions are unique as they are linked to the caller I like the suggestion of adding the export default class extends Controller {
static params = {
id: Number,
active: Boolean
}
...
upvote(e, { id, active }) {
//active is a Boolean
if (active) {
fetch(this.baseUrl + id, { method: "POST" })
}
}
} |
Don't have a problem bumping a major version for this, and we can have a feature toggle that still supports the old signature. |
@javan Has an idea for this that doesn't involve breaking backwards compatibility. I'll let him present that. |
I'm not a fan of moving Another option, which wouldn't be a breaking a change, is to hang <button data-action="items#upvote" data-items-upvote-url-param="/items/1/votes">…</button> upvote(event) {
event.preventDefault()
console.log(event.params) // { url: "/items/1/votes" }
} Or, if you're only interested in the params: upvote({ params }) {
console.log(params) // { url: "/items/1/votes" }
} Or even: upvote({ params: { url } }) {
console.log(url) // "/items/1/votes"
} |
I also do like the idea of defining the params ala values, btw. Rather than having them be fully namespaced in the attributes. Would give us the power to typecast as well. The only issue is that it's a global params definition, even if the invocation is mostly per-action. But maybe that doesn't matter. A lot of the time there's bound to be overlap anyway (like with ids). |
Because params would be read-only (unlike values which are read/write), we could (attempt to) typecast them automatically without the need to declare types in the controller. Example: function getParams(element, descriptor) {
const params = {}
const [ identifier, action ] = descriptor.split("#")
const pattern = new RegExp(`^data-${identifier}-${action}-(.+)-param$`)
for (const { name, value } of element.attributes) {
const match = name.match(pattern)
const key = match && match[1]
if (key) params[key] = typecast(value)
}
return params
}
function typecast(value) {
try {
return JSON.parse(value)
} catch (o_O) {
return value
}
} <button data-action="item#upvote"
data-item-upvote-id-param="12345"
data-item-upvote-url-param="/votes"
data-item-upvote-active-param="true">…</button> ❭ getParams(button, "item#upvote")
{ id: 12345, url: "/votes", active: true } |
I like that. The primary appeal for declaring the params up front would be a sense that you'd say "this controller uses these params", especially since you'd feel more OK with having the params listed without controller/action namespaces. But starting to to think that it probably doesn't matter. We could even do both. So the default would be that params are simply available to all actions, but you could also namespace them, in case there was a conflict between two equally named params for the same element. So: <button data-action="item#upvote"
data-id-param="12345"
data-item-upvote-url-param="/votes"
data-item-upvote-active-param="true">…</button> In that case, params.id would be available to all controllers/actions invoked off that element. But params.url and params.active would only be there for the item#upvote. I think this works particularly well since params is now just a event.params call, that you can destruct as you please on the method signature. |
please merge this PR I think what @dhh suggested is very good |
@saiqulhaq what about: <button type="button" data-action="controller.doAction" data-id="1">Do Action</button> controller: doAction(event) {
console.log('data attached to button=', event.target.dataset);
console.log('id=', event.target.dataset.id);
} or: doAction({ target: { dataset: { id } } }) {
console.log('id=', id);
} |
@jmas so it could be
|
@saiqulhaq I haven't had time yet to update this PR as per our talk above. I plan to do so by the end of the month. |
I love the idea of global and controller/action specific params. This would allow me to open the scope of a lot of my stimulus controllers. |
How can I access data params in controller action? |
@rubab you can use $(event.target).data(' data attribute here ') |
This looks great @adrienpoly will it be merged? |
Is this PR still in the works? |
@jturmel yeah still need to work on it. Last time I hit a few limits of my TS knowledge but I have practiced a bit more TS since then. I ll try to get back to it in August but if you feel like taking over feel free to give it a try |
I see a possible issue with having shared params. What constitutes a shared param vs. a namespaced param? Would it be a case of looking through each of the actions, finding their namespaced params and the remainder are shared? In this example: <button data-action="item#upvote remote#load"
data-id-param="12345"
data-remote-load-url-param="/votes/summary"
data-item-upvote-url-param="/votes">…</button> Would Maybe we could get around the ambiguity by using a JSON syntax? Seems a little messier though… <button data-action="remote#load item#upvote"
data-params="{ id: 12345 }"
data-remote-load-params="{ url: '/votes/summary' }"
data-item-upvote-params="{ url: '/votes', active: true }">…</button> Another option would be to have a reserved global param namespace. Something like <button data-action="item#upvote remote#load"
data-shared-id-param="12345"
data-remote-load-url-param="/votes/summary"
data-item-upvote-url-param="/votes">…</button> |
ff86e24
to
cbe6b48
Compare
0b580d5
to
61f804e
Compare
I have noticed a few things that needs to be corrected after the merge with 2.0 will try to address them |
I have corrected a few merge issues with 2.0 and updated a bit the code to match as close as possible to the latest additions. The features remain the same as described in my previous post As always let me know if you have questions / suggestions / comments |
Merge this bro, I need it. 😄 |
@adrienpoly thank you for your hard work! I’ve been thinking about the API and documentation for this PR all day. The targets, values, and css classes APIs all have clear, explicit declarations at the top of the controller. Actions methods currently
The params API should similarly have clear declarations in the action method arguments. I think the rest of the event object can still be accessed with this destructuring syntax: upvote({ params: { id, url = “/test/me” }, ...event }) but this buries the event at the end of the arguments. I’d like to suggest adding an get event() {
return this;
} which allows us the write the event at the front of the destructured list: upvote({ event, params: {
id = undefined,
url = undefined,
active = true } }) {
...
} allowing the params to feel like an appended optional declaration:
As a side note, inserting params into the event object still feels very awkward to me. Does this not feel cleaner? upvote(event, params) {
const {
id = undefined,
url = undefined,
active = true
} = params
...
} |
I feel like destructuring too much in the function declaration reduces readability (agreeing with your side note). Why not something like this? upvote(event) {
const {
id = undefined,
url = undefined,
active = true
} = event.params
// ...
} |
@dancallaghan Indeed, this is much clearer, but it still irks me that the params are riding in the event object and we can’t require developers to declare params like we do for targets, values and css classes. Then again, technically the developers could just access the data bypassing those other APIs, so the final solution needs to offer enough benefit that everyone wants to use it. Are we naming this feature correctly if they don’t appear in the method parameters? Are we not really just referring to a subset of data values declared on the element with the action descriptor? |
Then again, we can simply add a section to the end of Event Objects in the Actions reference entitled “Event Params”, where we can show the best practice example of declaring the event params. so, this covers getters and @javan said here that these params are read-only so no need for setters. I don’t remember seeing discussion above about adding existential properties, which could be a useful check that lets the developer know, for example, that the attribute wasn’t in the data (or is undefined?) so the restructured value is the provided default value. If we go with the “Event Params” branding then what about While we may not want to overly pollute the event object, Also |
b276290
to
310927c
Compare
310927c
to
6b40e2b
Compare
Hi, upVote({params}) {
console.log(params)
} |
@seb-jean are you running the hotwired/stimulus 3 beta 2 version ? |
Same issue as @seb-jean in v3.0.1 using the Looking at the source for the actions logic, the re-assignment of the event target is a bit of code smell imo and just to pass the |
Very often the question comes up on how to pass arguments to Stimulus action methods.
Global parameters can be passed to the action using the data API but when a controller has multiple elements and individual parameters (an
id
by example) for each of those elements, it is a common practice to pass the value using the standard dataset API:event.currentTarget.dataset.id
One typical example would be for a set of buttons with
ids
and associated fetch actionsProposal
This PR is a draft proposal for a more concise API by passing all data attributes value of the
currentTarget
as a parameter of the action.Using basic destructuring the above example can be simplified like this:
I think this could make it simpler to understand and avoids confusions around
target
andcurrentTarget
.If the element is of a type
HTMLFormElement
we could also pass thevalue
at the same time.Just wanted to propose this for now if you feel this could make sense, I ll add some associated test