Skip to content

Writing DSL Components

In order to take advantage of the auto-completion and validation of TypeScript types, asset libraries can export a component library for content authoring. Creating components isn’t much different than writing a React component for the web. The primative elements uses the react-json-reconciler to create the JSON content tree, with utilities to make it quick and painless to create new asset-components.

Creating a Basic Component

The Asset component from the @player-tools/dsl package is the quickest way to create a new component. The Asset component will take all the Asset’s properties and convert them to their equivalent JSON representation when serialized.

In the examples below, we’ll be creating a TSX component for the action asset in our reference set.

The action asset has a label slot (which is typically used as a text asset), a value (for flow transitions), and an exp for evaluating expressions. For this example we’ll use a resemblance of this type, but in practice types should be imported directly from their asset rather than duplicating them.

import type { Asset, AssetWrapper, Expression } from "@player-ui/player";
export interface ActionAsset<AnyTextAsset extends Asset = Asset>
extends Asset<"action"> {
/** The transition value of the action in the state machine */
value?: string;
/** A text-like asset for the action's label */
label?: AssetWrapper<AnyTextAsset>;
/** An optional expression to execute before transitioning */
exp?: Expression;
}

Note: The Asset type we’re importing here from the @player-ui/player package is different than the Asset component from the @player-tools/dsl package. The former is the basic TypeScript definition for what an Asset in Player is while the latter is a helper function for allowing DSL components to be created. Fundamentally they share a name to reinforce the abstraction of foundational capabilities to core libraries

To turn this interface into a usable component, create a new React component that renders an Asset:

import { Asset, AssetPropsWithChildren } from "@player-tools/dsl";
export const Action = (props: AssetPropsWithChildren<ActionAsset>) => {
return <Asset type="action" {...props} />;
};

This would allow users to import the Action component, and render it to JSON:

const myView = <Action value="next" />;

which when compiled would look like

{
"id": "root",
"type": "action",
"value": "next"
}

The AssetPropsWithChildren type is a utility type to help convert the Asset type (which has a required id and type properties) to a type more suited for components. It changes the id to be optional, and adds a applicability property automatically.

Slots

Continuing the example fo the ActionAsset, we need a way for users to users to specify the nested label property, which itself is another asset. This can be accomplished using the createSlot utility function. The createSlot function also accept components to enable automatically creating text and collection assets when they aren’t specified where needed. If these components aren’t passed into the slot when used, the resulting content may be invalid. Let’s add a Label slot to our Action component to allow it to be easily authored. Lets assume we already have a Text and Collection component.

import React from 'react';
import { Asset, AssetPropsWithChildren, createSlot } from '@player-tools/dsl';
export const Action = (props: AssetPropsWithChildren<ActionAsset>) => {
return <Asset type="action" {...props} />;
}
Action.Label = createSlot({
name: 'label',
wrapInAsset: true,
TextComp: SomeTextComponent
CollectionComp: SomeCollectionComponent
})

This adds component (Action.Label) that will automatically place any nested children under the label property of the parent asset:

const myView = (
<Action value="next">
<Action.Label>
<Text value="Continue" />
</Action.Label>
</Action>
);

which can also be written as the following because the slot has a TextComp passed in for it which allows the automatic creation of a text asset for any regular text that is passed in the slot

import React from "react";
const myView = (
<Action value="next">
<Action.Label>Continue</Action.Label>
</Action>
);

When compiled, both examples would look like (note the auto injection of the Text asset and corresponding Asset Wrapper):

{
"id": "root",
"type": "action",
"value": "next",
"label": {
"asset": {
"id": "root-label-text",
"type": "text",
"value": "Continue"
}
}
}

And if we wanted to have the label property to have to text assets we could write the following DSL

const myView = (
<Action value="next">
<Action.Label>
<Text>Some</Text>
<Text>Text</Text>
</Action.Label>
</Action>
);

which when compiled would look like the following (note the automatic insertion of the Collection Asset):

{
"id": "root",
"type": "action",
"value": "next",
"label": {
"asset": {
"id": "root-collection",
"type": "text",
"values": [
{
"asset": {
"id": "root-collection-1-text",
"type": "text",
"value": "Some"
}
},
{
"asset": {
"id": "root-collection-2-text",
"type": "text",
"value": "Text"
}
}
]
}
}
}

Creating a Complex Component

While a majority of Assets can be described simply via the base Action Component, there are certain cases where DSL components need to contain a bit more logic. This section aims to describe further tools that are offered in the @player-tools/dsl package.

Components with Specially Handled Properties

In the previous example, we covered how to create a DSL Component for our reference Action Asset. Our actual Action Asset however looks a little bit different.

import React from "react";
export const Action = (
props: Omit<AssetPropsWithChildren<ActionAsset>, "exp"> & {
/** An optional expression to execute before transitioning */
exp?: ExpressionTemplateInstance;
},
) => {
const { exp, children, ...rest } = props;
return (
<Asset type="action" {...rest}>
<property name="exp">{exp?.toValue()}</property>
{children}
</Asset>
);
};

Crucially, the difference is in how the exp property is handled. As the exp property is an Expression, if we just allowed the Action component to process this property, we would end up with an ExpressionTemplate instance not an Expression instance. While technically they are equivalent, there is no need to wrap the final string in the Expression Template tags (@[]@) since we know the string will be an Expression and it will just lead to additonal procssing at runtime. Therefore, we need to do a few things to properly construct this DSL component.

The first is to modify the type for the commponent. In the above code snippit we are using the Omit type to remove the base exp property from the source type and replacing it with an exp property that expects a ExpressionTemplateInstance which allows an DSL expression to be passed in.

The second is to extract out the exp property from the props and use a property component to manually control how that property will get serialized. This component is exposed by the underlying react-json-reconciler library which also supplies an array, obj and value component to allow full control over more complicated data structures. The @player-tools/dsl package also exposes the toJsonProperties function to process whole non-Asset objects.

View Components

For Assets that are intended to be Views, a View component is exported from the @player-tools/dsl package. Its usage is exactly the same as the Asset component, however it correctly handles the serialization of any Crossfield Validations that exist on the View.