Skip to content

Commit

Permalink
feat(runtime): proxy form associated custom element lifecycle callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
Hans Claasen authored and HansClaasen committed Oct 29, 2023
1 parent b97dadc commit 5f8ce33
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 3 deletions.
29 changes: 26 additions & 3 deletions src/runtime/proxy-component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BUILD } from '@app-data';
import { consoleDevWarn, getHostRef, plt } from '@platform';
import { CMP_FLAGS } from '@utils';

import type * as d from '../declarations';
import { HOST_FLAGS, MEMBER_FLAGS } from '../utils/constants';
import { PROXY_FLAGS } from './runtime-constants';
import { FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS, PROXY_FLAGS } from './runtime-constants';
import { getValue, setValue } from './set-value';

/**
Expand All @@ -21,14 +22,36 @@ export const proxyComponent = (
cmpMeta: d.ComponentRuntimeMeta,
flags: number,
): d.ComponentConstructor => {
const prototype = (Cstr as any).prototype;

/**
* proxy form associated custom element lifecycle callbacks
* @ref https://web.dev/articles/more-capable-form-controls#lifecycle_callbacks
*/
if (BUILD.formAssociated && cmpMeta.$flags$ & CMP_FLAGS.formAssociated && flags & PROXY_FLAGS.isElementConstructor) {
FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS.forEach((cbName) =>
Object.defineProperty(prototype, cbName, {
value(this: d.HostElement, ...args: any[]) {
const hostRef = getHostRef(this);
const elm = BUILD.lazyLoad ? hostRef.$hostElement$ : this;
const instance = BUILD.lazyLoad ? hostRef.$lazyInstance$ : (elm as any);
if (!instance) {
hostRef.$onReadyPromise$.then((instance) => instance?.[cbName](...args));
} else {
const cb = instance[cbName];
typeof cb === 'function' && cb(...args);
}
},
}),
);
}

if (BUILD.member && cmpMeta.$members$) {
if (BUILD.watchCallback && Cstr.watchers) {
cmpMeta.$watchers$ = Cstr.watchers;
}
// It's better to have a const than two Object.entries()
const members = Object.entries(cmpMeta.$members$);
const prototype = (Cstr as any).prototype;

members.map(([memberName, [memberFlags]]) => {
if (
(BUILD.prop || BUILD.state) &&
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/runtime-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,10 @@ export const HYDRATE_CHILD_ID = 'c-id';
export const HYDRATED_CSS = '{visibility:hidden}.hydrated{visibility:inherit}';

export const XLINK_NS = 'http://www.w3.org/1999/xlink';

export const FORM_ASSOCIATED_CUSTOM_ELEMENT_CALLBACKS = [
'formAssociatedCallback',
'formResetCallback',
'formDisabledCallback',
'formStateRestoreCallback',
] as const;
4 changes: 4 additions & 0 deletions test/karma/test-app/form-associated/cmp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export class FormAssociatedCmp {
this.internals.setFormValue('my default value');
}

formAssociatedCallback(form: HTMLFormAssociatedElement) {
form.ariaLabel = 'formAssociated called';
}

render() {
return <input type="text" />;
}
Expand Down
5 changes: 5 additions & 0 deletions test/karma/test-app/form-associated/karma.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ describe('form associated', function () {
expect(elm).not.toBeNull();
});

it('should trigger form associated custom element lifecycle callbacks', async () => {
const formEl = app.querySelector('form');
expect(formEl.ariaLabel).toBe('formAssociated called');
});

it('should link up to the surrounding form', async () => {
const formEl = app.querySelector('form');
// this shows that the element has, through the `ElementInternals`
Expand Down

0 comments on commit 5f8ce33

Please sign in to comment.