Skip to content

Inline JS Expressions in Interactivity API - #79780

Closed
nickchomey wants to merge 13 commits into
WordPress:trunkfrom
nickchomey:add/interactivity-full-expressions
Closed

Inline JS Expressions in Interactivity API#79780
nickchomey wants to merge 13 commits into
WordPress:trunkfrom
nickchomey:add/interactivity-full-expressions

Conversation

@nickchomey

@nickchomey nickchomey commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What?

Closes #79765

Why?

As described in detail in the associated issue, this PR adds support for evaluating js expressions contained inline in the iAPI HTML attribute directives. This simplifies development and maintainance and can improve performance by reducing the amount of external js that needs to be fetched and parsed.

How?

Inspiration and code implementation on the client-side for this comes directly from the fantastic Datastar framework, which allows an arbitrary amount of JS expressions to be provided in an attribute, delimited by ;. This framework received significant scrutiny by many highly experienced developers over the course of its multi-year pre-v1 period.

The following args are exposed to each expression:

  • actions, callbacks, context, state, el (for triggering element) and evt (for the triggering event). This allows the expressions to be that much more... expressive...

Testing Instructions

packages/interactivity/src/test/hooks.ts - 36 tests:

  • splitStatements: 15 tests covering empty strings, no-semicolons, simple splits, string/regex/template-literal semicolons, IIFEs, escaped quotes, division-not-mistaken-for-regex, trailing semicolons, whitespace handling, and non-string inputs.
  • getEvaluate full-expression path: 21 tests covering comparisons, logical operators, ternaries, array filters, multi-statement "last value wins", direct state/context mutation, short-circuit operand return, JS truthiness (empty arrays, '0'), loose vs strict equality, nullish coalescing, string concatenation, bitwise/shift/exponentiation, and nested ternaries.

Unit Tests

cd gutenberg
npm run test:unit -- packages/interactivity/src/test/hooks.ts --runInBand

A simple "real-world" test:

  1. Check out the accompanying wordpress-develop PR (Inline JS Expressions in Interactivity API wordpress-develop#12383)
  2. npm install, build:dev , env:start, env:install
  3. Install this PR's build artifact plugin and activate
  4. add this snippet somewhere to process the output of any page through the php JS evaluator
add_filter('wp_template_enhancement_output_buffer', function($buffer) {
	return wp_interactivity_process_directives($buffer);
});
  1. create a post
  2. add an html block
  3. add this html
<div data-wp-interactive="myPlugin" 
	data-wp-context='{"location": null}' 
	data-wp-on--click="evt.target.tagName == 'BUTTON' 
		? (context.location = evt.target.id + ' was clicked at ' + evt.pageX + ',' + evt.pageY) 
		: null">

	<div data-wp-context='{"count": 1}'>
		<button id="button1" data-wp-on--click="context.count++; console.log(el.innerText)">
			Increment A
		</button>
		<div data-wp-text="context.count"></div>

		<div data-wp-text="Math.abs(context.count % 2) == 0 ? 'even' : 'odd'"></div>
	</div>
	<div data-wp-context='{"count": 10}'>
		<button id="button2" data-wp-on--click="context.count--; console.log(el.innerText)">
			Decrement B
		</button>
		<div data-wp-text="context.count"></div>
		<div data-wp-text="Math.abs(context.count % 2) == 0 ? 'even' : 'odd'"></div>
	</div>
	<div data-wp-text="context.location"></div>
</div>
  1. Save and load the page
  2. Open dev tools, reload page. Inspect the html doc response to see that "Odd" and "Even" were pre-populated from the server's expression evaluator.
  3. Click the numbers change, text changes between Odd and Even, and it also displays which button was pressed and its x,y coordinates (showing use of the el and evt
  4. Also check the console to see that "Increment A" and "Decrement B" were logged for each time you clicked, which uses the el arg available to the expressions and multi-expression directive capabilities
  5. For extra points, you can also step debug to see that the fnCache Map used in getEvaluate() prevents new functions from being created for identical expressions. Screen recording shows this as well.
Kooha-2026-07-03-18-17-55.mp4

Additionally, you could try moving any existing plugin's simple getter, actions, callback etc.. function from an external js file to the html callsite. It should evaluate as-normal.

Use of AI Tools

AI assistance: Yes
Tool(s): VSCode GitHub Copilot
Model(s): GPT-5.4, Deepseek V4 Flash and Pro
Used for: Frankly everything. But I iterated with it as a pair programmer.

@nickchomey nickchomey changed the title Add/interactivity full expressions Inline JS Expressions in Interactivity API Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Warning: Type of PR label mismatch

To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.

  • Required label: Any label starting with [Type].
  • Labels found: First-time Contributor, [Package] Interactivity.

Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task.

@github-actions github-actions Bot added the [Package] Interactivity /packages/interactivity label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: nickchomey <nickchomey@git.wordpress.org>
Co-authored-by: luisherranz <luisherranz@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@nickchomey
nickchomey marked this pull request as draft July 1, 2026 18:24
@github-actions github-actions Bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @nickchomey! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@nickchomey
nickchomey force-pushed the add/interactivity-full-expressions branch 7 times, most recently from 0098c6a to 7c6535d Compare July 4, 2026 00:46
@nickchomey
nickchomey marked this pull request as ready for review July 4, 2026 00:55
@nickchomey
nickchomey force-pushed the add/interactivity-full-expressions branch from 7c6535d to a6762f8 Compare July 4, 2026 01:28
@nickchomey

Copy link
Copy Markdown
Contributor Author

I should note that I took the liberty of addressing the comments to remove the hasNegativeOperator mechanism for function calls. It is isolated in its own commit for review, reversion etc...

@nickchomey
nickchomey force-pushed the add/interactivity-full-expressions branch from a6762f8 to d30ef0e Compare July 8, 2026 22:53

@luisherranz luisherranz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the substantial work you’ve put into this exploration. I want to clarify the direction, because my Slack comments seem to have been interpreted as a broader green light than I intended.

I understand the motivation. However, I don’t support allowing arbitrary JavaScript expressions in directive values, and I don’t think these PRs should move forward in their current direction.

What I said could be worth exploring was a very small expression syntax that:

  • Does not resemble JavaScript or PHP.
  • Is limited to simple cases such as comparisons and conditionals.
  • Has identical semantics in JavaScript and PHP.
  • Remains compatible with strict CSP.
  • Does not become a general-purpose code evaluator.

This proposal is fundamentally different. It compiles directive values using new Function(). The browser therefore accepts arbitrary JavaScript while PHP understands only a subset, creating inconsistent server and client behavior.

The CSP implications are particularly problematic. Sites using these expressions would need to permit 'unsafe-eval' or have them fail. In the WordPress ecosystem, that means a plugin could either be incompatible with a site’s security policy or require weakening that policy for the entire document. Core should not establish a public API with that requirement. A future WordPress CSP system should also never silently reduce a site’s protection simply because an installed plugin requires it, so it wouldn't solve this problem either.

There is a broader security concern as well. WordPress permits data-* attributes in sanitized post content, and directive markup can be produced by blocks, plugins, themes, filters, and dynamic data. Turning those values into executable source changes the trust boundary significantly. HTML escaping such as esc_attr() does not make a value safe to compile as JavaScript.

Finally, although this proposal offers some convenience, it adds no new capability: the same logic can be placed in a JavaScript store, where it benefits from normal linting, bundling, caching, type checking, and debugging tools. I don’t think avoiding a few small getters justifies the security, compatibility, and maintenance costs.

I would still consider a separate proposal for a small, read-only DSL with syntax distinct from JavaScript and PHP. It could support state and context paths, literals, comparisons, simple conditionals… Its grammar and behavior would need to be exactly the same in PHP and JavaScript.

For those reasons, I don’t support this proposal or merging the current PRs. If there is interest in that narrower alternative, I would welcome a design-focused exploration of it.

@nickchomey

nickchomey commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@luisherranz

Thanks for the review. Since this is really a discussion about the proposal rather than the implementation, I've replied on #79765 to keep the design discussion in one place.

- Tests for splitStatements: null cases, basic split, string/regex/template
  literal handling, mixed content, non-string input
- Tests for getEvaluate full-expression path: context comparisons (!==, ===),
  ternary, negation, logical operators, array filter, multi-statement,
  invalid expressions, backward-compatible fast path
…ake sure that hasNegativeOperator is working
@nickchomey
nickchomey force-pushed the add/interactivity-full-expressions branch from d30ef0e to 00cdc67 Compare July 16, 2026 15:34
@nickchomey

Copy link
Copy Markdown
Contributor Author

As per discussion in #79765, this is simply not a direction that the iAPI maintainers are willing to consider. Closing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] Interactivity /packages/interactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Support evaluation of inline JS expressions in Interactivity API

2 participants