Server-Side Components
Introduction
IHP Server-Side Components provide a toolkit for building interactive client-side functionality without needing to write too much JavaScript.
A Server-Side Component consist of a state object, a set of actions and a render
function.
The typical lifecycle is like this:
- The component is rendered based as part of a view
- Once loaded, elements inside the component, e.g. a button, can call server-side actions using a simple javascript library
- On the server-side the actions are evaluated and a new state object is generated
- The new state will trigger a re-render
- The re-rendered content will be diffed with the existing HTML and then HTML update instructions will be sent to client and the view is updated accordingly
- Repeat at step 2
- The component is stopped when the page is closed
The Server-Side Component toolkit is currently still in a early development stage. So expect bugs and API changes.
Creating a Component
In this example we’re building a counter component: The counter shows a number. When a button is clicked the number will be incremented.
To create this new component first we’re creating a new file at Web/Component/Counter.hs
(the Component
directory likely does not exist yet, so you need to create it):
module Web.Component.Counter where
import IHP.ViewPrelude
import IHP.ServerSideComponent.Types
import IHP.ServerSideComponent.ControllerFunctions
-- The state object
data Counter = Counter { value :: !Int }
-- The set of actions
data CounterController
= IncrementCounterAction
| SetCounterValue { newValue :: !Int }
deriving (Eq, Show, Data)
$(deriveSSC ''CounterController)
instance Component Counter CounterController where
initialState = Counter { value = 0 }
-- The render function
render Counter { value } = [hsx|
Current: {value} <br />
<button onclick="callServerAction('IncrementCounterAction')">Plus One</button>
<hr />
<input type="number" value={inputValue value} onchange="callServerAction('SetCounterValue', { newValue: parseInt(this.value, 10) })"/>
|]
-- The action handlers
action state IncrementCounterAction = do
state
|> incrementField #value
|> pure
action state SetCounterValue { newValue } = do
state
|> set #value newValue
|> pure
instance SetField "value" Counter Int where setField value' counter = counter { value = value' }
You can see that the Counter
component has a state object with a number data Counter = Counter { value :: !Int }
. It has two actions IncrementCounterAction
and SetCounterValue
. The initialState = Counter { value = 0 }
means that the counter starts at 0.
Inside the render
function you can see how server-side actions are triggered from the client-side:
<button onclick="callServerAction('IncrementCounterAction')">Plus One</button>
When the callServerAction('IncrementCounterAction')
is called, it will trigger the action state IncrementCounterAction = do
haskell block to be called on the server.
You can see that the action
handler get’s passed the current state and will return a new state based on the action and the current state.
FrontController
To make the component available to the app, we need to add it to our Web.FrontController
.
Open the Web/FrontController.hs
and add these imports:
import IHP.ServerSideComponent.RouterFunctions
import Web.Component.Counter
Inside the instance FrontController WebApplication
add a routeComponent @Counter
:
instance FrontController WebApplication where
controllers =
[ startPage WelcomeAction
-- ...
, routeComponent @Counter
]
Now the websocket server for Counter is activated.
Using the Component
We’re adding the component to the standard Welcome view inside our project. But you can use it basically on any view you want.
Open the Web/View/Static/Welcome.hs
and add these imports:
import IHP.ServerSideComponent.ViewFunctions
import Web.Component.Counter
Now we change the welcome view to this:
instance View WelcomeView where
html WelcomeView = [hsx|
<h1>Counter</h1>
{counter}
|]
where
counter = component @Counter
If you wonder why we’re using the where
instead of writing {component @Counter}
: Currently the at-symbol @
is not supported in HSX expressions.
We also need to load the ihp-ssc.js
from our Layout.hs
. Open the Web/View/Layout.hs
and add <script src="/vendor/ihp-ssc.js"></script>
inside your scripts
section:
scripts :: Html
scripts = [hsx|
<!-- ... ->
<script src="/vendor/ihp-ssc.js"></script>
|]
Now when opening the WelcomeView
you will see the newly created counter.
Advanced
Actions with Parameters
Let’s say we have actions like this:
data BooksTableController
= SetSearchQuery { searchQuery :: Text }
| SetOrderBy { column :: Text }
deriving (Eq, Show, Data, Read)
To call the SetSearchQuery
action with a specific searchQuery
value, we can pass this to callServerAction
:
<input
type="text"
value={inputValue searchQuery}
onkeyup="callServerAction('SetSearchQuery', { searchQuery: this.value })"
/>
Fetching from the Database
You can use the typical IHP database operations like query @Post
or createRecord
from your actions.
To fill the inital data you can use the componentDidMount
lifecycle function:
data PostsTable = PostsTable
{ posts :: Maybe [Post]
}
deriving (Eq, Show)
instance Component PostsTable PostsTableController where
initialState = PostsTable { posts = Nothing }
componentDidMount state = do
books <- query @Post |> fetch
state
|> setJust #posts posts
|> pure
render PostsTable { .. } = [hsx|
{when (isNothing posts) loadingIndicator}
{forEach posts renderPost}
|]
The componentDidMount
get’s passed the initial state and returns a new state. It’s called right after the first render once the client has wired up the WebSocket connection.
When the posts
field is set to Nothing
we know that the data is still being fetched. In that case we render a loading spinner inside our render
function.
HTML Diffing & Patching
IHP uses a HTML Diff & Patch approach to update the components HTML. You can see this by analysing the data that is sent over the WebSocket connection.
In the above example, when the Plus One
button of the counter is clicked, the client will send the following message to the server using the WebSocket connection:
{"action":"IncrementCounterAction"}
After that the server will respond:
[{"type":"UpdateTextContent","textContent":"Current: 1","path":[0]}]
So the server only responds with update instructions that transform the counter’s Current: 0
to Counter: 1
.
This is useful if you have many interactive elements that are controlled by JavaScript libraries (e.g. a. <video>
element that is playing). As long as the HTML code of these interactive elements doesn’t change on the server-side, the DOM nodes will not be touched by IHP.
Example Components
If you want to see some more code, you can find components inside the IHP SSC Playground or inside the ihp-ssc-block-editor-demo
repository.