Show TOC

Step 11: AddProduct ViewLocate this document in the navigation structure

Let's round out the core functionality of the app by bringing in the facility to add a new product.

We've already got the "add" button in the footer of the Page in the master view, but right now, when we press it, predictably, nothing happens visually, and we get this error in the console:

Uncaught Error: resource sap/ui/demo/tdg/view/AddProduct.view.xml could not be loaded from ./view/AddProduct.view.xml. Check for 'file not found' or parse errors.

So let's add the view and controller.

View

Let's first take a peek at what we're trying to achieve in this view definition.

It's an sap.m.Page, with an sap.ui.layout.form.SimpleForm within the flexible sap.ui.layout.Grid control. Like the Page controls in the master and detail views, this Page also has a Bar in the footer, and in this case, there are two buttons on the right hand side.

<mvc:View
	controllerName="sap.ui.demo.tdg.view.AddProduct"
	xmlns:mvc="sap.ui.core.mvc"
	xmlns:l="sap.ui.layout"
	xmlns:f="sap.ui.layout.form"
	xmlns:c="sap.ui.core"
	xmlns="sap.m">

This should be familiar by now; we're declaring our view with the namespaces for the controls we're going to use within.

	<Page
		class="sapUiFioriObjectPage"
		title="{i18n>addProductTitle}">
		<l:Grid
			defaultSpan="L12 M12 S12"
			width="auto">
			<l:content>
				<f:SimpleForm
					id="idAddProductForm"
					minWidth="800"
					maxContainerCols="2"
					editable="true"
					layout="ResponsiveGridLayout"
					title="New Product"
					labelSpanL="3"
					labelSpanM="3"
					emptySpanL="4"
					emptySpanM="4"
					columnsL="1"
					columnsM="1"
					class="editableForm">
					<f:content>

The SimpleForm sits within the Grid. The SimpleForm's content aggregation is where we define our separate form sections. Each section begins with an sap.ui.core.Title control, which causes a subheader style title to appear (these are the Basic Info, Discontinued and Supplier & Category texts visible in the screenshot above).


						<!-- Basic info -->
						<c:Title text="{i18n>addProductTitleBasic}" />
						<Label text="{i18n>addProductLabelName}" />
						<Input value="{newProduct>/Detail/Name}" />
						<Label text="{i18n>addProductLabelDescription}" />
						<TextArea value="{newProduct>/Detail/Description}" />
						<Label text="{i18n>addProductLabelReleaseDate}" />
						<DateTimeInput
							type="Date"
							value="{newProduct>/Detail/ReleaseDate}" />
						<Label text="{i18n>addProductLabelPrice}" />
						<Input value="{newProduct>/Detail/Price}" />
						<Label text="{i18n>addProductLabelRating}" />
						<RatingIndicator
							visualMode="Full"
							value="{newProduct>/Detail/Rating}" />

To collect the data in the form fields, and have access to it in the controller, we're using a named client side model - an sap.ui.model.json.JSONModel to be precise. The model's name is newProduct and we can see the prefix in use in the property bindings such as {newProduct>/Detail/Name}. The model is instantiated and set on this view in the init event (see the details on the controller below).

In the OData service, the Rating property of the Product entity id defined as an integer ( <Property Name="Rating" Type="Edm.Int32" Nullable="false"/> ) and therefore we set the visualMode property of the sap.m.RatingIndicator control to "Full" to cause the values to be rounded to the nearest integer value.

					<!-- Discontinued? -->
					<c:Title text="{i18n>addProductTitleDiscontinued}" />
					<Label text="{i18n>addProductLabelDiscontinuedFlag}" />
					<CheckBox selected="{newProduct>/Detail/DiscontinuedFlag}" />
					<Label
						visible="{newProduct>/Detail/DiscontinuedFlag}"
						text="{i18n>addProductLabelDiscontinuedDate}" />
					<DateTimeInput
						type="Date"
						visible="{newProduct>/Detail/DiscontinuedFlag}"
						value="{newProduct>/Detail/DiscontinuedDate}" />

This section is similar to the Basic Info section of the form. Noteworthy, however, is the use of the Detail/DiscontinuedFlag property in the newProduct JSON model. It's set to false by default, and is used to set the visibility of the sap.m.Label and sap.m.DateTimeInput controls for the DiscontinuedDate value. So only if the Discontinued checkbox is checked does the extra label and field for the date appear. This is done just with property bindings on a model, without controller logic.

	<!-- Supplier & Category -->
						<c:Title text="{i18n>addProductTitleSupplierCategory}" />
						<Label text="{i18n>addProductLabelSupplier}" />
						<Select
							id="idSelectSupplier"
							items="{/Suppliers}"
							width="100%">
							<c:Item text="{Name}" />
						</Select>
						<Label text="{i18n>addProductLabelCategory}" />
						<Select
							id="idSelectCategory"
							items="{/Categories}"
							width="100%">
							<c:Item text="{Name}" />
						</Select>
					</f:content>
				</f:SimpleForm>
			</l:content>
		</l:Grid>

The last form section is a pair of sap.m.Select controls, each bound to entity sets in the ODataModel so that a Supplier and a Category can be chosen for the new Product. Note that in this case, the binding path is absolute, because there's no binding in this form that would be relevant for any relative connection.

<footer>
			<Bar>
				<contentRight>
					<Button text="{i18n>addProductButtonSave}" type="Emphasized" press="onSave" />
					<Button text="{i18n>addProductButtonCancel}" press="onCancel" />
				</contentRight>
			</Bar>
		</footer>
	</Page>
</mvc:View>

Finally, there are the two sap.m.Button controls in the footer's Bar. The action button (Save) is highlighted using the Emphasized type from the sap.m.ButtonType enumeration.

Controller
sap.ui.core.mvc.Controller.extend("sap.ui.demo.tdg.view.AddProduct", {

	oAlertDialog : null,
	oBusyDialog : null,

We're going to show an alert dialog if there's a problem with the input, and we also have an sap.m.BusyDialog to show while the process of saving the new product is carried out. We'll hold references to these here.

Note In this demo app, there is little to no client side validation. That is left as an exercise for you, dear reader!
initializeNewProductData : function() {
		this.getView().getModel("newProduct").setData({
			Detail: {
				DiscontinuedFlag: false
			}
		});
	},

With this initializeNewProductData function, we can reset the data in the JSON model, effectively clearing the values in the form fields. We're holding the properties within a single member "Detail" to make it simpler to reset.

onInit : function() {
		this.getView().setModel(new sap.ui.model.json.JSONModel(), "newProduct");
		this.initializeNewProductData();
	},

This is the named model that is used to communicate data between the form in the view and controller.

showErrorAlert : function(sMessage) {
		jQuery.sap.require("sap.m.MessageBox");
		sap.m.MessageBox.alert(sMessage);
	},

Later, we instantiate an sap.m.Dialog control to alert the user to issues with form input. But not all alerts need to use explicitly instantiated controls; in this showErrorAlert function, invoked in a couple of places in this controller, we use the static method sap.m.MessageBox.alert as a convenience.

	dateFromString : function(sDate) {
		// Try to create date directly, otherwise assume dd/mm/yyyy
		var oDate = new Date(sDate);
		return oDate === "Invalid Date" ? new Date(sDate.split("/").reverse()) : oDate;

	},

	saveProduct : function(nID) {
		var mNewProduct = this.getView().getModel("newProduct").getData().Detail;
		// Basic payload data
		var mPayload = {
			ID: nID,
			Name: mNewProduct.Name,
			Description: mNewProduct.Description,
			ReleaseDate: this.dateFromString(mNewProduct.ReleaseDate),
			Price: mNewProduct.Price.toString(),
			Rating: mNewProduct.Rating
		};

		if (mNewProduct.DiscontinuedDate) {
			mPayload.DiscontinuedDate = this.dateFromString(mNewProduct.DiscontinuedDate);
		}

The saveProduct function does the heavy lifting of invoking the OData CREATE operation on the OData service via the domain model. First, the data in the JSON model is retrieved and unpacked, and a payload for the CREATE operation is created.

// Add supplier & category associations
		["Supplier", "Category"].forEach(function(sRelation) {
			var oSelect = this.getView().byId("idSelect" + sRelation);
			var sPath = oSelect.getSelectedItem().getBindingContext().getPath();
			mPayload[sRelation] = {
				__metadata: {
					uri: sPath
				}
			};
		}, this);

Remember that the Product entity has associations to the Supplier and Category entities. So when we create the new Product, we must ensure that the associations are made to the Supplier and Category chosen in the form. This is done by specifying a __metadata object for each association, with a uri property pointing to the chosen entity's path.

Here's an example of what that payload looks like:

{
   "ID":9,
   "Name":"Galaxy S4",
   "Description":"Samsung smartphone",
   "ReleaseDate":null,
   "Price":"499.00",
   "Rating":4,
   "Supplier":{
      "__metadata":{
         "uri":"/Suppliers(1)"
      }
   },
   "Category":{
      "__metadata":{
         "uri":"/Categories(2)"
      }
   }
}

This example is for a new product that is associated with the supplier Tokyo Traders (/Suppliers(1)) and category "Electronics" (/Categories(2)).

	// Send OData Create request
		var oModel = this.getView().getModel();
		oModel.create("/Products", mPayload, {
			success : jQuery.proxy(function(mResponse) {
				this.initializeNewProductData();
				sap.ui.core.UIComponent.getRouterFor(this).navTo("product", {
					from: "master",
					product: "Products(" + mResponse.ID + ")",
					tab: "supplier"
				}, false);
				jQuery.sap.require("sap.m.MessageToast");
				// ID of newly inserted product is available in mResponse.ID
				this.oBusyDialog.close();
				sap.m.MessageToast.show("Product '" + mPayload.Name + "' added");
			}, this),
			error : jQuery.proxy(function() {
				this.oBusyDialog.close();
				this.showErrorAlert("Problem creating new product");
			}, this)
		});

	},

Once the payload is ready the OData CREATE request is invoked. On success, a MessageToast is shown with the ID of the newly created Product entity, the form data is reset, and we get the Router to navigate the user to the display of the new Product entry. Otherwise a simple alert message is shown using the showAlertError function that we've already seen.


	onSave : function() {
		// Show message if no product name has been entered
		// Otherwise, get highest existing ID, and invoke create for new product
		if (!this.getView().getModel("newProduct").getProperty("/Detail/Name")) {
			if (!this.oAlertDialog) {
				this.oAlertDialog = sap.ui.xmlfragment("sap.ui.demo.tdg.view.NameRequiredDialog", this);
				this.getView().addDependent(this.oAlertDialog);
			}
			this.oAlertDialog.open();

We impose a small restriction on the input of the new product details - there must be a name specified. If not, we instantiate an alert Dialog (if it doesn't exist already) and show it. Where is the definition? In an XML fragment, of course!

Fragments are not only useful for for separating out UI definitions into discrete and maintainable chunks, but also allow you to use your choice of view definition language (in our case XML) for all of your UI elements. So instead of declaring the sap.m.Dialog-based alert in-line, in this condition, with JavaScript, you can and should declare it using the same approach (XML) as the rest of your UI. See below for the XML fragment where this Dialog is defined.

	} else {
			if (!this.oBusyDialog) {
				this.oBusyDialog = new sap.m.BusyDialog();
			}
			this.oBusyDialog.open();
			this.getView().getModel().read("/Products", {
				urlParameters : {
					"$top" : 1,
					"$orderby" : "ID desc",
					"$select" : "ID"
				},
				async : false,
				success : jQuery.proxy(function(oData) {
					this.saveProduct(oData.results[0].ID + 1);
				}, this),
				error : jQuery.proxy(function() {
					this.oBusyDialog.close();
					this.showErrorAlert("Cannot determine next ID for new product");
				}, this)
			});

		}
	},

If all is ok with the form input, we want to go ahead with saving the new product. Our OData service (Northwind) requires an ID key to be specified (it's not auto-generated on creation). So we need to find the highest ID and then increment it.For the first time in this app, we're going to invoke an explicit OData operation to retrieve data: a QUERY, via the sap.ui.model.odata.ODataModel.read function. For READ and QUERY operations on an OData service, this should be the exception. But in this case it's required - we need to use a combination of the $top, $orderby and $select query options: "Give me the ID property of the entity that has the highest ID in the Products entity set"

	onCancel : function() {
		sap.ui.core.UIComponent.getRouterFor(this).backWithoutHash(this.getView());
	},

This function is called to handle the press event of the "Cancel" Button. It uses the sap.ui.demo.tdg.MyRouter.backWithoutHash function to return to the product detail that was visible before this AddProduct view was displayed. This backWithoutHash function recursively ascends the UI tree via a helper function _findSplitApp, to discover the app's "root' control, the sap.m.SplitApp. On finding this, it can call the appropriate back function.

onDialogClose : function(oEvent) {
		oEvent.getSource().getParent().close();
	}
});

The last function in this controller is a handler for the press event on the sap.m.Dialog defined in the XML fragment NameRequiredDialog.fragment.xml.

Fragment

The XML fragment NameRequiredDialog.fragment.xml is where the alert Dialog is defined, and looks like this:

<core:FragmentDefinition
	xmlns:core="sap.ui.core"
	xmlns="sap.m">
	<Dialog
		title="{i18n>nameRequiredDialogTitle}"
		type="Message">
		<content>
			<Text text="{i18n>nameRequiredDialogText}" />
		</content>
		<beginButton>
			<Button text="{i18n>nameRequiredDialogButton}" press="onDialogClose" />
		</beginButton>
	</Dialog>
</core:FragmentDefinition>

Like the other fragments in this app (see the Detail XML Fragments section) this fragment has only a single root node ( <Dialog> ), nevertheless we're using the <core:FragmentDefinition> wrapper here too. Note also that we're also explicitly specifying the sap.m.Dialog's default aggregation rather than implying it ( <content> ). Finally, it's worth pointing out that the handler for the press event is to be found in the controller related to the view where this fragment is inserted, i.e. AddProduct.controller.js.

Progress Check

Ok, we're almost there. Our app folder content now looks like this:

tdg/
  |
  +-- i18n/
  |     |
  |     +-- messageBundle.properties
  |
  +-- util/
  |     |
  |     +-- Formatter.js
  |
  +-- view/
  |     |
  |     +-- AddProduct.controller.js
  |     +-- AddProduct.view.xml
  |     +-- App.view.xml
  |     +-- CategoryInfoForm.fragment.xml
  |     +-- Detail.controller.js
  |     +-- Detail.view.xml
  |     +-- Master.controller.js
  |     +-- Master.view.xml
  |     +-- NameRequiredDialog.fragment.xml
  |     +-- SupplierAddressForm.fragment.xml
  |
  +-- Component.js
  +-- index.html
  +-- MyRouter.html

Here's a shot of the AddProduct view with the alert Dialog shown (as we have deliberately not specified a value for the Product Name: