Skip to content

Conversation

benjamin-thomas
Copy link
Contributor

Attempt to demonstrate how one could use Hyperbole, with just CSS, see #100

The idea is to demonstrate side-by-side the same implementation of the todomvc example.

For the moment, I've managed to inject a custom head via path detection before the library code starts.

I've then extracted common logic to a "shared" module, but I'm not sure how to go about sharing the hyperviews, since they depend on the views themselves, which need to be unique to the 2 module implementations.

Currently, I've renamed some types, e.g. AllTodos => MkTodosView to facilitate my own understanding, but I'll revert this at the end.

hSetBuffering stdout LineBuffering
putStrLn "Starting Examples on http://localhost:3000"

port <- do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put any unreleated improvements / refactors into a separate PR


-- Use the embedded version for real applications (see basicDocument).
-- The link to /hyperbole.js here is just to make local development easier
toDocument :: BL.ByteString -> BL.ByteString
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to branch here. You could either load your custom css on every page by modifying toDocument, or, even better, do everything in TodoCSS.page:

page :: (Todos :> es) => Eff es (Page '[TodosView, TodoView])
page = do
  todos <- Todos.loadAll
  pure $ tag "div" id $ do
     stylesheet "https://cdn.jsdelivr.net/npm/[email protected]/index.min.css"
     -- the rest of your page
     ...

That way, your stylesheet is loaded on your page, and nothing else. All without modifying App.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I branched here because the toDocument function of the example app injects CSS via the head tag, and that would interfere with the CSS loaded after.

image

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.... The issue is that Example.App.hs is serving double-duty as an example of how to bootstrap an application. I'd really like to avoid anything as complex as a branch like this if possible.

This CSS is just a reset. Are you sure it would conflict? If it will definitely mess it up, maybe let's put this in its own app. I'm open to better ideas

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with that assumption but I didn't actually try it. I'll keep you posted.

--- TodosView ----------------------------------------------------------------------------

data AllTodos = AllTodos
data TodosView = MkTodosView
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful not to rename / refactor things to your preference in this PR. It'll make it hard to separate merging the example from a discussion on preferred naming.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I said in the description

Currently, I've renamed some types, e.g. AllTodos => MkTodosView to facilitate my own understanding, but I'll revert this at the end.

It looks like you didn't see it

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it thanks!

page :: (Todos :> es) => Eff es (Page '[TodosView, TodoView])
page = do
todos <- Todos.loadAll
pure $ tag "div" id $ do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment in App.hs, but you can load your stylesheet here:

pure $ tag "div" id $ do
  stylesheet "https://cdn.jsdelivr.net/npm/[email protected]/index.min.css"
  tag "h1" id $ text "Todos CSS"
  ...

todos <- Todos.loadAll
pure $ tag "div" id $ do
tag "h1" id $ text "Todos CSS"
tag "p" id $
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use tag repeatedly? Define aliases for any tags you are missing:

p :: Mod id -> View id () -> View id ()
p = tag "p"

todoForm filt = do
let f :: TodoForm FieldName = fieldNames
tag "div" id $ do
tag "span" id $ do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "span" necessary here? (Does the CSS specifically target span?) Why not just use el?

Perhaps you are also expressing a preference to control the exact html tags used / semantic DOM?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I will definitely create nicely named functions.

By opening the PR in draft mode, my intention was to get early feedback regarding the way I managed to inject the CSS rules for this specific page, and also maybe to get advice and sharing hyperview code wich looks tricky (but you already answered that, so I'll just duplicate code for now)

So the UI is not done currently, I've only implemented the bare essentials to move forward.

@seanhess
Copy link
Owner

great start! I added some comments and questions.

I don't see any use of the external stylesheet yet?

I'm also not sure the best way to include both examples without code duplication. I'd really prefer not to duplicate any non-external-stylesheet code if possible, so the examples don't get out of sync. I'd also prefer to avoid making the examples too complicated or generalized.

Since our TodoMVC with external CSS example is an extension of the normal one, maybe it will help it have it import the original example? Then at least you don't need the .Shared module, you can keep them in the original example and import from there.

@benjamin-thomas benjamin-thomas force-pushed the add-external-css-example branch from ce2c298 to 545106c Compare March 29, 2025 09:29
@benjamin-thomas benjamin-thomas marked this pull request as ready for review March 29, 2025 13:10
@benjamin-thomas
Copy link
Contributor Author

I'm happy with the result currently @seanhess

Maybe you can take a look to see if there are any snags left? And if not maybe as a last step I can try to fuse both examples into the same module and remove the shared module as discussed? Or maybe it's good enough with that bit of duplication?

I suspect the module itself may be a bit messy after that, so I reckon it's a good point to take a look now. Then I can try that last change or revert if it turns out not to be better.

hyper AllTodos $ todosView FilterAll todos

--- AllTodos ----------------------------------------------------------------------------
--- TodosView ----------------------------------------------------------------------------
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function seemed old/unused so I deleted it.

FIXME: but since this example is meant to match as close as possible to the original CSS version
FIXME: and not diverge too much from the other todo example, I'm leaving as-is.
-}
. att "autocomplete" "off"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should go fix the source for input, right? Probably set to "off" most of the time. (Right now input TextInput, which is in the main example, sets it to "text-input" which is wrong).

I wanted to make it easy to set "type" and "autocomplete" with one type, but maybe it wasn't a great choice?

where
filterLi f str =
li' (extClass "filter" . selectedFilter f) $ do
a
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you avoid using Web.View.link deliberately? Or just not know about it? Web.View.Types.Url has an IsString instance:

link "" (onClick (Filter f)) (text str)

@seanhess
Copy link
Owner

seanhess commented Apr 1, 2025

Cool, thanks for doing this! I'll can play with it from here (I need to think about how to present this in a clear way that shows the minimum necessary changes between the two examples).

The Big Question: how did it go? Were you fighting the framework too much? Which helpers or tools would make your experience smoother?

@benjamin-thomas
Copy link
Contributor Author

Great.

The Big Question: how did it go? Were you fighting the framework too much? Which helpers or tools would make your experience smoother?

It went great! I definitely want to play with the library some more. As a learner of the language still, I've been on the lookout for a good Haskell web library for a while, and Hyperbole feels nice because it brings something to the table that's quite different from more traditional solutions. I've played a fair bit with Elixir/Phoenix prior to learning Haskell, but didn't feel compelled to invest into that technology because I was more interested in having a solid language rather than a cool tech. But I'm quite keen to explore a server-first approach to reactivity, it feels quite effective.

The only thing were I felt friction was the global import, that kinda made my understanding a bit muddy at the start.

What I'd like to understand next is:

  • what are the good patterns that play well with Hyperbole?
  • what is the limit of this server-managed workflow, how far can I push it?
    • when/if I need more, how can I delegate to front-end logic gracefully?

@seanhess
Copy link
Owner

Hey @benjamin-thomas, sorry to pull the rug out from under you, but I tagged you in a big refactor PR for web-view (now atomic-css). It'll directly affect your example. seanhess/atomic-css#21

We're getting rid of the global import, but I'm not 100% sure what to replace it with. The "default" utility-based approach will be to import both hyperbole and atomic-css:

import Web.Atomic.CSS
import Web.Hyperbole

If you import only Hyperbole, you'll have access to the @ operator and a class_ attribute modifier that will merge classes for you, but that's it. We aren't exporting a full set of html tags or anything.

LMK if you have any questions, but after reviewing the PR can you update this?

@benjamin-thomas
Copy link
Contributor Author

Hi @seanhess,

Yeah no problem! I intend to look at the refactor PR this week-end.

Then I'll bring this one up to date once it's ready.

I'll keep you posted

@seanhess
Copy link
Owner

Hey @benjamin-thomas any progress on this?

@benjamin-thomas benjamin-thomas force-pushed the add-external-css-example branch from 47be348 to bebc1b4 Compare August 9, 2025 17:02
@benjamin-thomas
Copy link
Contributor Author

benjamin-thomas commented Aug 9, 2025

I've rebased my branch onto main and my original example mostly works now.

I still have a bit of work to do though so I'm putting the PR in draft for now (mainly cleanup and possible remove some unnecessary duplication)

@benjamin-thomas benjamin-thomas marked this pull request as draft August 9, 2025 17:13
@benjamin-thomas benjamin-thomas force-pushed the add-external-css-example branch from 27b8a4c to 36be271 Compare August 10, 2025 10:59
@benjamin-thomas benjamin-thomas changed the title WIP: Add external css example Add external css example Aug 10, 2025
@benjamin-thomas benjamin-thomas marked this pull request as ready for review August 10, 2025 11:02
@benjamin-thomas
Copy link
Contributor Author

@seanhess I think it's ready if you want to take a look

  • I added a missing "delete" action to both implementations
  • I tried to centralize logic between both implementations via wrapper types.
  • I've made it so the list is shared between both implementations

To counteract the CSS reset, I had already tweaked things with a few "style" attributes here and there. It's only minor though, so I don't think it's too much of a problem to leave it as such.

| Destroy FilterTodo Todo
deriving (Generic, ViewAction)

updateTodos :: (Todos :> es, Hyperbole :> es) => TodosAction -> Eff es [Todo]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice factoring here!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@seanhess seanhess merged commit a647441 into seanhess:main Aug 11, 2025
2 checks passed
@seanhess
Copy link
Owner

Look great. I'm going to merge and see if I can get rid of the style tags with another stylesheet or conditionally import the rest. Thanks for doing this!

@seanhess
Copy link
Owner

If you're curious, take a look at my tweaks here: d81e25f

  1. I un-factored your Shared module and duplicated the update logic. I would rather we repeat ourselves and keep the main todo mvc example easy to understand.

  2. I had the css version import things from the main one, rather than from a shared module. Again, your factoring is good for a real app, but for the examples we want to go out of our way for simplicity

  3. Added a todomvc.css to undo the reset and fix rendering issues with DOM differences

  4. field worked fine for the input form. Am I missing something? Just using field "task" does the same thing as manual context you were adding.

Thanks again for your work on this!

@benjamin-thomas
Copy link
Contributor Author

Yeah I wasn't too sure about the Shared module, since it did make the example quite complicated comparatively, makes sens!

Good idea to undo via a following stylesheet

Regarding the input field, I think I did that a while ago, because I wanted to turn off autocomplete and "off" wasn't available as a data constructor (for InputType)

@seanhess
Copy link
Owner

Ah, ok. Feel free to open a PR for the InputType change too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants