diff --git a/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.test.ts b/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.test.ts
index 62786ade02b3..3ab84b295072 100644
--- a/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.test.ts
+++ b/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.test.ts
@@ -77,6 +77,22 @@ describe('angular source decorator', () => {
});
});
+ describe('with component with void element and attribute selector', () => {
+ @Component({
+ selector: 'input[foo]',
+ template: '',
+ })
+ class VoidElementWithAttributeComponent {}
+
+ it('should create without separate closing tag', async () => {
+ const component = VoidElementWithAttributeComponent;
+ const props = {};
+ const argTypes: ArgTypes = {};
+ const source = computesTemplateSourceFromComponent(component, props, argTypes);
+ expect(source).toEqual(``);
+ });
+ });
+
describe('with component with attribute and value only selector', () => {
@Component({
selector: '[foo="bar"]',
@@ -93,6 +109,22 @@ describe('angular source decorator', () => {
});
});
+ describe('with component with void element, attribute and value only selector', () => {
+ @Component({
+ selector: 'input[foo="bar"]',
+ template: '',
+ })
+ class VoidElementWithAttributeComponent {}
+
+ it('should create and add attribute to template without separate closing tag', async () => {
+ const component = VoidElementWithAttributeComponent;
+ const props = {};
+ const argTypes: ArgTypes = {};
+ const source = computesTemplateSourceFromComponent(component, props, argTypes);
+ expect(source).toEqual(``);
+ });
+ });
+
describe('with component with class selector', () => {
@Component({
selector: 'doc-button.foo',
diff --git a/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.ts b/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.ts
index 297b63c57af5..c6703bf9e3c5 100644
--- a/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.ts
+++ b/app/angular/src/client/preview/angular-beta/ComputesTemplateFromComponent.ts
@@ -141,6 +141,26 @@ const buildTemplate = (
inputs: string,
outputs: string
) => {
+ // https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#syntax-elements
+ const voidElements = [
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'command',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'keygen',
+ 'link',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr',
+ ];
+
const firstSelector = selector.split(',')[0];
const templateReplacers: [
string | RegExp,
@@ -153,7 +173,14 @@ const buildTemplate = (
[/#([\w-]+)/, ` id="$1"`],
[/((\.[\w-]+)+)/, (_, c) => ` class="${c.split`.`.join` `.trim()}"`],
[/(\[.+?])/g, (_, a) => ` ${a.slice(1, -1)}`],
- [/([\S]+)(.*)/, `<$1$2${inputs}${outputs}>${innerTemplate}$1>`],
+ [
+ /([\S]+)(.*)/,
+ (template, elementSelector) => {
+ return voidElements.some((element) => elementSelector === element)
+ ? template.replace(/([\S]+)(.*)/, `<$1$2${inputs}${outputs} />`)
+ : template.replace(/([\S]+)(.*)/, `<$1$2${inputs}${outputs}>${innerTemplate}$1>`);
+ },
+ ],
];
return templateReplacers.reduce(