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
.
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