-
Notifications
You must be signed in to change notification settings - Fork 309
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
Comments
I think this is a tagged interpolated string: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals |
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:
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 |
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. |
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 |
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. |
lit-element also uses interpolated strings and really lends itself to be used functionally |
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 |
Some support for |
@alfonsogarciacaro Would it be possible to add 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? |
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 |
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)
|
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" |
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 😸 |
The clock sample with Lit. |
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?
Haha this is my fave. Is there an advantage or convention for: let create (tag: JsTag<'T>) : Tag<'T> =
fun fmt -> Compared to 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. |
That'd be absolutely great :)
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. |
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.)
The text was updated successfully, but these errors were encountered: