Skip to content

Commit 42bf073

Browse files
authored
feat(playground): ability to open examples in Stackblitz (#2215)
1 parent f6fb9f1 commit 42bf073

21 files changed

+14631
-296
lines changed

Diff for: package-lock.json

+14,088-285
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@
3232
"dependencies": {
3333
"@docusaurus/core": "0.0.0-4192",
3434
"@docusaurus/mdx-loader": "0.0.0-4192",
35+
"@docusaurus/plugin-client-redirects": "0.0.0-4192",
3536
"@docusaurus/plugin-content-docs": "0.0.0-4192",
3637
"@docusaurus/plugin-content-pages": "0.0.0-4192",
37-
"@docusaurus/plugin-client-redirects": "0.0.0-4192",
3838
"@docusaurus/plugin-debug": "0.0.0-4192",
3939
"@docusaurus/plugin-google-analytics": "0.0.0-4192",
4040
"@docusaurus/plugin-google-gtag": "0.0.0-4192",
@@ -45,6 +45,7 @@
4545
"@ionic-internal/docusaurus-plugin-tag-manager": "^2.0.0",
4646
"@ionic-internal/ionic-ds": "^7.0.0",
4747
"@mdx-js/react": "^1.6.22",
48+
"@stackblitz/sdk": "^1.6.0",
4849
"clsx": "^1.1.1",
4950
"concurrently": "^6.2.0",
5051
"crowdin": "^3.5.0",

Diff for: src/components/global/Playground/index.tsx

+59-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
import React, { useEffect, useRef, useState } from 'react';
22

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

5-
enum Mode {
6-
iOS = 'ios',
7-
MD = 'md',
8-
}
9-
10-
type SupportedFrameworks = 'angular' | 'react' | 'vue' | 'javascript';
11-
12-
export default function Playground({ code }: { code: { [key in SupportedFrameworks]?: () => {} } }) {
7+
/**
8+
* @param code The code snippets for each supported framework target.
9+
* @param title Optional title of the generated playground example. Specify to customize the Stackblitz title.
10+
* @param description Optional description of the generated playground example. Specify to customize the Stackblitz description.
11+
*/
12+
export default function Playground({
13+
code,
14+
title,
15+
description,
16+
}: {
17+
code: { [key in SupportedFrameworks]?: () => {} };
18+
title?: string;
19+
description?: string;
20+
}) {
1321
if (!code || Object.keys(code).length === 0) {
1422
console.warn('No code usage examples provided for this Playground example.');
1523
return;
1624
}
1725
const codeRef = useRef(null);
1826

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

42+
function openEditor(event) {
43+
// TODO assign code block value based on active framework button and loaded code snippets
44+
const codeBlock = '';
45+
const editorOptions: EditorOptions = {
46+
title,
47+
description,
48+
};
49+
50+
switch (usageTarget) {
51+
case UsageTarget.Angular:
52+
openAngularEditor(codeBlock, editorOptions);
53+
break;
54+
case UsageTarget.Html:
55+
openHtmlEditor(codeBlock, editorOptions);
56+
break;
57+
case UsageTarget.React:
58+
openReactEditor(codeBlock, editorOptions);
59+
break;
60+
case UsageTarget.Vue:
61+
openVueEditor(codeBlock, editorOptions);
62+
break;
63+
}
64+
}
65+
3366
useEffect(() => {
3467
const codeSnippets = {};
3568
Object.keys(code).forEach((key) => {
@@ -108,7 +141,24 @@ export default function Playground({ code }: { code: { [key in SupportedFramewor
108141
<rect x="3" y="3" width="8" height="8" rx="1.5" stroke="current" />
109142
</svg>
110143
</button>
111-
{/* TODO FW-740: Open Stackblitz Button */}
144+
<button className="playground__icon-button playground__icon-button--primary" onClick={openEditor}>
145+
<svg
146+
aria-hidden="true"
147+
width="12"
148+
height="12"
149+
viewBox="0 0 12 12"
150+
fill="none"
151+
xmlns="http://www.w3.org/2000/svg"
152+
>
153+
<path d="M6 11L11 11" stroke="#92A0B3" strokeLinecap="round" strokeLinejoin="round" />
154+
<path
155+
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"
156+
stroke="current"
157+
stroke-linecap="round"
158+
stroke-linejoin="round"
159+
/>
160+
</svg>
161+
</button>
112162
</div>
113163
</div>
114164
<div className="playground__preview">{/* TODO FW-743: iframe Preview */}</div>

Diff for: src/components/global/Playground/playground.types.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export enum UsageTarget {
2+
Html = 'Basic',
3+
Angular = 'Angular',
4+
React = 'React',
5+
Vue = 'Vue',
6+
}
7+
8+
export const UsageTargetList = Object.keys(UsageTarget);
9+
10+
export enum Mode {
11+
iOS = 'ios',
12+
MD = 'md',
13+
}
14+
15+
export type SupportedFrameworks = 'angular' | 'react' | 'vue' | 'javascript';

Diff for: src/components/global/Playground/stackblitz.utils.ts

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import sdk from '@stackblitz/sdk';
2+
3+
// The default title to use for Stackblitz examples (when not overwritten)
4+
const DEFAULT_EDITOR_TITLE = 'Ionic Docs Example';
5+
// The default description to use for Stackblitz examples (when not overwritten)
6+
const DEFAULT_EDITOR_DESCRIPTION = '';
7+
// Default package version to use for all @ionic/* packages.
8+
const DEFAULT_IONIC_VERSION = '^6.0.0';
9+
10+
export interface EditorOptions {
11+
/**
12+
* The title of the Stackblitz example.
13+
*/
14+
title?: string;
15+
/**
16+
* The description of the Stackblitz example.
17+
*/
18+
description?: string;
19+
}
20+
21+
const loadSourceFiles = async (files: string[]) => {
22+
const sourceFiles = await Promise.all(files.map(f => fetch(`/docs/code/stackblitz/${f}`)));
23+
return (await Promise.all(sourceFiles.map(res => res.text())));
24+
}
25+
26+
const openHtmlEditor = async (code: string, options?: EditorOptions) => {
27+
const [index_ts, index_html] = await loadSourceFiles([
28+
'html/index.ts',
29+
'html/index.html',
30+
]);
31+
32+
sdk.openProject({
33+
template: 'typescript',
34+
title: options?.title ?? DEFAULT_EDITOR_TITLE,
35+
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
36+
files: {
37+
// Injects our code sample into the body of the HTML document
38+
'index.html': index_html.replace(/<body><\/body>/g, `<body>\n` + code + '</body>'),
39+
'index.ts': index_ts,
40+
},
41+
dependencies: {
42+
'@ionic/core': DEFAULT_IONIC_VERSION,
43+
},
44+
})
45+
}
46+
47+
const openAngularEditor = async (code: string, options?: EditorOptions) => {
48+
const [main_ts, app_module_ts, app_component_ts, styles_css, angular_json] = await loadSourceFiles([
49+
'angular/main.ts',
50+
'angular/app.module.ts',
51+
'angular/app.component.ts',
52+
'angular/styles.css',
53+
'angular/angular.json'
54+
])
55+
56+
sdk.openProject({
57+
template: 'angular-cli',
58+
title: options?.title ?? DEFAULT_EDITOR_TITLE,
59+
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
60+
files: {
61+
'src/main.ts': main_ts,
62+
'src/polyfills.ts': `import 'zone.js/dist/zone';`,
63+
'src/app/app.module.ts': app_module_ts,
64+
'src/app/app.component.ts': app_component_ts,
65+
'src/app/app.component.html': code,
66+
'src/index.html': '<app-root></app-root>',
67+
'src/styles.css': styles_css,
68+
'angular.json': angular_json,
69+
},
70+
dependencies: {
71+
'@ionic/angular': DEFAULT_IONIC_VERSION,
72+
},
73+
});
74+
}
75+
76+
const openReactEditor = async (code: string, options?: EditorOptions) => {
77+
// Matches the name after `export default` to use as the component tag.
78+
let componentTagName;
79+
try {
80+
componentTagName = new RegExp(/function([\S\s]*?)\(/g).exec(code)[1].trim();
81+
} catch (e) {
82+
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;');
83+
}
84+
85+
if (!componentTagName) {
86+
return;
87+
}
88+
89+
const [index_js, app_tsx] = await loadSourceFiles([
90+
'react/index.js',
91+
'react/app.tsx'
92+
]);
93+
94+
const app_tsx_renamed = app_tsx
95+
// Inserts the component name from the sample into the <IonApp> tag.
96+
.replace(/<IonApp><\/IonApp>/g, `<IonApp><${componentTagName} /></IonApp>`)
97+
// Imports the component from our `main` example file.
98+
.replace(/setupIonicReact\(\);/g, `import ${componentTagName} from "./main";\n\n` + 'setupIonicReact();');
99+
100+
sdk.openProject({
101+
template: 'create-react-app',
102+
title: options?.title ?? DEFAULT_EDITOR_TITLE,
103+
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
104+
files: {
105+
'index.html': `<div id="root"></div>`,
106+
'index.js': index_js,
107+
'App.js': app_tsx_renamed,
108+
'main.js': code,
109+
},
110+
dependencies: {
111+
react: 'latest',
112+
'react-dom': 'latest',
113+
'@ionic/react': DEFAULT_IONIC_VERSION,
114+
// Stackblitz requires this dependency to run
115+
'@stencil/core': '^2.13.0',
116+
},
117+
})
118+
}
119+
120+
const openVueEditor = async (code: string, options?: EditorOptions) => {
121+
const [package_json, index_html, vite_config_js, main_js, app_vue] = await loadSourceFiles([
122+
'vue/package.json',
123+
'vue/index.html',
124+
'vue/vite.config.js',
125+
'vue/main.js',
126+
'vue/App.vue'
127+
]);
128+
/**
129+
* We have to use Stackblitz web containers here (node template), due
130+
* to multiple issues with Vite, Vue/Vue Router and Vue 3's script setup.
131+
*
132+
* https://github.com/stackblitz/core/issues/1308
133+
*/
134+
sdk.openProject({
135+
template: 'node',
136+
title: options?.title ?? DEFAULT_EDITOR_TITLE,
137+
description: options?.description ?? DEFAULT_EDITOR_DESCRIPTION,
138+
files: {
139+
'src/App.vue': app_vue,
140+
'src/components/Example.vue': code,
141+
'src/main.js': main_js,
142+
'index.html': index_html,
143+
'vite.config.js': vite_config_js,
144+
'package.json': package_json,
145+
'.stackblitzrc': `{
146+
"startCommand": "yarn run dev"
147+
}`
148+
}
149+
});
150+
}
151+
152+
export { openAngularEditor, openHtmlEditor, openReactEditor, openVueEditor };

Diff for: static/code/stackblitz/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Stackblitz
2+
3+
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.
4+
5+
## Angular
6+
7+
| File | Description |
8+
| ------------------ | ------------------------------------------------------ |
9+
| `angular.json` | Main configuration file for any Angular application. |
10+
| `app.component.ts` | Primary component class/entry point. |
11+
| `app.module.ts` | Primary `AppModule`. Specifies required `IonicModule`. |
12+
| `main.ts` | Responsive for bootstrapping the main `AppModule`. |
13+
| `styles.css` | Ionic default styles |
14+
15+
## HTML
16+
17+
| File | Description |
18+
| ------------ | ----------------------------------------------------------------- |
19+
| `index.html` | Main template file with CDN link to latest `@ionic/core` release. |
20+
| `index.ts` | Defines the Stencil hydrated bundle for Ionic. |
21+
22+
## React
23+
24+
| File | Description |
25+
| ---------- | -------------------------------------------------------------------------------------------- |
26+
| `app.tsx` | Imports required Ionic styles and `setupIonicReact()` function to initialize web components. |
27+
| `index.js` | Boilerplate to render a React app. |
28+
29+
## Vue
30+
31+
| File | Description |
32+
| ---------------- | ------------------------------------------------------------- |
33+
| `App.vue` | Main Vue component that wraps each example in `ion-app`. |
34+
| `index.html` | The HTML template to create an element to mount Vue to. |
35+
| `main.js` | Initializes Ionic Vue and imports global styles. |
36+
| `package.json` | Project specific dependencies to create an example with Vite. |
37+
| `vite.config.js` | Vite configuration file. |

0 commit comments

Comments
 (0)