-
Notifications
You must be signed in to change notification settings - Fork 7
Core Concepts
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.
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:
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>
);
}
}
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();
}
}
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.
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();
}
}
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.
// 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.
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.
// 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.
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!
To find out more on how to set up a skill and start using JSX for APL, check out the Getting Started guide.