From 508ddb1d9b8f8dadbad4eede20424ac937372441 Mon Sep 17 00:00:00 2001 From: Burcu Noyan Date: Fri, 5 Jun 2026 13:55:49 -0400 Subject: [PATCH 1/3] add option to render containsMany field without the chrome --- packages/base/contains-many-component.gts | 47 +++++++++++++++-------- packages/base/field-component.gts | 8 +++- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/base/contains-many-component.gts b/packages/base/contains-many-component.gts index 81c7b5408c1..1c244f9f707 100644 --- a/packages/base/contains-many-component.gts +++ b/packages/base/contains-many-component.gts @@ -361,23 +361,40 @@ export function getContainsManyComponent({ (coalesce @format defaultFormats.fieldDef) as |effectiveFormat| }} -
+ {{#if (coalesce @displayContainer true)}} +
+ {{#each (getComponents) as |Item i|}} +
+ +
+ + {{/each}} +
+ {{else}} {{#each (getComponents) as |Item i|}} -
- -
+ {{/each}} -
+ {{/if}} {{/let}} {{/if}} diff --git a/packages/base/field-component.gts b/packages/base/field-component.gts index feb40ded310..37f5b120ac1 100644 --- a/packages/base/field-component.gts +++ b/packages/base/field-component.gts @@ -29,6 +29,7 @@ import { import type { ComponentLike } from '@glint/template'; import { CardContainer } from '@cardstack/boxel-ui/components'; import { + coalesce, extractCssVariables, sanitizeHtmlSafe, } from '@cardstack/boxel-ui/helpers'; @@ -371,7 +372,12 @@ export function getBoxComponent( {{/let}} - {{else if (isCompoundField model.value)}} + {{else if + (and + (isCompoundField model.value) + (coalesce @displayContainer true) + ) + }} From 4cc0f66e294af167a1c07236648784a6d293a37d Mon Sep 17 00:00:00 2001 From: Burcu Noyan Date: Fri, 5 Jun 2026 17:15:19 -0400 Subject: [PATCH 2/3] add test coverage --- packages/base/contains-many-component.gts | 6 +- .../2a48e225-71c2-445d-affc-76fcf4f8c21c.json | 41 ++++++++ packages/experiments-realm/family.gts | 56 +++++++++++ .../components/card-basics-test.gts | 95 +++++++++++++++++++ 4 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 packages/experiments-realm/Family/2a48e225-71c2-445d-affc-76fcf4f8c21c.json create mode 100644 packages/experiments-realm/family.gts diff --git a/packages/base/contains-many-component.gts b/packages/base/contains-many-component.gts index 1c244f9f707..91e442efa15 100644 --- a/packages/base/contains-many-component.gts +++ b/packages/base/contains-many-component.gts @@ -384,14 +384,10 @@ export function getContainsManyComponent({ {{/each}} {{else}} - {{#each (getComponents) as |Item i|}} + {{#each (getComponents) as |Item|}} {{/each}} {{/if}} diff --git a/packages/experiments-realm/Family/2a48e225-71c2-445d-affc-76fcf4f8c21c.json b/packages/experiments-realm/Family/2a48e225-71c2-445d-affc-76fcf4f8c21c.json new file mode 100644 index 00000000000..c8a46e5b79c --- /dev/null +++ b/packages/experiments-realm/Family/2a48e225-71c2-445d-affc-76fcf4f8c21c.json @@ -0,0 +1,41 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "Family", + "module": "../family" + } + }, + "type": "card", + "attributes": { + "people": [ + { + "firstName": "Mango", + "lastName": "A." + }, + { + "firstName": "Marco", + "lastName": "N." + } + ], + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + }, + "cardInfo.cardThumbnail": { + "links": { + "self": null + } + } + } + } +} diff --git a/packages/experiments-realm/family.gts b/packages/experiments-realm/family.gts new file mode 100644 index 00000000000..4a9542d2cbc --- /dev/null +++ b/packages/experiments-realm/family.gts @@ -0,0 +1,56 @@ +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, + StringField, +} from 'https://cardstack.com/base/card-api'; +import { BoxelContainer } from '@cardstack/boxel-ui/components'; + +class FamilyMember extends FieldDef { + static displayName = 'Family Member'; + @field firstName = contains(StringField); + @field lastName = contains(StringField); + static embedded = class Embedded extends Component { + + }; + static fitted = this.embedded; +} + +export class Family extends CardDef { + static displayName = 'Family'; + @field people = containsMany(FamilyMember); + static isolated = class Isolated extends Component { + + }; +} diff --git a/packages/host/tests/integration/components/card-basics-test.gts b/packages/host/tests/integration/components/card-basics-test.gts index 128855122a9..3aa33cc5284 100644 --- a/packages/host/tests/integration/components/card-basics-test.gts +++ b/packages/host/tests/integration/components/card-basics-test.gts @@ -2991,6 +2991,101 @@ module('Integration | card-basics', function (hooks) { .hasStyle({ height: '32px' }); }); + test('renders containsMany composite field without chrome when displayContainer is false', async function (this: RenderingTestContext, assert) { + class Person extends FieldDef { + @field firstName = contains(StringField); + @field lastName = contains(StringField); + static embedded = class Embedded extends Component { + + }; + } + + class Family extends CardDef { + @field people = containsMany(Person); + static isolated = class Isolated extends Component { + + }; + } + loader.shimModule(`${testRealmURL}test-cards`, { Family, Person }); + + let abdelRahmans = new Family({ + people: [ + new Person({ firstName: 'Mango', lastName: 'Abdel-Rahman' }), + new Person({ firstName: 'Van Gogh', lastName: 'Abdel-Rahman' }), + ], + }); + + await renderCard(loader, abdelRahmans, 'isolated'); + + assert + .dom('.plural-field') + .doesNotExist('containsMany wrapper div is not rendered'); + assert + .dom('.containsMany-item') + .doesNotExist('per-item wrapper divs are not rendered'); + assert + .dom('[data-test-compound-field-component]') + .doesNotExist('compound field wrapper is not rendered'); + assert + .dom('[data-test-person-firstName]') + .exists({ count: 2 }, 'items are rendered'); + assert.deepEqual( + [...this.element.querySelectorAll('[data-test-person-firstName]')].map( + (el) => el.textContent?.trim(), + ), + ['Mango', 'Van Gogh'], + 'item content is rendered in order', + ); + }); + + test('renders compound field without chrome when displayContainer is false', async function (assert) { + class Name extends FieldDef { + @field firstName = contains(StringField); + @field lastName = contains(StringField); + static embedded = class Embedded extends Component { + + }; + } + + class Person extends CardDef { + @field name = contains(Name); + static isolated = class Isolated extends Component { + + }; + } + loader.shimModule(`${testRealmURL}test-cards`, { Person, Name }); + + let person = new Person({ + name: new Name({ firstName: 'Arthur', lastName: 'Dent' }), + }); + + await renderCard(loader, person, 'isolated'); + + assert + .dom('[data-test-compound-field-component]') + .doesNotExist('compound field wrapper div is not rendered'); + assert + .dom('[data-test-name]') + .exists('field content is still rendered') + .containsText('Arthur Dent'); + }); + test('can #each over a containsMany primitive @fields', async function (assert) { class Person extends CardDef { @field firstName = contains(StringField); From 195c1553f4ccd242ab4c18d4f8afe79df679ba90 Mon Sep 17 00:00:00 2001 From: Burcu Noyan Date: Tue, 9 Jun 2026 12:43:28 -0400 Subject: [PATCH 3/3] adjust playground atom preview displayContainer logic --- .../code-submode/playground/playground-preview.gts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/app/components/operator-mode/code-submode/playground/playground-preview.gts b/packages/host/app/components/operator-mode/code-submode/playground/playground-preview.gts index 814e4c80054..c5eb5284050 100644 --- a/packages/host/app/components/operator-mode/code-submode/playground/playground-preview.gts +++ b/packages/host/app/components/operator-mode/code-submode/playground/playground-preview.gts @@ -91,7 +91,7 @@ const PlaygroundPreview: TemplateOnlyComponent =