Help modeling an editable table

Hey folks, I’m modeling a high-level, project-specific editable table view function, and I’m facing a type challenge when modeling the columns input types:

type alias Column record msg comparable selectType =
    { header : String
    , sortable : Maybe (record -> comparable)
    , content : CellContent record selectType
    }


type CellContent record selectType
    = Text { toLabel : record -> String }
    | Select
        { options : List selectType
        , selected : Maybe selectType
        , toLabel : selectType -> String
        }


editableTable :
    { records : Array record
    , columns : List (Column record msg comparable selectType)
    , canReorder : Bool
    , state : Model
    }
    -> Html msg
editableTable config =
    Debug.todo "editableTable"

The problem I’m facing is in CellContent > Select > options

Notice how it’s introducing a type variable. There are multiple problems with this type variable. If the user of this table needs to have multiple Select columns, they’ll all need to be of the same type. If the user of this table does not need a Select type of a column, then this type variable will be an unnecessary extra in the table type.

My goal is to abstract as high as possible, and as delightful as possible.

Help?

: )

Maybe you need to give up some flexibility in the sub type Select. I assume you would render it using Html.select and Html.option. In that case you actually need selectType to be exactly String, right?

If you have in your model some other type let’s say a type, it’s easy to transform:

type CellContent record
    = Text { toLabel: record -> String }
    | Select
        { options: List String
        , selected: Maybe String
        }

type alias GeneralSelector =
    { options: List a
    , selected:Maybe a
    , toLabel: a -> String
    } 

toSelectColumn : GeneralSelector -> CellContent record
toSelectColumn d =
    Select
        { options = List.map d.toLabel d.options
        , selected = Maybe.map d.toLabel
        }

That way you can have list of select columns that are based on different types in your model, but type-unified using the above function toSelectColumn.

HTH

Hi there,

I think I would go for more variants of the CellContent type, each of which would represent a different selectable:

type alias Person =
    { firstName : String
    , lastName : String
    }

type alias Column record comparable =
    { header : String
    , sortable : Maybe (record -> comparable)
    , content : CellContent record
    }

type alias Selector type_ =
    { options : List type_
    , selected : Maybe type_
    , toLabel : type_ -> String
    , toValue : type_ -> String
    }

type CellContent record
    = Text { toLabel : record -> String }
    | StringSelect (Selector String)
    | IntSelect (Selector Int)
    | PersonSelect (Selector Person)

select : Selector type_ -> Html msg
select selector =
    let
        buildOption element =
            Html.option
               [ Html.Attributes.value (selector.toValue element)
               , Html.Attributes.selected (selector.selected == Just element)
               ]
               [ Html.text (selector.toLabel element)
               ]

    in
    Html.select [] (List.map buildOption selector.options)


editableTable :
    { records : Array record
    , columns : List (Column record comparable)
    , canReorder : Bool
    , state : Model
    }
    -> Html msg
editableTable config =
    Debug.todo "editableTable"

I think this addresses both of the issues you mentioned.

Hope it helps.

Fellas, I appreciate your suggestions very much, but my teammate managed to come up with a solution that keeps the abstraction boundary exactly where we want it.

To reiterate the goal: the abstraction needs to take responsibility of drawing out all possible inputs, but not consider their domain-specific options. This gives us the perfect balance between flexibility and encapsulation.

So while it would’ve been ideal if we could utilize the explicit nature of custom types to satisfy this requirement, the type variables are on the way.

Therefore, the way we’re solving this is by avoiding the use of custom type to list all the options (which still breaks my heart), and relying on functions:

cellContentSelectBox: 
	{ options: List option
	, selected: record -> Maybe option
	, setOption: option -> record -> record
	, toLabel: option -> String
	}
	-> CellContent record

Where CellContent record is an opaque type that glues functions like this to the abstraction. This way, the option type variable stays specific to this function, and never touches the rest of the abstraction, which remains concerned only with the record type variable.

To finish the refactor, we’ll be removing the exposed CellContent record selectType, and we’ll be instead exposing a set of functions for each input type that we wanna support with the type of:

{ ..some configuration.. } -> CellContent record

And thus, the column configuration will look like so:

type alias Column record comparable =
    { header : String
    , sortable : Maybe (record -> comparable)
    , content : CellContent record
    }

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.