blob: 0fef7a116ac12caa7b064a7b7de1d252581348c8 [file] [log] [blame]
<h1 id="add-webview">
<span class="h1-step">Step 4:</span>
Open External Links With a Webview
</h1>
<p class="note">
<strong>Want to start fresh from here?</strong>
Find the previous step's code in the <a href="https://github.com/mangini/io13-codelab/archive/master.zip">reference code zip</a> under <strong><em>cheat_code > solution_for_step3</strong></em>.
</p>
<p>In this step, you will learn:</p>
<ul>
<li>How to show external web content inside your app in a secure and sandboxed way.</li>
</ul>
<p>
<em>Estimated time to complete this step: 10 minutes.</em>
<br>
To preview what you will complete in this step, <a href="#launch">jump down to the bottom of this page &#8595;</a>.
</p>
<h2 id="overview">Learn about the webview tag</h2>
<p>Some applications need to present external web content directly to the user but
keep them inside the application experience. For example, a news aggregator
might want to embed the news from external sites with all the formatting, images,
and behavior of the original site. For these and other usages, Chrome Apps have
a custom HTML tag called <a href="/apps/tags/webview">webview</a>.</p>
<figure>
<img src="{{static}}/images/app_codelab/webview-example.png" alt="The Todo app using a webview">
</figure>
<p class="note"><strong>Webviews are sandboxed processes:</strong>
The enclosing Chrome App (also known as the "embedder page")
cannot easily access the webview's loaded DOM.
You can only interact with the webview using its API.</p>
<h2 id="implement-webview">Implement the webview tag</h2>
<p>Update the Todo app to search for URLs in the todo item text and create a hyperlink.
The link, when clicked, opens a new Chrome App window
(not a browser tab) with a webview presenting the content.</p>
<h3 id="update-permissions">Update permissions</h3>
<p>In <strong><em>manifest.json</em></strong>, request the <code>webview</code> permission:</p>
<pre data-filename="manifest.json">
"permissions": ["storage", "alarms", "notifications"<b>, "webview"</b>],
</pre>
<h3 id="create-webview">Create a webview embedder page</h3>
<p>Create a new file in the root of your project folder and name it <strong><em>webview.html</em></strong>.
This file is a basic webpage with one <code>&lt;webview&gt;</code> tag:</p>
<pre data-filename="webview.html">
<b>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;webview style="width: 100%; height: 100%;"&gt;&lt;/webview&gt;
&lt;/body&gt;
&lt;/html&gt;</b>
</pre>
<h3 id="parse-urls">Parse for URLs in todo items</h3>
<p>At the end of <strong><em>controller.js</em></strong>, add a new method called <code>_parseForURLs()</code>:</p>
<pre data-filename="controller.js">
Controller.prototype._getCurrentPage = function () {
return document.location.hash.split('/')[1];
};
<b>Controller.prototype._parseForURLs = function (text) {</b>
<b> var re = /(https?:\/\/[^\s"&lt;&gt;,]+)/g;</b>
<b> return text.replace(re, '&lt;a href="$1" data-src="$1"&gt;$1&lt;/a&gt;');</b>
<b>};</b>
// Export to window
window.app.Controller = Controller;
})(window);
</pre>
<p>Whenever a string starting with "http://" or "https://" is found, a HTML anchor tag is created to wrap around the URL.</p>
<h3 id="render-links">Render hyperlinks in the todo list</h3>
<p>Find <code>showAll()</code> in <em>controller.js</em>. Update <code>showAll()</code>
to parse for links by using the <code>_parseForURLs()</code> method added previously:</p>
<pre data-filename="controller.js">
/**
* An event to fire on load. Will get all items and display them in the
* todo-list
*/
Controller.prototype.showAll = function () {
this.model.read(function (data) {
<strike>this.$todoList.innerHTML = this.view.show(data);</strike>
<b>this.$todoList.innerHTML = this._parseForURLs(this.view.show(data));</b>
}.bind(this));
};
</pre>
<p>Do the same for <code>showActive()</code> and <code>showCompleted()</code>:</p>
<pre data-filename="controller.js">
/**
* Renders all active tasks
*/
Controller.prototype.showActive = function () {
this.model.read({ completed: 0 }, function (data) {
<strike>this.$todoList.innerHTML = this.view.show(data);</strike>
<b>this.$todoList.innerHTML = this._parseForURLs(this.view.show(data));</b>
}.bind(this));
};
/**
* Renders all completed tasks
*/
Controller.prototype.showCompleted = function () {
this.model.read({ completed: 1 }, function (data) {
<strike>this.$todoList.innerHTML = this.view.show(data);</strike>
<b>this.$todoList.innerHTML = this._parseForURLs(this.view.show(data));</b>
}.bind(this));
};
</pre>
<p>And finally, add <code>_parseForURLs()</code> to <code>editItem()</code>:</p>
<pre data-filename="controller.js">
Controller.prototype.editItem = function (id, label) {
...
var onSaveHandler = function () {
...
// Instead of re-rendering the whole view just update
// this piece of it
<strike>label.innerHTML = value;</strike>
<b>label.innerHTML = this._parseForURLs(value);</b>
...
}.bind(this);
...
}
</pre>
<p>Still in <code>editItem()</code>, fix the code so that it uses the
<code>innerText</code> of the label instead of the label's <code>innerHTML</code>:</p>
<pre data-filename="controller.js">
Controller.prototype.editItem = function (id, label) {
...
// Get the <strike>innerHTML</strike> <b>innerText</b> of the label instead of requesting the data from the
// ORM. If this were a real DB this would save a lot of time and would avoid
// a spinner gif.
<strike>input.value = label.innerHTML;</strike>
<b>input.value = label.innerText;</b>
...
}
</pre>
<h3 id="open-webview">Open new window containing webview</h3>
<p>Add a <code>_doShowUrl()</code> method to <em>controller.js</em>. This method opens a new Chrome App window
via <a href="/apps/app_window.html#method-create">chrome.app.window.create()</a>
with <em>webview.html</em> as the window source:</p>
<pre data-filename="controller.js">
Controller.prototype._parseForURLs = function (text) {
var re = /(https?:\/\/[^\s"&lt;&gt;,]+)/g;
return text.replace(re, '&lt;a href="$1" data-src="$1"&gt;$1&lt;/a&gt;');
};
<b>Controller.prototype._doShowUrl = function(e) {</b>
<b> // only applies to elements with data-src attributes</b>
<b> if (!e.target.hasAttribute('data-src')) {</b>
<b> return;</b>
<b> }</b>
<b> e.preventDefault();</b>
<b> var url = e.target.getAttribute('data-src');</b>
<b> chrome.app.window.create(</b>
<b> 'webview.html',</b>
<b> {hidden: true}, // only show window when webview is configured</b>
<b> function(appWin) {</b>
<b> appWin.contentWindow.addEventListener('DOMContentLoaded',</b>
<b> function(e) {</b>
<b> // when window is loaded, set webview source</b>
<b> var webview = appWin.contentWindow.</b>
<b> document.querySelector('webview');</b>
<b> webview.src = url;</b>
<b> // now we can show it:</b>
<b> appWin.show();</b>
<b> }</b>
<b> );</b>
<b> });</b>
<b>};</b>
// Export to window
window.app.Controller = Controller;
})(window);
</pre>
<p>In the <code>chrome.app.window.create()</code> callback, note how the webview's URL is set via the <a href="/apps/tags/webview#tag"><code>src</code> tag attribute</a>.</p>
<p>Lastly, add a click event listener inside the <code>Controller</code> constructor to call
<code>doShowUrl()</code> when a user clicks on a link:</p>
<pre data-filename="controller.js">
function Controller(model, view) {
...
this.router = new Router();
this.router.init();
<b>this.$todoList.addEventListener('click', this._doShowUrl);</b>
window.addEventListener('load', function () {
this._updateFilterState();
}.bind(this));
...
}
</pre>
<h2 id="launch">Launch your finished Todo app</h2>
<p>You are done Step 4! If you reload your app and add a todo item with a full URL
starting with http:// or https://, you should see something like this:</p>
<figure>
<img src="{{static}}/images/app_codelab/step4-completed.gif" alt="The Todo app with webview"/>
</figure>
<h2 id="recap">For more information</h2>
<p>For more detailed information about some of the APIs introduced in this step, refer to:</p>
<ul>
<li>
<a href="/apps/declare_permissions" title="Read 'Declare Permissions' in the Chrome developer docs">Declare Permissions</a>
<a href="#update-permissions" class="anchor-link-icon" title="This feature mentioned in 'Update app permissions'">&#8593;</a>
</li>
<li>
<a href="/apps/tags/webview" title="Read '&lt;webview&gt; Tag' in the Chrome developer docs">&lt;webview&gt; Tag</a>
<a href="#overview" class="anchor-link-icon" title="This feature mentioned in 'Learn about the webview tag'">&#8593;</a>
</li>
<li>
<a href="/apps/app_window.html#method-create" title="Read 'chrome.app.window.create()' in the Chrome developer docs">chrome.app.window.create()</a>
<a href="#open-webview" class="anchor-link-icon" title="This feature mentioned in 'Open new window containing webview'">&#8593;</a>
</li>
</ul>
<p>Ready to continue onto the next step? Go to <a href="app_codelab_images.html">Step 5 - Add images from the web &raquo;</a></p>