Develop the Web UI in SOMOD


SOMOD supports creating the Web UI using NextJS pages.

NextJs is a complete framework in ReactJs with support for routing, static site generation, server-side rendering, image optimization, ...etc

SOMOD helps to create and reuse NextJS pages in modules.

Example:-

In the example from the previous chapter, we have created REST APIs for user management. Now let us create the UI pages for these APIs by following the below steps.

  1. Choose a Styling library
    A styling library is an essential part of Web UI development. It defines the look and feel of the whole application. SOMOD works with any styling libraries used with NextJS and React.

    For this example, let us install and configure Material UI

    npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
  2. Create the following pages under the ui directory

    • ui/pages/_document.tsx

      // ui/pages/_document.tsx // This adds Roboto font across all pages import { Head, Html, Main, NextScript } from "next/document"; const Document = () => { return ( <Html> <Head> <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }; export default Document;
    • ui/pages/_app.tsx

      // ui/pages/_app.tsx import { CssBaseline } from "@mui/material"; import { NextComponentType } from "next"; import { AppProps } from "next/app"; import Head from "next/head"; import { FunctionComponent } from "react"; const App: FunctionComponent<AppProps & { Component: NextComponentType }> = ({ Component, pageProps }) => { return ( <> <Head> <meta name="viewport" content="initial-scale=1, width=device-width" /> </Head> <CssBaseline enableColorScheme /> <Component {...pageProps} /> </> ); }; export default App;
    • ui/pages/index.tsx

      // ui/pages/index.tsx import { Box, Button, Link, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; const GettingStartedHome: NextComponentType = () => { return ( <Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" height="100vh" > <Typography variant="h1">User Management</Typography> <Typography variant="h4"> A Getting Started Project from{" "} <Link href="https://somod.dev" target="_blank"> SOMOD </Link> </Typography> <NextLink href="/user/list" passHref> <Button variant="contained" sx={{ m: 2 }}> Manage Users </Button> </NextLink> </Box> ); }; export default GettingStartedHome;
    • /ui/pages/user/list.tsx

      // /ui/pages/user/list.tsx import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import EditIcon from "@mui/icons-material/Edit"; import { Box, Button, Container, IconButton, Link, List, ListItem, ListItemText, Paper, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useEffect, useState } from "react"; import { UserWithId } from "../../../lib/types"; const deleteUser = async (userId: string) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId, { method: "DELETE" } ); await response.text(); }; const fetchUsers = async () => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/list" ); const users = await response.json(); return users as UserWithId[]; }; const Users: NextComponentType = () => { const [users, setUsers] = useState<UserWithId[]>([]); useEffect(() => { fetchUsers().then(result => { setUsers(result); }); }, []); const onDeleteUser = (userId: string) => { deleteUser(userId).then(() => { const newUsers = users.filter(u => u.userId != userId); setUsers(newUsers); }); }; return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">Users</Typography> <NextLink href="/user/create" passHref> <Button variant="text" sx={{ float: "right" }}> Create New User </Button> </NextLink> </Box> <Paper> <List> {users.map(user => ( <ListItem key={user.userId} secondaryAction={ <> <NextLink href={"/user/" + user.userId + "/edit"} passHref > <IconButton> <EditIcon /> </IconButton> </NextLink> <IconButton onClick={() => { onDeleteUser(user.userId); }} > <DeleteForeverIcon /> </IconButton> </> } > <ListItemText primary={ <NextLink href={"/user/" + user.userId} passHref> <Link>{user.name}</Link> </NextLink> } secondary={user.email} /> </ListItem> ))} </List> </Paper> </Container> ); }; export default Users;
    • /ui/pages/user/create.tsx

      // /ui/pages/user/create.tsx import { Box, Button, Container, Paper, Stack, Switch, TextField, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useRouter } from "next/router"; import { useState } from "react"; import { CreateUserInput, UserWithId } from "../../../lib/types"; const createUser = async (user: CreateUserInput) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user", { method: "POST", body: JSON.stringify(user), headers: { "Content-Type": "application/json" } } ); const createdUser = await response.json(); return createdUser as UserWithId; }; const UserCreate: NextComponentType = () => { const [user, setUser] = useState<CreateUserInput>({ name: "", email: "", active: true, dob: "" }); const router = useRouter(); const updateName = (name: string) => { setUser({ ...user, name }); }; const updateEmail = (email: string) => { setUser({ ...user, email }); }; const updateDob = (dob: string) => { setUser({ ...user, dob }); }; const updateActive = (active: boolean) => { setUser({ ...user, active }); }; const submit = () => { createUser({ name: user.name, email: user.email, dob: user.dob, active: user.active }).then(result => { router.push("/user/" + result.userId); }); }; return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">Create New User</Typography> <Paper sx={{ p: 2 }}> <Stack spacing={2}> <Box> <TextField value={user.name} label="Name" fullWidth required onChange={event => { updateName(event.target.value); }} /> </Box> <Box> <TextField value={user.email} label="Email" fullWidth required onChange={event => { updateEmail(event.target.value); }} /> </Box> <Box> <TextField value={user.dob} label="Date of Birth" fullWidth placeholder="dd/mm/yyyy" InputLabelProps={{ shrink: true }} onChange={event => { updateDob(event.target.value); }} /> </Box> <Box> <Typography variant="subtitle2">Active</Typography> <Switch checked={user.active} size="small" onChange={event => { updateActive(event.target.checked); }} /> </Box> <Button variant="contained" onClick={submit}> Create </Button> <NextLink href={"/user/list"} passHref> <Button variant="text" color="info"> Back to User List </Button> </NextLink> </Stack> </Paper> </Box> </Container> ); }; export default UserCreate;
    • /ui/pages/user/[userId].tsx

      // /ui/pages/user/[userId].tsx import { Box, Button, Container, Paper, Skeleton, Stack, Switch, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { UserWithId } from "../../../lib/types"; const fetchUser = async (userId: string) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId ); const users = await response.json(); return users as UserWithId; }; const UserView: NextComponentType = () => { const [user, setUser] = useState<UserWithId>(); const router = useRouter(); const { userId } = router.query; useEffect(() => { if (userId) { fetchUser(userId as string).then(result => { setUser(result); }); } }, [userId]); return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">User</Typography> <Paper sx={{ p: 2 }}> <Stack spacing={2}> {user ? ( <> <Box> <Typography variant="caption">{user.userId}</Typography> </Box> <Box> <Typography variant="subtitle2">Name</Typography> <Typography variant="body2">{user.name}</Typography> </Box> <Box> <Typography variant="subtitle2">Email</Typography> <Typography variant="body2">{user.email}</Typography> </Box> <Box> <Typography variant="subtitle2"> Date of Birth </Typography> <Typography variant="body2">{user.dob}</Typography> </Box> <Box> <Typography variant="subtitle2">Active</Typography> <Switch readOnly checked={user.active} size="small" /> </Box> <Box> <Typography variant="subtitle2"> Last Updated At </Typography> <Typography variant="body2"> {new Date(user.lastUpdatedAt).toDateString()} </Typography> </Box> <Box> <Typography variant="subtitle2">Created At</Typography> <Typography variant="body2"> {new Date(user.createdAt).toDateString()} </Typography> </Box> <NextLink href={"/user/" + user.userId + "/edit"} passHref> <Button variant="outlined">Edit</Button> </NextLink> <NextLink href={"/user/list"} passHref> <Button variant="text" color="info"> Back to User List </Button> </NextLink> </> ) : ( <> <Skeleton variant="rounded" height={20} /> <Skeleton variant="rounded" height={60} /> <Skeleton variant="rounded" height={60} /> </> )} </Stack> </Paper> </Box> </Container> ); }; export default UserView;
    • /ui/pages/user/[userId]/edit.tsx

      // /ui/pages/user/[userId]/edit.tsx import { Box, Button, Container, Paper, Skeleton, Stack, Switch, TextField, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { UpdateUserInput, UserWithId } from "../../../../lib/types"; const fetchUser = async (userId: string) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId ); const user = await response.json(); return user as UserWithId; }; const updateUser = async (userId: string, user: UpdateUserInput) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId, { method: "PUT", body: JSON.stringify(user), headers: { "Content-Type": "application/json" } } ); const updatedUser = await response.json(); return updatedUser as UserWithId; }; const UserEdit: NextComponentType = () => { const [user, setUser] = useState<UserWithId>(); const router = useRouter(); const { userId } = router.query; useEffect(() => { if (userId) { fetchUser(userId as string).then(result => { setUser(result); }); } }, [userId]); const updateName = (name: string) => { setUser({ ...user, name }); }; const updateEmail = (email: string) => { setUser({ ...user, email }); }; const updateDob = (dob: string) => { setUser({ ...user, dob }); }; const updateActive = (active: boolean) => { setUser({ ...user, active }); }; const submit = () => { updateUser(user.userId, { name: user.name, email: user.email, dob: user.dob, active: user.active }).then(() => { router.push("/user/" + user.userId); }); }; return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">Edit User</Typography> <Paper sx={{ p: 2 }}> <Stack spacing={2}> {user ? ( <> <Box> <Typography variant="caption">{user.userId}</Typography> </Box> <Box> <TextField value={user.name} label="Name" fullWidth required onChange={event => { updateName(event.target.value); }} /> </Box> <Box> <TextField value={user.email} label="Email" fullWidth required onChange={event => { updateEmail(event.target.value); }} /> </Box> <Box> <TextField value={user.dob} label="Date of Birth" fullWidth placeholder="dd/mm/yyyy" InputLabelProps={{ shrink: true }} onChange={event => { updateDob(event.target.value); }} /> </Box> <Box> <Typography variant="subtitle2">Active</Typography> <Switch checked={user.active} size="small" onChange={event => { updateActive(event.target.checked); }} /> </Box> <Button variant="contained" onClick={submit}> Submit </Button> <NextLink href={"/user/" + user.userId} passHref> <Button variant="outlined" color="secondary"> Cancel </Button> </NextLink> </> ) : ( <> <Skeleton variant="rounded" height={20} /> <Skeleton variant="rounded" height={60} /> <Skeleton variant="rounded" height={60} /> </> )} </Stack> </Paper> </Box> </Container> ); }; export default UserEdit;
    • /ui/config.yaml
      Let us use the UI Configuration to bind the API Endpoint URL to the FrontEnd

      # yaml-language-server: $schema=../node_modules/somod-schema/schemas/ui-config/index.json env: NEXT_PUBLIC_USER_API_URL: # adds NEXT_PUBLIC_USER_API_URL environmental variable to nextjs project SOMOD::Parameter: apigateway.http.endpoint # The value of the environmental variable is read from the SOMOD Parameter apigateway.http.endpoint in parameters.json
  3. Start a dev server for the UI

    npx somod start --dev -v

    Open the URL http://localhost:3000 in the browser to see the User Management App

Now the Module is ready with Backend and Frontend Code working together. In the Next Chapter, let us understand how to build and ship the SOMOD module.

Does this page need improvements?
Edit This Page in GitHub
Did this page help you?
Provide feedback in the GitHub Discussion Page
Need More help?

Write an email to opensource@sodaru.com

This documentation is built using
Developed and Maintained By
Sodaru Technologies
https://sodaru.com
© 2023