-
-
Notifications
You must be signed in to change notification settings - Fork 923
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
Make latest attrs available to event callbacks #1744
Comments
A solution to this would be to pass the |
This should be doable with the next release which includes factory components: const component = function() {
let count = 0
function increment() {
count++
}
function view() {
return [
m('p', 'count: ' + count),
m('button', {onclick: increment}, 'Increment')
]
}
return {view}
}
m.mount(document.body, component) |
you guys might end up with domvm's API [1], including ES6 classes ;) |
@spacejack shouldn't Mithril perform state comparison between |
@chebum Mithril diffs the virtual DOM trees (a.k.a. trees of Edit: Specifically, the engine diffs the |
@spacejack the closure component solves the issue of simple references to state; the I can only assume the performance benefit that differentiates this from simple partial application or any other kind of function declaration is special diff logic which allows the render loop to gloss over differences in @bdpartridge reading between the lines I get the impression your main draw from As regards implementing I think it'd be good to hash out a (tediously long) document replete with code examples to cover all the aesthetic preferences in combination with all the practical motivations for function bindings in Mithril components. This would at least help us avoid going round the houses trying to remember preferred patterns every time this kinda thing comes up. 🤔 @leeoniya slowly slowly… ;) I was surprised you didn't mention the DOMVM architecture pattern in #1716. |
Since Mithril's 1.x API is now baked, I think a philosophical discussion is rather moot, plus adding extra signatures and nuances to what exists leads to jquery-level of docs, user confusion and a lot of internal complexity with likely negative perf implications. I try to keep my trolling lighthearted and down to a minimum here; feel free to incorporate any of domvm's ideas. If anyone still remembers 2015 (600 BC in javascript years), it was Mithril 0.2x's strange "controller", "this" and MVC concepts [1] that led me down my own path. Thankfully that's all behind us 1245 issues later :) It's good to see the Controller finally dead and the speed vastly improved. However even with 1.x, I still notice occasional mithril-isms carried over from 0.2, which is understandable but still not my cup of tea, especially given that I ended up writing exactly the lib i wanted to use. [1] #499 |
@chebum to answer your question about redraws, changes in state (vnode.state or otherwise) will not trigger redraws. Redraws are only triggered by events that are added to elements via |
Thanks for the comments. @pygy I like the idea of passing @spacejack with factory components, how would I access @barneycarroll yes, I'm specifically interested in getting access to |
I like best @pygy's proposal of just passing |
Giving credit where it's due, the idea comes from @chebum! |
nice. that's a much simpler approach to an issue for consideration a while back -- #1484 (comment). |
@cavemansspa The nicer thing about it is that we wouldn't have to add anything extra. You could just do it and access the |
I am not sure if this is a bad practice, but what do you think about this: // Main class to set vnode property
class MainClass<A> {
private _vnode: m.CVnodeDOM<A>
get vnode() {
return this._vnode
}
render = (vnode: m.CVnodeDOM<A>) => { return m('span') }
view(vnode: m.CVnodeDOM<A>) {
this._vnode = vnode
return this.render(vnode)
}
}
// And after that - just beautiful syntax with vnode accessible everywhere
class TestButton extends MainClass<{ value: number, onclick: Function }> {
onclick = () => {
// Extend behaviour, or override
if (this.vnode.attrs.onclick) this.vnode.attrs.onclick()
console.log(this.vnode.attrs.value)
}
render = () => {
return m('button', { ...this.vnode.attrs, onclick: this.onclick }, this.vnode.attrs.value)
}
} |
@vladpazych It's an equivalent workaround, but it'd be better to just pass a second argument. |
Welcome! |
A little late here (sorry), but passing the vnode as a second arg seems like a bad idea to me. We're making mithril event handlers proprietary and I disagree that adding an additional argument isn't a major semver release, I have code that will throw with this change. This reminds me of passing the index in To illustrate my point, here's an example of passing additional args radically changing behaviour.
R.range(0,3).map( R.slice ) //=> [[],[],[]]
R.map( R.slice, R.range(0, 3)) //=> [R.slice(0), R.slice(1), R.slice(2)] I also don't see what problem this solves, vnode is always going to be in scope, so we're diverging from mithril's thin wrapper over the DOM API for little benefit. This notion of avoiding I can mitigate this change with a helper that forces the function to be unary, but its awkward, and I wanted to voice my objection. I expect this will be a change we end up regretting later, like prop's having a If this improves performance, we should have benchmarks before making decisions about the value it adds. It may be in the noise, and unless its a significant boost I don't agree its a valid trade off. Ironically, if one wants to avoid that second argument we're forced to create an intermediate function, which is the exact problem this feature is meant to solve. |
i'm no mithril expert, but it seems that only the component's root vnode would be in scope, but not the vnode of a handler defined deeper in the returned vtree. right? or do you guys hang the current vnode off |
At the risk of offending my coworker, @bdpartridge, who filed this issue, I agree with @JAForbes. Encroaching on a DOM API has a bit of a code smell. Factory components seem to solve the problem, at least for the common case.
@barneycarroll, accessing the latest state or attrs is easily solved by stashing a reference in a component-scoped var each time
@bdpartridge, I’d love to see a code sample illustrating the nested component situation you’re describing. Is it common? |
Could someone clarify what they mean by "stale attrs". I'm still on 0.2x day to day so apologies if I misunderstand something, but aren't attrs bound at component creation time, and aren't updated every redraw? var root = document.body;
function Parent(){
return {
view(){
return m('div'
,m(Child, { value: Math.random() })
,m('button', { onclick: i => i }, 'Redraw')
)
}
}
}
function Child(){
return {
view(vnode){
return m('p', JSON.stringify(vnode.attrs) )
}
}
}
m.render(root, m(Parent)); In the above example, my understanding is, we'll always render the same random number, no matter how many times we redraw, even though the That's my understanding. That said, receiving So is my understanding wrong? I just tried the above code in a bin and it seems to follow my expectations. Could someone elaborate what "stale attrs" means in their context? |
Nope, you get updated attrs every redraw: http://jsbin.com/caxosogeqe/edit?html,js,output |
@spacejack oh wow! 🎉 |
Just found out why my example code failed, the template was using |
But yeah as @spacejack's bin shows, |
React's solution to this problem is to have |
A summary of the thread @cavemansspa referenced for consideration.
|
I don't have enough experience using 1.0 to feel comfortable challenging any of the above arguments from the other thread. I still oppose an extra argument for event handlers and it does seem like a smell to me to have event handlers accessing mithril specific properties on an event. I personally think that currying the handler is a great solution, and worrying about performance for creation of functions is untested and probably misguided - but I can understand the frustration of having to do that every time, it feels like something that should be supported somehow in the default API without any trickery. If anything closure components are more susceptible to this particular annoyance. I think when I eventually migrate to 1.0 I'd decorate the component interface so I get a stream of "fresh" attrs that I can subscribe to and merge with other event streams in the closure. But streams aren't part of the core library so that isn't a solution everyone else is likely to want to adopt. Of the three proposed solutions, I don't think there is a satisfying addition, but adding a property to the event seems like the path of least resistance, so if we have no other options and we want to make some kind of change then I'd reluctantly support that. As @spacejack has stated We're already modifying the event object to have a But if someone has a different solution I'd welcome that 😄 |
To be clear, what @dwaltrip suggested is different from what was proposed in that other thread. @dwaltrip (this thread):
@barneycarroll (other thread):
In addition, that other thread predates closure components. @lhorie’s harsh criticism, in particular, was about the idea of mutating the vnode passed to The closure component API might have benefitted from a little more thought about how closures work. That new API would have been a natural place to introduce a new object type—one whose properties are always up-to-date: const ChildComponent = function (o) {
// Use o.vnode, o.attrs, o.state (each always up-to-date).
} I’ve shown it as Or could’ve passed a latest-vnode getter instead of just the initial vnode: const ChildComponent = function (getVnode) {
// Use getVnode().attrs for up-to-date attrs.
} |
While I like @dwaltrip’s suggestion and the const ChildComponent = function (vnode, attrs) {
...
function onclick() {
showSuccess = true;
setTimeout(() => {
showSuccess = false;
m.redraw();
}, attrs().showSuccessTime);
}
...
} |
@JAForbes As someone who has done significant hacking on the renderer itself, the difficulties of making the vnode persistent is generally overstated. The main requirement is that you understand the renderer algorithm (which only a few of us do), but beyond that, it's mostly name changes, and it's something you can generally do for components and DOM vnodes separately. It's more tedious than it is hard. |
One more point for having some way to access to the current vnode in an event handler is getting the current dom (which isn't available in the vnode provided to |
@spacejack It's inadvisable to access |
I mean in an onclick etc. handler. |
@spacejack actually, the |
I suppose you can get the clicked element via const c = {
view() {
m(comp, {
onFoo: () => { /* want to use dom here */ }
})
}
} EDIT: Sorry my brain isn't working today... that's not a mithril event handler duh. I suppose the dom access is mostly a non-issue. |
@spacejack This is what you meant? Looks like it could cause issues as you were suggesting. const FooComponent = {
view(fooVnode) {
return m('.foo-div', m(BazComponent, {
handleSomething: () => { /* want to use FooComponent dom. using fooVnode won't work */ }
}));
}
} |
@pygy I think you missed this part 😉
|
@isiahmeadows It's not there in |
Oh okay. |
I agree that always adding the vnode to the event handler call feels problematical. The point here is to find a way that event handler functions can get extra information they need, right? And without breaking existing code? The linkEvent idea seems good to me to accomplish that and the code does not look that complex: Another variation along the lines of linkEvent is to optionally pass extra parameters to the called event handler function based on defining some new property of attrs like "bind" or "extraArgs" or whatever. Such an approach might also be useful to selectively bind "this" as well. Thinking about it, it does not seem as general as linkEvent though. The linkEvent approach could even perhaps be expanded to return a boolean that controls whether to do a redraw? Adding a field to the event seems reasonable to me as an alternative too. With either optional approach, if users want the extra parameters, they get them if they set an extra field in attrs. If they don't want them, things work as-is so existing code will not break. Maybe there could even be a flag to pass the vnode in to the event handler too if that was really desirable? Either idea avoids the need to to create a new closure with bind or an anonymous or curried function -- which can have performance and garbage collection implications depending on whether that is done in the view function or now. I proposed a variant of a similar approach for Maquette where, ultimately, you could set a "bind" property for "this" to make it easier to call instance methods for TypeScript classes without extra closures. Here is the issue which was resolved with such a change: AFASSoftware/maquette#35 Of course, one can question if the extra complexity is worth it given there are these possible workarounds for where performance matters. There would also be a small overhead all the time for extra checks if such fields were defined. But there is something to say for supporting a more idiomatic object-oriented use even if Mithril encourages a more functional coding style -- and for that, being able to easily bind "this" is important (which modifying the event does not address). @spacejack pointed me at this conversation from Gitter discussion about possibly changing Mithril to use a constant stub function which calls user functions rather than create a new wrapper function and touch the DOM ever time the user event handler changes (like if a bind call or anonymous new function is used in defining the attrs). Supporting optional parameters or binding this as desired might reduce the likelihood people would write such code and tickle this possibly computationally inefficient aspect of Mithril. Again though, the alternative is restructuring user code when computational efficiency is needed -- and also perhaps updating how Mithril handles event handler changes. |
@pdfernhout That stub would reduce the memory load, too. Oh, and event listeners don't have to be functions: they can be objects, too, provided they have a |
That could allow us to just add var handleEvent = typeof onevent === "function"
? function (e) {
var type = "on" + e.type
if (this[type] != null) this[type].call(this.vnode, e)
onevent.call(this.vnode.dom, e)
}
: function (e) {
var type = "on" + e.type
if (this[type] != null) this[type].call(this.vnode, e)
}
function addEvent(vnode, key, value) {
if (vnode.events == null) vnode.events = {handleEvent: handleEvent, vnode: vnode}
if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false)
vnode.events[key] = value
}
function removeEvent(vnode, key) {
vnode.events[key] = undefined
vnode.dom.removeEventListener(key.slice(2), vnode.events, false)
} And in practice, |
I just ran into this problem yesterday on 1.x. I remember running into this problem in 0.x as well. I've never had to think about this problem when working with React, so this is one aspect it has over Mithril. For the record, my use case is integrating with a 3rd party lib: oncreate(vnode) {
var editor = CodeMirror.fromTextArea(dom.children[1], ...)
editor.on('mousedown',function (editor, e) {
e.preventDefault()
if ( vnode.attrs.mode === 'pending' ) {
editor.focus()
editor.setCursor(0,0)
}
})
} |
@gilbert I could've used this feature, too, in multiple cases. Now that I have taken a bit off, I have an idea that might work a little better:
It would both simplify internals some and resolve this bug. Also, it wouldn't require us breaking the world to do so (few components in practice depend on the current behavior apart from the |
Sounds wonderful! Would this mean |
@gilbert Eventually, it could, but that's not an absolute requirement for my proposal. |
BTW, here's how you can work around it: set function Component() {
var count = 0
var attrs
function increment(ev) {
count += attrs.count || 1
}
return {view: function (vnode) {
attrs = vnode.attrs
return [
m('p', 'Count: ', count),
m('button', {onclick: increment}, 'Increment by ', attrs.count),
]
}}
} You can do similar with classes and object components if you leverage the IMHO underused class Component {
constructor() {
this.count = 0
this.attrs = undefined
}
handleEvent(ev) {
this.count += this.attrs.count || 1
}
view(vnode) {
this.attrs = vnode.attrs
return [
m('p', 'Count: ', this.count),
m('button', {onclick: this}, 'Increment by ', this.attrs.count),
]
}
}
var Component = {
oninit: function () {
this.count = 0
this.attrs = undefined
},
handleEvent: function (ev) {
this.count += this.attrs.count || 1
},
view(vnode) {
return [
m('p', 'Count: ', this.count),
m('button', {onclick: this}, 'Increment by ', this.attrs.count),
]
},
} And of course, this carries for literally any vnode property or even the vnode itself - it works the same way. So since you can basically just design your way out of the (almost never significant) performance issue by just using native stuff and existing functionality, I'm closing this. |
I really like Inferno's
linkEvent(...)
helper function (see doc), and I'm hoping that others also have interest in seeing a similar function get added to mithril.It just feels bad/strange to have to recreate anonymous/arrow functions (or use function binding) to enable passing data from a component to event callbacks. Adding first-class support for a utility function like this provides an alternative that some may find cleaner. As a bonus, the Inferno doc claims a performance boost from this approach. I haven't verified the performance difference, but given the nature of closures and function binding, it makes sense.
Given the mithril doc's recommendation to "avoid fat components", a utility function like this seems like a natural fit since it helps to enable keeping state manipulation code separate from components. How about adding a similar helper function to mithril? Is there any interest? If so, I'd be happy to start working on it.
The text was updated successfully, but these errors were encountered: