Skip to content

Commit 27c7e33

Browse files
committed
refactor: Finalize mount/unmount/update
1 parent 85f6ec0 commit 27c7e33

4 files changed

Lines changed: 106 additions & 102 deletions

File tree

src/components/code-editor/index.jsx

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState, useRef, useEffect } from 'preact/hooks';
1+
import { useRef, useEffect } from 'preact/hooks';
22
import { EditorView } from 'codemirror';
33
import { lineNumbers, keymap, highlightActiveLineGutter, highlightActiveLine } from '@codemirror/view';
4-
import { EditorState } from '@codemirror/state';
4+
import { EditorState, Transaction } from '@codemirror/state';
55
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
66
import { javascript } from '@codemirror/lang-javascript';
77
import { syntaxHighlighting, HighlightStyle, indentUnit, bracketMatching } from '@codemirror/language';
@@ -26,21 +26,39 @@ const highlightStyle = HighlightStyle.define([
2626
{ tag: tags.invalid, class: 'cm-invalid' }
2727
]);
2828

29+
/**
30+
* @param {object} props
31+
* @param {string} props.editorCode
32+
* @param {(value: string) => void} props.onInput
33+
* @param {string} props.slug
34+
* @param {string} [props.class]
35+
*/
2936
export default function CodeEditor(props) {
3037
const editorParent = useRef(null);
38+
/** @type {{ current: EditorView | null }} */
3139
const editor = useRef(null);
32-
// eslint-disable-next-line no-unused-vars
33-
const [_, setEditor] = useState(null);
40+
41+
const routeHasChanged = useRef(false);
42+
43+
useEffect(() => {
44+
if (props.slug || !editor.current) routeHasChanged.current = true;
45+
}, [props.slug]);
3446

3547
useEffect(() => {
36-
console.log('editor code:\n', props.value);
37-
if (editor.current && !props.baseExampleSlug) return;
38-
if (editor.current) editor.current.destroy();
48+
if (routeHasChanged.current === false) return;
49+
routeHasChanged.current = false;
50+
51+
if (editor.current) {
52+
editor.current.dispatch({
53+
changes: { from: 0, to: editor.current.state.doc.length, insert: props.editorCode }
54+
});
55+
return;
56+
}
3957

4058
const theme = EditorView.theme({}, { dark: true });
4159

4260
const state = EditorState.create({
43-
doc: props.value,
61+
doc: props.editorCode,
4462
extensions: [
4563
lineNumbers(),
4664
highlightActiveLine(),
@@ -54,8 +72,9 @@ export default function CodeEditor(props) {
5472
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
5573
[theme, syntaxHighlighting(highlightStyle, { fallback: true })],
5674
EditorView.updateListener.of(update => {
57-
if (update.docChanged) {
58-
if (props.onInput) props.onInput({ value: update.state.doc.toString() });
75+
// Ignores changes from swapping out the editor code programmatically
76+
if (isViewUpdateFromUserInput(update)) {
77+
props.onInput(update.state.doc.toString());
5978
}
6079
})
6180
]
@@ -65,16 +84,23 @@ export default function CodeEditor(props) {
6584
state,
6685
parent: editorParent.current
6786
});
68-
69-
setEditor(editor.current);
70-
}, [props.baseExampleSlug]);
87+
}, [props.editorCode]);
7188

7289
useEffect(() => (
7390
() => {
74-
editor.current.destroy();
75-
setEditor(null);
91+
if (editor.current) editor.current.destroy();
7692
}
7793
), []);
7894

7995
return <div ref={editorParent} class={cx(style.codeEditor, props.class)} />;
8096
}
97+
98+
/** @param {import('@codemirror/view').ViewUpdate} viewUpdate */
99+
function isViewUpdateFromUserInput(viewUpdate) {
100+
if (viewUpdate.docChanged) {
101+
for (const transaction of viewUpdate.transactions) {
102+
if (transaction.annotation(Transaction.userEvent)) return true;
103+
}
104+
}
105+
return false;
106+
}

src/components/controllers/repl-page.jsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRoute } from 'preact-iso';
1+
import { useLocation, useRoute } from 'preact-iso';
22
import { Repl } from './repl';
33
import { useExample } from './repl/examples';
44
import { useContent, useResource } from '../../lib/use-resource';
@@ -15,7 +15,7 @@ export default function ReplPage() {
1515
useTitle(meta.title);
1616
useDescription(meta.description);
1717

18-
const [code, slug] = initialCode(query);
18+
const [code] = initialCode(query);
1919

2020
return (
2121
<div class={style.repl}>
@@ -28,7 +28,7 @@ export default function ReplPage() {
2828
display: none !important;
2929
}
3030
`}</style>
31-
<Repl code={code} slug={slug} />
31+
<Repl code={code} />
3232
</div>
3333
);
3434
}
@@ -39,6 +39,7 @@ export default function ReplPage() {
3939
* ?code -> ?example -> localStorage -> simple counter example
4040
*/
4141
function initialCode(query) {
42+
const { route } = useLocation();
4243
let code, slug;
4344
if (query.code) {
4445
try {
@@ -48,13 +49,9 @@ function initialCode(query) {
4849
code = useExample([query.example]);
4950
if (code) {
5051
slug = query.example;
51-
history.replaceState(
52-
null,
53-
null,
54-
`/repl?example=${encodeURIComponent(slug)}`
55-
);
52+
route(`/repl?example=${encodeURIComponent(slug)}`, true);
5653
}
57-
else history.replaceState(null, null, '/repl');
54+
else route('/repl', true);
5855
}
5956

6057
if (!code) {
@@ -63,11 +60,7 @@ function initialCode(query) {
6360
} else {
6461
slug = 'counter';
6562
if (typeof window !== 'undefined') {
66-
history.replaceState(
67-
null,
68-
null,
69-
`/repl?example=${encodeURIComponent(slug)}`
70-
);
63+
route(`/repl?example=${encodeURIComponent(slug)}`, true);
7164
}
7265
code = useExample([slug]);
7366
}

src/components/controllers/repl/index.jsx

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState } from 'preact/hooks';
2+
import { useLocation, useRoute } from 'preact-iso';
23
import { Splitter } from '../../splitter';
3-
import { EXAMPLES, getExample, loadExample } from './examples';
44
import { ErrorOverlay } from './error-overlay';
5+
import { EXAMPLES, getExample, loadExample } from './examples';
56
import { useStoredValue } from '../../../lib/localstorage';
67
import { useResource } from '../../../lib/use-resource';
78
import { parseStackTrace } from './errors';
@@ -13,32 +14,29 @@ import REPL_CSS from './examples.css?raw';
1314
* @param {string} props.code
1415
* @param {string} [props.slug]
1516
*/
16-
export function Repl({ code, slug }) {
17+
export function Repl({ code }) {
18+
const { route } = useLocation();
19+
const { query } = useRoute();
1720
const [editorCode, setEditorCode] = useStoredValue('preact-www-repl-code', code);
18-
const [exampleSlug, setExampleSlug] = useState(slug || '');
1921
const [error, setError] = useState(null);
2022
const [copied, setCopied] = useState(false);
2123

22-
// TODO: CodeMirror v5 cannot load in Node, and loading only the runner
23-
// causes some bad jumping/pop-in. For the moment, this is the best option
24+
// TODO: Needs some work for prerendering to not cause pop-in
2425
if (typeof window === 'undefined') return null;
2526

27+
/**
28+
* @type {{ Runner: import('./runner').default, CodeEditor: import('../../code-editor').default }}
29+
*/
2630
const { Runner, CodeEditor } = useResource(() => Promise.all([
2731
import('../../code-editor'),
2832
import('./runner')
2933
]).then(([CodeEditor, Runner]) => ({ CodeEditor: CodeEditor.default, Runner: Runner.default })), ['repl']);
3034

31-
const applyExample = (e) => {
32-
const slug = e.target.value;
35+
const applyExample = (slug) => {
3336
loadExample(getExample(slug).url)
3437
.then(code => {
3538
setEditorCode(code);
36-
setExampleSlug(slug);
37-
history.replaceState(
38-
null,
39-
null,
40-
`/repl?example=${encodeURIComponent(slug)}`
41-
);
39+
route(`/repl?example=${encodeURIComponent(slug)}`, true);
4240
});
4341
};
4442

@@ -47,25 +45,16 @@ export function Repl({ code, slug }) {
4745

4846
// Clears the example & code query params when a user
4947
// begins to modify the code
50-
if (!exampleSlug || !location.search) return;
51-
const example = getExample(exampleSlug);
52-
if (example) {
53-
loadExample(example.url).then(exampleCode => {
54-
if (exampleCode !== value) {
55-
setExampleSlug('');
56-
history.replaceState(null, null, '/repl');
57-
}
58-
});
48+
if (query.example || query.code) {
49+
route('/repl', true);
5950
}
6051
};
6152

6253
const share = () => {
63-
if (!exampleSlug) {
64-
history.replaceState(
65-
null,
66-
null,
67-
`/repl?code=${encodeURIComponent(btoa(editorCode))}`
68-
);
54+
// No reason to share semi-sketchy btoa'd code if there's
55+
// a perfectly good example we can use instead
56+
if (!query.example) {
57+
route(`/repl?code=${encodeURIComponent(btoa(editorCode))}`, true);
6958
}
7059

7160
try {
@@ -94,13 +83,13 @@ export function Repl({ code, slug }) {
9483
<header class={style.toolbar}>
9584
<label>
9685
Examples:{' '}
97-
<select value={exampleSlug} onChange={applyExample}>
86+
<select value={query.example || ''} onChange={(e) => applyExample(e.currentTarget.value)}>
9887
<option value="" disabled>
9988
Select Example...
10089
</option>
10190
{EXAMPLES.map(function item(ex) {
10291
const selected =
103-
ex.slug !== undefined && ex.slug === exampleSlug;
92+
ex.slug !== undefined && ex.slug === query.example;
10493
return ex.group ? (
10594
<optgroup label={ex.group}>{ex.items.map(item)}</optgroup>
10695
) : (
@@ -139,9 +128,8 @@ export function Repl({ code, slug }) {
139128
>
140129
<CodeEditor
141130
class={style.code}
142-
value={editorCode}
143-
baseExampleSlug={exampleSlug}
144-
error={error}
131+
editorCode={editorCode}
132+
slug={query.example}
145133
onInput={onEditorInput}
146134
/>
147135
</Splitter>

src/components/controllers/tutorial/index.jsx

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ export function Tutorial({ html, meta }) {
5858
const hasCode = meta.code !== false;
5959
const showCode = showCodeOverride && hasCode;
6060

61-
// TODO: CodeMirror v5 cannot load in Node, and loading only the runner
62-
// causes some bad jumping/pop-in. For the moment, this is the best option
61+
// TODO: Needs some work for prerendering to not cause pop-in
6362
if (typeof window === 'undefined') return null;
6463

64+
/**
65+
* @type {{ Runner: import('../repl/runner').default, CodeEditor: import('../../code-editor').default }}
66+
*/
6567
const { Runner, CodeEditor } = useResource(() => Promise.all([
6668
import('../../code-editor'),
6769
import('../repl/runner')
@@ -73,8 +75,7 @@ export function Tutorial({ html, meta }) {
7375
solutionCtx.setSolved(false);
7476
content.current.scrollTo(0, 0);
7577
}
76-
}, [html]);
77-
78+
}, [meta.tutorial?.initial]);
7879

7980
const useResult = fn => {
8081
useEffect(() => {
@@ -140,47 +141,43 @@ export function Tutorial({ html, meta }) {
140141
orientation="horizontal"
141142
force={!showCode ? '100%' : undefined}
142143
other={
143-
// TODO: CodeMirror v5 cannot load in Node, and loading only the runner
144-
// causes some bad jumping/pop-in. For the moment, this is the best option
145-
typeof window === 'undefined'
146-
? null
147-
: <Splitter
148-
orientation="vertical"
149-
other={
150-
<>
151-
<div class={style.output}>
152-
{error && (
153-
<ErrorOverlay
154-
name={error.name}
155-
message={error.message}
156-
stack={parseStackTrace(error)}
157-
/>
158-
)}
159-
<Runner
160-
ref={runner}
161-
onSuccess={onSuccess}
162-
onRealm={onRealm}
163-
onError={onError}
164-
code={editorCode}
144+
<Splitter
145+
orientation="vertical"
146+
other={
147+
<>
148+
<div class={style.output}>
149+
{error && (
150+
<ErrorOverlay
151+
name={error.name}
152+
message={error.message}
153+
stack={parseStackTrace(error)}
165154
/>
166-
</div>
167-
{hasCode && (
168-
<button
169-
class={style.toggleCode}
170-
title="Toggle Code"
171-
onClick={toggleCode}
172-
>
173-
<span>Toggle Code</span>
174-
</button>
175155
)}
176-
</>
177-
}
178-
>
156+
<Runner
157+
ref={runner}
158+
onSuccess={onSuccess}
159+
onRealm={onRealm}
160+
onError={onError}
161+
code={editorCode}
162+
/>
163+
</div>
164+
{hasCode && (
165+
<button
166+
class={style.toggleCode}
167+
title="Toggle Code"
168+
onClick={toggleCode}
169+
>
170+
<span>Toggle Code</span>
171+
</button>
172+
)}
173+
</>
174+
}
175+
>
179176
<div class={style.codeWindow}>
180177
<CodeEditor
181178
class={style.code}
182-
value={editorCode}
183-
error={error}
179+
editorCode={meta.tutorial?.initial || ''}
180+
slug={path}
184181
onInput={setEditorCode}
185182
/>
186183
</div>

0 commit comments

Comments
 (0)