Uh oh!
There was an error while loading. Please reload this page.
invokeHelper argument-based thunks - #762
Conversation
Uh oh!
There was an error while loading. Please reload this page.
ef4
commented
Aug 20, 2021
I'm not fully convinced this solves the problem better than what can already be done with existing primitives. It's the author of the helper who knows whether the entangling of argument tracking matters or not. But it's the caller of the helper who would be obligated to pick the right invocation pattern. So this doesn't make it possible for helper authors to solve their own problem reliably. Instead, a helper author who cares about fine-grained autotracking of several different values could design their API to make that mandatory by making thunks or tracked properties explicitly part of what you're expected to pass in. For example, nothing stops the author from accepting an argument that it itself a thunk: invokeHelper(this,SomeHelper,()=>({firstArg: ()=>theTrueValue,}))Or accepting an argument with tracked properties: classHelperArgs{
@trackedsomeArgument;
@trackedsomeOtherArgument;}// ---letargs=newHelperArgs();invokeHelper(this,TheHelper,()=>args);args.someArgument=1;// fine-grained invalidation visible to the helper. |
NullVoxPopuli
commented
Sep 6, 2021
How would something like this be possible if we don't have argument-based thunks? #434 (comment) // wrapper around curry because glimmer components only take named argsfunctioncomponent(Klass,thunk){returncurry(Klass,()=>{letnamed=thunk();// all args are consumed, regardless of usagereturn{ named };});}letcurried=component(MyComponent,()=>({fruit: this.pepper})) |
unless... we make a public API for the Lazy Args Proxy class that I've seen duplicated in a couple places 🤔 Like, letargs=argsProxyFor({positional: [()=>this.a,()=>this.b,],named: {c: ()=>this.c,d: ()=>this.d,}});args.named.c// consumes and evaluates the this.cargs.positional[0]// consumes and evaluates this.aIt'd be cool if we had the same sort of API for all of components/helpers/modifiers maybe // example of something I want for components / modifiers / helpers / etc.// (not a goal of this RFC, but something I do want to work towards)functioncomponent(Klass,namedArgumentThunks){returncurry(Klass,argsProxyFor({named: namedArgumentThunks}));}// usagecurriedComponent=component(MyComponent,{c: ()=>this.c,d: ()=>this.d,};This is still verbose, but would allow curried components to have lazily consumed args. idk. Anywho, I still think we need a way to build out these lazy-evaluating argument objects. I'm open to suggestions, but I don't think the framework provides the building blocks we need atm. |
What I was saying above is that the author of a helper can design their arguments to be independently lazy so their consumption can be tracked independently. That is different from being able to invoke an arbitrary existing helper in a way that provides the laziness. And I think that's OK because it's the author of the helper who even knows whether some arguments will remain unconsumed in some important cases. If they know it's important, they can design the arguments to accommodate it.
I'm pretty sure it doesn't require anything from the framework, just native Finally, an alternative to currying-in-JS would be to implement argument splatting plus GJS or equivalent, which would give a very clear one-liner way to do currying: importMyComponentfrom'./whatever';letcurriedValue="Some Title";letcurriedComponent=<template><MyComponent @title=curriedValue ...attributes...arguments/></template>And this case avoids all the complexities of visible thunks, which is in one sense the whole part of our declarative template language. |
NullVoxPopuli
commented
Oct 5, 2021
thanks!
this is very exciting, and if we added block splatting, that would solve a ton of design problems I'm currently having creating components. letcurriedComponent=<template><MyComponent @title=curriedValue ...attributes...arguments...blocks/></template> |
rwjblue
commented
Oct 8, 2021
After some discussion with the core team today, I am moving this RFC into "FCP to Close". Based on some conversations that @NullVoxPopuli and I had in discord, using getters for the named properties should allow the laziness desired here without changes to the system (and "its just JavaScript" 🎉 ). exportdefaultclassPlusOneextendsComponent{plusOne=invokeHelper(this,RemoteData,()=>{return{positional: [],named: {getfoo(){returnthis.args.qux;}}}});} |
NullVoxPopuli
commented
Oct 19, 2021
a sort of caveat I learned while doing the getter approach with modifiers, and maybe this is obvious... I mean, it makes sense, but this will get you an infinite loop: classMyComponentextendsComponent{
@trackedfoo;myModifier=configureModifier({getfoo(){// `this` is the context of the getter (the object passed to configureModifier), so accessing `foo` here leads to an infinite loopreturnthis.foo;}})}so, looking at @rwjblue's example, I tried: classMyComponentextendsComponent{
@trackedfoo;myModifier=configureModifier(()=>({getfoo(){// `this` is *still* the object being returnedreturnthis.foo;}}))}and lastly: classMyComponentextendsComponent{
@trackedfoo;myModifier=configureModifier(()=>{return{getfoo(){// `this` is *still* the object being returnedreturnthis.foo;}};})}So, the recommended approach to achieve lazy tracked consumption doesn't work 🤔 Here is a reproduction: NullVoxPopuli/ember-resources#248 |
NullVoxPopuli
commented
Oct 19, 2021
This "works", plusOne=invokeHelper(this,RemoteData,()=>{return{positional: [],named: {foo: ()=>this.startingNumber,},};});but it means that all accesses to |
rwjblue
commented
Oct 20, 2021
Ya, sorry about that. I think the syntax would be: exportdefaultclassPlusOneextendsComponent{plusOne=invokeHelper(this,RemoteData,()=>{return{positional: [],named: {getfoo: ()=>this.args.qux,}}});} |
NullVoxPopuli
commented
Oct 20, 2021
|
rwjblue
commented
Oct 22, 2021
Eeck. That's what I get for quickly typing out a reply on mobile. The point I was making is still valid, just need to fix the syntax. Another shot at the syntax is here: exportdefaultclassPlusOneextendsComponent{plusOne=invokeHelper(this,RemoteData,()=>{letcomponentContext=this;return{positional: [],named: {getfoo(){returncomponentContext.args.qux;}}}});}But you could also do a nice bit of composition with a class in that location (which would be cool if you were doing more complicated things, and you wanted to unit test just this functionality): classPlusOneData{
@trackedargs;constructor(args){this.args=args;this.positional=[];}getfoo(){returnthis.args.qux;}}exportdefaultclassPlusOneextendsComponent{plusOne=invokeHelper(this,RemoteData,()=>{returnPlusOneData(this.args);});}And if that data bag had its own destructor logic (you could imagine it caring about cleanup stuff, depending on what it does), you could do: import{registerDestructor,associateDestroyableChild}from'@ember/destroyable';classPlusOneData{
@trackedargs;constructor(args){this.args=args;this.positional=[];registerDestructor(this,()=>{// do whatever cleanup you need});}getfoo(){returnthis.args.qux;}}exportdefaultclassPlusOneextendsComponent{plusOne=invokeHelper(this,RemoteData,()=>{letdata=PlusOneData(this.args);associateDestroyableChild(this,data);returndata;});} |
NullVoxPopuli
commented
Oct 22, 2021
That makes sense. I guess I was just too tired when I was trying this out to think about assigning I'll have to play around with these last couple examples (thanks for taking the time to write them! <3 ) and see if I can make a nice public API out of these. I'm good with closing this RFC now :D |

Rendered