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:

committed by
Chromium LUCI CQ

parent
2e32b15395
commit
acc480cdee
@ -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>
|
||||
|
Reference in New Issue
Block a user