Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support tagged template literals (e.g. for styled-components) #1973

Closed
cmeeren opened this issue Feb 6, 2020 · 16 comments · Fixed by #2460
Closed

Support tagged template literals (e.g. for styled-components) #1973

cmeeren opened this issue Feb 6, 2020 · 16 comments · Fixed by #2460

Comments

@cmeeren
Copy link
Contributor

cmeeren commented Feb 6, 2020

It seems that styled-components is all the rage now; for example, it's officially the future for styling in Material-UI.

It seems to use a new kind of syntax I haven't seen (and don't yet understand). I have no idea what, if anything, must change in Fable for it to support this in some way or another, but I thought I'd raise an issue sooner rather than later, just in case.

(I'm not talking about a new syntax for the F# code, which of course is impossible for Fable to do anything about directly, but rather some way to utilize styled-components from F#/Fable.)

@0x53A
Copy link
Contributor

0x53A commented Feb 6, 2020

I think this is a tagged interpolated string: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

@0x53A
Copy link
Contributor

0x53A commented Feb 6, 2020

The good thing is the tags from the tagged interpolated strings are just functions.

If you don't need to add any logic, it's easy:

https://styled-components.com/docs/basics#getting-started

const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;
let Wrapper = styled.section([|"""
  padding: 4em;
  background: papayawhip;
"""|])

If you do need the props logic, it becomes uglier:

const Button = styled.button`
  /* Adapt the colors based on primary prop */
  background: ${props => props.primary ? "palevioletred" : "white"};
  color: ${props => props.primary ? "white" : "palevioletred"};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;
let strings = [|
    """
  /* Adapt the colors based on primary prop */
  background: """

    """;
  color: """

    """;

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;"""
|]
let Button = styled.button(strings , fun (props:MyPropType) -> if props.primary then "palevioletred" else "white", fun (props:MyPropType) -> if props.primary then "white" else "palevioletred")

I think to actually use it you then need to wrap it in React.createElement:

div [] [
    React.createElement(Button, butttonProps)
]

So the best solution is probably to wait for string interpolation in F#? https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1001-StringInterpolation.md

@inosik
Copy link
Contributor

inosik commented Feb 7, 2020

Could be possible already, albeit pretty ugly:

open System

// TODO: Come up with proper types
type StyledToken = obj
type StyledComponent = obj

type Styled private () =
    static member button ([<ParamArray>] style : StyledToken array) : StyledComponent =
        failwith "To be done"

type ButtonProps =
    { Primary : bool }

let button = Styled.button ("
    padding: 4em;
    background: ", (fun props -> if props.Primary then "paleviolettred" else "white"), ";
    color: ", (fun props -> if props.Primary then "white" else "palevioletred"), ";
    fonts-ize: 1em;
")

Another alternative would be to define the components in JS/TS and import them.

@klcantrell
Copy link

klcantrell commented Mar 14, 2020

I came up with a hacky solution that looks like it will work for my purposes, just wanted to share.

Bindings:

type IStyled =
    { div: obj array -> obj }

[<ImportDefault("styled-components")>]
let _styled: IStyled = jsNative

module styled =
    let div styles props element = React.createElement (_styled.div styles, props, [ element ])

Usage:

let StyledApp = styled.div [| "
    background-color: ", (fun props -> if props.primary then "blue" else "red"), ";
    height: 100px;
    border: ", (fun props -> if props.primary then "20px" else "10px"), " solid black;
    border-radius: 10px;
" |]

// appView defined elsewhere
let App = StyledApp { primary = true } (appView())

It works because of the way that styled-components happens to process the arguments passed to the tagged template function. It'd be better if f# had support for string interpolation but until then, this kind of solution will work okay for my side projects anyway.

Unfortunately, since the above bindings wrap the execution of say something like styled.div, you'll get warnings about creating components dynamically inside of other components from the styled-components library. With that, this hacky solution is probably not going to be ideal for many.

@inosik
Copy link
Contributor

inosik commented Mar 18, 2020

That's kind of what I had in mind as well. And as of now, this is the best we can get, besides maybe leveraging JS/TS to define components.

To support this properly, we either have to wait until string interpolation lands in the language (dotnet/fsharp#6770), or come up with something ourselves. I wouldn't "fork" the language, though.

@AngelMunoz
Copy link

lit-element also uses interpolated strings and really lends itself to be used functionally
I tried using it yesterday but it's quite ugly as well
image

@inosik
Copy link
Contributor

inosik commented Dec 2, 2020

String interpolation is in F# now, so we can plan how to use it with Fable. The obvious use case does already work with Fable:

Console.WriteLine $"x = {42}"

To enable the "tagged template literals" use case, we'd need to extend Fable to support System.FormattableString. Library authors can then build on that.

@alfonsogarciacaro
Copy link
Member

Some support for FormattableString has been implemented in 3.2.2. I haven't tried it yet with JS tagged templates, but I think something like this should work: https://gist.github.com/alfonsogarciacaro/ea8a3cfe07e773664ea5078141ca9f82

@texastoland
Copy link

texastoland commented Aug 18, 2021

@alfonsogarciacaro Would it be possible to add jsTemplate to Fable.Core.JsInterop such that:

let test() =
    let name = "Angel"
    let litHTML = jsTemplate Lit.HTML
    litHTML $"<div>Hello {name}</div>"

Alternatively:

type Lit =
    [<ImportTemplate("lit-html")>]
    static member html(s: FormattableString): LitComponent = jsNative

I think I prefer the function over the attribute. Better in Fable.Extras?

@alfonsogarciacaro
Copy link
Member

Yes, the function would be much simpler. An attribute would require a plugin or modifying the compiler. It would be a good idea to standardize something like this, but Fable.Core is a bit tricky because it doesn't contain actual code, only metadata and many calls are resolved by compiler magic. Fable.Extras could be a good place to include the function. It should also be simple to add it directly in your projects:

let jsTemplate (template: string[] -> obj[] -> 'T): FormattableString -> 'T =
    let convertTemplate (s: FormattableString) =
        let str = s.Format
        let mutable prevIndex = 0
        let matches = Regex.Matches(str, @"{\d+}")
        Array.init (matches.Count + 1) (fun i ->
            if i < matches.Count then
                let m = matches.[i]
                let idx = prevIndex
                prevIndex <- m.Index + m.Length
                str.Substring(idx, m.Index - idx)
            else
                str.Substring(prevIndex)), s.GetArguments()

    fun s ->
        let strs, args = convertTemplate s
        template strs args

Example in the REPL

@texastoland
Copy link

texastoland commented Aug 19, 2021

FormattableString is so close to the JS representation I don't see the benefit of an attribute either. I managed to simplify the function a bit:

type JSTemplateTag<'T> = string[] -> obj[] -> 'T

let jsTemplate (tag: JSTemplateTag<'T>) (raw: FormattableString) : 'T =
  let strs = Regex(@"{\d+}").Split(raw.Format)
  let args = raw.GetArguments()
  tag strs args

// Example
[<Emit("up($0,...$1...)")>]
let up (strs: string[]) (args: obj[]) : string = jsNative

let up' = jsTemplate up
let name = "f#"
System.Console.WriteLine(up' $"hello {name}")

(See REPL)

[<Emit>] was necessary because tag functions are invoked like tag([str1, str2, str3], arg1, arg2) in JS. I tried an interface using [<ParamArray>] but it was significantly more verbose and ultimately didn't work. @Shmew Open for PR?

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Aug 20, 2021

Oh, that's nice! I always forget you can split with regex 😅

Yes, curried functions in F# are restricted in the sense they don't accept spread or optional arguments. For that we need a delegate. "Delegate" is the original term in .NET for function pointers, and F# uses it to denote non-curried functions. In Fable at the beginning we tried to use delegates to represent functions coming from JS, but the syntax was not very nice, so now Fable tries to hide the distinction by automatically uncurrying lambdas. However, sometimes like in this case we do need a delegate declaration. You can do it like this:

type JSTemplateTag<'T> = delegate of strs: string[] * [<ParamArray>] args: obj[] -> 'T

let jsTemplate (tag: JSTemplateTag<'T>) (fmt: FormattableString) : 'T =
  let strs = Regex(@"{\d+}").Split(fmt.Format)
  let args = fmt.GetArguments()
  tag.Invoke(strs, args)

A nice side-effect is this makes it easier to import tags as values (with lambdas this sometimes messes up the uncurrying if we don't declare the import as a function). Example:

let html: JSTemplateTag<HtmlTemplate> = importMember "https://unpkg.com/lit-html?module"

See in REPL

@alfonsogarciacaro
Copy link
Member

I'm having too much fun with this. This is a full (simple) Elmish app with lit in only 70 lines of code (including adapters): REPL

I guess it should be also simple to convert Lit templates into React components for users who want to integrate components written in HTML into their apps. Now we just need an editor that can recognize HTML in F# code 😸

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Aug 20, 2021

The clock sample with Lit.

@texastoland
Copy link

texastoland commented Aug 20, 2021

A nice side-effect is this makes it easier to import tags as values

This is exactly what I was trying to accomplish! Static methods weren't working at all. Should I PR delegate pros/cons in the FFI doc?

The clock sample with Lit.

Haha this is my fave. Is there an advantage or convention for:

let create (tag: JsTag<'T>) : Tag<'T> =
  fun fmt ->

Compared to let create (tag: JsTag<'T>) (fmt: FormattableString) : Tag<'T> = in Fable?

The beauty of ECMA interpolated strings for DSLs is you can embed anything from event listeners to looping over child components. That isn't possible with type providers right? I'm liking Feliz and even its JSX interop but just thinking to myself.

PS my original use case was this i18n toolkit.

@alfonsogarciacaro
Copy link
Member

Should I PR delegate pros/cons in the FFI doc?

That'd be absolutely great :)

That isn't possible with type providers right?

Well, with type providers you can generate new methods on-the-fly (they've even been used to create games) so maybe it could be possible to chain the template parts and the arguments (and make them typed), but definitely interpolated strings are a better fit here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants