{"id":14955765,"url":"https://github.com/loomchild/theo-rails","last_synced_at":"2025-05-11T03:59:08.636Z","repository":{"id":244666168,"uuid":"815922433","full_name":"loomchild/theo-rails","owner":"loomchild","description":"Theo is a small and elegant HTML-like template language for Ruby on Rails, featuring natural partials and computed attributes.","archived":false,"fork":false,"pushed_at":"2025-03-16T15:02:22.000Z","size":93,"stargazers_count":27,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-11T02:51:19.175Z","etag":null,"topics":["alpinejs","erb-template","html","ruby-gem","ruby-on-rails","tailwindcss","vuejs"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loomchild.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-16T14:39:17.000Z","updated_at":"2025-05-04T10:58:22.000Z","dependencies_parsed_at":null,"dependency_job_id":"f1df47bc-a71b-42c6-b3cb-3be2ef48631e","html_url":"https://github.com/loomchild/theo-rails","commit_stats":{"total_commits":53,"total_committers":1,"mean_commits":53.0,"dds":0.0,"last_synced_commit":"c0480ab5441a94703777d9c6321d5817775ad8b6"},"previous_names":["loomchild/theo-rails"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loomchild%2Ftheo-rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loomchild%2Ftheo-rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loomchild%2Ftheo-rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loomchild%2Ftheo-rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loomchild","download_url":"https://codeload.github.com/loomchild/theo-rails/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253509775,"owners_count":21919588,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpinejs","erb-template","html","ruby-gem","ruby-on-rails","tailwindcss","vuejs"],"created_at":"2024-09-24T13:11:41.693Z","updated_at":"2025-05-11T03:59:08.619Z","avatar_url":"https://github.com/loomchild.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Gem Version](https://badge.fury.io/rb/theo-rails.svg)](https://badge.fury.io/rb/theo-rails)\n\n# Theo\nTheo is a small and elegant HTML-like template language for Ruby on Rails, featuring natural partials and computed attributes.\n\n\u003e [!WARNING]\n\u003e Please note that this software is still experimental - use at your own risk.\n\n\n## Introduction\n\nThanks to Hotwire, it's now possible to build sophisticated server-rendered user interfaces in Ruby on Rails. However, ERB, Rails' most popular template language, has unintuitive partial syntax, especially for those used to working with Vue.js or React components.\n\nWith Theo, you can render a partial using HTML-like syntax:\n```html\n\u003c_button size=\"large\" label%=\"label\" /\u003e\n```\n\n\u003e [!IMPORTANT]\n\u003e For rendering partials, you can also use PascalCase:\n\u003e ```html\n\u003e \u003cButton size=\"large\" label%=\"label\" /\u003e\n\u003e ```\n\n\n## Getting started\n\n### Install\n\nRun\n\n    bundle add theo-rails\n\n\u003e [!NOTE]\n\u003e If you are using TailwindCSS version 3, add `.theo` extension to the `content` key in your `tailwind.config.js`: `./app/views/**/*.{erb,haml,html,slim,theo}`.\n\n### Try\n\nCreate a new view named `hello.html.theo` (note the `.theo` suffix), with the following content:\n```html\n\u003cspan style%=\"'background-color: ' + 'yellow'\"\u003eHello from Theo!\u003c/span\u003e\n```\n\nVisit the URL corresponding to this view and you should see a highlighed text.\n\n\n## Syntax\n\n\n### Computed attributes\n\nComputing attribute value in ERB feels awkward because angle brackets `\u003c\u003e` clash with the surrounding HTML tag.\n\nIn Theo, an attribute with computed value can be expressed using `%=`. For example:\n```html\n\u003ca href%=\"root_path\"\u003eHome\u003c/a\u003e\n```\nis equivalent to:\n```erb\n\u003ca href=\"\u003c%= root_path %\u003e\"\u003eHome\u003c/a\u003e\n```\n\u003e [!TIP]\n\u003e Computed attributes work with partials as well as standard HTML tags.\n\n#### Short form\n\nIf value of a dynamic attribute is the same as its name, you can omit the value.\n\nFor example:\n```html\n\u003cimg src%\u003e\n```\nis equivalent to:\n```erb\n\u003cimg src%=\"src\"\u003e\n```\nwhich in turn is equivalent to:\n```erb\n\u003cimg src=\"\u003c%= src %\u003e\"\u003e\n```\n\n\u003e [!NOTE]\n\u003e Since reserved ruby keywords such as `class` can't be used as variable names but still can be passed as locals to a partial, they are converted to the following:\n\u003e ```erb\n\u003e \u003cdiv class=\"\u003c%= binding.local_variable_get('class') %\u003e\"\u003eText\u003c/div\u003e\n\u003e ```\n\n### Partials\n\nRendering a partial in ERB requires context-switching between HTML markup and Ruby code, and the `render` verb makes it difficult to imagine a page as a component structure.\n\nIn Theo, you render a partial by writing a tag with '_' prefix, followed by kebab-cased partial name, for example:\n```html\n\u003c_special-button size=\"large\" /\u003e\n```\nis equivalent to:\n```erb\n\u003c%= render 'special_button', size: 'large' %\u003e\n```\n\n\u003e [!TIP]\n\u003e Alternatively, you can also use PascalCase, for example:\n\u003e ```html\n\u003e \u003cSpecialButton size=\"large\" /\u003e\n\u003e ```\n\u003e The benefit is that this form is recognized as valid HTML by most parsers.\n\nNaturally, partials can also include content, e.g.:\n```html\n\u003c_button size=\"large\"\u003e\n  Create\n\u003c/_button\u003e\n```\n\n\u003e [!TIP]\n\u003e Rendered partials can be implemented in ERB, Theo or any other template language.\n\n#### Boolean partial attributes\n\nIf a partial attribute has no value, you can omit it, for example:\n```html\n\u003c_button disabled /\u003e\n```\nis equivalent to:\n```html\n\u003c_button disabled=\"\" /\u003e\n```\n\n\n### Special attributes\n\nSpecial attributes always start with `%` and their value is always dynamic.\n\n\n#### Collections\n\nYou can render a collection of partials using `%collection` special attribute:\n```html\n\u003c_widget %collection=\"widgets\" /\u003e\n```\nwhich is equivalent to:\n```erb\n\u003c%= render partial: 'widget', collection: widgets %\u003e\n```\n\nYou can also customize the local variable name via the `%as` special attribute, e.g.:\n```html\n\u003c_widget %collection=\"widgets\" %as=\"item\" /\u003e\n```\n\n#### Path\n\nTo render a partial from another folder, use the `%path` special attribute, e.g.:\n```html\n\u003c_widget %path=\"widgets\" /\u003e\n```\nis equivalent to:\n```erb\n\u003c%= render 'widgets/widget' %\u003e\n```\n\n#### Yields\n\nPartials can yield a value, such as a builder object that can be used by child partials. For example:\n```html\n\u003c_widget-builder %yields=\"widget\"\u003e\n  \u003c_widget-element widget%=\"widget\" /\u003e\n\u003c/_widget-builder\u003e\n```\nis equivalent to:\n```erb\n\u003c%= render 'widget_builder' do |widget| %\u003e\n  \u003c%= render 'widget_element', widget: %\u003e\n\u003c% end %\u003e\n```\n\n#### Conditionals\n\nYou can omit the tag conditionally using `%if` special attribute:\n```\n\u003cspan %if=\"content\"\u003e\u003c%= content %\u003e\u003c/span\u003e\n```\nis equivalent to:\n```erb\n\u003c% if content %\u003e\n  \u003cspan\u003e\u003c%= content %\u003e\u003c/span\u003e\n\u003c% end %\u003e\n```\n\nIt also works with partials, so this will work as expected:\n```\n\u003c_special-button %if=\"count \u003e 3\" size=\"large\" /\u003e\n```\n\n\u003e [!NOTE]\n\u003e Conditionals can't be applied to nested tags (e.g. `div` in `div`). Please use ERB conditions in such cases. \n\n\n### ERB backwards compatibility\n\nYou can freely mix ERB and Theo syntax, e.g.:\n```erb\n\u003c% if total_amount \u003e 100 %\u003e\n  \u003c_free-shipping amount%=\"total_amount\" /\u003e\n\u003c% end %\u003e\n```\n\n### Utilities\n\n#### Merging `class` and `style` attributes\nYou can specify both static and dynamic version of `class` and `style` attribute on a tag, and they will be merged.\n\nFor example:\n```html\n\u003cdiv class% class=\"big\" style% style=\"color: red\"\u003eText\u003c/div\u003e\n```\nis equivalent to:\n```erb\n\u003cdiv class=\"\u003c%= binding.local_variable_get('class').to_s + ' big' %\u003e\" style=\"\u003c%= style.to_s + '; color: red' %\u003e\"\u003eText\u003c/div\u003e\n```\n\nThis is especially useful when you want to apply a `class` and `style` attribute to a partial root and merge the dynamic local with default value. For example, if you have the following partial:\n```erb\n\u003c%# locals: (class: nil) -%\u003e\n\u003cbutton class% class=\"big\"\u003eButton\u003c/button\u003e\n```\nthat is used as follows:\n```html\n\u003c_button class=\"blue\" /\u003e\n```\nit will render:\n```html\n\u003cbutton class=\"big blue\"\u003eButton\u003c/button\u003e\n```\n\n#### `provide` and `inject` helpers\n\nInstead of using `%yields` attribute, a parent partial can indirectly pass a variable to its children using the `provide` and `inject` helpers. The example above can be modified as follows:\n```html\n\u003c_widget-builder\u003e\n  \u003c_widget-element /\u003e\n\u003c/_widget-builder\u003e\n```\n\n`_widget_builder.html.theo`:\n```erb\n\u003c% provide(widget:) do %\u003e\n  \u003c%= yield %\u003e\n\u003c% end %\u003e\n```\n\n`_widget_element.html.theo`:\n```erb\n\u003c% widget = inject(:widget) %\u003e\n```\n\n\u003e [!NOTE]\n\u003e This technique is used by [form partials](#form-partials). Use it sparingly, as implicit variables can reduce code readability. \n\n\n#### Forms\n\nYou can build a `\u003cform\u003e` element in ERB using [ActionView form helpers](https://guides.rubyonrails.org/form_helpers.html). Theo provides corresponding partials. For example:\n```html\n\u003c_form-with model%=\"widget\" data-turbo-confirm=\"Are you sure?\"\u003e\n  \u003cdiv\u003e\n    \u003c_label name=\"name\" /\u003e\n    \u003c_text-field name=\"name\" /\u003e\n  \u003c/div\u003e\n\n  \u003cdiv\u003e\n    \u003c_label name=\"size\" /\u003e\n    \u003c_select name=\"size\" options%=\"['Big', 'Small']\" /\u003e\n  \u003c/div\u003e\n\n  \u003c_submit value=\"Create\" /\u003e\n\u003c/_form-with\u003e\n```\nis equivalent to:\n```erb\n\u003c%= form_with model: widget, data: { turbo_confirm: 'Are you sure?' } do |form| %\u003e\n  \u003cdiv\u003e\n    \u003c%= form.label :name %\u003e\n    \u003c%= form.text_field :name %\u003e\n  \u003c/div\u003e\n\n  \u003cdiv\u003e\n    \u003c%= form.label :size %\u003e\n    \u003c%= form.select :size, ['Big', 'Small'] %\u003e\n  \u003c/div\u003e\n\n  \u003c%= form.submit \"Create\" %\u003e\n\u003c% end %\u003e\n```\n\n## ViewComponents\n\nTheo is compatible with [ViewComponent](https://viewcomponent.org/) framework.\n\nHere's a component using Theo template syntax:\n\n```\nclass ButtonComponent \u003c ViewComponent::Base\n  theo_template \u003c\u003c-THEO\n    \u003cspan class%=\"@size\"\u003e\u003c%= content %\u003e\u003c/span\u003e\n  THEO\n\n  def initialize(size:)\n    @size = size\n  end\nend\n```\n\nIf a components exists, and you use PascalCase syntax, Theo automatically renders it instead of a partial. Therefore:\n```html\n\u003cButton size=\"large\" /\u003e\n```\nis equivalent to:\n```erb\n\u003c%= render(ButtonComponent.new(size: \"large\")) %\u003e\n```\n\nComponents can yield a value:\n```html\n\u003cButton size=\"large\" %yields=\"component\"\u003e\n  \u003c% component.with_header do %\u003eIcon\u003c% end %\u003e\n  Create\n\u003c/Button\u003e\n```\n\nYou can also render a component collection as follows:\n```html\n\u003cWidget %collection=\"widgets\" /\u003e\n```\nwhich is equivalent to:\n```erb\n\u003c%= render WidgetComponent.with_collection(widgets) %\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floomchild%2Ftheo-rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floomchild%2Ftheo-rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floomchild%2Ftheo-rails/lists"}