diff --git a/apps/webview_example/BUILD.bazel b/apps/webview_example/BUILD.bazel new file mode 100644 index 00000000..2debf2f3 --- /dev/null +++ b/apps/webview_example/BUILD.bazel @@ -0,0 +1,28 @@ +load("//bzl/valdi:valdi_application.bzl", "valdi_application") +load("//bzl/valdi:valdi_module.bzl", "valdi_module") + +valdi_module( + name = "webview_example", + srcs = glob([ + "**/*.ts", + "**/*.tsx", + ]), + res = glob([ + "res/**/*.svg", + ]), + visibility = ["//visibility:public"], + deps = [ + "//src/valdi_modules/src/valdi/valdi_core", + "//src/valdi_modules/src/valdi/valdi_tsx", + "//src/valdi_modules/src/valdi/valdi_webview", + ], +) + +valdi_application( + name = "webview_example_app", + ios_bundle_id = "com.snap.valdi.webviewexample", + ios_families = ["iphone"], + root_component_path = "App@webview_example/WebViewExample", + title = "Valdi WebView Example", + deps = [":webview_example"], +) diff --git a/apps/webview_example/IconButton.tsx b/apps/webview_example/IconButton.tsx new file mode 100644 index 00000000..ed25ead3 --- /dev/null +++ b/apps/webview_example/IconButton.tsx @@ -0,0 +1,36 @@ +import { Component } from 'valdi_core/src/Component'; +import { Style } from 'valdi_core/src/Style'; +import { Asset } from 'valdi_tsx/src/Asset'; +import { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements'; + +export interface IconButtonViewModel { + icon: Asset; + onTap: () => void; +} + +export class IconButton extends Component { + onRender(): void { + + + ; + } +} + +const styles = { + button: new Style({ + width: 40, + height: 40, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + marginRight: 6, + backgroundColor: '#eef1f5', + }), + + icon: new Style({ + width: 22, + height: 22, + objectFit: 'contain', + touchEnabled: false, + }), +}; diff --git a/apps/webview_example/WebViewExample.tsx b/apps/webview_example/WebViewExample.tsx new file mode 100644 index 00000000..34cb8c7d --- /dev/null +++ b/apps/webview_example/WebViewExample.tsx @@ -0,0 +1,165 @@ +import { StatefulComponent } from 'valdi_core/src/Component'; +import { Device } from 'valdi_core/src/Device'; +import { Style } from 'valdi_core/src/Style'; +import { systemFont } from 'valdi_core/src/SystemFont'; +import { TextField, View, WebViewElement } from 'valdi_tsx/src/NativeTemplateElements'; +import { IWebViewController, IWebViewListener, WebView as WebViewModule } from 'valdi_webview/src/WebView'; + +import { IconButton } from './IconButton'; +import res from './res'; + +const SNAPCHAT_URL = 'https://www.snapchat.com'; +const BAR_HEIGHT = 56; + +interface State { + urlText: string; +} + +export class App extends StatefulComponent<{}, State> { + state: State = { + urlText: SNAPCHAT_URL, + }; + + private readonly webViewController: IWebViewController = WebViewModule.createController(); + + private readonly webViewListener: IWebViewListener = { + onMessage(message: string): void { + console.log(`WebView message: ${message}`); + }, + onLoadFailed(errorMessage: string): void { + console.log(`WebView load failed: ${errorMessage}`); + }, + onLoadCompleted(): void { + console.log('WebView load completed'); + }, + }; + + onCreate(): void { + this.registerDisposable(() => { + this.webViewController.dispose(); + }); + + this.webViewController.setListener(this.webViewListener); + this.webViewController.load({ url: SNAPCHAT_URL }); + } + + onDestroy(): void { + this.webViewController.setListener(undefined); + } + + onRender(): void { + + + + + + + + + + + ; + } + + private goBack = (): void => { + this.webViewController.getState().then(state => { + if (state.canGoBack) { + this.webViewController.goBack(); + } + }); + }; + + private goForward = (): void => { + this.webViewController.getState().then(state => { + if (state.canGoForward) { + this.webViewController.goForward(); + } + }); + }; + + private reload = (): void => { + this.webViewController.reload(); + }; + + private readonly onUrlTextChange: NonNullable = event => { + this.setState({ urlText: event.text }); + }; + + private readonly onUrlEditEnd: NonNullable = event => { + this.loadUrl(event.text); + }; + + private loadUrl(rawUrl: string): void { + const url = this.normalizeUrl(rawUrl); + this.setState({ urlText: url }); + this.webViewController.load({ url }); + } + + private normalizeUrl(rawUrl: string): string { + const trimmedUrl = rawUrl.trim(); + if (trimmedUrl.length === 0) { + return SNAPCHAT_URL; + } + if (trimmedUrl.indexOf('://') >= 0) { + return trimmedUrl; + } + return `https://${trimmedUrl}`; + } +} + +const styles = { + root: new Style({ + backgroundColor: 'white', + width: '100%', + height: '100%', + }), + + navigationBar: new Style({ + width: '100%', + paddingLeft: 10, + paddingRight: 10, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#f8fafc', + }), + + urlFieldContainer: new Style({ + flexGrow: 1, + height: 40, + borderRadius: 10, + backgroundColor: 'white', + borderWidth: 1, + borderColor: '#cbd5e1', + paddingLeft: 12, + paddingRight: 12, + justifyContent: 'center', + }), + + urlField: new Style({ + width: '100%', + height: 38, + color: '#0f172a', + tintColor: '#2563eb', + font: systemFont(15), + placeholder: 'Enter a URL', + contentType: 'url', + returnKeyText: 'go', + autocapitalization: 'none', + autocorrection: 'none', + closesWhenReturnKeyPressed: true, + }), + + webview: new Style({ + width: '100%', + flexGrow: 1, + }), +}; diff --git a/apps/webview_example/res/back.svg b/apps/webview_example/res/back.svg new file mode 100644 index 00000000..86e67dad --- /dev/null +++ b/apps/webview_example/res/back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/webview_example/res/forward.svg b/apps/webview_example/res/forward.svg new file mode 100644 index 00000000..2076dcd0 --- /dev/null +++ b/apps/webview_example/res/forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/webview_example/res/reload.svg b/apps/webview_example/res/reload.svg new file mode 100644 index 00000000..8775c671 --- /dev/null +++ b/apps/webview_example/res/reload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/api/api-quick-reference.md b/docs/api/api-quick-reference.md index 644a1b4a..06553490 100644 --- a/docs/api/api-quick-reference.md +++ b/docs/api/api-quick-reference.md @@ -477,6 +477,18 @@ class MyComponent extends Component { ``` +### Web Content + +```tsx + +``` + +Create the controller with `WebView.createController()` from `valdi_webview`, then call controller methods such as `load`, `reload`, and `goBack` imperatively. + ### Form Input ```tsx diff --git a/docs/api/api-reference-elements.md b/docs/api/api-reference-elements.md index ee86c36c..4ff71b69 100644 --- a/docs/api/api-reference-elements.md +++ b/docs/api/api-reference-elements.md @@ -8,6 +8,7 @@ This document provides a comprehensive reference for all native template element - [View](#view) - [ScrollView](#scrollview) - [ImageView](#imageview) +- [WebView](#webview) - [VideoView](#videoview) - [Label](#label) - [TextField](#textfield) @@ -825,6 +826,32 @@ All view properties from [View](#view) including: --- +## WebView + +**JSX Element:** `` + +**iOS Native:** `SCValdiWebView` +**Android Native:** `com.snap.valdi.views.ValdiWebView` + +A host element for a native webview controller created by the `valdi_webview` module. + +### Properties + +All properties from [Layout Attributes](#layout), plus: + +#### Controller + +**`controller`**: `IWebViewNativeController` +- Native webview controller created by `WebView.createController()` from `valdi_webview`. +- The controller owns the platform webview and is attached to this host element when the attribute is applied. + +#### Styling + +**`style`**: `IStyle` +- See [View Style Attributes](api-style-attributes.md#view-styles) for common view styling attributes. + +--- + ## VideoView **JSX Element:** `