Building an Ionic App with Grafbase Api.
Welcome to another exciting adventure into the world of coding.
Today we are going to be building an Ionic todo app with grafbase and React.
Grafbase is an exciting solution for developers to create and deploy mobile/ web apps with ease and speed. It also helps to create Api's easily for use, you can check their documentation out in this link https://hshno.de/grafbase-signup.
To get Started, click on the get Started button in the grafbase hompage, you will need to signin if you already have an account, and if you don't you will have to create one, after successfully signing in it will take you to their starter guide where you will install their cli using:
command
The todoapp is the name of the app, which you can change to the name of your app. Next, we are going to be building the schema for our todo app. From the code below you can see that we defined the todo type and the todoList type. According to the documentation you will have to manually install the grafbase sdk using the command "npm install @grafbase/sdk".
import { g, auth, config } from '@grafbase/sdk';
// Welcome to Grafbase!
// Define your data models, integrate auth, permission rules, custom resolvers, search, and more with Grafbase.
// Integrate Auth
// https://grafbase.com/docs/auth
//
// const authProvider = auth.OpenIDConnect({
// issuer: process.env.ISSUER_URL ?? ''
// })
//
// Define Data Models
// https://grafbase.com/docs/database
const Todo = g.model('Todo', {
title: g.string().length({ min: 4, max: 255 }),
notes: g.string().optional(),
complete: g.boolean().default(false),
});
const TodoList = g.model('TodoList', {
title: g.string().length({ min: 4, max: 50 }),
todos: g.relation(Todo).optional().list(),
});
// Extend models with resolvers
// https://grafbase.com/docs/edge-gateway/resolvers
// gravatar: g.url().resolver('user/gravatar')
export default config({
schema: g
// Integrate Auth
// https://grafbase.com/docs/auth
// auth: {
// providers: [authProvider],
// rules: (rules) => {
// rules.private()
// }
// }
})
Then we run the api with the command: "npx grafbase dev", this will give you that endpoint and , which you can use to go to the graphiql playground where you will test the api using mutationa and queries.
Next, we create the React app, this is basically the commands you will need to create the Ionic-React app:
command
$ ionic start myApp tabs --type react
$ ionic serve
You will install the cli in your root folder using the first command, then create you ionic-react app using the second command, and in ionic your app name should always start with a small letter, then you can start the ionic-react app with "ionic serve". This will start your app on a localhost link which will be shown when it is ready.
After that, you will start buiding your Todo app Ui with ionic web components and React hooks. From the code below, you can see the Home Page: We are using the useQuery, gql and useMutation hooks gotten from apollo/client to access the data from the graphql api.
import {
IonButton,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle,
IonContent,
IonHeader,
IonIcon,
IonPage,
IonTitle,
IonToolbar,
useIonAlert,
useIonLoading,
} from '@ionic/react'
import { useQuery, gql, useMutation } from '@apollo/client'
import { add } from 'ionicons/icons'
export interface TodoList {
id: string
title: string
todos: { edges: { node: Todo }[] }
}
export interface Todo {
id: string
title: string
notes: string
complete: boolean
}
// Retrieve collection with Live Queries
export const TODOLIST_QUERY = gql`
query @live {
todoListCollection(first: 10) {
edges {
node {
id
title
todos(first: 100) {
edges {
node {
id
}
}
}
}
}
}
}
`
const CREATE_LIST_MUTATION = gql`
mutation CreateList($title: String!) {
todoListCreate(input: { title: $title, todos: [] }) {
todoList {
id
title
todos(first: 100) {
edges {
node {
id
}
}
}
}
}
}
`
const Home: React.FC = () => {
// Query our collection on load
const { data, loading, error } = useQuery<{
todoListCollection: { edges: { node: TodoList }[] }
}>(TODOLIST_QUERY)
const [createList] = useMutation(CREATE_LIST_MUTATION)
const [presentAlert] = useIonAlert()
const [showLoading, hideLoading] = useIonLoading()
const addList = () => {
presentAlert({
header: 'Please enter a name for your new list',
buttons: ['Create List'],
inputs: [
{
name: 'name',
placeholder: 'Tasklist',
min: 2,
},
],
onDidDismiss: async (ev: CustomEvent) => {
await showLoading()
console.log('run mutation..')
// Run the mutation
const res = await createList({
variables: { title: ev.detail.data.values.name },
})
console.log('๐ ~ file: Home.tsx:97 ~ onDidDismiss: ~ res', res)
hideLoading()
},
})
}
return (
<IonPage>
<IonHeader>
<IonToolbar color="primary">
<IonTitle>My Todos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
{data && !loading && !error && (
<>
{data?.todoListCollection?.edges?.map(({ node }) => (
<IonCard button routerLink={`/home/${node?.id}`} key={node?.id}>
<IonCardHeader>
<IonCardTitle>{node?.title}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
Tasks: {node?.todos?.edges.length ?? 0}
</IonCardContent>
</IonCard>
))}
</>
)}
<IonButton expand="block" onClick={() => addList()}>
<IonIcon icon={add} slot="start"></IonIcon>
Create new List
</IonButton>
</IonContent>
</IonPage>
)
}
export default Home
We also have the TodoList which is written below:
import {
IonBackButton,
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonItemOption,
IonItemOptions,
IonItemSliding,
IonLabel,
IonList,
IonPage,
IonTitle,
IonToolbar,
useIonLoading,
useIonRouter,
} from '@ionic/react'
import React, { useRef, useState } from 'react'
import { RouteComponentProps } from 'react-router'
import { useQuery, gql, useMutation } from '@apollo/client'
import { checkmarkDoneOutline, trashBinOutline } from 'ionicons/icons'
import { Todo, TODOLIST_QUERY } from './Home'
interface TodolistPageProps
extends RouteComponentProps<{
id: string
}> {}
const TODOLIST_DETAILS_QUERY = gql`
query GetTodolistDetails($id: ID!) {
todoList(by: { id: $id }) {
title
id
todos(first: 100) {
edges {
node {
id
title
notes
complete
}
}
}
}
}
`
const CREATE_TODO_MUTATION = gql`
mutation CreateTodo($title: String!) {
todoCreate(input: { title: $title, complete: false, notes: "" }) {
todo {
id
}
}
}
`
const UPDATE_TODOLIST_MUTATION = gql`
mutation UpdateTodolist($listID: ID!, $todoID: ID!) {
todoListUpdate(by: { id: $listID }, input: { todos: [{ link: $todoID }] }) {
todoList {
title
todos(first: 100) {
edges {
node {
id
}
}
}
}
}
}
`
const DELETE_TODO_MUTATION = gql`
mutation DeleteTodo($id: ID!) {
todoDelete(by: { id: $id }) {
deletedId
}
}
`
const UPDATE_TODO_MUTATION = gql`
mutation UpdateTodo($id: ID!, $complete: Boolean) {
todoUpdate(by: { id: $id }, input: { complete: $complete }) {
todo {
title
complete
id
}
}
}
`
const DELETE_TODOLIST_MUTATION = gql`
mutation DeleteTodolist($id: ID!) {
todoListDelete(by: { id: $id }) {
deletedId
}
}
`
const Todolist: React.FC<TodolistPageProps> = ({ match }) => {
const { data, loading, error } = useQuery<{
todoList: { id: String; title: String; todos: { edges: { node: Todo }[] } }
}>(TODOLIST_DETAILS_QUERY, {
variables: { id: match.params.id },
})
const [updateTodolist] = useMutation(UPDATE_TODOLIST_MUTATION)
const [deleteTodolist] = useMutation(DELETE_TODOLIST_MUTATION)
const [createTodo] = useMutation(CREATE_TODO_MUTATION)
const [deleteTodo] = useMutation(DELETE_TODO_MUTATION)
const [updateTodo] = useMutation(UPDATE_TODO_MUTATION)
const [title, setTitle] = useState('')
const listRef = useRef<HTMLIonListElement>(null)
const inputRef = useRef<HTMLIonInputElement>(null)
const ionRouter = useIonRouter()
const [showLoading, hideLoading] = useIonLoading()
const addTodo = async () => {
showLoading()
// Create a todo
const createTodoResult = await createTodo({
variables: { title },
})
// Access the result ID
const todoId = createTodoResult.data.todoCreate.todo.id
// Update the todolikst to include the todo
await updateTodolist({
variables: { listID: match.params.id, todoID: todoId },
refetchQueries: [TODOLIST_DETAILS_QUERY],
})
inputRef.current!.value = ''
hideLoading()
}
const deleteTodoById = (id: string) => {
deleteTodo({
variables: { id: id },
refetchQueries: [TODOLIST_DETAILS_QUERY],
})
}
const updateTodoById = async (id: string) => {
await updateTodo({
variables: { id: id, complete: true },
refetchQueries: [TODOLIST_DETAILS_QUERY],
})
listRef.current?.closeSlidingItems()
}
const deleteList = () => {
deleteTodolist({
variables: { id: match.params.id },
refetchQueries: [TODOLIST_QUERY],
})
ionRouter.goBack()
}
return (
<IonPage>
<IonHeader>
<IonToolbar color="secondary">
<IonButtons slot="start">
<IonBackButton defaultHref="/home" />
</IonButtons>
<IonTitle>{data?.todoList.title}</IonTitle>
<IonButtons slot="end">
<IonButton onClick={() => deleteList()}>
<IonIcon icon={trashBinOutline} slot="icon-only" />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonItem>
<IonInput
ref={inputRef}
placeholder="Use Grafbase"
onIonChange={(e: any) => setTitle(e.target.value)}
></IonInput>
<IonButton expand="full" slot="end" onClick={() => addTodo()}>
Add Task
</IonButton>
</IonItem>
{data && !loading && !error && (
<IonList ref={listRef}>
{data?.todoList?.todos?.edges.map(({ node }) => (
<IonItemSliding key={node.id}>
<IonItem>
<span
style={{
textDecoration: node.complete ? 'line-through' : '',
opacity: node.complete ? 0.4 : 1,
}}
>
{node.title}
</span>
</IonItem>
<IonItemOptions side="start">
{!node.complete && (
<IonItemOption
color="success"
onClick={() => updateTodoById(node.id)}
>
<IonIcon
icon={checkmarkDoneOutline}
slot="icon-only"
></IonIcon>
</IonItemOption>
)}
</IonItemOptions>
<IonItemOptions side="end">
<IonItemOption
color="danger"
onClick={() => deleteTodoById(node.id)}
>
<IonIcon icon={trashBinOutline} slot="icon-only"></IonIcon>
</IonItemOption>
</IonItemOptions>
</IonItemSliding>
))}
</IonList>
)}
{data && data?.todoList?.todos?.edges.length === 0 && (
<div
className="ion-padding"
style={{ textAlign: 'center', fontSize: 'large' }}
>
<IonLabel color="medium">Add your first item now!</IonLabel>
</div>
)}
</IonContent>
</IonPage>
)
}
export default Todolist
This is what our App.tsx will contain:
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css'
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css'
import '@ionic/react/css/structure.css'
import '@ionic/react/css/typography.css'
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css'
import '@ionic/react/css/float-elements.css'
import '@ionic/react/css/text-alignment.css'
import '@ionic/react/css/text-transformation.css'
import '@ionic/react/css/flex-utils.css'
import '@ionic/react/css/display.css'
/* Theme variables */
import './theme/variables.css'
import Todolist from './pages/Todolist'
setupIonicReact()
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/home/:id" component={Todolist} />
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
export default App;
Then to finish it up in the index.tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
import reportWebVitals from './reportWebVitals'
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
split,
HttpLink,
} from '@apollo/client'
import { isLiveQuery, SSELink } from '@grafbase/apollo-link'
import { getOperationAST } from 'graphql'
import { SignJWT } from 'jose'
//Example of genreating a JWT:
// const secret = new Uint8Array(
// 'mysupersecretk3y!'.split('').map((c) => c.charCodeAt(0)),
// )
// const getToken = () => {
// return new SignJWT({ sub: 'user_1234', groups: [] })
// .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
// .setIssuer('https://devdactic.com')
// .setIssuedAt()
// .setExpirationTime('2h')
// .sign(secret)
// }
// getToken().then((token) => {
// console.log(token);
// })
// const GRAFBASE_API_URL = 'http://127.0.0.1:4000/graphql'
const GRAFBASE_API_URL =
'https://todo-main-dkingofcode.grafbase.app/graphql'
// Use JWT in a real app or API Key for testing with x-api-key
const JWT_TOKEN =
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQiLCJncm91cHMiOltdLCJpc3MiOiJodHRwczovL2RldmRhY3RpYy5jb20iLCJpYXQiOjE2OTE5MzAxMjMsImV4cCI6MTY5MTkzNzMyM30.DbaFWX1ROsOAubsZBpQdKDwaJsimY5-WNkQyXihHuls'
export const createApolloLink = () => {
const sseLink = new SSELink({
uri: GRAFBASE_API_URL,
headers: {
authorization: JWT_TOKEN,
},
})
const httpLink = new HttpLink({
uri: GRAFBASE_API_URL,
headers: {
authorization: JWT_TOKEN,
},
})
return split(
({ query, operationName, variables }) =>
isLiveQuery(getOperationAST(query, operationName), variables),
sseLink,
httpLink,
)
}
const link = createApolloLink()
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
})
const container = document.getElementById('root')
const root = createRoot(container!)
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister()
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
Now you will need to deploy the grafbase api you first created by creating a git repository, first you will need to got to the grafbase root directory in your terminal and type:
command
Then after initialising your root directory, you will add all your code to the stagin area using this command:
command
After adding all your code, you will commit this code using the command:
command
You can change the commit message if you want to, then you got to your github account and create a new repository and then you copy the url link given to you, then you connect it to your directory using this command:
command
Then you push your code to github using this command:
command
You can change your remote branch to main before pushing if you want to, it's up to you.
Then since you have successfully uploaded your code to github, then you can go to grafbase and connect your githhub account to grafbase, after that you will type the name of your grafbase api repository you just uploaded and search for it on grafbase, if you have connected your github account you could just search for the repositiory straight away. Then when you have found it, you will have to import the repository to grafbase using the import button right next to the repo name. Then you will be directed to a deployment page, where you can deploy your api.
You can now change the grafbase api url in the index.tsx to the grafbase api url given to you after you have deployed. To get it you click on the connect button, which will show you your url and your api key.