diff --git a/docs/site/config/navigation.ts b/docs/site/config/navigation.ts index 60392715f..35c13e714 100644 --- a/docs/site/config/navigation.ts +++ b/docs/site/config/navigation.ts @@ -69,6 +69,10 @@ const navigation: Navigation = { title: 'Overview', path: '/content', }, + { + title: 'Navigation', + path: '/content/navigation', + }, { title: 'Assets & Views', path: '/content/assets-views', @@ -78,12 +82,25 @@ const navigation: Navigation = { path: '/content/data-expressions', }, { - title: 'Navigation', - path: '/content/navigation', + title: 'Schema', + path: '/content/schema', }, + ], + }, + { + title: 'Authoring', + routes: [ { - title: 'Templates', - path: '/content/templates', + title: 'Overview', + path: '/dsl', + }, + { + title: 'Views', + path: '/dsl/views', + }, + { + title: 'Schema', + path: '/dsl/schema', }, ], }, @@ -106,6 +123,10 @@ const navigation: Navigation = { title: 'Custom Assets', path: '/assets/custom', }, + { + title: 'DSL Components', + path: '/assets/dsl', + }, ], }, { @@ -115,10 +136,6 @@ const navigation: Navigation = { title: 'Storybook', path: '/tools/storybook', }, - { - title: 'TSX Content Authoring', - path: '/tools/dsl', - }, { title: 'CLI', path: '/tools/cli', diff --git a/docs/site/pages/assets/dsl.mdx b/docs/site/pages/assets/dsl.mdx new file mode 100644 index 000000000..d83da32ca --- /dev/null +++ b/docs/site/pages/assets/dsl.mdx @@ -0,0 +1,207 @@ +--- +title: Writing DSL Components +--- + +# Creating TSX 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](https://github.com/intuit/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. + +```ts +import type { Asset, AssetWrapper, Expression } from '@player-ui/player'; + +export interface ActionAsset 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; + + /** 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: + +```tsx +import { Asset, AssetPropsWithChildren } from '@player-tools/dsl'; + +export const Action = (props: AssetPropsWithChildren) => { + return ; +} +``` + +This would allow users to import the `Action` component, and _render_ it to JSON: + +```tsx +const myView = +``` + +which when compiled would look like + +```json +{ + "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. + + +```tsx +import React from 'react'; +import { Asset, AssetPropsWithChildren, createSlot } from '@player-tools/dsl'; + +export const Action = (props: AssetPropsWithChildren) => { + return ; +} + +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: + +```tsx +const myView = ( + + + + + +); +``` + + +```tsx +import React from 'react'; + +const myView = ( + + Continue + +); +``` + +which when compiled would look like (note the auto injection of the `Text` asset and corresponding Asset Wrapper): + +```json +{ + "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 + +```tsx +const myView = ( + + + Some + Text + + +); +``` + +which when compiled would look like the following (note the automatic insertion of the `Collection` Asset): + +```json +{ + "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. + +```tsx +import React from 'react'; + +export const Action = ( + props: Omit, 'exp'> & { + /** An optional expression to execute before transitioning */ + exp?: ExpressionTemplateInstance; + } +) => { + const { exp, children, ...rest } = props; + + return ( + + {exp?.toValue()} + {children} + + ); +}; +``` + +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. + + diff --git a/docs/site/pages/content/assets-views.mdx b/docs/site/pages/content/assets-views.mdx index 9a3907786..364ed2743 100644 --- a/docs/site/pages/content/assets-views.mdx +++ b/docs/site/pages/content/assets-views.mdx @@ -33,10 +33,6 @@ Nested assets are represented as objects containing an `asset` property. For exa The `label` of the parent contains a nested asset reference. These are _slots_ that can usually contain any asset type. -## Applicability - -Any object in the tree (including _assets_) may contain an `applicability` property. This is an _expression_ that may conditionally show or hide an asset (and all of it's children) from the view tree. Applicability is dynamically calculated and will automatically update as data changes on the page. - # Views Views are _assets_ that exist at the top level of the tree. They typically include the navigation actions, a title, or other top-level information. @@ -69,3 +65,284 @@ Example: ``` They follow the same guidelines for normal validation references, with the addition of a `ref` property that points to the binding that this validation is tied to. + +## Applicability + +Any object in the tree (including _assets_) may contain an `applicability` property. This is an _expression_ that may conditionally show or hide an asset (and all of it's children) from the view tree. Applicability is dynamically calculated and will automatically update as data changes on the page. + +# Switches + +Switches are ways of dynamically changing the structure of the view based on data. There are 2 types of switches: `static` and `dynamic`, but their structures are identical. `switches` can appear anywhere you'd find a normal asset, and (similar to [templates](./templates)) are removed from the view before it reaches the UI layer. + +## Usage + +The switch is simply a list of objects with `case` and `asset` properties: + +- `asset` - The asset that will replace the switch if the case is true +- `case` - An [expression](./expression) to evaluate. + +The switch will run through each _case_ statement until the first case expression evaluates to true. For the _default_ case, simple use a value of `true` at the end of the array. + +## Static v Dynamic Switches + +The only difference between a `static` and `dynamic` switch is the timing update behavior after the first rendering of a view. + +A `staticSwitch` calculates the applicable case when a view first renders. It will not re-calculate any of the case statements as data in the view is updated. If you transition away from view-node, and revisit it later-on in the flow, the switch will re-compute the appropriate case statement. + +A `dynamicSwitch` will always update the applicable case statement whenever data changes. If data is changed while a view is still showing, the switch will be updated to reflect the new case. + +## Example + +Anywhere you can place an `asset` node, a `dynamicSwitch` or `staticSwitch` can be placed instead. + +```json +{ + "staticSwitch": [ + { + "case": "{{name.first}} == 'adam'", + "asset": { + "id": "name", + "type": "text", + "value": "Yay" + } + }, + { + "case": "{{name.first}} == 'notadam'", + "asset": { + "id": "name", + "type": "text", + "value": "Nay" + } + }, + { + "case": true, + "asset": { + "id": "name", + "type": "text", + "value": "🤷" + } + } + ] +} +``` + +# Templates + +Templates provide a way to dynamically create a list of assets, or _any_ object, based on data from the model. All of the templating semantics are removed by the time it reaches an asset's transform or UI layer. + +## Usage + +Within any asset, specify a `template` property as an array of: + +- `data` - A binding that points to an array in the model +- `output` - A property to put the mapped objects +- `value` - The template to use for each object/item in the data array. +- `dynamic` - (optional, false by default) A boolean that specifies whether template should be recomputed when data changes + +Within a template, the `_index_` string can be used to substitute the array-index of the item being mapped. + +### Example + +**Authored** + +```json +{ + "asset": { + "id": "top-level", + "type": "collection", + "template": [ + { + "data": "list.of.names", + "output": "values", + "value": { + "asset": { + "id": "value-_index_", + "type": "text", + "value": "{{list.of.names._index_}}" + } + } + } + ] + } +} +``` + +**Output** + +```json +{ + "asset": { + "id": "top-level", + "type": "collection", + "values": [ + { + "asset": { + "id": "value-0", + "type": "text", + "value": "Adam" + } + }, + { + "asset": { + "id": "value-1", + "type": "text", + "value": "Not Adam" + } + } + ] + } +} +``` + +## Multiple templates + +There's a few ways to leverage multiple templates within a single asset. Templates can be _nested_ or multiple used on a single node. These can also be combined to build out complicated nested expansion. + +### Nested Templates + +Templates can contain other templates. When referencing a nested template, append the template depth to the `_index_` string to reference the correct data-item. + +For example, if 1 template contains another, use `_index_` to reference the outer-loop, and `_index1_` to reference the inner loop. Furthermore, if templates are nested three levels deep, the first level loop will still be referenced by `_index_`, the second level will be referenced by `_index1_` and the bottom most loop will be referenced by `_index2_`. + +### Multiple Templates - Single Output + +Templates will, by default, create an array, if needed, for the `output` property of each template. If that array already exits (either by manually writing it in the JSON, or from a previous template run), each item will be appended to the end of the existing array. + +This can be leveraged by combining multiple template directives that use the same `output` property, or by having an `output` use an existing array: + +**_Example_** + +Both templates in the example below output to the `values` array on the parent object. Since no `values` array exists, the first template will create said array, and the second will append to that. + +```json +{ + "asset": { + "id": "top-level", + "type": "collection", + "template": [ + { + "data": "list.of.names", + "output": "values", + "value": { + "asset": { + "id": "name-_index_", + "type": "text", + "value": "{{list.of.names._index_}}" + } + } + }, + { + "data": "list.of.other-names", + "output": "values", + "value": { + "asset": { + "id": "other-name-_index_", + "type": "text", + "value": "{{list.of.names._index_}}" + } + } + } + ] + } +} +``` + +**_Example_** + +The template below will append it's values to the pre-existing `values` array. + +```json +{ + "asset": { + "id": "top-level", + "type": "collection", + "values": [ + { + "asset": { + "id": "existing-name", + "type": "text", + "value": "Something hard-coded" + } + } + ], + "template": [ + { + "data": "list.of.names", + "output": "values", + "value": { + "asset": { + "id": "name-_index_", + "type": "text", + "value": "{{list.of.names._index_}}" + } + } + } + ] + } +} +``` + +## Dynamic and Static Templates + +Like switches, the only difference between a `static` and `dynamic` template is the timing update behavior after the first rendering of a view. If not defined, the value of `dynamic` is default to `false`. + +If `dynamic` is `false`, the template will be parsed when a view first renders. The template will not be parsed again as data in the view is updated. + +If `dynamic` is `true`, template will be always updated whenever data changes. If data is changed while a view is still showing, the template will be updated to reflect the new data. + +**_Example_** + +```json +{ + "asset": { + "id": "top-level", + "type": "collection", + "template": [ + { + "dynamic": true, + "data": "list.of.names", + "output": "values", + "value": { + "asset": { + "id": "value-_index_", + "type": "text", + "value": "{{list.of.names._index_}}" + } + } + } + ] + } +} +``` + +```typescript +model.set([['list.of.names', ['Jain']]]); +model.set([['list.of.names', ['Jain', 'Erica']]]); +``` + +**Output** + +```json +{ + "asset": { + "id": "top-level", + "type": "collection", + "values": [ + { + "asset": { + "id": "value-0", + "type": "text", + "value": "Jain" + } + }, + { + "asset": { + "id": "value-1", + "type": "text", + "value": "Erica" + } + } + ] + } +} +``` diff --git a/docs/site/pages/content/dsl.mdx b/docs/site/pages/content/dsl.mdx deleted file mode 100644 index 42f044f59..000000000 --- a/docs/site/pages/content/dsl.mdx +++ /dev/null @@ -1,417 +0,0 @@ ---- -title: TSX DSL ---- - -# TSX/JSX Content Authoring - -While Player content _can_ be written directly in JSON, it's not always the preferable authoring format. To take advantage of existing developer tool-chains, Player provides a mechanism for authoring content in (J/T)SX as React components. This is paired with a `cli` to transpile the React tree into a JSON content. - -One thing to note is that the paths in the input folder should match the expected paths in your config file. E.g if the `pages` option is set to `topics`, the source TSX/JSX files should be under a `topics` directory in your input directory. Similarly if the schema option is set to `topic_schema.json` the ts file containing the schema object should be in the root directory and named `topic_schema.ts`. - -## Writing JSX Content - -In order to use the JSX-variant to write content, your asset library should ship a JSX component package to leverage. These will define the primitive _components_ to use to build up the tree. - -In the examples below, we will assume one exists. - -### Bindings and Expressions - -Both `binding` and `expression` in the JSX authoring leverages a tagged template, typically abbreviated as `b` and `e` respectively. In a similar fashion to using `css` or `graphql` in a JS file, this enables syntax-highlighting and validation of bindings and expressions within a JS file. - -To create a binding, or expression: - -```tsx -import { binding as b, expression as e } from '@player-tools/dsl'; - -const myBinding = b`foo.bar`; -const myExpression = e`foo()`; -``` - -The binding and expression instances can also automatically dereference themselves when used inside of another string: - -```tsx -const stringWithBinding = `Some text: ${myBinding}`; // 'Some text: {{foo.bar}}' -const stringWithExp = `Some expr: ${myExpression}`; // 'Some expr: @[foo()]@' -``` - -### Assets/Views - -Writing assets or views is as simple as creating a React element: - -```tsx -import { Input, Text, Collection } from 'my-assets'; - -const view = ( - - Some value - - Some label - - -); -``` - -this would generate something similar to: - -```json -{ - "id": "root", - "type": "collection", - "values": [ - { - "asset": { - "id": "root-values-1", - "type": "text", - "value": "Some value" - } - }, - { - "asset": { - "id": "root-values-2", - "type": "input", - "label": { - "asset": { - "id": "root-values-2-label", - "type": "text", - "value": "Some label" - } - } - } - } - ] -} -``` - -#### IDs - -Any asset can accept an `id` property (just like the JSON version), however ids will automatically be generated for assets missing them. - -#### Collection/Text Creation - -In the event that an asset object is expected, but a `string` or `number` is found, Player will attempt to automatically create a text node, provided the asset-library has a text-asset-factory configured. - -Similarly, if a single asset is expected but a list of them is found instead, Player will attempt to create a _collection_ asset provided the library has the proper configuration set. - -### Templates - -Templates are included via the `@player-tools/dsl` package. This can be used in any asset slot: - -```tsx -import { dataTypes } from '@player-ui/common-types-plugin'; -import { makeBindingsForObject, Template } from '@player-tools/dsl'; - -const schema = { - foo: [{ - bar: dataTypes.StringType, - }], -}; - -const bindings = makeBindingsForObject(schema); - - - - - - -``` - -Templates can be nested within one another, and the auto-id generation will handle adding the `_index_` information to any generated `id`. - -### Switches - -The `@player-tools/dsl` module also includes support for _static_ and _dynamic_ switches. - -Use the `isDynamic` flag to denote this should be a `dynamicSwitch` instead of a `staticSwitch`: - -```tsx -import { Switch } from '@player-ui/dsl'; - - - - - - Text 1 - - - Text 1 - - - - -``` - -### Navigation - -At this time the `navigation` section is a basic JS object. The `@player-ui/types` package provides typescript typings for these. - -```tsx -import { Navigation, Schema } from '@player-ui/types'; - -const navigation: Navigation = { - BEGIN: 'Start', - Start: { - startState: 'VIEW_1', - VIEW_1: { - state_type: 'VIEW', - ref: 'view-1', - transitions: { - '*': 'END_Done', - }, - }, - END_Done: { - state_type: 'END', - outcome: 'done', - }, - }, -}; -``` - -### Schema - -To author a schema object you should first start by constructing a standard typescript object where the nested paths correlate to the paths on your desired schema. At the final conversion to a Player `Schema` object during the serialization phase the intermediate types and ROOT elements will automatically be constructed. A basic example would be: - -```typescript -const mySchema = { - foo: { - bar: { - baz: //somevalue - faz: //somevalue - } - } -} -``` - -which correlates to a schema of: - -```json -{ - "ROOT": { - "foo": { - "type": "fooType" - } - }, - "fooType": { - "bar": { - "type": "barType" - } - }, - "barType": { - "baz": { - "type": "" - }, - "faz": { - "type": "" - } - } -} -``` - -#### Arrays - -A single object array can be used to indicate an array type, for example: - -```typescript -const mySchema = { - foo: [ - { - bar: //some type - } - ] -} -``` - -will generate the schema: - -```json -{ - "ROOT": { - "foo": { - "type": "fooType", - "isArray": true - } - }, - "fooType": { - "bar": { - "type": "barType" - } - }, - "barType": { - "baz": { - "type": "" - }, - "faz": { - "type": "" - } - } -} -``` - -#### Changing the Name of a Generated Type - -To change the name of the generated type at any point in the tree, import the `SchemaTypeName` symbol from the `@player-tools/dsl` and use it as a key to change the name like so: - -```typescript -const mySchema = { - foo: { - bar: { - [SchemaTypeName]: "buzz", - baz: //somevalue - faz: //somevalue - } - } -} -``` - -#### Defining Data Types - -The leaf nodes of the data structure will need some concrete definition of what data exists at that point of the schema. There are two ways to provide this data. - -##### Data Refs - -The `common-types-plugin` package exposes the types it provides to Player when used and _references_ to those types as well. Using these `Language.DataTypeRef` values you can indicate what the data type will be at that node and that it will be a type explicitly defined in the Player so no additional information needs to be provided (e.g. validations nor formats) - -Various plugins in the `@cg-player` scope also expose similar `DataTypeRef` objects that include common tax types. For convenience the `@cg-player/components` package reexports all the data references for one convenient import. It is important to note that there will be an issue if you use a `DataTypeRef` from a plugin but run the content in a Player that doesn't have that plugin loaded. - -##### Local Data Types - -Sometimes you need to define specific data types that extend existing types for certain pieces of data in a schema, whether that be for specific validations, formatting or both. In this case, in your DSL project you can define an object of type `Schema.DataType` and provide that value to a leaf node. That will indicate that this unique type needs to be included in its entirety to Player as it has information not already contained in Player. - -##### What that Looks Like - -Using our previous example we can fill in the values with some types now to look like this in the ts object: - -```typescript -import { dataTypes } from '@player-ui/common-types-plugin'; -import type { Schema } from '@player-ui/types'; - -const mycustombooleantype: Schema.DataType = { - type: "BooleanType", - validation: [ - { - type: 'oneOf', - message: 'Value has to be true or false', - options: [true, false], - }, - ], -} - -const mySchema = { - foo: { - bar: { - baz: BooleanTypeRef - faz: mycustombooleantype - } - } -} - -export default mySchema -``` - -and like this in the final schema: - -```json -{ - "ROOT":{ - "foo":{ - "type": "fooType" - } - }, - "fooType":{ - "bar": { - "type":"barType" - } - }, - "barType":{ - "baz":{ - "type": "BooleanType" - }, - "faz":{ - "type": "BooleanType", - "validation": [ - { - "type": "oneOf", - "message": "Value has to be true or false", - "options": [true, false], - }, - ], - } - } -} - -It should be noted that unless the schema object is a default export the `schema.json` will not be created in the output folder. -``` - -#### Using the Schema Object in JSX/TSX Content - -There is one important function that enables us to use our schema object in content.`makeBindingsForObject()` takes your schema object and constructs the bindings opaquely so that you can use the native object path with functions like `.toString()`, `.toValue()`, and `toRefString()` like you could with regular string template bindings. - -Using these functions we can use the schema directly in content: - -```jsx -import { makeBindingsForObject } from '@player-tools/dsl'; - -const schema = makeBindingsForObject(mySchema) - -const baz = schema.foo.bar.baz - -const view = ( - - - - The current value is {baz.toString()} - - - -) - -const validations = [ - { - type: "requiredIf", - ref: baz.toRefString(), - message: "This is required", - }, -]; - -const navigation = {...} - -export default { - id: "example-topic", - topic: "exampletopic", - views: [view], - navigation, -}; - -``` - -### Custom Assets - -If you need to make use of a custom asset or an asset that is in Player but not yet implemented in the Player DSL there are a couple of ways you can do this. - -Define Asset -```typescript -const customAsset = { - asset: { - id: 'id', - type: 'custom', (use the type of the asset you want to use) - ... - } -} -``` -Pass asset into an existing asset -``` - -``` -Pass asset in as child -```jsx - - {toJsonProperties(customAsset)} - -``` -OR - -You can create an Asset using the `` component from `@player-tools/dsl`. -```jsx - -``` diff --git a/docs/site/pages/content/schema.mdx b/docs/site/pages/content/schema.mdx index 0f059253b..3530d73ee 100644 --- a/docs/site/pages/content/schema.mdx +++ b/docs/site/pages/content/schema.mdx @@ -114,7 +114,7 @@ To attach a validation to a path in the data-model, add a reference to a validat } ``` -Each _validation_ reference must include a `type` property which corresponds to the name of the validator to run. Player includes some validators out-of-the-box, and custom `validators` can be registered as well. See [here]() for more details around which validators are supported, and how to add custom ones. +Each _validation_ reference must include a `type` property which corresponds to the name of the validator to run. Player includes some validators out-of-the-box, and custom `validators` can be registered as well. See [the Common Types Plugin](../plugins/common-types) docs for more details around which validators are supported, and how to add custom ones. Any additional properties on the validation reference are passed as _options_ to the `validator`. In the example above, a hypothetical `length` validator can take a `min` and `max` as the boundaries for the length of a string. diff --git a/docs/site/pages/content/switches.mdx b/docs/site/pages/content/switches.mdx deleted file mode 100644 index 3fb6fb13c..000000000 --- a/docs/site/pages/content/switches.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Switches ---- - -# Switches - -Switches are ways of dynamically changing the structure of the view based on data. There are 2 types of switches: `static` and `dynamic`, but their structures are identical. `switches` can appear anywhere you'd find a normal asset, and (similar to [templates](./templates)) are removed from the view before it reaches the UI layer. - -## Schema - -The switch is simply a list of objects with `case` and `asset` properties: - -- `asset` - The asset that will replace the switch if the case is true -- `case` - An [expression](./expression) to evaluate. - -The switch will run through each _case_ statement until the first case expression evaluates to true. For the _default_ case, simple use a value of `true` at the end of the array. - -## `Static` vs `Dynamic` - -The only difference between a `static` and `dynamic` switch is the timing update behavior after the first rendering of a view. - -A `staticSwitch` calculates the applicable case when a view first renders. It will not re-calculate any of the case statements as data in the view is updated. If you transition away from view-node, and revisit it later-on in the flow, the switch will re-compute the appropriate case statement. - -A `dynamicSwitch` will always update the applicable case statement whenever data changes. If data is changed while a view is still showing, the switch will be updated to reflect the new case. - -## Example - -Anywhere you can place an `asset` node, a `dynamicSwitch` or `staticSwitch` can be placed instead. - -```json -{ - "staticSwitch": [ - { - "case": "{{name.first}} == 'adam'", - "asset": { - "id": "name", - "type": "text", - "value": "Yay" - } - }, - { - "case": "{{name.first}} == 'margie'", - "asset": { - "id": "name", - "type": "text", - "value": "Nay" - } - }, - { - "case": true, - "asset": { - "id": "name", - "type": "text", - "value": "🤷" - } - } - ] -} -``` diff --git a/docs/site/pages/content/templates.mdx b/docs/site/pages/content/templates.mdx deleted file mode 100644 index cea5e0acf..000000000 --- a/docs/site/pages/content/templates.mdx +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: Templates ---- - -# Templates - -Templates provide a way to dynamically create a list of assets, or _any_ object, based on data from the model. All of the templating semantics are removed by the time it reaches an asset's transform or UI layer. - -## Usage - -Within any asset, specify a `template` property as an array of: - -- `data` - A binding that points to an array in the model -- `output` - A property to put the mapped objects -- `value` - The template to use for each object/item in the data array. -- `dynamic` - (optional, false by default) A boolean that specifies whether template should be recomputed when data changes - -Within a template, the `_index_` string can be used to substitute the array-index of the item being mapped. - -### Example - -**Authored** - -```json -{ - "asset": { - "id": "top-level", - "type": "collection", - "template": [ - { - "data": "list.of.names", - "output": "values", - "value": { - "asset": { - "id": "value-_index_", - "type": "text", - "value": "{{list.of.names._index_}}" - } - } - } - ] - } -} -``` - -**Output** - -```json -{ - "asset": { - "id": "top-level", - "type": "collection", - "values": [ - { - "asset": { - "id": "value-0", - "type": "text", - "value": "Adam" - } - }, - { - "asset": { - "id": "value-1", - "type": "text", - "value": "Margie" - } - } - ] - } -} -``` - -## Multiple templates - -There's a few ways to leverage multiple templates within a single asset. Templates can be _nested_ or multiple used on a single node. These can also be combined to build out super complicated nested expansion. - -### Nested Templates - -Templates can contain other templates. When referencing a nested template, append the template depth to the `_index_` string to reference the correct data-item. - -For example, if 1 template contains another, use `_index_` to reference the outer-loop, and `_index1_` to reference the inner loop. - -### Multiple Templates - Single Output - -Templates will, by default, create an array, if needed, for the `output` property of each template. If that array already exits (either by manually writing it in the JSON, or from a previous template run), each item will be appended to the end of the existing array. - -This can be leveraged by combining multiple template directives that use the same `output` property, or by having an `output` use an existing array: - -**_Example_** - -Both templates in the example below output to the `values` array on the parent object. Since no `values` array exists, the first template will create said array, and the second will append to that. - -```json -{ - "asset": { - "id": "top-level", - "type": "collection", - "template": [ - { - "data": "list.of.names", - "output": "values", - "value": { - "asset": { - "id": "name-_index_", - "type": "text", - "value": "{{list.of.names._index_}}" - } - } - }, - { - "data": "list.of.other-names", - "output": "values", - "value": { - "asset": { - "id": "other-name-_index_", - "type": "text", - "value": "{{list.of.names._index_}}" - } - } - } - ] - } -} -``` - -**_Example_** - -The template below will append it's values to the pre-existing `values` array. - -```json -{ - "asset": { - "id": "top-level", - "type": "collection", - "values": [ - { - "asset": { - "id": "existing-name", - "type": "text", - "value": "Something hard-coded" - } - } - ], - "template": [ - { - "data": "list.of.names", - "output": "values", - "value": { - "asset": { - "id": "name-_index_", - "type": "text", - "value": "{{list.of.names._index_}}" - } - } - } - ] - } -} -``` - -## Dynamic and Static Templates - -As in like switches, the only difference between a `static` and `dynamic` template is the timing update behavior after the first rendering of a view. If not defined, the value of `dynamic` is default to `false`. - -If `dynamic` is `false`, the template will be parsed when a view first renders. The template will not be parsed again as data in the view is updated. - -If `dynamic` is `true`, template will be always updated whenever data changes. If data is changed while a view is still showing, the template will be updated to reflect the new data. - -**_Example_** - -```json -{ - "asset": { - "id": "top-level", - "type": "collection", - "template": [ - { - "dynamic": true, - "data": "list.of.names", - "output": "values", - "value": { - "asset": { - "id": "value-_index_", - "type": "text", - "value": "{{list.of.names._index_}}" - } - } - } - ] - } -} -``` - -```typescript -model.set([['list.of.names', ['Jain']]]); -model.set([['list.of.names', ['Jain', 'Erica']]]); -``` - -**Output** - -```json -{ - "asset": { - "id": "top-level", - "type": "collection", - "values": [ - { - "asset": { - "id": "value-0", - "type": "text", - "value": "Jain" - } - }, - { - "asset": { - "id": "value-1", - "type": "text", - "value": "Erica" - } - } - ] - } -} -``` diff --git a/docs/site/pages/dsl/index.mdx b/docs/site/pages/dsl/index.mdx new file mode 100644 index 000000000..5836f976f --- /dev/null +++ b/docs/site/pages/dsl/index.mdx @@ -0,0 +1,169 @@ +--- +title: DSL Content Overview +--- + +# TSX/JSX Content Authoring (Player DSL) + +While Player content _can_ be written directly in JSON, it's definitely not the preferable authoring format. To take advantage of existing developer tool-chains, Player provides a mechanism for authoring content in (J/T)SX as React components and simple TypeScript objects. The Player CLI can then be used to transpile the React tree into a JSON content. + +## DSL Benefits + +At a high level, the benefits to writing Player content in the DSL can be summarized by three key factors: + +#### Easier maintainability +Simply put, DSL code more concise than its JSON equivalent. That means there is less code for you to have to maintain. Also, as its easier to read than JSON, when you do need to make updates to it, its much more wieldy to work with. + +#### Better development experience +Since the DSL leverages a lot of standard TypeScript language features, most editors will offer quality of life features like typechecking, suggestions, and code generation. All of this is in service of shortening the feedback loop of writing content and ensuring it is what you intended for it. + +#### Easier to extend +The DSL now offers a easily accessible programatic hook into Player content. This allows custom tooling to be created around your DSL integration much easier that before. Common patterns can be extracted into higher level compoennts, functions can be created to generate code, and code generation can be integrated into almost any process where relevant data is present. + +For a further explination on the benefits, see the DSL Benefits section in the [DSL Views](./views.mdx#dsl-benefits-in-views) and the [DSL Schema](./schema.mdx#dsl-benefit-in-schema) + +## Writing DSL Content + +In order to use the DSL to write content, your plugin library should ship a DSL component package. These will define the primitive _components_ to use to build up the tree. Authorship of these components is covered in the [Writing DSL Components](../assets/dsl) secton. The Player Reference Assets ship their own DSL Components via the `@player-ui/reference-assets-components` pacakge. + +In the examples below, we will use the Player Reference Assets Components. + +### Basic Setup + +To get started, you'll need the following dependencies in your `package.json`: + +```json +{ + "dependencies": { + "@player-tools/dsl": "0.4.1", + "@player-tools/cli": "0.4.1", + "@player-ui/reference-assets-components": "0.6.0", + "@types/react": "17.0.39", + "react": "17.0.2" + } +} +``` + +Next, you'll need to configure your environment for DSL Compilation and JSON validation. Below is a basic configuration that can be added in your `package.json`. For a more detailed explination and examples on further customization please refer to the [CLI](../tools/cli) section. + +```json +{ + "player": { + "dsl": { + "src": "./src/main/tsx", + "outDir": "./out" + }, + "json": { + "src": "./out/*.json" + }, + } +} +``` + +### Basic Format and File Layout + +By default, all files that contain a Player Flow should be exported as a `.tsx` file and the schema should be in a `.ts` file. For how to change this behavior, please refer to the [DSL Plugins](./plugins) section of the docs. Each of these files should contain a default export of their appropriate object. For example a file that exports a flow should look like the following: + +```tsx +export default { + id: 'my-flow', + views: [....], + navigation: {....} +} +``` + +and a file that exports the schema should look like: + +```typescript +const mySchema = {...} + +export default mySchema + +``` + +### Navigation + +At this time the `navigation` section is a basic JS object. The `@player-ui/types` package provides typescript typings for this. + +```tsx +import { Navigation } from '@player-ui/types'; + +const navigation: Navigation = { + BEGIN: 'Start', + Start: { + startState: 'VIEW_1', + VIEW_1: { + state_type: 'VIEW', + ref: 'view-1', + transitions: { + '*': 'END_Done', + }, + }, + END_Done: { + state_type: 'END', + outcome: 'done', + }, + }, +}; +``` + +One convenience feature is the auto injection of the the `ref` property for a `VIEW` type state if the corresponding view is a React tree. + +```tsx +import { Navigation } from '@player-ui/types'; + +const view = ( + + Some value + + Some label + + +); + +const navigation: Navigation = { + BEGIN: 'Start', + Start: { + startState: 'VIEW_1', + VIEW_1: { + state_type: 'VIEW', + ref: view, + transitions: { + '*': 'END_Done', + }, + }, + END_Done: { + state_type: 'END', + outcome: 'done', + }, + }, +}; +``` +### Bindings and Expressions + +Both `binding` and `expression` in the JSX authoring leverages a tagged template, typically abbreviated as `b` and `e` respectively. In a similar fashion to using `css` or `graphql` in a JS file, this enables syntax-highlighting and validation of bindings and expressions within a JS file. + +```tsx +import { binding as b, expression as e } from '@player-tools/dsl'; + +const myBinding = b`foo.bar`; +const myExpression = e`foo()`; +``` + +The binding and expression instances can also automatically dereference themselves when used inside of another string: + +```tsx +const stringWithBinding = `Some text: ${myBinding}`; // 'Some text: {{foo.bar}}' +const stringWithExp = `Some expr: ${myExpression}`; // 'Some expr: @[foo()]@' +``` + +### View + +Please refer to the [Views](./views) section for a detailed overview of how to write DSL Views + +### Schema + +Please refer to the [Schema](./schema) section for a detailed overview of how to write DSL Schemas + +## Compiling DSL Content + +Once your DSL content is authored, you can use the Player CLI to compile and validate your content. For documentation on this functionality, please refer to the [Player CLI](../tools/cli) section \ No newline at end of file diff --git a/docs/site/pages/dsl/plugins.mdx b/docs/site/pages/dsl/plugins.mdx new file mode 100644 index 000000000..1e81c7058 --- /dev/null +++ b/docs/site/pages/dsl/plugins.mdx @@ -0,0 +1,46 @@ +--- +title: DSL Plugins +--- + +# DSl Plugins + +Much like the rest of Player, DSL compilation supports plugins that can influce how content gets compiled and generated. DSL Plugins are a subset of CLI Plugins that use either the hooks available on the CLI itself or on the DSL compiler instance created by the CLI. This section will cover the hooks that are available for use and why you might want to tap them. + +## CLI Hooks + +The `createCompilerContext` function available to plugins that extend the `PlayerCLIPlugin` class gives access to the `CompilationContext` instance. This class manages the context around DSL compilation and exposes two related hooks. + +### `identifyContentType` + +The `identifyContentType` hooks's purpose is to allow plugins to inject custom behavior around detecting what kind of file is being compiled. By default there are three types of content the CLI is aware of (`view`, `flow`, and `schema`). Its methods for detecting which kind of content is contained within a file is very rudimentary (the logic can be found [here](https://github.com/player-ui/tools/blob/main/language/dsl/src/compiler/utils.ts#L5)). In order to allow desired convention or orchestrate the compilation of custom file types, this hook provides a mechanism for allowing that custom logic to be injected. The result of this hook is used in the next hook + +### `compileContent` + +The `compileContent` hook's purpose is to allow the custom compilation logic for any identified file type. As it is an `AsyncSeriesBailHook` it will take the first result returned from a tap who was able to return a result for the compilation for the given file of the identified type. In the case where no external logic is added, the hook will attempt to compile any of its known content types with the built in compiler instance. + +## Compilation Hooks + +The CLI will initialize an instance of the `DSLCompiler` and provide a reference to it via the `onCreateDSLCompiler` function available to plugins that extend the `PlayerCLIPlugin` class. On the compiler itself, the following hook are available to modify the behavior of how DSL content is compiled. + +### `preProcessFlow` +_Note: Only called for `view` or `flow` content_ + +This hook allows transformations on the content before it is compiled. This enables the injection of additonal data or resolving any integration specific convention into something that may be understood by the compiler. This hook can also be used to collate information on what is being compiled for use later. + +### `postProcessFlow` +_Note: Only called for `view` or `flow` content_ + +This hook allows transformations on the content after it is compiled. This allows modifications to the compiled content which in some cases may be preferable as manipulating JSON may be easier than a React Tree. + +### `schemaGenerator` + +This hook gives access to the internal `SchemaGenerator` object which is responsible for compiling the schema. On this generator there are the following hooks. + +#### `createSchemaNode` + +This hook allows custom logic for processing schema nodes as they are generated. This enables arbitrary properties to be statically or dynamically added based on the authored schema node. One potential usecase of this is to allow integration specific semantic conventions to be defined and injected into the final schema. For example, the presence of a specific `Symbol` might mean that a property needs to be injected or even that the schema tree from this point on needs to be modified. + + +### `onEnd` + +This hook is called to signal that the compilation of all files has been completed. This allows any post processing on the output as a whole to take place as a part of the build process. This may include actions like moving or bundling the compilation results or writing new files based on information collected via other hooks on the files that were processed. \ No newline at end of file diff --git a/docs/site/pages/dsl/schema.mdx b/docs/site/pages/dsl/schema.mdx new file mode 100644 index 000000000..85b9ac24c --- /dev/null +++ b/docs/site/pages/dsl/schema.mdx @@ -0,0 +1,232 @@ +--- +title: Writing DSL Schemas +--- + +# Basic Schema + +To author a schema object you should first start by constructing a standard typescript object where the nested paths correlate to the paths on your desired schema. When compiled to the final Player `Schema` object, the intermediate types and ROOT elements will automatically be constructed. A basic example would be: + +```typescript +export default { + foo: { + bar: { + baz: {...} + faz: {...} + } + } +} +``` + +which correlates to a schema of: + +```json +{ + "ROOT": { + "foo": { + "type": "fooType" + } + }, + "fooType": { + "bar": { + "type": "barType" + } + }, + "barType": { + "baz": {...}, + "faz": {...} + } +} +``` + +## Arrays + +A single object array can be used to indicate an array type, for example: + +```typescript +export default { + foo: [ + { + bar: {...} + } + ] +} +``` + +will generate the schema: + +```json +{ + "ROOT": { + "foo": { + "type": "fooType", + "isArray": true + } + }, + "fooType": { + "bar": { + "type": "barType" + } + }, + "barType": { + "baz": {...}, + "faz": {...} + } +} +``` + +## Changing the Name of a Generated Type + +To change the name of the generated type at any point in the tree, import the `SchemaTypeName` symbol from the `@player-tools/dsl` and use it as a key on the object whos name you want to change: + +```typescript +import { SchemaTypeName } from "@player-tools/dsl" +export default { + foo: { + bar: { + [SchemaTypeName]: "buzz", + baz: {...} + faz: {...} + } + } +} +``` + +will generate the schema: + +```json +{ + "ROOT": { + "foo": { + "type": "fooType", + "isArray": true + } + }, + "fooType": { + "buzz": { + "type": "buzzType" + } + }, + "buzzType": { + "baz": { + "type": "" + }, + "faz": { + "type": "" + } + } +} +``` + +# Defining Data Types + +The leaf nodes of the schema will need some concrete definition of what data exists at that point of the schema. There are two ways to provide this data. + +## Data Refs + +The `@player-ui/common-types-plugin` package exposes the types it provides to Player when used and _references_ to those types as well. Using these `Language.DataTypeRef` values you can indicate what the data type will be at that node and that it will be a type explicitly defined in Player so no additional information needs to be provided (e.g. validations nor formats) as at runtime it will use the type loaded into Player by the plugin. + +It is recommended that if your player integration loads additional types, to export similar references to those types to make authorship easier. + +##### Local Data Types + +Sometimes you need to define specific data types that extend existing types for certain pieces of data in a schema, whether that be for specific validations, formatting or both. In this case, in your DSL project you can define an object of type `Schema.DataType` and provide that value to a leaf node. That will indicate that this unique type needs to be included in its entirety to Player as it has information not already contained in Player. + +##### What that Looks Like + +Using our previous example we can fill in the values with some types now to look like this in the ts object: + +```typescript +import { dataTypes } from '@player-ui/common-types-plugin'; +import type { Schema } from '@player-ui/types'; + +const mycustombooleantype = { + type: "BooleanType", + validation: [ + { + type: 'oneOf', + message: 'Value has to be true or false', + options: [true, false], + }, + ], +} satisfies Schema.DataType + +const mySchema = { + foo: { + bar: { + baz: dataTypes.BooleanTypeRef + faz: mycustombooleantype + } + } +} + +export default mySchema +``` + +and like this in the final schema: + +```json +{ + "ROOT":{ + "foo":{ + "type": "fooType" + } + }, + "fooType":{ + "bar": { + "type":"barType" + } + }, + "barType":{ + "baz":{ + "type": "BooleanType" + }, + "faz":{ + "type": "BooleanType", + "validation": [ + { + "type": "oneOf", + "message": "Value has to be true or false", + "options": [true, false], + }, + ], + } + } +} +``` + +# Using the Schema Object in JSX/TSX Content + +As the schema is now a TypeScript obejct, you can now directly reference the schema anywhere in content. The `makeBindingsForObject()` function takes your schema object and constructs the bindings opaquely within the object. This allows the use of the native path in your authored content and for the actual underlying binding to be used when the content is compiled. Additionally, as the underlying bindings are exposed, can you can use the native object path with functions like `.toString()`, `.toValue()`, and `toRefString()` like you could with regular string template bindings. + +```jsx +import { makeBindingsForObject } from '@player-tools/dsl'; +import { mySchema } from './schema' + +const schema = makeBindingsForObject(mySchema) + +const baz = schema.foo.bar.baz + +const view = ( + + + + The current value is {baz.toString()} + + + +) + +const navigation = {...} + +export default { + id: "example", + views: [view], + navigation, +} +``` + +# DSL Benefit in Schema + +## Format + +## Ease of Use \ No newline at end of file diff --git a/docs/site/pages/dsl/views.mdx b/docs/site/pages/dsl/views.mdx new file mode 100644 index 000000000..c2e0278f6 --- /dev/null +++ b/docs/site/pages/dsl/views.mdx @@ -0,0 +1,133 @@ +--- +title: Writing DSL Views +--- + +# Overview +Writing assets or views is as simple as creating a React element using your base DSL components: + +```tsx +import React from 'react'; +import { Input, Text, Collection } from '@player-ui/reference-assets-components'; + +const view = ( + + Some value + + Some label + + +); +``` + +When compiled, this would produce the following JSON. + +```json +{ + "id": "root", + "type": "collection", + "values": [ + { + "asset": { + "id": "root-values-1", + "type": "text", + "value": "Some value" + } + }, + { + "asset": { + "id": "root-values-2", + "type": "input", + "label": { + "asset": { + "id": "root-values-2-label", + "type": "text", + "value": "Some label" + } + } + } + } + ] +} +``` + +Not only is the source DSL content a fraction of the output object's size (making it easier to read and maintain) as the base components use the same TypeScript types as the assets themselves, you will receive in editor suggestions and type checks as you author your content. + +# View Concepts in DSL + +## Templates + +Templates are included via the `@player-tools/dsl` package. This can be used in any asset slot: + +```tsx +import React from 'react'; +import { dataTypes } from '@player-ui/common-types-plugin'; +import { makeBindingsForObject, Template } from '@player-tools/dsl'; + +const schema = { + foo: [{ + bar: dataTypes.StringType, + }], +}; + +const bindings = makeBindingsForObject(schema); + + + + + + +``` + +Templates can be nested within one another, and the auto-id generation will handle adding the `_index_` information to any generated `id`. + +## Switches + +The `@player-tools/dsl` module also includes support for _static_ and _dynamic_ switches. + +Use the `isDynamic` flag to denote this should be a `dynamicSwitch` instead of a `staticSwitch`: + +```tsx +import React from 'react'; +import { Switch } from '@player-ui/dsl'; + + + + + + Text 1 + + + Text 1 + + + + +``` + +# DSL Benefits in Views + +## IDs + +Any asset can accept an `id` property, however automatic ID creation is supported out of the box by the base `Asset` component and it's generation behavior can be further customized via your component's implementation. + +## Collection/Text Creation + +In the event that an asset object is expected, but a `string` or `number` is found, Player will attempt to automatically create a text node, provided the asset-library has a text-asset-factory configured. + +Similarly, if a single asset is expected but a list of them is found instead, Player will attempt to create a _collection_ asset provided the library has the proper configuration set. + +## Meta Components + +As DSL components are React component, they can be composed into reusable building blocks to simplify and abstract away common UI patterns. By centralizing these patterns, code duplication can be minimized and updates across multiple sets of content can be simplified. These composed components don't just have to be built on top of the base set of DSL components, DSL components themselves can offer common shortcuts for behavior. For example, if we wanted to offer an out of the box `Action` component that could be used as a `Next` action asset, we could export the following from the DSL components library. + +```tsx +import React from 'react'; + +Action.Next = () => ( + + Continue + +); +``` \ No newline at end of file diff --git a/docs/site/pages/tools/dsl.mdx b/docs/site/pages/tools/dsl.mdx deleted file mode 100644 index 5e4529d4f..000000000 --- a/docs/site/pages/tools/dsl.mdx +++ /dev/null @@ -1,245 +0,0 @@ ---- -title: Writing content using TSX ---- - - -# TSX Content Authoring - -While JSON content makes for a decent transport layer, it's not always the preferable authoring format. To take advantage of existing developer tool-chains, Player provides a mechanism for authoring content in (J/T)SX as React components. This is paired with a cli to transpile the React tree into a JSON content. - -## Writing TSX Content - -In order to use the TSX-variant to write content, your asset library should ship a TSX component package to leverage. Read more [here](#creating-tsx-components) for more details on creating one. - -In the examples below, we will assume one already exists. - -## Assets/Views - -Assets and views are treated as basic React elements: - -```tsx -import { Input, Text, Collection } from '@player-ui/reference-assets-components'; - -const view = ( - - Some value - - Some label - - -); -``` - -which would generate something equivalent to: - -```json -{ - "id": "root", - "type": "collection", - "values": [ - { - "asset": { - "id": "root-values-1", - "type": "text", - "value": "Some value" - } - }, - { - "asset": { - "id": "root-values-2", - "type": "input", - "label": { - "asset": { - "id": "root-values-2-label", - "type": "text", - "value": "Some label" - } - } - } - } - ] -} -``` - -### Automatic ID generation - -Any component leveraging the base `Asset` component will automatically generate an `id` if one isn't provided. - -### Automatic Text/Collection generation - -In thee event that an asset is expected, but a `string` or `number` is found, a `Text` node will automatically be created (provided the asset-library as a _text-asset-factory_ configured). - -Similarly, if a single asset is expected, but a list is found, a `Collection` node will be created. - -## Bindings/Expressions - -Both binding and expression in the TSX authoring leverages a tagged template, typically abbreviated as `b` and `e` respectively. In a similar fashion to using `css` or `graphql` in a TS/JS file, this enables syntax-highlighting and validation of bindings and expressions within a JS file. - -To create a binding or expression: - -```tsx -import { binding as b, expression as e } from '@player-tools/dsl'; - -const myBinding = b`foo.bar`; -const myExpression = e`foo()`; -``` - -Bindings and expressions can also be used within template strings and automatically dereference themselves: - -```tsx -const stringWithBinding = `Some text: ${myBinding}`; // 'Some text: {{foo.bar}}' -const stringWithExp = `Some expr: ${myExpression}`; // 'Some expr: @[foo()]@' -``` - -## Templates - -Template support is included via the `@player-tools/dsl` package. This can be used in place of an asset slot: - -```tsx - - - - Value 1 - - -``` - -Templates can be nested within one another, and the auto-id generation will handle adding the `_index_` information to any generated `id`. - -## Switches - -Included as well is the ability to specify switches: - -```tsx - - - - - Text 1 - - - Text 1 - - - - -``` - -Use the `isDynamic` flag to denote if it should be a `static` or `dynamic` switch. - -## Schema - -The detailed format for the schema section is described [here](../content/dsl###Schema). The schema object is a mix of JS object structure with leaf nodes that are Player data types or type references. At DSL compile time, this data sctructure will be compiled down into the schema format required in JSON. While you can write the schema in the native format, using this noation allows it to be referenced directly in the TSX content in addition to being much more intuitive to write. - -## Navigation - -The `navigation` content segment is a basic JS object. The `@player-ui/player` package exports TypeScript interfaces which can provide typings for these: - -```tsx -import { Navigation } from '@player-ui/player'; - -const navigation: Navigation = { - BEGIN: 'Start', - Start: { - startState: 'VIEW_1', - VIEW_1: { - state_type: 'VIEW', - ref: 'view-1', - transitions: { - '*': 'END_Done', - }, - }, - END_Done: { - state_type: 'END', - outcome: 'done', - }, - }, -}; -``` - -# Creating TSX 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](https://github.com/intuit/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. - -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. -It's type resembles something akin to: - -```ts -import { Asset, AssetWrapper, Expression } from '@player-ui/player'; - -export interface ActionAsset 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; - - /** An optional expression to execute before transitioning */ - exp?: Expression; -} -``` - -To turn this interface into a usable component, create a new React component that _renders_ an Asset: - -```tsx -import { Asset, AssetPropsWithChildren } from '@player-tools/dsl'; - -export const Action = (props: AssetPropsWithChildren) => { - return ; -} -``` - -This would allow users to import the `Action` component, and _render_ it to JSON: - -```tsx -const myView = -``` - -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. - -```tsx -import { Asset, AssetPropsWithChildren } from '@player-tools/dsl'; - -export const Action = (props: AssetPropsWithChildren) => { - return ; -} - -Action.Label = createSlot({ - name: 'label' -}) -``` - -This adds component (`Action.Label`) that will automatically place any nested children under the `label` property of the parent asset: - -```tsx -const myView = ( - - - - - -); -``` - -It also accepts components for automatically creating `text` and `collection` assets, allowing users to shortcut the `Text` component: - -```tsx -const myView = ( - - Continue - -); -``` diff --git a/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts b/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts index c36c05a23..b248b535c 100644 --- a/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts +++ b/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts @@ -180,12 +180,12 @@ describe('expr functions', () => { pet: 'dog', }, { - name: 'Margie', + name: 'Frodo', pet: 'cat', }, ], ], - ['names', ['Adam', 'Tyler', 'Andrew', 'Kendall']], + ['names', ['Adam', 'Spencer', 'Ketan', 'Harris']], ]); }); @@ -194,7 +194,7 @@ describe('expr functions', () => { context, 'people', 'name', - 'Margie', + 'Frodo', 'pet', undefined ); @@ -203,7 +203,7 @@ describe('expr functions', () => { context, 'people', 'name', - 'Margie' + 'Frodo' ); expect(property).toBe('cat'); @@ -217,7 +217,7 @@ describe('expr functions', () => { pet: 'dog', }, { - name: 'Margie', + name: 'Frodo', pet: 'cat', }, ]; @@ -226,12 +226,12 @@ describe('expr functions', () => { context, arr, 'name', - 'Margie', + 'Frodo', 'pet', undefined ); - const propertyIndex = findPropertyIndex(context, arr, 'name', 'Margie'); + const propertyIndex = findPropertyIndex(context, arr, 'name', 'Frodo'); expect(property).toBe('cat'); expect(propertyIndex).toBe(1); @@ -242,7 +242,7 @@ describe('expr functions', () => { context, 'people', 'name', - 'Tyler' + 'Spencer' ); expect(propertyIndex).toBe(-1); @@ -253,7 +253,7 @@ describe('expr functions', () => { context, undefined as any, 'name', - 'Tyler' + 'Spencer' ); expect(propertyIndex).toBe(-1); @@ -278,7 +278,7 @@ describe('expr functions', () => { context, 'people', 'name', - 'Tyler', + 'Spencer', 'pet', 'rabbit' ); @@ -304,12 +304,12 @@ describe('expr functions', () => { pet: 'dog', }, { - name: 'Margie', + name: 'Frodo', pet: 'cat', }, ], ], - ['names', ['Adam', 'Tyler', 'Andrew', 'Kendall']], + ['names', ['Adam', 'Spencer', 'Ketan', 'Harris']], ]); }); diff --git a/plugins/reference-assets/components/src/index.tsx b/plugins/reference-assets/components/src/index.tsx index 9a8e99e0e..0629db879 100644 --- a/plugins/reference-assets/components/src/index.tsx +++ b/plugins/reference-assets/components/src/index.tsx @@ -2,8 +2,9 @@ import React from 'react'; import type { AssetPropsWithChildren, BindingTemplateInstance, + ExpressionTemplateInstance, } from '@player-tools/dsl'; -import { createSlot, Asset } from '@player-tools/dsl'; +import { createSlot, Asset, View } from '@player-tools/dsl'; import type { Asset as AssetType } from '@player-ui/player'; import type { ActionAsset, @@ -63,8 +64,20 @@ Collection.Values = createSlot({ Collection.Label = LabelSlot; -export const Action = (props: AssetPropsWithChildren) => { - return ; +export const Action = ( + props: Omit, 'exp'> & { + /** An optional expression to execute before transitioning */ + exp?: ExpressionTemplateInstance; + } +) => { + const { exp, children, ...rest } = props; + + return ( + + {exp?.toValue()} + {children} + + ); }; Action.Label = LabelSlot; @@ -87,7 +100,7 @@ export const Input = ( Input.Label = LabelSlot; export const Info = (props: AssetPropsWithChildren) => { - return ; + return ; }; Info.Title = TitleSlot;