8000 Initial commit of Components by dmjio · Pull Request #766 · dmjio/miso · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Initial commit of Components #766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Mar 6, 2025
Merged

Initial commit of Components #766

merged 56 commits into from
Mar 6, 2025

Conversation

dmjio
Copy link
Owner
@dmjio dmjio commented Feb 19, 2025

🍜 Miso Components

Adds React-style components to Miso. This allows the embedding of App within App using an existential wrapper SomeComponent implemented as a new branch of the View tree.

⚠️ This is a work in progress ⚠️

API

data View action
  = Node NS  MisoString (Maybe Key) [Attribute action]  [View action] 
  | Text MisoString (View action)                                                                                                                                                                                                       
  | TextRaw MisoString  (View action)                                                                                                                                                                                                    
  | ComponentNode SomeComponent (ComponentOptions action) (View action)                                                                                                                                                                                               
                                                                              
8000
                                                                                                                                                            
data SomeComponent = forall name model action . Eq model => SomeComponent (Component name model action)                                                                                                                                                           

data Component (name :: Symbol) model action
  = Component
   { name :: MisoString
   , app :: App model action
   }

data ComponentOptions action
  = Component
   { onMounted :: Maybe action
   , onUnmounted :: Maybe action
   , key :: Maybe Key
   , attributes :: [ Attribute action ]
   }

-- Smart constructor
componentOptions :: ComponentOptions action
componentOptions = Component Nothing Nothing Nothing []

component :: App model action -> Component name model action
component app = Component (symbolVal @name) app
-- ^ dmj: we do this so 'name' is statically known and used as the `data-component-id`
                                                                                                                                                                                                                                          
embed :: Eq model => Component name model a -> View action                                                                                                                                                                                       
embed app = SomeComponent (Component app) componentOptions -- used for mount / unmount hooks                                                                                                                                                                                                                                           
-- ^ note how `action` and `a` are different. This is how we embed `App` inside each other (recursively)

embedWith :: Eq model => Component name model a -> ComponentOptions action -> View action                                                                                                                                                                                       
embedWith app = SomeComponent (Component app)

-- example
calendar :: Component "calendar-app" CalendarModel CalendarAction
calendar = component calendarApp

view m = div_ [ ] [ embed calendar ]

notify :: Component name model action -> action -> Effect a m
-- ^ `actions`'s unify here, ensuring that we're sending the right `action` to the right `App`

-- example
update action model = do
  notify calendar (NewEntry (todo model))
  • Adds SomeComponent existential wrapper used for nesting App
  • Adds notify for bidirectional communication between components (parent, child or siblings)
  • Modifies diff.js to mount / unmount components
  • Creates top-level ComponentMap used to store Sinks and Threads for each Component
  • Adds components example (modified from simple.jsexe)
  • Renames Notify to Waiter (reuses 'notify' elsewhere)
  • Creates Miso.Internal
  • Updates nix infra

Outstanding:

  • Make a better example in examples/components
  • Add documentation
  • Add test for recursive component mounting
  • Make top-level componentApp startComponent
  • console.error in mountComponent if a component has already been mounted w/ the same name.
  • Add tests for Component mounting and unmounting, and syncChildren
  • Experiment with ways to statically enforce uniqueness of App names and mountPoints (e.g. App "todo-app" model action)
  • Add [Attribute action] on Component (allows events, attributes to be placed on Component)
  • Use data-component-id attribute for identifying Components
  • Consider defining Effect as Transition
  • Add syncCallback' (sync callbacks that return a value), under wasm, ghcjs, new js backend and native.
  • Implement Key on Component
  • Consider modifying Effect to be a State monad so m won't be required for use with notify(will just add mail function in Transition interface for now.
  • Add 'mail' function for message passing
  • Implement stubs for diffing components w/ other elements in diff.js
  • Add component lifecycle hooks (onMounted, onUnmounted, etc.)
  • Ensure you free all Callbacks and removeEventListener for all events in a Component on unmount
  • Update isomorphic functionality to support components
  • Event delegation updated to account for components

Pitfalls:

  • It is possible to create cycles in the component graph by sending mutually recursive notifications. This is true currently in miso as well (vanilla non-component miso has the same issue). So just be responsible.

  • Components can accidentally be nested recursively and mutually recursively. This is near impossible to catch statically w/o introducing GADTs (we tried HRose), and while it works, it significantly alters the UI. The component implementation as it exists right now is completely backwards compatible. Simplicity is in the spirit of the project so we don't plan on introducing fancy types to ameliorate the situation. Component names should always be unique, and the nesting of components should always form an acyclic graph.

- Adds Component existential wrapper used for nesting App
- Adds `notify` for bidirectional communication between components (parent, child or siblings)
- Modifies diff.js to mount / unmount components
- Creates top-level ComponentMap used to store sinks and threads for each Component
- Adds components example (modified from simple.jsexe)
- Renames Notify to Waiter (reuse 'notify' elsewhere)
- Creates Miso.Internal
@brakubraku
Copy link
Contributor

Let's go!

@brakubraku
Copy link
Contributor

Perhaps effectful can be a useful guide/inspiration on how to implement static safety?

@dmjio
Copy link
Owner Author
dmjio commented Feb 21, 2025

@brakubraku Will check it out. We have options. Want to keep it simple for the LLMs too. You can join our matrix server if you want to discuss more.

dmjio added 11 commits February 21, 2025 15:01
- Update interface for haskell-miso
- Major diff.js refactoring (tests pass) to handle 'vcomp'
- Added mounting and unmounting of Components
- removeEventListener called during unmount
- Fix 'destroy' name clash in tests
- Bring in synchronous callbacks from ghcjs-base for the ghcjs build
- Export code coverage from top-level
- Add mail function (like notify)
- Use strict Map
- Adds syncCallback, and uses it with lifecycle hooks
@dmjio dmjio force-pushed the components branch 3 times, most recently from c1cee59 to fb4d9b7 Compare February 23, 2025 13:44
8000
- Adds a builder for packaging wasm built from cabal
- Updates components to remove -threaded, add hs_start export
- Remove allowUnfree, allowUnbroken
- Move releases into top-level export
- Since syncCallback' has not yet been included there are bugs when
attempting to free resources (when doing it async).

- When syncCallback' is added to jsaddle-wasm, incorporate it.
@dmjio dmjio force-pushed the components branch 2 times, most recently from 0a1caa3 to 297fcdb Compare March 5, 2025 11:17
@dmjio dmjio force-pushed the components branch 2 times, most recently from 99bb975 to 7076c8c Compare March 6, 2025 04:47
@dmjio dmjio merged commit c6b1f4d into master Mar 6, 2025
3 checks passed
@dmjio
Copy link
Owner Author
dmjio commented Mar 6, 2025

cc @Bobris

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.

3 participants
0