mayu-live / framework

Mayu is a live updating server-side component-based VDOM rendering framework written in Ruby
https://mayu.live
GNU Affero General Public License v3.0
130 stars 4 forks source link

Use HAML #21

Closed aalin closed 1 year ago

aalin commented 1 year ago

JSX is great, but I remember when first learning React that I thought it would have been nicer to use HAML... HAML is pretty common in the Ruby world and it's quite nice to work with. Today I found a HAML parser that returns an AST, https://github.com/ruby-syntax-tree/syntax_tree-haml

That parser can be used to transform this HAML code:

%Section.container.asd
  %h1= post.title
  %h2= post.subtitle
  .lol(class="lol2")
  .hopp{class: hopp2}
  .content
    = post.content
  %ul.items
    = items.map do |item|
      %li= item

into this ruby code:

h(Section,
  h(:h1, (post.title)),
  h(:h2, (post.subtitle)),
  h(:div, class: styles[:lol, :lol2]),
  h(:div, class: styles[:hopp], **{class: hopp2}),
  h(:div,
    (post.content), class: styles[:content]),
  h(:ul,
    (items.map do |item|
      h(:li, (item))
    end), class: styles[:items]), class: styles[:container, :asd])

I think HAML would be a really good choice for templating... Could maybe use this naming:

Some pros using HAML:

Here's a more complete example of a pagination component. Not sure if it would work but it looks pretty cool

:ruby
  Fieldset = require("/app/components/Form/Fieldset")
  Button = require("/app/components/Form/Button")

  def pagination_window(current_page:, total_pages:, window_size: 5)
    half_window_size = (window_size - 1) / 2
    first = current_page - half_window_size.ceil
    last = current_page + half_window_size.floor

    case
    when first < 1
      1..[total_pages, window_size].min
    when last >= total_pages
      [total_pages + first - last, 1].max..total_pages
    else
      first..last
    end
  end

:ruby
  props => {
    page:,
    per_page:,
    total_pages:,
  }

  prev_page_link = page > 1 && "?page=#{page.pred}"
  next_page_link = page <= props[:total_pages] && "?page=#{page.succ}"

  pages = pagination_window(
    current_page: page,
    total_pages: props[:total_pages].succ,
    window_size: props[:window_size] || 5,
  )

%Fieldset
  %legend
    Page #{page} of #{total_pages.succ}, showing #{per_page} per page

  .wrap
    %nav.buttons(aria-label="pagination")
      %a.button(rel="prev" href="#{prev_page_link}")
        Previous page

      %ul.pages
        = pages.map do |num|
          %li{key: num}
            %a.page{href: "?page=#{num}", aria_current: (page == num && "page")}
              {num}

      %a.button(rel="next" href="#{next_page_link}")
        Next page

    .perPage
      Per page:
      %select{on_change: props[:on_change_per_page], value: per_page}
        = [20, 40, 80].map do |value|
          %option{key: value, value: value}= value

And this would generate the following Ruby code:

Fieldset = require("/app/components/Form/Fieldset")
Button = require("/app/components/Form/Button")

def pagination_window(current_page:, total_pages:, window_size: 5)
  half_window_size = (window_size - 1) / 2
  first = current_page - half_window_size.ceil
  last = current_page + half_window_size.floor

  case
  when first < 1
    1..[total_pages, window_size].min
  when last >= total_pages
    [total_pages + first - last, 1].max..total_pages
  else
    first..last
  end
end

def render
  props => {
    page:,
    per_page:,
    total_pages:,
  }

  prev_page_link = page > 1 && "?page=#{page.pred}"
  next_page_link = page <= props[:total_pages] && "?page=#{page.succ}"

  pages = pagination_window(
    current_page: page,
    total_pages: props[:total_pages].succ,
    window_size: props[:window_size] || 5,
  )

  h(Fieldset,
    h(:legend,
      ("Page #{page} of #{total_pages.succ}, showing #{per_page} per page")),
    h(:div,
      h(:nav,
        h(:a,
          "Previous page", class: styles[:button], rel: "prev"),
        h(:ul,
          (pages.map do |num|
            h(:li,
              h(:a,
                "{num}", class: styles[:page], **{href: "?page=#{num}", aria_current: (page == num && "page")}), **{key: num})
          end), class: styles[:pages]),
        h(:a,
          "Next page", class: styles[:button], rel: "next"), class: styles[:buttons], aria-label: "pagination"),
      h(:div,
        "Per page:",
        h(:select,
          ([20, 40, 80].map do |value|
            h(:option, (value), **{key: value, value: value})
          end), **{on_change: props[:on_change_per_page], value: per_page}), class: styles[:perPage]), class: styles[:wrap]))
end

It would be cool to auto-resolve components, so that:

%Form::Fieldset
  %legend Hello

would translate to:

Fieldset = require('/app/components/Form/Fieldset')
h(Fieldset,
  h(:legend, ("Hello"))
)

or something... %./Pagination could maybe be used to refer to relative paths or something... %.Foo::Bar maybe for require("./Foo/Bar")?