Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/webview_example/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
)
36 changes: 36 additions & 0 deletions apps/webview_example/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -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<IconButtonViewModel> {
onRender(): void {
<view style={styles.button} onTap={this.viewModel.onTap}>
<image style={styles.icon} src={this.viewModel.icon} />
</view>;
}
}

const styles = {
button: new Style<View>({
width: 40,
height: 40,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
marginRight: 6,
backgroundColor: '#eef1f5',
}),

icon: new Style<ImageView>({
width: 22,
height: 22,
objectFit: 'contain',
touchEnabled: false,
}),
};
165 changes: 165 additions & 0 deletions apps/webview_example/WebViewExample.tsx
Original file line number Diff line number Diff line change
@@ -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 {
<view style={styles.root}>
<view
style={styles.navigationBar}
height={BAR_HEIGHT + Device.getDisplayTopInset()}
paddingTop={Device.getDisplayTopInset()}
>
<IconButton icon={res.back} onTap={this.goBack} />
<IconButton icon={res.forward} onTap={this.goForward} />
<IconButton icon={res.reload} onTap={this.reload} />
<view style={styles.urlFieldContainer}>
<textfield
style={styles.urlField}
value={this.state.urlText}
onChange={this.onUrlTextChange}
onEditEnd={this.onUrlEditEnd}
/>
</view>
</view>
<webview controller={this.webViewController} style={styles.webview} />
</view>;
}

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<TextField['onChange']> = event => {
this.setState({ urlText: event.text });
};

private readonly onUrlEditEnd: NonNullable<TextField['onEditEnd']> = 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<View>({
backgroundColor: 'white',
width: '100%',
height: '100%',
}),

navigationBar: new Style<View>({
width: '100%',
paddingLeft: 10,
paddingRight: 10,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f8fafc',
}),

urlFieldContainer: new Style<View>({
flexGrow: 1,
height: 40,
borderRadius: 10,
backgroundColor: 'white',
borderWidth: 1,
borderColor: '#cbd5e1',
paddingLeft: 12,
paddingRight: 12,
justifyContent: 'center',
}),

urlField: new Style<TextField>({
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<WebViewElement>({
width: '100%',
flexGrow: 1,
}),
};
4 changes: 4 additions & 0 deletions apps/webview_example/res/back.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/webview_example/res/forward.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/webview_example/res/reload.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions docs/api/api-quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,18 @@ class MyComponent extends Component {
</scroll>
```

### Web Content

```tsx
<webview
width="100%"
height={400}
controller={this.webViewController}
/>
```

Create the controller with `WebView.createController()` from `valdi_webview`, then call controller methods such as `load`, `reload`, and `goBack` imperatively.

### Form Input

```tsx
Expand Down
27 changes: 27 additions & 0 deletions docs/api/api-reference-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -825,6 +826,32 @@ All view properties from [View](#view) including:

---

## WebView

**JSX Element:** `<webview>`

**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<WebViewElement | View | Layout>`
- See [View Style Attributes](api-style-attributes.md#view-styles) for common view styling attributes.

---

## VideoView

**JSX Element:** `<video>`
Expand Down
1 change: 1 addition & 0 deletions src/valdi_modules/src/valdi/valdi_core/src/JSXBootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ jsx.registerNativeElement('label', 'SCValdiLabel', 'com.snap.valdi.views.ValdiTe
jsx.registerNativeElement('scroll', 'SCValdiScrollView', 'com.snap.valdi.views.ValdiScrollView');
jsx.registerNativeElement('image', 'SCValdiImageView', 'com.snap.valdi.views.ValdiImageView');
jsx.registerNativeElement('video', 'SCValdiVideoView', 'com.snap.valdi.views.ValdiVideoView');
jsx.registerNativeElement('webview', 'SCValdiWebView', 'com.snap.valdi.modules.webview.ValdiWebView');
jsx.registerNativeElement('button', 'UIButton', 'android.widget.Button');
jsx.registerNativeElement('spinner', 'SCValdiSpinnerView', 'com.snap.valdi.views.ValdiSpinnerView');
jsx.registerNativeElement('blur', 'SCValdiBlurView', 'com.snap.valdi.views.ValdiView');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { AnimatedImage, ImageView, Label, Layout, ScrollView, ShapeView, SpinnerView, TextField, TextView, View } from "valdi_tsx/src/NativeTemplateElements";
import { AnimatedImage, ImageView, Label, Layout, ScrollView, ShapeView, SpinnerView, TextField, TextView, View, WebView } from "valdi_tsx/src/NativeTemplateElements";
import { IRenderedElementViewClass } from "./IRenderedElementViewClass";

type Mapping = {
[IRenderedElementViewClass.View]: View;
[IRenderedElementViewClass.Layout]: Layout;
[IRenderedElementViewClass.Label]: Label;
[IRenderedElementViewClass.Image]: ImageView;
[IRenderedElementViewClass.WebView]: WebView;
[IRenderedElementViewClass.Spinner]: SpinnerView;
[IRenderedElementViewClass.TextField]: TextField;
[IRenderedElementViewClass.TextView]: TextView;
Expand All @@ -14,4 +15,4 @@ type Mapping = {
[IRenderedElementViewClass.AnimatedImage]: AnimatedImage;
}

export type ElementForViewClass<T extends IRenderedElementViewClass> = Mapping[T];
export type ElementForViewClass<T extends IRenderedElementViewClass> = Mapping[T];
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum IRenderedElementViewClass {
View = 'SCValdiView',
Label = 'SCValdiLabel',
Image = 'SCValdiImageView',
WebView = 'SCValdiWebView',
Spinner = 'SCValdiSpinnerView',
TextField = 'SCValdiTextField',
TextView = 'SCValdiTextView',
Expand Down
19 changes: 19 additions & 0 deletions src/valdi_modules/src/valdi/valdi_test/test/JSXTest.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, StatefulComponent } from 'valdi_core/src/Component';
import { IWebViewNativeController } from 'valdi_tsx/src/NativeTemplateElements';
import 'jasmine/src/jasmine';
import { createComponent, makeComponentTest } from './JSXTestUtils';

Expand Down Expand Up @@ -43,6 +44,24 @@ describe('JSX', () => {
});
});

it('can render a webview element', async () => {
class WebViewComponent extends Component {
private controller: IWebViewNativeController = {};

onRender() {
<webview controller={this.controller} />;
}
}

const component = createComponent(WebViewComponent);
const rootNode = await component.getRenderedNode();
const simplified = rootNode.simplify(['viewClass', 'attributes']);
const attributes = simplified.attributes!;

expect(simplified.viewClass).toEqual('SCValdiWebView');
expect(attributes.controller).toBeDefined();
});

it('can render a component with view model', async () => {
interface ViewModel {
text: string;
Expand Down
2 changes: 2 additions & 0 deletions src/valdi_modules/src/valdi/valdi_tsx/src/JSX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ContainerTemplateElement,
ImageView,
VideoView,
WebViewElement,
Label,
Layout,
ScrollView,
Expand Down Expand Up @@ -43,6 +44,7 @@ declare global {
label: Label;
image: ImageView;
video: VideoView;
webview: WebViewElement;
textfield: TextField;
textview: TextView;
blur: BlurView;
Expand Down
Loading
Loading