web-frontendpurescriptpurescript-halogen

Purescript compiles but js throws error : component.initialState is not a function


I'm building a simple video player component using purescript-halogen. The component is supposed to show a blank div with only an input button for the user to select a local file which will then act as a source URL for the video element.

I completed a working model in plain javascript and wanted to port it to purescript/halogen. I got the purescript version to compile but the web console gives me an error message Uncaught TypeError: component.initialState is not a function and points me to this MDN reference.

This would suggest an issue with how I defined my initialState function but it's fairly standard:

component :: forall q i o m. MonadAff m => H.Component q i o m
component =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }

type State =
  { videoUrl :: Maybe String }

initialState :: forall i. i -> State
initialState _ =
  { videoUrl : Nothing
  }

Is it possible that the function is called at the wrong time during its lifecycle, hence making it undefined?

The code for the component is hosted on this Github gist. And I've been following this Github issue as reference.

My package.json :


  "private": true,
  "devDependencies": {
    "parcel": "1.12.3",
    "purescript": "^0.14.0",
    "spago": "^0.19.1"
  },
  "scripts": {
    "build": "spago build",
    "test": "spago test",
    "serve": "parcel dev/index.html --open",
    "build-prod": "mkdir -p prod && cp dev/index.html prod/ && rm -rf dist && spago bundle-app --to prod/index.js && parcel build prod/index.html"
  },
  "dependencies": {
    "node": "^15.12.0"
  }
}

I compiled using purescript v0.14.0, spago v0.20.0, node v16.3.0 and tested on Firefox v89.0 in Ubuntu 20.04.2 LTS.


Solution

  • Seems the bug is from a pesky line in the code:

    HH.input 
      [ ...
      , HP.value "Select file"
      ...]
    

    According to MDN, the value property of an input element is used to hold the path to the selected files. Therefore, setting it during element creation is not a well defined action.

    The final code (which runs as expected) is:

    module App.Video (component) where
    
    import DOM.HTML.Indexed.InputAcceptType (InputAcceptType, InputAcceptTypeAtom(..))
    import Data.Foldable (traverse_)
    import Data.Maybe (Maybe(..))
    import Data.MediaType (MediaType(..))
    import Data.Traversable (for_)
    import Effect.Aff.Class (class MonadAff)
    import Halogen as H
    import Halogen.HTML as HH
    import Halogen.HTML.Events as HE
    import Halogen.HTML.Properties as HP
    import Prelude (Unit, bind, const, discard, pure, unit, ($), (=<<), (>>=))
    import Web.Event.Event as Event
    
    import Web.File.File as File
    import Web.File.FileList as FileList
    import Web.File.FileReader.Aff as FileReaderAff
    import Web.HTML.HTMLInputElement as InputElement
    
    data Action = HandleFileUpload Event.Event
    
    type State =
      { videoUrl :: Maybe String }
    
    component :: forall q i o m. MonadAff m => H.Component q i o m
    component =
      H.mkComponent
        { initialState
        , render
        , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
        }
    
    initialState :: forall input. input -> State
    initialState inp =
      { videoUrl : Nothing
      }
    
    render :: forall m slots. State -> H.ComponentHTML Action slots m
    render state =
        case state.videoUrl of
            Nothing -> blank_player
            Just url -> video_player url
    
    supported_formats :: InputAcceptType
    supported_formats = 
        HP.InputAcceptType
            [ AcceptMediaType (MediaType "video/mp4")
            , AcceptMediaType (MediaType "video/webm")
            ]
    
    blank_player :: forall w. HH.HTML w Action
    blank_player = 
        HH.div_ 
            [ HH.span_ [HH.text "Choose file to upload"]
            , HH.input
                [ HP.type_ HP.InputFile
                , HP.accept supported_formats
                , HE.onChange HandleFileUpload
                ]
        ]
    
    video_player :: forall w i. String -> HH.HTML w i
    video_player url = 
        HH.div_ [
            HH.video [HP.src url] []
        ]
    
    handleAction :: forall m slots o. MonadAff m => Action -> H.HalogenM State Action slots o m Unit 
    handleAction = case _ of
      HandleFileUpload ev → do
        traverse_ handleFileUpload (InputElement.fromEventTarget =<< Event.target ev)
    
    handleFileUpload :: forall m slots o. MonadAff m => InputElement.HTMLInputElement -> H.HalogenM State Action slots o m Unit 
    handleFileUpload inputEl = do
      H.liftEffect (InputElement.files inputEl) >>= traverse_ \files ->
        for_ (FileList.item 0 files) \file → do
          video_url ← H.liftAff $ FileReaderAff.readAsDataURL (File.toBlob file)
          H.modify_ (const { videoUrl : Just video_url})
          pure unit