<!DOCTYPE html>
<html style="height: 100%;">
<head>
<meta charset="utf-8">
<title>Demo</title>
<script>
globalThis["sap-ui-config"] = {
securityTokenHandlers: [
// Dummy token as Trippin doesn't handle the "X-CSRF-Token" header.
() => Promise.resolve({ "X-CSRF-Token": "42" })
]
};
</script>
<!-- The below bootstrap settings are for testing and demos only. -->
<!-- In production: use a specific UI5 version, not the "latest" one. -->
<script id="sap-ui-bootstrap"
src="https://sdk.openui5.org/resources/sap-ui-core.js"
data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"
data-sap-ui-async="true"
data-sap-ui-compat-version="edge"
data-sap-ui-exclude-jquery-compat="true"
data-sap-ui-resource-roots='{ "demo": "./" }'
data-sap-ui-xx-wait-for-theme="init"
data-sap-ui-xx-component-preload="off"
></script>
</head>
<body id="content" class="sapUiBody">
<div data-sap-ui-component
data-id="rootComponentContainer"
data-name="demo"
data-settings='{ "id": "rootComponent" }'
data-height="100%"
style="height: 100%;"
></div>
</body>
</html>
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/core/ComponentSupport",//https://github.com/SAP/ui5-tooling/issues/381
], function(UIComponent) {
"use strict";
return UIComponent.extend("demo.Component", {
metadata: {
interfaces: [
"sap.ui.core.IAsyncContentCreation",
],
manifest: "json",
},
init: function() {
UIComponent.prototype.init.apply(this, arguments);
this.getModel("detailViewModel").setProperty("/editing", false);
this.getRouter().initialize();
window.addEventListener("beforeunload", this.onBeforeLeave.bind(this), {
capture: true
}); // <-- TODO: https://developer.chrome.com/blog/page-lifecycle-api/#the-beforeunload-event should be registered only if an unsaved change is available and detached immediately after all changes are saved.
},
onBeforeLeave: function(event) {
if (this.getModel().hasPendingChanges()) { // display confirmation dialog
event.preventDefault(); // as specified by the HTML standard
event.returnValue = ""; // as required by Chromium-based UAs
}
},
});
});
{
"_version": "1.45.0",
"start_url": "index.html",
"sap.app": {
"id": "demo",
"type": "application",
"title": "Demo",
"description": "Sample Code",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"myDataSource": {
"uri": "https://services.odata.org/TripPinRESTierService/(S(hdyjzmkhowfphht22d2llukw))/",
"__NOTE__": "This sample OData service is CORS enabled (See https://github.com/OData/ODataSamples/issues/29). Your OData service might need a reverse proxy server if it doesn't suppprt CORS.",
"settings": {
"odataVersion": "4.0",
"localUri": "ui5://demo/localService/metadata.xml"
}
}
}
},
"sap.ui": {
"technology": "UI5",
"deviceTypes": {
"desktop": true,
"tablet": true,
"phone": true
}
},
"sap.ui5": {
"handleValidation": true,
"dependencies": {
"minUI5Version": "1.99.0",
"libs": {
"sap.m": {},
"sap.f": {},
"sap.ui.layout": {},
"sap.ui.unified": {}
}
},
"contentDensities": {
"compact": true,
"cozy": true
},
"models": {
"": {
"dataSource": "myDataSource",
"settings": {
"autoExpandSelect": true,
"operationMode": "Server",
"earlyRequests": true
},
"preload": true
},
"detailViewModel": {
"type": "sap.ui.model.json.JSONModel"
}
},
"rootView": {
"viewName": "demo.view.Root",
"id": "rootView",
"type": "XML",
"async": true,
"height": "100%"
},
"routing": {
"routes": {
"master": {
"pattern": "",
"target": [
"master"
],
"layout": "OneColumn"
},
"masterDetail": {
"pattern": "user/{userName}",
"target": [
"master",
"detail"
],
"layout": "TwoColumnsMidExpanded"
}
},
"targets": {
"master": {
"id": "masterView",
"name": "Master",
"level": 1,
"controlAggregation": "beginColumnPages"
},
"detail": {
"id": "detailView",
"name": "Detail",
"controlAggregation": "midColumnPages",
"level": 2,
"transition": "show"
}
},
"config": {
"routerClass": "sap.f.routing.Router",
"async": true,
"type": "View",
"viewType": "XML",
"path": "demo.view",
"controlId": "flexibleColumnLayout"
}
}
}
}
<mvc:View controllerName="demo.controller.Detail"
xmlns:mvc="sap.ui.core.mvc"
xmlns:f="sap.f"
xmlns="sap.m"
xmlns:form="sap.ui.layout.form"
xmlns:core="sap.ui.core"
core:require="{
ODataStringType: 'sap/ui/model/odata/type/String',
ODataTypeDateTimeOffset: 'demo/model/type/DateTimeOffset',
TypeDateTimeInterval: 'sap/ui/model/type/DateTimeInterval'
}"
>
<f:DynamicPage id="page"
headerExpanded="false"
backgroundDesign="Solid"
toggleHeaderOnTitleClick="false"
showFooter="{detailViewModel>/editing}"
>
<f:title>
<f:DynamicPageTitle>
<f:heading>
<Title text="User: {UserName}" />
</f:heading>
<f:actions>
<ToggleButton
text="Edit"
type="Emphasized"
pressed="{detailViewModel>/editing}"
visible="{= !${detailViewModel>/editing}}"
/>
</f:actions>
</f:DynamicPageTitle>
</f:title>
<f:content>
<VBox renderType="Bare">
<form:SimpleForm id="form"
class="sapFDynamicPageAlignContent"
width="auto"
layout="ResponsiveGridLayout"
editable="true"
>
<Label text="First Name"/>
<Input
value="{
path: 'FirstName',
type: 'ODataStringType',
constraints: {
nullable: false,
maxLength: 30
}
}"
editable="{detailViewModel>/editing}"
/>
<Label text="Last Name"/>
<Input
value="{
path: 'LastName',
type: 'ODataStringType',
constraints: {
nullable: false,
maxLength: 30
}
}"
editable="{detailViewModel>/editing}"
/>
</form:SimpleForm>
<Table headerText="Trips"
class="sapFDynamicPageAlignContent sapUiMediumMarginTop"
width="auto"
growing="true"
items="{
path: 'Trips',
templateShareable: false
}"
>
<columns>
<Column width="40%">
<Text text="Name" />
</Column>
<Column width="auto">
<Text text="Start At" />
</Column>
</columns>
<ColumnListItem>
<Text text="{Name}"/>
<DateTimePicker
editable="{detailViewModel>/editing}"
showCurrentDateButton="true"
showCurrentTimeButton="true"
value="{
path: 'StartsAt',
type: 'ODataTypeDateTimeOffset',
constraints: {
minimum: 'now',
nullable: false
}
}"
/> <!-- DTP issue: https://github.com/SAP/openui5/issues/3694 -->
</ColumnListItem>
</Table>
</VBox>
</f:content>
<f:footer>
<OverflowToolbar asyncMode="true">
<ToolbarSpacer/>
<Button id="submitBtn"
text="Submit"
type="Emphasized"
press=".onSubmitPress($controller.getView().getBindingContext())"
/>
<Button id="cancelBtn"
text="Cancel"
press=".reset($controller.getView().getBindingContext())"
/>
</OverflowToolbar>
</f:footer>
</f:DynamicPage>
</mvc:View>
<mvc:View xmlns:mvc="sap.ui.core.mvc"
xmlns:f="sap.f"
xmlns="sap.m"
displayBlock="true"
height="100%"
>
<Shell showLogout="false"> <!-- Use the <Shell> only if the app is:
- NOT launched from the FLP (i.e. the app is standalone)
- NOT a master-detail-detail (only master-detail) -->
<App autoFocus="false">
<f:FlexibleColumnLayout id="flexibleColumnLayout"
autoFocus="false"
restoreFocusOnBackNavigation="true"
>
<f:beginColumnPages><!-- Added by the Router --></f:beginColumnPages>
<f:midColumnPages><!-- Added by the Router --></f:midColumnPages>
<f:endColumnPages><!-- Unused --></f:endColumnPages>
</f:FlexibleColumnLayout>
</App>
</Shell>
</mvc:View>
<mvc:View controllerName="demo.controller.Master"
xmlns:mvc="sap.ui.core.mvc"
xmlns:f="sap.f"
xmlns="sap.m"
>
<f:DynamicPage id="page"
headerExpanded="false"
toggleHeaderOnTitleClick="false"
backgroundDesign="List"
>
<f:title>
<f:DynamicPageTitle>
<f:heading>
<Title text="Users" />
</f:heading>
</f:DynamicPageTitle>
</f:title>
<f:content>
<List id="masterList"
growing="true"
growingScrollToLoad="true"
class="sapFDynamicPageAlignContent"
width="auto"
mode="SingleSelectMaster"
items="{
path: '/People',
templateShareable: false,
parameters: {
$select: 'Gender',
$$getKeepAliveContext: true,
$$updateGroupId: 'myGroupId'
},
sorter: [
{
path: 'Gender',
group: true,
descending: true
},
{
path: 'FirstName'
}
]
}"
itemPress=".navToDetailOf(${$parameters>/listItem}.getBindingContext())"
>
<StandardListItem icon="sap-icon://person-placeholder"
title="{FirstName}"
info="{LastName}"
infoState="Information"
type="Navigation"
/>
</List>
</f:content>
</f:DynamicPage>
</mvc:View>
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/UIComponent",
"sap/m/MessageToast",
"sap/m/MessageBox",
], function (Controller, UIComponent, MessageToast, MessageBox) {
"use strict";
return Controller.extend("demo.controller.Detail", {
onInit: function () {
const router = UIComponent.getRouterFor(this);
const route = router.getRoute("masterDetail");
route.attachPatternMatched(this.onPatternMatched, this);
},
onPatternMatched: function (event) {
const { userName } = event.getParameter("arguments");
this.bindSelectedItem(userName, this.getView());
},
bindSelectedItem: function (userName, detailView) {
const key = window.decodeURIComponent(userName);
const model = detailView.getModel();
const context = model.getKeepAliveContext(`/People('${key}')`);
detailView.setBindingContext(context); // instead of .bindObject
},
onSubmitPress: function (context) {
const model = context.getModel();
if (model.hasPendingChanges()) {
context.getBinding().attachEventOnce("patchCompleted", event => {
if (event.getParameter("success")) {
this.resetEditingStatus();
window.requestAnimationFrame(() => MessageToast.show("Updated"));
} else {
MessageBox.error(`One or more users could not be updated.`);
}
});
model.submitBatch(context.getUpdateGroupId());
}
},
reset: function (context) {
const updateGroupId = context.getUpdateGroupId();
this.getOwnerComponent().getModel().resetChanges(updateGroupId);
this.resetEditingStatus();
},
resetEditingStatus: function () {
const model = this.getOwnerComponent().getModel("detailViewModel");
model.setProperty("/editing", false, null, true);
},
onExit: function() {
this.getView().getBindingContext().setKeepAlive(false);
},
});
});
sap.ui.define([
"sap/ui/core/mvc/Controller",
], function (Controller) {
"use strict";
return Controller.extend("demo.controller.Master", {
navToDetailOf: function (selectedContext) {
const selectedUserName = selectedContext.getProperty("UserName");
const userName = window.encodeURIComponent(selectedUserName);
this.getOwnerComponent().getRouter().navTo("masterDetail", { userName });
},
});
});
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="Trippin" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Person">
<Key>
<PropertyRef Name="UserName" />
</Key>
<Property Name="UserName" Type="Edm.String" Nullable="false" />
<Property Name="FirstName" Type="Edm.String" Nullable="false" />
<Property Name="LastName" Type="Edm.String" MaxLength="26" />
<Property Name="MiddleName" Type="Edm.String" />
<Property Name="Gender" Type="Trippin.PersonGender" Nullable="false" />
<Property Name="Age" Type="Edm.Int64" />
<Property Name="Emails" Type="Collection(Edm.String)" />
<Property Name="AddressInfo" Type="Collection(Trippin.Location)" />
<Property Name="HomeAddress" Type="Trippin.Location" />
<Property Name="FavoriteFeature" Type="Trippin.Feature" Nullable="false" />
<Property Name="Features" Type="Collection(Trippin.Feature)" Nullable="false" />
<NavigationProperty Name="Friends" Type="Collection(Trippin.Person)" />
<NavigationProperty Name="BestFriend" Type="Trippin.Person" />
<NavigationProperty Name="Trips" Type="Collection(Trippin.Trip)" />
</EntityType>
<EntityType Name="Airline">
<Key>
<PropertyRef Name="AirlineCode" />
</Key>
<Property Name="AirlineCode" Type="Edm.String" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
<EntityType Name="Airport">
<Key>
<PropertyRef Name="IcaoCode" />
</Key>
<Property Name="Name" Type="Edm.String" />
<Property Name="IcaoCode" Type="Edm.String" Nullable="false" />
<Property Name="IataCode" Type="Edm.String" />
<Property Name="Location" Type="Trippin.AirportLocation" />
</EntityType>
<ComplexType Name="Location">
<Property Name="Address" Type="Edm.String" />
<Property Name="City" Type="Trippin.City" />
</ComplexType>
<ComplexType Name="City">
<Property Name="Name" Type="Edm.String" />
<Property Name="CountryRegion" Type="Edm.String" />
<Property Name="Region" Type="Edm.String" />
</ComplexType>
<ComplexType Name="AirportLocation" BaseType="Trippin.Location">
<Property Name="Loc" Type="Edm.GeographyPoint" />
</ComplexType>
<ComplexType Name="EventLocation" BaseType="Trippin.Location">
<Property Name="BuildingInfo" Type="Edm.String" />
</ComplexType>
<EntityType Name="Trip">
<Key>
<PropertyRef Name="TripId" />
</Key>
<Property Name="TripId" Type="Edm.Int32" Nullable="false" />
<Property Name="ShareId" Type="Edm.Guid" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
<Property Name="Budget" Type="Edm.Single" Nullable="false" />
<Property Name="Description" Type="Edm.String" />
<Property Name="Tags" Type="Collection(Edm.String)" />
<Property Name="StartsAt" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="EndsAt" Type="Edm.DateTimeOffset" Nullable="false" />
<NavigationProperty Name="PlanItems" Type="Collection(Trippin.PlanItem)" />
</EntityType>
<EntityType Name="PlanItem">
<Key>
<PropertyRef Name="PlanItemId" />
</Key>
<Property Name="PlanItemId" Type="Edm.Int32" Nullable="false" />
<Property Name="ConfirmationCode" Type="Edm.String" />
<Property Name="StartsAt" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="EndsAt" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="Duration" Type="Edm.Duration" Nullable="false" />
</EntityType>
<EntityType Name="Event" BaseType="Trippin.PlanItem">
<Property Name="OccursAt" Type="Trippin.EventLocation" />
<Property Name="Description" Type="Edm.String" />
</EntityType>
<EntityType Name="PublicTransportation" BaseType="Trippin.PlanItem">
<Property Name="SeatNumber" Type="Edm.String" />
</EntityType>
<EntityType Name="Flight" BaseType="Trippin.PublicTransportation">
<Property Name="FlightNumber" Type="Edm.String" />
<NavigationProperty Name="Airline" Type="Trippin.Airline" />
<NavigationProperty Name="From" Type="Trippin.Airport" />
<NavigationProperty Name="To" Type="Trippin.Airport" />
</EntityType>
<EntityType Name="Employee" BaseType="Trippin.Person">
<Property Name="Cost" Type="Edm.Int64" Nullable="false" />
<NavigationProperty Name="Peers" Type="Collection(Trippin.Person)" />
</EntityType>
<EntityType Name="Manager" BaseType="Trippin.Person">
<Property Name="Budget" Type="Edm.Int64" Nullable="false" />
<Property Name="BossOffice" Type="Trippin.Location" />
<NavigationProperty Name="DirectReports" Type="Collection(Trippin.Person)" />
</EntityType>
<EnumType Name="PersonGender">
<Member Name="Male" Value="0" />
<Member Name="Female" Value="1" />
<Member Name="Unknown" Value="2" />
</EnumType>
<EnumType Name="Feature">
<Member Name="Feature1" Value="0" />
<Member Name="Feature2" Value="1" />
<Member Name="Feature3" Value="2" />
<Member Name="Feature4" Value="3" />
</EnumType>
<Function Name="GetPersonWithMostFriends">
<ReturnType Type="Trippin.Person" />
</Function>
<Function Name="GetNearestAirport">
<Parameter Name="lat" Type="Edm.Double" Nullable="false" />
<Parameter Name="lon" Type="Edm.Double" Nullable="false" />
<ReturnType Type="Trippin.Airport" />
</Function>
<Function Name="GetFavoriteAirline" IsBound="true" EntitySetPath="person">
<Parameter Name="person" Type="Trippin.Person" />
<ReturnType Type="Trippin.Airline" />
</Function>
<Function Name="GetFriendsTrips" IsBound="true">
<Parameter Name="person" Type="Trippin.Person" />
<Parameter Name="userName" Type="Edm.String" Nullable="false" Unicode="false" />
<ReturnType Type="Collection(Trippin.Trip)" />
</Function>
<Function Name="GetInvolvedPeople" IsBound="true">
<Parameter Name="trip" Type="Trippin.Trip" />
<ReturnType Type="Collection(Trippin.Person)" />
</Function>
<Action Name="ResetDataSource" />
<Action Name="UpdateLastName" IsBound="true">
<Parameter Name="person" Type="Trippin.Person" />
<Parameter Name="lastName" Type="Edm.String" Nullable="false" Unicode="false" />
<ReturnType Type="Edm.Boolean" Nullable="false" />
</Action>
<Action Name="ShareTrip" IsBound="true">
<Parameter Name="personInstance" Type="Trippin.Person" />
<Parameter Name="userName" Type="Edm.String" Nullable="false" Unicode="false" />
<Parameter Name="tripId" Type="Edm.Int32" Nullable="false" />
</Action>
<EntityContainer Name="Container">
<EntitySet Name="People" EntityType="Trippin.Person">
<NavigationPropertyBinding Path="BestFriend" Target="People" />
<NavigationPropertyBinding Path="Friends" Target="People" />
<NavigationPropertyBinding Path="Trippin.Employee/Peers" Target="People" />
<NavigationPropertyBinding Path="Trippin.Manager/DirectReports" Target="People" />
</EntitySet>
<EntitySet Name="Airlines" EntityType="Trippin.Airline">
<Annotation Term="Org.OData.Core.V1.OptimisticConcurrency">
<Collection>
<PropertyPath>Name</PropertyPath>
</Collection>
</Annotation>
</EntitySet>
<EntitySet Name="Airports" EntityType="Trippin.Airport" />
<Singleton Name="Me" Type="Trippin.Person">
<NavigationPropertyBinding Path="BestFriend" Target="People" />
<NavigationPropertyBinding Path="Friends" Target="People" />
<NavigationPropertyBinding Path="Trippin.Employee/Peers" Target="People" />
<NavigationPropertyBinding Path="Trippin.Manager/DirectReports" Target="People" />
</Singleton>
<FunctionImport Name="GetPersonWithMostFriends" Function="Trippin.GetPersonWithMostFriends" EntitySet="People" />
<FunctionImport Name="GetNearestAirport" Function="Trippin.GetNearestAirport" EntitySet="Airports" />
<ActionImport Name="ResetDataSource" Action="Trippin.ResetDataSource" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
sap.ui.define([
"sap/ui/model/odata/type/DateTimeOffset",
"sap/ui/model/ValidateException",
], function(ODataTypeDateTimeOffset, ValidateException) {
"use strict";
return ODataTypeDateTimeOffset.extend("demo.model.type.DateTimeOffset", {
constructor: function (oFormatOptions, oConstraints) {
ODataTypeDateTimeOffset.apply(this, arguments);
this.oConstraints = Object.assign(this.getConstraints(), {
minimum: oConstraints?.minimum,
});
},
validateValue: function(oValue) { // Minimal sample
ODataTypeDateTimeOffset.prototype.validateValue.apply(this, arguments);
const min = this.getConstraints()?.minimum?.trim().toLowerCase();
if (min === "now" && new Date(oValue).getTime() <= new Date().getTime()) {
throw new ValidateException("Minimum violated!", ["minimum"]);
}
},
});
});