John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 1 | <style> |
| 2 | .note::before { |
| 3 | content: 'Note: '; |
| 4 | font-variant: small-caps; |
| 5 | font-style: italic; |
| 6 | } |
| 7 | |
| 8 | .doc h1 { |
| 9 | margin: 0; |
| 10 | } |
| 11 | </style> |
| 12 | |
| 13 | # Creating WebUI Interfaces outside components/ |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 14 | This guide is based on [Creating WebUI Interfaces in components](webui_in_components.md), and comments from reviewers when creating the ChromeOS emoji picker. |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 15 | |
| 16 | [TOC] |
| 17 | |
| 18 | <a name="creating_web_ui_page"></a> |
| 19 | WebUI pages live in `chrome/browser/resources`. You should create a folder for your project `chrome/browser/resources/hello_world`. |
| 20 | When creating WebUI resources, follow the [Web Development Style Guide](https://chromium.googlesource.com/chromium/src/+/master/styleguide/web/web.md). For a sample WebUI page you could start with the following files: |
| 21 | |
| 22 | `chrome/browser/resources/hello_world/hello_world.html` |
| 23 | ```html |
| 24 | <!DOCTYPE HTML> |
| 25 | <html> |
| 26 | <head> |
| 27 | <meta charset="utf-8"> |
| 28 | <link rel="stylesheet" href="hello_world.css"> |
| 29 | <script type="module" src="hello_world.js"></script> |
| 30 | </head> |
| 31 | <body> |
| 32 | <h1>Hello World</h1> |
| 33 | <div id="example-div"></div> |
| 34 | </body> |
| 35 | </html> |
| 36 | ``` |
| 37 | |
| 38 | `chrome/browser/resources/hello_world/hello_world.css` |
| 39 | ```css |
| 40 | body { |
| 41 | margin: 0; |
| 42 | } |
| 43 | ``` |
| 44 | |
| 45 | `chrome/browser/resources/hello_world/hello_world.js` |
| 46 | ```js |
| 47 | import './strings.m.js'; |
| 48 | |
| 49 | import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; |
| 50 | import {$} from 'chrome://resources/js/util.m.js'; |
| 51 | |
| 52 | function initialize() { |
| 53 | const block = document.createElement('div'); |
| 54 | block.innerText = loadTimeData.getString('message'); |
| 55 | $('example-div').appendChild(block); |
| 56 | } |
| 57 | |
| 58 | document.addEventListener('DOMContentLoaded', initialize); |
| 59 | ``` |
| 60 | |
| 61 | Add a `BUILD.gn` file to get Javascript type checking: |
| 62 | |
| 63 | `chrome/browser/resources/hello_world/BUILD.gn` |
| 64 | ``` |
| 65 | import("//third_party/closure_compiler/compile_js.gni") |
| 66 | |
| 67 | js_library("hello_world") { |
| 68 | deps = [ |
| 69 | "//ui/webui/resources/js:load_time_data.m", |
| 70 | "//ui/webui/resources/js:util.m", |
| 71 | ] |
| 72 | } |
| 73 | |
| 74 | js_type_check("closure_compile") { |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 75 | deps = [ ":hello_world" ] |
| 76 | } |
| 77 | ``` |
| 78 | |
| 79 | Then refer to the new `:closure_compile` target from `chrome/browser/resources/BUILD.gn`: |
| 80 | |
| 81 | ``` |
| 82 | group("closure_compile) { |
| 83 | deps = [ |
| 84 | ... |
| 85 | "hello_world:closure_compile" |
| 86 | ... |
| 87 | ] |
| 88 | ``` |
| 89 | |
| 90 | Finally, create an `OWNERS` file for the new folder. |
| 91 | |
| 92 | ## Adding the resources |
| 93 | Resources for the browser are stored in `grd` files. Current best practice is to autogenerate a grd file for your |
| 94 | component in the `BUILD` file we created earlier |
| 95 | |
| 96 | `chrome/browser/resources/hello_world/BUILD.gn` |
| 97 | ``` |
| 98 | import("//tools/grit/grit_rule.gni") |
| 99 | import("//ui/webui/resources/tools/generate_grd.gni") |
| 100 | |
| 101 | resources_grd_file = "$target_gen_dir/resources.grd" |
| 102 | generate_grd("build_grd") { |
| 103 | grd_prefix = "hello_world" |
| 104 | out_grd = resources_grd_file |
| 105 | input_files = [ |
| 106 | "hello_world.css", |
| 107 | "hello_world.html", |
| 108 | "hello_world.js", |
| 109 | ] |
| 110 | input_files_base_dir = rebase_path(".", "//") |
| 111 | } |
| 112 | |
| 113 | grit("resources") { |
| 114 | enable_input_discovery_for_gn_analyze = false |
| 115 | source = resources_grd_file |
| 116 | deps = [ ":build_grd" ] |
| 117 | outputs = [ |
| 118 | "grit/hello_world_resources.h", |
| 119 | "grit/hello_world_resources_map.cc", |
| 120 | "grit/hello_world_resources_map.h", |
| 121 | "hello_world_resources.pak", |
| 122 | ] |
| 123 | output_dir = "$root_gen_dir/chrome" |
| 124 | } |
| 125 | ``` |
| 126 | |
| 127 | Then add the new resource target to `chrome/browser/resources/BUILD.gn` |
| 128 | ``` |
| 129 | group("resources") { |
| 130 | public_deps += [ |
| 131 | ... |
| 132 | "hello_world:resources" |
| 133 | ... |
| 134 | ] |
| 135 | } |
| 136 | ``` |
| 137 | |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 138 | Also add to `chrome/chrome_paks.gni` |
| 139 | |
| 140 | ``` |
| 141 | template("chrome_extra_paks") { |
| 142 | ... (lots) |
| 143 | sources += [ |
| 144 | ... |
| 145 | "$root_gen_dir/chrome/hello_world_resources.pak", |
| 146 | ... |
| 147 | ] |
| 148 | deps += [ |
| 149 | ... |
| 150 | "//chrome/browser/resources/hello_world:resources", |
| 151 | ... |
| 152 | ] |
| 153 | } |
| 154 | ``` |
| 155 | |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 156 | ## Adding URL constants for the new chrome URL |
| 157 | |
| 158 | `chrome/common/webui_url_constants.cc:` |
| 159 | ```c++ |
| 160 | const char kChromeUIHelloWorldURL[] = "chrome://hello-world/"; |
| 161 | const char kChromeUIHelloWorldHost[] = "hello-world"; |
| 162 | ``` |
| 163 | |
| 164 | `chrome/common/webui_url_constants.h:` |
| 165 | ```c++ |
| 166 | extern const char kChromeUIHelloWorldURL[]; |
| 167 | extern const char kChromeUIHelloWorldHost[]; |
| 168 | ``` |
| 169 | |
| 170 | ## Adding a WebUI class for handling requests to the chrome://hello-world/ URL |
| 171 | Next we need a class to handle requests to this new resource URL. Typically this will subclass `WebUIController` (WebUI |
| 172 | dialogs will also need another class which will subclass `WebDialogDelegate`, this is shown later). |
| 173 | |
| 174 | `chrome/browser/ui/webui/hello_world_ui.h` |
| 175 | ```c++ |
| 176 | #ifndef CHROME_BROWSER_UI_WEBUI_HELLO_WORLD_HELLO_WORLD_H_ |
| 177 | #define CHROME_BROWSER_UI_WEBUI_HELLO_WORLD_HELLO_WORLD_H_ |
| 178 | |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 179 | #include "content/public/browser/web_ui_controller.h" |
| 180 | |
| 181 | // The WebUI for chrome://hello-world |
| 182 | class HelloWorldUI : public content::WebUIController { |
| 183 | public: |
| 184 | explicit HelloWorldUI(content::WebUI* web_ui); |
| 185 | ~HelloWorldUI() override; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 186 | }; |
| 187 | |
| 188 | #endif // CHROME_BROWSER_UI_WEBUI_HELLO_WORLD_HELLO_WORLD_H_ |
| 189 | ``` |
| 190 | |
| 191 | `chrome/browser/ui/webui/hello_world_ui.cc` |
| 192 | ```c++ |
| 193 | #include "chrome/browser/ui/webui/hello_world_ui.h" |
| 194 | |
| 195 | #include "chrome/browser/ui/webui/webui_util.h" |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 196 | #include "chrome/common/webui_url_constants.h" |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 197 | #include "content/public/browser/browser_context.h" |
| 198 | #include "content/public/browser/web_contents.h" |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 199 | #include "chrome/grit/hello_world_resources.h" |
| 200 | #include "chrome/grit/hello_world_resources_map.h" |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 201 | #include "content/public/browser/web_ui.h" |
| 202 | #include "content/public/browser/web_ui_data_source.h" |
| 203 | |
| 204 | |
| 205 | HelloWorldUI::HelloWorldUI(content::WebUI* web_ui) |
| 206 | : content::WebUIController(web_ui) { |
| 207 | // Set up the chrome://hello-world source. |
| 208 | content::WebUIDataSource* html_source = |
| 209 | content::WebUIDataSource::Create(chrome::kChromeUIHelloWorldHost); |
| 210 | |
| 211 | // As a demonstration of passing a variable for JS to use we pass in some |
| 212 | // a simple message. |
| 213 | html_source->AddString("message", "Hello World!"); |
| 214 | html_source->UseStringsJs(); |
| 215 | |
| 216 | // Add required resources. |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 217 | webui::SetupWebUIDataSource(html_source, base::make_span(kHelloWorldResources, kHelloWorldResourcesSize), IDR_HELLO_WORLD_HELLO_WORLD_HTML); |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 218 | |
| 219 | content::BrowserContext* browser_context = |
| 220 | web_ui->GetWebContents()->GetBrowserContext(); |
| 221 | content::WebUIDataSource::Add(browser_context, html_source); |
| 222 | } |
| 223 | |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 224 | HelloWorldUI::~HelloWorldUI() = default; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 225 | ``` |
| 226 | |
| 227 | To ensure that your code actually gets compiled, you need to add it to `chrome/browser/ui/BUILD.gn`: |
| 228 | |
| 229 | ``` |
| 230 | static_library("ui") { |
| 231 | sources = [ |
| 232 | ... (lots) |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 233 | "webui/hello_world_ui.cc", |
| 234 | "webui/hello_world_ui.h", |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 235 | ``` |
| 236 | |
| 237 | ## Adding your WebUI request handler to the Chrome WebUI factory |
| 238 | |
| 239 | The Chrome WebUI factory is where you setup your new request handler. |
| 240 | |
| 241 | `chrome/browser/ui/webui/chrome_web_ui_controller_factory.cc:` |
| 242 | ```c++ |
| 243 | + #include "chrome/browser/ui/webui/hello_world_ui.h" |
| 244 | ... |
| 245 | + if (url.host() == chrome::kChromeUIHelloWorldHost) |
| 246 | + return &NewWebUI<HelloWorldUI>; |
| 247 | ``` |
| 248 | |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 249 | ## Add an entry to resource_ids.spec |
| 250 | This file is for automatically generating resource ids. Ensure that the previous |
| 251 | entry number plus the size equals the next entry number. |
| 252 | |
| 253 | `tools/gritsettings/resource_ids.spec` |
| 254 | |
| 255 | ``` |
| 256 | # START chrome/ WebUI resources section |
| 257 | ... (lots) |
| 258 | "<(SHARED_INTERMEDIATE_DIR)/chrome/browser/resources/hello_world/resources.grd": { |
| 259 | "META": {"sizes": {"includes": [5]}}, |
| 260 | "includes": [2085], |
| 261 | }, |
| 262 | ``` |
| 263 | |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 264 | ## Check everything works |
| 265 | |
| 266 | You're done! Assuming no errors (because everyone gets their code perfect the first time) you should be able to compile |
| 267 | and run chrome and navigate to `chrome://hello-world/` and see your nifty welcome text! |
| 268 | |
| 269 | |
| 270 | ## Making a WebUI Dialog |
| 271 | |
| 272 | Instead of having a full page for your WebUI, you might want a dialog in order to have a fully independent window. To |
| 273 | do that, some small changes are needed to your code. First, we need to add a new class which inherits from |
| 274 | `ui::WebDialogDelegate`. The easiest way to do that is to edit the `hello_world_ui.*` files |
| 275 | |
| 276 | |
| 277 | `chrome/browser/ui/webui/hello_world_ui.h` |
| 278 | ```c++ |
| 279 | // Leave the old content, but add this new code |
| 280 | class HelloWorldDialog : public ui::WebDialogDelegate { |
| 281 | public: |
| 282 | static void Show(); |
| 283 | ~HelloWorldDialog() override; |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 284 | HelloWorldDialog(const HelloWorldDialog&) = delete; |
| 285 | HelloWorldDialog& operator=(const HelloWorldDialog&) = delete; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 286 | |
| 287 | private: |
| 288 | HelloWorldDialog(); |
| 289 | // ui::WebDialogDelegate: |
| 290 | ui::ModalType GetDialogModalType() const override; |
Jan Wilken Dörrie | 85285b0 | 2021-03-11 23:38:47 | [diff] [blame] | 291 | std::u16string GetDialogTitle() const override; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 292 | GURL GetDialogContentURL() const override; |
| 293 | void GetWebUIMessageHandlers( |
| 294 | std::vector<content::WebUIMessageHandler*>* handlers) const override; |
| 295 | void GetDialogSize(gfx::Size* size) const override; |
| 296 | std::string GetDialogArgs() const override; |
| 297 | void OnDialogShown(content::WebUI* webui) override; |
| 298 | void OnDialogClosed(const std::string& json_retval) override; |
| 299 | void OnCloseContents(content::WebContents* source, |
| 300 | bool* out_close_dialog) override; |
| 301 | bool ShouldShowDialogTitle() const override; |
| 302 | |
| 303 | content::WebUI* webui_ = nullptr; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 304 | }; |
| 305 | ``` |
| 306 | |
| 307 | `chrome/browser/ui/webui/hello_world_ui.cc` |
| 308 | ```c++ |
| 309 | // Leave the old content, but add this new stuff |
| 310 | |
Toby Huang | d7752f9 | 2021-05-06 16:37:34 | [diff] [blame^] | 311 | HelloWorldDialog::HelloWorldDialog() = default; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 312 | |
| 313 | void HelloWorldDialog::Show() { |
| 314 | chrome::ShowWebDialog(nullptr, ProfileManager::GetActiveUserProfile(), |
| 315 | new HelloWorldDialog()); |
| 316 | } |
| 317 | |
| 318 | ui::ModalType HelloWorldDialog::GetDialogModalType() const { |
| 319 | return ui::MODAL_TYPE_NONE; |
| 320 | } |
| 321 | |
Jan Wilken Dörrie | 85285b0 | 2021-03-11 23:38:47 | [diff] [blame] | 322 | std::u16string HelloWorldDialog::GetDialogTitle() const { |
Jan Wilken Dörrie | 2c470ea | 2021-03-22 22:26:24 | [diff] [blame] | 323 | return u"Hello world"; |
John Palmer | d3225c2 | 2020-12-17 00:33:17 | [diff] [blame] | 324 | } |
| 325 | |
| 326 | GURL HelloWorldDialog::GetDialogContentURL() const { |
| 327 | return GURL(chrome::kChromeUIHelloWorldURL[); |
| 328 | } |
| 329 | |
| 330 | void HelloWorldDialog::GetWebUIMessageHandlers( |
| 331 | std::vector<content::WebUIMessageHandler*>* handlers) const {} |
| 332 | |
| 333 | void HelloWorldDialog::GetDialogSize(gfx::Size* size) const { |
| 334 | const int kDefaultWidth = 544; |
| 335 | const int kDefaultHeight = 628; |
| 336 | size->SetSize(kDefaultWidth, kDefaultHeight); |
| 337 | } |
| 338 | |
| 339 | std::string HelloWorldDialog::GetDialogArgs() const { |
| 340 | return ""; |
| 341 | } |
| 342 | |
| 343 | void HelloWorldDialog::OnDialogShown(content::WebUI* webui) { |
| 344 | webui_ = webui; |
| 345 | } |
| 346 | |
| 347 | void HelloWorldDialog::OnDialogClosed(const std::string& json_retval) { |
| 348 | delete this; |
| 349 | } |
| 350 | |
| 351 | void HelloWorldDialog::OnCloseContents(content::WebContents* source, |
| 352 | bool* out_close_dialog) { |
| 353 | *out_close_dialog = true; |
| 354 | } |
| 355 | |
| 356 | bool HelloWorldDialog::ShouldShowDialogTitle() const { |
| 357 | return true; |
| 358 | } |
| 359 | |
| 360 | HelloWorldDialog::~HelloWorldDialog() = default; |
| 361 | ``` |
| 362 | |
| 363 | Finally, you will need to do something to actually show your dialog, which can be done by calling `HelloWorldDialog::Show()`. |