Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gf3 committed Jan 11, 2019
0 parents commit 30de59d
Show file tree
Hide file tree
Showing 109 changed files with 11,267 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
umd
*.js
*.js.map
*.d.ts

/node_modules/
/actions/
/client/
/umd/
/util/
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# API Extractor Issue

API Extractor is unable to parse out API for the `actions` portion of this package. See:

* [`src/index.ts`](src/index.ts)
* [`src/actions/index.ts`](src/actions/index.ts)

## To build

1. `yarn`
2. `yarn run build`
3. `yarn run api`
10 changes: 10 additions & 0 deletions api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/api-extractor.schema.json",
"compiler": {
"configType": "tsconfig",
"rootFolder": "."
},
"project": {
"entryPointSourceFile": "index.d.ts"
}
}
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "api-extractor-issue",
"version": "1.0.0",
"types": "index.d.ts",
"main": "index.js",
"files": [
"MessageTransport.d.ts",
"MessageTransport.js",
"MessageTransport.js.map",
"actions/",
"client/",
"index.d.ts",
"index.js",
"index.js.map",
"util/"
],
"private": true,
"repository": "[email protected]:gf3/api-extractor-issue.git",
"author": "Gianni Chiappetta <[email protected]>",
"license": "MIT",
"scripts": {
"api": "api-extractor run",
"docs": "api-documenter markdown",
"build": "NODE_ENV=production tsc"
},
"sideEffects": false,
"devDependencies": {
"@microsoft/api-documenter": "^7.0.13",
"@microsoft/api-extractor": "^7.0.9",
"@types/node": "^10.12.5",
"typescript": "3.2.1"
}
}
112 changes: 112 additions & 0 deletions src/MessageTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {AnyAction} from './actions/types';
import {fromAction, AppActionType} from './actions/Error';
import {isAppBridgeAction, isAppMessage} from './actions/validator';
import {TransportDispatch} from './client';
import {addAndRemoveFromCollection} from './util/collection';

/**
* @internal
*/
export type HandlerData = {data: AnyAction};

/**
* @internal
*/
export type Handler = (event: HandlerData) => void;

/**
* @internal
*/
export interface MessageTransport {
dispatch(message: any): void;
hostFrame: Window;
localOrigin: string;
subscribe(handler: Handler): () => void;
}

/**
* Create a MessageTransport from an IFrame.
* @remarks
* Used on the host-side to create a postMessage MessageTransport.
* @beta
*/
export function fromFrame(frame: HTMLIFrameElement, localOrigin: string): MessageTransport {
const handlers: Handler[] = [];

if (typeof frame === 'undefined' || !frame.ownerDocument || !frame.ownerDocument.defaultView) {
throw fromAction('App frame is undefined', AppActionType.WINDOW_UNDEFINED);
}

const parent = frame.ownerDocument.defaultView;

parent.addEventListener('message', event => {
if (event.origin !== localOrigin || !isAppMessage(event)) {
return;
}

for (const handler of handlers) {
handler(event);
}
});

return {
localOrigin,

hostFrame: parent,

dispatch(message) {
const contentWindow = frame.contentWindow;
if (contentWindow) {
contentWindow.postMessage(message, '*');
}
},

subscribe(handler) {
return addAndRemoveFromCollection(handlers, handler);
},
};
}

/**
* Create a MessageTransport from a parent window.
* @remarks
* Used on the client-side to create a postMessage MessageTransport.
* @beta
*/
export function fromWindow(contentWindow: Window, localOrigin: string): MessageTransport {
const handlers: Handler[] = [];
if (typeof window !== undefined && contentWindow !== window) {
window.addEventListener('message', event => {
if (
event.source !== contentWindow ||
!(isAppBridgeAction(event.data.payload) || isAppMessage(event))
) {
return;
}

for (const handler of handlers) {
handler(event);
}
});
}

return {
localOrigin,

hostFrame: contentWindow,

dispatch(message: TransportDispatch) {
if (!message.source || !message.source.shopOrigin) {
return;
}

const messageOrigin = `https://${message.source.shopOrigin}`;

contentWindow.postMessage(message, messageOrigin);
},

subscribe(handler) {
return addAndRemoveFromCollection(handlers, handler);
},
};
}
103 changes: 103 additions & 0 deletions src/actions/Button/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Button

## Setup

Create an app and import the `Button` module from `@shopify/app-bridge/actions`. Note that we'll be referring to this sample application throughout the examples below.

```js
import createApp from '@shopify/app-bridge';
import {Button} from '@shopify/app-bridge/actions';

const app = createApp({
apiKey: '12345',
});
```

## Create a button

Generate a primary button with the label `Save`:

```js
const myButton = Button.create(app, {label: 'Save'});
```

## Subscribe to click action

You can subscribe to button actions by calling `subscribe`. This returns a method that you can call to unsubscribe from the action:

```js
const myButton = Button.create(app, {label: 'Save'});
const clickUnsubscribe = myButton.subscribe(Button.Action.CLICK, data => {
// Do something with the click event
});

// Unsubscribe to click actions
clickUnsubscribe();
```

## Dispatch click action

```js
const myButton = Button.create(app, {label: 'Save'});
myButton.dispatch(Button.Action.CLICK);
```

## Dispatch click action with a payload

```js
const myButton = Button.create(app, {label: 'Save'});
// Trigger the action with a payload
myButton.dispatch(Button.Action.CLICK, {message: 'Saved'});

// Subscribe to the action and read the payload
myButton.subscribe(Button.Action.CLICK, data => {
// data = { payload: { message: 'Saved'} }
console.log(`Received ${data.payload.message} message`);
});
```

## Attach buttons to a modal

You can attach buttons to other actions such as modals. To learn more about modals, see [Modal](../Modal).

```js
const okButton = Button.create(app, {label: 'Ok'});
const cancelButton = Button.create(app, {label: 'Cancel'});
const modalOptions = {
title: 'My Modal',
message: 'Hello world!',
footer: {primary: okButton, secondary: [cancelButton]},
};

const myModal = Modal.create(app, modalOptions);
```

## Button Style

You can change the style of the button by passing the `style` property. Buttons support a single alternate style, the `Danger` style:

```js
const myButton = Button.create(app, {label: 'Delete', style: Button.Style.Danger});
```

## Update options

You can call the `set` method with partial button options to update the options of an existing button. This automatically triggers the `update` action on the button and merges the new given options with existing options:

```js
const myButton = Button.create(app, {label: 'Save'});
myButton.set({disabled: true});
```

## Unsubscribe

You call `unsubscribe` to remove all current subscriptions on the button:

```js
const myButton = Button.create(app, {label: 'Save'});
myButton.subscribe(Button.Action.CLICK, data => {
// Do something with the click event
});

myButton.unsubscribe();
```
113 changes: 113 additions & 0 deletions src/actions/Button/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @module Button
*/

import {ClientApplication} from '../../client';
import {actionWrapper, getEventNameSpace, getMergedProps, ActionSet} from '../helper';
import {ActionSetProps, ClickAction, Component, ComponentType, Group, MetaAction} from '../types';
import {Action, ClickPayload, Icon, Options, Payload, Style} from './types';

export interface ButtonUpdateAction extends MetaAction {
readonly group: string;
payload: Payload;
}

export type ButtonAction = ButtonUpdateAction | ClickAction | MetaAction;

export function clickButton(
group: string,
component: Component,
payload?: ClickPayload,
): ClickAction {
const {id} = component;
const action = getEventNameSpace(group, Action.CLICK, component);
const buttonPayload: ClickPayload = {
id,
payload,
};

return actionWrapper({type: action, group, payload: buttonPayload});
}

export function update(group: string, component: Component, props: Payload): ButtonUpdateAction {
const {id} = component;
const {label} = props;
const action = getEventNameSpace(group, Action.UPDATE, component);
const buttonPayload: Payload = {
id,
label,
...props,
};

return actionWrapper({type: action, group, payload: buttonPayload});
}

export function isValidButtonProps(button: Payload) {
return typeof button.id === 'string' && typeof button.label === 'string';
}

export class Button extends ActionSet implements ActionSetProps<Options, Payload> {
label!: string;
disabled = false;
icon?: Icon;
style?: Style;

constructor(app: ClientApplication<any>, options: Options) {
super(app, ComponentType.Button, Group.Button);
this.set(options, false);
}

get options(): Options {
return {
disabled: this.disabled,
icon: this.icon,
label: this.label,
style: this.style,
};
}

get payload(): Payload {
return {
id: this.id,
...this.options,
};
}

set(options: Partial<Options>, shouldUpdate = true) {
const mergedOptions = getMergedProps(this.options, options);
const {label, disabled, icon, style} = mergedOptions;

this.label = label;
this.disabled = !!disabled;
this.icon = icon;
this.style = style;

if (shouldUpdate) {
this.dispatch(Action.UPDATE);
}

return this;
}

dispatch(action: Action.UPDATE): ActionSet;

dispatch(action: Action.CLICK, payload?: any): ActionSet;

dispatch(action: Action, payload?: any) {
switch (action) {
case Action.CLICK:
this.app.dispatch(clickButton(this.group, this.component, payload));
break;
case Action.UPDATE:
const updateAction = update(this.group, this.component, this.payload);
this.app.dispatch(updateAction);
break;
}

return this;
}
}

export function create(app: ClientApplication<any>, options: Options) {
return new Button(app, options);
}
Loading

0 comments on commit 30de59d

Please sign in to comment.