Skip to content

Commit

Permalink
fix: disable form association entirely on older API versions (#4041)
Browse files Browse the repository at this point in the history
Fixes #3929
  • Loading branch information
nolanlawson authored Mar 11, 2024
1 parent ccd0253 commit 754d9bb
Show file tree
Hide file tree
Showing 21 changed files with 252 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,11 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) {
renderer: { attachInternals },
} = vm;

if (!isAPIFeatureEnabled(APIFeature.ENABLE_ELEMENT_INTERNALS, apiVersion)) {
if (!isAPIFeatureEnabled(APIFeature.ENABLE_ELEMENT_INTERNALS_AND_FACE, apiVersion)) {
throw new Error(
`The attachInternals API is only supported in API version 61 and above. To use this API please update the LWC component API version.`
`The attachInternals API is only supported in API version 61 and above. ` +
`The current version is ${apiVersion}. ` +
`To use this API, update the LWC component API version. https://lwc.dev/guide/versioning`
);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/@lwc/engine-core/src/framework/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { logError, logWarn } from '../shared/logger';

import { RendererAPI } from './renderer';
import { cloneAndOmitKey, parseStyleText } from './utils';
import { cloneAndOmitKey, parseStyleText, shouldBeFormAssociated } from './utils';
import { allocateChildren, mount, removeNode } from './rendering';
import {
createVM,
Expand Down Expand Up @@ -321,7 +321,8 @@ function hydrateCustomElement(

const { sel, mode, ctor, owner } = vnode;
const { defineCustomElement, getTagName } = renderer;
defineCustomElement(StringToLowerCase.call(getTagName(elm)), Boolean(ctor.formAssociated));
const isFormAssociated = shouldBeFormAssociated(ctor);
defineCustomElement(StringToLowerCase.call(getTagName(elm)), isFormAssociated);

const vm = createVM(elm, ctor, renderer, {
mode,
Expand Down
4 changes: 3 additions & 1 deletion packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export { parseFragment, parseSVGFragment } from './template';
export { hydrateRoot } from './hydration';

// Internal APIs used by compiled code -------------------------------------------------------------
export { registerComponent, getComponentAPIVersion } from './component';
export { registerComponent } from './component';
export { registerTemplate } from './secure-template';
export { registerDecorators } from './decorators/register';

Expand All @@ -39,6 +39,8 @@ export { reportingControl as __unstable__ReportingControl } from './reporting';
export { swapTemplate, swapComponent, swapStyle } from './hot-swaps';
export { setHooks } from './overridable-hooks';
export { freezeTemplate } from './freeze-template';
export { getComponentAPIVersion } from './component';
export { shouldBeFormAssociated } from './utils';

// Experimental or Internal APIs
export { getComponentConstructor } from './get-component-constructor';
Expand Down
4 changes: 2 additions & 2 deletions packages/@lwc/engine-core/src/framework/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { logError } from '../shared/logger';
import { getComponentTag } from '../shared/format';
import { RendererAPI } from './renderer';
import { EmptyArray, shouldUseNativeCustomElementLifecycle } from './utils';
import { EmptyArray, shouldBeFormAssociated, shouldUseNativeCustomElementLifecycle } from './utils';
import { markComponentAsDirty } from './component';
import { getScopeTokenClass } from './stylesheet';
import { lockDomMutation, patchElementWithRestrictions, unlockDomMutation } from './restrictions';
Expand Down Expand Up @@ -337,7 +337,7 @@ function mountCustomElement(
const useNativeLifecycle = shouldUseNativeCustomElementLifecycle(
ctor as LightningElementConstructor
);
const isFormAssociated = Boolean(ctor.formAssociated);
const isFormAssociated = shouldBeFormAssociated(ctor);
const elm = createCustomElement(
normalizedTagname,
upgradeCallback,
Expand Down
23 changes: 22 additions & 1 deletion packages/@lwc/engine-core/src/framework/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import {
isUndefined,
isNull,
} from '@lwc/shared';
import { logWarnOnce } from '../shared/logger';
import { StylesheetFactory, TemplateStylesheetFactories } from './stylesheet';
import { getComponentAPIVersion } from './component';
import { getComponentAPIVersion, getComponentRegisteredName } from './component';
import { LightningElementConstructor } from './base-lightning-element';
import { VElementData } from './vnodes';

Expand Down Expand Up @@ -157,3 +158,23 @@ export function applyTemporaryCompilerV5SlotFix(data: VElementData) {
}
return data;
}

export function shouldBeFormAssociated(Ctor: LightningElementConstructor) {
const ctorFormAssociated = Boolean(Ctor.formAssociated);
const apiVersion = getComponentAPIVersion(Ctor);
const apiFeatureEnabled = isAPIFeatureEnabled(
APIFeature.ENABLE_ELEMENT_INTERNALS_AND_FACE,
apiVersion
);

if (process.env.NODE_ENV !== 'production' && ctorFormAssociated && !apiFeatureEnabled) {
const tagName = getComponentRegisteredName(Ctor);
logWarnOnce(
`Component <${tagName}> set static formAssociated to true, but form ` +
`association is not enabled because the API version is ${apiVersion}. To enable form association, ` +
`update the LWC component API version to 61 or above. https://lwc.dev/guide/versioning`
);
}

return ctorFormAssociated && apiFeatureEnabled;
}
3 changes: 2 additions & 1 deletion packages/@lwc/engine-dom/src/apis/create-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
disconnectRootElement,
LightningElement,
getComponentAPIVersion,
shouldBeFormAssociated,
} from '@lwc/engine-core';
import { renderer } from '../renderer';

Expand Down Expand Up @@ -135,7 +136,7 @@ export function createElement(
!lwcRuntimeFlags.DISABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE &&
isAPIFeatureEnabled(APIFeature.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE, apiVersion);

const isFormAssociated = Boolean(Ctor.formAssociated);
const isFormAssociated = shouldBeFormAssociated(Ctor);

// the custom element from the registry is expecting an upgrade callback
/*
Expand Down
7 changes: 3 additions & 4 deletions packages/@lwc/engine-dom/src/apis/hydrate-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
hydrateRoot,
connectRootElement,
getAssociatedVMIfPresent,
shouldBeFormAssociated,
} from '@lwc/engine-core';
import { StringToLowerCase, isFunction, isNull, isObject } from '@lwc/shared';
import { renderer } from '../renderer';
Expand Down Expand Up @@ -89,10 +90,8 @@ export function hydrateComponent(

try {
const { defineCustomElement, getTagName } = renderer;
defineCustomElement(
StringToLowerCase.call(getTagName(element)),
Boolean(Ctor.formAssociated)
);
const isFormAssociated = shouldBeFormAssociated(Ctor);
defineCustomElement(StringToLowerCase.call(getTagName(element)), isFormAssociated);
const vm = createVMWithProps(element, Ctor, props);

hydrateRoot(vm);
Expand Down
6 changes: 5 additions & 1 deletion packages/@lwc/integration-karma/helpers/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,10 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
customElementCallbackReactionErrorListener,
true
),
toThrowCallbackReactionErrorEvenInSyntheticLifecycleMode: errorMatcherFactory(
windowErrorListener,
true
),
};

beforeAll(function () {
Expand Down Expand Up @@ -565,7 +569,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
USE_COMMENTS_FOR_FRAGMENT_BOOKENDS: process.env.API_VERSION >= 60,
USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS: process.env.API_VERSION >= 60,
DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION: process.env.API_VERSION >= 60,
ENABLE_ELEMENT_INTERNALS: process.env.API_VERSION >= 61,
ENABLE_ELEMENT_INTERNALS_AND_FACE: process.env.API_VERSION >= 61,
ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE: process.env.API_VERSION >= 61,
USE_LIGHT_DOM_SLOT_FORWARDING: process.env.API_VERSION >= 61,
};
Expand Down
4 changes: 2 additions & 2 deletions packages/@lwc/integration-karma/scripts/karma-plugins/lwc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ function createPreprocessor(config, emitter, logger) {

// Wrap all the tests into a describe block with the file structure name
// This avoids needing to manually write `describe()` for every file.
// Also add a dummy test because otherwise Jasmine complains about empty describe()s:
// Also add an empty test because otherwise Jasmine complains about empty describe()s:
// https://github.com/jasmine/jasmine/pull/1742
const ancestorDirectories = path.relative(basePath, suiteDir).split(path.sep);
const intro =
ancestorDirectories.map((tag) => `describe("${tag}", function () {`).join('\n') +
`\nxit("dummy test", () => { /* empty */ });\n`;
`\nxit("empty test", () => { /* empty */ });\n`;
const outro = ancestorDirectories.map(() => `});`).join('\n');

// TODO [#3370]: remove experimental template expression flag
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { createElement } from 'lwc';
import { customElementCallbackReactionErrorListener, ENABLE_ELEMENT_INTERNALS } from 'test-utils';
import {
customElementCallbackReactionErrorListener,
ENABLE_ELEMENT_INTERNALS_AND_FACE,
} from 'test-utils';

import ShadowDomCmp from 'ai/shadowDom';
import LightDomCmp from 'ai/lightDom';
Expand Down Expand Up @@ -54,7 +57,7 @@ const attachInternalsSanityTest = (tagName, ctor) => {
});
};

if (ENABLE_ELEMENT_INTERNALS) {
if (ENABLE_ELEMENT_INTERNALS_AND_FACE) {
if (typeof ElementInternals !== 'undefined') {
// ElementInternals API is supported in the browser
if (process.env.NATIVE_SHADOW) {
Expand Down Expand Up @@ -101,7 +104,7 @@ if (ENABLE_ELEMENT_INTERNALS) {
// Note CustomElementConstructor is not upgraded by LWC and inherits directly from HTMLElement which means it calls the native
// attachInternals API.
expect(() => document.body.appendChild(elm)).toThrowError(
/The attachInternals API is only supported in API version 61 and above. To use this API please update the LWC component API version./
/The attachInternals API is only supported in API version 61 and above/
);
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { createElement } from 'lwc';
import { ENABLE_ELEMENT_INTERNALS } from 'test-utils';
import { ENABLE_ELEMENT_INTERNALS_AND_FACE } from 'test-utils';

import NotFormAssociated from 'x/notFormAssociated';
import FormAssociated from 'x/formAssociated';
import FormAssociatedFalse from 'x/formAssociatedFalse';
import NotFormAssociatedNoAttachInternals from 'x/notFormAssociatedNoAttachInternals';
import FormAssociatedNoAttachInternals from 'x/formAssociatedNoAttachInternals';
import FormAssociatedFalseNoAttachInternals from 'x/formAssociatedFalseNoAttachInternals';

if (
ENABLE_ELEMENT_INTERNALS &&
ENABLE_ELEMENT_INTERNALS_AND_FACE &&
typeof ElementInternals !== 'undefined' &&
!process.env.SYNTHETIC_SHADOW_ENABLED
) {
Expand Down Expand Up @@ -35,3 +38,69 @@ if (
).not.toThrow();
});
}

if (typeof ElementInternals !== 'undefined' && !process.env.SYNTHETIC_SHADOW_ENABLED) {
const isFormAssociated = (elm) => {
const form = document.createElement('form');
document.body.appendChild(form);
form.appendChild(elm);
const result = elm.formAssociatedCallbackCalled;
document.body.removeChild(form); // cleanup
return result;
};

it('disallows form association on older API versions', () => {
let elm;

// formAssociated = true
const createFormAssociatedTrue = () => {
elm = createElement('x-form-associated-no-attach-internals', {
is: FormAssociatedNoAttachInternals,
});
};
if (ENABLE_ELEMENT_INTERNALS_AND_FACE) {
createFormAssociatedTrue();
expect(isFormAssociated(elm)).toBe(true);
} else {
expect(createFormAssociatedTrue).toLogWarningDev(
/Component <x-form-associated-no-attach-internals> set static formAssociated to true, but form association is not enabled/
);
expect(isFormAssociated(elm)).toBe(false);
}

// formAssociated = false
elm = createElement('x-form-associated-false-no-attach-internals', {
is: FormAssociatedFalseNoAttachInternals,
});
expect(isFormAssociated(elm)).toBe(false);

// formAssociated = undefined
elm = createElement('x-not-form-associated-no-attach-internals', {
is: NotFormAssociatedNoAttachInternals,
});
expect(isFormAssociated(elm)).toBe(false);
});
}

if (!ENABLE_ELEMENT_INTERNALS_AND_FACE) {
it('warns for attachInternals on older API versions', () => {
// formAssociated = true
expect(() => {
expect(() => createElement('x-form-associated', { is: FormAssociated })).toThrowError(
/The attachInternals API is only supported in API version 61 and above/
);
}).toLogWarningDev(
/Component <x-form-associated> set static formAssociated to true, but form association is not enabled/
);

// formAssociated = false
expect(() =>
createElement('x-form-associated-false', { is: FormAssociatedFalse })
).toThrowError(/The attachInternals API is only supported in API version 61 and above/);

// formAssociated = undefined
expect(() =>
createElement('x-not-form-associated', { is: NotFormAssociated })
).toThrowError(/The attachInternals API is only supported in API version 61 and above/);
});
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { LightningElement } from 'lwc';
import { api, LightningElement } from 'lwc';

export default class extends LightningElement {
static formAssociated = false;

@api
internals;

constructor() {
super();
this.internals = this.attachInternals();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { api, LightningElement } from 'lwc';

export default class extends LightningElement {
static formAssociated = false;

@api
formAssociatedCallbackCalled = false;

formAssociatedCallback() {
this.formAssociatedCallbackCalled = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { api, LightningElement } from 'lwc';

export default class extends LightningElement {
static formAssociated = true;

@api
formAssociatedCallbackCalled = false;

formAssociatedCallback() {
this.formAssociatedCallbackCalled = true;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { api, LightningElement } from 'lwc';

export default class extends LightningElement {
@api
formAssociatedCallbackCalled = false;

formAssociatedCallback() {
this.formAssociatedCallbackCalled = true;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createElement } from 'lwc';
import { ariaProperties, ariaAttributes, ENABLE_ELEMENT_INTERNALS } from 'test-utils';
import { ariaProperties, ariaAttributes, ENABLE_ELEMENT_INTERNALS_AND_FACE } from 'test-utils';

import ElementInternal from 'ei/component';

if (
ENABLE_ELEMENT_INTERNALS &&
ENABLE_ELEMENT_INTERNALS_AND_FACE &&
process.env.NATIVE_SHADOW &&
typeof ElementInternals !== 'undefined'
) {
Expand Down
Loading

0 comments on commit 754d9bb

Please sign in to comment.