diff --git a/e2e/html/directive-bind.html b/e2e/html/directive-bind.html
new file mode 100644
index 00000000..98dc7025
--- /dev/null
+++ b/e2e/html/directive-bind.html
@@ -0,0 +1,40 @@
+
+
+
+ Directives -- data-wp-bind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/html/directive-bind.js b/e2e/html/directive-bind.js
new file mode 100644
index 00000000..ec68a2c8
--- /dev/null
+++ b/e2e/html/directive-bind.js
@@ -0,0 +1,15 @@
+import { store } from '../../src/runtime/store';
+
+// State for the store hydration tests.
+store({
+ state: {
+ url: '/some-url',
+ checked: true,
+ },
+ actions: {
+ toggle: ({ state }) => {
+ state.url = '/some-other-url';
+ state.checked = !state.checked;
+ },
+ },
+});
diff --git a/e2e/specs/directive-bind.spec.ts b/e2e/specs/directive-bind.spec.ts
new file mode 100644
index 00000000..6006cd8b
--- /dev/null
+++ b/e2e/specs/directive-bind.spec.ts
@@ -0,0 +1,56 @@
+import { test, expect } from '../tests';
+
+test.describe('data-wp-bind', () => {
+ test.beforeEach(async ({ goToFile }) => {
+ await goToFile('directive-bind.html');
+ });
+
+ test('add missing href at hydration', async ({ page }) => {
+ const el = page.getByTestId('add missing href at hydration');
+ await expect(el).toHaveAttribute('href', '/some-url');
+ });
+
+ test('change href at hydration', async ({ page }) => {
+ const el = page.getByTestId('change href at hydration');
+ await expect(el).toHaveAttribute('href', '/some-url');
+ });
+
+ test('update missing href at hydration', async ({ page }) => {
+ const el = page.getByTestId('add missing href at hydration');
+ await expect(el).toHaveAttribute('href', '/some-url');
+ page.getByTestId('toggle').click();
+ await expect(el).toHaveAttribute('href', '/some-other-url');
+ });
+
+ test('add missing checked at hydration', async ({ page }) => {
+ const el = page.getByTestId('add missing checked at hydration');
+ await expect(el).toHaveAttribute('checked', '');
+ });
+
+ test('remove existing checked at hydration', async ({ page }) => {
+ const el = page.getByTestId('remove existing checked at hydration');
+ await expect(el).not.toHaveAttribute('checked', '');
+ });
+
+ test('update existing checked', async ({ page }) => {
+ const el = page.getByTestId('add missing checked at hydration');
+ const el2 = page.getByTestId('remove existing checked at hydration');
+ let checked = await el.evaluate(
+ (element: HTMLInputElement) => element.checked
+ );
+ let checked2 = await el2.evaluate(
+ (element: HTMLInputElement) => element.checked
+ );
+ expect(checked).toBe(true);
+ expect(checked2).toBe(false);
+ await page.getByTestId('toggle').click();
+ checked = await el.evaluate(
+ (element: HTMLInputElement) => element.checked
+ );
+ checked2 = await el2.evaluate(
+ (element: HTMLInputElement) => element.checked
+ );
+ expect(checked).toBe(false);
+ expect(checked2).toBe(true);
+ });
+});
diff --git a/src/runtime/directives.js b/src/runtime/directives.js
index 574e3282..9df99d27 100644
--- a/src/runtime/directives.js
+++ b/src/runtime/directives.js
@@ -116,9 +116,22 @@ export default () => {
Object.entries(bind)
.filter((n) => n !== 'default')
.forEach(([attribute, path]) => {
- element.props[attribute] = evaluate(path, {
+ const result = evaluate(path, {
context: contextValue,
});
+ element.props[attribute] = result;
+
+ useEffect(() => {
+ // This seems necessary because Preact doesn't change the attributes
+ // on the hydration, so we have to do it manually. It doesn't need
+ // deps because it only needs to do it the first time.
+ result === false
+ ? element.ref.current.removeAttribute(attribute)
+ : element.ref.current.setAttribute(
+ attribute,
+ result === true ? '' : result
+ );
+ }, []);
});
}
);
diff --git a/src/runtime/hooks.js b/src/runtime/hooks.js
index 40b9f320..cc2b0093 100644
--- a/src/runtime/hooks.js
+++ b/src/runtime/hooks.js
@@ -20,9 +20,11 @@ export const component = (name, Comp) => {
// Resolve the path to some property of the store object.
const resolve = (path, context) => {
+ // If path starts with !, remove it and save a flag.
+ const isNegative = path[0] === '!' && !!(path = path.slice(1));
let current = { ...store, context };
path.split('.').forEach((p) => (current = current[p]));
- return current;
+ return isNegative ? !current : current;
};
// Generate the evaluate function.
diff --git a/webpack.config.js b/webpack.config.js
index 88cb9201..0747f183 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -10,6 +10,7 @@ module.exports = [
runtime: './src/runtime',
'e2e/page-1': './e2e/page-1',
'e2e/page-2': './e2e/page-2',
+ 'e2e/html/directive-bind': './e2e/html/directive-bind',
},
output: {
filename: '[name].js',