microsoft / AdaptiveCards

A new way for developers to exchange card content in a common and consistent way.
https://adaptivecards.io
MIT License
1.76k stars 550 forks source link

Adaptive Components #4761

Open productboard-ac opened 4 years ago

productboard-ac commented 4 years ago

Adaptive Components

Enabling the creation of purely declarative high-level components powered by templating and native Adaptive Card elements. You can think of them like “molecules”, built by arranging Adaptive Card elements (“atoms”) into unique and helpful ways.


Status: Draft Approved

9/25/2020: Draft approved per the changes made in the comment below from 9/25. Root spec to be updated shortly. 9/9/2020: Initial draft proposed.


Try it yourself

Interested in trying it out? You're in luck! We have an early prototype to play with, note that it is pre-alpha and will be buggy!

Screenshot of a Stock component in the Card Designer

  1. Load up the Designer from the above URL.
  2. Drag over a Component from Card Elements toolbox on the left.
  3. On the designer surface, select the Component, and click Choose...
  4. A dialog should open with a list of available components, this might take ~30 seconds or so due to the service starting up.
  5. Click on the iextrading.com/stockQuote component.
  6. Change the Element Properties and observe the component UI updating. E.g., Set Change value to -2.69
  7. Play around with it by mix-and-matching different components that are currently available.

Objectives

  1. Allow anyone to define, use, and share higher-level "component" abstractions (with the general public or within their organization)
  2. Tooling integration: Component authoring should be a first-class experience in the Designer.
  3. Composable: Components can be used anywhere inside the card body, and chained together when necessary.
  4. Responsive: Components should be fully responsive and support different viewports as appropriate.
  5. Local and Remote Component registries: Host Apps can register local components, but they can also enable support for remote registies to "learn" new components dynamically, without requiring client-side app updates.
  6. [P2] Actionable: Components should be able to provide actions that work on Hosts that have enabled "Universal Actions" (Lots of details to work out here)

Open issues

Examples

Example: "File Chicklet" Component

The file chicklet is a way to represent basic file metadata somewhere inside the card.

image

Component Usage:

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "Component",
            "name": "graph.microsoft.com/file",
            "properties": {
               "name": "FY2020-Contoso.docx",
               "fileType": "docx",
               "webUrl": "https://adaptivecardsblob.blob.core.windows.net/assets/AdaptiveCardsSpec.docx"
            }
        }
    ]
}

Component Definition: Component.File.json

{
    "type": "AdaptiveComponent",
    "componentName": "graph.microsoft.com/file",
    "schema": {
        "properties": {
            "name": {
                "type": "string"
            },
            "fileType": {
                "type": "string"
            },           
            "webUrl": {
                "type": "string"
            }
        },
        "required": [ "title", "fileType" ]
    },
    "views": {
        "default": {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "35px",
                    "items": [
                        {
                            "type": "Image",
                            "$when": "${fileType == 'docx'}",
                            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/Microsoft_Word_2013_logo.svg/782px-Microsoft_Word_2013_logo.svg.png"
                        },
                        {
                            "type": "Image",
                            "$when": "${fileType == 'xlsx'}",
                            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/Microsoft_Office_Excel_%282018%E2%80%93present%29.svg/1101px-Microsoft_Office_Excel_%282018%E2%80%93present%29.svg.png"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "${name}",
                            "weight": "bolder"
                        },
                        {
                            "type": "TextBlock",
                            "text": "${webUrl}",
                            "isSubtle": true,
                            "spacing": "none"
                        }
                    ]
                }
            ]
        }
    }
}

Example: "Thing" Component

image

"Things" are all over the web today, and represent the basic metadata for objects. They are broadly categorized by these pieces of metadata:

Component Usage:

{
    "type": "AdaptiveCard",
    "body": [ 
        {
            "type": "Component",
            "name": "schema.org/thing",
            "properties": {
               "name": "Golden Gate Bridge",
               "description": "Suspension bridge in San Francisco, California",
               "image": "..."
           }
        }        
    ]
}

Component Definition: Component.Thing.json

{
    "type": "AdaptiveComponent",
    "componentName": "schema.org/thing",
    "schema": {
        "properties": {
            "name": {
                "type": "string"
            },
            "description": {
                "type": "string"
            },
            "image": {
                "type": "uri-template"
            }
        },
        "required": [ "name" ]
    },
    "views": {
        "default": {
            "type": "Container",
            "items": [
                {
                    "type": "Image",
                    "url": "${image}",
                    "size": "Medium"
                },
                {
                    "type": "TextBlock",
                    "text": "${name}",
                    "size": "Medium"
                },
                {
                    "type": "TextBlock",
                    "text": "${description}",
                    "size": "Default"
                }
            ]
        }
    }
}

Example: User component

{
    "type": "Component",
    "name": "graph.microsoft.com/user",
    "properties": {
        "displayName": "Megan Bowen",
        "givenName": "Megan",
        "jobTitle": "Auditor",
        "mail": "MeganB@M365x214355.onmicrosoft.com",
        "mobilePhone": null,
        "officeLocation": "12/1110",
        "preferredLanguage": "en-US",
        "surname": "Bowen",
        "userPrincipalName": "MeganB@M365x214355.onmicrosoft.com",
        "id": "48d31887-5fad-4d73-a9f5-3c356e68a038"
    }
}

Example: Composition

Components can be used alongside any native Adaptive Card elements, or other Components.

{
    "type": "AdaptiveCard",
    "body": [{
            "type": "TextBlock",
            "text": "Here are 3 famous bridges"
    }, {
        "type": "ColumnSet",
        "columns": [{
            "type": "Component",
            "name": "schema.org/thing",
            "properties": {
               "name": "Golden Gate Bridge",
               "description": "Suspension Bridge in California",
               "image": "..."
           }
        }, {
            "type": "Component",
            "name": "schema.org/thing",
            "properties": {
               "name": "Brooklyn Bridge",
               "description": "Cable-stayed bridge in New York",
               "image": "..." 
           }
        }, {
            "type": "Component",
            "name": "schema.org/thing",
            "properties": {
               "name": "London Bridge",
               "description": "Box girder bridge in London",
               "image": "..."
           }
        }
    ]}
]}

Requirements

  1. All renderers will support local and remote component lookups (configurable by the Host). Details below.
  2. Components must be able to bind to Host-specific properties, like viewport dimensions (e.g. "$when": "${$host.width > 300}")

Renderer behavior and component registries

The renderer will loop through all elements in the body as normal. When it encounters a type of Component then a component lookup occurs.

  1. First check the local component registry, provided by the Host via Host Config or platform API.
  2. If not found there, check the remote component registry. Remote registries are also configurable by the Host, and can be disabled if required.

Example remote registry lookup

HTTP GET https://api.adaptivecards.io/components/<COMPONENT-NAME>.json

(NOTE: Hosts can configure this via host config)

Based on the schema.org/thing component above, the renderer would request the component at the following URL:

HTTP GET https://api.adaptivecards.io/components/schema.org/thing.json

{
    "type": "AdaptiveComponent",
    "name": "schema.org/thing",
    "views": {
        "default": {
            "type": "Container",
            "items": [{
                    "type": "Image",
                    "url": "${image}"
                }, {
                    "type": "TextBlock",
                    "text": "${name}",
                    "size": "large"
                }, {
                    "type": "TextBlock",
                    "text": "${description}"
                }
            ]
        }
    }
}

Multiple "views" for Components

In many cases, a single layout many not suffice for a given component. For example, if a Component contains an image, the desired UI may place the image above the other fields, or it may want a small thumbnail of the image to the left of the other content, or perhaps as the background image behind the content.

This becomes increasingly important in a world where a single payload roams between hosts with vastly different UI constraints or characteristics.

Imagine if the schema.org/thing component wanted to provide 3 different "layouts" or "views":

image

The ultimate UI that gets rendered would be determined by multiple factors:

  1. Implicitly
    1. Based on renderer version of the client (e.g., a 1.0 "Thing" view, or a 2.0 "Thing" view)
    2. Based on display constraints (e.g., a narrow screen vs wide screen)
    3. Others?
  2. Explicitly
    1. By name (e.g., the card author chooses a particular layout that works best for them)

Card Author explicitly selecting a particular view

If we allow authors to pick a particular view for a given component, we need a standardized set of properties on the component definitions.

Property Type Required Description
type "Component" Yes Type must be Component
name string Yes The name of the Component to load from the local or remote component registry. The recommended format is [DOMAIN]/[COMPONENT-NAME], e.g., graph.microsoft.com/file
view string[] No The desired view for the component, first match wins.
properties object No Passed directly to the component template

If we apply this to our schema.org/thing, it becomes:

{
    "type": "AdaptiveCard",
    "body": [ 
        {
            "type": "Component",
            "name": "schema.org/thing",
            "properties": {
               "name": "Golden Gate Bridge",
               "description": "Suspension bridge in San Francisco, California",
               "image": "...",
            },
            "view": [ "hero", "thumnbail" ]
        }
    ]
}

In this example, the card author is asking for a view in a specific order, where the first match will be used.

Why would a match not be found?

Great question! The reason for the array is because we will allow hosts to modify the remote component registries, which would let them define a custom version of a well-known component, or replacing the entire registry and all of its Components. If they do choose to modify a well-known component (like schema.org/thing), they may want to modify, add, or even remove support for particular views.

Example: "Thing" component with multiple views

Let's apply this concept to our schema.org/thing component.

image

HTTP GET https://api.adaptivecards.io/components/schema.org/thing.json

{
    "type": "AdaptiveComponent",
    "name": "schema.org/thing",
    "schema": ...,
    "views": {
        "default": { 
            "type": "Container",
            "items": [{
                    "type": "Image",
                    "url": "${image}"
                }, {
                    "type": "TextBlock",
                    "text": "${name}",
                    "size": "large"
                }, {
                    "type": "TextBlock",
                    "text": "${description}"
                }
            ]
        },
        "thumbnail": {
            "type": "ColumnSet",
            "columns": [{
                    "type": "Column",
                    "items": [{
                            "type": "Image",
                            "url": "${image}"
                        }
                    ]
                }, {
                    "type": "Column",
                    "items": [{
                            "type": "TextBlock",
                            "text": "${name}"
                        },
                        {
                            "type": "TextBlock",
                            "text": "${description}"
                        }
                    ]
                }
            ]   
        },
        "stack": { 
            "type": "Container",
            "backgroundImage": "${image}",
            "items": [{
                    "type": "TextBlock",
                    "text": "${name}",
                    "size": "large"
                }, {
                    "type": "TextBlock",
                    "text": "${description}"
                }
            ]
        }
    }
}

Supporting multiple renderer versions in the same component

As we release new versions of Adaptive Card schema, it's important that components are compatible with multiple versions.

For example, maybe the "Thing" template wants to use new 1.6 elements if the client supports them, but still has a great experience for 1.3, or even 1.0.

Thankfully, we get this support for free, thanks to the existing fallback model in 1.2! 👍

{
    "type": "AdaptiveComponent", 
    "views": {
        "default": { 
            "type": "Container",
            "requires": {
                "version": "1.6",
            },
            "items": [ /* 3.0 elements */ ],
            "fallback": {
                "type": "Container",
                "items": [ /* fallback elements */ ]
            }
        },
        "thumbnail": {
            ...
        }
    }
}

Designer Integration

image

We should make working with components and component registries a first-class experience.

  1. Authoring a component that includes multiple views over the data
  2. Connecting to a component registry to browse, edit, and even save changes back to the registry
  3. Seamless experience to author a card that uses remote component registries, selects a particular view, and simulates the ultimate rendered UI based on a given host (which may have their own registries and a particular renderer version)

Components can declare their expected schema and sample data

Components have a first-class way to describe the schema of the data/properties it expects, which enables the Designer to generate the Property Sheet automatically. This is also useful for tooling more advanced component resolution, and even validation.

Thing example

The schema.org/thing component expects data as follows:

{
    "name": "Golden Gate Bridge",
    "description": "San Francisco, CA",
    "image": "..."
}

Thing declaration:

{
    "type": "AdaptiveComponent", 
    "name": "schema.org/thing",
    "schema": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "description": "The most important piece of information"
            },
            "description": {
                "type": "string",
                "description": "The second most important piece of information"
            },
            "image": {
                "type": "string",
                "format": "uri",
                "description": "Image associated wih the information"
            }
        }
    },
    "views": {
        ...
    }
}

Appendix

Other component-lookup approaches

There are other ways we can explore Component-resolution.

For example, schema.org has a clear hierachy for their objects. "Thing" is the base type of everything, but maybe instead of a Thing I have a LocalBusiness, which includes all the properties of Thing and some addition ones.

If a Restaurant component doesn't exist, could we make a best-effort to find a base type the hierarchy until a match is found, such as LocalBusiness:

{
    "type": "Component",
    "name": "schema.org/restaurant",
    "properties": {
       "name": "Malt and Vine",
       "description": "A fine place for beer",
       "address": "....",
       "openingHours": "...." 
    }
}
productboard-ac commented 4 years ago

Link to feature: https://adaptivecards.productboard.com/feature-board/planning/features/5478079

ghost commented 4 years ago

ProductBoard auto-generated feature.

matthidinger commented 4 years ago

Discussion 9/25/2020

  1. Component lookup conventions
    1. Should we encapsulate the component name in the type property or a separate fully-qualified a name property?
      1. DECISION: type will be Component only, use the name property as a fully-qualify the identifier of a component. See examples below for more.
    2. For scoping should we use (1) "namespaces" (like odata); (2) "url" (like schema.org); or (3) a bit of both
      1. DECISION: The name property will be a string that may optionally start with a domain and/or path qualifier. See examples below for more.
    3. Should we provide a mechanism for a default namespace at the card level? This is really only useful to reduce redundancy in cards with many components.
      1. DECISION: Not right now, can always revisit later.
    4. Should we provide automatic lookup based on a data type or data shape?
      1. DECISION: leave aside for now but seems like a natural evolution later
  2. Where should the component data/properties go? As "addtional properties" directly on the Component object or in a specific attribute called properties
    1. DECISION: Components will have multiple first-class properties to make this concept work, such as name, view preference, potentially version, and other pieces of metadata that are required to make the Component model work. To remove risk of conflict we will put all domain-specific properties for a given component into a properties attribute, which will get passed along to the component to be used for binding. See examples below for more.
  3. Versioning consideration
    1. DECISION: We will likely add a version segment to the name, similar to many REST APIs. E.g., "name": "graph.microsoft.com/file/v2"
  4. What should our guidance be for vastly different UX for given data type? E.g., there are a bunch of different ways to visualize a "flight". E.g., maybe I want the "Boarding Pass" component, or maybe I want to "Flight Status" component, or even a "Check in Reminder". Should we suggest those to be separate components or different views in the same component?
    1. DECISION: Based on what we know now we will recommend making those separate components.
  5. What about arrays of data? E.g., should there be a component for a single User and another component to handle multiple Users?
    1. DECISION: As of now we are going to recommend different components for a single object vs an array of obecjts.

Next steps

Updated examples based on the decisions above

Some inspiration for the component name-as-path can be found from the current "Template Service Registry" which has existed for a little while now. It allows organizations to eventually "own" and have editorial control over components within their domain.

See the current template registry as an example: https://github.com/microsoft/adaptivecards-templates/tree/master/templates

Schema.Org components

{
    "type": "Component",
    "name": "schema.org/Thing",
    "properties": {
        "name": "...",
        "description": "..."
    }
}
{
    "type": "Component",
    "name": "schema.org/Book",
    "properties": {
        "author": "...",
        "title": "..."
    }
}
{
    "type": "Component",
    "name": "schema.org/Book/v2",
    "properties": {
        "author": "...",
        "title": "...",
        "somev2Property": "..."
    }
}
{
    "type": "Component",
    "name": "schema.org/LocalBusiness",
    "properties": {
        "name": "...",
        "description": "...",
        "hoursOfOperation": "..."
    }
}
{
    "type": "Component",
    "name": "schema.org/boardingPass",
    "view": [ "hero", "compact" ], 
    "properties": {
        "name": "..."
    }
}

Microsoft Graph components

File chiclet

Component based on the Graph file/driveItem

{
    "type": "Component",
    "name": "graph.microsoft.com/file",
    "properties":  {
        "createdDateTime": "2015-08-17T21:12:15Z",
        "eTag": "\"{BE9DD131-A7DC-4E38-94CE-9030F0F69DBA},1\"",
        "id": "01IYGFKCBR2GO35XFHHBHJJTUQGDYPNHN2",
        "lastModifiedDateTime": "2015-08-17T21:12:15Z",
        "name": "3604904",
        "webUrl": "https://microsoft-my.sharepoint-df.com/personal/mahiding_microsoft_com/Documents/3604904",
        "cTag": "\"c:{BE9DD131-A7DC-4E38-94CE-9030F0F69DBA},0\"",
        "size": 278612999,
        "createdBy": {
            "user": {
                "email": "Matt.Hidinger@microsoft.com",
                "id": "f6bbdfd5-6895-46c3-a549-0bafea69e07e",
                "displayName": "Matt Hidinger"
            }
        },
        "lastModifiedBy": {
            "user": {
                "email": "Matt.Hidinger@microsoft.com",
                "id": "f6bbdfd5-6895-46c3-a549-0bafea69e07e",
                "displayName": "Matt Hidinger"
            }
        },
        "parentReference": {
            "driveId": "b!-qa0V7NugEm0-AQZERKrUXb5GGSEcANIj95SK8RTLh-1QdYdMdUNTal44w5iVLEb",
            "driveType": "business",
            "id": "01IYGFKCF6Y2GOVW7725BZO354PWSELRRZ",
            "path": "/drive/root:"
        },
        "fileSystemInfo": {
            "createdDateTime": "2015-08-17T21:12:15Z",
            "lastModifiedDateTime": "2015-08-17T21:12:15Z"
        },
        "folder": {
            "childCount": 3
        }
    }
}

Single user

Component based on the Graph User

{
    "type": "Component",
    "name": "graph.microsoft.com/user",
    "properties": {
        "displayName": "Megan Bowen",
        "givenName": "Megan",
        "jobTitle": "Auditor",
        "mail": "MeganB@M365x214355.onmicrosoft.com",
        "mobilePhone": null,
        "officeLocation": "12/1110",
        "preferredLanguage": "en-US",
        "surname": "Bowen",
        "userPrincipalName": "MeganB@M365x214355.onmicrosoft.com",
        "id": "48d31887-5fad-4d73-a9f5-3c356e68a038"
    }
}

Multiple users

{
    "type": "Component",
    "name": "graph.microsoft.com/users",
    "view": "compact",
    "properties": {
        "users": [
            {
                "userPrincipalName": "MeganB@M365x214355.onmicrosoft.com",
            },
            {
                "userPrincipalName": "User2@M365x214355.onmicrosoft.com",
            }
        ]
    }
}

Full profile component that needs data from multiple endpoints

{
    "type": "Component",
    "name": "graph.microsoft.com/fullProfile",
    "properties": {
        "user": {
            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
            "businessPhones": [
                "+1 412 555 0109"
            ],
            "displayName": "Megan Bowen",
            "givenName": "Megan",
            "jobTitle": "Auditor",
            "mail": "MeganB@M365x214355.onmicrosoft.com",
            "mobilePhone": null,
            "officeLocation": "12/1110",
            "preferredLanguage": "en-US",
            "surname": "Bowen",
            "userPrincipalName": "MeganB@M365x214355.onmicrosoft.com",
            "id": "48d31887-5fad-4d73-a9f5-3c356e68a038"
        },
        "reports": {
            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#directoryObjects",
            "value": [
                {
                    "@odata.type": "#microsoft.graph.user",
                    "id": "ec63c778-24e1-4240-bea3-d12a167d5232",
                    "businessPhones": [
                        "+20 255501070"
                    ],
                    "displayName": "Pradeep Gupta",
                    "givenName": "Pradeep",
                    "jobTitle": "Accountant II",
                    "mail": "PradeepG@M365x214355.onmicrosoft.com",
                    "mobilePhone": null,
                    "officeLocation": "98/2202",
                    "preferredLanguage": "en-US",
                    "surname": "Gupta",
                    "userPrincipalName": "PradeepG@M365x214355.onmicrosoft.com"
                }
            ]
        }
    }
}
matthidinger commented 3 years ago

Test

weyCC81 commented 1 year ago

Is here the integration of Loop Components (Teams Loop Components) to Adaptive Cards meant?

ahmedrafeq commented 3 months ago

Hello, We are trying to leverage the Multiple Users component outlined above. When posting the adaptive card to Teams group chat, none of the profiles are shown (see screenshot below). When adjusting the adaptive Card JSON to list one person, the profile is rendered correctly in Teams. Are there any limitations on using the multi-Users (persona Set) in Teams adaptive card version 1.4? I am using the JSON below. If I remove the ID or display name, the adaptive card never gets posted to Teams. Power Automates generates an error regarding inability to parse JSON.

{ "type": "Component", "name": "graph.microsoft.com/users", "view": "compact", "properties": { "users": [ { "id": "user1AADObjectID",
"userPrincipalName": "user1UPN", "DisplayName": "user1DisplayName" }, { "id": "user2AADObjectID",
"userPrincipalName": "user2UPN", "DisplayName": "user2DisplayName" } ] } }

Adaptive_Card_RendersOnePersonWhenMultipleAreAdded_07312024