The problem I have is that the WebView does not load height dynamically in iOS (in Android if it does), the question is that my content is dynamic and can grow high, and putting the fixed height would not work for me. Could you help me?
<CardView *ngFor="let itinerario of itinerario" class="card" elevation="40" radius="10" ios:shadowRadius="3">
<StackLayout class="card-layout text-center">
<WebView [src]="itinerario.mEstructura" height="auto"></WebView>
</StackLayout>
</CardView>
Use native method to evaluate JavaScript that can return height of the document.
HTML
<GridLayout>
<ScrollView class="page">
<StackLayout>
<WebView src="https://www.nativescript.org/" [height]="height"
(loadFinished)="onWebViewLoadFinished($event)"></WebView>
<Button class="btn btn-primary" text="Hello!"></Button>
</StackLayout>
</ScrollView>
</GridLayout>
TS
onWebViewLoadFinished(event: EventData) {
const webView = <WebView>event.object,
jsStr = `var body = document.body;
var html = document.documentElement;
Math.max( body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight);`;
if (webView.ios) {
webView.ios.scrollView.scrollEnabled = false;
webView.ios.evaluateJavaScriptCompletionHandler(jsStr,
(
result,
error
) => {
if (error) {
console.log("error...");
} else if (result) {
this.height = layout.toDeviceIndependentPixels(result);
this.changeDetectorRef.detectChanges();
}
});
} else if (webView.android) {
// Works only on Android 19 and above
webView.android.evaluateJavascript(
jsStr,
new android.webkit.ValueCallback({
onReceiveValue: (height) => {
this.height = layout.toDeviceIndependentPixels(height);
this.changeDetectorRef.detectChanges();
}
})
);
}
}
Playground Sample
I adapted #manoj's answer for nativescript-vue.
<template>
<ScrollView>
<StackLayout>
<WebView
src="https://www.nativescript.org/"
#loadFinished="loadFinished"
:ios:height="iosHeight"
/>
</StackLayout>
</ScrollView>
</template>
<script>
export default {
methods: {
loadFinished(args) {
if (!global.isIOS) {
return;
}
const webview = args.object;
const jsStr = `let body = document.body;
let html = document.documentElement;
Math.max(body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight);`;
webview.ios.scrollView.scrollEnabled = false;
webview.ios.evaluateJavaScriptCompletionHandler(
jsStr,
(result, error) => {
if (result) {
this.iosHeight = result;
}
}
);
},
},
};
</script>
Related
<style>
.hide .el-upload {
display: none;
}
</style>
<el-upload
class="w-2d5 upload-box"
:class="{hide:upLoadHide}"
action=""
accept=".png,.jpg,.jpeg"
:on-preview="handlePreview"
:on-remove="handleRemove"
:http-request="customUpload"
:file-list="fileList"
list-type="picture-card"
:limit="1"
:on-change="handleChange"
>
<div>
<el-icon><Plus /></el-icon>
</div>
</el-upload>
const handleChange = () => {
upLoadHide.value = true
}
const handleRemove = () => {
upLoadHide.value = false
}
Now this is done, but adding a new image does not show the upload button
dialog
Add new picture display upload button
Please provide us with more details to help you with your problem.
Here is the working playground with Element+ Upload Control.
const { ref, createApp } = Vue;
const App = {
setup() {
const fileList = ref([]);
const upLoadHide = ref(false);
const handleChange = () => {
upLoadHide.value = true
}
const handleRemove = () => {
upLoadHide.value = false
}
const handlePreview = () => {
}
const customUpload = () => {
}
return {
fileList,
upLoadHide,
handleChange,
handleRemove,
handlePreview,
customUpload
}
}
}
const app = createApp(App)
app.use(ElementPlus);
app.mount('#app')
.hide .el-upload {
display: none;
}
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<div id="app">
<el-upload
class="w-2d5 upload-box"
:class="{hide:upLoadHide}"
action=""
accept=".png,.jpg,.jpeg"
:on-preview="handlePreview"
:on-remove="handleRemove"
:http-request="customUpload"
:file-list="fileList"
list-type="picture-card"
:limit="1"
:on-change="handleChange"
>
<div>
<el-icon><Plus /></el-icon>
</div>
</el-upload>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus"></script>
// store.js
import { createStore, combineReducers } from "redux";
const INITIAL_STATE = [];
const postingReducer = (state, action) => {
if (action.type === "POST_SUCCESS") {
const newPost = {
title: action.payload.title,
content: action.payload.content,
};
return state.concat(newPost);
} else if (action.type === "POST_DELETE") {
return state.filter((item) => {
return item.title !== action.payload;
});
} else if (action.type === "POST_EDIT_SUCCESS") {
const modifiedPost = {
title: action.payload.title,
content: action.payload.content,
};
// console.log(modifiedPost)
// console.log(state.map((item,index)=>item[0]))
// console.log(state[0].title)
// state[0].title = modifiedPost.title
// state[0].content = modifiedPost.title
return state.concat(modifiedPost);
}
return INITIAL_STATE;
};
const store = createStore(
combineReducers({
posting: postingReducer,
})
);
export default store;
// EditForm.js
import { Link , useParams} from "react-router-dom";
import { useState } from "react";
import { useDispatch , useSelector } from "react-redux";
const EditForm = () => {
const dispatch = useDispatch();
const state = useSelector(state=>state.posting)
const params = useParams()
const [titleInput, setTitleInput] = useState(state[params.title].title);
const [contentInput, setContentInput] = useState(state[params.title].content);
const publishBtn = () => {
window.alert('Modified.')
return dispatch({
type: "POST_EDIT_SUCCESS",
payload: { title: titleInput, content: contentInput },
});
}
return (
<>
<form>
<div>
<label htmlFor="Title">Title</label>
<br />
<input
id="Title"
value={titleInput}
onChange={event=>{setTitleInput(event.target.value)}}
type="text"
/>
<br />
</div>
<br />
<div>
<label htmlFor="Content">Content</label>
<br />
<textarea
id="Content"
value={contentInput}
onChange={event=>{setContentInput(event.target.value)}}
type="text"
/>
<br />
</div>
<div>
<Link to="/">
<button onClick={publishBtn}>Modify</button>
</Link>
</div>
</form>
</>
);
};
export default EditForm
I want to get my modified post, net ADD modified post on list.
when i click Modify button in 'EditForm.js' I want to get my modified post, net ADD modified post on list.
in this situation, modified post added on the postlist.
I don't know how to fix "POST_EDIT_SUCCESS" return in 'store.js'
please help me!
I am developping a Nativscript-Vue mobile app, and I need to be able to select a file from the image gallery and upload it to an online file manager (I use 000webhost.com).
I worked with a code sample I got from the NativeScript website, and I am able to get the picture without problems, but I can't manage to upload it to WebHost (the logs says that the upload is successful, and there's no error showing up, but my pictures folder remains empty for some reason).
Here's my code :
<template>
<Page class="page">
<ActionBar title="imagepicker + background-http" class="action-bar"></ActionBar>
<GridLayout rows="*, auto">
<GridLayout v-if="!showWelcome" rows="auto auto auto, *">
<Progress :value="event && event.eventData ? event.eventData.currentBytes : 0"
:maxValue="event && event.eventData ? event.eventData.totalBytes : 0">
</Progress>
<Label row="1" v-if="event && event.eventData && event.eventData.currentBytes !== NaN"
class="m-10 text" :text="'Transferred: ' + event.eventData.currentBytes / 1000 + ' KB'"></Label>
<Label row="2" class="m-10 text" text="Events"></Label>
<ListView row="3" for="item in eventLog">
<v-template>
<StackLayout class="t-12">
<Label :text="item.eventTitle" textWrap="true"></Label>
<Label v-if="item.eventData && item.eventData.error"
:text="item.eventData ? 'Error: ' + item.eventData.error : ''"
textWrap="true"></Label>
<Label v-if="item.eventData && item.eventData.body"
:text="item.eventData ? 'Body: ' + item.eventData.body : ''"
textWrap="true"></Label>
<Label v-if="item.eventData && item.eventData.raw"
:text="item.eventData ? 'Raw: ' + item.eventData.raw : ''"
textWrap="true"></Label>
</StackLayout>
</v-template>
</ListView>
</GridLayout>
<StackLayout v-if="showWelcome" verticalAlignment="middle">
<Label class="m-10 nativescript-label text" text="{N}"></Label>
<Label class="m-10 text" v-if="showWelcome" text="This sample app shows how to pick an image with"
textWrap="true"></Label>
<Label class="m-10 text plugin" text="nativescript-imagepicker"></Label>
<Label class="m-10 text" v-if="showWelcome" text="and upload it using"
textWrap="true"></Label>
<Label class="m-10 text plugin" text="nativescript-background-http"></Label>
</StackLayout>
<Button class="m-b-10 m-t-10 t-20" row="1" text="Choose image to upload" #tap="onSelectImageTap($event)"></Button>
</GridLayout>
</Page>
</template>
<script>
import VueRx from "../vue-rx";
import Vue from "nativescript-vue";
const app = require("tns-core-modules/application");
const platform = require("platform");
const fs = require("file-system");
const imagePicker = require("nativescript-imagepicker");
const rxjs = require("rxjs");
const operators = require("rxjs/operators");
const bgHttp = require("nativescript-background-http");
Vue.use(VueRx);
// Vue.config.silent = false; // uncomment for debugging purposes
export default {
data() {
return {
showWelcome: true,
session: bgHttp.session("image-upload"),
currentFileNameBeingUploaded: ""
};
},
subscriptions() {
this.event$ = new rxjs.BehaviorSubject({});
return {
event: this.event$,
eventLog: this.event$.pipe(
operators.sampleTime(200),
operators.concatMap(value => rxjs.of(value)),
operators.scan((acc, logEntry) => {
acc.push(logEntry);
return acc;
}, []),
// emit only logs for the this.currentFileNameBeingUploaded
operators.map(allLogs => allLogs.filter(logEntry => !!logEntry && logEntry.eventTitle && logEntry.eventTitle.indexOf(this.currentFileNameBeingUploaded) > 0))
)
};
},
methods: {
onSelectImageTap() {
let context = imagePicker.create({
mode: "single"
});
this.startSelection(context);
},
startSelection(context) {
context
.authorize()
.then(() => {
return context.present();
})
.then(selection => {
this.showWelcome = false;
let imageAsset = selection.length > 0 ? selection[
0] : null;
if (imageAsset) {
this.getImageFilePath(imageAsset).then(path => {
console.log(`path: ${path}`);
this.uploadImage(path);
});
}
})
.catch(function(e) {
console.log(e);
});
},
uploadImage(path) {
let file = fs.File.fromPath(path);
this.currentFileNameBeingUploaded = file.path.substr(
file.path.lastIndexOf("/") + 1
);
let request = this.createNewRequest();
request.description = "uploading image " + file.path;
request.headers["File-Name"] = this.currentFileNameBeingUploaded;
// -----> multipart upload
// var params = [{
// name: "test",
// value: "value"
// },
// {
// name: "fileToUpload",
// filename: file.path,
// mimeType: "image/jpeg"
// }
// ];
// var task = this.session.multipartUpload(params, request);
// <----- multipart upload
let task = this.session.uploadFile(file.path, request);
task.on("progress", this.onEvent.bind(this));
task.on("error", this.onEvent.bind(this));
task.on("responded", this.onEvent.bind(this));
task.on("complete", this.onEvent.bind(this));
},
createNewRequest() {
let url;
// NOTE: using https://httpbin.org/post for testing purposes,
// you'll need to use your own service in real-world app
if (platform.isIOS) {
url = "https://ipaccovoiturage.000webhostapp.com/pictures";
} else {
url = "https://ipaccovoiturage.000webhostapp.com/pictures";
}
let request = {
url: url,
method: "POST",
headers: {
"Content-Type": "application/octet-stream"
},
description: "uploading file...",
androidAutoDeleteAfterUpload: false,
androidNotificationTitle: "NativeScript HTTP background"
};
return request;
},
getImageFilePath(imageAsset) {
return new Promise(resolve => {
if (platform.isIOS) {
const options = PHImageRequestOptions.new();
options.synchronous = true;
options.version =
PHImageRequestOptionsVersion.Current;
options.deliveryMode =
PHImageRequestOptionsDeliveryMode.HighQualityFormat;
PHImageManager.defaultManager().requestImageDataForAssetOptionsResultHandler(
imageAsset.ios,
options,
nsData => {
// create file from image asset and return its path
const tempFolderPath = fs.knownFolders
.temp()
.getFolder("nsimagepicker").path;
const tempFilePath = fs.path.join(
tempFolderPath,
Date.now() + ".jpg"
);
nsData.writeToFileAtomically(
tempFilePath, true);
resolve(tempFilePath);
}
);
} else {
// return imageAsset.android, since it 's the path of the file
resolve(imageAsset.android);
}
});
},
onEvent(e) {
let eventEntry = {
eventTitle: e.eventName + " " + e.object.description,
eventData: {
error: e.error ? e.error.toString() : e.error,
currentBytes: e.currentBytes,
totalBytes: e.totalBytes,
body: e.data
// raw: JSON.stringify(e) // uncomment for debugging purposes
}
};
this.event$.next(eventEntry);
}
}
};
</script>
<style scoped>
.home-panel {
vertical-align: center;
font-size: 20;
margin: 15;
}
.description-label {
margin-bottom: 15;
}
.text {
text-align:
center;
font-size: 18px;
}
.plugin {
font-weight: 600;
font-size: 23px;
}
.nativescript-label {
font-size: 60px;
background-color: #3d5afe;
font-weight: 600;
color: white;
border-radius: 20px/20px;
width: 230px;
height: 230px;
}
</style>
I thought maybe WebHost was the problem, but I've been able to insert photos with PHP and HTML (unfortunately NativeScript-Vue doesn't allow HTML).
I had a similar issue in plain javascript. I was working on it for a couple of days. #Manoj answers didn't work for me. I had to use multipartUpload since I am sending an additional parameter.
Make sure to match the name parameter (fileToUpload) in the request to the name expected by your service. "file" in my case.
The service API signature looks like:
public async Task<IActionResult> Upload(string listingId, IFormFile file)
My request:
const request = {
url: "yoururl",
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
"Authorization": token
},
description: `Uploading image ${name}`,
androidAutoDeleteAfterUpload: true,
androidNotificationTitle: "HTTP background"
};
const params = [
{
name: "listingId",
value: 2
},
{
name: "file", // Note it is not fileToUpload but file instead.
filename: path,
mimeType: "image/jpeg"
}
];
const task = session.multipartUpload(params, request);
I hope it helps. By the way both Content-Type multipart/form-data and application/octet-stream work for me, but I use multipart/form-data for consistency
I'm building an app with React-Native.
I'm trying to render a URL with a Webview, even some basic HTML or a site like m.facebook.com will not render in iOS. Tried different solution like other Webviews but none give results.
When in Android i don't have any problems and the page(s) will render just fine. Am i missing some key information.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions } from 'react-native-navigation-redux-helpers';
import { Container, Header, Title, Content, Button, Icon, Footer } from 'native-base';
import { Platform, WebView } from 'react-native';
import FooterTabs from '../../components/footerTabs/FooterTabs';
import { openDrawer } from '../../actions/drawer';
import { setWebsiteUrl } from '../../actions/website';
import styles from './styles';
const {
pushRoute,
popRoute,
} = actions;
class Website extends Component {
constructor(props, context) {
super(props, context);
this.state = {};
this.openFrame = this.openFrame.bind(this);
}
popRoute() {
this.props.popRoute(this.props.navigation.key);
}
pushRoute(route, index) {
this.props.pushRoute({ key: route, index: 1 }, this.props.navigation.key);
}
openFrame(url, name) {
this.props.setWebsiteUrl(url, name);
this.pushRoute('website', 2);
}
render() {
const { props } = this;
const { website, totalQuantity } = props;
const { frameUrl, frameName } = website;
return (
<Container style={styles.container} theme={deenTheme}>
<Header toolbarDefaultBg="#FFF" toolbarTextColor="FBFAFA">
<Button transparent onPress={this.props.openDrawer}>
<Icon name="ios-menu" />
</Button>
<Title style={styles.headerText}>{frameName}</Title>
<Button transparent onPress={() => this.openFrame('/cart', 'Winkelwagen')}>
<Icon style={{ fontSize: 25 }} name="md-basket" />
</Button>
<Button transparent>
<Icon style={{ fontSize: 25 }} name="md-search" />
</Button>
</Header>
<Content>
<WebView
source={{ uri: frameUrl }}
startInLoadingState
javaScriptEnabledAndroid
javaScriptEnabled
domStorageEnabled
scrollEnabled
style={{ flex: 1, width: 320 }}
/>
</Content>
<Footer theme={deenTheme}>
<FooterTabs />
</Footer>
</Container>
);
}
}
Website.propTypes = {
totalQuantity: React.PropTypes.number,
openDrawer: React.PropTypes.func,
setWebsiteUrl: React.PropTypes.func,
popRoute: React.PropTypes.func,
pushRoute: React.PropTypes.func,
navigation: React.PropTypes.shape({
key: React.PropTypes.string,
}),
};
function bindAction(dispatch) {
return {
openDrawer: () => dispatch(openDrawer()),
popRoute: key => dispatch(popRoute(key)),
pushRoute: (route, key) => dispatch(pushRoute(route, key)),
setWebsiteUrl: (url, name) => dispatch(setWebsiteUrl(url, name)),
};
}
const mapStateToProps = state => ({
navigation: state.cardNavigation,
website: state.website,
totalQuantity: state.cart.totalQuantity,
});
export default connect(mapStateToProps, bindAction)(Website);
UPDATE!
in iOS i need to configure style width & height else it won't work.
I'm implementing a WebView with dynamic height. I found the solution that works like a charm on iOS and doesn't work on android. The solution uses JS inside the WV to set the title to the value of the content height. Here's the code:
...
this.state = {webViewHeight: 0};
...
<WebView
source={{html: this.wrapWevViewHtml(this.state.content)}}
style={{width: Dimensions.get('window').width - 20, height: this.state.webViewHeight}}
scrollEnabled={false}
javaScriptEnabled={true}
injectedJavaScript="window.location.hash = 1;document.title = document.height;"
onNavigationStateChange={this.onWebViewNavigationStateChange.bind(this)}
/>
...
onWebViewNavigationStateChange(navState) {
// navState.title == height on iOS and html content on android
if (navState.title) {
this.setState({
webViewHeight: Number(navState.title)
});
}
}
...
But on android the value of the title inside onWebViewNavigationStateChange is equal to page content.
What am I doing wrong?
I was baffled by this too. It actually works but it's hard to debug why it does not work because Chrome remote debugging is not enabled for the React Native WebViews on Android.
I had two issues with this:
The script I injected to the Webview contained some single line comments and on Android all the line breaks are removed (another bug?). It caused syntax errors in the WebView.
On the first call the title content indeed is the full content of the Webview. No idea why but on latter calls it's the height. So just handle that case.
Here's the code I'm using now which on works on React Native 0.22 on Android and iOS
import React, {WebView, View, Text} from "react-native";
const BODY_TAG_PATTERN = /\<\/ *body\>/;
// Do not add any comments to this! It will break line breaks will removed for
// some weird reason.
var script = `
;(function() {
var wrapper = document.createElement("div");
wrapper.id = "height-wrapper";
while (document.body.firstChild) {
wrapper.appendChild(document.body.firstChild);
}
document.body.appendChild(wrapper);
var i = 0;
function updateHeight() {
document.title = wrapper.clientHeight;
window.location.hash = ++i;
}
updateHeight();
window.addEventListener("load", function() {
updateHeight();
setTimeout(updateHeight, 1000);
});
window.addEventListener("resize", updateHeight);
}());
`;
const style = `
<style>
body, html, #height-wrapper {
margin: 0;
padding: 0;
}
#height-wrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
}
</style>
<script>
${script}
</script>
`;
const codeInject = (html) => html.replace(BODY_TAG_PATTERN, style + "</body>");
/**
* Wrapped Webview which automatically sets the height according to the
* content. Scrolling is always disabled. Required when the Webview is embedded
* into a ScrollView with other components.
*
* Inspired by this SO answer http://stackoverflow.com/a/33012545
* */
var WebViewAutoHeight = React.createClass({
propTypes: {
source: React.PropTypes.object.isRequired,
injectedJavaScript: React.PropTypes.string,
minHeight: React.PropTypes.number,
onNavigationStateChange: React.PropTypes.func,
style: WebView.propTypes.style,
},
getDefaultProps() {
return {minHeight: 100};
},
getInitialState() {
return {
realContentHeight: this.props.minHeight,
};
},
handleNavigationChange(navState) {
if (navState.title) {
const realContentHeight = parseInt(navState.title, 10) || 0; // turn NaN to 0
this.setState({realContentHeight});
}
if (typeof this.props.onNavigationStateChange === "function") {
this.props.onNavigationStateChange(navState);
}
},
render() {
const {source, style, minHeight, ...otherProps} = this.props;
const html = source.html;
if (!html) {
throw new Error("WebViewAutoHeight supports only source.html");
}
if (!BODY_TAG_PATTERN.test(html)) {
throw new Error("Cannot find </body> from: " + html);
}
return (
<View>
<WebView
{...otherProps}
source={{html: codeInject(html)}}
scrollEnabled={false}
style={[style, {height: Math.max(this.state.realContentHeight, minHeight)}]}
javaScriptEnabled
onNavigationStateChange={this.handleNavigationChange}
/>
{process.env.NODE_ENV !== "production" &&
<Text>Web content height: {this.state.realContentHeight}</Text>}
</View>
);
},
});
export default WebViewAutoHeight;
As gist https://gist.github.com/epeli/10c77c1710dd137a1335
Loading a local HTML file on the device and injecting JS was the only method I found to correctly set the title / hash in Android.
/app/src/main/assets/blank.html
<!doctype html>
<html>
<head>
<title id="title">Go Web!</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
</style>
</head>
<body>
<div id="content"></div>
<script>
var content = document.getElementById('content');
var fireEvent = function(event, data) {
document.title = data;
window.location.hash = event;
};
var setContent = function(html) {
content.innerHTML = html;
};
</script>
</body>
</html>
And the component
class ResizingWebView extends Component {
constructor(props) {
super(props)
this.state = {
height: 0
}
}
onNavigationStateChange(navState) {
var event = navState.url.split('#')[1]
var data = navState.title
console.log(event, data)
if (event == 'resize') {
this.setState({ height: data })
}
}
render() {
var scripts = "setContent('<h1>Yay!</h1>');fireEvent('resize', '300')";
return (
<WebView
source={{ uri: 'file:///android_asset/blank.html' }}
injectedJavaScript={ scripts }
scalesPageToFit={ false }
style={{ height: this.state.height }}
onNavigationStateChange={ this.onNavigationStateChange.bind(this) }
/>
)
}
}