Rendez votre Web Front-end fiable avec Elm

Publié: 2022-03-11

Combien de fois avez-vous essayé de déboguer votre interface Web et vous êtes-vous retrouvé enchevêtré dans du code traitant de chaînes d'événements complexes ?

Avez-vous déjà essayé de refactoriser le code d'une interface utilisateur traitant de nombreux composants construits avec jQuery, Backbone.js ou d'autres frameworks JavaScript populaires ?

L'une des choses les plus pénibles à propos de ces scénarios est d'essayer de suivre les multiples séquences indéterminées d'événements et d'anticiper et de corriger tous ces comportements. Un cauchemar tout simplement !

J'ai toujours cherché des moyens d'échapper à cet aspect infernal du développement web front-end. Backbone.js a bien fonctionné pour moi à cet égard en donnant aux frontaux Web la structure qui leur manquait depuis longtemps. Mais étant donné la verbosité nécessaire pour faire certaines des choses les plus triviales, cela ne s'est pas avéré beaucoup mieux.

Rendez votre Web Front-end fiable avec Elm

Puis j'ai rencontré Elm.

Elm est un langage fonctionnel typé statiquement basé sur le langage de programmation Haskell, mais avec une spécification plus simple. Le compilateur (également construit à l'aide de Haskell) analyse le code Elm et le compile en JavaScript.

Elm a été conçu à l'origine pour le développement frontal, mais les ingénieurs en logiciel ont également trouvé des moyens de l'utiliser pour la programmation côté serveur.

Cet article donne un aperçu de la façon dont Elm peut changer notre façon de penser le développement Web frontal et une introduction aux bases de ce langage de programmation fonctionnel. Dans ce didacticiel, nous allons développer une application simple de type panier d'achat à l'aide d'Elm.

Pourquoi Orme ?

Elm promet de nombreux avantages, dont la plupart sont extrêmement utiles pour obtenir une architecture Web frontale propre. Elm offre de meilleurs avantages en termes de performances de rendu HTML par rapport aux autres frameworks populaires (même React.js). De plus, Elm permet aux développeurs d'écrire du code qui, en pratique, ne produit pas la plupart des exceptions d'exécution qui affectent les langages à typage dynamique comme JavaScript.

Le compilateur déduit automatiquement les types et émet des erreurs amicales, informant le développeur de tout problème potentiel avant l'exécution.

NoRedInk a 36 000 lignes d'Elm et, après plus d'un an de production, n'a toujours pas produit une seule exception d'exécution. [La source]

Vous n'avez pas besoin de convertir l'intégralité de votre application JavaScript existante pour pouvoir essayer Elm. Grâce à sa superbe interopérabilité avec JavaScript, vous pouvez même prendre une petite partie de votre application existante et la réécrire dans Elm.

Elm dispose également d'une excellente documentation qui vous donne non seulement une description détaillée de ce qu'il a à offrir, mais vous donne également un guide approprié pour créer un front-end Web suivant l'architecture Elm - quelque chose qui est idéal pour la modularité, la réutilisation du code et les tests. .

Faisons un chariot simple

Commençons par un très court extrait de code Elm :

 import List exposing (..) cart = [] item product quantity = { product = product, qty = quantity } product name price = { name = name, price = price } add cart product = if isEmpty (filter (\item -> item.product == product) cart) then append cart [item product 1] else cart subtotal cart = -- we want to calculate cart subtotal sum (map (\item -> item.product.price * toFloat item.qty) cart)

Tout texte précédé de -- est un commentaire en Elm.

Ici, nous définissons un panier comme une liste d'articles, où chaque article est un enregistrement à deux valeurs (le produit auquel il correspond et la quantité). Chaque produit est une fiche avec un nom et un prix.

L'ajout d'un produit au panier consiste à vérifier si l'article existe déjà dans le panier.

Si c'est le cas, nous ne faisons rien; sinon, nous ajoutons le produit au panier en tant que nouvel article. Nous vérifions si le produit existe déjà dans le panier en filtrant la liste, en associant chaque article au produit et en vérifiant si la liste filtrée résultante est vide.

Pour calculer le sous-total, nous parcourons les articles du panier, en trouvant la quantité et le prix du produit correspondant, et en additionnant le tout.

C'est aussi minimaliste qu'un chariot et ses fonctions connexes peuvent l'être. Nous allons commencer par ce code et l'améliorer étape par étape pour en faire un composant Web complet, ou un programme selon les termes d'Elm.

Commençons par ajouter des types aux différents identifiants de notre programme. Elm est capable de déduire des types par lui-même, mais pour tirer le meilleur parti d'Elm et de son compilateur, il est recommandé d'indiquer explicitement les types.

 module Cart1 exposing ( Cart, Item, Product , add, subtotal , itemSubtotal ) -- This is module and its API definition {-| We build an easy shopping cart. @docs Cart, Item, Product, add, subtotal, itemSubtotal -} import List exposing (..) -- we need list manipulation functions {-| Cart is a list of items. -} type alias Cart = List Item {-| Item is a record of product and quantity. -} type alias Item = { product : Product, qty : Int } {-| Product is a record with name and price -} type alias Product = { name : String, price : Float } {-| We want to add stuff to a cart. This is a function definition, it takes a cart, a product to add and returns new cart -} add : Cart -> Product -> Cart {-| This is an implementation of the 'add' function. Just append product item to the cart if there is no such product in the cart listed. Do nothing if the product exists. -} add cart product = if isEmpty (filter (\item -> item.product == product) cart) then append cart [Item product 1] else cart {-| I need to calculate cart subtotal. The function takes a cart and returns float. -} subtotal : Cart -> Float {-| It's easy -- just sum subtotal of all items. -} subtotal cart = sum (map itemSubtotal cart) {-| Item subtotal takes item and return the subtotal float. -} itemSubtotal : Item -> Float {-| Subtotal is based on product's price and quantity. -} itemSubtotal item = item.product.price * toFloat item.qty

Avec les annotations de type, le compilateur peut désormais détecter des problèmes qui, autrement, auraient entraîné des exceptions d'exécution.

Cependant, Elm ne s'arrête pas là. L'architecture Elm guide les développeurs à travers un modèle simple pour structurer leurs frontaux Web, et il le fait à travers des concepts que la plupart des développeurs connaissent déjà :

  • Modèle : les modèles contiennent l'état du programme.
  • Vue : la vue est une représentation visuelle de l'état.
  • Mettre à jour : la mise à jour est un moyen de modifier l'état.

Si vous considérez la partie de votre code traitant des mises à jour comme le contrôleur, alors vous avez quelque chose de très similaire au bon vieux paradigme Model-View-Controller (MVC).

Elm étant un langage de programmation purement fonctionnel, toutes les données sont immuables, ce qui signifie que le modèle ne peut pas être modifié. Au lieu de cela, nous pouvons créer un nouveau modèle basé sur le précédent, ce que nous faisons via des fonctions de mise à jour.

Pourquoi est-ce si génial ?

Avec des données immuables, les fonctions ne peuvent plus avoir d'effets secondaires. Cela ouvre un monde de possibilités, y compris le débogueur de voyage dans le temps d'Elm, dont nous parlerons bientôt.

Les vues sont rendues chaque fois qu'un changement dans le modèle nécessite un changement dans la vue, et nous aurons toujours le même résultat pour les mêmes données dans le modèle - de la même manière qu'une fonction pure renvoie toujours le même résultat pour le mêmes arguments d'entrée.

Commencez par la fonction principale

Continuons et implémentons la vue HTML pour notre application de panier.

Si vous êtes familier avec React, c'est quelque chose que je suis sûr que vous apprécierez : Elm a un package juste pour définir les éléments HTML. Cela vous permet d'implémenter votre vue à l'aide d'Elm, sans avoir à recourir à des langages de modèles externes.

Les wrappers pour les éléments HTML sont disponibles dans le package Html :

 import Html exposing (Html, button, table, caption, thead, tbody, tfoot, tr, td, th, text, section, p, h1)

Tous les programmes Elm commencent par exécuter la fonction main :

 type alias Stock = List Product type alias Model = { cart : Cart, stock : Stock } main = Html.beginnerProgram { model = Model [] [ Product "Bicycle" 100.50 , Product "Rocket" 15.36 , Product "Biscuit" 21.15 ] , view = view , update = update }

Ici, la fonction main initialise un programme Elm avec quelques modèles, une vue et une fonction de mise à jour. Nous avons défini quelques types de produits et leurs prix. Pour simplifier, nous supposons que nous avons un nombre illimité de produits.

Une fonction de mise à jour simple

La fonction de mise à jour est l'endroit où notre application prend vie.

Il prend un message et met à jour l'état en fonction du contenu du message. Nous le définissons comme une fonction qui prend deux paramètres (un message et le modèle courant) et renvoie un nouveau modèle :

 type Msg = Add Product update : Msg -> Model -> Model update msg model = case msg of Add product -> { model | cart = add model.cart product }

Pour l'instant, nous traitons un seul cas lorsque le message est Add product , où nous appelons la méthode add on cart avec le product .

La fonction de mise à jour augmentera à mesure que la complexité du programme Elm augmentera.

Implémentation de la fonction d'affichage

Ensuite, nous définissons la vue de notre panier.

La vue est une fonction qui traduit le modèle dans sa représentation HTML. Cependant, ce n'est pas seulement du HTML statique. Le générateur HTML est capable de renvoyer des messages à l'application en fonction de diverses interactions et événements de l'utilisateur.

 view : Model -> Html Msg view model = section [style [("margin", "10px")]] [ stockView model.stock , cartView model.cart ] stockView : Stock -> Html Msg stockView stock = table [] [ caption [] [ h1 [] [ text "Stock" ] ] , thead [] [ tr [] [ th [align "left", width 100] [ text "Name" ] , th [align "right", width 100] [ text "Price" ] , th [width 100] [] ] ] , tbody [] (map stockProductView stock) ] stockProductView : Product -> Html Msg stockProductView product = tr [] [ td [] [ text product.name ] , td [align "right"] [ text ("\t$" ++ toString product.price) ] , td [] [ button [ onClick (Add product) ] [ text "Add to Cart" ] ] ]

Le package Html fournit des enveloppes pour tous les éléments couramment utilisés en tant que fonctions avec des noms familiers (par exemple, la section de fonction génère un élément <section> ).

La fonction style , qui fait partie du package Html.Attributes , génère un objet qui peut être passé à la fonction section pour définir l'attribut style sur l'élément résultant.

Il est préférable de diviser la vue en fonctions distinctes pour une meilleure réutilisation.

Pour garder les choses simples, nous avons intégré le CSS et certains attributs de mise en page directement dans notre code de vue. Cependant, il existe des bibliothèques qui rationalisent le processus de stylisation de vos éléments HTML à partir du code Elm.

Remarquez le button près de la fin de l'extrait et comment nous avons associé le message Add product à l'événement de clic du bouton.

Elm s'occupe de générer tout le code nécessaire pour lier une fonction de rappel à l'événement réel et générer et appeler la fonction de mise à jour avec les paramètres pertinents.

Enfin, nous devons implémenter la dernière partie de notre vue :

 cartView : Cart -> Html Msg cartView cart = if isEmpty cart then p [] [ text "Cart is empty" ] else table [] [ caption [] [ h1 [] [ text "Cart" ]] , thead [] [ tr [] [ th [ align "left", width 100 ] [ text "Name" ] , th [ align "right", width 100 ] [ text "Price" ] , th [ align "center", width 50 ] [ text "Qty" ] , th [ align "right", width 100 ] [ text "Subtotal" ] ] ] , tbody [] (map cartProductView cart) , tfoot [] [ tr [style [("font-weight", "bold")]] [ td [ align "right", colspan 4 ] [ text ("$" ++ toString (subtotal cart)) ] ] ] ] cartProductView : Item -> Html Msg cartProductView item = tr [] [ td [] [ text item.product.name ] , td [ align "right" ] [ text ("$" ++ toString item.product.price) ] , td [ align "center" ] [ text (toString item.qty) ] , td [ align "right" ] [ text ("$" ++ toString (itemSubtotal item)) ] ]

Ici, nous avons défini l'autre partie de notre vue où nous rendons le contenu de notre panier. Bien que la fonction de vue n'émette aucun message, elle doit toujours avoir le type de retour Html Msg pour être qualifiée de vue.

La vue répertorie non seulement le contenu du panier, mais calcule et affiche également le sous-total en fonction du contenu du panier.

Vous pouvez trouver le code complet de ce programme Elm ici.

Si vous deviez exécuter le programme Elm maintenant, vous verriez quelque chose comme ceci :

Comment ça fonctionne?

Notre programme commence par un état assez vide de la fonction main - un panier vide avec quelques produits codés en dur.

Chaque fois que vous cliquez sur le bouton "Ajouter au panier", un message est envoyé à la fonction de mise à jour, qui met ensuite à jour le panier en conséquence et crée un nouveau modèle. Chaque fois que le modèle est mis à jour, les fonctions d'affichage sont appelées par Elm pour régénérer l'arborescence HTML.

Étant donné qu'Elm utilise une approche DOM virtuelle, similaire à celle de React, les modifications de l'interface utilisateur ne sont effectuées que lorsque cela est nécessaire, garantissant des performances rapides.

Pas seulement un vérificateur de type

Elm est typé statiquement, mais le compilateur peut vérifier bien plus que de simples types.

Modifions notre type Msg et voyons comment le compilateur réagit à cela :

 type Msg = Add Product | ChangeQty Product String

Nous avons défini un autre type de message - quelque chose qui changerait la quantité d'un produit dans le panier. Cependant, tenter de relancer le programme sans gérer ce message dans la fonction de mise à jour générera l'erreur suivante :

Vers un chariot plus fonctionnel

Notez que dans la section précédente, nous avons utilisé une chaîne comme type pour la valeur de la quantité. En effet, la valeur proviendra d'un élément <input> qui sera de type string.

Ajoutons une nouvelle fonction changeQty au module Cart . Il est toujours préférable de conserver l'implémentation à l'intérieur du module pour pouvoir la modifier ultérieurement si nécessaire sans modifier l'API du module.

 {-| Change quantity of the product in the cart. Look at the result of the function. It uses Result type. The Result type has two parameters: for bad and for good result. So the result will be Error "msg" or a Cart with updated product quantity. -} changeQty : Cart -> Product -> Int -> Result String Cart {-| If the quantity parameter is zero the product will be removed completely from the cart. If the quantity parameter is greater then zero the quantity of the product will be updated. Otherwise (qty < 0) the error will be returned. -} changeQty cart product qty = if qty == 0 then Ok (filter (\item -> item.product /= product) cart) else if qty > 0 then Result.Ok (map (\item -> if item.product == product then { item | qty = qty } else item) cart) else Result.Err ("Wrong negative quantity used: " ++ (toString qty))

Nous ne devons faire aucune hypothèse sur la façon dont la fonction sera utilisée. Nous pouvons être sûrs que le paramètre qty contiendra un Int mais la valeur peut être n'importe quoi. Nous vérifions donc la valeur et signalons une erreur lorsqu'elle est invalide.

Nous mettons également à jour notre fonction de mise à update en conséquence :

 update msg model = case msg of Add product -> { model | cart = add model.cart product } ChangeQty product str -> case toInt str of Ok qty -> case changeQty model.cart product qty of Ok cart -> { model | cart = cart } Err msg -> model -- do nothing, the wrong input Err msg -> model -- do nothing, the wrong quantity

Nous convertissons le paramètre de quantity de chaîne du message en un nombre avant de l'utiliser. Si la chaîne contient un nombre invalide, nous le signalons comme une erreur.

Ici, nous gardons le modèle inchangé lorsqu'une erreur se produit. Alternativement, nous pourrions simplement mettre à jour le modèle de manière à signaler l'erreur sous forme de message dans la vue pour que l'utilisateur puisse le voir :

 type alias Model = { cart : Cart, stock : Stock, error : Maybe String } main = Html.beginnerProgram { model = Model [] -- empty cart [ Product "Bicycle" 100.50 -- stock , Product "Rocket" 15.36 , Product "Bisquit" 21.15 ] Nothing -- error (no error at beginning) , view = view , update = update } update msg model = case msg of Add product -> { model | cart = add model.cart product } ChangeQty product str -> case toInt str of Ok qty -> case changeQty model.cart product qty of Ok cart -> { model | cart = cart, error = Nothing } Err msg -> { model | error = Just msg } Err msg -> { model | error = Just msg }

Nous utilisons le type Maybe String pour l'attribut error dans notre modèle. Maybe est un autre type qui peut soit contenir Nothing , soit une valeur d'un type spécifique.

Après avoir mis à jour la fonction d'affichage comme suit :

 view model = section [style [("margin", "10px")]] [ stockView model.stock , cartView model.cart , errorView model.error ] errorView : Maybe String -> Html Msg errorView error = case error of Just msg -> p [style [("color", "red")]] [ text msg ] Nothing -> p [] []

Vous devriez voir ceci :

Tenter d'entrer une valeur non numérique (par exemple "1a") entraînerait un message d'erreur comme indiqué dans la capture d'écran ci-dessus.

Monde des forfaits

Elm possède son propre référentiel de packages open source. Avec le gestionnaire de packages pour Elm, il devient un jeu d'enfant d'exploiter ce pool de packages. Bien que la taille du référentiel ne soit pas comparable à celle de certains autres langages de programmation matures comme Python ou PHP, la communauté Elm travaille dur pour implémenter plus de packages chaque jour.

Remarquez comment les décimales des prix rendus à notre avis sont incohérentes ?

Remplaçons notre utilisation naïve de toString par quelque chose de mieux du référentiel : numeral-elm.

 cartProductView item = tr [] [ td [] [ text item.product.name ] , td [ align "right" ] [ text (formatPrice item.product.price) ] , td [ align "center" ] [ input [ value (toString item.qty) , onInput (ChangeQty item.product) , size 3 --, type' "number" ] [] ] , td [ align "right" ] [ text (formatPrice (itemSubtotal item)) ] ] formatPrice : Float -> String formatPrice price = format "$0,0.00" price

Nous utilisons ici la fonction format du package Numeral. Cela formaterait les nombres d'une manière que nous formatons généralement les devises :

 100.5 -> $100.50 15.36 -> $15.36 21.15 -> $21.15

Génération automatique de documentation

Lors de la publication d'un package dans le référentiel Elm, la documentation est générée automatiquement en fonction des commentaires dans le code. Vous pouvez le voir en action en consultant la documentation de notre module Panier ici. Tous ces éléments ont été générés à partir des commentaires vus dans ce fichier : Cart.elm.

Un véritable débogueur pour le front-end

Les problèmes les plus évidents sont détectés et signalés par le compilateur lui-même. Cependant, aucune application n'est à l'abri d'erreurs logiques.

Étant donné que toutes les données dans Elm sont immuables et que tout se passe par le biais de messages transmis à la fonction de mise à jour, le flux entier d'un programme Elm peut être représenté comme une série de modifications de modèle. Pour le débogueur, Elm est comme un jeu de stratégie au tour par tour. Cela permet au débogueur d'accomplir des exploits vraiment incroyables, comme voyager dans le temps. Il peut aller et venir dans le flux d'un programme en sautant entre les différents changements de modèle qui se sont produits pendant la durée de vie d'un programme.

Vous pouvez en savoir plus sur le débogueur ici.

Interagir avec un back-end

Donc, vous dites, nous avons construit un joli jouet, mais Elm peut-il être utilisé pour quelque chose de sérieux ? Absolument.

Connectons notre front-end de panier avec un back-end asynchrone. Pour le rendre intéressant, nous allons implémenter quelque chose de spécial. Disons que nous voulons inspecter tous les paniers et leur contenu en temps réel. Dans la vraie vie, nous pourrions utiliser cette approche pour apporter des capacités de marketing/vente supplémentaires à notre boutique en ligne ou à notre place de marché, ou faire des suggestions à l'utilisateur, ou estimer les ressources de stock nécessaires, et bien plus encore.

Ainsi, nous stockons le panier côté client et informons également le serveur de chaque panier en temps réel.

Pour garder les choses simples, nous allons implémenter notre back-end en utilisant Python. Vous pouvez trouver le code complet du back-end ici.

Il s'agit d'un simple serveur Web qui utilise un WebSocket et garde une trace du contenu du panier en mémoire. Pour garder les choses simples, nous afficherons le panier de tout le monde sur la même page. Cela peut facilement être implémenté dans une page séparée ou même en tant que programme Elm séparé. Pour l'instant, chaque utilisateur pourra voir le résumé des paniers des autres utilisateurs.

Avec le back-end en place, nous devrons maintenant mettre à jour notre application Elm pour envoyer et recevoir les mises à jour du panier sur le serveur. Nous utiliserons JSON pour encoder nos charges utiles, pour lesquelles Elm a un excellent support.

CartEncoder.elm

Nous allons implémenter un encodeur pour convertir notre modèle de données Elm en une représentation de chaîne JSON. Pour cela, nous devons utiliser la bibliothèque Json.Encode.

 module CartEncoder exposing (cart) import Cart2 exposing (Cart, Item, Product) import List exposing (map) import Json.Encode exposing (..) product : Product -> Value product product = object [ ("name", string product.name) , ("price", float product.price) ] item : Item -> Value item item = object [ ("product", product item.product) , ("qty", int item.qty) ] cart : Cart -> Value cart cart = list (map item cart)

La bibliothèque fournit certaines fonctions (telles que string , int , float , object , etc.) qui prennent des objets Elm et les transforment en chaînes codées JSON.

CartDecoder.elm

L'implémentation du décodeur est un peu plus délicate car toutes les données Elm ont des types et nous devons définir quelle valeur JSON doit être convertie en quel type :

 module CartDecoder exposing (cart) import Cart2 exposing (Cart, Item, Product) -- decoding for Cart import Json.Decode exposing (..) -- will decode cart from string cart : Decoder (Cart) cart = list item -- decoder for cart is a list of items item : Decoder (Item) item = object2 Item -- decoder for item is an object with two properties: ("product" := product) -- 1) "product" of product ("qty" := int) -- 2) "qty" of int product : Decoder (Product) product = object2 Product -- decoder for product also an object with two properties: ("name" := string) -- 1) "name" ("price" := float) -- 2) "price"

Mise à jour de l'application Elm

Comme le code Elm final est un peu plus long, vous pouvez le trouver ici. Voici un résumé des modifications apportées à l'application frontale :

Nous avons enveloppé notre fonction de mise à update d'origine avec une fonction qui envoie les modifications apportées au contenu du panier au back-end chaque fois que le panier est mis à jour :

 updateOnServer msg model = let (newModel, have_to_send) = update msg model in case have_to_send of True -> -- send updated cart to server (!) newModel [ WebSocket.send server (encode 0 (CartEncoder.cart newModel.cart)) ] False -> -- do nothing (newModel, Cmd.none)

Nous avons également ajouté un type de message supplémentaire de ConsumerCarts String pour recevoir les mises à jour du serveur et mettre à jour le modèle local en conséquence.

La vue a été mise à jour pour afficher le résumé des paniers des autres à l'aide de la fonction consumersCartsView .

Une connexion WebSocket a été établie pour s'abonner au back-end afin d'écouter les modifications apportées aux paniers des autres.

 subscriptions : Model -> Sub Msg subscriptions model = WebSocket.listen server ConsumerCarts server = "ws://127.0.0.1:8765"

Nous avons également mis à jour notre fonction principale. Nous utilisons maintenant Html.program avec des paramètres init et subscriptions supplémentaires. init spécifie le modèle initial du programme et subscription spécifie une liste d'abonnements.

Un abonnement est un moyen pour nous de dire à Elm d'écouter les changements sur des canaux spécifiques et de transmettre ces messages à la fonction de update à jour.

 main = Html.program { init = init , view = view , update = updateOnServer , subscriptions = subscriptions } init = ( Model [] -- empty cart [ Product "Bicycle" 100.50 -- stock , Product "Rocket" 15.36 , Product "Bisquit" 21.15 ] Nothing -- error (no error at beginning) [] -- consumer carts list is empty , Cmd.none)

Enfin, nous avons géré la façon dont nous décodons le message ConsumerCarts que nous recevons du serveur. Cela garantit que les données que nous recevons d'une source externe n'endommageront pas l'application.

 ConsumerCarts message -> case decodeString (Json.Decode.list CartDecoder.cart) message of Ok carts -> ({ model | consumer_carts = carts }, False) Err msg -> ({ model | error = Just msg, consumer_carts = [] }, False)

Gardez vos frontaux sains

L'orme est différent. Cela oblige le développeur à penser différemment.

Toute personne venant du domaine de JavaScript et des langages similaires se retrouvera à essayer d'apprendre la façon de faire d'Elm.

En fin de compte, Elm offre quelque chose que d'autres frameworks - même les plus populaires - ont souvent du mal à faire. À savoir, il fournit un moyen de créer des applications frontales robustes sans s'emmêler dans un énorme code verbeux.

Elm élimine également bon nombre des difficultés que pose JavaScript en combinant un compilateur intelligent avec un puissant débogueur.

Elm est ce à quoi aspirent les développeurs front-end depuis si longtemps. Maintenant que vous l'avez vu en action, essayez-le vous-même et profitez-en en créant votre prochain projet Web dans Elm.