DEV Community

digitallyinduced
digitallyinduced

Posted on

IHP v0.11.0 brings Server-Side Components and Joins!

We've just released IHP v0.11.0, which includes:

  • Server-Side-Components, similar to React or elm
  • Joins, for more complex queries
  • IHP.FileStorage, which makes it easy to store files on AWS S3 or any other compatible service
  • ...and more!

Server-Side-Components aka. you don't need React

IHP's new Server-Side-Components allow you to build React-like components on the server (as the name implies). This allows you to build highly interactive pages without having to worry about server vs. client state, and no change of environment - as everything is still in Haskell.

Let's look at an example component, a basic counter:

-- everything starts with the normal module definition and imports
module Web.Component.Counter where

import IHP.ViewPrelude
import IHP.ServerSideComponent.Types
import IHP.ServerSideComponent.ControllerFunctions

-- Followed by a type definition for the state.
-- This means there will never be any surprise what the state contains.
-- In this case it's simply an Int, so a number.
data Counter = Counter { value :: !Int }

-- Then comes a list of possible actions that can be used to update the state.
-- If you've used redux or the elm architecture, this should be familiar.
-- Defining what actions are available means it's there won't be any surprises here either, and future developers can immediately see what behavior this component supports.
data CounterController
    = IncrementCounterAction
    deriving (Eq, Show, Data)

-- just one line of boilerplate to generate some code that we really don't want to write ourselves...
$(deriveSSC ''CounterController)

-- The heart of any component are
-- 1. the render function, taking care of turning the state into html
-- 2. action handlers, which update the state based on selected actions
instance Component Counter CounterController where
    -- but first, we need to define the default state
    initialState = Counter { value = 0 }

    -- the render function takes the state and return HTML
    render Counter { value } = [hsx|
        Current: {value} <br />
        <button onclick="callServerAction('IncrementCounterAction')">Plus One</button>
    |]

    -- this is the handler for the IncrementCounterAction
    -- the function takes a state and the action, modifies the state (in this case incrementing the counter's value) and returns the new value
    action state IncrementCounterAction = do
        state
            |> incrementField #value
            |> pure

-- this allows us to use 'incrementField' to increment the counter
instance SetField "value" Counter Int where setField value' counter = counter { value = value' }
Enter fullscreen mode Exit fullscreen mode

And the result is a beautiful counter, all controlled server-side:
A short video showing part of a webpage with the headline "Counter demo", some text that says "Current: 0" and a button that says "Plus One", which is being clicked a couple of times. Every click increases the number after "Current" by one.

Here's a little gif showcasing a more complex use-case of a filterable and sortable table:
A short video showing a webpage with a table with titles and "published at" dates. Underneath there's an input element. When something is typed in the input, the rows are filtered based on the input's value. Clicking on the "published at" column title sorts the rows by that column.

To learn more check out the documentation.

Joins

If you've tried to do complex queries involving joins of multiple tables in IHP, this was previously a little bit of a hassle. With first-class support for joins, there's now a completely type-safe way to do anything you can imagine.

Here's an example of how you can select all posts that were written by a user with the name "Tom":

tomPosts <- query @Post
        |> innerJoin @User (#authorId, #id)
        |> filterWhereJoinedTable @User (#name, "Tom")
        |> fetch
Enter fullscreen mode Exit fullscreen mode

And here's a more complex example that selects all posts that were tagged by a tag named "haskell" in the case of a many-to-many relationship:

query @Posts
        |> innerJoin @Tagging (#id, #postId)
        |> innerJoinThirdTable @Tag @Tagging (#id, #tagId)
        |> filterWhereJoinedTable @Tag (#tagText, "haskell")
        |> fetch
Enter fullscreen mode Exit fullscreen mode

IHP.FileStorage to store files in AWS S3-compatible services

Instead of having to manually use the APIs of AWS S3, you can now use our special support for these services to upload anything you'd like.

Check out this example for an action used to upload a logo of a company to S3:

action UpdateCompanyAction { companyId } = do
    company <- fetch companyId
    company
        |> fill @'["name"]
        |> uploadToStorage #logoUrl
        >>= ifValid \case
            Left company -> render EditView { .. }
            Right company -> do
                company <- company |> updateRecord
                redirectTo EditCompanyAction { .. }
Enter fullscreen mode Exit fullscreen mode

As you can see it's not much different from any other action used to update the company data. All the magic happens due to one line: |> uploadToStorage #logoUrl.

You can do much more though, for example:

  • use ImageMagick to do pre-processing of the uploaded image file (resizing, conversion,...)
  • generate secure, signed download URLs for private files
  • in development, files are stored on-disk in the static/ folder for ease of debugging and offline-only development

Find out how to use it and configure it for your project in the documentation.

Case Insensitivity

We've added filterWhereCaseInsensitive and validateIsUniqueCaseInsensitive, which do exactly what you expect them to do.

If you're using IHP's login, be aware that these functions are now used for checking the emails. This increases UX for your users, but in case someone messed up and created two accounts - one with some letters of their email uppercase, one not - you will have to migrate that account.

Conclusion

If you haven't tried out IHP yet, now's a great time to do so. We'll be at ZuriHack (huge Haskell Hackathon), so sign up for free there if you haven't already and get an introduction to IHP from us directly!

Top comments (0)