Skip to content

Instantly share code, notes, and snippets.

@habdelra
Last active July 24, 2019 19:30
Show Gist options
  • Save habdelra/04f822b59fc551d345c539b3f20c47c9 to your computer and use it in GitHub Desktop.
Save habdelra/04f822b59fc551d345c539b3f20c47c9 to your computer and use it in GitHub Desktop.
// Sample Ticket card
// A card can represent both its schema and its data in a single document
{
data: {
type: 'cards'
id: '@acme/ticket-tools::ticket-card:123',
attributes: {
'suggested-name': 'ticket',
'edit-template': `
{{delegate-to content.event}} {{!-- framework decides the format to use, appears as embedded until a user clicks on it, for example --}}
{{delegate-to content.price}}
`,
'isolated-template': `
Event: {{cardstack-content content.event}}
Attendee: {{content.user.name}}
Start time: {{moment-format content.time 'MM/DD/YYYY hh:mm A'}} {{!-- we can access adopted cards metadata the same as our own --}}
Price: {{content.price}}
Purchased date: {{moment-format content.purchaseDate 'MM/DD/YYYY'}}
`,
'embedded-template': `
Event name: {{content.eventName}}
Start time: {{moment-format content.eventTime 'MM/DD/YYYY hh:mm A'}}
Price: {{content.price}}
`,
'isolated-js': `
import Component from '@glimmer/component';
import moment from 'moment';
export default class TicketIsolatedComponent extends Component { ... }
`,
// Maybe this actually comes from the base-card
'embedded-js': `
import Component from '@glimmer/component';
export default class TicketEmbeddedComponent extends Component { ... }
`,
'embedded-css': '',
'edit-css': '',
'isolated-css': '',
'dependencies': [
'moment': '^2.24.0'
],
// Maybe this actually comes from the base-card
'peer-dependencies': [
'@glimmer/component': '*'
],
// intentionally not including dev-dependencies, I dont think the hub actually cares about those...
},
relationships: {
fields: [
{ data: { type: 'fields', id: '@acme/ticket-tools::ticket-card::price' } },
{ data: { type: 'fields', id: '@acme/ticket-tools::ticket-card::purchase-date' } },
{ data: { type: 'fields', id: '@acme/ticket-tools::ticket-card::attendee' } },
{ data: { type: 'fields', id: '@acme/ticket-tools::ticket-card::event' } },
],
// not using the example of a ticket card that adopts an event card--this example would be better suited
// to using card relationships as a composition tool. a better example would be to create a custom event
// card that has some extra fields that adopts a core event card.
adopts: {
// interesting idea: all cards can trace their origin to a cardstack "genesis card" which can
// provide default template, component, peer-deps, ...
data: { type: 'cards', id: '@cardstack/core-types::genesis-card::0::v1.0.0'} // unclear about the card ID here...
},
// a card always implements it's own interface, so it's implemeting its own ticket card
// interface as well as implementing the interfaces below. the implements definitions
// below allow the ticket card to be able to act as a user card and as an event card
implements: [{
data: {
type: 'cards',
id: '@cardstack/core-types::user-card::0::v1.0.0', // unclear about the card ID here...
meta: {
// here we provide mappings from our card's metadata fields to the user interface's fields.
// in this case the mapping is traversing the ticket card's attenee relationship, for which
// the attenee is a metadata field, and the fields specified from the attendee card are the
// attenee's metadata fields
'metadata-mappings': {
id: 'attendee.id',
username: 'attentee.username',
name: 'attendee.name',
email: 'attendee.email'
}
}
}
},{
data: {
type: 'cards',
id: '@cardstack/core-types::event-card::0::v1.0.0', // unclear about the card ID here...
meta: {
// here we provide mappings from our card's metadata fields to the user interface's fields.
// in this case the mapping is traversing the ticket card's event relationship, for which
// the event is a metadata field, and the fields specified from the event card are the
// event's metadata fields
'metadata-mappings': {
name: 'event.name',
address: 'event.address',
'date-time': 'event.date-time'
}
}
}
}],
model: {
data: { type: '@acme/ticket-tools::ticket-cards', id: '@acme/ticket-tools::ticket-card::123' }, // note the model id matches the card id
}
}
},
included: [
// This is a bootstrap model, I'm just adding it here for clarity
{
type: 'cards',
id: '@cardstack/core-types::string-field-card', // this id means that it's a string field
attributes: {
'isolated-template': ...,
'embedded-template': ...,
'edit-template' ...,
'isolated-js': ...,
'embedded-js': ...,
'edit-js': ...
},
},
{
type: 'fields'
id: '@acme/ticket-tools::ticket-card::price',
attributes: {
'is-metadata': true,
'needed-when-embedded': true
},
relationships: {
'field-type': {
data: { type: 'cards', id: '@cardstack/core-types::string-field-card' } // because this card's adopt graph can be traced back to a string, then we know this is a string
}
}
},
{
type: 'fields'
id: '@acme/ticket-tools::ticket-card::purchase-date',
attributes: {
'is-metadata': true,
'needed-when-embedded': true
},
relationships: {
'field-type': {
data: { type: 'cards', id: '@cardstack/core-types::date-field-card' }
}
}
},
// This field is a belongs-to to another card,
{
type: 'fields'
id: '@acme/ticket-tools::ticket-card::attendee',
attributes: {
// how does its metadata get reflected on this card's metadata, what about the search doc? (are they the same actually?)
// if we flatten metadata of the user card into our ticket card, how do we prevent namespace collision of metadata field names?
// Also when you get this card in a metadata format, should we expand this relationship or not? maybe we need a new property to indicate that?
'is-metadata': true,
'needed-when-embedded': true
},
relationships: {
'field-type': {
data: { type: 'cards', id: '@cardstack/core-types::belongs-to-field-card' }
},
// The idea is that we accept any card whose metadata has the shape of the card specified below. In this
// example cardstack is providing a core user card that has metadata for a user. Cards
// can adopt from @cardstack/user::user-card to provide a user card
'related-cards': {
data: [
{ type: 'cards', id: '@cardstack/user::user-card::v1.0.0' }
]
}
}
},
{
type: 'fields'
id: '@acme/ticket-tools::ticket-card::event',
attributes: {
'is-metadata': true,
'needed-when-embedded': true
},
relationships: {
'field-type': {
data: { type: 'cards', id: '@cardstack/core-types::belongs-to-field-card' }
},
// The idea is that we accept any card whose metadata has the shape of the card specified below. In this
// example cardstack is providing a core event card that has metadata for an event. Cards
// can adopt from @cardstack/event::event-card to provide an event card
'related-cards': {
data: [
{ type: 'cards', id: '@cardstack/event::event-card::v1.0.0' }
]
}
}
},
{
type: '@acme/ticket-tools::ticket-cards', // note that we are not including the card id here
id: '@acme/ticket-tools::ticket-card:123', // note that the id of the model is the same as the id of the card
attributes: {
price: '45.00',
'purchase-date': 2019-07-20T12:30:00+04:00,
},
relationships: {
// This ticket card has a relationship to a user card. note how the relationship's name is the last component of the field's id: "attendee"
attendee: {
data: { type: 'cards', id: '@github/github-auth::github-user::habdelra' }
},
event: {
data: { type: 'cards', id: '@foobar/events-are-us::event-cards::emberconf-2020' }
}
}
},
// Here we have an embedded card (user card)
{
type: 'cards',
id: '@github/github-auth::github-user::habdelra',
// imagine that these are populated with interesting things like the ticket card...
attributes: {
'isolated-template': ...,
'embedded-template': ...,
'isolated-js': ...,
'embedded-js': ...,
'peer-dependencies': [
'@glimmer/component': '*'
],
},
relationships: {
fields: [ ... ]
}
},
// Event card resources
{
type: 'computed-fields'
id: '@foobar/events-are-us::event-cards::nearby-events',
attributes: {
'is-metadata': true,
'needed-when-embedded': true
'computed-field-type': '@acme/events-are-us::nearby-events' // note that i'm just using the package name here and not the card name...
// From within the computed-field js, I should be able to access the metadata fields that have been grafted on to this card.
// We might want to use an `await model.getAdopted('metadataFieldName')` or `await model.getMetadata('metadataFieldName')`
// in the Model API that we use in the computed.
}
},
{
type: 'fields'
id: '@foobar/events-are-us::event-cards::vegan-friendly',
attributes: {
'is-metadata': true,
'needed-when-embedded': true,
},
relationships: {
'field-type': {
data: { type: 'cards', id: '@cardstack/core-types::boolean-field-card' }
},
}
},
{
type: 'cards',
id: '@foobar/events-are-us::event-cards::emberconf-2020',
attributes: {
'suggested-name': 'event',
// no need to define templates and components if we have not changed it
// when hub instantiates this card it will traverse the "adopts" graph to get the template and components for this card
// when there is more than 1 adopted card, then you must define your card's templates and components here
// Also use similar approach as ember in terms of not having a component js, where the component is just a glimmer comnponent of the outer HTML of the template,
// in the case the card you are adopting does not have a component js
'isolated-template': `
{{yield}} {{!-- single adoption means that delegated rendered templates are just yields --}}
Vegan Friendly: {{content.veganFriendly}}
Nearby events:
{{#each content.nearbyEvents as |event|}}
<div>{{event.name}}</div>
{{/each}}
`,
// no need to define templates, components, and dependencies if we have not changed it
// when hub instantiates this card it will traverse the "adopts" graph to get the template, components, and npm dependencies for this card
},
relationships: {
fields: [
// these fields (and the template above) are what makes this a custom event--which was the rationale for adopting the core event card
{ data: { type: 'computed-fields', id: '@foobar/events-are-us::event-cards::nearby-events'}},
{ data: { type: 'fields', id: '@foobar/events-are-us::event-cards::vegan-friendly'}},
],
// When we adopt a card, its fields definitions become our fields, and it's data can become our "example" data for
// the specific fields that we are adopting. Note that you can only adopt a single card. More sophisticated composition
// should leverage card relationships.
adopts: {
// because this card adopts @cardstack/event::event-card::v1.0.0, it can be used for any relationship that that specifies
// a `related-cards` of card @cardstack/event::event-card::v1.0.0. All cards that can trace their `adopts` graph back to the card
// @cardstack/event::event-card::v1.0.0 can be used in relationships that specify this related card.
data: { type: 'cards', id: '@cardstack/event::event-card::v1.0.0' }
},
model: {
data: { type: '@foobar/events-are-us::event-cards', id: '@foobar/events-are-us::event-cards::emberconf-2020' }
}
}
},
{
type: '@foobar/events-are-us::event-cards', // note that we are not including the card id here
id: '@foobar/events-are-us::event-cards::emberconf-2020', // note that the id of the model is the same as the id of the card
attributes: {
'vegan-friendly': true,
// We can set the adopted fields just as if they were own fields
'name': 'Ember Conf 2020',
'date-time': '2020-03-21T10:00:00+07:00',
'address': 'Oregon Convention Center, 777 NE Martin Luther King Jr Blvd, Portland, OR 97232'
}
}
]
}
@habdelra
Copy link
Author

habdelra commented Jul 22, 2019

In the latest revision on 7/22/19 I have introduced the following:

  • Cards can only adopt a single card. The example of a ticket card adopting an event card was not a good fit for adoption--rather, that is better served by using card relationships as a tool to compose cards (which should absolutely be part of the card builder). I've updated this example to use adopts in an event card where we are adding a computed to the core event card that can show nearby events. Things we still need to think about: how can a card be used to populate multiple different form fields (using Chris' example)? How can a Card act like an event and some other thing simultaneously, such that a card is the union of a bunch of different metadata shapes? Maybe the answer here is to look at the card's relationships--if a ticket card has a relationship to an event via a related-cards of @cardstack/event::event-card::v1.0.0, maybe then we can use the ticket card's event to fill out a form that takes in an event card....
  • removed the idea of a model being just a javascript object living in the card. We want to be able to validate the structure of the card models and not intermingle card schema relationships and card model relationships in the same relationships property (set at the card level). Rather cards have model resources, and all the card data is represented in this model resources, including any relationships that the card instance has. This also greatly simplifies how we specify model data for fields that the card is adopting
  • clarified what it means for a relationship to have related-cards, where we accept cards that adopt from a specific card (like a core event card)
  • introduced the idea that card templates, javascript, and deps can be omitted when the card does not wish to change the template, js, or deps from the card that it adopts
  • added the version to the relationship id of the adopted card

@habdelra
Copy link
Author

habdelra commented Jul 24, 2019

In the revision made on 7/24/19 I have introduced the following:

  • Cards can "implement" interfaces. When implementing an interface a card declares the interface that it implements and then how to map its metadata fields to the implemented interface. Ultimately when we implement this logic, this mapping will be leveraged in order for a card to access another card's metadata via a declared interface. Likely when we implement this we'll need to understand who is asking for the information, as accessing the information will be the result of a relationship traversal via userland API's (like the Model API). we'll use the relationship that you are traversing in order to determine the interface in which to use to access a card.
  • For interface mappings where the card's metadata field name matches the interface's metadata field name, no explicit mapping needs to be provided. A mapping only needs to be provided when the names differ.
  • For interface mappings you can also traverse a card's relationships in order to project an interface from relationships that a card may have. For instance, a ticket card can implement an Event interface, and then project it's related event's metadata fields in the interface mappings for an event (provided the event is itself metadata on the card, as well as the event's own metadata fields--such that we never cross a public metadata boundary. An example of this could look like this, where the following is an implements relationship:
      {
        data: {
          type: 'cards',
          id: '@cardstack/core-types::event-card::0::v1.0.0', // unclear about the card ID here...
          meta: {
            // here we provide mappings from our card's metadata fields to the user interface's fields.
            // in this case the mapping is traversing the ticket card's event relationship, for which
            // the event is a metadata field, and the fields specified from the event card are the
            // event's metadata fields
            'metadata-mappings': {
              name: 'event.name',
              address: 'event.address',
              'date-time': 'event.date-time'
            }
          }
        }
      }
  • The fields that cards specify in their schema are actually in-and-of-themselves cards.
  • what about computed fields, are those cards too, even though computeds are read-only?
  • Cards can define editing-js, and editing-template which is used when the framework decides to render the card in an "editing" format. This can be used for cards that are used as fields when soliciting user input.
  • Cards can define CSS for the isolated template, embedded template, and editing templates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment