Skip to content
This repository has been archived by the owner on May 10, 2023. It is now read-only.

Core Concepts

yusufsipahi edited this page Oct 19, 2020 · 5 revisions

JSX for APL leverages React's XML-style formatting to represent the Alexa Presentation Language document in a code-first approach to make it easier to read, manage and develop.

Here are some core concepts that will help you understand what issues JSX for APL seeks to solve and how JSX for APL works ahead of using the library.

What problem does JSX for APL solve?

Alexa Presentation Language (APL) is an standard templating language for creating the Alexa device views. It is used to create an interactive visual experiences to an Alexa skill in devices such as Echo Show, Fire TV, Fire tablets and more.

In order to create an APL document, you write a JSON document containing the content of an APL view, import as a JS object and include into the skill intent response as an APL directive.

This poses the next three inconveniences:

1. Management of dynamic views becomes complicated

When using JSON to manage an APL document, each component needs to be explicitly defined in the document. This static structure creates a challenge when we are dealing with component that may or may not appear according to the logic of the skill. Current approach to this is to provide a boolean logic to "when" tag via data binding, but this can increase the complexity of the skill with every extra logic introduced. An alternative is limiting the interactivity or the number of components in the view to make development more manageable.

JSX for APL resolves this issue by integrating the APL document development seamlessly into the code, making organization, logical expression, scaling and even unit testing with modern tools easier.

Illustration with examples

Let's imagine you are developing a chess game using APL to display the chess board. Chess board contains 32 pieces with a grid based board to position against.

When using a JSON based APL document, you can model it by setting up the positions of each piece in datasource binds and mapping that to each Image or Text type component in the main template. This can make any updates to code or debugging very difficult to track down and creates excessively redundant JSON lines.

{
    "type": "APL",
    "version": "1.4",
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "item": [
            // ...
            {
                "type": "Text",
                "text": "♙",
                "padding-left": "${payload.pawn0.paddingLeft}"
                // ...
            },
            // ... 7 more white pawns, 8 block pawns etc.
        ]
    }
}

With APL for JSX, you elect to dynamically add only the necessary pieces by adding a React-style iterative component addition within the XML-style tags. You can implement a robust Object-oriented feature with ease of debugging and changes.

class ChessPiece {
    // Chess piece definition
    get topPadding() {}
    get leftPadding() {}
    get visibility() {}

    renderPiece() {} // Returns JSX for APL component to display
}

class ChessGame {
    generatePieces() {
        // Chess piece generation logic
        for (let i = 0; i < 8; i++) {
            this.pieces.push(new ChessPiece('pawn', 'white'));
        }

        // ... generate rest of pieces
    }

    getPieces() {
        // ... returns pieces left in game
    }
}

class ChessBoardApl extends React.Component {
    render() {
        return (
            <APL>
                <MainTemplate>
                    <Container>
                        <Image src="img/board.svg">
                        {
                            this.game.getPieces().map(piece => {
                                return piece.renderPiece();
                            });
                        }
                    </Container>
                </MainTemplate>
            </APL>
        );
    }
}

2. Code readability becomes difficult in complex projects

When a project grow and becomes more complex, the more and more components will be added the APL document. It is not uncommon for the JSON definition to grow up to hundreds, if not thousands, of lines long. Without the native support to import other JSON documents, JSON-based APL document users are faced with managing a long JSON document via tools like APL Authoring tool, or implementing a complicated setup to generate a componentized documents.

By leveraging the XML/HTML support from React framework, we can break down the document into custom subcomponents. These subcomponents can, depending of the project requirement, have a dynamic render process. Since these are essentially React components, it can be used with standard Javascript/React testing framework as well to include unit tests.

Let's look at an example. Suppose you want to create a document that shows a picture of an apple with a label underneath. Below examples compare the JSON implementation and the JSX for APL implementation of the visual component.

Example JSON APL document
{
    "type": "APL",
    "version": "1.4",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.0.0"
        }
    ],
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "items": [
            {
                "type": "Container",
                "height": "100vh",
                "width": "100vw",
                "items": [
                    {
                        "type": "Image",
                        "source": "${payload.path.to.background.url}",
                        "scale": "best-fill",
                        "position": "absolute",
                        "width": "100vw",
                        "height": "100vh"
                    },
                    {
                        "type": "AlexaHeader",
                        "headerTitle": "${payload.path.to.title}",
                        "headerAttributionImage": "${payload.path.to.logo}"
                    },
                    {
                        "type": "Container",
                        "direction": "row",
                        "paddingLeft": "5vw",
                        "paddingRight": "5vw",
                        "paddingBottom": "5vh",
                        "alignItems": "center",
                        "justifyContent": "center",
                        "items": [
                            {
                                "type": "Image",
                                "height": "70vh",
                                "width": "90vw",
                                "source": "${payload.path.to.fruit.url}",
                                "scale": "best-fill",
                                "align": "center"
                            },
                            {
                                "type": "Text",
                                "text": "${payload.path.to.label.text}",
                                "fontSize": "16dp",
                                "paddingTop": "12dp",
                                "paddingBottom": "12dp",
                                "height": "32dp",
                                "width": "300dp"
                            }
                        ]
                    }
                ]
            }
        ]
    }
}
// Skill Intent

import * as aplDocumentJson from './apl-view.json';

// ...

class FruitViewIntent {
    handle(handlerInput) {
        const speakOutput = 'Hello World!';
        const aplDocument = new AplDocument(<HelloWorldApl />);
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                document: aplDocumentJson,
                datasources: {
                    payload: {
                        // ... logos, url, etc.
                    }
                }
            })
            .getResponse();
    }
}
JSX for APL equivalent
// FruitView APL Document
import * as React from 'react';
import {APL, MainTemplate, Container, Text, Image} from 'ask-sdk-jsx-for-apl';
import {AlexaHeader} from 'ask-sdk-jsx-for-apl/alexa-layouts';

export default class FruitViewApl extends React.Component {
    render() {
        return (
            <Container
                direction="row"
                paddingLeft="5vw"
                paddingRight="5vw"
                paddingBottom="5vh"
                alignItems="center"
                justifyContent="center">
                <Image
                    height="70vh"
                    width="90vw"
                    scale="best-fill"
                    align="center"
                    source={this.props.fruit.url}
                />
                <Text
                    fontSize="16dp"
                    paddingTop="12dp"
                    paddingBottom="12dp"
                    height="32dp"
                    width="300dp"
                    text={this.props.fruit.label}
                />
            </Container>
        );
    }
}
// Main APL Document
import * as React from 'react';
import {APL, MainTemplate, Container, Text, Image} from 'ask-sdk-jsx-for-apl';
import {AlexaHeader} from 'ask-sdk-jsx-for-apl/alexa-layouts';

import FruitViewApl from './FruitViewApl';

export default class ShowFruitIntentApl extends React.Component {
    render() {
        const bananas = {
            url: '<url to banana image>',
            label: 'Delicious Bananas!'
        };
        const backgroundImgUrl = '<url to background image>';
        const logoUrl = '<url to logo image>';

        return (
            <APL>
                <MainTemplate>
                    <Image
                        source={backgroundImgUrl}
                        scale="best-fill"
                        position="absolute"
                        width="100vw"
                        height="100vh"
                    />
                    <AlexaHeader
                        headerTitle={bananas.label}
                        headerAttributionImage={logoUrl}
                    />
                    <FruitViewApl fruit={bananas} />
                </MainTemplate>
            </APL>
        );
    }
}
// Response Builder
import { AplDocument } from 'ask-sdk-jsx-for-apl';

import ShowFruitIntentApl from './ShowFruitIntentApl';

class FruitViewIntent {
    handle(handlerInput) {
        const speakOutput = 'Hello World!';
        const aplDocument = new AplDocument(<ShowFruitIntentApl />);
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .addDirective(aplDocument.getDirective())
            .getResponse();
    }
}

3. APL document can grow bigger due to product requirements

Currently, Alexa's response size needs to be less than 24KB. Depending on the implementation, this can be a barrier to creating a more dynamic, creative skills. Some common product requirements, such as ambitious/detailed APL views, complex vector graphics or multipl device type support, can gradually grow the response size as developments continue and one day require a difficult refactor to inclue more specifications. Developing a minimal artifact is crucial in APL development.

By usign reusable custom subcomponents, you can make minimal copies of the source to support divergent behaviours in the skill's view. In JSON-based APL authoring, support for multiple viewports handled either by combining into one APL document via "when" tag or maintaining two different JSON documents and selecting which one to serve. With JSX for APL, however, you can instead create a two different components sharing common subcomponents, minizing the number of source files and logic to maintain and test.

How does JSX for APL work?

In its very core, JSX for APL is a JSON generator for APL document modeled as React components. Let's take this example code and dissect it.

// APL Document Subcomponent
import * as React from 'react';
import {Container, Image, Text} from 'ask-sdk-jsx-for-apl';

class MyAplColumn extends React.Component {
    render() {
        return (
            <Container
                width="33%"
                height="100%">
                {this.props.children}
            </Container>
        );
    }
}
// APL Document Component
import * as React from 'react';
import {APL, MainTemplate, Container, Text, Image } from 'ask-sdk-jsx-for-apl';

class MyAplDocument extends React.Component {
    constructor(props) {
        super(props);
        this.fruits = [
            {
                name: 'Apple',
                imgUrl: '<image url>',
                description: 'Red and round'
            },
            {
                name: 'Banana',
                imgUrl: '<image url>',
                description: 'Yellow and long'
            },
            {
                name: 'Kiwi',
                imgUrl: '<image url>',
                description: 'Brown and green and oblong'
            }
        ];
    }

    render() {
        return (
            <APL theme="dark">
                <MainTemplate>
                    <Container width="100%" height="80vh" direction="row">
                        {
                            this.fruits.map(fruit => {
                                return (
                                    <MyAplColumn>
                                        {renderFruitContent(fruit)}
                                    </MyAplColumn>
                                );
                            });
                        }
                    </Container>
                </MainTemplate>
            </APL>
        );
    }

    renderFruitContent(fruit) {
        return (
            <>
                <Text text={ fruit.name } />
                <Image source={ fruit.imgUrl } />
                <Text text={ fruit.description } />
            </>
        );
    }

}
// ASK SDK Intent Handler
import { AplDocument } from 'ask-sdk-jsx-for-apl';

import MyAplDocument from './MyAplDocument';

class FruitViewIntent {
    handle(handlerInput) {
        const speakOutput = 'My first APL document!';
        const aplDocument = new AplDocument(<MyAplDocument />);
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .addDirective(aplDocument.getDirective())
            .getResponse();
    }
}

0. JSON vs JSX

Before we dive into the structure of the JSON, we need to review how each components in JSX for APL match to JSON.

JSON definition of APL components specify the type by adding a type key to the JSON object. The all other keys in the object serves as parameters to that component, specifying how the component should be displayed. items key is used to list which subcomponents should be displayed inside the component.

JSX for APL matches the structure by naming the component type as the XML tag name, items as its children and all other as parameters of the XML tag.

Check out the example below of the comparison of the equivalent components.

JSON

{
    "type": "container",
    "width": "50vw",
    "paddingLeft": "25vw",
    "items": [
        {
            "type": "text",
            "text": "Hello!"
        }
    ]
}

JSX

<Container width="50vw" paddingLeft="25vw">
    <Text text="Hello!">
</Container>

You can see which components are available and what parameters they accept by checking the Alexa Presentation Language documentation.

1. Document Structure

// Main APL Document
<APL theme="dark">
    <MainTemplate>
        ...
    </MainTemplate>
</APL>

All JSX for APL documents start at APL root component. Within the APL tag, we add all templates relavant to show on the viewport within the MainTemplate tag.

2. React Component Lifecycle

constructor(props) {
    super(props);
    this.fruits = [
        // ...
    ];
}
renderFruitContent(fruit) {
    return (
        <>
            <Text text={ fruit.name } />
            <Image source={ fruit.imgUrl } />
            <Text text={ fruit.description } />
        </>
    );
}
// within render()
<Container width="100%" height="80vh" direction="row">
    {
        this.fruits.map(fruit => {
            return (
                <MyAplColumn>
                    { renderFruitContent(fruit) }
                </MyAplColumn>
            );
        });
    }
</Container>

As a React component, you can utilize the React Component Lifecycle to handle data and state processing in each component. In the example, the fruits data has been loaded in the constructor and is then utilized in the render function to generate the content via renderFruitContent function.

3. Using Subcomponents, Props and Children

// Subcomponent
class MyAplColumn extends React.Component {
    render() {
        return (
            <Container
                width="33%"
                height="100%">
                {this.props.children}
            </Container>
        );
    }
}
// Main Document: render()
this.fruits.map(fruit => {
    return (
        <MyAplColumn>
            {renderFruitContent(fruit)}
        </MyAplColumn>
    );
});

JSX format supports JS code processing within the markup definition. This allows us to make generalized components that support different child components. In the example, we define a general purpose column component MyAplColumn and use it to display three different columns with components with different content inside each of them.

4. Including the JSX APL document into the Skill Response

import { AplDocument } from 'ask-sdk-jsx-for-apl';

import MyAplDocument from './MyAplDocument';

// ...

const speakOutput = 'My first APL document!';
const aplDocument = new AplDocument(<MyAplDocument />);
return handlerInput.responseBuilder
    .speak(speakOutput)
    .addDirective(aplDocument.getDirective())
    .getResponse();

To include the document, we wrap the main component in AplDocument class and call .getDirective() to get the directive object. Simple as that!

Getting Started

To find out more on how to set up a skill and start using JSX for APL, check out the Getting Started guide.