Skip to content

Commit c436875

Browse files
committed
wip
1 parent 7b34699 commit c436875

File tree

4 files changed

+215
-2
lines changed

4 files changed

+215
-2
lines changed

packages/alpinejs/src/alpine.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed } fro
33
import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle'
44
import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation'
55
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
6-
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
6+
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions, evaluateRaw } from './evaluator'
77
import { transition } from './directives/x-transition'
88
import { clone, cloneNode, skipDuringClone, onlyDuringClone, interceptClone } from './clone'
99
import { interceptor, initInterceptors } from './interceptor'
@@ -65,6 +65,7 @@ let Alpine = {
6565
throttle,
6666
debounce,
6767
evaluate,
68+
evaluateRaw,
6869
initTree,
6970
nextTick,
7071
prefixed,

packages/alpinejs/src/evaluator.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export function normalEvaluator(el, expression) {
5050

5151
export function generateEvaluatorFromFunction(dataStack, func) {
5252
return (receiver = () => {}, { scope = {}, params = [], context } = {}) => {
53+
// If auto-evaluation is disabled, pass the function itself instead of calling it
54+
if (! shouldAutoEvaluateFunctions) {
55+
runIfTypeOfFunction(receiver, func, mergeProxies([scope, ...dataStack]), params)
56+
57+
return
58+
}
59+
5360
let result = func.apply(mergeProxies([scope, ...dataStack]), params)
5461

5562
runIfTypeOfFunction(receiver, result)
@@ -67,7 +74,7 @@ function generateFunctionFromString(expression, el) {
6774

6875
// Some expressions that are useful in Alpine are not valid as the right side of an expression.
6976
// Here we'll detect if the expression isn't valid for an assignment and wrap it in a self-
70-
// calling function so that we don't throw an error AND a "return" statement can b e used.
77+
// calling function so that we don't throw an error AND a "return" statement can be used.
7178
let rightSideSafeExpression = 0
7279
// Support expressions starting with "if" statements like: "if (...) doSomething()"
7380
|| /^[\n\s]*if.*\(.*\)/.test(expression.trim())
@@ -149,3 +156,40 @@ export function runIfTypeOfFunction(receiver, value, scope, params, el) {
149156
receiver(value)
150157
}
151158
}
159+
160+
export function evaluateRaw(el, expression, extras = {}) {
161+
let overriddenMagics = {}
162+
163+
injectMagics(overriddenMagics, el)
164+
165+
let dataStack = [overriddenMagics, ...closestDataStack(el)]
166+
167+
let scope = mergeProxies([extras.scope ?? {}, ...dataStack])
168+
169+
if (expression.includes('await')) {
170+
let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
171+
172+
let func = new AsyncFunction(
173+
["scope"],
174+
`with (scope) { return ${expression} }`
175+
)
176+
177+
let result = func.call(extras.context, scope)
178+
179+
return result
180+
} else {
181+
let func = new Function(
182+
["scope"],
183+
`with (scope) { return ${expression} }`
184+
)
185+
186+
let result = func.call(extras.context, scope)
187+
188+
// If the result is a function, call it
189+
if (typeof result === 'function' && shouldAutoEvaluateFunctions) {
190+
return result()
191+
}
192+
193+
return result
194+
}
195+
}

tests/vitest/evaluator.spec.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// @vitest-environment jsdom
2+
3+
import { describe, it, expect, beforeAll } from 'vitest';
4+
import Alpine from '../../packages/alpinejs/src/index.js';
5+
import { evaluate, evaluateLater } from '../../packages/alpinejs/src/evaluator.js';
6+
7+
beforeAll(() => Alpine.start())
8+
9+
describe('evaluate([String])', () => {
10+
it('simple expression', () => {
11+
let element = { parentNode: null, _x_dataStack: [] }
12+
13+
expect(evaluate(element, '42')).toBe(42)
14+
});
15+
16+
it('with scope', () => {
17+
let element = { parentNode: null, _x_dataStack: [] }
18+
19+
expect(evaluate(element, 'foo', { scope: { foo: 42 } })).toBe(42)
20+
});
21+
22+
it('with params', () => {
23+
let element = { parentNode: null, _x_dataStack: [] }
24+
25+
expect(evaluate(element, '(foo) => foo', { params: [42] })).toBe(42)
26+
});
27+
28+
it('with context', () => {
29+
let element = { parentNode: null, _x_dataStack: [] }
30+
31+
expect(evaluate(element, 'this.foo', { context: { foo: 42 } })).toBe(42)
32+
});
33+
34+
it('auto-evaluating function expression', () => {
35+
let element = { parentNode: null, _x_dataStack: [] }
36+
37+
expect(evaluate(element, '() => 42')).toBe(42)
38+
});
39+
40+
it('non auto-evaluating function expression', () => {
41+
let element = { parentNode: null, _x_dataStack: [] }
42+
43+
Alpine.dontAutoEvaluateFunctions(() => {
44+
expect(evaluate(element, '() => 42')()).toBe(42)
45+
})
46+
});
47+
48+
it('conditional', () => {
49+
let element = { parentNode: null, _x_dataStack: [] }
50+
51+
expect(evaluate(element, 'if (true) { return 42 }')).toBe(undefined)
52+
});
53+
54+
it('assignment', () => {
55+
let element = { parentNode: null, _x_dataStack: [] }
56+
57+
expect(evaluate(element, 'let foo = 42')).toBe(undefined)
58+
});
59+
60+
it('await', () => {
61+
let element = { parentNode: null, _x_dataStack: [] }
62+
63+
let scope = { foo: { bar: 'baz' } }
64+
65+
expect(evaluate(element, 'await new Promise(resolve => { foo.bar = "qux"; resolve() })', { scope })).toBe(undefined)
66+
67+
expect(scope.foo.bar).toBe('qux')
68+
});
69+
});
70+
71+
describe('evaluateLater([String])', () => {
72+
it('simple expression', () => {
73+
let element = { parentNode: null, _x_dataStack: [] }
74+
75+
let receiver = evaluateLater(element, '42')
76+
77+
receiver(value => {
78+
expect(value).toBe(42)
79+
})
80+
});
81+
82+
it('await', () => {
83+
let element = { parentNode: null, _x_dataStack: [] }
84+
85+
let receiver = evaluateLater(element, 'await new Promise(resolve => { setTimeout(() => resolve(42), 10) })')
86+
87+
receiver(value => {
88+
expect(value).toBe(42)
89+
})
90+
});
91+
})
92+
93+
describe('evaluate([Function])', () => {
94+
it('simple expression', () => {
95+
let element = { parentNode: null, _x_dataStack: [] }
96+
97+
expect(evaluate(element, () => 42)).toBe(42)
98+
});
99+
100+
it('with scope', () => {
101+
let element = { parentNode: null, _x_dataStack: [] }
102+
103+
expect(evaluate(element, function() { return this.foo }, { scope: { foo: 42 } })).toBe(42)
104+
});
105+
106+
it('with params', () => {
107+
let element = { parentNode: null, _x_dataStack: [] }
108+
109+
expect(evaluate(element, (foo) => foo, { params: [42] })).toBe(42)
110+
});
111+
112+
it.skip('with context', () => {
113+
// This is not supported with direct function evaluation...
114+
let element = { parentNode: null, _x_dataStack: [] }
115+
116+
expect(evaluate(element, () => this.foo, { context: { foo: 42 } })).toBe(42)
117+
});
118+
119+
it('auto-evaluating function expression', () => {
120+
let element = { parentNode: null, _x_dataStack: [] }
121+
122+
expect(evaluate(element, () => 42)).toBe(42)
123+
});
124+
125+
it('non auto-evaluating function expression', () => {
126+
let element = { parentNode: null, _x_dataStack: [] }
127+
128+
Alpine.dontAutoEvaluateFunctions(() => {
129+
expect(evaluate(element, () => 42)()).toBe(42)
130+
})
131+
});
132+
});
133+
134+
describe('evaluateLater([Function])', () => {
135+
it('simple expression', () => {
136+
let element = { parentNode: null, _x_dataStack: [] }
137+
138+
let receiver = evaluateLater(element, () => 42)
139+
140+
receiver(value => {
141+
expect(value).toBe(42)
142+
})
143+
});
144+
145+
it('await', () => {
146+
let element = { parentNode: null, _x_dataStack: [] }
147+
148+
let receiver = evaluateLater(element, () => new Promise(resolve => { setTimeout(() => resolve(42), 10) }))
149+
150+
receiver(value => {
151+
expect(value).toBe(42)
152+
})
153+
});
154+
})

vitest.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from 'vitest/config';
2+
import { readFileSync } from 'fs';
3+
4+
// Read the version from alpinejs package.json
5+
const alpinePackageJson = JSON.parse(
6+
readFileSync('./packages/alpinejs/package.json', 'utf-8')
7+
);
8+
const alpineVersion = alpinePackageJson.version;
9+
10+
export default defineConfig({
11+
define: {
12+
'ALPINE_VERSION': `'${alpineVersion}'`,
13+
},
14+
});

0 commit comments

Comments
 (0)