From 18819ef1dc438d6db0bbca788f55c0c6edb3e391 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:41:22 -0400 Subject: [PATCH 01/11] Add failing test for value operator --- .../cypress/integration/custom-prefix.spec.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/cypress/integration/custom-prefix.spec.js b/tests/cypress/integration/custom-prefix.spec.js index f310c4f62..c88eb4153 100644 --- a/tests/cypress/integration/custom-prefix.spec.js +++ b/tests/cypress/integration/custom-prefix.spec.js @@ -1,4 +1,4 @@ -import { haveText, html, test } from '../utils' +import { haveAttribute, haveText, html, test } from '../utils' test('can set a custom x- prefix', html` @@ -14,3 +14,24 @@ test('can set a custom x- prefix', `, ({ get }) => get('span').should(haveText('bar')) ) + +test('can set a custom value operator', + html` + + +
+ + + +
+ `, + ({ get }) => { + get('span').should(haveAttribute('foo', 'bar')) + get('button').click() + get('span').should(haveAttribute('foo', 'baz')) + } +) From acd28f9ecafd46688b2a13e277e26c247b82c1ef Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:50:02 -0400 Subject: [PATCH 02/11] Extract the value operator to a variable --- packages/alpinejs/src/directives.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index 60ae8448e..97fae4fd7 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -4,6 +4,7 @@ import { elementBoundEffect } from './reactivity' import Alpine from './alpine' let prefixAsString = 'x-' +let valueOperatorAsString = ':' export function prefix(subject = '') { return prefixAsString + subject @@ -46,7 +47,7 @@ export function directives(el, attributes, originalAttributeOverride) { vAttributes = vAttributes.map(attribute => { if (staticAttributes.find(attr => attr.name === attribute.name)) { return { - name: `x-bind:${attribute.name}`, + name: `x-bind${valueOperatorAsString}${attribute.name}`, value: `"${attribute.value}"`, } } @@ -178,7 +179,7 @@ function outNonAlpineAttributes({ name }) { return alpineAttributeRegex().test(name) } -let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^:^.]+)\\b`)) +let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^${valueOperatorAsString}^.]+)\\b`)) function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) { return ({ name, value }) => { @@ -187,7 +188,7 @@ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) if (name === value) value = '' let typeMatch = name.match(alpineAttributeRegex()) - let valueMatch = name.match(/:([a-zA-Z0-9\-_:]+)/) + let valueMatch = name.match(new RegExp(`${valueOperatorAsString}([a-zA-Z0-9\\-_:]+)`)) let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [] let original = originalAttributeOverride || transformedAttributeMap[name] || name From 09d4b8d84de623497f4cce46facb5aeddcd44073 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:43:16 -0400 Subject: [PATCH 03/11] Add `useValueOperator` method to change the operator --- packages/alpinejs/src/alpine.js | 3 ++- packages/alpinejs/src/directives.js | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/alpinejs/src/alpine.js b/packages/alpinejs/src/alpine.js index 942fd1147..cfed954b8 100644 --- a/packages/alpinejs/src/alpine.js +++ b/packages/alpinejs/src/alpine.js @@ -1,5 +1,5 @@ import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw, watch, transaction } from './reactivity' -import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed } from './directives' +import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed, useValueOperator } from './directives' import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle' import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation' import { deferInit } from './deferInit' @@ -86,6 +86,7 @@ let Alpine = { walk, data, bind, + useValueOperator, } export default Alpine diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index 97fae4fd7..b7a8e9d03 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -14,6 +14,10 @@ export function setPrefix(newPrefix) { prefixAsString = newPrefix } +export function useValueOperator(newOperator) { + valueOperatorAsString = newOperator +} + let directiveHandlers = {} export function directive(name, callback) { From 6febf2fd9734f3e15189ba62e25849605889cd2c Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:44:02 -0400 Subject: [PATCH 04/11] Allow regex characters to be used in the value operator --- packages/alpinejs/src/directives.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index b7a8e9d03..87d9aef71 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -183,7 +183,7 @@ function outNonAlpineAttributes({ name }) { return alpineAttributeRegex().test(name) } -let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^${valueOperatorAsString}^.]+)\\b`)) +let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^${RegExp.escape(valueOperatorAsString)}^.]+)\\b`)) function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) { return ({ name, value }) => { @@ -192,7 +192,7 @@ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) if (name === value) value = '' let typeMatch = name.match(alpineAttributeRegex()) - let valueMatch = name.match(new RegExp(`${valueOperatorAsString}([a-zA-Z0-9\\-_:]+)`)) + let valueMatch = name.match(new RegExp(`${RegExp.escape(valueOperatorAsString)}([a-zA-Z0-9\\-_:]+)`)) let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [] let original = originalAttributeOverride || transformedAttributeMap[name] || name From 3d9d65e27c673c2427940f105d4a63a3f0a47aeb Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:45:07 -0400 Subject: [PATCH 05/11] Allow the value operator to be more than one character --- packages/alpinejs/src/directives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index 87d9aef71..b1b30592c 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -183,7 +183,7 @@ function outNonAlpineAttributes({ name }) { return alpineAttributeRegex().test(name) } -let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}([^${RegExp.escape(valueOperatorAsString)}^.]+)\\b`)) +let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}(.+?)(?=${RegExp.escape(valueOperatorAsString)}|\.|$)\\b`)) function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) { return ({ name, value }) => { From a7aca7153950699b989cfe5b76fdd9699542bc82 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:45:26 -0400 Subject: [PATCH 06/11] Add failing test for modifier operator --- .../cypress/integration/custom-prefix.spec.js | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/cypress/integration/custom-prefix.spec.js b/tests/cypress/integration/custom-prefix.spec.js index c88eb4153..e34c26c66 100644 --- a/tests/cypress/integration/custom-prefix.spec.js +++ b/tests/cypress/integration/custom-prefix.spec.js @@ -1,4 +1,4 @@ -import { haveAttribute, haveText, html, test } from '../utils' +import { haveAttribute, haveData, haveText, html, test } from '../utils' test('can set a custom x- prefix', html` @@ -35,3 +35,28 @@ test('can set a custom value operator', get('span').should(haveAttribute('foo', 'baz')) } ) + +test('can set a custom modifier operator', + html` + + +
+ +
+ `, + ({ get }) => { + get('button').click() + get('div').should(haveData('defaultPrevented', false)) + } +) From 4004a22167590acf66049baa134a42cdef651eb6 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:58:01 -0400 Subject: [PATCH 07/11] Extract the modifier operator to a variable --- packages/alpinejs/src/directives.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index b1b30592c..df17c2e2e 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -3,6 +3,7 @@ import { evaluate, evaluateLater } from './evaluator' import { elementBoundEffect } from './reactivity' import Alpine from './alpine' +let modifierOperatorAsString = '.' let prefixAsString = 'x-' let valueOperatorAsString = ':' @@ -183,7 +184,7 @@ function outNonAlpineAttributes({ name }) { return alpineAttributeRegex().test(name) } -let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}(.+?)(?=${RegExp.escape(valueOperatorAsString)}|\.|$)\\b`)) +let alpineAttributeRegex = () => (new RegExp(`^${prefixAsString}(.+?)(?=${RegExp.escape(valueOperatorAsString)}|${RegExp.escape(modifierOperatorAsString)}|$)\\b`)) function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) { return ({ name, value }) => { @@ -193,13 +194,13 @@ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) let typeMatch = name.match(alpineAttributeRegex()) let valueMatch = name.match(new RegExp(`${RegExp.escape(valueOperatorAsString)}([a-zA-Z0-9\\-_:]+)`)) - let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [] + let modifiers = name.match(new RegExp(`${RegExp.escape(modifierOperatorAsString)}[^.\\]]+(?=[^\\]]*$)`, 'g')) || [] let original = originalAttributeOverride || transformedAttributeMap[name] || name return { type: typeMatch ? typeMatch[1] : null, value: valueMatch ? valueMatch[1] : null, - modifiers: modifiers.map(i => i.replace('.', '')), + modifiers: modifiers.map(i => i.replace(modifierOperatorAsString, '')), expression: value, original, } From 1dfc3ea3eb3b505b62ce6579bf2c67942afaa8dc Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 14:48:28 -0400 Subject: [PATCH 08/11] Add `useModifierOperator` method to change the operator --- packages/alpinejs/src/alpine.js | 3 ++- packages/alpinejs/src/directives.js | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/alpinejs/src/alpine.js b/packages/alpinejs/src/alpine.js index cfed954b8..c7b12896d 100644 --- a/packages/alpinejs/src/alpine.js +++ b/packages/alpinejs/src/alpine.js @@ -1,5 +1,5 @@ import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw, watch, transaction } from './reactivity' -import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed, useValueOperator } from './directives' +import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed, useModifierOperator, useValueOperator } from './directives' import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle' import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation' import { deferInit } from './deferInit' @@ -86,6 +86,7 @@ let Alpine = { walk, data, bind, + useModifierOperator, useValueOperator, } diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index df17c2e2e..96d71fc84 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -15,6 +15,10 @@ export function setPrefix(newPrefix) { prefixAsString = newPrefix } +export function useModifierOperator(newOperator) { + modifierOperatorAsString = newOperator +} + export function useValueOperator(newOperator) { valueOperatorAsString = newOperator } From 333d303a65abbb26bac87f4bdb6ef2eefaa2f0aa Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 15:07:12 -0400 Subject: [PATCH 09/11] Simplify the test --- tests/cypress/integration/custom-prefix.spec.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/cypress/integration/custom-prefix.spec.js b/tests/cypress/integration/custom-prefix.spec.js index e34c26c66..962279b18 100644 --- a/tests/cypress/integration/custom-prefix.spec.js +++ b/tests/cypress/integration/custom-prefix.spec.js @@ -1,4 +1,4 @@ -import { haveAttribute, haveData, haveText, html, test } from '../utils' +import { haveAttribute, haveText, html, notBeChecked, test } from '../utils' test('can set a custom x- prefix', html` @@ -44,19 +44,12 @@ test('can set a custom modifier operator', }) -
- +
+
`, ({ get }) => { - get('button').click() - get('div').should(haveData('defaultPrevented', false)) + get('input').check() + get('input').should(notBeChecked()) } ) From 28787b0fe0bffdc28ba2f3036a9be20ce1ac6245 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 15:07:47 -0400 Subject: [PATCH 10/11] Allow modifier operator with more than one character --- packages/alpinejs/src/directives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index 96d71fc84..5a3e417fc 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -198,7 +198,7 @@ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) let typeMatch = name.match(alpineAttributeRegex()) let valueMatch = name.match(new RegExp(`${RegExp.escape(valueOperatorAsString)}([a-zA-Z0-9\\-_:]+)`)) - let modifiers = name.match(new RegExp(`${RegExp.escape(modifierOperatorAsString)}[^.\\]]+(?=[^\\]]*$)`, 'g')) || [] + let modifiers = name.match(new RegExp(`${RegExp.escape(modifierOperatorAsString)}(.+?)(?=${RegExp.escape(modifierOperatorAsString)}|\\]|$)(?=[^\\]]*$)`, 'g')) || [] let original = originalAttributeOverride || transformedAttributeMap[name] || name return { From 69de60ac81903683a234c484456eec227d94ddc1 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Wed, 9 Sep 2026 15:09:07 -0400 Subject: [PATCH 11/11] Fix the value operator regex to only include characters up until the modifier or end of the attribute name --- packages/alpinejs/src/directives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index 5a3e417fc..7e8c426b9 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -197,7 +197,7 @@ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) if (name === value) value = '' let typeMatch = name.match(alpineAttributeRegex()) - let valueMatch = name.match(new RegExp(`${RegExp.escape(valueOperatorAsString)}([a-zA-Z0-9\\-_:]+)`)) + let valueMatch = name.match(new RegExp(`${RegExp.escape(valueOperatorAsString)}(.+?)(?=${RegExp.escape(modifierOperatorAsString)}|$)`)) let modifiers = name.match(new RegExp(`${RegExp.escape(modifierOperatorAsString)}(.+?)(?=${RegExp.escape(modifierOperatorAsString)}|\\]|$)(?=[^\\]]*$)`, 'g')) || [] let original = originalAttributeOverride || transformedAttributeMap[name] || name