diff --git a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs
index a3ca0cf583..abd22d82e7 100644
--- a/src/Framework/Framework/Configuration/DotvvmConfiguration.cs
+++ b/src/Framework/Framework/Configuration/DotvvmConfiguration.cs
@@ -364,6 +364,13 @@ private static void RegisterResources(DotvvmConfiguration configuration)
debugName: "DotVVM.Framework.obj.javascript.root_spa_debug.dotvvm-root.js"),
dependencies: new[] { ResourceConstants.KnockoutJSResourceName },
module: true);
+ configuration.Resources.RegisterScript(ResourceConstants.DotvvmResourceName + ".internal-webview",
+ new EmbeddedResourceLocation(
+ typeof(DotvvmConfiguration).Assembly,
+ "DotVVM.Framework.obj.javascript.root_webview.dotvvm-root.js",
+ debugName: "DotVVM.Framework.obj.javascript.root_webview_debug.dotvvm-root.js"),
+ dependencies: new[] { ResourceConstants.KnockoutJSResourceName },
+ module: true);
configuration.Resources.Register(ResourceConstants.DotvvmResourceName,
new InlineScriptResource(@"", ResourceRenderPosition.Anywhere, defer: true) {
Dependencies = new[] { ResourceConstants.DotvvmResourceName + ".internal" }
diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj
index 3dfdfd4085..962ff9d192 100644
--- a/src/Framework/Framework/DotVVM.Framework.csproj
+++ b/src/Framework/Framework/DotVVM.Framework.csproj
@@ -26,6 +26,8 @@
+
+
@@ -115,8 +117,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
NO_COLOR=1;FORCE_COLOR=0;TERM=dumb
diff --git a/src/Framework/Framework/Resources/Scripts/compileConstants.ts b/src/Framework/Framework/Resources/Scripts/compileConstants.ts
index dc84a64eba..e8c9ccd46f 100644
--- a/src/Framework/Framework/Resources/Scripts/compileConstants.ts
+++ b/src/Framework/Framework/Resources/Scripts/compileConstants.ts
@@ -1,6 +1,8 @@
declare var compileConstants : {
/** If the compiled bundle is for SPA applications */
isSpa: boolean,
+ /** If the compiled bundle is for WebView-hosted applications */
+ isWebview: boolean,
/** If the compiled bundle is unminified */
debug: boolean
};
diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts
index bd02db317c..2e695befd6 100644
--- a/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts
+++ b/src/Framework/Framework/Resources/Scripts/dotvvm-base.ts
@@ -16,6 +16,10 @@ type DotvvmCoreState = {
_viewModelCacheId?: string
_virtualDirectory: string
_initialUrl: string,
+ _routeName: string,
+ _routeParameters: {
+ [name: string]: any
+ },
_stateManager: StateManager
}
@@ -60,7 +64,8 @@ export function clearViewModelCache() {
delete getCoreState()._viewModelCache;
}
export function getCulture(): string { return getCoreState()._culture; }
-
+export function getRouteName(): string { return getCoreState()._routeName; }
+export function getRouteParameters(): { [name: string]: any } { return getCoreState()._routeParameters; }
export function getStateManager(): StateManager { return getCoreState()._stateManager }
let initialViewModelWrapper: any;
@@ -85,7 +90,9 @@ export function initCore(culture: string): void {
_culture: culture,
_initialUrl: thisViewModel.url,
_virtualDirectory: thisViewModel.virtualDirectory!,
- _stateManager: manager
+ _stateManager: manager,
+ _routeName: thisViewModel.routeName,
+ _routeParameters: thisViewModel.routeParameters
}
// store cached viewmodel
@@ -106,7 +113,9 @@ export function initCore(culture: string): void {
_culture: currentCoreState!._culture,
_initialUrl: a.serverResponseObject.url,
_virtualDirectory: a.serverResponseObject.virtualDirectory!,
- _stateManager: currentCoreState!._stateManager
+ _stateManager: currentCoreState!._stateManager,
+ _routeName: a.serverResponseObject.routeName,
+ _routeParameters: a.serverResponseObject.routeParameters
}
});
}
diff --git a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts
index 1064edfdef..e070ddb3bf 100644
--- a/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts
+++ b/src/Framework/Framework/Resources/Scripts/dotvvm-root.ts
@@ -1,4 +1,4 @@
-import { initCore, getViewModel, getViewModelObservable, initBindings, getCulture, getState, getStateManager } from "./dotvvm-base"
+import { initCore, getViewModel, getViewModelObservable, initBindings, getCulture, getState, getStateManager, getRouteName, getRouteParameters } from "./dotvvm-base"
import * as events from './events'
import * as spa from "./spa/spa"
import * as validation from './validation/validation'
@@ -30,6 +30,7 @@ import * as string from './utils/stringHelper'
import { StateManager } from "./state-manager"
import { DotvvmEvent } from "./events"
import * as dateTime from './utils/dateTimeHelper'
+import * as webview from './webview/webview'
if (window["dotvvm"]) {
throw new Error('DotVVM is already loaded!')
@@ -86,6 +87,8 @@ const dotvvmExports = {
get viewModel() { return getViewModel() }
}
},
+ get routeName() { return getRouteName() },
+ get routeParameters() { return getRouteParameters() },
get state() { return getState() },
patchState(a: any) {
getStateManager().patchState(a)
@@ -136,13 +139,18 @@ if (compileConstants.isSpa) {
(dotvvmExports as any).isSpaReady = isSpaReady;
(dotvvmExports as any).handleSpaNavigation = handleSpaNavigation;
}
-
+if (compileConstants.isWebview) {
+ (dotvvmExports as any).webview = webview;
+}
if (compileConstants.debug) {
(dotvvmExports as any).debug = true
}
declare global {
- const dotvvm: typeof dotvvmExports & {debug?: true, isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation};
+ const dotvvm: typeof dotvvmExports &
+ { isSpaReady?: typeof isSpaReady, handleSpaNavigation?: typeof handleSpaNavigation } &
+ { webview?: typeof webview } &
+ { debug?: true };
interface Window {
dotvvm: typeof dotvvmExports
diff --git a/src/Framework/Framework/Resources/Scripts/postback/http.ts b/src/Framework/Framework/Resources/Scripts/postback/http.ts
index 065df5d308..acda3e11bc 100644
--- a/src/Framework/Framework/Resources/Scripts/postback/http.ts
+++ b/src/Framework/Framework/Resources/Scripts/postback/http.ts
@@ -3,12 +3,15 @@ import { DotvvmPostbackError } from '../shared-classes';
import { logInfoVerbose, logWarning } from '../utils/logging';
import { keys } from '../utils/objects';
import { addLeadingSlash, concatUrl } from '../utils/uri';
+import { webMessageFetch } from '../webview/messaging';
export type WrappedResponse = {
readonly result: T,
readonly response?: Response
}
+const fetch = compileConstants.isWebview ? webMessageFetch : window.fetch;
+
export async function getJSON(url: string, spaPlaceHolderUniqueId?: string, signal?: AbortSignal, additionalHeaders?: { [key: string]: string }): Promise> {
const headers = new Headers();
headers.append('Accept', 'application/json');
diff --git a/src/Framework/Framework/Resources/Scripts/spa/spa.ts b/src/Framework/Framework/Resources/Scripts/spa/spa.ts
index 9b25581f71..e11154069b 100644
--- a/src/Framework/Framework/Resources/Scripts/spa/spa.ts
+++ b/src/Framework/Framework/Resources/Scripts/spa/spa.ts
@@ -14,7 +14,7 @@ export const isSpaReady = ko.observable(false);
export function init(): void {
const spaPlaceHolders = getSpaPlaceHolders();
if (spaPlaceHolders.length == 0) {
- throw new Error("No SpaContentPlaceHolder control was found!");
+ return; // if there are no SPA placeholder, ignore the SPA plugin
}
window.addEventListener("hashchange", event => handleHashChangeWithHistory(spaPlaceHolders, false));
diff --git a/src/Framework/Framework/Resources/Scripts/webview/messaging.ts b/src/Framework/Framework/Resources/Scripts/webview/messaging.ts
new file mode 100644
index 0000000000..995a59e723
--- /dev/null
+++ b/src/Framework/Framework/Resources/Scripts/webview/messaging.ts
@@ -0,0 +1,92 @@
+type ReceivedMessage = { messageId: number } &
+ (
+ { action: "HttpRequest", body: string, headers: [{ Key: string, Value: string }], status: number }
+ );
+
+const pendingRequests: { resolve: (result: any) => void, reject: (result: any) => void }[] = [];
+
+// send messages
+export function sendMessage(message: any) {
+ (window.external as any).sendMessage(message);
+}
+
+export async function sendMessageAndWaitForResponse(message: any): Promise {
+ message.id = pendingRequests.length;
+ const promise = new Promise((resolve, reject) => {
+ pendingRequests[message.id] = { resolve, reject };
+ sendMessage(message);
+ });
+ return await promise;
+}
+
+// handle commands from the webview
+(window.external as any).receiveMessage(async (json: any) => {
+
+ function handleCommand(message: ReceivedMessage) {
+ if (message.action === "HttpRequest") {
+ // handle incoming HTTP request responses
+ const promise = pendingRequests[message.messageId]
+
+ const headers = new Headers();
+ for (const h of message.headers) {
+ headers.append(h.Key, h.Value);
+ }
+ const response = new Response(message.body, { headers, status: message.status });
+ promise.resolve(response);
+ return;
+
+ } else {
+ // allow register custom message processors
+ for (const processor of messageProcessors) {
+ const result = processor(message);
+ if (typeof result !== "undefined") {
+ return result;
+ }
+ }
+ throw `Command ${message.action} not found!`;
+ }
+ }
+
+ const message = JSON.parse(json);
+ try {
+ const result = await handleCommand(message);
+ if (typeof result !== "undefined") {
+ sendMessage({
+ type: "HandlerCommand",
+ id: message.messageId,
+ result: JSON.stringify(result)
+ });
+ }
+ }
+ catch (err) {
+ sendMessage({
+ type: "HandlerCommand",
+ id: message.messageId,
+ errorMessage: JSON.stringify(err)
+ });
+ }
+});
+
+type MessageProcessor = (processor: { action: string }) => any;
+const messageProcessors: MessageProcessor[] = [];
+
+export function registerMessageProcessor(processor: MessageProcessor) {
+ messageProcessors.push(processor);
+}
+
+export async function webMessageFetch(url: string, init: RequestInit): Promise {
+ if (init.method?.toUpperCase() === "GET") {
+ return await window.fetch(url, init);
+ }
+
+ const headers: any = {};
+ (init.headers)?.forEach((v, k) => headers[k] = v);
+
+ return await sendMessageAndWaitForResponse({
+ type: "HttpRequest",
+ url,
+ method: init.method || "GET",
+ headers: headers,
+ body: init.body as string
+ });
+}
diff --git a/src/Framework/Framework/Resources/Scripts/webview/webview.ts b/src/Framework/Framework/Resources/Scripts/webview/webview.ts
new file mode 100644
index 0000000000..d22e0d8285
--- /dev/null
+++ b/src/Framework/Framework/Resources/Scripts/webview/webview.ts
@@ -0,0 +1,6 @@
+import { sendMessage, registerMessageProcessor } from './messaging';
+
+export {
+ sendMessage,
+ registerMessageProcessor
+};
diff --git a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs
index ce78606f16..5e74056ea1 100644
--- a/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs
+++ b/src/Framework/Framework/ViewModel/Serialization/DefaultViewModelSerializer.cs
@@ -149,6 +149,12 @@ public void BuildViewModel(IDotvvmRequestContext context, object? commandResult)
result["renderedResources"] = JArray.FromObject(context.ResourceManager.GetNamedResourcesInOrder().Select(r => r.Name));
}
+ if (context.Route != null)
+ {
+ result["routeName"] = context.Route.RouteName;
+ result["routeParameters"] = new JObject(context.Parameters.Select(p => new JProperty(p.Key, p.Value)).ToArray());
+ }
+
// TODO: do not send on postbacks
if (validationRules?.Count > 0) result["validationRules"] = validationRules;
diff --git a/src/Framework/Framework/rollup.config.js b/src/Framework/Framework/rollup.config.js
index 86c4977ee7..44cc7a6841 100644
--- a/src/Framework/Framework/rollup.config.js
+++ b/src/Framework/Framework/rollup.config.js
@@ -8,7 +8,7 @@ const build = process.env.BUILD || "debug";
const production = build == "production";
const suffix = production ? "" : "-debug";
-const config = ({ minify, input, output, spa }) => ({
+const config = ({ minify, input, output, spa, webview }) => ({
input,
output: [
@@ -30,7 +30,8 @@ const config = ({ minify, input, output, spa }) => ({
commonjs(),
replace({
"compileConstants.isSpa": spa,
- "compileConstants.debug": !minify,
+ "compileConstants.isWebview": webview,
+ "compileConstants.debug": !minify
}),
minify && terser({
@@ -82,23 +83,25 @@ const config = ({ minify, input, output, spa }) => ({
})
export default [
- // config({ minify: production, input: ['./Resources/Scripts/dotvvm-root.ts', './Resources/Scripts/dotvvm-light.ts'], output: "default" }),
config({
minify: production,
input: ['./Resources/Scripts/dotvvm-root.ts'],
output: "root-only" + suffix,
- spa: false
+ spa: false,
+ webview: false
}),
config({
minify: production,
input: ['./Resources/Scripts/dotvvm-root.ts'],
output: "root-spa" + suffix,
spa: true,
+ webview: false
}),
- //config({
- // minify: production,
- // input: ['./Resources/Scripts/dotvvm-light.ts'],
- // output: "root-light",
- // spa: false
- //})
+ config({
+ minify: production,
+ input: ['./Resources/Scripts/dotvvm-root.ts'],
+ output: "root-webview" + suffix,
+ spa: true,
+ webview: true
+ })
]