Il est souvent affirmé que les développeurs Elm devraient éviter de considérer leurs vues comme des composants étatiques. Bien que cela soit effectivement une pratique de conception générale recommandée, parfois vous pouvez vouloir rendre vos vues réutilisables (par exemple, sur différentes pages ou projets), et si elles sont fournies avec un état… vous finissez par copier-coller beaucoup de choses.
Nous avons récemment publié elm-daterange-picker, un sélecteur de plage de dates écrit en Elm. C’était l’occasion parfaite d’investiguer à quoi ressemblerait une API raisonnable pour un composant de vue étatique réutilisable.
De nombreux packages Elm orientés composants/widgets présentent une API Architecture Elm (TEA) plutôt brute, exposant directement Model, Msg(..), init, update et view, vous permettant essentiellement d’importer ce qui définit une application réelle et de l’intégrer dans votre propre application.
Avec ceux-ci, vous finissez généralement par écrire des choses comme ceci :
import Countertype alias Model = { counter : Counter.Model , value : Maybe Int }type Msg = CounterMsg Counter.Msginit : () -> ( Model, Cmd Msg )init _ = ( { counter = Counter.init, value = Nothing } , Cmd.none )update : Msg -> Model -> ( Model, Cmd Msg )update msg model = case msg of CounterMsg counterMsg -> let ( newCounterModel, newCounterCommands ) = Counter.update counterMsg in ( { model | counter = newCounterModel , value = case counterMsg of Counter.Apply value -> Just value _ -> Nothing } , newCommands |> Cmd.map CounterMsg )view : Model -> Html Msgview model = div [] [ Counter.view model.counter |> Html.map CounterMsg , text (String.fromInt model.value) ]
Cela fonctionne certainement, mais soyons francs pendant une minute et admettons que c’est super verbeux et pas très convivial pour les développeurs :
Vous devez utiliser Cmd.map et Html.map ici et là
Vous devez faire correspondre des Msg de Counter pour intercepter tout événement qui vous intéresse…
… ce qui signifie que Counter expose tous les Msgs, qui sont des détails d’implémentation sur lesquels vous comptez maintenant.
Il y a une autre façon, qu’Evan a expliquée dans son package elm-sortable-table désormais obsolète. Parmi les nombreuses bonnes idées qu’il a eues, une idée m’a semblé brillamment simple mais efficace pour simplifier la conception d’API de tels composants de vue étatique :
Les mises à jour d’état peuvent être gérées directement depuis les gestionnaires d’événements !
Imaginons un simple compteur ; et si, en cliquant sur le bouton d’incrément, au lieu d’appeler onClick avec un message Increment quelconque, nous appellerions un message fourni par l’utilisateur avec le nouvel état du compteur mis à jour en conséquence ?
-- Counter.elmview : (Int -> msg) -> Int -> Html msgview toMsg counter = button [ onClick (toMsg (counter + 1)) ] [ text "increment" ]
Ou si vous voulez utiliser un type opaque, ce qui est une excellente idée pour maintenir la plus petite surface d’API possible :
-- Counter.elmtype State = State Intview : (State -> msg) -> State -> Html msgview toMsg (State value) = button [ onClick (toMsg (State (value + 1))) ] [ text "increment" ]
Notez que comme nous traitons un état de compteur, nous ne nous sommes pas embêtés à avoir autre chose qu’un simple Int pour le représenter. Mais vous pourriez bien sûr avoir un enregistrement ou tout ce que vous voulez.
Gérer la mise à jour de l’état interne pourrait être simplement de créer des Msg internes et non exposés et des fonctions update :
-- Counter.elmtype State = State Inttype Msg = Dec | Incupdate : Msg -> Int -> Intupdate msg value = case msg of Dec -> value - 1 Inc -> value + 1view : (State -> msg) -> State -> Html msgview toMsg (State value) = div [] [ button [ onClick (toMsg (State (update Dec value))) ] [ text "-" ] , div [] [ text (String.fromInt value) ] , button [ onClick (toMsg (State (update Inc value))) ] [ text "+" ] ]
Ou, si vous préférez travailler avec un type opaque :
-- Counter.elmtype State = State Inttype Msg = Dec | Incupdate : Msg -> State -> Stateupdate msg (State value) = case msg of Dec -> State (value - 1) Inc -> State (value + 1)view : (State -> msg) -> State -> Html msgview toMsg (State value) = div [] [ button [ onClick (toMsg (update Dec (State value))) ] [ text "-" ] , div [] [ text (String.fromInt value) ] , button [ onClick (toMsg (update Inc (State value))) ] [ text "+" ] ]
Et maintenant, pour l’intégrer, c’est facile :
-- Main.elmimport Counter exposing (State, Msg(..), view)type alias Model = { counter : State , value : Maybe Int }type Msg = CounterMsg Counter.Msginit : () -> ( Model, Cmd Msg )init _ = ( { counter = State 0, value = Nothing } , Cmd.none )update : Msg -> Model -> ( Model, Cmd Msg )update msg model = case msg of CounterMsg counterMsg -> let ( newCounterState, _ ) = Counter.update counterMsg model.counter in ( { model | counter = newCounterState } , Cmd.none )view : Model -> Html Msgview model = div [] [ Counter.view CounterMsg model.counter ]
C’est ça ! Toutes les responsabilités de gestion des mises à jour de l’état interne sont dans le composant lui-même. Et pour utiliser un événement interne spécifique, vous pouvez simplement intercepter les messages appropriés dans votre update de modèle principal.
Cette approche a le mérite d’être simple, intuitive et réduit considérablement la surface de l’API exposée aux utilisateurs du composant.
La seule critique que je peux penser est que vous vous retrouverez à devoir manipuler l’état interne directement, ce qui peut sembler contre-intuitif pour certains, mais honnêtement, je pense que c’est une très petite concession à faire pour la simplicité et la facilité d’utilisation que cela apporte.
Mots-clésAPIbest design practicecopyingdate range pickerElm developerselm-daterange-pickerpagespastingprojectsreusable viewsstateful componentsstateful view componentviews
Pour faciliter le développement des services EDA, nous avons créé un framework dédié, Eventail, qui masque la topologie et prend en charge tous les comportements attendus des services…