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',