### Scaffold an extension ```bash yo code # ? What type of extension do you want to create? New Extension (TypeScript) # ? What's the name of your extension? vscodeangularwebview # ? What's the identifier of your extension? vscodeangularwebview # ? What's the description of your extension? LEAVE BLANK # ? Initialize a git repository? No # ? Bundle the source code with webpack? No # ? Which package manager to use? npm ``` ### Scaffold an Angular application Target folder is the root folder of the extension ```bash ng new ? What name would you like to use for the new workspace and initial project? webview-ui ? Would you like to add Angular routing? No ? Which stylesheet format would you like to use? CSS ``` ### Create a webview With this basic extension created, you now need to create a webview. The following steps are an adapted version of those provided in the [Webview API](https://code.visualstudio.com/api/extension-guides/webview) guide – for more info about webviews, read the guide. Start by navigating to the `extensions.ts` file inside the `src` directory and replacing the contents of the `activate` function with the following: ```typescript // file: src/extension.ts export function activate(context: vscode.ExtensionContext) { const helloCommand = vscode.commands.registerCommand("vscodeangularwebview.helloWorld", () => { HelloWorldPanel.render(); }); context.subscriptions.push(helloCommand); } ``` At this point, you probably have noticed that there's an error because `HelloWorldPanel` doesn't exist. Here's how to fix that. ### Create a webview panel class Create a new directory/file at `src/panels/HelloWorldPanel.ts`. Inside this file, create a class that manages the state and behavior of Hello World webview panels. It'll contain all the data and methods for: - Creating and rendering Hello World webview panels - Properly cleaning up and disposing webview resources when the panel is closed - Setting the HTML content of the webview panel - Setting message listeners so data can be passed between the webview and extension **Constructor and properties** Start by importing the Visual Studio Code API and creating an exported `HelloWorldPanel` class with the following properties and constructor method: ```typescript // file: src/panels/HelloWorldPanel.ts import * as vscode from "vscode"; export class HelloWorldPanel { public static currentPanel: HelloWorldPanel | undefined; private readonly _panel: vscode.WebviewPanel; private _disposables: vscode.Disposable[] = []; private constructor(panel: vscode.WebviewPanel) { this._panel = panel; } } ``` **Render method** Now, add the render method. This will be responsible for rendering the current webview panel – if it exists – or creating and displaying a new webview panel. ```typescript // file: src/panels/HelloWorldPanel.ts export class HelloWorldPanel { // ... properties and constructor method ... public static render() { if (HelloWorldPanel.currentPanel) { HelloWorldPanel.currentPanel._panel.reveal(vscode.ViewColumn.One); } else { const panel = vscode.window.createWebviewPanel("hello-world", "Hello World", vscode.ViewColumn.One, { enableScripts: true, }); HelloWorldPanel.currentPanel = new HelloWorldPanel(panel); } } } ``` At this point, you can also go back to the `src/extension.ts` file and add an import statement to resolve the earlier error. ```typescript // file: src/extension.ts import * as vscode from "vscode"; import { HelloWorldPanel } from "./panels/HelloWorldPanel"; // ... activate function ... ``` **Dispose method** Back in the `HelloWorldPanel` class, you now need to define a `dispose` method so that webview resources are cleaned up when the webview panel is closed by the user or closed programmatically. ```typescript // file: src/panels/HelloWorldPanel.ts export class HelloWorldPanel { // ... other properties and methods ... public dispose() { HelloWorldPanel.currentPanel = undefined; this._panel.dispose(); while (this._disposables.length) { const disposable = this._disposables.pop(); if (disposable) { disposable.dispose(); } } } } ``` With the `dispose` method defined, you also need to update the constructor method. To do this, add an `onDidDispose` event listener, so the method can be triggered when the webview panel is closed. ```typescript // file: src/panels/HelloWorldPanel.ts private constructor(panel: vscode.WebviewPanel) { // ... other code ... this._panel.onDidDispose(() => this.dispose(), null, this._disposables); } ``` **Get webview content method** The `_getWebviewContent` method is where the UI of the extension will be defined. This is also where references to CSS and JavaScript files/packages are created and inserted into the webview HTML. You'll configure the Webview UI Toolkit here, in the second part of this guide. ```typescript // file: src/panels/HelloWorldPanel.ts export class HelloWorldPanel { // ... other properties and methods ... private _getWebviewContent() { // Tip: Install the es6-string-html VS Code extension to enable code highlighting below return /*html*/ ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hello World!</title> </head> <body> <h1>Hello World!</h1> </body> </html> `; } } ``` This is another point in which you need to update the constructor method to set the HTML content for the webview panel. ```typescript // file: src/panels/HelloWorldPanel.ts private constructor(panel: vscode.WebviewPanel) { // ... other code ... this._panel.webview.html = this._getWebviewContent(); } ``` ### Using the extension URI parameter With the package installed, you need to adjust the project so the toolkit is usable within your webview. To do this, start by updating the `_getWebviewContent` method to accept two new parameters. ```typescript // file: src/panels/HelloWorldPanel.ts private _getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { // ... Implementation details should be left unchanged for now ... } ``` With this change, you also need to update the parameters of a few other methods and method calls. Update the `constructor` method with the following: ```typescript // file: src/panels/HelloWorldPanel.ts private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { // ... other code ... this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri); } ``` Update the `render` method with the following: ```typescript // file: src/panels/HelloWorldPanel.ts public static render(extensionUri: vscode.Uri) { // ... other code ... HelloWorldPanel.currentPanel = new HelloWorldPanel(panel, extensionUri); } ``` Finally, update the call to the `render` method: ```typescript // file: src/extension.ts HelloWorldPanel.render(context.extensionUri); ``` ### Create a webview uri With those changes, you can now use some Visual Studio Code APIs to create a URI pointing to the toolkit package. Just a heads-up: These API calls can get a bit verbose, so you also need to create a small helper function to keep your code clean. Create a new file at `src/utilities/getUri.ts` with the following: ```typescript // file: src/utilities/getUri.ts import { Uri, Webview } from "vscode"; export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); } ``` ### Referencing Angular build output Build the angular application without hash ```bash ng build --output-hashing=none ``` Update getWebViewContent method ```typescript private _getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri) { const stylesUri = getUri(webview, extensionUri, ["webview-ui", "dist", "webview-ui", "styles.css"]); // The JS files from the Angular build output const runtimeUri = getUri(webview, extensionUri, ["webview-ui", "dist", "webview-ui", "runtime.js"]); const polyfillsUri = getUri(webview, extensionUri, ["webview-ui", "dist", "webview-ui", "polyfills.js"]); const scriptUri = getUri(webview, extensionUri, ["webview-ui", "dist", "webview-ui", "main.js"]); return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="${stylesUri}"> <title>Hello World!</title> </head> <body> <app-root></app-root> <script type="module" src="${runtimeUri}"></script> <script type="module" src="${polyfillsUri}"></script> <script type="module" src="${scriptUri}"></script> </body> </html> `; } ``` ## Not under rootDir error After creating the above application, you might face the following error message: > File 'xy' is not under 'rootDir' 'xy'. 'rootDir' is expected to contain all source files. The file is in the program because: Matched by default include pattern '**/*' This can be fixed by excluding your Angular app directory in the extension's tsconfig.json: ```json "exclude": ["webview-ui"] ``` ## Angular build on launch At this point to apply the changes made in the embedded angular application, you must manually call an ng build each time before you start the application. Using VSCode launch settings, this can be done automatically. In the extension root folder, you will find a .vscode directory, with a tasks.json inside it. Owerwrite its content with the following definition: ```json { "version": "2.0.0", "tasks": [ { "label": "ng build", "type": "shell", "command": "ng build --output-hashing=none", "options": { "cwd": "${workspaceFolder}/webview-ui/" }, }, { "label":"npm", "type": "shell", "command": "npm run compile", "dependsOn": [ "ng build" ], "group": { "kind": "build", "isDefault": true } } ] } ``` # Call VSCode API from Angular project ### Create listener into the extension ```typescript // HelloWorldPanel.ts private _setWebviewMessageListener(webview: vscode.Webview) { webview.onDidReceiveMessage( (message: any) => { const command = message.command; const text = message.text; switch (command) { case "hello": vscode.window.showInformationMessage(text); return; } }, undefined, this._disposables ); } ``` ```typescript // HelloWorldPanel.ts private constructor(panel: WebviewPanel, extensionUri: Uri) { ... // Set an event listener to listen for messages passed from the webview context this._setWebviewMessageListener(this._panel.webview); } ``` ### Add vscode-webview npm package to the angular project ```bash npm install --save @types/vscode-webview ``` ### Create an Angular service to notify the listener ```typescript import type { WebviewApi } from "vscode-webview"; class VSCodeAPIWrapper { private readonly vsCodeApi: WebviewApi<unknown> | undefined; constructor() { if (typeof acquireVsCodeApi === "function") { this.vsCodeApi = acquireVsCodeApi(); } } public postMessage(message: unknown) { if (this.vsCodeApi) { this.vsCodeApi.postMessage(message); } else { console.log(message); } } } export const vscode = new VSCodeAPIWrapper(); ``` ### Call the service from the components bound function ``` html <button (click)="handleHowdyClick()"></button> ``` ```typescript // component's class handleHowdyClick() { vscode.postMessage({ command: "hello", text: "Hey there partner! 🤠", }); } ```