microsoft / microsoft/AdaptiveCards

Adaptive Components

Open
#4,761 6 comments 3 reactions 1 assignee View on GitHub

@rahulamlekar is already working on this.

Since Jun 11, 2021.

Epic Feature
Dominant language
C#
Stars
2k
Forks
595
Avg merge
1d 19h
Merged PRs (30d)
1

Description

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

  • Need to consider versioning the components, which use different data types or significantly change the view
  • Need to consider how namespacing and component names should be done
  • What about arrays of objects, e.g., multiple Files and multiple Persona's?
  • Fetching Authenticated images
  • Can the view definition start with an array instead of just an object? Would make converting from body to a view easier
  • Fetching remote potentially authenticated data via an identifier (such as URL or UPN)
  • concerns about security with remotely downloadable components
  • look into custom html elements in BF cards and if this could be handled with components
  • fallback implementation could get complicated
  • latency considerations, especially when components rely on other components
  • scrolling in teams will be negatively impacted by async component loading
  • look into offline registries such as npm packages
  • We need guidance or a policy on what should be modified by Components in other registries. E.g., if a custom registry changes the schema.org/thing component, it can add/remove/change views, but should it be able to change the schema? I'm leaning towards no, but should we make that a policy or just strong guidance?
    • What good is a component if there is no guarantee that it will work across hosts, even when the host implements its own version?
    • Isn't the whole point of components to offer a way for card authors to be able to use an always growing collections of, well, components that are guaranteed to work across all hosts?
    • A component's schema should be contractual, that is core to ensuring that it'll work everywhere
    • And as a result, if the schema is contractual, why aren't views?

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:

  • Name
  • Description
  • Image
  • Url
  • Actions
  • (Sometimes) Category

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.

  • hero
  • thumbnail

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": "...." 
    }
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.