Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use dynamic context for defaultFormat #1195

Merged
merged 2 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 49 additions & 48 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import { BoxelInput, FieldContainer } from '@cardstack/boxel-ui/components';
import { cn, eq, pick } from '@cardstack/boxel-ui/helpers';
import { on } from '@ember/modifier';
import { startCase } from 'lodash';
import { getBoxComponent, type BoxComponent } from './field-component';
import {
getBoxComponent,
type BoxComponent,
DefaultFormatConsumer,
} from './field-component';
import { getContainsManyComponent } from './contains-many-component';
import { getLinksToEditor } from './links-to-editor';
import { LinksToEditor } from './links-to-editor';
import { getLinksToManyComponent } from './links-to-many-component';
import {
SupportedMimeType,
Expand Down Expand Up @@ -284,7 +288,7 @@ export interface Field<
): Promise<any>;
emptyValue(instance: BaseDef): any;
validate(instance: BaseDef, value: any): void;
component(model: Box<BaseDef>, defaultFormat: Format): BoxComponent;
component(model: Box<BaseDef>): BoxComponent;
getter(instance: BaseDef): BaseInstanceType<CardT>;
queryableValue(value: any, stack: BaseDef[]): SearchT;
queryMatcher(
Expand Down Expand Up @@ -578,27 +582,17 @@ class ContainsMany<FieldT extends FieldDefConstructor>
);
}

component(model: Box<BaseDef>, format: Format): BoxComponent {
component(model: Box<BaseDef>): BoxComponent {
let fieldName = this.name as keyof BaseDef;
let arrayField = model.field(
fieldName,
useIndexBasedKey in this.card,
) as unknown as Box<BaseDef[]>;

let renderFormat: Format | undefined = undefined;
if (
format === 'edit' &&
'isFieldDef' in model.value.constructor &&
model.value.constructor.isFieldDef
) {
renderFormat = 'atom';
}

return getContainsManyComponent({
model,
arrayField,
field: this,
format: renderFormat ?? format,
cardTypeFor,
});
}
Expand Down Expand Up @@ -750,8 +744,8 @@ class Contains<CardT extends FieldDefConstructor> implements Field<CardT, any> {
);
}

component(model: Box<BaseDef>, format: Format): BoxComponent {
return fieldComponent(this, model, format);
component(model: Box<BaseDef>): BoxComponent {
return fieldComponent(this, model);
}
}

Expand Down Expand Up @@ -1042,14 +1036,40 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
return fieldInstance;
}

component(model: Box<CardDef>, format: Format): BoxComponent {
if (format === 'edit' && !this.computeVia) {
let innerModel = model.field(
this.name as keyof BaseDef,
) as unknown as Box<CardDef | null>;
return getLinksToEditor(innerModel, this);
}
return fieldComponent(this, model, format);
component(model: Box<CardDef>): BoxComponent {
let isComputed = !!this.computeVia;
let fieldName = this.name as keyof CardDef;
let linksToField = this;
let getInnerModel = () => {
let innerModel = model.field(fieldName);
return innerModel as unknown as Box<CardDef | null>;
};
function shouldRenderEditor(
format: Format | undefined,
defaultFormat: Format,
isComputed: boolean,
) {
return (format ?? defaultFormat) === 'edit' && !isComputed;
}
return class LinksToComponent extends GlimmerComponent<{
Args: { Named: { format?: Format; displayContainer?: boolean } };
Blocks: {};
}> {
<template>
<DefaultFormatConsumer as |defaultFormat|>
{{#if (shouldRenderEditor @format defaultFormat isComputed)}}
<LinksToEditor @model={{(getInnerModel)}} @field={{linksToField}} />
{{else}}
{{#let (fieldComponent linksToField model) as |FieldComponent|}}
<FieldComponent
@format={{@format}}
@displayContainer={{@displayContainer}}
/>
{{/let}}
{{/if}}
</DefaultFormatConsumer>
</template>
};
}
}

Expand Down Expand Up @@ -1424,28 +1444,16 @@ class LinksToMany<FieldT extends CardDefConstructor>
return fieldInstances;
}

component(model: Box<CardDef>, format: Format): BoxComponent {
component(model: Box<CardDef>): BoxComponent {
let fieldName = this.name as keyof BaseDef;
let arrayField = model.field(
fieldName,
useIndexBasedKey in this.card,
) as unknown as Box<CardDef[]>;
let renderFormat: Format | undefined = undefined;
if (
format === 'edit' &&
'isFieldDef' in model.value.constructor &&
model.value.constructor.isFieldDef
) {
renderFormat = 'atom';
}
if (format === 'edit' && this.computeVia) {
renderFormat = 'embedded';
}
return getLinksToManyComponent({
model,
arrayField,
field: this,
format: renderFormat ?? format,
cardTypeFor,
});
}
Expand All @@ -1454,7 +1462,6 @@ class LinksToMany<FieldT extends CardDefConstructor>
function fieldComponent(
field: Field<typeof BaseDef>,
model: Box<BaseDef>,
defaultFormat: Format,
): BoxComponent {
let fieldName = field.name as keyof BaseDef;
let card: typeof BaseDef;
Expand All @@ -1465,7 +1472,7 @@ function fieldComponent(
(model.value[fieldName]?.constructor as typeof BaseDef) ?? field.card;
}
let innerModel = model.field(fieldName) as unknown as Box<BaseDef>;
return getBoxComponent(card, defaultFormat, innerModel, field);
return getBoxComponent(card, innerModel, field);
}

// our decorators are implemented by Babel, not TypeScript, so they have a
Expand Down Expand Up @@ -1643,8 +1650,8 @@ export class BaseDef {
return _createFromSerialized(this, data, doc, relativeTo, identityContext);
}

static getComponent(card: BaseDef, format: Format, field?: Field) {
return getComponent(card, format, field);
static getComponent(card: BaseDef, field?: Field) {
return getComponent(card, field);
}

static assignInitialFieldValue(
Expand Down Expand Up @@ -1862,7 +1869,6 @@ export type BaseDefComponent = ComponentLike<{
cardOrField: typeof BaseDef;
fields: any;
format: Format;
displayContainer?: boolean;
model: any;
set: Setter;
fieldName: string | undefined;
Expand Down Expand Up @@ -2712,15 +2718,10 @@ export type SignatureFor<CardT extends BaseDefConstructor> = {
};
};

export function getComponent(
model: BaseDef,
format: Format,
field?: Field,
): BoxComponent {
export function getComponent(model: BaseDef, field?: Field): BoxComponent {
let box = Box.create(model);
let boxComponent = getBoxComponent(
model.constructor as BaseDefConstructor,
format,
box,
field,
);
Expand Down
129 changes: 109 additions & 20 deletions packages/base/contains-many-component.gts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ import {
type FieldDef,
type BaseDef,
} from './card-api';
import { getBoxComponent, getPluralViewComponent } from './field-component';
import {
type BoxComponentSignature,
getBoxComponent,
DefaultFormatConsumer,
} from './field-component';
import { AddButton, IconButton } from '@cardstack/boxel-ui/components';
import { getPlural } from '@cardstack/runtime-common';
import { IconTrash } from '@cardstack/boxel-ui/icons';
import { TemplateOnlyComponent } from '@ember/component/template-only';

interface Signature {
interface ContainsManyEditorSignature {
Args: {
model: Box<FieldDef>;
arrayField: Box<FieldDef[]>;
format: Format;
field: Field<typeof FieldDef>;
cardTypeFor(
field: Field<typeof BaseDef>,
Expand All @@ -28,7 +32,7 @@ interface Signature {
};
}

class ContainsManyEditor extends GlimmerComponent<Signature> {
class ContainsManyEditor extends GlimmerComponent<ContainsManyEditorSignature> {
<template>
<div data-test-contains-many={{@field.name}}>
{{#if @arrayField.children.length}}
Expand All @@ -37,11 +41,11 @@ class ContainsManyEditor extends GlimmerComponent<Signature> {
<li class='editor' data-test-item={{i}}>
{{#let
(getBoxComponent
(@cardTypeFor @field boxedElement) @format boxedElement @field
(@cardTypeFor @field boxedElement) boxedElement @field
)
as |Item|
}}
<Item @format={{@format}} />
<Item />
{{/let}}
<div class='remove-button-container'>
<IconButton
Expand Down Expand Up @@ -118,33 +122,118 @@ class ContainsManyEditor extends GlimmerComponent<Signature> {
};
}

function getPluralChildFormat(effectiveFormat: Format, model: Box<FieldDef>) {
if (
effectiveFormat === 'edit' &&
'isFieldDef' in model.value.constructor &&
model.value.constructor.isFieldDef
) {
return 'atom';
}
return effectiveFormat;
}

function coalesce<T>(arg1: T | undefined, arg2: T): T {
return arg1 ?? arg2;
}

export function getContainsManyComponent({
model,
arrayField,
format,
field,
cardTypeFor,
}: {
model: Box<FieldDef>;
arrayField: Box<FieldDef[]>;
format: Format;
field: Field<typeof FieldDef>;
cardTypeFor(
field: Field<typeof BaseDef>,
boxedElement: Box<BaseDef>,
): typeof BaseDef;
}): BoxComponent {
if (format === 'edit') {
return <template>
<ContainsManyEditor
@model={{model}}
@arrayField={{arrayField}}
@field={{field}}
@format={{format}}
@cardTypeFor={{cardTypeFor}}
/>
</template>;
} else {
return getPluralViewComponent(arrayField, field, format, cardTypeFor);
let getComponents = () =>
arrayField.children.map((child) =>
getBoxComponent(cardTypeFor(field, child), child, field),
); // Wrap the the components in a function so that the template is reactive to changes in the model (this is essentially a helper)
let isComputed = !!field.computeVia;
function shouldRenderEditor(
format: Format | undefined,
defaultFormat: Format,
isComputed: boolean,
) {
if (
'isFieldDef' in model.value.constructor &&
model.value.constructor.isFieldDef
) {
return false;
}
if (isComputed) {
return false;
}
return (format ?? defaultFormat) === 'edit';
}
let containsManyComponent: TemplateOnlyComponent<BoxComponentSignature> =
<template>
<DefaultFormatConsumer as |defaultFormat|>
{{#if (shouldRenderEditor @format defaultFormat isComputed)}}
<ContainsManyEditor
@model={{model}}
@arrayField={{arrayField}}
@field={{field}}
@cardTypeFor={{cardTypeFor}}
/>
{{else}}
{{#let (coalesce @format defaultFormat) as |effectiveFormat|}}
<div
class='plural-field containsMany-field
{{effectiveFormat}}-format
{{unless arrayField.children.length "empty"}}'
data-test-plural-view={{field.fieldType}}
data-test-plural-view-format={{effectiveFormat}}
>
{{#each (getComponents) as |Item i|}}
<div data-test-plural-view-item={{i}}>
<Item
@format={{getPluralChildFormat effectiveFormat model}}
/>
</div>
{{/each}}
</div>
{{/let}}
{{/if}}
</DefaultFormatConsumer>
<style>
.containsMany-field.edit-format {
padding: var(--boxel-sp-sm);
background-color: var(--boxel-100);
border: none !important;
border-radius: var(--boxel-border-radius);
}
</style>
</template>;
return new Proxy(containsManyComponent, {
get(target, property, received) {
// proxying the bare minimum of an Array in order to render within a
// template. add more getters as necessary...
let components = getComponents();

if (property === Symbol.iterator) {
return components[Symbol.iterator];
}
if (property === 'length') {
return components.length;
}
if (typeof property === 'string' && property.match(/\d+/)) {
return components[parseInt(property)];
}
return Reflect.get(target, property, received);
},
getPrototypeOf() {
// This is necessary for Ember to be able to locate the template associated
// with a proxied component. Our Proxy object won't be in the template WeakMap,
// but we can pretend our Proxy object inherits from the true component, and
// Ember's template lookup respects inheritance.
return containsManyComponent;
},
});
}
Loading
Loading