Build Luigi App with React
- How to add content to your React web shopping app
- How to use Luigi to configure navigation
- How to create simple micro-frontends using React
- Step 1
In this step, you will create a file with information about the products on sale in your shopping app.
In a real life implementation, this data would be provided by an external service/API. But for simplicity, you will create a
.json
file containing dummy data. This file will provide the displayed data in the micro-frontend.- Navigate to
ui5-mf/uimodule/webapp/model
and create aproducts.json
file with the following content:JSONCopy{ "ProductCollection": [{ "id": 101, "name": "Logitech Mouse", "price": 45.0, "stock": 80, "icon": "product", "currencyCode": "EUR", "orderQuantity": 2, "description": "LIGHTSPEED Wireless Gaming Mouse with HERO Sensor" }, { "id": 102, "name": "Logitech Keyboard", "price": 50.0, "stock": 22, "icon": "product", "currencyCode": "EUR", "orderQuantity": 1, "description": "A physical keyboard that uses an individual spring and switch for each key. Today, only premium keyboards are built with key switches; however, they were also used in the past, such as in the Model M keyboard from IBM, which used buckling spring switches" }, { "id": 103, "name": "HP Optical Mouse", "price": 35.0, "stock": 4, "icon": "product", "currencyCode": "EUR", "orderQuantity": 2, "description": "Utilizing the latest optical sensing technology, the HP USB Optical Scroll Mouse records precise motion." }, { "id": 104, "name": "MacBook Pro", "price": 1299.0, "stock": 11, "icon": "laptop", "currencyCode": "EUR", "orderQuantity": 3, "description": "It features a touch-sensitive OLED display strip located in place of the function keys, a Touch ID sensor integrated with the power button, a butterfly mechanism keyboard similar to the MacBook, and four USB-C ports that also serve as Thunderbolt 3 ports." }, { "id": 105, "name": "Magic Mouse", "price": 40.0, "stock": 20, "icon": "product", "currencyCode": "EUR", "orderQuantity": 6, "description": "The Magic Mouse 2 (Apple Magic Mouse 2), is a computer mouse developed and released by Apple Inc. It features a multi-touch acrylic surface for scrolling. ... The mouse features a lithium-ion rechargeable battery and Lightning connector for charging and pairing." }, { "id": 106, "name": "Brother Printer", "price": 235.0, "stock": 24, "icon": "fx", "currencyCode": "EUR", "orderQuantity": 1, "description": "Our affordable, quality machines provide you with the optimal way to take care of all your printing needs. Shop for the right printer, all-in-one, or fax machine for your home or home office today." }, { "id": 107, "name": "iPhone 11", "price": 835.0, "stock": 45, "icon": "iphone", "currencyCode": "EUR", "orderQuantity": 8, "description": "The iPhone 11 dimensions are 150.9mm x 75.7mm x 8.3mm (H x W x D). It weighs about 194 grams (6.84 ounces).It features a 6.1-inch all-screen LCD display and is powered by Apple new A13 bionic chip with Third-Generation Neural Engine." }, { "id": 108, "name": "Google Pixel 3a", "price": 299.0, "stock": 54, "icon": "desktop-mobile", "currencyCode": "EUR", "orderQuantity": 7, "description": "At 5.6 inches, the Google Pixel 3a display is proportionate to the relatively small body of the phone – that is to say, it is rather small. The display is Full HD+ and OLED, with a resolution of 2220 x 1080, and because of the relatively small screen size the pixels per inch count is rather high at 441." }, { "id": 109, "name": "PlayStation 4", "price": 330.0, "stock": 94, "icon": "video", "currencyCode": "EUR", "orderQuantity": 1, "description": "PS4 is the fourth home video game console produced by Sony Computer Entertainment and is compatible with the PlayStation 3. It was officially announced at a press conference on February 20, 2013 and launched on November 15, 2013." }, { "id": 110, "name": "Dell Monitor", "price": 630.0, "stock": 20, "icon": "sys-monitor", "currencyCode": "EUR", "orderQuantity": 3, "description": "34'' U3419W Monitor, Display with stand Height adjustable (115 mm), tiltable (-5° to 21°), rotatable (-30° to 30°) Security slot (cable lock sold separately), anti-theft slot for locking to stand (for display). Includes: DisplayPort cable, HDMI cable, Power cable, Stand, USB 3.0 Type-A to Type-B cable, USB-C cable" } ] }
- Navigate to
- Step 2
In this step, you will let Luigi take control of the
index.hmtl
file - the entry point for your app.- Go to
react-core-mf/public/index.html
and change its content to:HTMLCopy<!DOCTYPE html> <html> <head> <title>Luigi</title> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" /> <link rel="stylesheet" href="/luigi-core/luigi.css" /> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/luigi-core/luigi.js"></script> <script src="/luigi-config.js"></script> </body> </html>
- Go to
- Step 3
In this step, you will create another HTML file which will serve as a template for React to create the React micro-frontends.
- Go to
react-core-mf/public
and create a new file calledsampleapp.html
. Paste this code into the file:HTMLCopy<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
- Go to
- Step 4
In this step, we configure
webpack
and adjust dependencies in order to make it easier to develop and build the app.-
Go to
react-core-mf/config/webpack.config.js
-
Find the following line from the file and comment it out by surrounding it with
/*
or//
tags:JavaScriptCopy// Around line 19 // const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
-
In the second occurrence, comment out this whole entry as well:
JavaScriptCopy//Around line 348 /* new ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson, reactRefreshOverlayEntry, ]),*/
-
- Step 5
With the help of simple parameters pertaining to navigation and general settings, you will create your first “Home” navigation node and make your application responsive.
These are the Luigi navigation parameters you will use:
pathSegment
- text segment added to the URLlabel
- the name of the node displayed in the navigationicon
- a SAP icon shown next to the labelviewUrl
- the URL of your micro-frontend
-
Delete the folder
react-core-mf/src/luigi-config
. (This step is not required.) -
Go to the file
react-core-mf/public/luigi-config.js
. This is where you can find the Luigi configuration. Copy and paste this code:JavaScriptCopy!(function (e) { var t = {}; function n(r) { if (t[r]) return t[r].exports; var o = (t[r] = { i: r, l: !1, exports: {} }); return e[r].call(o.exports, o, o.exports, n), (o.l = !0), o.exports; } (n.m = e), (n.c = t), (n.d = function (e, t, r) { n.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: r }); }), (n.r = function (e) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }); }), (n.t = function (e, t) { if ((1 & t && (e = n(e)), 8 & t)) return e; if (4 & t && "object" == typeof e && e && e.__esModule) return e; var r = Object.create(null); if ( (n.r(r), Object.defineProperty(r, "default", { enumerable: !0, value: e }), 2 & t && "string" != typeof e) ) for (var o in e) n.d( r, o, function (t) { return e[t]; }.bind(null, o) ); return r; }), (n.n = function (e) { var t = e && e.__esModule ? function () { return e.default; } : function () { return e; }; return n.d(t, "a", t), t; }), (n.o = function (e, t) { return Object.prototype.hasOwnProperty.call(e, t); }), (n.p = ""), n((n.s = 0)); })([ function (e, t) { Luigi.setConfig({ navigation: { nodes: () => [ { pathSegment: "home", label: "Home", icon: "home", viewUrl: "/sampleapp.html#/home", }, ], }, settings: { header: { title: "Luigi React App", logo: "/logo192.png" }, responsiveNavigation: "simpleMobileOnly", customTranslationImplementation: myTranslationProvider, }, lifecycleHooks: { luigiAfterInit: () => { Luigi.i18n().setCurrentLocale(defaultLocale); }, }, communication: { customMessagesListeners: { "set-language": (msg) => { Luigi.i18n().setCurrentLocale(msg.locale); }, }, }, }); }, ]); var defaultLocale = "en-US"; function myTranslationProvider() { var dict = { "en-US": { PRODUCTS: "Products", ORDERHISTORY: "Order History" }, }; return { getTranslation: function (label, interpolation, locale) { return ( dict[locale || Luigi.i18n().getCurrentLocale() || defaultLocale][ label ] || label ); }, }; }
- Step 6
In this step, you will make changes to the entry point
index.js
for the React app. You will create a new view calledhome.js
inreact-core-mf/src/views
, configure the router for this view, and import the Luigi Client. You will also create a file calledlanguage.js
which will be useful for the next tutorials dealing with localization.-
Open
react-core-mf/src/index.js
and change its content to:JavaScriptCopyimport React, { Component } from "react"; import { render } from "react-dom"; import { BrowserRouter, Route } from "react-router-dom"; import Home from "./views/home.js"; import { addInitListener, addContextUpdateListener, uxManager} from "@luigi-project/client"; import { dict } from "./language.js"; import "./index.css"; class App extends Component { constructor(props) { super(props); this.state = { currentLocale: "en-US" }; const updateCurrentLanguage = () => { this.setState({ currentLocale: uxManager().getCurrentLocale(), }); }; addInitListener(() => { console.log("Luigi Client initialized."); updateCurrentLanguage(); }); addContextUpdateListener(() => { updateCurrentLanguage(); }); } render() { return ( <BrowserRouter basename={`sampleapp.html#`}> <Route path="/home" render={(props) => (<Home {...props} localeDict={dict[this.state.currentLocale]} currentLocale={this.state.currentLocale}/>)}/> </BrowserRouter> ); } } render(<App />, document.getElementById("root"));
-
Next, go to the
react-core-mf/src/views
directory created in step 6 of the previous tutorial. Create a file calledhome.js
and paste the following code into it:JavaScriptCopyimport React, { Component, useState } from "react"; import "../../node_modules/fundamental-styles/dist/fundamental-styles.css"; import {addInitListener, addContextUpdateListener,removeContextUpdateListener,removeInitListener,sendCustomMessage} from "@luigi-project/client"; import {Grid, Panel,Select,Option} from "@ui5/webcomponents-react"; export default class Home extends Component { constructor(props) { super(props); this.locale = null; this.initListener = null; this.contextUpdateListener = null; this.locale = props.currentLocale; this.setLocale = null; this.options = [{ key: "en-US", text: "en-US" }]; } componentDidMount() { this.initListener = addInitListener((initialContext) => { this.setState({ message: "Luigi Client initialized.", }); }); this.contextUpdateListener = addContextUpdateListener( (updatedContext) => { this.setState({ message: "Luigi Client updated.", }); } ); } componentWillUnmount() { removeContextUpdateListener(this.contextUpdateListener); removeInitListener(this.initListener); } onChangeValue(event) { this.locale = event.detail.selectedOption.innerText; sendCustomMessage({ id: "set-language", locale: event.detail.selectedOption.innerText, }); } render() { return ( <Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10"> <Panel headerText={this.props.localeDict.WELCOME_LUIGI} headerLevel="H3"> <Select onChange={this.onChangeValue}> {this.options.map((language) => ( <Option key={language.key}>{language.text}</Option> ))} </Select> </Panel> </Grid> ); } }
-
Create a new file in
react-core-mf/src/
calledlanguage.js
with following content:JavaScriptCopyexport const dict = { "en-US": { ITEMS: "Products", STOCKS: "Stocks", SELECTLANGUAGE: "Please select a language", PRICE: "Price", WELCOME_LUIGI: "Welcome to Luigi - a micro-frontend framework", DESCRIPTION: "Description", PRODUCTADDED: "Product has been added to cart", AVAILABLE: "Available", AVAILABLEQUANT: "Available quantity: ", ADDTOCART: "Add to cart", BACK: "Back", OUTOFSTOCK: "Out of stock", }, };
-
- Step 7
In this step, you will add a navigation node in Luigi for the “Products” micro-frontend.
-
Open
react-core-mf/public/luigi-config.js
-
Add a new “Products” node to your navigation:
JavaScriptCopynavigation: { nodes: () => [ { pathSegment: "home", label: "Home", icon: "home", viewUrl: "/sampleapp.html#/home", //ADD THIS SECTION TO THE CONFIG FILE children: [ { pathSegment: "products", label: "Products", icon: "product", viewUrl: "/sampleapp.html#/products", } ], //UNTIL HERE }, ], },
-
Next, create the
products.js
file inreact-core-mf/src/views
and past following code into it:JavaScriptCopyimport React, { Component } from "react"; import "../../node_modules/fundamental-styles/dist/fundamental-styles.css"; import { ProductCollection } from "../../../ui5-mf/uimodule/webapp/model/products.json"; import { Grid, List, StandardListItem } from "@ui5/webcomponents-react"; import { linkManager } from "@luigi-project/client"; import "@ui5/webcomponents-icons/dist/AllIcons.js"; export default class Product extends Component { constructor(props) { super(props); this.toast = React.createRef(); } render() { const handleItemClick = (event) => { linkManager().withParams({ root: "products" }); linkManager().navigate( "/home/products/" + event.detail.item.id.toString() ); }; const listItems = []; ProductCollection.forEach((product) => { listItems.push( <StandardListItem id={product.id} key={product.id} additionalText={product.price + " " + product.currencyCode} additionalTextState="Information" description={product.description} growing="None" headerText={product.orderQuantity} icon={product.icon} type="Active" mode="None" onItemClick={() => handleItemClick(product.id)}> <p onClick={() => handleChildClick(product.id)}>{product.name}</p> </StandardListItem> ); }); return ( <Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10"> <List headerText={this.props.localeDict.ITEMS + ": " + ProductCollection.length} onItemClick={handleItemClick}> {listItems} </List> </Grid> ); } }
-
Add the routing module to the
index.js
by adding the following lines to the file:JavaScriptCopy//AROUND LINE 4 PASTE THIS LINE: import Products from "./views/products.js"; //AROUND LINE 35 PASTE THIS LINE: <BrowserRouter basename={`sampleapp.html#`}> <Route path="/home" render={(props) => <Home {...props} localeDict={dict[this.state.currentLocale]} currentLocale={this.state.currentLocale} />} /> //THIS LINE HAS TO BE ADDED: <Route path="/products" render={(props) => <Products {...props} localeDict={dict[this.state.currentLocale]} />} /> </BrowserRouter>
-
- Step 8
In this step, you will add the
ProductDetail.js
view to the app. You will be able to show details for each product via a Luigi dynamic parameter, in this case named:id
.-
In
react-core-mf/public/luigi-config.js
add a child node:id
to theproducts
node:jsCopychildren: [ { pathSegment: "products", label: "Products", icon: "product", viewUrl: "/sampleapp.html#/products", // PASTE THIS SECTION keepSelectedForChildren: true, children: [{ pathSegment: ':id', viewUrl: '/sampleapp.html#/productDetail/:id', context: { id: ':id' } }] //UNTIL HERE },
-
Next create a new file in
react-core-mf/src/views
namedproductDetail.js
and paste following content into it:jsCopyimport React, { Component, useRef, useState } from "react"; import "../../node_modules/fundamental-styles/dist/fundamental-styles.css"; import { ProductCollection } from "../../../ui5-mf/uimodule/webapp/model/products.json"; import { Grid,ObjectPage,Label,DynamicPageHeader,DynamicPageTitle,ObjectStatus,FlexBox,Button,Toast,ObjectPageSection,FormItem,Form,Text,ObjectPageSubSection,Bar } from "@ui5/webcomponents-react"; import { linkManager } from "@luigi-project/client"; import "@ui5/webcomponents-icons/dist/AllIcons.js"; import { render } from "react-dom"; export default class ProductDetail extends Component { constructor(props) { super(props); this.toast = React.createRef(); } render() { const id = linkManager().currentContext.context.id; let product = ProductCollection.find( (product) => product.id.toString() === id ); if (!product) { product = ProductCollection[0]; } const showToast = () => { this.toast.current.show(); }; const navBack = () => { if (linkManager().hasBack()) { linkManager().goBack(); } else { linkManager().navigate("/home/products"); } }; const icon = () => { return ( <ui5-icon class="samples-margin" name={product.icon} style={{ width: "3rem", height: "3rem" }} ></ui5-icon> ); }; let availability = { state: "Warning", text: this.props.localeDict.OUTOFSTOCK, }; if (product.stock) { availability.state = "Success"; availability.text = this.props.localeDict.AVAILABLE; } return ( <Grid position="Center" defaultIndent="XL1 L1 M1 S1" defaultSpan="XL10 L10 M10 S10"> <ObjectPage headerContent={ <DynamicPageHeader> <FlexBox alignItems="Center" wrap="Wrap"> <FlexBox direction="Column" style={{ padding: "10px" }}> <Label> {this.props.localeDict.AVAILABLEQUANT + product.stock} </Label> </FlexBox> </FlexBox> </DynamicPageHeader> } footer={ <Bar design="FloatingFooter" endContent={ <> <Button design="Emphasized" onClick={showToast}> {this.props.localeDict.ADDTOCART} </Button> <Button design="Default" onClick={navBack}> {this.props.localeDict.BACK} </Button> </> } /> } headerContentPinnable headerTitle={ <DynamicPageTitle actions={ <> <Toast ref={this.toast}> {this.props.localeDict.PRODUCTADDED} </Toast> <Button design="Emphasized" onClick={showToast}> {this.props.localeDict.ADDTOCART} </Button> <Button onClick={navBack}> {this.props.localeDict.BACK} </Button> </> } header={product.name} > <ObjectStatus state={availability.state}> {availability.text} </ObjectStatus> </DynamicPageTitle> } image={icon()} imageShapeCircle onSelectedSectionChange={function noRefCheck() {}} onSelectedSectionChanged={null} selectedSectionId="details" showHideHeaderButton style={{ height: "700px", }} > <ObjectPageSection aria-label="Details" id="details" titleText="Details"> <ObjectPageSubSection aria-label="Details" id="details-subsection" titleText="Details"> <Form columnsL={2} columnsM={2} columnsXL={3} labelSpanL={1} labelSpanM={1} labelSpanXL={1}> <FormItem label="Name"> <Text>{product.name}</Text> </FormItem> <FormItem label={this.props.localeDict.DESCRIPTION}> <Text>{product.description}</Text> </FormItem> <FormItem label={this.props.localeDict.PRICE}> <Text> {product.price + " " + product.currencyCode} </Text> </FormItem> </Form> </ObjectPageSubSection> </ObjectPageSection> </ObjectPage> </Grid> ); } }
-
Add the route configuration to the
index.js
by pasting below code into it:JavaScriptCopy//PASTE THIS AROUND LINE 5 import ProductDetail from './views/productDetail'; //PASTE THE MENTIONED LINE AROUND LINE 39 <BrowserRouter basename={`sampleapp.html#`}> <Route path="/home" render={(props) => <Home {...props} localeDict={dict[this.state.currentLocale]} currentLocale={this.state.currentLocale} />} /> <Route path="/products" render={(props) => <Products {...props} localeDict={dict[this.state.currentLocale]} />} /> //THIS LINE HAS TO BE ADDED: <Route path="/productDetail/:id" render={(props) => <ProductDetail {...props} localeDict={dict[this.state.currentLocale]} />} /> </BrowserRouter>
-
- Step 9
In this step, you can check if your core app is configured correctly so far by running it locally.
-
Open a terminal/command prompt window. Navigate to the
react-core-mf
folder. -
Input
npm start
. Your application should be up and running athttp://localhost:3000/
. You should be able to see the homepage and “Products” view. You can also navigate toward theProductDetail
view via “Products”.
In your Luigi configuration, what is the viewUrl of the "Product Detail" view? (For example, the viewUrl of the "Products" view is /app.html#/products).
-