9

This question is kind of silly but i didn't find a straight forward solution.

Assuming I have a model that resembles this: - at least this big.

initModel =
{ selectedCategory = "Vacantion"
, context = "root/Work"
, abstractSyntaxTree =
    [ { categoryName = "Work"
      , categoryContent =
            []
      }
    , { categoryName = "Vacation"
      , categoryContent =
            [ { folderName = "Hawaii"
              , folderContent =
                    FolderContent
                        ( [ { folderName = "Booking"
                            , folderContent = FolderContent ( [], [] )
                            }
                          ]
                        , [ "flightTicket.jpg" ]
                        )
              }
            ]
      }
    ]
}

Question: How can i display it in the browser so that it looks good? - nothing fancy - just to see what's happening like a quick debugger..

What I've tried so far:

view = 
    div [] 
        [ pre [style [("width", "300") ] ] [ text (toString model)]
        ]

Works great on smaller models but on this case, I get this long -single line - formatted json like structure: enter image description here

I think this is the Problem: The prettify extension i installed in google chrome doesn't know how to deal with strings that do not contain \n in them. To check for this, i manually added a \n - and that string was split on the second row, as expected.

The output form text (toSting model) - it's a string that has no \n in it - so that's why everything is displayed on a single line in the browser - regardless of the 300 px limit on width.

Splitting up the string - by adding \n by myself - works except i don't know where exactly to add the \n. To make it dynamic, this requires a full-flagged parser for the model. A way to know where the expression starts, where is the next matching bracket .. etc. I'm not good enough to build this parser. I feel i'm over complicating this stuff. Must be a better solution..

How do you folks do it?

AIon
  • 12,521
  • 10
  • 47
  • 73

3 Answers3

8

Elm does not allow you to enumerate over the items in a record. For type-safety no doubt. So there is no "clean" way to do display a record nicely.


UPDATE: The solution below will not work anymore as of Elm 0.19.

It relies on a (deprecated) function toString that would convert any type into a string. Since Elm 0.17 (when I made this), Elm had already released a great debugger in 0.18, which already has a feature to follow model and messages in a separate window.


For debugging, you can do some trickery with toString to print it more neatly. Below is example code, which you can copy/ paste to elm-lang.org/try.

Simply pass any record to the viewModel function, and it displays it in the browser. It is quite crude, and probably not very performant with large records, but it will do the job..

import Html exposing (Html, text, div, p, pre)
import Html.Attributes exposing (style)
import String

quote = "\""
indentChars = "[{("
outdentChars = "}])"
newLineChars = ","
uniqueHead = "##FORMAT##"
incr = 20


model = 
  { name = "Abe"
  , age = 49
  , someTuple = (18,49)
  , relatives = [ "Claire", "Bill" ]
  , comments = "any special characters like []{}, will not be parsed"
  , cars = [ { brand = "BMW", model = "535i" } ]
  }

viewModel : a -> Html msg
viewModel model =
  let
    lines =
      model
      |> toString
      |> formatString False 0 
      |> String.split uniqueHead
  in
    pre [] <| List.map viewLine lines

viewLine : String -> Html msg
viewLine lineStr =
  let
    (indent, lineTxt) = splitLine lineStr
  in
    p [ style 
        [ ("paddingLeft", px (indent))
        , ("marginTop", "0px")
        , ("marginBottom", "0px")
        ] 
      ]
      [ text lineTxt ]


px : Int -> String
px int =
  toString int
  ++ "px"

formatString : Bool -> Int -> String -> String
formatString isInQuotes indent str =
  case String.left 1 str of
    "" -> ""

    firstChar -> 
      if isInQuotes then
        if firstChar == quote then
          firstChar 
          ++ formatString (not isInQuotes) indent (String.dropLeft 1 str)
        else
          firstChar 
          ++ formatString isInQuotes indent (String.dropLeft 1 str)
      else
        if String.contains firstChar newLineChars then
          uniqueHead ++ pad indent ++ firstChar
          ++ formatString isInQuotes indent (String.dropLeft 1 str)
        else if String.contains firstChar indentChars then
          uniqueHead ++ pad (indent + incr) ++ firstChar
          ++ formatString isInQuotes (indent + incr) (String.dropLeft 1 str)
        else if String.contains firstChar outdentChars then
          firstChar ++ uniqueHead ++ pad (indent - incr)
          ++ formatString isInQuotes (indent - incr) (String.dropLeft 1 str)
        else if firstChar == quote then
          firstChar 
          ++ formatString (not isInQuotes) indent (String.dropLeft 1 str)
        else
          firstChar 
          ++ formatString isInQuotes indent (String.dropLeft 1 str)

pad : Int -> String
pad indent =
  String.padLeft 5 '0' <| toString indent

splitLine : String -> (Int, String)
splitLine line =
  let
    indent = 
      String.left 5 line
      |> String.toInt
      |> Result.withDefault 0
    newLine =
      String.dropLeft 5 line
  in
    (indent, newLine)

main =
  viewModel model
wintvelt
  • 13,855
  • 3
  • 38
  • 43
  • thanks man! Wow! You can make a package out of this :). In slack nobody knew how to do this, and i'm guessing others could benefit as well.. I have a question still: What exactly do yo mean by: `"Elm does not allow you to enumerate over the items in a record. "`? You can't have something like Object.keys form javascript? – AIon Nov 11 '16 at 17:56
  • Glad to help :) I'm not so familiar with making packages, but will look into it. – wintvelt Nov 11 '16 at 18:07
  • 1
    As to your question: your intuition is correct. Elm does not have `Object.keys`. I guess that has to do with type safety: every item in an object can be of a any type, and Elm needs to know which type you're using. With something like JavaScript's `myObject[someString]` dynamic types, this would break that guarantee. If every item has the same type, you could also use a `Dict`. – wintvelt Nov 11 '16 at 18:13
  • 1
    Just a heads up: tonight I published a package to print any record to the DOM or to the console. You can [check it out here](http://package.elm-lang.org/packages/wintvelt/elm-print-any/latest). Thanks for the suggestion! I learned a lot about git and Elm packages. – wintvelt Nov 13 '16 at 21:16
  • Brilliant!!! @wintvelt - I checkout the package you made, but it wont work with Elm 0.18.0 `There are no versions of wintvelt/elm-print-any that work with Elm 0.18.0.` I was able to use the code above though. Again, brilliant work! – Benjamin Gandhi-Shepard Oct 26 '17 at 19:59
  • @BenjaminGandhi-Shepard Good to hear it works for you! I wrote this bit back in 0.17. Ever since 0.18, with the new debugger - which also includes some print of the model - the need for the print-any sort of disappeared. But if I get around to it, I might update.. – wintvelt Oct 27 '17 at 06:20
  • @wintvelt Elm's rapidly changing. in 0.19 as far as I know can't use toString model – BenKoshy Oct 18 '18 at 07:49
  • Yeah, I know, this trick (and the package) can't be upgraded to 0.19. Because it relies on the (deprecated) property of `toString` that it will accept any type. I'll update the answer. – wintvelt Oct 18 '18 at 07:52
3

By now we have a nice package for 0.19.

elm install ThinkAlexandria/elm-pretty-print-json

And run

json = """{"name": "Arnold", "age": 70, "isStrong": true,"knownWeakness": null,"nicknames": ["Terminator", "The Governator"],"extra": {"foo": "bar","zap": {"cat": 1,"dog": 2},"transport": [[ "ford", "chevy" ],[ "TGV", "bullet train", "steam" ]]}}"""

{-| Formating configuration.
`indent` is the number of spaces in an indent.
`columns` is the desired column width of the formatted string. The formatter
will try to fit it as best as possible to the column width, but can still
exceed this limit. The maximum column width of the formatted string is
unbounded.
-}
config = {indent = 4, columns = 80}

-- run prettifier
Result.withDefault "" (Json.Print.prettyString config json)

-- result
{-
{
    "extra": {
        "transport": [
            [
                "ford",
                "chevy"
            ],
            [
                "TGV",
                "bullet train",
                "steam"
            ]
        ],
        "zap": {
            "dog": 2,
            "cat": 1
        },
        "foo": "bar"
    },
    "nicknames": [
        "Terminator",
        "The Governator"
    ],
    "knownWeakness": null,
    "isStrong": true,
    "age": 70,
    "name": "Arnold"
}
-}
Tails
  • 636
  • 7
  • 16
  • do you first need to stringify the elm model to be able to pass it to this `Json.Print.prettyString` function? Sounds like a pain the the but to implement toStrings for all union types one might have, seems like it does not work on elm types directly like `Debug.log` does it for example. I think there still isn't any nice way to print the model/an elm type itself. But thanks for the package though. Is still good to have the json in a nice format. – AIon Oct 01 '19 at 10:01
  • Thank you for sharing this solution on SO! Really nice. – wintvelt Oct 02 '19 at 14:00
  • 2
    @AIon It's indeed a total PITA. However, ```Debug.toString``` on a union type results in a structure that _somewhat_ resembles JSON. I think it's relatively straightforward to parse that String output, replace all the '=' with ':', enclose variables with " and interpret capitalized keys as strings to form valid JSON. – Tails Oct 02 '19 at 15:11
  • @Tails very good powerful approach. This approach can be totally isolated in a special package called elm-debug-model or something :)) It should be able to be published with no problems. It basically does a `Debug.toString` onto any `a` type.. then does what you said to transform that type as string into a `real json`.. then also uses `the package you shared/built` -- to make this json pretty printed. Im gonna build this myself if you don't :)) But was your solution so please go ahead if you want to publish your own package :) I want to do it. But is you which should take the credit :) – AIon Oct 03 '19 at 08:30
  • @Alon I had started writing that kind of (Debug.toString union) -> JsonString function but it distracted me from what I was supposed to be doing with my code, so in the end I just checked the state transition tool that comes with the --debug compile flag. However, if you start a repository with the start of this function I would consider contributing to it! – Tails Oct 03 '19 at 08:41
  • @Tails - tried it and i get a bunch of `"\\\"` backslashes in front of things.I rely needed this for the project im working (https://prnt.sc/peesvr) but i must solve this project now. But definetly gonna give this another shot over the weekend. We might look at how is done in the elm debugger. I think they use `toString` and then build the `AST` from there. Part of the difficulty comes form human reading it since the console output is already escaped, and that interferes with the `\n`'s which are inserted by prettypriting. A bunch of `"\n"` are inserted everywhere makes a complete visual mess. – AIon Oct 03 '19 at 15:42
  • @Alon Cool, you working on automation for UpWork? I believe the ```toString``` relies on 'native' (JS) code. A parser for the value output would have to: - delete newlines (except for in strings) - replace with an empty object {} (or some other label) - replace '=' with ':' - encapsulate object keys with " - replace tuples () with array brackets - delete Just labels - replace Nothing with null - replace unions with {type: "UnionLabel", values: [val1, val2]} - ... – Tails Oct 04 '19 at 09:37
2

I found this in slack : elm-debug-transformer

how to pretty print elm model or elm types in the browser

Not rely what i asked for, because for example im gonna need to make it work with node.js also, but still seems like good solution for now.

Community
  • 1
  • 1
AIon
  • 12,521
  • 10
  • 47
  • 73