-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
(Improve extensibility) Export injectMagics
, remove dontAutoEvaluateFunctions
#4309
base: main
Are you sure you want to change the base?
(Improve extensibility) Export injectMagics
, remove dontAutoEvaluateFunctions
#4309
Conversation
While I'm generally in favor of making more internals exposed, I am quite curious (and concerned) about what in your plugin you feel warrants access to these specifically. Right now They don't have particularly clear utility outside of where they are used, so I immediately worry about the approach your plugin is taking that makes these appear necessary. Separately there is the concern that, at least for
But anway. Yeah, if you have something demonstrating the plugin you're trying to make, it will be easier to advise on this. |
@ekwoka If this didn't happen, then I could just write my own function runner, and it'd be OK. So if I understand these dependencies correctly, I need to extract the one that comes from By the way, my plugin is just a tweaked evaluator (that's why In my opinion it'd be better to pass a flag to the evaluate function that tells it to not run functions automatically, then get rid of |
A more complicated alternative would be to deprecate return dontAutoEvaluateFunctions(() => {
return evaluate(el, binding.expression)
}) Change it to: evaluate(el, binding.expression, {}, false) // <--- extra flag for auto execution Then we can remove |
I enjoy this rethought approach. Solve the problem with less code :) |
injectMagics
and runIfTypeOfFunction
(for plugin makers)injectMagics
, remove dontAutoEvaluateFunctions
@ekwoka I did the refactors I was talking about. Please check them out. By the way, the
I manually tested it with the examples in the doc, and they worked. |
packages/alpinejs/src/evaluator.js
Outdated
@@ -117,7 +103,7 @@ function generateEvaluatorFromString(dataStack, expression, el) { | |||
// Check if the function ran synchronously, | |||
if (func.finished) { | |||
// Return the immediate result. | |||
runIfTypeOfFunction(receiver, func.result, completeScope, params, el) | |||
runIfTypeOfFunction(autoEvaluateFunctions, receiver, func.result, completeScope, params, el) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a clean way to avoid calling this entirely if we aren't auto evaluating functions?
Since why call runIfTypeOfFunction
if we aren't auto evaluating it, right? Then the function name doesn't make much sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean to conditionally execute it if it's a function, else simply return the value?
One thing to mind is that runIfTypeOfFunction
will execute the function once, and return whatever value it is (including another function), but if it returns another async
function, then it will continue executing it recursively. In other words, when it's async
, it will unwrap all the promises and return the final value.
Check this example.
<html>
<head>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
</head>
<body>
<h1 x-data="{ fn: () => () => () => () => 5 }" x-text="fn"></h1>
<h1 x-data="{ fn: async () => async () => async () => async () => 5 }" x-text="fn"></h1>
</body>
</html>
Result on screen:
() => () => () => 5
5
So runIfTypeOfFunction
not only changes with different values of the auto-execute flag, but also whether the values are promises or not. I don't feel confident enough to decide how to refactor this, because maybe some part of the code needs the recursive unwrapping (although I've never seen something like this in the source code). I'd personally just leave it like that and refactor it next time, which wouldn't be that hard considering runIfTypeOfFunction
is contained (private) inside evaluator.js
(alpinejs/csp).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't write it in the first place, but I feel I can say with high confidence that the case of functions returning promises that resolve to functions that return promises that resolve to functions was never a case that came up when writing that code...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ekwoka I refactored and renamed the function.
- Refactored
runIfTypeOfFunction
to remove the recursive behavior. Now the behavior is more predictable: it just evaluates once without recursively unwrapping functions (which by the way only worked for promises, which made it even harder to understand what it was doing) - Renamed
runIfTypeOfFunction
tohandleEvalResult
, because the name may not make sense anymore. Basically the purpose of this function is to send a result to the receiver, so this name is more generic I guess.
Code snippet:
export function handleEvalResult(autoEvaluateFunctions, receiver, value, scope, params, el) {
const shouldCallFunc = autoEvaluateFunctions && typeof value === 'function'
const result = shouldCallFunc ? value.apply(scope, params) : value
if (result instanceof Promise) {
result.then(i => receiver(i)).catch( error => handleError( error, el, value ) )
} else {
receiver(result)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's looking a lot better.
But I think we go further!
What if being passed to handle eval result, the autoEvaluateFunctions prop just wrapped the receiver
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like
const inner receiver = autoCall
? (maybeFn) => maybeFn instanceof Function
? receiver(maybeFn.apply(scope, params))
: receiver(maybeFn)
: receiver
well, prettier than that I mean...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ekwoka Thank you for the clarification! I think the problem is that this would make other codes more verbose. The wrapping logic would need to be done in the caller, and currently there are several places where handleEvalResult(...)
is called, so all those places would need to get modified.
Basically all places that look like this (three in total):
return (receiver = () => {}, /* ... */) => {
// ....
// modify the receiver here
handleEvalResult(...modifiedArgs, result)
}
So I guess in this case it'd be better to just let handleEvalResult
do the dirty job of deciding whether to execute or not. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. Just exploring.
I guess it being the default case to auto evaluate makes it clunkier to abstract.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ekwoka Yes, I think it's a bit difficult to abstract it further (without doing a major revamp)
So from me that's all I have to add to the PR, it achieves everything I wanted, with only exporting injectMagics
and by refactoring the other involved parts (handleEvalResult
function)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making things better by removing code :)
Summary:
injectMagics
to make it possible to make certain plugins.dontAutoEvaluateFunctions
becauserunIfTypeOfFunction
depends on data managed by that function, butrunIfTypeOfFunction
is not exported, therefore it's impossible to make certain plugins that need to change the evaluator.runIfTypeOfFunction
to remove the recursive behavior. Now the behavior is more predictable: it just evaluates once without recursively unwrapping functions (which by the way only worked for promises, which made it even harder to understand what it was doing)runIfTypeOfFunction
tohandleEvalResult
, because the name may not make sense anymore. Basically the purpose of this function is to send a result to the receiver, so this name is more generic I guess.I tried to sort the imports by length size but I didn't want to touch it too much.
Some extra info regarding
injectMagics
: This function injects into an object all the magics that are inside this array hidden inside the module. Since this array is inside the module (and not exported), it can't be accessed, and even if I copied the source code ofinjectMagics
, I wouldn't be able to get the data. Therefore for plugin makers that need to inject magics, this is a requirement.Additionally, injectDataProviders does an equivalent thing for the
datas
array (which gets populated when usingAlpine.data(...)
). I don't need this function for my plugins so I didn't export it, but may be for someone.