Feliz: Sintaxe fluente alternativa

Criado em 9 out. 2019  ·  13Comentários  ·  Fonte: Zaid-Ajaj/Feliz

O que você acha de uma sintaxe como essa? (Eu esbocei isso rapidamente; pode haver muito espaço para melhorias - por exemplo, pode haver uma maneira de eliminar .reactElement no final de cada componente quando uma reflexão mais cuidadosa é dada a isso.)

`` `f #
AppBar ()
.position.absolute
.crianças([
Tipografia()
.variant.h6
.color.primary
.text ("Foo")
.reactElement
])
.reactElement

Benefits: Supports inheritance, and puts a final end to any discoverability issues still left in Feliz: No more hunting for the correct prop type; you're just dotting through and seeing what's available. Also supports overloaded props and enum props just like Feliz; we have basically just replaced property lists with "fluent builders".

Drawbacks: I don't know which impacts this has for bundle size or performance (see quick and dirty implementation below). Also it's "unusual" as far as Fable goes (no prop lists). Not that the latter matters by itself. **Update:** See more challenges here: https://github.com/Zaid-Ajaj/Feliz/issues/54#issuecomment-541658081

I'm not saying we should necessarily do this; I just wanted to get this out of my head and post it for discussion.

For the record, below is the implementation of the above syntax. I have not made any attempts to inline or erase stuff. And there's a bit of type trickery which would likely cause OO fanatics to spin in their graves (what with subtypes inheriting base types parametrized by the subtype and all).

```f#
open Fable.Core.JsInterop
open Fable.React

let reactElement (el: ReactElementType) (props: 'a) : ReactElement =
  import "createElement" "react"


[<AbstractClass>]
type PropsBase() =
  abstract ElementType : ReactElementType
  member internal __.Props = ResizeArray<string * obj>()
  member internal this.reactElement =
    reactElement this.ElementType (createObj this.Props)


[<AbstractClass>]
type PropsBase<'a>() =
  inherit PropsBase()

  member this.custom (key: string) (value: obj) =
    this.Props.Add(key, value)
    this |> unbox<'a>

  member this.children (elems: seq<ReactElement>) =
    // should also call React.Children.ToArray
    this.custom "children" elems

  member this.text (text: string) =
    this.custom "children" text


type AppBarPosition<'a when 'a :> PropsBase<'a>>(parent: 'a) =
  member __.absolute = parent.custom "position" "absolute"
  member __.relative = parent.custom "position" "relative"


type AppBar() =
  inherit PropsBase<AppBar> ()
  member this.position = AppBarPosition(this)
  override __.ElementType = importDefault "@material-ui/core/AppBar"


type TypographyVariant<'a when 'a :> PropsBase<'a>>(parent: 'a) =
  member __.h5 = parent.custom "variant" "h5"
  member __.h6 = parent.custom "variant" "h6"

type TypographyColor<'a when 'a :> PropsBase<'a>>(parent: 'a) =
  member __.primary = parent.custom "color" "primary"
  member __.secondary = parent.custom "color" "secondary"


type Typography() =
  inherit PropsBase<Typography> ()
  member this.variant = TypographyVariant(this)
  member this.color = TypographyColor(this)
  override __.ElementType = importDefault "@material-ui/core/Typography"

Todos 13 comentários

Se .reactElement fosse embora, a sintaxe seria OK por si só, mas pareceria diferente quando usada com outras associações de terceiros, mas acho que os usuários podem pelo menos conviver com isso:

Html.div [
  Mui.appBar()
    .position.absolute
    .children([
      Mui.typography()
        .variant.h6
        .color.primary
        .text("Foo")
    ])
]

Mas ainda acho que duplicar as propriedades básicas seria o mais fácil de fazer. Não acho que você precise duplicar todas as propriedades porque muitas vezes você não as usará, apenas duplique aquelas que provavelmente serão usadas: id , key , className , style , children e voltando para prop para os casos muito raros. O que você acha?

Continuando a discussão sobre herança em https://github.com/cmeeren/Feliz.MaterialUI/issues/20, uma vez que este problema é apenas sobre a sintaxe alternativa, que é uma consideração por si própria separada de minhas lutas com herança (desculpe se isso não estava claro).

Acho que a sintaxe fluente tem outra desvantagem, por exemplo, se eu quiser criar um controle extensível, posso usar o rendimento! para acrescentar novos adereços. Ou posso criar caixas de união para adereços limitados e reduzir para obter os adereços básicos e produzi-los em qualquer lugar que eu quiser. Mas, com uma sintaxe fluente, só posso acrescentar adereços no final.

let myButton props =
    Html.button [
        prop.Classes [ ... ]
        prop.Style [....]
        yield! props
    ]

@albertwoo , não tenho certeza do que você quer dizer com

Ou posso criar caixas de união para adereços limitados e reduzir para obter os adereços básicos e produzi-los em qualquer lugar que eu quiser.

Mas não há problema em adicionar suporte para adereços arbitrários usando a sintaxe fluente:

f# let myButton props = Html.button .classes([...]) .style([...]) .custom(props)

onde custom pode ter sobrecargas aceitando qualquer coisa que quisermos, realmente:

  • string * obj
  • O tipo de adereço Feliz (para interoperabilidade)
  • O tipo de prop Fable.React (para interoperabilidade)
  • Seqüências de qualquer um dos anteriores

Em qualquer caso, para componentes extensíveis com a sintaxe fluente, você provavelmente não passaria adereços como esse. Você faria algo assim:

`` `f #
deixe meuBotão () =
Html.button
.Aulas([...])
.estilo([...])

And then use it like this:

```f#
myButton()
  .text("foo")

Eu meio que gosto do estilo da lista de adereços. No meu código, fiz algo como:

type ISimpleFieldProp = interface end

[<RequireQualifiedAccess>]
type SimpleFieldProp =
  | Label of string
  | Errors of string list 
  | OuterClasses of string list
  | LabelClasses of string list
  | ErrorClasses of string list
  | FieldView of ReactElement
  | OuterAttrs of IHTMLProp list
  interface ISimpleFieldProp

let simpleField (props: ISimpleFieldProp list) =
    let props = props |> List.choose (function :? SimpleFieldProp as x -> Some x | _ -> None)
    let label             = props |> UnionProps.tryLast (function SimpleFieldProp.Label x -> Some x | _ -> None)
    let errorClasses      = props |> UnionProps.tryLast (function SimpleFieldProp.ErrorClasses x -> Some x | _ -> None) |> Option.defaultValue []
    let outerClasses      = props |> UnionProps.concat (function SimpleFieldProp.OuterClasses x -> Some x | _ -> None)
    let labelClasses      = props |> UnionProps.concat (function SimpleFieldProp.LabelClasses x -> Some x | _ -> None)

    div </> [
      yield! props |> UnionProps.concat (function SimpleFieldProp.OuterAttrs x -> Some x | _ -> None)
      match outerClasses with
        | [] -> Style [ Margin "5px"; Padding "5px" ]
        | cs -> Classes cs
      Children [
        if label.IsSome then fieldLabel labelClasses label.Value 

        match props > UnionProps.tryLast (function SimpleFieldProp.FieldView x -> Some x | _ -> None) with
          | Some x -> x
          | None    -> ()

        yield! 
          props 
          |> UnionProps.concat (function SimpleFieldProp.Errors x -> Some x | _ -> None)
          |> fieldError errorClasses
      ]
    ]

Com UnionProps.concat posso mesclar todas as coisas como IHTMLProp, string de classe, estilo, etc. e alimentar o Fable.React. Então posso usá-lo como:

let simpleField1 props =
   simpleField [
      SimpleFieldProp.OuterClasses [ ... ]
   ]
let simpleField2 props =
   simpleField1 [
      SimpleFieldProp.OuterClasses [ ... ]
   ]

e todas as OuterClasses podem se fundir. Claro, se eu quiser mesclar, posso apenas usar UnionProps.tryLast.

Obrigado pelo exemplo detalhado. Devo admitir que me deixou confuso. Por exemplo, parece que você não está usando Feliz, mas sim Fable.React. Portanto, estou tendo dificuldade em entender como isso é relevante para esta discussão.

Além disso, mesclar valores de prop para os mesmos props de várias classes "herdadas" / compostas me parece uma maneira desnecessariamente confusa de fazer extensibilidade, visto que na sintaxe baseada em React / JSX (AFAIK) um componente pode ter apenas uma ocorrência de qualquer prop. .

Parece que o que você está tentando fazer é definir um componente customizado que suporte um punhado de props customizados opcionais, alguns dos quais são props e alguns dos quais são elementos filhos.

Em Feliz (como está agora, sem nenhuma sintaxe fluente), logo de cara, posso resolver assim (implementação parcial; espero que o resto dos adereços estejam claros):

`` `f #
feliz aberto

digite Html com

membro simpleField (
? label: string,
? erros: lista de strings,
? labelClasses: lista de strings,
? outerClasses: lista de strings
? props: IReactProperty list) =
// Não tenho certeza se isso está correto, pois não sei o que UnionProps.concat realmente
// faz, mas tenho certeza que você entendeu
let lbClasses = labelClasses |> Option.defaultValue []

Html.div [
  yield! props |> Option.defaultValue []
  outerClasses |> Option.map prop.classes |> Option.defaultValue (prop.style [ ... ])
  prop.children [
    match label with Some lb -> fieldLabel lbClasses lb | None -> ()
    ...
  ]
]
This also allows you to easily make any of the props required, if you desire.

As regards the fluent syntax under discussion, it would work the same way.

The usage of the example above would be:

```f#
Html.simpleField(
  label = "foo",
  outerClasses = ["bar"]
)

Observe também que a criação de componentes React personalizados (por exemplo, componentes de função com FunctionComponent.Of Fable.React) pode ser uma ótima maneira de reutilizar.

Sim, não estou usando o Feliz (tentei, mas ainda não há suporte para SSR). Mas eu só quero explicar o que penso sobre o estilo da lista de acessórios.
Sim, para mesclar adereços como classe e estilo seria um pouco bagunçado, mas apenas torna as coisas mais simples para eu escrever meu aplicativo.
Seu exemplo acima é muito parecido com o modo padrão do Fabulous Xamarin (eu também uso esse projeto 😀). Às vezes, não é fácil estender o simpleField. Por exemplo, se eu quiser criar um simpleField2 para ser reutilizado, tenho que repetir os parâmetros de entrada como:

type Html with
   member simpleField2(
      ?label: string,
      ?errors: string list,
      ?labelClasses: string list,
      ?outerClasses: string list
      ?props: IReactProperty list) =
     simpleField(label, errors, labelClasses, [ "bar"; yield! outerClasses ], props)

Agradeço os trabalhos de vocês !!!

:)

Possível solução alternativa:

`` `f #
tipo SimpleFieldProps = {
Rótulo: opção de string
Erros: lista de strings
// ...
} com
membro estático Criar (? label: string,? errors: string list, ..) = {..}

digite Html com
membro simpleField (props: SimpleFieldProps) = // ...
membro simpleField2 (props: SimpleFieldProps) = // ...

Usage:

```f#
Html.simpleField (SimpleFieldProps.create(..))

:) Obrigado pelo conselho. Dessa forma pode ajudar, mas se eu tiver muitos adereços como no Fabulous Xamarin, daria muito trabalho. É também por isso que eu também uso Fabulous.SimpleElements (Obrigado @ Zaid-Ajaj novamente :))

Em relação ao .reactElement : AFAIK, não é realmente um problema, porque podemos ter certeza de que ele só é necessário para usá-lo ao interagir com o código existente que requer reactElement . Caso contrário, propriedades que aceitam ReactElement na API fluente (por exemplo, children ) também podem ter sobrecargas aceitando PropsBase , chamando internamente .reactElement .

No entanto, uma desvantagem é que, uma vez que as listas podem conter apenas um tipo, e não há upcasting automático dentro de uma lista, precisamos de um upcast explícito (por exemplo, para PropsBase ) de qualquer maneira se tivermos mais de um elemento. A menos que haja uma boa solução para isso, podemos muito bem manter reactElement .

Uma possível solução para o acima é não ter listas de elementos filhos, mas usar [<ParamArray>] de PropsBase . Então eu acho que os parâmetros serão atualizados automaticamente. Mas então você precisa de vírgulas entre cada item. (Você também substitui [] por () , mas eu realmente não me importo com isso.)

Também precisa haver uma API para adereços condicionais. No atual Feliz (e Fable.React) DSL, podemos seletivamente yield props, mas com chamadas de prop em cadeia conforme proposto aqui, isso não é possível. Isso é uma questão de design de API, mas não estou convencido de que será bonito, por exemplo, para adereços de enum onde não há parâmetros - então não podemos adicionar um parâmetro bool , o que pode ser uma solução para métodos.

Poderíamos ter um embrulho .conditional(bool, 'builder -> 'ignored) , por exemplo

f# .prop1("foo") .conditional(false, fun p -> p.prop2("bar")) .prop3("baz")

Mas não é a API mais bonita que já vi. (Nada mal, mas a condicional atual yield é IMHO melhor.)

Se eu estivesse escrevendo o DSL em C #, esta é a sintaxe que eu gostaria de escrever com certeza, mas como temos F # e toda sua bela sintaxe de lista, prefiro usar a atual em vez desta

Peguei vocês. A única coisa que realmente gosto nessa sintaxe é que é a mais detectável que já vi até agora. Caso contrário, parece ter muitas desvantagens em comparação com o atual.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

cmeeren picture cmeeren  ·  4Comentários

mastoj picture mastoj  ·  3Comentários

Dzoukr picture Dzoukr  ·  9Comentários

cmeeren picture cmeeren  ·  13Comentários

alfonsogarciacaro picture alfonsogarciacaro  ·  6Comentários