MithrilJS / mithril.js

A JavaScript Framework for Building Brilliant Applications
https://mithril.js.org
MIT License
14.02k stars 926 forks source link

Make latest attrs available to event callbacks #1744

Closed bdpartridge closed 5 years ago

bdpartridge commented 7 years ago

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.

pygy commented 7 years ago

A solution to this would be to pass the vnode as a second argument to event handlers, as suggested by @chebum (here: https://github.com/lhorie/mithril.js/issues/1739#issuecomment-289090634 it could be done easily)... Too tired for thinking of drawbacks right now...

spacejack commented 7 years ago

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)

demo

leeoniya commented 7 years ago

you guys might end up with domvm's API [1], including ES6 classes ;)

[1] https://github.com/leeoniya/domvm#views

chebum commented 7 years ago

@spacejack shouldn't Mithril perform state comparison between view method calls? Reading docs I thought that it checks if the state changed and then redraws the DOM. How does it work in reality?

pygy commented 7 years ago

@chebum Mithril diffs the virtual DOM trees (a.k.a. trees of vnode objects). The vnode.state field is persisted in the tree from one redraw to the next (it is transferred, for each node, from the old vnode to the new one), but isn't really needed with closure components (previously referred to as "factory components" but the term was deemed confusing).

Edit: Specifically, the engine diffs the tag, attrs and children fields of vnode objects. The other vnode fields are there for inner bookkeeping.

barneycarroll commented 7 years ago

@spacejack the closure component solves the issue of simple references to state; the linkEvent pattern allows you to reference anything available to the view (eg latest attributes).

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 linkEvent handlers and pick up the task of applying the function as part of Inferno's event bubbling system.

@bdpartridge reading between the lines I get the impression your main draw from linkEvent is a formal contract for binding events in views (as opposed to freeform functions). Mithril kinda offers this, albeit in a much more focused way: m.withAttr is a function for DOM event bindings that ensures a specified property of the DOM element will be fed to a specified function when the event triggers. It's especially useful in combination with stream. Ironically this was the prescribed method for handling events in Mithril v0.X, but Mithril v1 decided the scope of any given event handler was too large and the formality of invoking extra APIs to achieve simple assignment wasn't worth it. In v0.X, streams were simple getter / setters called m.propthe old documentation for m.withAttr might prove insightful if you're looking for ways to standardise your event handling code.

As regards implementing linkEvent in Mithril… I think you could only really justify it in terms of performance gains. There are problems in Mithril components WRT expectations of what input is available to any given function, and you're not the only person to prefer documentedLibraryAPI(prescribedSignature) to myFunction(theArgumentsIWantToGiveIt).

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.

leeoniya commented 7 years ago

@barneycarroll

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] https://github.com/lhorie/mithril.js/issues/499

spacejack commented 7 years ago

@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 attrs and when m.requests complete. Surprisingly (at least to me when I started using mithril) this covers most cases when you want redraws to happen. For cases that mithril doesn't detect, there is m.redraw().

bdpartridge commented 7 years ago

Thanks for the comments.

@pygy I like the idea of passing vnode as the second argument to event callbacks. It's simple and easy to document. Since I really just need access to vnode.state and vnode.attrs, this solution could work.

@spacejack with factory components, how would I access vnode.attrs in a nested component without creating a new function in view(...)?

@barneycarroll yes, I'm specifically interested in getting access to vnode.attrs (and vnode.state). But, I should've been clearer about the main thing that appeals to me about the linkEvent(...) approach. While I think it looks cleaner to not use .bind(this) or arrow functions, the main appeal for me is the potential for performance improvement (arguably unproven). We're using mithril v0.2.x in a fairly large production app at my current employer, and we've avoided recreating callback functions on every render because it seems needlessly wasteful. Maybe it is a micro-optimization, but I would still argue for the ergonomics of providing a utility function or just passing in vnode as a second argument to the callback, as @pygy suggested.

dead-claudia commented 7 years ago

I like best @pygy's proposal of just passing vnode as a second argument. That would fix this with no practical performance overhead, and it doesn't have the API boilerplate issues that Inferno's solution does (you don't have to explicitly call linkEvent to get the same benefits).

pygy commented 7 years ago

Giving credit where it's due, the idea comes from @chebum!

cavemansspa commented 7 years ago

nice. that's a much simpler approach to an issue for consideration a while back -- https://github.com/lhorie/mithril.js/issues/1484#issuecomment-267860717.

dead-claudia commented 7 years ago

@cavemansspa The nicer thing about it is that we wouldn't have to add anything extra. You could just do it and access the config via vnode.attrs.config 😄

vladpazych commented 7 years ago

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)
  }
}
dead-claudia commented 7 years ago

@vladpazych It's an equivalent workaround, but it'd be better to just pass a second argument.

dead-claudia commented 7 years ago

Implemented @chebum's idea of passing vnode as a second argument here.

dead-claudia commented 7 years ago

Welcome!

JAForbes commented 7 years ago

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 Array::map, it seems convenient, but it actually discourages composition and leads to sometimes surprising behaviour.

To illustrate my point, here's an example of passing additional args radically changing behaviour.

R.map( R.slice, range(0,10) ) has very different behaviour to R.range(0,10).map( R.slice ), the first creates a list of functions that slice from a particular index, the second fully evaluates slice because the index, and the list are provided to the Array::map visitor function.

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 oninit/controller by spreading coupled logic into various hooks and event handlers mystifies me. On the quest to avoid "fat controllers" we end up with spaghetti code, in denial of its truly coupled nature.

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 .then method. It seems convenient, but it is surprising and makes mithril more complex.

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.

leeoniya commented 7 years ago

I also don't see what problem this solves, vnode is always going to be in scope

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 e, e.target or this?

2is10 commented 7 years ago

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.

@spacejack the closure component solves the issue of simple references to state; the linkEvent pattern allows you to reference anything available to the view (eg latest attributes).

@barneycarroll, accessing the latest state or attrs is easily solved by stashing a reference in a component-scoped var each time view is called.

@spacejack with factory components, how would I access vnode.attrs in a nested component without creating a new function in view(...)?

@bdpartridge, I’d love to see a code sample illustrating the nested component situation you’re describing. Is it common?

JAForbes commented 7 years ago

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 Parent component appears to be passing a new random number on every render.

That's my understanding. That said, receiving vnode.attrs fresh every render would be a great thing in my opinion, it would solve a lot of problems I have with stale components that need to be instantiated to receive new data, I currently solve that by either using a stream that is passed down, or by using a plain old function (where possible).

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?

spacejack commented 7 years ago

Nope, you get updated attrs every redraw: http://jsbin.com/caxosogeqe/edit?html,js,output

JAForbes commented 7 years ago

@spacejack oh wow! 🎉

JAForbes commented 7 years ago

Just found out why my example code failed, the template was using m.render instead of m.mount 😮

JAForbes commented 7 years ago

But yeah as @spacejack's bin shows, vnode is in scope, we get the fresh value, and I still oppose this change.

spacejack commented 7 years ago

I think the use case is like this:

var Comp = function() {
  function move(e) {
    // I want fresh attrs here...
  }
  return {
    view(vnode) {
      return m('div', {
          onmousemove: move
        },
        "Test"
      )
    }
  }
}

What about tacking vnode on to the event? The event is already quasi-proprietary in that you set event.redraw=false to prevent redraws.

2is10 commented 7 years ago

I agree that tacking vnode onto the event would be less intrusive than passing it as an extra arg.

However, this specific case is easily handled. Here’s what I meant by “stashing a reference in a component-scoped var”:

var Comp = function() {
  var attrs;
  function move(e) {
    // Use attrs.
  }
  return {
    view(vnode) {
      attrs = vnode.attrs
      return m('div', {onmousemove: move}, "Test")
    }
  }
}

If others are asking for access to the event target’s vnode, well, what if what you actually need is the vnode of some element/component between the element with the handler and the event target?

The reason .target, .currentTarget, etc. work well in the DOM API is because the DOM tree is navigable (e.g. using .parentNode). I could be mistaken, but the virtual DOM tree doesn’t appear to be. I do see vnode.children, but no way to get a vnode’s siblings or parents.

One way to address this issue would be for mithril to expose a mapping from real DOM nodes to vnodes. Event handlers that rely on capturing or bubbling could use m.vnode(event.target) or m.vnode(event.target.parentNode) or m.vnode(event.relatedTarget), etc.

JAForbes commented 7 years ago

I think the ref trick is valid, but I personally think this would usually be premature optimization, instead of having a ref to attrs that we iteratively update, I'd use a curried function. I do this everywhere, and it hasn't made a dent on performance.

// in the closure / etc
const move = vnode => e => { ... }

// in the view
{ onmousemove: move(vnode) }

If someone's actually running into performance issues then I'll acquiesce, but the above is what I would do, and if I did have performance issues I'd do what @2is10 is doing (but only if that component is on a hot path, and making the change had a measurable difference)

I've just been having some interesting discussions with @spacejack in the gitter, and I've discovered 1.0 has behaviour I was completely surprised and unaware of.

I'm surprised the vnode reference changes from render to render, I expected a fully qualified vnode.attrs from the outer closure would grab the latest attrs, but apparently vnode itself is recreated each draw and state is attached to it. That's kind of fascinating, and shows I really need to get acquainted with 1.0. I imagine that behaviour is there to unify component vnodes and regular hyperscript vnodes.

I'm not sure why we'd want access to the vnode of an arbitrary element within the vtree of a component, as opposed to the vnode of the component itself. Could someone explain why that would be useful?

dead-claudia commented 7 years ago

@JAForbes Could you come up with a concrete proposal for how it could be better done? Considering the existing vnode-as-second-argument is already in next, it'd have to be detailed and implemented quickly so it can make the next release without making a net breaking change (because people will start using the solution as soon as it gets published).

JAForbes commented 7 years ago

@isiahmeadows Can't we just roll back the PR, it hasn't been released yet right?

JAForbes commented 7 years ago

Also I think this is adequate:

// in the closure / etc
const move = vnode => e => { ... }

// in the view
{ onmousemove: move(vnode) }
tivac commented 7 years ago

After thinking about this more I'm with @JAForbes in not loving this solution.

I prefer either recommending that people store a ref to vnode within their scope or making e.vnode a thing. I prefer the version that doesn't require mithril changes :smile:

IMO taking the PR out of next isn't a big deal, it's only a problem once it goes to master. We're still about a week out from discussing whether or not we want to cut 1.1.2 or 1.2.0 so there's still time.

bdpartridge commented 7 years ago

I'm not opposed to alternatives, and I appreciate the drawbacks of having a proprietary event handler callback. For the record, I originally suggested we add a helper utility like Inferno's linkEvent(...) to enable arbitrary data passing, but on further thought, I realize that this doesn't really give me want I want, which is simply a way to access the latest attrs without having to resort to higher order functions, or stashing vnode.attrs from view(...), or any other boilerplate.

Let me illustrate what I want to achieve with some examples:

Mithril 0.2.x

As @2is10 alluded to, this is an example of what we've been doing in mithril 0.2.x to provide event handlers with the latest attrs. We're essentially stashing them in ctrl:

const ParentComponent = {
  controller: function () {
    let ctrl = {
      showSuccessTime: 500,
      onchange: (event) => {
        ctrl.showSuccessTime = event.target.value;
      },
    };
    return ctrl;
  },

  view: function (ctrl) {
    let showSuccessTime = ctrl.showSuccessTime;
    return m("div",
             m("input", {value: showSuccessTime, onchange: ctrl.onchange}),
             m(ChildComponent, {showSuccessTime}));
  },
};

const ChildComponent = {
  controller: function (initialAttrs) {
    let ctrl = {
      attrs: initialAttrs,
      showSuccess: false,
      onclick: () => {
        ctrl.showSuccess = true;
        setTimeout(() => {
          ctrl.showSuccess = false;
          m.redraw();
        }, ctrl.attrs.showSuccessTime);
      },
    };
    return ctrl;
  },

  view: function (ctrl, attrs) {
    ctrl.attrs = attrs;
    return m("button[type='button']", {onclick: ctrl.onclick}, ctrl.showSuccess ? "Success!" : "Click me");
  },
};

m.mount(document.getElementById("app"), ParentComponent);

https://jsbin.com/vuhecas/edit?js,output

Mithril 1.x

Here's what I want to do in the latest version of mithril:

const ParentComponent = function () {
  let showSuccessTime = 500;

  function onchange (event) {
    showSuccessTime = event.target.value;
  }

  return {
    view: function () {
      return m("div",
              m("input", {value: showSuccessTime, onchange}),
              m(ChildComponent, {showSuccessTime}));
    },
  };
};

const ChildComponent = function (vnode) {
  let showSuccess = false;

  function onclick() {
    showSuccess = true;
    setTimeout(() => {
      showSuccess = false;
      m.redraw();
    }, vnode.attrs.showSuccessTime);
  }

  return {
    view: function () {
      return m("button[type='button']", {onclick}, showSuccess ? "Success!" : "Click me");
    },
  };
};

m.mount(document.getElementById("app"), ParentComponent);

https://jsbin.com/tozibew/edit?js,output

In the second example, updating showSuccessTime with the <input> doesn't work because the reference to vnode.attrs in the closure component is stale.

Yes, there are workarounds like stashing and currying, but these add little bits of what I consider to be boilerplate in every component where this is needed. In the codebase I work on currently, access to the latest attrs in event handlers is needed extensively.

dwaltrip commented 7 years ago

It seems to me the fundamental problem is that the object from which attrs is attached is a vnode. Vnodes do not have a strong conceptual mapping to a component instance (at least in a way that matters for this context).

Component instances can be thought of as persistent entities that are around as long as their parent continues to render them, while the vnode for a component is discarded and rebuilt every single render. What if instead of passing in vnode everywhere, we passed a persistent object representing the component instance, and every render loop this object is updated with fresh references to the vnode, attrs state before passed to any lifecycle methods?

This would give us the following semantics:

// The name `obj` is not a good name, this is just for demonstration
oninit: function(obj) {
  var func1 = ()=> console.log('obj.attrs could be kept fresh by mithril:', obj.attrs);
  var oldAttrs = obj.attrs;
  var func2 = ()=> console.log('This is a deliberately created old copy of attrs:', oldAttrs);
}

We almost always want fresh attrs, and it should be easy and natural to do so without any extra boilerplate. Getting old attrs from previous render loops should require extra work to stash them somewhere -- it shouldn't happen without deliberate effort. I think it may be a design smell that it is really easy to mistakenly create a closure over a stale vnode that has old attrs.

As @bdpartridge points out, this doesn't happen in 0.2.x, as vnode is not a concept. Everything lives on the controller (or is some other abstraction that the dev has full control over).

Disclaimer: I haven't written much code with mithril v1 yet, so there is a decent chance I don't have the full picture. But I've read many of the github issues and gitter discussions, so hopefully my comment makes sense.

Edit: Reword for clarity & a few semantic tweaks to better express what I mean. Added a code exmaple

dead-claudia commented 7 years ago

@tivac @JAForbes I reverted the change for now.

JAForbes commented 7 years ago

@dwaltrip

Component instances can be thought of as persistent entities that are around as long as their parent continues to render them, while the vnode for a component is discarded and rebuilt every single render. What if instead of passing in vnode everywhere, we passed a persistent object representing the component instance, and every render loop this object is updated with fresh references to the vnode, attrs state before passed to any lifecycle methods?

Until just recently I thought this was existing behaviour and I was quite surprised we get a new vnode every render. I'd really like to hear the justification for this design before critiquing it, but what you're suggesting seems an improvement to me, particularly now that closure components are a part of mithril 1.x. And this change would fix this particular problem, you could re-use existing handlers without losing a reference to fresh attrs.

Are there any issues where this decision was discussed that I could read up on? Or was this emergent behaviour that was simply never opted out of for components?

It seems potentially dangerous to have a persistent reference to vnode in oninit that has no relationship to the vnode in subsequent draws.

dead-claudia commented 7 years ago

@JAForbes

Until just recently I thought this was existing behaviour and I was quite surprised we get a new vnode every render.

I wrote up on this earlier from a performance viewpoint (see specifically suggestion 3 of this gist), but it really just comes down to mildly sloppy coding inherited from the beginning of the rewrite IMHO, and it is surprising behavior.

A patch for this could be non-trivial, but it'd be worth it, both for reduced object lifetimes and just general sanity.

cavemansspa commented 7 years ago

@JAForbes -- i recall a a related conversation on vnode here: #1521

dwaltrip commented 7 years ago

React's solution to this problem is to have this.props always point to the latest values that were passed into the component, where this is a persistent object representing the component instance.

JAForbes commented 7 years ago

A summary of the thread @cavemansspa referenced for consideration.

@lhorie:

Pros: it makes the actual behavior be in line w/ the developer's (flawed) expectations. Cons: this is voodoo magic

Link

@barneycarroll:

I'm withdrawing the proposal to make component vnodes persistent objects because that treads on the toes of the update methods, which rely on newVnode, oldVnode signatures.

Link

@brlewis:

Making changes to the mithril codebase in order to hide the fact that vnodes don't persist through the entire lifecycle won't work. People will still encounter confusion, only later and in more subtle ways.

Link

JAForbes commented 7 years ago

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 redraw=false property, so from that perspective its consistent. If we're modelling mithril event handlers in typescript or flow it's already going to be an intersection type.

But if someone has a different solution I'd welcome that 😄

2is10 commented 7 years ago

To be clear, what @dwaltrip suggested is different from what was proposed in that other thread.

@dwaltrip (this thread):

What if instead of passing in vnode everywhere, we passed a persistent object representing the component instance, and every render loop this object is updated with fresh references to the vnode, attrs, state before passed to any lifecycle methods?

@barneycarroll (other thread):

We can mitigate this by changing render logic such that the vnode received by components — which need not necessarily be the same construct used by the render internals — be stateful, this ensuring that the vnode received by any given component instance will be the same throughout that component's perpetuity.

In addition, that other thread predates closure components. @lhorie’s harsh criticism, in particular, was about the idea of mutating the vnode passed to oninit for a “developer who misundertands how closures work”.

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 o above as a pictogram of the One ring and as a symbol of renewal. Also has the nice effect of making each property access sound poetic. :)

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.
}
2is10 commented 7 years ago

While I like @dwaltrip’s suggestion and the event.vnode suggestion, here’s one more concrete, easy (?), small, backwards-compatible possibility to consider: Pass a latest-attrs getter as an additional arg to closure components. People could name it however they please (e.g. getLatestAttrs, getAttrs, attrs).

const ChildComponent = function (vnode, attrs) {
  ...  
  function onclick() {
    showSuccess = true;
    setTimeout(() => {
      showSuccess = false;
      m.redraw();
    }, attrs().showSuccessTime);
  }
  ...
}
dead-claudia commented 7 years ago

@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.

spacejack commented 7 years ago

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 view.)

dead-claudia commented 7 years ago

@spacejack It's inadvisable to access vnode.dom in the view, anyways - it's not there during the first render, and it doesn't see the updates from the current render (and is likely to get interfered with).

spacejack commented 7 years ago

I mean in an onclick etc. handler.

pygy commented 7 years ago

... getting the current dom (which isn't available in the vnode provided to view.)

@spacejack actually, the vnode is the same in all hooks of a given redraw() cycle, it is mutated after the view returns. @isiahmeadows I don't think it ever has the .dom set until the view returns...

spacejack commented 7 years ago

I suppose you can get the clicked element via event.currentTarget (maybe need to crawl through the dom for the element you want) so that's not so bad. However there's this case:

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.

dwaltrip commented 7 years ago

@spacejack This is what you meant? Looks like it could cause issues as you were suggesting. Although this approach could be somewhat inherently precarious, I can imagine a few situations in which it might make sense. I've found that these edge cases come up more frequently when you are doing things with 3rd party libraries. Of course, there are work arounds, but it usually involves a bit of extra code & manual bookkeeping.

const FooComponent = {
    view(fooVnode) {
        return m('.foo-div', m(BazComponent, {
            handleSomething: () => { /* want to use FooComponent dom. using fooVnode won't work */ }
        }));
    }
}