Skip to content

Latest commit

 

History

History
689 lines (594 loc) · 20.2 KB

File metadata and controls

689 lines (594 loc) · 20.2 KB
title: Develop the Web UI in SOMOD
meta:
  description:
    Developing the Web UI is made easy with SOMOD. SOMOD helps to create and reuse NextJS pages in modules.

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.