0

WebUI docs: Update examples to use TypeScript/latest guidance

Change-Id: Iefeaa74886f1a06bb13b3ba0c78d57083a7650f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3500160
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: Rebekah Potter <rbpotter@chromium.org>
Cr-Commit-Position: refs/heads/main@{#977553}
This commit is contained in:
rbpotter
2022-03-04 08:42:19 +00:00
committed by Chromium LUCI CQ
parent 2e32b15395
commit acc480cdee
3 changed files with 153 additions and 84 deletions

@ -379,7 +379,7 @@ if (loadTimeData.getBoolean('myFeatureIsEnabled')) {
<div class="note">
Data sources are not recreated on refresh, and therefore values that are dynamic
(i.e. that can change while Chrome is running) may easily become stale. It may
be preferable to use <code>cr.sendWithPromise()</code> to initialize dynamic
be preferable to use <code>sendWithPromise()</code> to initialize dynamic
values and call <code>FireWebUIListener()</code> to update them.
If you really want or need to use <code>AddBoolean()</code> for a dynamic value,
@ -513,7 +513,7 @@ alternatives:
* [`FireWebUIListener()`](#FireWebUIListener) allows easily notifying the page
when an event occurs in C++ and is more loosely coupled (nothing blows up if
the event dispatch is ignored). JS subscribes to notifications via
[`cr.addWebUIListener`](#cr_addWebUIListener).
[`addWebUIListener`](#addWebUIListener).
* [`ResolveJavascriptCallback`](#ResolveJavascriptCallback) and
[`RejectJavascriptCallback`](#RejectJavascriptCallback) are useful
when Javascript requires a response to an inquiry about C++-canonical state
@ -529,7 +529,7 @@ happen in timely manner, or may be caused to happen by unpredictable events
Here's some example to detect a change to Chrome's theme:
```js
cr.addWebUIListener("theme-changed", refreshThemeStyles);
addWebUIListener("theme-changed", refreshThemeStyles);
```
This Javascript event listener can be triggered in C++ via:
@ -544,7 +544,7 @@ Because it's not clear when a user might want to change their theme nor what
theme they'll choose, this is a good candidate for an event listener.
If you simply need to get a response in Javascript from C++, consider using
[`cr.sendWithPromise()`](#cr_sendWithPromise) and
[`sendWithPromise()`](#sendWithPromise) and
[`ResolveJavascriptCallback`](#ResolveJavascriptCallback).
### WebUIMessageHandler::OnJavascriptAllowed()
@ -626,7 +626,7 @@ imperatively unsubscribe in `OnJavascriptDisallowed()`.
### WebUIMessageHandler::RejectJavascriptCallback()
This method is called in response to
[`cr.sendWithPromise()`](#cr_sendWithPromise) to reject the issued Promise. This
[`sendWithPromise()`](#sendWithPromise) to reject the issued Promise. This
runs the rejection (second) callback in the [Promise's
executor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
and any
@ -657,16 +657,16 @@ See also: [`ResolveJavascriptCallback`](#ResolveJavascriptCallback)
### WebUIMessageHandler::ResolveJavascriptCallback()
This method is called in response to
[`cr.sendWithPromise()`](#cr_sendWithPromise) to fulfill an issued Promise,
[`sendWithPromise()`](#sendWithPromise) to fulfill an issued Promise,
often with a value. This results in runnings any fulfillment (first) callbacks
in the associate Promise executor and any registered
[`then()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then)
callbacks.
So, given this JS code:
So, given this TypeScript code:
```js
cr.sendWithPromise('bakeDonuts').then(function(numDonutsBaked) {
sendWithPromise('bakeDonuts').then(function(numDonutsBaked: number) {
shop.donuts += numDonutsBaked;
});
```
@ -734,7 +734,7 @@ callback with the deserialized arguments:
message_callbacks_.find(message)->second.Run(&args);
```
### cr.addWebUIListener()
### addWebUIListener()
WebUI listeners are a convenient way for C++ to inform JavaScript of events.
@ -745,7 +745,9 @@ listening for events in some cases. **cr.addWebUIListener** is preferred in new
code.
Adding WebUI listeners creates and inserts a unique ID into a map in JavaScript,
just like [cr.sendWithPromise()](#cr_sendWithPromise).
just like [sendWithPromise()](#sendWithPromise).
addWebUIListener can be imported from 'chrome://resources/js/cr.m.js'.
```js
// addWebUIListener():
@ -774,18 +776,18 @@ void OvenHandler::OnBakingDonutsFinished(size_t num_donuts) {
}
```
JavaScript can listen for WebUI events via:
TypeScript can listen for WebUI events via:
```js
var donutsReady = 0;
cr.addWebUIListener('donuts-baked', function(numFreshlyBakedDonuts) {
let donutsReady: number = 0;
addWebUIListener('donuts-baked', function(numFreshlyBakedDonuts: number) {
donutsReady += numFreshlyBakedDonuts;
});
```
### cr.sendWithPromise()
### sendWithPromise()
`cr.sendWithPromise()` is a wrapper around `chrome.send()`. It's used when
`sendWithPromise()` is a wrapper around `chrome.send()`. It's used when
triggering a message requires a response:
```js
@ -799,16 +801,18 @@ to make request specific or do from deeply nested code.
In newer WebUI pages, you see code like this:
```js
cr.sendWithPromise('getNumberOfDonuts').then(function(numDonuts) {
sendWithPromise('getNumberOfDonuts').then(function(numDonuts: number) {
alert('Yay, there are ' + numDonuts + ' delicious donuts left!');
});
```
Note that sendWithPromise can be imported from 'chrome://resources/js/cr.m.js';
On the C++ side, the message registration is similar to
[`chrome.send()`](#chrome_send) except that the first argument in the
message handler's list is a callback ID. That ID is passed to
`ResolveJavascriptCallback()`, which ends up resolving the `Promise` in
JavaScript and calling the `then()` function.
JavaScript/TypeScript and calling the `then()` function.
```c++
void DonutHandler::HandleGetNumberOfDonuts(const base::ListValue* args) {
@ -826,7 +830,7 @@ The callback ID is just a namespaced, ever-increasing number. It's used to
insert a `Promise` into the JS-side map when created.
```js
// cr.sendWithPromise():
// sendWithPromise():
var id = methodName + '_' + uidCounter++;
chromeSendResolverMap[id] = new PromiseResolver;
chrome.send(methodName, [id].concat(args));

@ -42,21 +42,21 @@ body {
<div id="example-div">[[message_]]</div>
```
`chrome/browser/resources/hello_world/hello_world.js`
`chrome/browser/resources/hello_world/hello_world.ts`
```js
import './strings.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './hello_world.html.js';
/** @polymer */
export class HelloWorldElement extends PolymerElement {
static get is() {
return 'hello-world';
}
static get template() {
return html`{__html_template__}`;
return getTemplate();
}
static get properties() {
@ -67,70 +67,91 @@ export class HelloWorldElement extends PolymerElement {
},
};
}
private message_: string;
}
customElements.define(HelloWorldElement.is, HelloWorldElement);
```
Add a `BUILD.gn` file to get Javascript type checking and Polymer compilation:
Add a 'tsconfig_base.json' file to configure TypeScript options. Typical options
needed by Polymer UIs include noUncheckedIndexAccess, noUnusedLocals, and
strictPropertyInitialization, all set to false.
`chrome/browser/resources/hello_world/tsconfig_base.gn`
```
{
"extends": "../../../../tools/typescript/tsconfig_base.json",
"compilerOptions": {
"noUncheckedIndexedAccess": false,
"noUnusedLocals": false,
"strictPropertyInitialization": false
}
}
```
Add a `BUILD.gn` file to get TypeScript compilation and to generate the JS file
from which the template will be imported.
`chrome/browser/resources/hello_world/BUILD.gn`
```
import("//third_party/closure_compiler/compile_js.gni")
import("//tools/polymer/html_to_js.gni")
import("//tools/polymer/html_to_wrapper.gni")
import("//tools/grit/preprocess_if_expr.gni")
import("//tools/typescript/ts_library.gni")
html_to_js("web_components") {
js_files = [ "hello_world.js" ]
html_to_wrapper("html_wrapper_files") {
in_files = [ "hello_world.html" ]
}
js_library("hello_world") {
# Move everything to one folder using preprocess_if_expr.
preprocess_folder = "preprocessed"
preprocess_if_expr("preprocess_generated") {
# This file is generated by html_to_wrapper().
in_files = [ "hello_world.html.ts" ]
in_folder = target_gen_dir
out_folder = "$target_gen_dir/$preprocess_folder"
deps = [ ":html_wrapper_files" ]
}
preprocess_if_expr("preprocess") {
in_files = [ "hello_world.ts" ]
in_folder = "."
out_folder = "$target_gen_dir/$preprocess_folder"
}
ts_library("build_ts") {
root_dir = "$target_gen_dir/$preprocess_folder"
out_dir = "$target_gen_dir/tsc"
tsconfig_base = "tsconfig_base.json"
in_files = [
"hello_world.ts",
"hello_world.html.ts"
]
deps = [
"//ui/webui/resources/js:load_time_data.m",
"//ui/webui/resources/js:util.m",
"//third_party/polymer/v3_0:library,"
"//ui/webui/resources:library",
]
extra_deps = [
":preprocess",
":preprocess_generated",
]
}
js_type_check("closure_compile") {
deps = [ ":hello_world" ]
}
```
Add the new `:closure_compile` target to `chrome/browser/resources/BUILD.gn` to
include it in coverage:
```
group("closure_compile) {
deps = [
...
"hello_world:closure_compile"
...
]
```
Finally, create an `OWNERS` file for the new folder.
## Adding the resources
Resources for the browser are stored in `grd` files. Current best practice is to autogenerate a grd file for your
component in the `BUILD` file we created earlier
component in the `BUILD` file we created earlier. See new content below:
`chrome/browser/resources/hello_world/BUILD.gn`
`chrome/browser/resources/hello_world/BUILD.gn new additions`
```
import("//tools/grit/grit_rule.gni")
import("//tools/grit/preprocess_if_expr.gni")
import("//ui/webui/resources/tools/generate_grd.gni")
preprocess_folder = "preprocessed"
preprocess_gen_manifest = "preprocessed_gen_manifest.json"
resources_grd_file = "$target_gen_dir/resources.grd"
preprocess_if_expr("preprocess_generated") {
deps = [ ":web_components" ]
in_folder = target_gen_dir
out_folder = "$target_gen_dir/$preprocess_folder"
out_manifest = "$target_gen_dir/$preprocess_gen_manifest"
in_files = [ "hello_world.js" ]
}
generate_grd("build_grd") {
grd_prefix = "hello_world"
out_grd = resources_grd_file
@ -139,8 +160,8 @@ generate_grd("build_grd") {
"hello_world_container.html",
]
input_files_base_dir = rebase_path(".", "//")
deps = [ ":preprocess_generated" ]
manifest_files = [ "$target_gen_dir/$preprocess_gen_manifest" ]
deps = [ ":build_ts" ]
manifest_files = [ "$target_gen_dir/$tsconfig.manifest" ]
}
grit("resources") {

@ -290,7 +290,10 @@ Use RTL-friendly versions of things like `margin` or `padding` where possible:
For properties that don't have an RTL-friendly alternatives, use
`html[dir='rtl']` as a prefix in your selectors.
## JavaScript
## JavaScript/TypeScript
New WebUI code (except for ChromeOS specific code) should be written in
TypeScript.
### Style
@ -308,7 +311,7 @@ Guide](https://google.github.io/styleguide/jsguide.html) as well as
* Use `@type` (instead of `@return` or `@param`) for JSDoc annotations on
getters/setters
* See [Annotating JavaScript for the Closure
* For legacy code using closure, see [Annotating JavaScript for the Closure
Compiler](https://developers.google.com/closure/compiler/docs/js-for-compiler)
for @ directives
@ -320,6 +323,9 @@ Guide](https://google.github.io/styleguide/jsguide.html) as well as
### Closure compiler
* Closure compiler should only be used by legacy code that has not yet been
converted to use TypeScript.
* Use the [closure
compiler](https://chromium.googlesource.com/chromium/src/+/main/docs/closure_compilation.md)
to identify JS type errors and enforce correct JSDoc annotations.
@ -361,20 +367,20 @@ Guide](https://google.github.io/styleguide/jsguide.html) as well as
Also see the [Google Polymer Style Guide](http://go/polymer-style).
* Elements with UI should have their HTML in a .html file and logic in a JS file
with the same name. The HTML should be copied into the final JS file at build
time, replacing the special `{__html_template__}` sequence, using the
html_to_js BUILD.gn rule. For example the following will paste the contents
of my_app.html into the final generated JS file:
* Elements with UI should have their HTML in a .html file and logic in a TS file
with the same name. The HTML template can be imported into the final JS file
at runtime from a generated JS wrapper file via the getTemplate() function.
THe wrapper file is generated using the html_to_wrapper gn rule:
```
html_to_js('web_components') {
js_files = [ 'my_app.js' ]
html_to_wrapper('html_wrapper_files') {
in_files = [ 'my_app.html' ]
}
```
* In new code, use class based syntax for custom elements. Example:
```js
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './my_app.html.js';
class MyAppElement extends PolymerElement {
static get is() {
@ -382,7 +388,7 @@ class MyAppElement extends PolymerElement {
}
static get template() {
return html`{__html_template__}`;
return getTemplate();
}
static get properties() {
@ -390,6 +396,8 @@ class MyAppElement extends PolymerElement {
foo: String,
};
}
foo: string;
}
customElements.define(MyAppElement.is, MyAppElement);
@ -409,6 +417,17 @@ customElements.define(MyAppElement.is, MyAppElement);
* Use camelCase for element IDs to simplify local DOM accessors (i.e.
`this.$.camelCase` instead of `this.$['dash-case']`).
* Note: In TypeScript, the `this.$.camelCase` accessor requires adding an
interface:
```js
interface MyAppElement {
$: {
camelCase: HTMLElement,
};
}
```
* Use `this.foo` instead of `newFoo` arguments in observers when possible.
This makes changing the type of `this.foo` easier (as the `@type` is
duplicated in less places, i.e. `@param`).
@ -473,8 +492,9 @@ be passed to the generate_grd rule to generate entries for them in a grd file.
### Example
The following BUILD.gn example code uses preprocess_if_expr to preprocess any
`<if expr>` in the final my_app.js file that is generated by the earlier
html_to_js example. It then uses the manifest from this operation and the
`<if expr>` in my_app.ts and in the my_app.html.ts file that is generated by
the earlier html_to_wrapper example. It then runs the TypeScript compiler on
the outputs of this operation and uses the manifest from this operation and the
in_files option to place both the final, preprocessed file and a separate (not
preprocessed) icon into a generated grd file using generate_grd:
@ -482,21 +502,45 @@ preprocessed) icon into a generated grd file using generate_grd:
preprocess_folder = "preprocessed"
preprocess_manifest = "preprocessed_manifest.json"
# Read file from target_gen_dir, where it will be pasted by html_to_js.
preprocess_if_expr("preprocess") {
deps = [ ":web_components" ]
in_folder = target_gen_dir
in_files = [ "my_app.js" ]
in_folder = "."
in_files = [ "my_app.ts" ]
out_folder = "$target_gen_dir/$preprocess_folder"
out_manifest = "$target_gen_dir/$preprocess_manifest"
}
# Put the preprocessed file as well as a separate my_icon.svg file in the grd:
# Read file from target_gen_dir, where it will be pasted by html_to_wrapper.
preprocess_if_expr("preprocess_generated") {
in_folder = target_gen_dir
in_files = [ "my_app.html.ts" ]
out_folder = "$target_gen_dir/$preprocess_folder"
deps = [ ":html_wrapper_files" ]
}
# Run TS compiler on the two files:
ts_library("build_ts") {
root_dir = "$target_gen_dir/$preprocess_folder"
out_dir = "$target_gen_dir/tsc"
tsconfig_base = "tsconfig_base.json"
in_files = [
"my_app.html.ts",
"my_app.ts",
]
deps = [
"//third_party/polymer/v3_0:library",
"//ui/webui/resources:library",
]
extra_deps = [
":preprocess",
":preprocess_generated",
]
}
# Put the compiled files as well as a separate my_icon.svg file in the grd:
generate_grd("build_grd") {
input_files = [ "my_icon.svg" ]
input_files_base_dir = rebase_path(".", "//")
deps = [ ":preprocess" ]
manifest_files = [ "$target_gen_dir/$preprocess_manifest" ]
deps = [ ":build_ts" ]
manifest_files = [ "$target_gen_dir/tsconfig.manifest" ]
grd_prefix = [ "foo" ]
out_grd = "$target_gen_dir/resources.grd"
}
@ -510,7 +554,7 @@ In a few legacy resources, preprocessing is enabled by adding the
Note #2:
These preprocessor statements can live in places that surprise linters or
formatters (for example: running clang-format on a .js file with an `<if>` in
formatters (for example: running clang-format on a .ts file with an `<if>` in
it). Generally, putting these language-invalid features inside of comments
helps alleviate problems with unexpected input.
***
@ -521,7 +565,7 @@ or excluding code.
Example:
```js
function isWindows() {
function isWindows(): boolean {
// <if expr="win">
return true;
// </if>