<!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"]);
      }
    },
  });
});