Skip to content

Commit

Permalink
[wip] starting to refactor toward decorator-driven fields
Browse files Browse the repository at this point in the history
  • Loading branch information
ef4 committed May 28, 2024
1 parent dee3d03 commit 1657cd1
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 84 deletions.
112 changes: 84 additions & 28 deletions packages/base/card-api.gts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
} from '@cardstack/runtime-common';
import type { ComponentLike } from '@glint/template';
import { initSharedState } from './shared-state';
import { tracked } from '@glimmer/tracking';

export { primitive, isField, type BoxComponent };
export const serialize = Symbol.for('cardstack-serialize');
Expand Down Expand Up @@ -293,7 +294,7 @@ export interface Field<
card: CardT;
name: string;
fieldType: FieldType;
computeVia: undefined | string | (() => unknown);
isComputed: boolean;
description: undefined | string;
// there exists cards that we only ever run in the host without
// the isolated renderer (RoomField), which means that we cannot
Expand Down Expand Up @@ -424,7 +425,7 @@ class ContainsMany<FieldT extends FieldDefConstructor>
readonly fieldType = 'containsMany';
constructor(
private cardThunk: () => FieldT,
readonly computeVia: undefined | string | (() => unknown),
readonly isComputed: boolean,
readonly name: string,
readonly description: string | undefined,
readonly isUsed: undefined | true,
Expand Down Expand Up @@ -635,7 +636,7 @@ class Contains<CardT extends FieldDefConstructor> implements Field<CardT, any> {
readonly fieldType = 'contains';
constructor(
private cardThunk: () => CardT,
readonly computeVia: undefined | string | (() => unknown),
readonly isComputed: boolean,
readonly name: string,
readonly description: string | undefined,
readonly isUsed: undefined | true,
Expand Down Expand Up @@ -781,7 +782,7 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
readonly fieldType = 'linksTo';
constructor(
private cardThunk: () => CardT,
readonly computeVia: undefined | string | (() => unknown),
readonly isComputed: boolean,
readonly name: string,
readonly description: string | undefined,
readonly isUsed: undefined | true,
Expand Down Expand Up @@ -1098,7 +1099,7 @@ class LinksToMany<FieldT extends CardDefConstructor>
readonly fieldType = 'linksToMany';
constructor(
private cardThunk: () => FieldT,
readonly computeVia: undefined | string | (() => unknown),
readonly isComputed: boolean,
readonly name: string,
readonly description: string | undefined,
readonly isUsed: undefined | true,
Expand Down Expand Up @@ -1535,25 +1536,75 @@ export function containsMany<FieldT extends FieldDefConstructor>(
}
containsMany[fieldType] = 'contains-many' as FieldType;

interface BabelDecoratorDescriptor {
configurable?: boolean;
enumerable?: boolean;
writable?: boolean;
get?(): any;
set?(v: any): void;
initializer?: null | (() => any);
value?: any;
}

type BabelDecorator = (
target: object,
prop: string | symbol,
desc: BabelDecoratorDescriptor,
) => BabelDecoratorDescriptor | null | undefined | void;

interface TrackedDescriptor<T> {
get(): T;
set(value: T): void;
}

function isTrackedDescriptor<T>(desc: any): desc is TrackedDescriptor<T> {
return Boolean(desc?.get && desc?.set);
}

export function contains<FieldT extends FieldDefConstructor>(
field: FieldT,
options?: Options,
): BaseInstanceType<FieldT> {
return {
setupField(fieldName: string) {
return makeDescriptor(
new Contains(
cardThunk(field),
options?.computeVia,
fieldName,
options?.description,
options?.isUsed,
),
);
},
} as any;
): PropertyDecorator {
// our decorators are implemented by Babel, not TypeScript, so they have a
// different signature than Typescript thinks they do.
let decorator: BabelDecorator = function (
target,
key,
desc,
): PropertyDescriptor {
if (typeof key !== 'string') {
throw new Error(`"contains" decorator only supports string field names`);
}
let isComputed = Boolean(desc.get);
let result: PropertyDescriptor;
if (isComputed) {
result = desc;
} else {
const trackedDesc = tracked(target, key, desc)!;

if (!isTrackedDescriptor(trackedDesc)) {
throw new Error(`bug: got unexpected result from @glimmer/tracking`);
}
result = {
get() {
return trackedDesc.get.call(this);
},
set(value) {
trackedDesc.set.call(this, value);
},
};
}
(result.get as any)[isField] = new Contains(
cardThunk(field),
isComputed,
key,
options?.description,
options?.isUsed,
);
return result;
};
return decorator as unknown as PropertyDecorator;
}
contains[fieldType] = 'contains' as FieldType;

export function linksTo<CardT extends CardDefConstructor>(
cardOrThunk: CardT | (() => CardT),
Expand Down Expand Up @@ -1992,13 +2043,19 @@ export class CardDef extends BaseDef {
[isSavedInstance] = false;
[realmInfo]: RealmInfo | undefined = undefined;
[realmURL]: URL | undefined = undefined;
@field id = contains(IDField);
@field title = contains(StringField);
@field description = contains(StringField);
@contains(IDField) declare id: BaseInstanceType<typeof IDField>;
@contains(StringField) declare title: BaseInstanceType<typeof StringField>;
@contains(StringField) declare description: BaseInstanceType<
typeof StringField
>;

// TODO: this will probably be an image or image url field card when we have it
// UPDATE: we now have a Base64ImageField card. we can probably refactor this
// to use it directly now (or wait until a better image field comes along)
@field thumbnailURL = contains(MaybeBase64Field);
@contains(MaybeBase64Field) declare thumbnailURL: BaseInstanceType<
typeof MaybeBase64Field
>;

static displayName = 'Card';
static isCardDef = true;

Expand Down Expand Up @@ -2266,7 +2323,7 @@ function serializedGet<CardT extends BaseDefConstructor>(
`tried to serializedGet field ${fieldName} which does not exist in card ${model.constructor.name}`,
);
}
return field.serialize(peekAtField(model, fieldName), doc, visited, opts);
return field.serialize((model as any)[fieldName], doc, visited, opts);
}

async function getDeserializedValue<CardT extends BaseDefConstructor>({
Expand Down Expand Up @@ -2588,19 +2645,18 @@ async function _updateFromSerialized<T extends BaseDefConstructor>(
`cannot change the id for saved instance ${originalId}`,
);
}
let deserialized = getDataBucket(instance);

// Before updating field's value, we also have to make sure
// the subscribers also subscribes to a new value.
let existingValue = deserialized.get(fieldName as string);
let existingValue = instance[fieldName];
if (
isCardOrField(existingValue) &&
isCardOrField(value) &&
existingValue !== value
) {
migrateSubscribers(existingValue, value);
}
deserialized.set(fieldName as string, value);
instance[fieldName] = value;
logger.log(recompute(instance));
}
if (isCardInstance(instance) && resource.id != null) {
Expand Down
21 changes: 20 additions & 1 deletion packages/host/tests/helpers/base-realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import type LoaderService from '@cardstack/host/services/loader-service';
import type * as CardAPIModule from 'https://cardstack.com/base/card-api';
import type * as NumberFieldModule from 'https://cardstack.com/base/number';
import type * as StringFieldModule from 'https://cardstack.com/base/string';
import type * as DateFieldModule from 'https://cardstack.com/base/date';
import type * as DatetimeFieldModule from 'https://cardstack.com/base/datetime';

let _string: (typeof StringFieldModule)['default'];
let _number: (typeof NumberFieldModule)['default'];
let _date: (typeof DateFieldModule)['default'];
let _datetime: (typeof DatetimeFieldModule)['default'];

let field: (typeof CardAPIModule)['field'];
let CardDef: (typeof CardAPIModule)['CardDef'];
let Component: (typeof CardAPIModule)['Component'];
Expand All @@ -18,8 +23,9 @@ let contains: (typeof CardAPIModule)['contains'];
let containsMany: (typeof CardAPIModule)['containsMany'];
let linksTo: (typeof CardAPIModule)['linksTo'];
let linksToMany: (typeof CardAPIModule)['linksToMany'];

let recompute: (typeof CardAPIModule)['recompute'];
let createFromSerialized: (typeof CardAPIModule)['createFromSerialized'];
let serializeCard: (typeof CardAPIModule)['serializeCard'];

let didInit = false;

Expand All @@ -38,6 +44,13 @@ async function initialize() {
await loader.import<typeof NumberFieldModule>(`${baseRealm.url}number`)
).default;

_date = (await loader.import<typeof DateFieldModule>(`${baseRealm.url}date`))
.default;

_datetime = (
await loader.import<typeof DatetimeFieldModule>(`${baseRealm.url}datetime`)
).default;

let cardAPI = await loader.import<typeof CardAPIModule>(
`${baseRealm.url}card-api`,
);
Expand All @@ -52,6 +65,8 @@ async function initialize() {
linksTo,
linksToMany,
recompute,
createFromSerialized,
serializeCard,
} = cardAPI);

didInit = true;
Expand All @@ -64,6 +79,8 @@ export async function setupBaseRealm(hooks: NestedHooks) {
export {
_string as StringField,
_number as NumberField,
_date as DateField,
_datetime as DatetimeField,
field,
CardDef,
Component,
Expand All @@ -73,4 +90,6 @@ export {
linksTo,
linksToMany,
recompute,
createFromSerialized,
serializeCard,
};
Original file line number Diff line number Diff line change
Expand Up @@ -1113,7 +1113,7 @@ module('Integration | card-basics', function (hooks) {
}
});

test('can render a linksTo field', async function (assert) {
QUnit.only('can render a linksTo field', async function (assert) {
let { field, contains, linksTo, CardDef, Component } = cardApi;
let { default: StringField } = string;

Expand Down
46 changes: 28 additions & 18 deletions packages/host/tests/integration/components/computed-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
linksToMany,
} from '../../helpers/base-realm';
import { renderCard } from '../../helpers/render-component';
import { BaseInstanceType } from 'https://cardstack.com/base/card-api';

let loader: Loader;

Expand All @@ -50,26 +51,35 @@ module('Integration | computeds', function (hooks) {
async () => await loader.import(`${baseRealm.url}card-api`),
);

test('can render a synchronous computed field', async function (assert) {
class Person extends CardDef {
@field firstName = contains(StringField);
@field lastName = contains(StringField);
@field fullName = contains(StringField, {
computeVia: function (this: Person) {
QUnit.only(
'can render a synchronous computed field',
async function (assert) {
class Person extends CardDef {
@contains(StringField) declare firstName: BaseInstanceType<
typeof StringField
>;

@contains(StringField) declare lastName: BaseInstanceType<
typeof StringField
>;

@contains(StringField)
get fullName(): BaseInstanceType<typeof StringField> {
return `${this.firstName} ${this.lastName}`;
},
});
static isolated = class Isolated extends Component<typeof this> {
<template>
<@fields.fullName />
</template>
};
}
}

let mango = new Person({ firstName: 'Mango', lastName: 'Abdel-Rahman' });
let root = await renderCard(loader, mango, 'isolated');
assert.strictEqual(root.textContent!.trim(), 'Mango Abdel-Rahman');
});
static isolated = class Isolated extends Component<typeof this> {
<template>
<@fields.fullName />
</template>
};
}

let mango = new Person({ firstName: 'Mango', lastName: 'Abdel-Rahman' });
let root = await renderCard(loader, mango, 'isolated');
assert.strictEqual(root.textContent!.trim(), 'Mango Abdel-Rahman');
},
);

test('can render a synchronous computed field (using a string in `computeVia`)', async function (assert) {
class Person extends CardDef {
Expand Down
Loading

0 comments on commit 1657cd1

Please sign in to comment.