I’ll describe approaches I’ve used that I’ve been happy with.
For records that are persisted in a database and have an arbitrary key, instead of storing that key in the model of the record, I would keep a Dict of those records indexed by the key. So instead of storing something like
records = List { id : Int, name : String }
I would instead store
records = Dict Int { name : String }
where the Int is the arbitrary key.
This gives a consistent model for valid records, whether they’re persisted or not.
Let’s say the type for a valid record is Record; I would have a separate type for unvalidated forms, RecordForm. You can convert between the two as needed:
recordToForm : Record -> RecordForm
formToRecord : RecordForm -> Result RecordFormErrors Record
Like you said, the advantage is that the RecordForm only has to deal with types that relate to user inputs. It also makes it more natural to define an emptyForm value without resorting to definitions like id = -1.
This approach to validation has been mentioned before and there is at least one library that promotes the approach.
At the top-level model, I tend to store server data separately from any other state. This makes it simple to reload server data, as it can safely supersede existing server data without needing to resolve conflicts with user-entered data or any other state.
type alias Model =
{ serverData :
{ records : Dict RecordId (Remote Record)
, ..
}
, clientData :
{ editedRecords : Dict RecordId RecordForm
, newRecord : Maybe RecordForm
, ..
}
}
It sounds like you’re on the right track! I hope this helps.