Skip to content

Custom Assets

One of the conscious design decisions we made when building Player was to abstract away the actual asset implementation and open it up for users to bring their own when using Player. This way you can seamlessly integrate Player into your existing experiences and reuse UI assets you may have already built. Below we’ve outlined the way to build custom assets on the various platforms Player supports.

Create Your Asset

First and foremost you need to create a component to handle rendering of your asset. Without any form of transforms, the props to the component will be those from the incoming player content. It’s recommended that you attach the id, and any other html properties to the root of the asset’s tree:

const CustomAssetComp = (props) => {
return (
<div id={props.id} style={{ color: "purple" }}>
{props.text}
</div>
);
};

Assuming your authored JSON has a string property named text, this will render that.

Register it Using a Plugin

Now that we have a React component to render our asset, let’s create a plugin to register with Player:

import AssetProviderPlugin from "@player-ui/asset-provider-plugin-react";
class CustomAssetPlugin implements ReactPlayerPlugin{
applyReact(reactPlayer) {
new AssetProviderPlugin([['custom', CustomAssetComp]]).applyReact(reactPlayer);
}
}

Typically you register assets by type, but the registry acts by finding the most specific partial object match. This allows you to register more specific implementations for assets of the same type.

Rendering Nested Assets

Often times, assets contain a reference or slot to another asset. For this to function properly, the custom asset needs to defer to the React Player to render the sub-asset. Say for instance we change our custom asset to now support a header property that takes another asset.

Use the ReactAsset Component from the @player-ui/react package with the nested asset as props to dynamically determine the rendering implementation to use:

import { ReactAsset } from "@player-ui/react";
const CustomAssetComp = (props) => {
return (
<div id={props.id} style={{ color: "purple" }}>
{props.header && <ReactAsset {...props.header} />}
{props.text}
</div>
);
};

This would automatically find the appropriate handler for the props.header asset and use that to render.

Registering your Asset

When registering your asset with an AssetRegistry, it can either be registered as a new type, if it is an entirely new construct, or registered as a variant of an existing asset type, to only be rendered under certain conditions.

// Using AssetProviderPlugin from '@player-ui/asset-provider-plugin-react'
new AssetProviderPlugin([
// This will register a match on { type: 'example' }
['example', ExampleAsset],
//This will register a match on { type: 'example', metaData: {"role": "someRole"} }
[{ type: 'example', metaData: {"role": "someRole"}}, ExampleAsset])

In the latter case, it is recommended to extend the original asset, so as to avoid boilerplate for data and construction, and just override the render function. If your variant will have additional data decoded that the original asset does not have, you will need to create the whole asset.

Why Would I Register my Asset as a Variant?
  1. Transform backed assets have functions that are attached to them, through shared JavaScript plugins. This simplifies setting data from the asset, by giving simple functions like run in the reference ActionAsset for example. Swift only asset types will not have any convenience functions.

  2. Registering as a variant allows you to maintain usage of the transform backed asset as well as your new asset, so both can be used by the same SwiftUIPlayer or AndroidPlayer instance, including in the same flow. This also maintains the semantics of Player content, an action asset is always an action type of interaction, but with metaData, it can be displayed differently.

For more info on transform registration see Asset Transform Plugin

Use Cases

Below are 3 different use cases for different ways to mix and match the asset and transform registry to simplify the asset implementation

Use Case 1: Same type with different variants and asset implementations that can share the same transform

// Using AssetProviderPlugin from '@player-ui/asset-provider-plugin-react'
new AssetProviderPlugin([
//This will register a match on { type: 'example' }
["example", ExampleAsset],
//This will register a match on { type: 'example', metaData: {"role": "someRole"} }
[{ type: "example", metaData: { role: "someRole" } }, ExampleAsset],
]);

If the common InputData fields for the decoded data looks like:

Taken from the reference asset Input Asset example, see full transform implementation

export interface InputAsset {
id: String;
type: String;
value: String?;
}
export interface TransformedInput extends InputAsset {
/** A function to commit the new value to the data-model */
set: (newValue: ValueType) => void;
/** The `DataType` associated with this asset, for formatting the keyboard */
dataType?: DataType;
}

And we would like to render two different assets based on whether or not “dataType” is present then both InputAsset and DateInputAsset can share the same InputData which can contain a transform (such as the function to perform after input data is set) but show different content for the views such as input accessories like a calender based on the DataType

Use Case 2: Same type with different variants and asset implementations that don’t share the same transform

// Using AssetProviderPlugin from '@player-ui/asset-provider-plugin-react'
new AssetProviderPlugin([
['input', InputAsset],
[{ type: 'input', dataType: "dataType": {"type": "DateType"}}, DateInputAsset]
])

If the common InputData fields for the decoded data looks like:

export interface InputAsset {
id: String;
type: String;
value: String?;
}
export interface DateInputAsset {
id: String;
type: String;
value: String?;
}
// Used in the transform function for inputTranform
export interface TransformedInput extends InputAsset {
/** A function to commit the new value to the data-model */
set: (newValue: ValueType) => void;
}
// Used in the transform function for inputDateTranform
export interface TransformedDateInput extends DateInputAsset {
/** A function to commit the new value to the data-model */
set: (newValue: ValueType) => void;
/** The `DataType` associated with this asset, for formatting the keyboard */
dataType?: DataType;
}

In the case where the regular InputAsset and the DateInputAsset should not share the same transform, its possible to target the variant in the transform registration (since transform also use the partial match registry) to specify a different transform when the “dataType” is present for example:

import { Player } from "@player-ui/player";
import { AssetTransformPlugin } from "@player-ui/asset-transform-plugin";
// Add it to Player
const player = new Player({
plugins: [
new AssetTransformPlugin(
new Registry([
// Register a match for any input type with a custom transform.
[{ type: "input" }, inputTransform],
// Register a match for any input type that has dataType DateType with a custom transform.
[{ type: "input", dataType: { type: "DateType" } }, dateInputTransform],
]),
),
],
});

Use Case 3: Different type, same asset implementation, different transforms

// Using AssetProviderPlugin from '@player-ui/asset-provider-plugin-react'
new AssetProviderPlugin([
["choiceA", Choice],
["choiceB", Choice],
]);

Its possible to register the same asset implementation to different type names with the same variant, this may be needed if the two types visually look the same but behaviourally is different such as when the choice is clicked “choiceA” does one action but “choiceB” does something else which is defined in the transform

Since the transform is called on “select” of the WrappedFunction or Invokable<Unit> in the data this doesnt change the ChoiceData (only the values of select function itself change depending on if we get ChoiceA or ChoiceB) which means they can both be registered to ChoiceAsset

export interface Choice {
id: String
type: String
value: String?
}
export interface TransformedChoice extends Choice {
/** A function to commit the new value to the data-model */
select: (newValue: ValueType) => void;
}

Create two transforms choiceATransform and choiceBTransform that both return TransformedChoice but have different functions on select. Then in the web transform registration choiceA and choiceB are registered to those different transforms

import { Player } from "@player-ui/player";
import { AssetTransformPlugin } from "@player-ui/asset-transform-plugin";
// Add it to Player
const player = new Player({
plugins: [
new AssetTransformPlugin(
new Registry([
// Register a match for any choiceA type with a custom transform.
[{ type: "choiceA" }, choiceATransform],
// Register a match for any choiceB type with a custom transform.
[{ type: "choiceB" }, choiceBTransform],
]),
),
],
});

Overall the asset and transform registry gives developers a lot of flexibility for extending and simplifying assets based on given constraints