Skip to content

Commit

Permalink
feat(playground): ability to open examples in Stackblitz (#2215)
Browse files Browse the repository at this point in the history
  • Loading branch information
sean-perkins committed Mar 14, 2022
1 parent f6fb9f1 commit 42bf073
Show file tree
Hide file tree
Showing 21 changed files with 14,631 additions and 296 deletions.
14,373 changes: 14,088 additions & 285 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
"dependencies": {
"@docusaurus/core": "0.0.0-4192",
"@docusaurus/mdx-loader": "0.0.0-4192",
"@docusaurus/plugin-client-redirects": "0.0.0-4192",
"@docusaurus/plugin-content-docs": "0.0.0-4192",
"@docusaurus/plugin-content-pages": "0.0.0-4192",
"@docusaurus/plugin-client-redirects": "0.0.0-4192",
"@docusaurus/plugin-debug": "0.0.0-4192",
"@docusaurus/plugin-google-analytics": "0.0.0-4192",
"@docusaurus/plugin-google-gtag": "0.0.0-4192",
Expand All @@ -45,6 +45,7 @@
"@ionic-internal/docusaurus-plugin-tag-manager": "^2.0.0",
"@ionic-internal/ionic-ds": "^7.0.0",
"@mdx-js/react": "^1.6.22",
"@stackblitz/sdk": "^1.6.0",
"clsx": "^1.1.1",
"concurrently": "^6.2.0",
"crowdin": "^3.5.0",
Expand Down
68 changes: 59 additions & 9 deletions src/components/global/Playground/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import React, { useEffect, useRef, useState } from 'react';

import './playground.css';
import { EditorOptions, openAngularEditor, openHtmlEditor, openReactEditor, openVueEditor } from './stackblitz.utils';
import { Mode, SupportedFrameworks, UsageTarget } from './playground.types';

enum Mode {
iOS = 'ios',
MD = 'md',
}

type SupportedFrameworks = 'angular' | 'react' | 'vue' | 'javascript';

export default function Playground({ code }: { code: { [key in SupportedFrameworks]?: () => {} } }) {
/**
* @param code The code snippets for each supported framework target.
* @param title Optional title of the generated playground example. Specify to customize the Stackblitz title.
* @param description Optional description of the generated playground example. Specify to customize the Stackblitz description.
*/
export default function Playground({
code,
title,
description,
}: {
code: { [key in SupportedFrameworks]?: () => {} };
title?: string;
description?: string;
}) {
if (!code || Object.keys(code).length === 0) {
console.warn('No code usage examples provided for this Playground example.');
return;
}
const codeRef = useRef(null);

const [usageTarget, setUsageTarget] = useState(UsageTarget.Html);
const [mode, setMode] = useState(Mode.iOS);
const [codeExpanded, setCodeExpanded] = useState(false);
const [codeSnippets, setCodeSnippets] = useState({});
Expand All @@ -30,6 +39,30 @@ export default function Playground({ code }: { code: { [key in SupportedFramewor
copyButton.click();
}

function openEditor(event) {
// TODO assign code block value based on active framework button and loaded code snippets
const codeBlock = '';
const editorOptions: EditorOptions = {
title,
description,
};

switch (usageTarget) {
case UsageTarget.Angular:
openAngularEditor(codeBlock, editorOptions);
break;
case UsageTarget.Html:
openHtmlEditor(codeBlock, editorOptions);
break;
case UsageTarget.React:
openReactEditor(codeBlock, editorOptions);
break;
case UsageTarget.Vue:
openVueEditor(codeBlock, editorOptions);
break;
}
}

useEffect(() => {
const codeSnippets = {};
Object.keys(code).forEach((key) => {
Expand Down Expand Up @@ -108,7 +141,24 @@ export default function Playground({ code }: { code: { [key in SupportedFramewor
<rect x="3" y="3" width="8" height="8" rx="1.5" stroke="current" />
</svg>
</button>
{/* TODO FW-740: Open Stackblitz Button */}
<button className="playground__icon-button playground__icon-button--primary" onClick={openEditor}>
<svg
aria-hidden="true"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6 11L11 11" stroke="#92A0B3" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M8.88491 1.36289C9.11726 1.13054 9.43241 1 9.76101 1C9.92371 1 10.0848 1.03205 10.2351 1.09431C10.3855 1.15658 10.5221 1.24784 10.6371 1.36289C10.7522 1.47794 10.8434 1.61453 10.9057 1.76485C10.968 1.91517 11 2.07629 11 2.23899C11 2.4017 10.968 2.56281 10.9057 2.71314C10.8434 2.86346 10.7522 3.00004 10.6371 3.11509L3.33627 10.4159L1 11L1.58407 8.66373L8.88491 1.36289Z"
stroke="current"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</div>
<div className="playground__preview">{/* TODO FW-743: iframe Preview */}</div>
Expand Down
15 changes: 15 additions & 0 deletions src/components/global/Playground/playground.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum UsageTarget {
Html = 'Basic',
Angular = 'Angular',
React = 'React',
Vue = 'Vue',
}

export const UsageTargetList = Object.keys(UsageTarget);

export enum Mode {
iOS = 'ios',
MD = 'md',
}

export type SupportedFrameworks = 'angular' | 'react' | 'vue' | 'javascript';
152 changes: 152 additions & 0 deletions src/components/global/Playground/stackblitz.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import sdk from '@stackblitz/sdk';

// The default title to use for Stackblitz examples (when not overwritten)
const DEFAULT_EDITOR_TITLE = 'Ionic Docs Example';
// The default description to use for Stackblitz examples (when not overwritten)
const DEFAULT_EDITOR_DESCRIPTION = '';
// Default package version to use for all @ionic/* packages.
const DEFAULT_IONIC_VERSION = '^6.0.0';

export interface EditorOptions {
/**
* The title of the Stackblitz example.
*/
title?: string;
/**
* The description of the Stackblitz example.
*/
description?: string;
}

const loadSourceFiles = async (files: string[]) => {
const sourceFiles = await Promise.all(files.map(f => fetch(`/docs/code/stackblitz/${f}`)));
return (await Promise.all(sourceFiles.map(res => res.text())));
}

const openHtmlEditor = async (code: string, options?: EditorOptions) => {
const [index_ts, index_html] = await loadSourceFiles([
'html/index.ts',
'html/index.html',
]);

sdk.openProject({
template: 'typescript',
title: options?.title ?? DEFAULT_EDITOR_TITLE,
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
files: {
// Injects our code sample into the body of the HTML document
'index.html': index_html.replace(/<body><\/body>/g, `<body>\n` + code + '</body>'),
'index.ts': index_ts,
},
dependencies: {
'@ionic/core': DEFAULT_IONIC_VERSION,
},
})
}

const openAngularEditor = async (code: string, options?: EditorOptions) => {
const [main_ts, app_module_ts, app_component_ts, styles_css, angular_json] = await loadSourceFiles([
'angular/main.ts',
'angular/app.module.ts',
'angular/app.component.ts',
'angular/styles.css',
'angular/angular.json'
])

sdk.openProject({
template: 'angular-cli',
title: options?.title ?? DEFAULT_EDITOR_TITLE,
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
files: {
'src/main.ts': main_ts,
'src/polyfills.ts': `import 'zone.js/dist/zone';`,
'src/app/app.module.ts': app_module_ts,
'src/app/app.component.ts': app_component_ts,
'src/app/app.component.html': code,
'src/index.html': '<app-root></app-root>',
'src/styles.css': styles_css,
'angular.json': angular_json,
},
dependencies: {
'@ionic/angular': DEFAULT_IONIC_VERSION,
},
});
}

const openReactEditor = async (code: string, options?: EditorOptions) => {
// Matches the name after `export default` to use as the component tag.
let componentTagName;
try {
componentTagName = new RegExp(/function([\S\s]*?)\(/g).exec(code)[1].trim();
} catch (e) {
console.error('Error parsing the component tag name from the React code snippet. Please make sure that the code snippet for React ends with export default ComponentName;');
}

if (!componentTagName) {
return;
}

const [index_js, app_tsx] = await loadSourceFiles([
'react/index.js',
'react/app.tsx'
]);

const app_tsx_renamed = app_tsx
// Inserts the component name from the sample into the <IonApp> tag.
.replace(/<IonApp><\/IonApp>/g, `<IonApp><${componentTagName} /></IonApp>`)
// Imports the component from our `main` example file.
.replace(/setupIonicReact\(\);/g, `import ${componentTagName} from "./main";\n\n` + 'setupIonicReact();');

sdk.openProject({
template: 'create-react-app',
title: options?.title ?? DEFAULT_EDITOR_TITLE,
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
files: {
'index.html': `<div id="root"></div>`,
'index.js': index_js,
'App.js': app_tsx_renamed,
'main.js': code,
},
dependencies: {
react: 'latest',
'react-dom': 'latest',
'@ionic/react': DEFAULT_IONIC_VERSION,
// Stackblitz requires this dependency to run
'@stencil/core': '^2.13.0',
},
})
}

const openVueEditor = async (code: string, options?: EditorOptions) => {
const [package_json, index_html, vite_config_js, main_js, app_vue] = await loadSourceFiles([
'vue/package.json',
'vue/index.html',
'vue/vite.config.js',
'vue/main.js',
'vue/App.vue'
]);
/**
* We have to use Stackblitz web containers here (node template), due
* to multiple issues with Vite, Vue/Vue Router and Vue 3's script setup.
*
* https://github.com/stackblitz/core/issues/1308
*/
sdk.openProject({
template: 'node',
title: options?.title ?? DEFAULT_EDITOR_TITLE,
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
files: {
'src/App.vue': app_vue,
'src/components/Example.vue': code,
'src/main.js': main_js,
'index.html': index_html,
'vite.config.js': vite_config_js,
'package.json': package_json,
'.stackblitzrc': `{
"startCommand": "yarn run dev"
}`
}
});
}

export { openAngularEditor, openHtmlEditor, openReactEditor, openVueEditor };
37 changes: 37 additions & 0 deletions static/code/stackblitz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Stackblitz

This directory contains the source files for generating the individual framework targets for a playground examples. The contents of the files will be loaded and injected into the Stackblitz example that is opened from the Playground.

## Angular

| File | Description |
| ------------------ | ------------------------------------------------------ |
| `angular.json` | Main configuration file for any Angular application. |
| `app.component.ts` | Primary component class/entry point. |
| `app.module.ts` | Primary `AppModule`. Specifies required `IonicModule`. |
| `main.ts` | Responsive for bootstrapping the main `AppModule`. |
| `styles.css` | Ionic default styles |

## HTML

| File | Description |
| ------------ | ----------------------------------------------------------------- |
| `index.html` | Main template file with CDN link to latest `@ionic/core` release. |
| `index.ts` | Defines the Stencil hydrated bundle for Ionic. |

## React

| File | Description |
| ---------- | -------------------------------------------------------------------------------------------- |
| `app.tsx` | Imports required Ionic styles and `setupIonicReact()` function to initialize web components. |
| `index.js` | Boilerplate to render a React app. |

## Vue

| File | Description |
| ---------------- | ------------------------------------------------------------- |
| `App.vue` | Main Vue component that wraps each example in `ion-app`. |
| `index.html` | The HTML template to create an element to mount Vue to. |
| `main.js` | Initializes Ionic Vue and imports global styles. |
| `package.json` | Project specific dependencies to create an example with Vite. |
| `vite.config.js` | Vite configuration file. |
Loading

0 comments on commit 42bf073

Please sign in to comment.