Giter VIP home page Giter VIP logo

purescript-halogen-formless's Introduction

Formless

CI Latest release Maintainer: thomashoneyman

Formless helps you write forms in Halogen without the boilerplate.

Installation

Install Formless with Spago:

$ spago install halogen-formless

Formless 3 is available in package sets beginning with psc-0.14.7-20220303. If you are using a package set that does not include Formless, then you can add it to your local set as shown in the example below:

let upstream = ...

in  upstream
  with halogen-formless =
    { version = "v3.0.0"
    , repo = "https://github.com/thomashoneyman/purescript-halogen-formless.git"
    , dependencies =
        [ "convertable-options"
        , "effect"
        , "either"
        , "foldable-traversable"
        , "foreign-object"
        , "halogen"
        , "heterogeneous"
        , "maybe"
        , "prelude"
        , "record"
        , "safe-coerce"
        , "type-equality"
        , "unsafe-coerce"
        , "unsafe-reference"
        , "variant"
        , "web-events"
        , "web-uievents"
        ]
    }

Tutorial

We're going to write a form from scratch, demonstrating how to use Formless with no helper functions. This tutorial can serve as the basis for your real applications, but you'll typically write your own helper functions for common form controls and validation in your app. Make sure to check out the examples directory after you read this tutorial to expand your knowledge!

Our form will let a user register their cat for pet insurance by recording its name, nickname, and age. Let's take the first step!

Define a form type

We'll start by defining a type for our form.

type Form :: (Type -> Type -> Type -> Type) -> Row Type
type Form f =
  ( name     :: f String String String
  , nickname :: f String Void   (Maybe String)
  , age      :: f String String Int
  --              input  error  output
  )

Form types are typically defined as a row of form fields, where each form field specifies its input, error, and output type as arguments to f.

  • The input type describes what the form field will receive from the user. For example, a text input will receive a String, while a radio group might use a custom sum type.
  • The error type describes what validation errors can occur for this form field. We'll stick to String for our example, but you can create your own form- or app-specific error types.
  • The output type describes what our input type will parse to, if it passes validation. For example, while we'll let the user type their cat's age into a text field and therefore accept a String as input, in our application we will only consider Int ages to be valid.

Take a moment and think about what the input, error, and output types for each of our three fields are. Our nickname field has an output type of Maybe String -- what do you think that represents?

Defining our form row this way provides maximum flexibility for defining other type synonyms in terms of the form row. This greatly reduces the amount of code you need to write for your form. For example, Formless requires that we provide an initial set of values for our form fields:

initialValues = { name: "", nickname: "", age: "" }

We can write a type for this value by writing a brand new record type, or by reusing our form type:

import Formless as F

-- Option 1: Define a new record type
type FormInputs = { name :: String, nickname :: String, age :: String }

-- Option 2: Reuse our form row
type FormInputs = { | Form F.FieldInput }

These two implementations of FormInputs are identical. However, reusing the form row requires less typing and ensures a single source of truth.

Write component types

Formless is a higher-order component, which means that it takes a component as an argument and returns a new component. The returned component can have any input, query, output, and monad types you wish -- Formless is entirely transparent from the perspective of a parent component.

Public Types

Let's write concrete types for our component's public interface. We don't need any input or to handle any queries, but we'll have our form raise a custom success message and a valid Cat as its output.

-- Reusing our form row again! This type is identical to:
-- { name :: String, nickname :: Maybe String, age :: Int }
type Cat = { | Form F.FieldOutput }

type Query = Const Void

type Input = Unit

type Output = { successMessage :: String, newCat :: Cat }

-- We now have the types necessary for our wrapped component,
-- which we'll run in `Aff`:
component :: H.Component Query Input Output Aff

Internal Types

Next, we'll turn to our internal component types: the state and action types (we don't need any child slots, so we'll hard code them to ()).

Formless requires our component to support two actions:

  • Your component must receive input of type FormContext, which includes the form fields and useful actions for controlling the form. It also includes any other input you want your component to take. By convention this action is called Receive.
  • Your component must raise actions of type FormlessAction to Formless for evaluation. By convention this action is called Eval.

The FormContext and FormlessAction types you need to write for your Action type can be easily implemented by reusing your form row along with type synonyms provided by Formless. Let's define these two types for our form:

-- Our form will receive `FormContext` as input. We can specialize the Formless-
-- provided `F.FormContext` type to our form by giving it our form row applied
-- to the `F.FieldState` and `F.FieldAction` type synonyms.
--
-- The form context includes the current state of all fields in the form, so its
-- first argument is our form row applied to `F.FieldState`. It also includes a
-- set of actions for controlling the form, so our second argument is our form
-- row and component action type applied to `F.FieldAction`. Finally, the form
-- context passes through the input type we already defined for our component
-- (in our case, `Unit`), and so it takes the `Input` type as its third
-- argument. Finally, it provides some form-wide helper actions, and so we must
-- provide our `Action` type as the fourth argument.
type FormContext = F.FormContext (Form F.FieldState) (Form (F.FieldAction Action)) Input Action

-- Our form raises Formless actions for evaluation, most of which track the
-- state of a particular form field. We can specialize `F.FormlessAction` to our
-- form by giving it our form row applied to the `F.FieldState` type synonym.
type FormlessAction = F.FormlessAction (Form F.FieldState)

With our FormContext and FormlessAction types specialized, we can now implement our component's internal Action type:

data Action
  = Receive FormContext
  | Eval FormlessAction

The FormContext and FormlessAction types can be confusing the first time you see them. If they are a lot to take in, don't worry: you'll get used to them, and after you define them once you don't have to touch them again (any changs you make to your form will happen on the form row).

Our final component type is the State type. We don't need any extra state beyond what Formless gives us, so we'll just reuse the FormContext as our state type:

type State = FormContext

Implement your form component

We can now write our form component and make use of the state and helper functions that Formless makes available to us.

You will typically implement your form component by applying Formless directly to H.mkComponent, which saves quite a bit of typing. The Formless higher-order component takes three arguments:

  • A FormConfig, which lets you control some of Formless' behavior, like when validation should be run, and lets you lift Formless actions into your Action type. The only required option is liftAction; all other fields are entirely optional.
  • A record of initial values for each field in your form. We already wrote an initialValues when we defined our form type, but since all our inputs are strings, we could also implement our initial form as a simple mempty. This is what's demonstrated below.
  • Your form component, which must accept FormContext as input, handle queries of type FormQuery, and raise outputs of type FormOutput. Don't worry -- we'll talk more about each of these!
import Halogen as H
import Effect.Aff (Aff)
import Data.Maybe (Maybe(..))

form :: H.Component Query Input Output Aff
form = F.formless { liftAction: Eval } mempty $ H.mkComponent
  { initialState: \context -> context
  , render
  , eval: H.mkEval $ H.defaultEval
      { receive = Just <<< Receive
      , handleAction = handleAction
      , handleQuery = handleQuery
      }
  }

Rendering Your Form

The Formless form context provides you with the state of each field in your form, along with pre-made actions for handling change, blur, and other events. You can use this information to implement a basic form.

In the below example, we make use of a form-wide action (handleSubmit), field-specific actions (handleChange, handleBlur), and field-specific state (value, result).

form = F.formless ...
  where
  render :: FormContext -> H.ComponentHTML Action () Aff
  render { formActions, fields, actions } =
    HH.form
      [ HE.onSubmit formActions.handleSubmit ]
      [ HH.div_
          [ HH.label_
              [ HH.text "Name" ]
          , HH.input
              [ HP.type_ HP.InputText
              , HP.placeholder "Scooby"
              , HP.value fields.name.value
              , HE.onValueInput actions.name.handleChange
              , HE.onBlur actions.name.handleBlur
              ]
            -- We can use the `result` field to check if we have an error
          , case fields.name.result of
              Just (Left error) -> HH.text error
              _ -> HH.text ""
          ]
      ]

It's tedious and error-prone manually wiring up form fields, so most applications should define their own reusable form controls by abstracting what you see here. You can see examples of that in the examples directory.

Handling Actions

Every form component you provide to Formless should implement a handleAction function that updates your component when new form context is provided and tells Formless to evaluate form actions when they arise in your component. A typical handleAction function in a form component looks like this:

form = F.formless ...
  where
  -- Here we've written out the full type signature for `handleAction`, but the
  -- compiler can infer these types for you if you would like to omit the type
  -- signature or provide `_` wildcards for lengthy types like `F.FormOutput`.
  --
  -- Remember that our outer component has an output type of `Output`, but our
  -- inner component raises messages to Formless rather than to the form parent
  -- directly. We raise both our own output messages, `Output`, and also Formless
  -- actions that need to be evaluated. For that reason, we use the `F.FormOutput`
  -- output type for our inner component.
  handleAction
    :: Action
    -> H.HalogenM State Action () (F.FormOutput (Form F.FieldState) Output) Aff Unit
  handleAction = case _ of
    -- When we receive new form context we need to update our form state.
    Receive context ->
      H.put context

    -- When a `FormlessAction` has been triggered we must raise it up to
    -- Formless for evaluation. We can do this with `F.eval`.
    Eval action ->
      F.eval action

You can freely add your own actions to your form for anything else your form needs to do. See the examples for...examples!

Handling Queries

Formless uses queries to notify your form component of important events like when a form is submitted or reset, or when a form field needs to be validated.

Unlike previous versions of Formless, you don't provide any validation functions to the form directly. Instead, you will receive a Validate query that contains an input from your form. You are required to return an Either error output for that field back to Formless.

The most important benefit of this approach is that you can write validation functions that run in the context of your form component. That means that your validators can freely access your form state, including the state of other fields in the form, and you can evaluate actions in your component as part of validation (for example, making a request or setting the value of another field). We'll just explore pure validation in this example, but the examples directory demonstrates various validation scenarios.

A typical handleQuery function uses the handleSubmitValidate or handleSubmitValidateM helper functions to only deal with form submission and validation events. In our case, we'll simply raise a successful form submission as output, and we'll provide a set of pure validation functions:

form = F.formless ...
  where
  -- Here we'll use wildcards rather than type everything out; the compiler is
  -- able to infer these types for us.
  handleQuery :: forall a. F.FormQuery _ _ _ _ a -> H.HalogenM _ _ _ _ _ (Maybe a)
  handleQuery = do
    let
      -- These validators would usually be in a separate validation module in
      -- your app rather than be defined inline like this.
      validateName :: String -> Either String String
      validateName input
        | input == "" = Left "Required"
        | otherwise = Right input

      validateNickname :: String -> Either Void (Maybe String)
      validateNickname input
        | input == "" = Right Nothing
        | otherwise = Right (Just input)

      validateAge :: String -> Either String Int
      validateAge input = case Int.fromString input of
        Nothing -> Left "Not a valid integer."
        Just n
          | n > 20 -> Left "No dog is over 20 years old!"
          | n <= 0 -> Left "No dog is less than 0 years old!"
          | otherwise -> Right n

      validation :: { | Form F.FieldValidation }
      validation =
        { name: validateName
        , nickname: validateNickname
        , age: validateAge
        }

      handleSuccess :: Cat -> H.HalogenM _ _ _ _ _ Unit
      handleSuccess cat = do
        let
          output :: Output
          output = { successMessage: "Got a cat!", newCat: cat }

        -- F.raise is a helper function for raising your `Output` type through
        -- Formless and up to the parent component.
        F.raise output

    -- handleSubmitValidate lets you provide a success handler and a record
    -- of validation functions to handle submission and validation events.
    F.handleSubmitValidate handleSuccess F.validate validation

In a typical form, you wouldn't write out all these types, and your validation functions would probably live in a separate Validation module in your project. In the real world, a more typical handleQuery looks like this:

import MyApp.Validation as V

form = F.formless ...
  where
  handleQuery :: forall a. F.FormQuery _ _ _ _ a -> H.HalogenM _ _ _ _ _ (Maybe a)
  handleQuery = F.handleSubmitValidate F.raise F.validate
    { name: V.required
    , nickname: V.optional
    , age: V.int >=> V.greaterThan 0 >=> V.lessThan 20
    }

If you would like to see all possible events that your handleQuery function can handle, please see the implementation of handleSubmitValidate.

Comments & Improvements

Have any comments about the library or any ideas to improve it for your use case? Please file an issue, or reach out on the PureScript forum or PureScript chat.

purescript-halogen-formless's People

Contributors

chiroptical avatar crcornwell avatar dnikolovv avatar farzadbekran avatar justinwoo avatar linearray avatar ondrejslamecka avatar skress avatar th-awake avatar thomashoneyman avatar vyorkin avatar yaitskov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

purescript-halogen-formless's Issues

Consider removal of `submitter` function and `out` type parameter

Environment

  • purescript-halogen 0.4.0
  • purescript-halogen-formless 0.2.0

Current design

The Formless component has a lot of type parameters. Here's the query type:

data Query parentQuery childQuery childSlot userForm formOutput m a = ...

The first three types are all used so that you can embed whatever components you'd like into a form. The m parameter specifies the monad Formless runs in, which can be whatever you want, and there's the standard a for Halogen queries. Let's look at a simpler type synonym:

type Query' userForm formOutput m a = ...
  • userForm is the form type the user has specified and passed to Formless. It might be { password1, password2, email }, for example.
  • formOutput is the resulting type the form is meant to produce. It might be { email, password }, for example, collapsing the password fields.

Formless accepts a function, submitter, which is essentially a function userForm -> m formOutput. The entire point of this function is that it will turn the form type into the output type before returning that to the end user in a message:

data Message formOutput
  = Submitted formOutput
  ...

How things work right now

In the current design, the user must pass a submitter function as input, and Formless will apply it before returning the output in a message when the user submits a form.

This doesn't save you much work -- you had to write the function either way, and you could have just applied it when you received the Submitted message rather than it being applied before the message is sent. It's the different between (Submitted userForm) and (Submitted formOutput).

For this marginal benefit, Formless picks up an extra type parameter, formOutput. Worse, this type parameter can get confusing if, say, you're doing a computation that could produce an error (now it's Either Error FormOutput) or multiple types, depending (now it's Either Form1 Form2), or you still have to do a transformation afterwards.

That said, the nice thing about this type parameter is that it's quite informative -- it's essentially saying "This form will produce this type." It's a particularly nice parameter:

Formless.Query pq cq cs MyForm User m a
-- vs.
Formless.Query pq cq cs MyForm m a

Given that the type parameter adds almost no functionality to a component using it, but it does add some possibly-useful information at a glance, is it worth the extra complexity of carrying it around?

Suggested design

In short, remove both the submitter function and the extra type parameter out. Once Halogen v5.0.0 comes out the childQuery parameter can be dropped as well. The user will simply receive the output of the form they put in, which they can transform as they see fit.

README example has invalid syntax

Environment

  • purescript-halogen v5.0.0-rc.7
  • purescript-halogen-formless v1.0.0-rc.1

Current behavior

The example in the README uses a pun on a record update

spec :: forall m. Monad m => F.Spec' DogForm Dog m
spec = F.defaultSpec { render, handleEvent = F.raiseResult }

this isn't valid to avoid syntactic ambiguities with the update syntax

Expected behavior

Probably just this:

spec :: forall m. Monad m => F.Spec' DogForm Dog m
spec = F.defaultSpec { render = render, handleEvent = F.raiseResult }

Cannot create forms with file uploads: `File` does not have an `Eq` or `Initial` type class instance

Environment

  • purescript-halogen v5.0.0-rc.7
  • purescript-halogen-formless v1.0.0-rc.1

Current Behavior

I'm trying to use this library to upload a file in a form. Since <input type="file"> doesn't have a onValueInput or something like that, I'm using a separate action to get the resulting file via onFileUpload:

-- inside the render function
HH.input
        [ HP.type_ InputFile
        , HP.accept -- accept args
        , HP.value "Select File..."
        , HE.onFileUpload (Just <<< F.injAction <<< HandleFileUpload)
        ]

I can get the file fine...

  handleAction
    :: Action
    -> F.HalogenM Form AddedState Action ChildSlots Message m Unit
  handleAction = case _ of
    HandleFileUpload fileArray -> do
      case fileArray of
        [file] -> do
          evalA (F.setValidate _artifactFile file)
        _ -> pure unit

... but F.handleAction requires the field whose value I'm setting to have an Eq instance. Unfortunately, File does not have such an instance. I'm not sure whether it can have an Eq instance, but I at least implemented the following to get around that issue:

newtype FileEq = FileEq File
derive instance newtypeFileEq :: Newtype FileEq _
instance eqFileEq :: Eq FileEq where
  eq (FileEq left) (FileEq right) =
    (File.name left == File.name right) &&
    ((ceil $ File.size left) == (ceil $ File.size right))

  handleAction
    :: Action
    -> F.HalogenM Form AddedState Action ChildSlots Message m Unit
  handleAction = case _ of
    HandleFileUpload fileArray -> do
      case fileArray of
        [file] -> do
          evalA (F.setValidate _artifactFile $ FileEq file)
        _ -> pure unit

The next issue I run into is that File does not have a type class instance for Initial.

Workaround...?

I think this would work:

  • add a row in the Additional State rows for storing the file as a Maybe File
  • Update that part of the state record when user selects a new file and also set the form's validity to Valid
  • when user clicks the submit button, the file is included in the message raised to the parent.

Confusing type error when trying to define a query type for the inner component

So I'm trying to define a query (ResetForm a) for my inner component (which is supposed to reset some state and re-enable some buttons) like this:

type Slot = H.Slot Query Output Unit

type Form :: (Type -> Type -> Type -> Type) -> Row Type
type Form f =
  ( verificationCode :: f String String String
  --              input  error  output
  )

type FormInputs = { | Form F.FieldInput }

type VerificationData = { | Form F.FieldOutput }

-- | a query to reset the state so the verification code can be sent again
data Query a = ResetForm a

type Input = String

data Output = Verify NonEmptyString | ResendVerificationCode

type FormContext =
  F.FormContext
    (Form F.FieldState)
    (Form (F.FieldAction Action))
    Input
    Action

type FormlessAction = F.FormlessAction (Form F.FieldState)

data Action
  = Receive FormContext
  | Eval FormlessAction
  | Initialize
  | Tick
  | RaiseResendVerificationCode

type State =
  { formContext :: FormContext
  , validationError :: Maybe String
  , isLoading :: Boolean
  }

form
  :: forall m
   . MonadAff m
  => H.Component Query Input Output m
form = F.formless { liftAction: Eval, validateOnModify: true } mempty $
  H.mkComponent
    { initialState:
        \context ->
          { formContext: context
          , validationError: Nothing
          , isLoading: false
          }
    , render
    , eval: H.mkEval $ H.defaultEval
        { receive = Just <<< Receive
        , handleAction = handleAction
        , handleQuery = handleQuery
        , initialize = Just Initialize
        }
    }
  where
  render :: State -> H.ComponentHTML Action () m
  render
    { formContext: { formActions, fields, actions }
    , validationError: err
    , isLoading
    } =
    HH.form
      [ HE.onSubmit formActions.handleSubmit
      , HP.classes
          [ ClassName "columns"
          , ClassName "is-centered"
          ]
      ]
      [ HH.text "Actual form here..."
      ]

  handleAction
    :: Action
    -> H.HalogenM _ _ _ _ _ Unit
  handleAction = case _ of
    Receive context ->
      H.modify_ _ { formContext = context }

    Eval action ->
      F.eval action

    Initialize -> pure unit

    Tick -> pure unit

    RaiseResendVerificationCode -> pure unit

  handleQuery
    :: forall a
     . F.FormQuery _ _ _ _ a
    -> H.HalogenM _ _ _ _ _ (Maybe a)
  handleQuery =
    case _ of
      F.Query (ResetForm a) -> pure Nothing
      F.Validate i o -> pure Nothing
      F.Submit r a -> pure Nothing
      _ -> pure Nothing

this will compile with the usual F.handleSubmitValidateM handleSuccess F.validateM validation inside handleQuery but when I use the code above I get this error:

Error found:
in module App.Component.Forms.Verification
at src/Component/Forms/Verification.purs:344:8 - 344:63 (line 344, column 8 - line 344, column 63)

  No type class instance was found for
                              
    Prim.RowList.RowToList t1 
                           t11
                              
  The instance head contains unknown type variables. Consider adding a type annotation.

while solving type class constraint
                                                                                           
  Heterogeneous.Mapping.HMap MkFieldState                                                  
                             (Record t1)                                                   
                             { verificationCode :: { initialValue :: String                
                                                   , result :: Maybe (Either String String)
                                                   , value :: String                       
                                                   }                                       
                             }                                                             
                                                                                           
while applying a function formless
  of type MonadEffect t0 => MkFieldStates t1 t2 => MkFieldActions t2 t3 t4 => MkFieldResults t2 t5 => MkFieldOutputs t5 t6 => MkConfig t7                                                                     
                                                                                                                                { liftAction :: FormlessAction t2 -> t4                                       
                                                                                                                                , validateOnBlur :: Boolean                                                   
                                                                                                                                , validateOnChange :: Boolean                                                 
                                                                                                                                , validateOnModify :: Boolean                                                 
                                                                                                                                , validateOnMount :: Boolean                                                  
                                                                                                                                }                                                                             
                                                                                                                               => Record t7 -> Record t1 -> Component ... ... ... t0 -> Component t8 t9 t10 t0
  to argument { liftAction: Eval      
              , validateOnModify: true
              }                       
while inferring the type of formless { liftAction: Eval      
                                     , validateOnModify: true
                                     }                       
in value declaration form

where t7 is an unknown type
      t1 is an unknown type
      t2 is an unknown type
      t3 is an unknown type
      t5 is an unknown type
      t6 is an unknown type
      t8 is an unknown type
      t4 is an unknown type
      t9 is an unknown type
      t10 is an unknown type
      t0 is an unknown type
      t11 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md for more information,
or to contribute content related to this error.


[error] Failed to build.
[warn] ExitFailure 1
[info] Type help for available commands. Press enter to force a rebuild.

I assume I'm missing something? I don't understand the connection to my handleQuery as it's not even doing anything yet.
Thanks in advance.

Provide better ergonomics for nested forms / array of fields

Environment

  • purescript-halogen v5.0.0-rc.7
  • purescript-halogen-formless v1.0.0-rc.1

Current behavior

Formless does not provide a type that can be used in the row of fields to indicate that the row is a collection of fields. To workaround this limitation, one can use a nested form based on the example in this repo.

However, the above example is not as "real world" as it could be in the following ways:

  • the "Submit All" button is clickable regardless of whether a member form has been added or not. I had to write my own queries to deal with this issue.
  • The form's submission can contain an empty array of member data. That works for that example, but in my example, all entities fields are required. To implement that logic, I need to use Halogen queries that must return a Maybe (Array entity) rather than an Array entity.

Here's my use case. I have a form with a dynamic array of entities that need to be submitted. On one run, there might only be one entity. In another, it might be as many as 6. Each entity has two fields. The submit button should only be clickable when all of the entities' 2 fields are valid. When the submit button is clicked, it should raise a Array entity, not a Maybe (Array entity).

Expected behavior

The developer can use a custom Formless-provided type that indicates that a given row is a collection of fields.

type MyRows f =
 ( entities :: collection (f Error Input Output) )

render st =
 mapWithIndex st.entities \idx entity -> -- render code

I'm not sure how the above could be supported. While collection could be Identity in normal situations and Array/List/Maybe in other situations, the definition also allows a weird sort of case (e.g. Map someKey) that might not make sense.

Example Code

Note: I haven't actually run this code to verify whether it works as intended. I do know that it compiles.

module Form.Example where

import Prelude

import Data.Array (all, catMaybes, cons, elem, mapWithIndex)
import Data.Const (Const)
import Data.Either (Either(..))
import Data.Foldable (for_)
import Data.Int (fromString)
import Data.Lens as Lens
import Data.Lens.Index (ix)
import Data.List (toUnfoldable)
import Data.Map as M
import Data.Maybe (Maybe(..), isNothing, maybe)
import Data.Newtype (class Newtype)
import Data.String.NonEmpty.Internal (NonEmptyString)
import Data.String.NonEmpty.Internal as NonEmpty
import Data.Symbol (SProxy(..))
import Effect.Aff.Class (class MonadAff)
import Eportfolio.Component.HTML.Utils (whenElem)
import Formless (ValidStatus(..))
import Formless as F
import Halogen (RefLabel(..), liftEffect)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Web.HTML.HTMLSelectElement as SE
import Web.HTML.HTMLTextAreaElement as TA

-- Types used in form and page
type LikertScaleData = { score :: Int, meaning :: String }

type SubmissionInfo =
  { submissionID :: Int
  , label :: String
  , description :: String
  }

-- Parent component

type SubmissionData =
  { submissionID :: Int
  , score :: Int
  , comment :: NonEmptyString
  }

type ParentFormRow f =
  ( entities :: f Void (Array SubmissionData) (Array SubmissionData)
  )

type ParentFormFields = { | ParentFormRow F.OutputType }

newtype ParentForm r f = ParentForm (r (ParentFormRow f))
derive instance newtypeParentForm :: Newtype (ParentForm r f) _

-- Form component types

type ParentFormInput = { likertScale :: Array LikertScaleData, entities :: Array SubmissionInfo }
type ParentFormState = ( likertScale :: Array LikertScaleData, entities :: Array SubmissionInfo, entitiesValid :: Array ValidStatus )
type ParentChildSlots = ( entity :: ChildFormSlot Int )
type ParentFormSlot = H.Slot (F.Query' ParentForm) ParentFormFields
data ParentFormAction
  = SubmitParentForm
  | UpdateValidity Int ValidStatus

parentForm
  :: forall m
   . MonadAff m
  => F.Component ParentForm (Const Void) ParentChildSlots ParentFormInput ParentFormFields m
parentForm = F.component mkInput $ F.defaultSpec
  { render = render
  , handleAction = handleAction
  , handleEvent = handleEvent
  }
  where
  mkInput :: ParentFormInput -> F.Input ParentForm ParentFormState m
  mkInput { likertScale, entities } =
    { validators: ParentForm
        { entities: F.hoistFn_ identity
        }
    , initialInputs: Nothing -- when Nothing, will use `Initial` type class

    , entities
    , likertScale
    , entitiesValid: map (const Invalid) entities
    }

  _entity = SProxy :: SProxy "entity"
  _entities = SProxy :: SProxy "entities"

  render
    :: F.PublicState ParentForm ParentFormState
    -> F.ComponentHTML ParentForm ParentFormAction ParentChildSlots m
  render st =
    HH.form_
      [ HH.div_ $
        st.entities # mapWithIndex \idx entity ->
          HH.slot _entity idx childForm { likertScale: st.likertScale, entity } (Just <<< F.injAction <<< UpdateValidity idx)
      , HH.button
        [ if st.submitting || st.validity /= F.Valid
            then HP.disabled true
            else HE.onClick \_ -> Just $ F.injAction SubmitParentForm
        ]
        [ HH.text "Submit Reflections" ]
      ]

  handleEvent = F.raiseResult
  evalA act = F.handleAction handleAction handleEvent act

  handleAction :: ParentFormAction -> F.HalogenM _ _ _ _ _ m Unit
  handleAction = case _ of
    UpdateValidity idx entityValidity -> do
      state <- H.get
      let updatedValidEntities = Lens.set (ix idx) entityValidity state.entitiesValid
      let original = all (_ == Valid) state.entitiesValid
      let next = all (_ == Valid) updatedValidEntities
      when (original /= next) do
        let validity = if next then Valid else Invalid
        H.modify_ \s -> s { entitiesValid = updatedValidEntities, validity = validity }

    SubmitParentForm -> do
      st <- H.get
      res <- H.queryAll _entity $ F.injQuery $ H.request GetFields
      case catMaybes $ toUnfoldable $ M.values res of
        [] -> pure unit
        entities -> do
          evalA (F.set _entities entities) *> evalA F.submit

-----------------------------------------------------------------------------

type ChildFormRow f =
  ( score :: f String String Int
  , comment :: f String String NonEmptyString
  )

type ChildFormFields = { | ChildFormRow F.OutputType }

newtype ChildForm r f = ChildForm (r (ChildFormRow f))
derive instance newtypeChildForm :: Newtype (ChildForm r f) _

-- Form component types

type ChildFormInput = { likertScale :: Array LikertScaleData, entity :: SubmissionInfo }
type ChildFormState = ( likertScale :: Array LikertScaleData, entity :: SubmissionInfo )
type ChildFormSlot = H.Slot (F.Query ChildForm ChildFormQuery ()) ValidStatus
data ChildAction
  = UpdateTextArea
  | UpdateDropdown

data ChildFormQuery a
  = GetFields (Maybe SubmissionData -> a)
derive instance functorChildFormQuery :: Functor ChildFormQuery

childForm
  :: forall m
   . MonadAff m
  => F.Component ChildForm ChildFormQuery () ChildFormInput ValidStatus m
childForm = F.component mkInput $ F.defaultSpec
  { render = render
  , handleAction = handleAction
  , handleQuery = handleQuery
  }
  where
  mkInput :: ChildFormInput -> F.Input ChildForm ChildFormState m
  mkInput { likertScale, entity } =
    { validators: ChildForm
        { score: F.hoistFnE_ \str ->
          case fromString str of
            Nothing -> Left "Not an integer"
            Just i ->
              if i `elem` validScores
                then Right i
                else Left "invalid choice"

        , comment: F.hoistFnE_ $
            maybe (Left "field is required") Right <<< NonEmpty.fromString
        }
    , initialInputs: Nothing -- when Nothing, will use `Initial` type class

    -- everything else below comes from our `AddedState` rows:
    , likertScale
    , entity
    }
    where
      validScores = map _.score likertScale

  _score = SProxy :: SProxy "score"
  _comment = SProxy :: SProxy "comment"

  handleEvent = const $ pure unit
  evalA act = F.handleAction handleAction handleEvent act
  evalQ q = F.handleQuery handleQuery handleEvent q

  dropdownRef = RefLabel "dropdown"
  textAreaRef = RefLabel "textArea"

  handleQuery :: forall a. ChildFormQuery a -> H.HalogenM _ _ _ _ m (Maybe a)
  handleQuery = case _ of
    GetFields reply -> do
      subId <- H.gets _.entity.submissionID
      mbRecord <- map (produceRecord subId) $ evalQ $ H.request F.submitReply
      pure (Just (reply mbRecord))
    where
      produceRecord submissionID maybeContainer = do
        mbShell <- maybeContainer
        form <- mbShell
        let { score, comment } = F.unwrapOutputFields form
        pure { score, comment, submissionID }

  handleAction :: ChildAction -> H.HalogenM _ _ _ _ m Unit
  handleAction = case _ of
    UpdateDropdown -> do
      mbEl <- H.getHTMLElementRef dropdownRef
      for_ mbEl \el -> do
        for_ (SE.fromHTMLElement el) \selectEl -> do
          valueAsString <- liftEffect $ SE.value selectEl
          evalA (F.setValidate _score valueAsString)
          validity <- H.gets _.validity
          H.raise validity

    UpdateTextArea -> do
      mbEl <- H.getHTMLElementRef textAreaRef
      for_ mbEl \el -> do
        for_ (TA.fromHTMLElement el) \textArea -> do
          valueAsString <- liftEffect $ TA.value textArea
          evalA (F.setValidate _comment valueAsString)
          validity <- H.gets _.validity
          H.raise validity

  render
    :: F.PublicState ChildForm ChildFormState
    -> F.ComponentHTML ChildForm ChildAction () m
  render st =
    HH.div_
      [ HH.div_
        [ HH.span_
          [ HH.text st.entity.label ]
        , HH.span_
          [ HH.text $ ": " <> st.entity.description]
        ]
      , whenElem ((st.validity /= Valid) && (isNothing $ F.getOutput _score st.form)) \_ ->
          HH.div_ [ HH.text "You did not provide a valid score below."]
      , HH.select
        [ HP.ref dropdownRef
        , HE.onChange (\_ -> Just $ F.injAction UpdateDropdown)
        ]
        $ cons
          (HH.option
            [ HP.selected (maybe true (const false) $ F.getOutput _score st.form)
            ]
            [ HH.text "-- Select --"
            ])
        $ st.likertScale <#> \{ score, meaning} ->
          HH.option
            [ HP.value (show score)
            , HP.selected (maybe false (\i -> i == score) $ F.getOutput _score st.form)
            ]
            [ HH.text meaning ]
      , whenElem ((st.validity /= Valid) && (isNothing $ F.getOutput _comment st.form)) \_ ->
          HH.div_ [ HH.text "You did not provide a comment below."]
      , HH.textarea
        [ HP.ref textAreaRef
        , HP.placeholder "Please explain your above score"
        , HP.value (F.getInput _comment st.form)
        , HE.onChange (\_ -> Just $ F.injAction UpdateTextArea)
        ]
      ]

Cannot send queries through Formless if there are multiple child component types

Current behavior

Formless only supports sending queries down to child components so long as it only has a single child component type. It doesn't support child paths at all, so if you have multiple types of child components mounted in Formless (a common use case), they aren't accessible to the parent.

This is especially bad when you need to support resetting a form, because there is no way to clear the values of these external components. Formless will reset properly, but the components will still have old things selected.

data Query pq cq cs (form :: (Type -> Type -> Type -> Type) -> Type) out m a
  = ...
  | Send cs (cq Unit) a

eval = case _ of
    Send cs cq a -> do
      _ <- H.query cs cq
      pure a

To see this behavior in practice, attempt to send a query through to a child component in the /real-world example folder.

Expected behavior

Queries ought to be able to reach child components through Formless. Perhaps this means carrying around a child path in the Formless type, though this is certainly an ugly solution and I'm not sure how this would affect using the component if you only have a single child component type (or none at all).

Send requests to external components

If I'm not mistaken, right now it's not possible to send requests to external components.

I can send actions with:
H.query slot $ H.action $ F.send slot' (H.action myAction)

But I can't do the equivalent of:
H.query slot $ H.request myRequest

Migrate to Halogen Hooks version

... and potentially fix #61, #62, and #64 along the way.

I think the hook version of this library could be broken up into two parts: various hooks for fields and a hook for the form itself

A field hook would have the following things:

  • a validator
  • an optional debouncer (not all require this)
  • an optional default value (i.e. fix #61)
  • two variants: one for single-elements and another for multi-elements (i.e. fix #62) where one debouncer is used for each one (i.e. fix #64), but the same validation and default value would be used across all of them

I'm not sure what a form hook would have as I can't recall all the things this library provides.

Adding a formless component with other child components causes a type error.

Environment

  • purescript-halogen v0.4.0
  • purescript-halogen-formless v4.0.0

Current behavior

Adding a formless component to a Halogen Parent component causes a type error.

Minimal reproduction scenario:


import Prelude

import Data.Either.Nested (Either3, Either2)
import Data.Newtype (class Newtype)
import Data.Functor.Coproduct.Nested (Coproduct3, Coproduct2)
import Data.Maybe (Maybe(..))
import Data.Either (Either(..))
import Effect (Effect)
import Formless as F
import Halogen.Aff as HA
import Halogen as H
import Halogen.Component.ChildPath as CP
import Halogen.HTML.Events as HE
import Halogen.HTML as HH
import Halogen.VDom.Driver (runUI)
import Effect.Aff.Class (class MonadAff)
import Effect.Aff (Aff)

data Query a =
    NoOp a
  | Formless (F.Message' Form) a

type Input = Unit
type ChildQuery m = Coproduct3 HeaderQuery FooterQuery (F.Query' Form m)
type ChildSlot = Either3 Unit Unit Unit
type AppState = Unit


app  :: forall m
      . MonadAff m
     => H.Component HH.HTML Query Input Void m
app =
  H.parentComponent
    { initialState: const initialState
    , render: render
    , eval: eval
    , receiver: const Nothing
    }
  where
    initialState = unit

    render :: AppState -> H.ParentHTML Query (ChildQuery m ) ChildSlot m
    render state =
      HH.div_
      [ HH.slot' CP.cp1 unit header unit (const Nothing)
      , HH.slot' CP.cp2 unit footer unit (const Nothing)
      , HH.slot' CP.cp3 unit F.component
        { initialInputs: inputs, validators, render: renderFormless }
        (HE.input Formless)
      ]

    eval :: Query ~> H.ParentDSL AppState Query ChildQuery ChildSlot Void m
    eval action = case action of
      (NoOp next) -> pure next


data HeaderQuery a = QueryNoOp a

header :: forall m. H.Component HH.HTML HeaderQuery Unit Void m
header =
  H.component
  { initialState: const initialState
  , render: render
  , eval: eval
  , receiver: const Nothing
  }
  where
    initialState = unit
    render :: Unit -> H.ComponentHTML HeaderQuery
    render state =
      HH.header_ [HH.text "Header"]
    eval :: HeaderQuery ~> H.ComponentDSL Unit HeaderQuery Void m
    eval (QueryNoOp next) =
      pure next

data FooterQuery a = FooterNoOp a
footer :: forall m. H.Component HH.HTML FooterQuery Unit Void m
footer =
  H.component
  { initialState: const initialState
  , render: render
  , eval: eval
  , receiver: const Nothing
  }
  where
    initialState = unit
    render :: Unit -> H.ComponentHTML FooterQuery
    render state =
      HH.header_ [HH.text "Footer"]
    eval :: FooterQuery ~> H.ComponentDSL Unit FooterQuery Void m
    eval (FooterNoOp next) =
      pure next

data Error = AllOK

newtype Form r f = Form (r
  ( email   :: f Error String String
  ))

derive instance newtypeForm :: Newtype (Form r f) _

inputs :: Form Record F.InputField
inputs = F.wrapInputFields
  { email: "" }

allValid :: forall form m. Monad m => F.Validation form m Error String String
allValid = F.hoistFnE_ $ \str -> Right str

validators :: Form Record (F.Validation Form Aff)
validators = Form
  { email : allValid }


renderFormless :: forall m. F.State Form m -> F.HTML' Form m
renderFormless fstate =
  HH.div_ []

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  driver <- runUI app unit body
  pure driver

with an error:

  Could not match type
       
    Aff
       
  with type
      
    m0
      

while trying to match type Query (Const Void) (Const Void) Void Form Aff
  with type Query (Const Void) (Const Void) Void Form m0
while checking that expression div_ [ ((...) unit) (const Nothing)
                                    , ((...) unit) (const Nothing)
                                    , ((...) { initialInputs: ... 
                                             , validators: ...    
                                             , render: ...        
                                             }                    
                                      )                           
                                      (input Formless)            
                                    ]                             
  has type HTML (ComponentSlot HTML (Coproduct HeaderQuery (Coproduct FooterQuery (Coproduct (Query (Const Void) (Const Void) Void Form m0) (Const Void)))) m0 (Either Unit (Either Unit (Either Unit Void))) (Query Unit)) (Query Unit)
in value declaration app

where m0 is a rigid type variable
        bound at line 35, column 3 - line 56, column 31

If I drop the formless component from the Query algebra and remove it from the render method everything is fine.

Force validation on non dirty fields?

Hi, not sure if I missed something in the code (and sorry for the spam), but here's what I need to do:

I have an email field and a "send email" checkbox. The email field becomes mandatory when the the checkbox is checked.
The problem is that I want the "missing email" error to appear as soon as the user checks the "send email" field and I had to resort to this solution which feels a little hacky:

      eval (HandleFormless (F.Changed f) next) = next <$ do
         ...
         -- if the value of the check changed:
          let e = F.getInput prx.email f.form
          void $ H.query' CP.cp1 unit $ F.modifyValidate_ prx.email e

I can't use validate_, as nothing will happen if the email field was still untouched.

sendQuery documentation outdated and I need an example

The documentation for sendQuery is confusing:

First, you can use H.query as usual within the handleExtraQuery function you provide to Formless as input. 

Is this up-to-date? I find no function handleExtraQuery in formless. I'm also not sure about H.query.

Also, it would be awesome to have an example of how to use the feature. What I'm trying to do is send the formless component a query so some input fields are then reset (I don't want to do this in the submit button's onClick handler since I want to make a HTTP request first and check the result).

DogForm from README does not work with v1.0.0

Thanks for creating this library. I've found a problem when trying out the example from the README. It seems to have an incomplete type for spec (it's missing a parameter), and I'm unsure what I need to put there.

Environment

  • purs v0.13.5
  • purescript-halogen v5.0.0-rc.7
  • purescript-halogen-formless v1.0.0-rc.1

Current behavior

import Prelude

import Data.Either (Either(..))
import Data.Int (fromString)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Symbol (SProxy(..))
import Effect.Class.Console (logShow)
import Formless as F
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP

type Dog = { name :: String, age :: Age }

newtype Age = Age Int

data AgeError = TooLow | TooHigh | InvalidInt

newtype DogForm r f = DogForm (r
  --          error    input  output
  ( name :: f Void     String String
  , age  :: f AgeError String Age
  ))

derive instance newtypeDogForm :: Newtype (DogForm r f) _

data Action = HandleDogForm Dog

input :: forall m. Monad m => F.Input' DogForm m
input =
  { initialInputs: Nothing -- same as: Just (F.wrapInputFields { name: "", age: "" })
  , validators: DogForm
      { name: F.noValidation
      , age: F.hoistFnE_ \str -> case fromString str of
          Nothing -> Left InvalidInt
          Just n
            | n < 0 -> Left TooLow
            | n > 30 -> Left TooHigh
            | otherwise -> Right (Age n)
      }
  }

spec :: forall m. Monad m => F.Spec' DogForm Dog m
spec = F.defaultSpec { render = render, handleEvent = F.raiseResult }
  where
  render st@{ form } =
    HH.form_
      [ HH.input
          [ HP.value $ F.getInput _name form
          , HE.onValueInput $ Just <<< F.set _name
          ]
      , HH.input
          [ HP.value $ F.getInput _age form
          , HE.onValueInput $ Just <<< F.setValidate _age
          ]
      , HH.text case F.getError _age form of
          Nothing -> ""
          Just InvalidInt -> "Age must be an integer"
          Just TooLow -> "Age cannot be negative"
          Just TooHigh -> "No dog has lived past 30 before"
      , HH.button
          [ HE.onClick \_ -> Just F.submit ]
          [ HH.text "Submit" ]
      ]
    where
    _name = SProxy :: SProxy "name"
    _age = SProxy :: SProxy "age"


page = H.mkComponent
    { initialState: const unit
    , render: const render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
    where
    handleAction (HandleDogForm dog) = logShow (dog :: Dog)

    render = HH.slot F._formless unit (F.component spec) input handler
        where
        handler = Just <<< HandleDogForm

Results in the following error message:

  47  spec :: forall m. Monad m => F.Spec' DogForm Dog m
                                   ^^^^^^^
  
  Could not match kind
  
    Type
  
  with kind
  
    Type -> Type

Expected behavior

A working example :-)

Also, the README speaks of handleMessage should this be handleEvent?

Replace RowToList / Row.Cons type classes with purescript-heterogeneous

Environment

  • purescript-halogen 4.0.0
  • purescript-halogen-formless 0.2.0

Problem

There are a huge number of boilerplate RowToList classes used to produce various transformations on heterogeneous records. This involves lots of code duplication and non-obvious type class errors for end users, and it's easy to get the constraints wrong and only realize the error later (the class will compile, but not work as expected once put into use on a particular record).

For example, there are 4 variations on essentially this same class:

class InputFieldsToFormFields (xs :: RL.RowList) (row :: # Type) (to :: # Type) | xs -> to where
  inputFieldsToFormFieldsBuilder :: RLProxy xs -> Record row -> FromScratch to

instance inputFieldsToFormFieldsNil :: InputFieldsToFormFields RL.Nil row () where
  inputFieldsToFormFieldsBuilder _ _ = identity

instance inputFieldsToFormFieldsCons
  :: ( IsSymbol name
     , Row.Cons name (InputField e i o) trash row
     , InputFieldsToFormFields tail row from
     , Row1Cons name (FormField e i o) from to
     )
  => InputFieldsToFormFields (RL.Cons name (InputField e i o) tail) row to where
  inputFieldsToFormFieldsBuilder _ r =
    first <<< rest
    where
      _name = SProxy :: SProxy name
      val = transform $ Record.get _name r
      rest = inputFieldsToFormFieldsBuilder (RLProxy :: RLProxy tail) r
      first = Builder.insert _name val
      transform (InputField input) = FormField
        { input
        , touched: false
        , result: Nothing
        }

Solution

Fortunately, @natefaubion has released purescript-heterogeneous, which might provide a way to replace a few of these classes with more straightforward, less-boilerplatey implementations.

Missing purescript-generics-rep

Environment

  • "purescript-halogen": "^4.0.0",
  • "purescript-halogen-formless": "^0.4.0",

Current behavior

Installed purescript-halogen-formless with bower.
When building getting this error:

Error 1 of 2:

  in module Formless.Types.Component
  at bower_components/purescript-halogen-formless/src/Formless/Types/Component.purs line 7, column 1 - line 7, column 40

    Module Data.Generic.Rep was not found.
    Make sure the source file exists, and that it has been provided as an input to the compiler.


  See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
  or to contribute content related to this error.

Error 2 of 2:

  in module Formless.Types.Component
  at bower_components/purescript-halogen-formless/src/Formless/Types/Component.purs line 8, column 1 - line 8, column 43

    Module Data.Generic.Rep.Show was not found.
    Make sure the source file exists, and that it has been provided as an input to the compiler.


  See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
  or to contribute content related to this error.

After manually installing purescript-generics-rep the build succeeds.

Expected behavior

Maybe purescript-generic-rep dependency should be added of the purescript-halogen-formless ?

Gracefully handle components with async behaviour

Quick recap from the Slack conversation:

Right now the only way to handle inputs with async validation is to write some "external" logic, as there are two problems:

  • the validation is run every time the input field value changes
  • new validations are started even when the previous one didn't complete; there's no guarantee that they will complete in the same order in which they were launched

Just writing a random idea here, not sure if it's doable with the current library architecture:

Add the possibility to define "async fields", which are initialized by providing an optional debounce time (to avoid having the validation run at every input change).
These fields will carry a boolean value to keep track of whether the validation is undergoing, with a helper function for the user to extract this value (for example to show a loading gif or disable the field).

The form component could keep a record of optional debouncers that are initialized during the initialization of the component only for async fields. They would restart the timer on every input change, and when enough time would have passed without changes they would start the validation function.

I'm not sure whether it would be necessary to avoid multiple validations running at the same time, since with these changes this behaviour would become more explicit to the user.

As a bonus, I think this would make writing stuff like typeahead inputs trivial.

Small bug in basic example

The basic example seems to miss binding the handling function for formless.

Current code:
https://github.com/thomashoneyman/purescript-halogen-formless/blob/v0.2.0/example/basic/Component.purs#L43-L49

HH.slot unit F.component
    { inputs
    , validators
    , submitter: pure <<< F.unwrapOutputFields
    , render: renderFormless
    }
    (const Nothing)

Fix:

HH.slot unit F.component
    { inputs
    , validators
    , submitter: pure <<< F.unwrapOutputFields
    , render: renderFormless
    }
    (HE.input HandleFormless)

Add `ModifyAll` and `ModifyValidateAll`?

At the moment there's a ValidateAll, but the same is not possible with Modify and ModifyValidate.

Use case:
I have a form that gets populated with some initial values when the user clicks on a product.
Right now I have to manually call modifyValidate_ for each form field in the parent component's HandleInput action.

It would be useful to be able to do something like

H.query unit $ F.modifyValidateAll_ { foo: 1, bar: "X", baz: false }

Not sure if it's better this way, or to have a modifyValidateMultiple, where one can just pass a subrecord instead of the whole one.

Extract the bits independent of a UI framework?

While porting the library to the Concur UI Framework, I realised that most of the important bits are completely independent of Halogen. So I went ahead and extracted formless completely and created Formless-independent.

I think it might be valuable to have an official package called formless-core or formless-independent for this, and then having framework specific bits in their own packages such as formless-halogen or formless-concur, which would in turn depend on the core library.

What do you think?

Check if the form is dirty

Hi!
I've recently updated formless to v4, but I've noticed that there isn't a dirty property anymore.

Is there a recommended way to implement this?

Add a component template file/directory?

Similar to what I did with ComponentTemplate.purs, would you be up for me adding something like that here? Or should I perhaps store that in my learning repo?

Due to how much stuff goes on in creating a form, I think this should be a directory with these files:

  • data and validation: defines a single type in the form, its instances, and its validation code
  • component - commented: a Formless component template file that explains everything (useful for new learners)
  • component - uncommented: a Formless component template file that explains nothing and is used to create the boilerplate to make the thing work (useful for developers)

Raised actions from external components are not reaching the parent

I'm not sure if I'm doing something wrong, but I can't manage to thread messages from external components to the parent. I get no errors, but just silent fails so I'm having a hard time figuring out what's wrong.
I'm using version 0.2.0

Edit to add that if I build the examples they work correctly. There has to be something that I'm failing to see here (but that should give me an error :( )

I've reduced my code to a pretty simple case:

data Query a 
  = HandleFormless (F.Message Query Form FormState) a
  | Typeahead TAMessage a

data TAQuery a = HandleSelect a

data TAMessage = SelectionsChanged

type ChildQuery = F.Query Query TAQuery Unit Form FormState Aff
type ChildSlot = Unit

component :: H.Component HH.HTML Query Input Void Aff
component =
  H.parentComponent
    { initialState: identity
    , render
    , eval
    , receiver: const Nothing
    }

   render :: State -> H.ParentHTML Query ChildQuery ChildSlot Aff
   render s = HH.div_ 
                    [ HH.slot unit F.Component { inputs, validators, submitter, render } 
                      (HE.input HandleFormless) 
                    ]

  eval :: Query ~> H.ParentDSL State Query ChildQuery ChildSlot Void Aff
  eval (Typeahead SelectionsChanged next) = next <$
    logShow "msg" -- <--- this is never called.

inputs ...
validators ...
submitter ...

render :: F.State Form FormState Aff -> F.HTML Query TAQuery Unit Form FormState Aff
render state = HH.div_ [ HH.slot unit tacomp unit 
                                    (HE.input $ F.Raise <<< H.action <<< Typeahead) 
                                  ]

tacomp :: H.Component HH.HTML TAQuery Unit TAMessage Aff
tacomp = 
  H.component
    { initialState: identity
    , render
    , eval
    , receiver: const Nothing
    }

    where
      render :: Unit -> H.ComponentHTML TAQuery
      render st = HH.button [ HE.onClick $ HE.input_ HandleSelect
                            , HP.type_ HP.ButtonButton
                            ] [ HH.text "CLICK" ]

      eval :: TAQuery ~> H.ComponentDSL Unit TAQuery TAMessage Aff
      eval (HandleSelect next) = next <$ do
        logShow "Check" -- <-- gets logged
        H.raise $ SelectionsChanged

Multiple async form fields share one debouncer, causing interference

Environment

  • purescript-halogen v5
  • purescript-halogen-formless v0.5.2

As described by @skress in #48:

When a debouncer is used for one field and then for another field the running validation for the first field might get killed in atomic in Internal.Debounce:

  atomic
    :: forall n
     . MonadAff n
    => HalogenM form st act ps msg n (form Record FormField)
    -> Maybe (HalogenM form st act ps msg n a)
    -> HalogenM form st act ps msg n Unit
  atomic process maybeLast = do
    state <- H.get
    let ref = (unwrap state.internal).validationRef
    mbRef <- readRef ref
    for_ mbRef H.kill
    H.liftEffect $ for_ ref $ Ref.write Nothing
    forkId <- H.fork do
      form <- process
      H.modify_ _ { form = form }
      H.liftEffect $ for_ ref $ Ref.write Nothing
      for_ maybeLast identity
    H.liftEffect $ for_ ref $ Ref.write (Just forkId)

Not sure what the best approach for a fix would be. Obviously there could be a debouncer per form field or only the forked validation (in atomic) could be run per form field.

handleChange doesn't do validation

Hi,

Checkbox doesn't have blur event and I want to do validation after every change anyway.
fields.startTs.value is updated on check/uncheck but fields.startTs.result keeps error for old value.

How to set value and do validation in one action?

Action doesn't have Semigroup nor Monad.

            , H.div_
                [ H.input
                    [ H.type_ H.InputCheckbox
                    , H.checked $ latestTs == fields.startTs.value
                    , if latestTs == fields.startTs.value
                      then H.onChange (\_ -> actions.startTs.handleChange "")
                      else H.onChange (\_ -> actions.startTs.handleChange latestTs)
                    ]
                , H.label_
                    [ H.text $ "TS of last row + interval hours: [" <>
                        fields.startTs.value <> "]"
                    ]
                , H.div_ $
                    if fields.startTs.value /= latestTs
                    then
                      [
                        H.input
                          [ H.type_ H.InputText
                          , H.value fields.startTs.value
                          , H.onValueInput actions.startTs.handleChange
                          , H.onBlur actions.startTs.handleBlur
                          ]
                      ]
                    else []
                , case fields.startTs.result of
                    Just (Left e) -> H.div_ [ H.text e ]
                    _ -> H.etext

Extra button triggering validation works

H.input
                [ H.type_ H.InputButton
                , H.value "Validate"
                , H.onClick \_ -> actions.startTs.validate
                ]

need to have both:

H.onChange (\_ -> actions.startTs.handleChange latestTs >> actions.startTs.validate)

How to optionally combine validators

Hi! Sorry for spamming your issues :D

I'll explain this use case, hoping that there's a better solution than the one I'm using.

I have an optional phone field and I already have a validPhone validator that runs a regex to check that the number is in the correct format.
Since the field is optional, I only want to run it when the field is not empty.

Out of stubborness I didn't want to write a validPhone_, so what I did is:

emptyStr = hoistFnE_ $ \str -> 
  if null str 
  then Right str 
  else Left Missing
```

and then in the validators:
```
phone: emptyStr <|> validPhone
```

This works, but I don't like this solution at all as I'm using a fake error that's never going to be used in `emptyStr`, and there's a chance that the validator is used improperly somewhere else.

Is there a better way to achieve this?
Thanks!

Add a helper function to submit a form and call preventDefault on the event

I'm pretty happy with formless so far! One thing is really strange and is bugging me though: I'm submitting my form using F.submit, which validates and then submits. However, my cypress tests fail, because cypress actually sees the form submit and the new URL that's being broadcast and navigates to that new URL, see here:

image

This prevents my tests from passing, since a whole new page is loaded. Judging from this YouTube video from the cypress authors, to prevent this behavior, I'd have to call event.preventDefault() and then make my XHR request.

Is there a way to do this (easily?) in formless?

Improve error messages for RowToList type classes

Environment

  • PureScript 0.12
  • Halogen 4.0.0
  • purescript-halogen-formless 0.1.0

Current behavior

When Formless isn't able to solve one of its various RowToList classes, an extraordinarily cryptic error results (noted by @Unisay)

  No type class instance was found for
                             
    Prim.RowList.RowToList t4
                           t5
                             
  The instance head contains unknown type variables. Consider adding a type annotation.

while checking that type forall pq cq cs form out m spec specxs field fieldxs output countxs count inputs inputsxs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
                           Ord cs => Monad m => RowToList spec specxs => RowToList field fieldxs => RowToList count countxs => RowToList inputs inputsxs => EqRecord inputsxs inputs => FormSpecToInputField specxs spec field => InputFieldsToInput fieldxs field inputs => SetInputFieldsTouched fieldxs field field => InputFieldToMaybeOutput fieldxs field output => CountErrors fieldxs field count => AllTouched fieldxs field => SumRecord countxs count (Additive Int) => Newtype (form FormSpec) { | spec } => Newtype (form InputField) { | field } => Newtype (form OutputField) { | output } => Newtype (form Input) { | inputs } => Component HTML (Query pq cq cs form out m)                                                                          
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    { validator :: form InputField -> m (form InputField)                                                             
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    , submitter :: form OutputField -> m out                                                                          
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    , formSpec :: form FormSpec                                                                                       
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    , render :: { validity :: ValidStatus                                                                             
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                , dirty :: Boolean                                                                                    
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                , submitting :: Boolean                                                                               
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                , errors :: Int                                                                                       
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                , submitAttempts :: Int                                                                               
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                , form :: form InputField                                                                             
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                , internal :: InternalState form out m                                                                
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                }                                                                                                     
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                -> HTML (ComponentSlot HTML cq m cs (Query pq cq cs form out m Unit)) (Query pq cq cs form out m Unit)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    }                                                                                                                 
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    (Message pq form out)                                                                                             
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    m                                                                                                                 
  is at least as general as type Component HTML t0 t1 t2 t3
while checking that expression component
  has type Component HTML t0 t1 t2 t3
in value declaration render

where t0 is an unknown type
      t2 is an unknown type
      t1 is an unknown type
      t3 is an unknown type
      t5 is an unknown type
      t4 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/NoInstanceFound.md for more information,
or to contribute content related to this error.

Users of the library have essentially no information to work from as to how to solve this error. There are some methods which are useful:

  • Try to eliminate as many polymorphic variables as you can (test out making everything concrete)
  • Make sure all the individual inputs to Formless compile (submitter, validator, etc.)
  • Use typed holes on parts of the input to see if you can narrow the error to a particular input

But the error message says nothing about this, nor is it obvious to users that they ought to do this.

Expected behavior

RowToList classes ought to have more informative error messages. @Unisay recommended custom type errors as written about here:

https://github.com/paf31/24-days-of-purescript-2016/blob/master/21.markdown

Why not apply these liberally to the type classes in order to help narrow down what to do about these errors?

Problem getting the code from current README.md to compile

Hi!
I put the code from the readme into a file and tried to compile it, but I'm getting a type error with type signatures for the handleAction and handleQuery functions. I've put the code here if you want to test it.

They work if I change the signatures to H.HalogenM _ _ _ _ _ Unit and H.HalogenM _ _ _ _ _ (Maybe a), but it would be nice to actually see the types.

Skip validation

It would be useful to be able to skip the validation and not having to write a record of H.hoistFn_ identity.

I guess the best approach whould be to change validators to a Maybe, but for now I'm using this approach with heterogeneous:

data EmptyValidators = EmptyValidators

instance emptyValidators ::
  (Monad m) =>
  Mapping EmptyValidators a (F.Validation form m e i i) where
  mapping EmptyValidators = const (F.hoistFn_ identity)

noValidation
  :: forall form fields m vs xs
   . Monad m
  => RL.RowToList fields xs
  => Newtype (form Record (F.Validation form m)) { | vs }
  => Newtype (form Record F.InputField) { | fields }
  => MapRecordWithIndex xs (ConstMapping EmptyValidators) fields vs
  => form Record F.InputField
  -> form Record (F.Validation form m)
noValidation = wrap <<< hmap EmptyValidators <<< unwrap

and then

validators :: Form Record (F.Validation Form Aff)
validators = noValidation inputs

Add a `getInputs` function

I'm listening to Changed messages from a form that's essentially a set of filters for a table. I want to update my filter section of the state, but right now there's no "easy" way to grab all the inputs values at once.

I came up with this solution using heterogeneous:

extractResultFromChanged
  :: forall xs a form fields r
   . RL.RowToList fields xs
  => UnwrapRecord xs fields a
  => Newtype (form Record F.FormField) { | fields }
  => { form :: form Record F.FormField | r }
  -> { | a }
extractResultFromChanged = F.unwrapRecord <<< unwrap <<< _.form

newtype GetProp (prop :: Symbol) = GetProp (SProxy prop)

instance getProp ::
  (IsSymbol prop, Row.Cons prop a rx r) =>
  Mapping (GetProp prop) { | r } a where
  mapping (GetProp prop) = Record.get prop

getInputs' :: forall rin rout.
  HMap (GetProp "input") { | rin } { | rout } =>
  { | rin } ->
  { | rout }
getInputs' = hmap (GetProp (SProxy :: SProxy "input")) 

getInputs
   :: forall a xs form fields rout r ys
    . Newtype (form Record F.FormField) { | fields }
   => RL.RowToList fields xs
   => UnwrapRecord xs fields a
   => RL.RowToList a ys
   => HMap (GetProp "input") { | a } { | rout }
   => MapRecordWithIndex ys (ConstMapping (GetProp "input")) a rout
   => { form :: form Record F.FormField | r }
   -> { | rout }
getInputs = getInputs' <<< extractResultFromChanged

I just noticed that this is not consistent with getInput which takes the form as argument (here I'm extracting it from the message's payload.

mkSProxies generates a LOT of boilerplate code

Environment

  • purescript-halogen 6.1.0
  • purescript-halogen-formless 2.0.1

Hi!
While doing some profiling for one of my purescript webapps, I found out that one single formless component was generating a 180kb js file on its own. A ~300LOC ps component was generating almost 3k LOC of javascript code.

I found out that the main culprit was mkSProxies. Every time one of the proxies is used, a huge block of code is generated.
For example by just using prx.maxUsages the js code would result in:

new Data_Symbol.IsSymbol(function () {
            return "maxUsages";
        }))()()((Components_Coupon_Types.prx()()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "active";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "code";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "description";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "individualUse";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "isAuto";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "maxUsages";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "minAmount";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "minQty";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "platforms";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "productsIn";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "productsOut";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "regionsIn";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "regionsOut";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "travelFrom";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "travelTo";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "type";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "typesIn";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "typesOut";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "validFrom";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "validTo";
        }))()(Formless_Transform_Row.makeSProxiesCons(new Data_Symbol.IsSymbol(function () {
            return "value";
        }))()(Formless_Transform_Row.makeSProxiesNil))))))))))))))))))))))).maxUsages

By replacing mkSproxies with a manually written record of proxies the file size went down from ~180K to ~90K.

Similar things happen in other places (for example when defining render, handleAction and handleEvent) but I guess that's a consequence of how this library was designed and are probably not as easily solvable as this one.

For now I'm replacing all of mkSproxies usages, but I was wondering if I'm doing anything wrong on my end.

Thanks!

No type class instance for RowToList t4 t5 error with explicit exports

Quick Summary

If you use explicit exports when defining a form, you must ensure that any types used within the Form newtype are also exported, or else you'll receive the dreaded No type class instance for RowToList t4 t5 error message.

It's not the only way to receive that error message, but if you receive the error, double-check explicit exports.

Given a form type like this:

data MyCustomError = MyCustomError

newtype Form f = Form { email :: f (Array MyCustomError) String String }
derive instance newtypeForm :: Newtype (Form f) _

then, if you are using explicit exports, ensure that you also export any types used within the newtype along with the newtype itself. For example, while this will not show any errors in this module, this is incorrect:

module MyModule
  ( Form
  ) where

Instead, ensure that you export the associated types, too:

module MyModule
  ( Form
  , MyCustomError(..)
  ) where

Environment

  • purescript-halogen-formless [0.1.1]

Current behavior

If you write a Form newtype that relies on custom types written in the same module, and then you explicitly export the Form newtype without also exporting the custom types, you don't get a warning or an error until you go to actually use it with the Formless component. At that moment, you'll receive the error seen in #15 (No type class instance for Prim.Row.RowToList t4 t5 ...) with essentially no information to go off of for debugging purposes beyond that.

The problem appears to be that the compiler can't find the correct instance for RowToList because it can't find the custom type that was never exported.

For example, this form file (provided by @Unisay) will compile just fine:

module EmailForm
  ( formInput
  , Form
  ) where

import Prelude
import Data.Bifunctor (lmap)
import Data.Newtype (class Newtype)
import Data.Symbol (SProxy(..))
import Formless as F
import Formless.Validation.Polyform (applyOnInputFields)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Polyform.Validation as V

type Errors = Array ValidationError
data ValidationError = InvalidEmail String

newtype Email = Email String

newtype Form f = Form { email :: f Errors String Email }
derive instance newtypeForm :: Newtype (Form f) _

formInput ::  m. Monad m => F.Input' Form Email m
formInput = { formSpec, validator, submitter, render }

formSpec :: Form F.FormSpec
formSpec = F.mkFormSpec { email: "" }

validator ::  m. Monad m => Form F.InputField -> m (Form F.InputField)
validator = applyOnInputFields $ identity { email: validateEmail }

validateEmail ::  m. Monad m => V.Validation m Errors String Email
validateEmail = V.hoistFnV $ mkEmail
  >>> lmap (InvalidEmail >>> pure)
  >>> V.fromEither

submitter ::  m. Monad m => Form F.OutputField -> m Email
submitter = F.unwrapOutput >>> _.email >>> pure

render ::  m. F.State Form Email m -> F.HTML' Form Email m
render fstate = do
  let _email = SProxy :: SProxy "email"
  HH.div_
    [ HH.label_ [HH.text "Email"]
    , HH.p_
        [ HH.input
            [ HP.value $ F.getInput _email fstate.form
            , HE.onValueInput $ HE.input $ F.ModifyValidate <<< F.setInput _email
            ]
        ]
    ]

However, as soon as it's used in a minimal component like this, the compiler spits out the RowToList error:

module Component
  ( component
  , Query(..)
  ) where

import Prelude

import EmailForm as EmailForm
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Formless as F
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

data Query a = Formless (F.Message' Email.Form Email) a
type ChildQuery = F.Query' EmailForm.Form Email Aff

component :: H.Component T.HTML Query Unit Void Aff
component = H.parentComponent
  { initialState: const unit
  , render: const render
  , eval: eval
  , receiver: const Nothing
  }

-- Error thrown here
render :: H.ParentHTML Query ChildQuery Unit Aff
render = HH.slot unit F.component Email.form (HE.input Formless)

eval :: Query ~> H.ParentDSL Unit Query ChildQuery Unit Void Aff
eval (Formless _ next) = pure next

The solution is to also export the custom error type defined in the module.

Expected behavior

  1. Ideally, the compiler ought to warn when exporting the Form type without any of the types used inside of it, if those types are not exported as well. For example, if the only place ValidationError is ever defined is the same module as Form, and it is used in the row inside Form, then exporting Form should also require exporting those other types even if you're exporting Form without its constructors. This is related to purescript/purescript#3394

  2. If (1) above is not a good solution, there isn't available compiler support, or there's some other reason it can't be the solution, then at least #15 should be implemented to provide a better message for this error. In fact, because that error can arise in a few specific scenarios, there ought to be a piece of documentation specifically about this error that the error message links to.

In the meantime, this issue will help others solve the problem if it arises in their project.

asyncSetValidate giving partial output

Environment

  • purescript-halogen v5.0.0-rc.4
  • purescript-halogen-formless halogen-5 (branch)

Current behavior

When I use asyncSetValidate with a debounce time the output from validation seems to chop off a bit of the end depending on how fast I type. Entering something like [email protected] quite fast gives -> john@jo etc.

This could of course simply be an issue with my code but it works well if I have no debounce time. Could it be that if I have debounceTime 300.0 for example and then validation kicks off after that time, the continuing input is disregarded until the validation function is finished?.
Because in my case it seems that I type something, and it valdiates a part of it and then it doesn't validate it again unless I trigger it again by writing one extra letter, then it validates the whole thing.

(Screenshot where debounceTime = 0.0 Milliseconds)
Screen Shot 2019-05-26 at 00 54 19

(Screenshot where debounceTime = 500.0 Milliseconds)
Screen Shot 2019-05-26 at 00 56 32

Code being used

(spec)

      , HH.input
          [ HP.value $ F.getInput _email form
          , HE.onValueInput $ Just <<< F.asyncSetValidate debounceTime _email
          ]
  
    where
    _email = Sproxy :: Sproxy "email"
    debounceTime = Milliseconds 500.0

(validation)

checkIsInUse :: forall form m r
              . MonadAff m 
             => MonadAsk { logLevel :: Environment, baseURL :: BaseURL, apiURL :: BaseURL | r } m
             => F.Validation form m EmailError String Email
checkIsInUse = F.hoistFnME_ $ \email -> do
  apiUrl <- asks _.apiURL
  let auth = Nothing
  let endpoint = "/users/exists/" <> email
  let opts = { endpoint: endpoint, method: Get } :: RequestOptions
  res <- liftAff $ AX.request $ defaultRequest apiUrl auth opts
  case res.body of 
    Left err -> do
      pure $ Left Error
    Right json -> do
      case (J.stringify json) of
        "true"  -> pure $ Left Exists
        "false" -> pure $ Right $ Email email
        _       -> pure $ Left Error

Expected behavior

The asyncSetValidate should validate all Input

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.