Skip to content

Commit

Permalink
feat: add base action binding support (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
dpilch committed Oct 13, 2021
1 parent 9db8bdc commit e6e60c0
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 75 deletions.
31 changes: 31 additions & 0 deletions packages/amplify-ui-codegen-schema/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,20 @@ export type StudioComponent = {
| StudioComponentSimplePropertyBinding
| StudioComponentEventPropertyBinding;
};

/**
* These are the collection properties
*/
collectionProperties?: {
[propertyName: string]: StudioComponentDataConfiguration;
};

/**
* Component actions
*/
actions?: {
[actionName: string]: StudioComponentAction;
};
};

/**
Expand Down Expand Up @@ -169,6 +177,14 @@ export type StudioComponentChild = {
* These are the nested components in a composite
*/
children?: StudioComponentChild[];

/**
* Event <-> Action mapping (e.g click => SignOutAction)
* When an event is triggered, an action is executed
*/
events?: {
[eventName: string]: string;
};
};

/**
Expand Down Expand Up @@ -491,3 +507,18 @@ type DeepPartial<T> = {
};

export type StudioTheme = DeepPartial<typeof theme>;

/**
* Component action types
*/
export type StudioComponentAction = AmplifyAuthSignOutAction;

/**
* Amplify Auth signout Action type
*/
export type AmplifyAuthSignOutAction = {
type: 'Amplify.Auth.SignOut';
parameters?: {
global: boolean;
};
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`amplify render tests actions should render sign out action 1`] = `
Object {
"componentText": "/* eslint-disable */
import React from \\"react\\";
import {
Button,
EscapeHatchProps,
Flex,
Heading,
findChildOverrides,
getOverrideProps,
} from \\"@aws-amplify/ui-react\\";
export type SiteHeaderProps = {
overrides?: EscapeHatchProps | undefined | null,
};
export default function SiteHeader(props: SiteHeaderProps): React.Element {
const overrides = { ...props.overrides };
const { invokeAction } = useActions({
signOutAction: {
type: \\"Amplify.Auth.SignOut\\",
parameters: { global: true },
},
});
return (
<Flex
direction=\\"row\\"
justifyContent=\\"space-between\\"
{...props}
{...getOverrideProps(overrides, \\"Flex\\")}
>
<Heading
level={1}
children=\\"Title\\"
{...findChildOverrides(props.overrides, \\"Heading\\")}
></Heading>
<Button
variation=\\"primary\\"
children=\\"Log off\\"
onClick={invokeAction(\\"signOutAction\\")}
{...getOverrideProps(overrides, \\"Flex.Button\\")}
></Button>
</Flex>
);
}
",
"declaration": undefined,
"renderComponentToFilesystem": [Function],
}
`;

exports[`amplify render tests basic component tests should generate a simple box component 1`] = `
"/* eslint-disable */
import React from \\"react\\";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,10 @@ describe('amplify render tests', () => {
expect(generateWithThemeRenderer('theme', { target: ScriptTarget.ES5, script: ScriptKind.JS })).toMatchSnapshot();
});
});

describe('actions', () => {
it('should render sign out action', () => {
expect(generateWithAmplifyRenderer('componentWithActionSignOut')).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"componentType": "Flex",
"name": "SiteHeader",
"actions": {
"signOutAction": {
"type": "Amplify.Auth.SignOut",
"parameters": {
"global": true
}
}
},
"properties": {
"direction": {
"value": "row"
},
"justifyContent": {
"value": "space-between"
}
},
"children": [
{
"componentType": "Heading",
"name": "Heading1",
"properties": {
"level": {
"value": 1
},
"children": {
"value": "Title"
}
}
},
{
"componentType": "Button",
"name": "Button1",
"events": {
"click": "signOutAction"
},
"properties": {
"variation": {
"value": "primary"
},
"children": {
"value": "Log off"
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,37 @@ export function buildOpeningElementAttributes(prop: ComponentPropertyValueTypes,
}
return factory.createJsxAttribute(factory.createIdentifier(propName), undefined);
}

/* Tempory stub function to map from generic event name to React event name. Final implementation will be included in
* amplify-ui.
*/
function mapGenericEventToReact(genericEventBinding: string): string {
switch (genericEventBinding) {
case 'click':
return 'onClick';
default:
throw new Error(`${genericEventBinding} is not a possible event.`);
}
}

/* Build React attribute for actions
*
* Example: onClick={invokeAction("signOutAction")}
*/
export function buildOpeningElementActions(genericEventBinding: string, action: string): JsxAttribute {
// TODO: map from generic to platform
const reactActionBinding = mapGenericEventToReact(genericEventBinding);
return factory.createJsxAttribute(
factory.createIdentifier(reactActionBinding),
factory.createJsxExpression(
undefined,
factory.createCallExpression(factory.createIdentifier('invokeAction'), undefined, [
factory.createStringLiteral(action),
]),
),
);
}

export function addBindingPropertiesImports(
component: StudioComponent | StudioComponentChild,
importCollection: ImportCollection,
Expand Down
18 changes: 14 additions & 4 deletions packages/studio-ui-codegen-react/lib/react-component-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { StudioComponent, StudioComponentChild } from '@amzn/amplify-ui-codegen-
import { ComponentRendererBase, StudioNode } from '@amzn/studio-ui-codegen';
import { JsxAttributeLike, JsxElement, JsxOpeningElement, factory } from 'typescript';

import { addBindingPropertiesImports, buildOpeningElementAttributes } from './react-component-render-helper';
import {
addBindingPropertiesImports,
buildOpeningElementAttributes,
buildOpeningElementActions,
} from './react-component-render-helper';
import { ImportCollection } from './import-collection';

export abstract class ReactComponentRenderer<TPropIn> extends ComponentRendererBase<TPropIn, JsxElement> {
Expand All @@ -16,17 +20,23 @@ export abstract class ReactComponentRenderer<TPropIn> extends ComponentRendererB
}

protected renderOpeningElement(tagName: string): JsxOpeningElement {
const propsArray = Object.entries(this.component.properties)
const attributes = Object.entries(this.component.properties)
// value should be child of Text, not a prop
.filter(([key]) => !(this.component.componentType === 'Text' && key === 'value'))
.map(([key, value]) => buildOpeningElementAttributes(value, key));

this.addPropsSpreadAttributes(propsArray);
if ('events' in this.component && this.component.events !== undefined) {
attributes.push(
...Object.entries(this.component.events).map(([key, value]) => buildOpeningElementActions(key, value)),
);
}

this.addPropsSpreadAttributes(attributes);

return factory.createJsxOpeningElement(
factory.createIdentifier(tagName),
undefined,
factory.createJsxAttributes(propsArray),
factory.createJsxAttributes(attributes),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { ComponentWithChildrenRendererBase, StudioNode } from '@amzn/studio-ui-c
import { StudioComponent, StudioComponentChild } from '@amzn/amplify-ui-codegen-schema';
import { JsxAttributeLike, JsxElement, JsxChild, JsxOpeningElement, SyntaxKind, Expression, factory } from 'typescript';
import { ImportCollection } from './import-collection';
import { addBindingPropertiesImports, buildOpeningElementAttributes } from './react-component-render-helper';
import {
addBindingPropertiesImports,
buildOpeningElementAttributes,
buildOpeningElementActions,
} from './react-component-render-helper';

export abstract class ReactComponentWithChildrenRenderer<TPropIn> extends ComponentWithChildrenRendererBase<
TPropIn,
Expand All @@ -19,30 +23,42 @@ export abstract class ReactComponentWithChildrenRenderer<TPropIn> extends Compon
}

protected renderCustomCompOpeningElement(tagName: string): JsxOpeningElement {
const propsArray = Object.entries(this.component.properties).map(([key, value]) =>
const attributes = Object.entries(this.component.properties).map(([key, value]) =>
buildOpeningElementAttributes(value, key),
);

this.addFindChildOverrideAttribute(propsArray, tagName);
if ('events' in this.component && this.component.events !== undefined) {
attributes.push(
...Object.entries(this.component.events).map(([key, value]) => buildOpeningElementActions(key, value)),
);
}

this.addFindChildOverrideAttribute(attributes, tagName);

return factory.createJsxOpeningElement(
factory.createIdentifier(tagName),
undefined,
factory.createJsxAttributes(propsArray),
factory.createJsxAttributes(attributes),
);
}

protected renderOpeningElement(tagName: string): JsxOpeningElement {
const propsArray = Object.entries(this.component.properties).map(([key, value]) =>
const attributes = Object.entries(this.component.properties).map(([key, value]) =>
buildOpeningElementAttributes(value, key),
);

this.addPropsSpreadAttributes(propsArray);
if ('events' in this.component && this.component.events !== undefined) {
attributes.push(
...Object.entries(this.component.events).map(([key, value]) => buildOpeningElementActions(key, value)),
);
}

this.addPropsSpreadAttributes(attributes);

return factory.createJsxOpeningElement(
factory.createIdentifier(tagName),
undefined,
factory.createJsxAttributes(propsArray),
factory.createJsxAttributes(attributes),
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import ts, { createPrinter, createSourceFile, NewLineKind, transpileModule, createProgram } from 'typescript';
import ts, {
createPrinter,
createSourceFile,
NewLineKind,
transpileModule,
factory,
StringLiteral,
NumericLiteral,
BooleanLiteral,
NullLiteral,
ArrayLiteralExpression,
ObjectLiteralExpression,
createProgram,
} from 'typescript';
import prettier from 'prettier';
import parserBabel from 'prettier/parser-babel';
import tmp, { FileResult, DirResult } from 'tmp';
Expand Down Expand Up @@ -90,3 +103,40 @@ export function buildPrinter(fileName: string, renderConfig: ReactRenderConfig)
export function getDeclarationFilename(filename: string): string {
return `${path.basename(filename, '.tsx')}.d.ts`;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export type json = string | number | boolean | null | json[] | { [key: string]: json };

// eslint-disable-next-line consistent-return
export function jsonToLiteral(
jsonObject: json,
): ObjectLiteralExpression | StringLiteral | NumericLiteral | BooleanLiteral | NullLiteral | ArrayLiteralExpression {
if (jsonObject === null) {
return factory.createNull();
}
// eslint-disable-next-line default-case
switch (typeof jsonObject) {
case 'string':
return factory.createStringLiteral(jsonObject);
case 'number':
return factory.createNumericLiteral(jsonObject);
case 'boolean': {
if (jsonObject) {
return factory.createTrue();
}
return factory.createFalse();
}
case 'object': {
if (jsonObject instanceof Array) {
return factory.createArrayLiteralExpression(jsonObject.map(jsonToLiteral), false);
}
// else object
return factory.createObjectLiteralExpression(
Object.entries(jsonObject).map(([key, value]) =>
factory.createPropertyAssignment(factory.createIdentifier(key), jsonToLiteral(value)),
),
false,
);
}
}
}
Loading

0 comments on commit e6e60c0

Please sign in to comment.