Skip to content

Commit dcba5c2

Browse files
feat(playground): add prop to display console messages from demo (#3060)
1 parent 95eaf9f commit dcba5c2

File tree

11 files changed

+251
-34
lines changed

11 files changed

+251
-34
lines changed

src/components/global/Playground/index.tsx

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
33
import useBaseUrl from '@docusaurus/useBaseUrl';
44
import './playground.css';
55
import { EditorOptions, openAngularEditor, openHtmlEditor, openReactEditor, openVueEditor } from './stackblitz.utils';
6-
import { Mode, UsageTarget } from './playground.types';
6+
import { ConsoleItem, Mode, UsageTarget } from './playground.types';
77
import useThemeContext from '@theme/hooks/useThemeContext';
88

99
import Tippy from '@tippyjs/react';
@@ -109,6 +109,7 @@ interface UsageTargetOptions {
109109
* @param src The absolute path to the playground demo. For example: `/usage/button/basic/demo.html`
110110
* @param size The height of the playground. Supports `xsmall`, `small`, `medium`, `large`, 'xlarge' or any string value.
111111
* @param devicePreview `true` if the playground example should render in a device frame (iOS/MD).
112+
* @param showConsole `true` if the playground should render a console UI that reflects console logs, warnings, and errors.
112113
*/
113114
export default function Playground({
114115
code,
@@ -118,6 +119,7 @@ export default function Playground({
118119
size = 'small',
119120
mode,
120121
devicePreview,
122+
showConsole,
121123
includeIonContent = true,
122124
version,
123125
}: {
@@ -133,6 +135,7 @@ export default function Playground({
133135
mode?: 'ios' | 'md';
134136
description?: string;
135137
devicePreview?: boolean;
138+
showConsole?: boolean;
136139
includeIonContent: boolean;
137140
/**
138141
* The major version of Ionic to use in the generated Stackblitz examples.
@@ -159,6 +162,7 @@ export default function Playground({
159162
const codeRef = useRef(null);
160163
const frameiOS = useRef<HTMLIFrameElement | null>(null);
161164
const frameMD = useRef<HTMLIFrameElement | null>(null);
165+
const consoleBodyRef = useRef<HTMLDivElement | null>(null);
162166

163167
const defaultMode = typeof mode !== 'undefined' ? mode : Mode.iOS;
164168

@@ -182,6 +186,15 @@ export default function Playground({
182186
const [codeSnippets, setCodeSnippets] = useState({});
183187
const [renderIframes, setRenderIframes] = useState(false);
184188
const [iframesLoaded, setIframesLoaded] = useState(false);
189+
const [mdConsoleItems, setMDConsoleItems] = useState<ConsoleItem[]>([]);
190+
const [iosConsoleItems, setiOSConsoleItems] = useState<ConsoleItem[]>([]);
191+
192+
/**
193+
* We don't actually care about the count, but this lets us
194+
* re-trigger useEffect hooks when the demo is reset and the
195+
* iframes are refreshed.
196+
*/
197+
const [resetCount, setResetCount] = useState(0);
185198

186199
/**
187200
* Rather than encode isDarkTheme into the frame source
@@ -258,6 +271,24 @@ export default function Playground({
258271
setFramesLoaded();
259272
}, [renderIframes]);
260273

274+
useEffect(() => {
275+
if (showConsole) {
276+
if (frameiOS.current) {
277+
frameiOS.current.contentWindow.addEventListener('console', (ev: CustomEvent) => {
278+
setiOSConsoleItems((oldConsoleItems) => [...oldConsoleItems, ev.detail]);
279+
consoleBodyRef.current.scrollTo(0, consoleBodyRef.current.scrollHeight);
280+
});
281+
}
282+
283+
if (frameMD.current) {
284+
frameMD.current.contentWindow.addEventListener('console', (ev: CustomEvent) => {
285+
setMDConsoleItems((oldConsoleItems) => [...oldConsoleItems, ev.detail]);
286+
consoleBodyRef.current.scrollTo(0, consoleBodyRef.current.scrollHeight);
287+
});
288+
}
289+
}
290+
}, [iframesLoaded, resetCount]); // including resetCount re-runs this when iframes are reloaded
291+
261292
useEffect(() => {
262293
/**
263294
* Using a dynamic import here to avoid SSR errors when trying to extend `HTMLElement`
@@ -311,13 +342,19 @@ export default function Playground({
311342
/**
312343
* Reloads the iOS and MD iframe sources back to their original state.
313344
*/
314-
function resetDemo() {
345+
async function resetDemo() {
315346
if (frameiOS.current) {
316347
frameiOS.current.contentWindow.location.reload();
317348
}
318349
if (frameMD.current) {
319350
frameMD.current.contentWindow.location.reload();
320351
}
352+
353+
setiOSConsoleItems([]);
354+
setMDConsoleItems([]);
355+
356+
await Promise.all([waitForNextFrameLoadEvent(frameiOS.current), waitForNextFrameLoadEvent(frameMD.current)]);
357+
setResetCount((oldCount) => oldCount + 1);
321358
}
322359

323360
function openEditor(event) {
@@ -444,11 +481,39 @@ export default function Playground({
444481
);
445482
}
446483

484+
function renderConsole() {
485+
const consoleItems = ionicMode === Mode.iOS ? iosConsoleItems : mdConsoleItems;
486+
487+
return (
488+
<div className="playground__console">
489+
<div className="playground__console-header">
490+
<code>Console</code>
491+
</div>
492+
<div className="playground__console-body" ref={consoleBodyRef}>
493+
{consoleItems.length === 0 ? (
494+
<div className="playground__console-item playground__console-item--placeholder">
495+
<code>Console messages will appear here when logged from the example above.</code>
496+
</div>
497+
) : (
498+
consoleItems.map((consoleItem, i) => (
499+
<div key={i} className={`playground__console-item playground__console-item--${consoleItem.type}`}>
500+
{consoleItem.type !== 'log' && (
501+
<div className="playground__console-icon">{consoleItem.type === 'warning' ? '⚠' : '❌'}</div>
502+
)}
503+
<code>{consoleItem.message}</code>
504+
</div>
505+
))
506+
)}
507+
</div>
508+
</div>
509+
);
510+
}
511+
447512
const sortedUsageTargets = useMemo(() => Object.keys(UsageTarget).sort(), []);
448513

449514
return (
450515
<div className="playground" ref={hostRef}>
451-
<div className="playground__container">
516+
<div className={`playground__container ${showConsole ? 'playground__container--has-console' : ''}`}>
452517
<div className="playground__control-toolbar">
453518
<div className="playground__control-group">
454519
{sortedUsageTargets.map((lang) => {
@@ -633,6 +698,7 @@ export default function Playground({
633698
]
634699
: []}
635700
</div>
701+
{showConsole && renderConsole()}
636702
<div ref={codeRef} className="playground__code-block">
637703
{renderCodeSnippets()}
638704
</div>
@@ -660,6 +726,26 @@ const waitForFrame = (frame: HTMLIFrameElement) => {
660726
});
661727
};
662728

729+
/**
730+
* Returns a promise that resolves on the *next* load event of the
731+
* given iframe. We intentionally don't check if it's already loaded
732+
* because this is used when the demo is reset and the iframe is
733+
* refreshed, so we don't want to return too early and catch the
734+
* pre-reset version of the window.
735+
*/
736+
const waitForNextFrameLoadEvent = (frame: HTMLIFrameElement) => {
737+
return new Promise<void>((resolve) => {
738+
const handleLoad = () => {
739+
frame.removeEventListener('load', handleLoad);
740+
resolve();
741+
};
742+
743+
if (frame) {
744+
frame.addEventListener('load', handleLoad);
745+
}
746+
});
747+
};
748+
663749
const isFrameReady = (frame: HTMLIFrameElement) => {
664750
if (!frame) {
665751
return false;

src/components/global/Playground/playground.css

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
--playground-tabs-background: var(--c-carbon-90);
1515
--playground-tab-btn-color: var(--c-carbon-20);
1616
--playground-tab-btn-border-color: transparent;
17+
18+
--playground-console-item-separator-color: var(--c-carbon-80);
19+
--playground-console-warning-background: #332B00;
20+
--playground-console-warning-color: var(--c-yellow-80);
21+
--playground-console-warning-separator-color: #665500;
22+
--playground-console-error-background: #290000;
23+
--playground-console-error-color: var(--c-red-40);
24+
--playground-console-error-separator-color: #5C0000;
1725
}
1826

1927
.playground {
@@ -28,6 +36,13 @@
2836
* @prop --playground-tabs-background: The background color of the tabs bar not including the active tab button.
2937
* @prop --playground-tab-btn-color: The text color of the tab buttons.
3038
* @prop --playground-tab-btn-border-color: The border color of the tab buttons.
39+
* @prop --playground-console-item-separator-color The color of the separator/border between console UI items.
40+
* @prop --playground-console-warning-background The background color of warning items in the console UI.
41+
* @prop --playground-console-warning-color The text color of warning items in the console UI.
42+
* @prop --playground-console-warning-separator-color The color of the top and bottom separator/border for warning items in the console UI.
43+
* @prop --playground-console-error-background The background color of error items in the console UI.
44+
* @prop --playground-console-error-color The text color of error items in the console UI.
45+
* @prop --playground-console-error-separator-color The color of the top and bottom separator/border for error items in the console UI.
3146
*/
3247
--playground-btn-color: var(--c-indigo-90);
3348
--playground-btn-selected-color: var(--c-blue-90);
@@ -41,6 +56,14 @@
4156
--playground-tab-btn-color: var(--c-carbon-100);
4257
--playground-tab-btn-border-color: var(--c-indigo-30);
4358

59+
--playground-console-item-separator-color: var(--c-indigo-20);
60+
--playground-console-warning-background: var(--c-yellow-10);
61+
--playground-console-warning-color: #5C3C00;
62+
--playground-console-warning-separator-color: var(--c-yellow-30);
63+
--playground-console-error-background: var(--c-red-10);
64+
--playground-console-error-color: var(--c-red-90);
65+
--playground-console-error-separator-color: var(--c-red-30);
66+
4467
overflow: hidden;
4568

4669
margin-bottom: var(--ifm-leading);
@@ -52,6 +75,11 @@
5275
border-radius: var(--ifm-code-border-radius);
5376
}
5477

78+
.playground__container--has-console {
79+
border-bottom-left-radius: 0;
80+
border-bottom-right-radius: 0;
81+
}
82+
5583
/* Playground preview contains the demo example*/
5684
.playground__preview {
5785
display: flex;
@@ -213,6 +241,86 @@
213241
}
214242
}
215243

244+
.playground__console {
245+
background-color: var(--code-block-bg-c);
246+
border: 1px solid var(--playground-separator-color);
247+
border-top: 0;
248+
border-bottom-left-radius: var(--ifm-code-border-radius);
249+
border-bottom-right-radius: var(--ifm-code-border-radius);
250+
}
251+
252+
.playground__console-header {
253+
background-color: var(--playground-separator-color);
254+
font-weight: bold;
255+
text-transform: uppercase;
256+
}
257+
258+
.playground__console-body {
259+
overflow-y: auto;
260+
261+
height: 120px;
262+
}
263+
264+
.playground__console-item {
265+
border-top: 1px solid var(--separator-color);
266+
267+
position: relative;
268+
}
269+
270+
.playground__console-header, .playground__console-item {
271+
padding: 3px 3px 3px 28px;
272+
}
273+
274+
.playground__console-item:first-child {
275+
border-top: none;
276+
}
277+
278+
.playground__console-item:last-child {
279+
border-bottom: 1px solid var(--separator-color);
280+
}
281+
282+
.playground__console-item--placeholder {
283+
font-style: italic;
284+
}
285+
286+
.playground__console-item--log {
287+
--separator-color: var(--playground-console-item-separator-color);
288+
}
289+
290+
.playground__console-item--warning {
291+
--separator-color: var(--playground-console-warning-separator-color);
292+
background-color: var(--playground-console-warning-background);
293+
border-bottom: 1px solid var(--separator-color);
294+
color: var(--playground-console-warning-color);
295+
}
296+
297+
.playground__console-item--error {
298+
--separator-color: var(--playground-console-error-separator-color);
299+
background-color: var(--playground-console-error-background);
300+
border-bottom: 1px solid var(--separator-color);
301+
color: var(--playground-console-error-color);
302+
}
303+
304+
/* warnings and errors have both borders colored, so hide the extra from the neighboring item */
305+
.playground__console-item--warning + .playground__console-item,
306+
.playground__console-item--error + .playground__console-item {
307+
border-top: none;
308+
}
309+
310+
.playground__console-icon {
311+
position: absolute;
312+
top: 3px;
313+
left: 3px;
314+
}
315+
316+
.playground__console code {
317+
background-color: transparent;
318+
font-size: 0.813rem;
319+
padding: 0;
320+
padding-block-start: 0; /* prevents text getting cut off vertically */
321+
padding-block-end: 0; /* prevents border from item below getting covered up */
322+
}
323+
216324
/** Tabs **/
217325
.playground .tabs-container {
218326
background: var(--playground-code-background);

src/components/global/Playground/playground.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ export enum Mode {
99
iOS = 'ios',
1010
MD = 'md',
1111
}
12+
13+
export interface ConsoleItem {
14+
type: 'log' | 'warning' | 'error';
15+
message: string;
16+
}

static/usage/common.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,41 @@ window.addEventListener('DOMContentLoaded', () => {
2222
}
2323
});
2424

25+
/**
26+
* Monkey-patch the console methods so we can dispatch
27+
* events when they're called, allowing the data to be
28+
* captured by the playground's console UI.
29+
*/
30+
const _log = console.log,
31+
_warn = console.warn,
32+
_error = console.error;
33+
34+
const dispatchConsoleEvent = (type, arguments) => {
35+
window.dispatchEvent(
36+
new CustomEvent('console', {
37+
detail: {
38+
type,
39+
message: Object.values(arguments).join(' '),
40+
},
41+
})
42+
);
43+
};
44+
45+
console.log = function () {
46+
dispatchConsoleEvent('log', arguments);
47+
return _log.apply(console, arguments);
48+
};
49+
50+
console.warn = function () {
51+
dispatchConsoleEvent('warning', arguments);
52+
return _warn.apply(console, arguments);
53+
};
54+
55+
console.error = function () {
56+
dispatchConsoleEvent('error', arguments);
57+
return _error.apply(console, arguments);
58+
};
59+
2560
/**
2661
* The Playground needs to wait for the message listener
2762
* to be created before sending any messages, otherwise
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
```html
22
<ion-range aria-label="Range with ionChange" (ionChange)="onIonChange($event)"></ion-range>
3-
<ion-label>ionChange emitted value: {{ lastEmittedValue }}</ion-label>
43
```

static/usage/v7/range/ion-change-event/angular/example_component_ts.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22
import { Component } from '@angular/core';
33

44
import { RangeCustomEvent } from '@ionic/angular';
5-
import { RangeValue } from '@ionic/core';
65

76
@Component({
87
selector: 'app-example',
98
templateUrl: 'example.component.html',
109
})
1110
export class ExampleComponent {
12-
lastEmittedValue: RangeValue;
13-
1411
onIonChange(ev: Event) {
15-
this.lastEmittedValue = (ev as RangeCustomEvent).detail.value;
12+
console.log('ionChange emitted value:', (ev as RangeCustomEvent).detail.value);
1613
}
1714
}
1815
```

0 commit comments

Comments
 (0)