{"id":19049294,"url":"https://github.com/cppforlife/viewlet","last_synced_at":"2025-02-22T00:17:03.945Z","repository":{"id":3969574,"uuid":"5064053","full_name":"cppforlife/viewlet","owner":"cppforlife","description":"view components","archived":false,"fork":false,"pushed_at":"2012-07-28T19:27:13.000Z","size":144,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-03-14T21:23:49.810Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cppforlife.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}},"created_at":"2012-07-16T07:06:17.000Z","updated_at":"2013-11-30T19:30:51.000Z","dependencies_parsed_at":"2022-09-16T17:40:49.795Z","dependency_job_id":null,"html_url":"https://github.com/cppforlife/viewlet","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cppforlife%2Fviewlet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cppforlife%2Fviewlet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cppforlife%2Fviewlet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cppforlife%2Fviewlet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cppforlife","download_url":"https://codeload.github.com/cppforlife/viewlet/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240105469,"owners_count":19748465,"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":[],"created_at":"2024-11-08T23:10:27.140Z","updated_at":"2025-02-22T00:17:03.925Z","avatar_url":"https://github.com/cppforlife.png","language":"Ruby","readme":"## Goals\n\n* to ease creation of view components\n\n  Problem: Most likely your site has few similar view structures that\n  are repeated throughout the site (e.g. *list* of members, groups, etc.).\n  One solution is to refactor such code into a shared partial\n  (may be `_list_section.html.haml`) and pass customization options\n  via locals hash; however, with this approach it can become quite\n  challenging/inelegant to apply customizations.\n\n* to organize HTML/JS/CSS files based on a feature rather than file type\n\n  Problem: As soon as you start extracting reusable view components\n  from your pages it becomes weird to have HTML/CSS/JS component files\n  spread out in three different directories. Turning your component into a\n  gem remedies that problem since gems can have separate `assets`\n  directory; however, I don't see a benefit in making every single\n  component into a gem, especially when it's application specific.\n\n## Installation\n\n```ruby\ngem \"viewlet\", :git =\u003e \"https://github.com/cppforlife/viewlet.git\"\n```\n\n## Usage\n\nLet's say we have `GroupsController#show` that lists group members.\nHere is how `show.html.haml` could look:\n\n```haml\n%h1= \"Group: #{@group.name}\"\n%p= @group.description\n\n= viewlet(:list_section) do |s|\n  - s.heading \"Group members\"\n  - s.empty_description \"No members in this group\"\n\n  - s.collapse_button false\n  - s.add_button do\n    = link_to \"Invite Members\", new_group_member_path(@group)\n\n  - s.items @group.members\n\n  - s.row_title do |member|\n    .name= member.name\n    .summary= member.summary\n\n  - s.row_details do |member|\n    = render :partial =\u003e \"some_other_partial\", :locals =\u003e {:member =\u003e member}\n```\n\nNow let's define list_section viewlet. Viewlets live in `app/viewlets`\nand each one must have at least `\u003cname\u003e.html.haml`.\n\nIn `app/viewlets/list_section/list_section.html.haml`:\n\n```haml\n.list_section\n  %h2\n    = heading\n\n    - if add_button\n      %small= add_button\n\n    - if collapse_button\n      %small.collapse_button= link_to \"Collapse\", \"#\"\n\n  - if items.empty?\n    - # outputs value regardless being defined as an argument-less block or a plain value\n    %p= empty_description\n\n  - else\n    %ul\n      - items.each do |item|\n        %li{:class =\u003e cycle(\"odd\", \"even\", :name =\u003e :list_section)}\n          .left= list_section.row_title(item)\n\n          - # alternative way of capturing block's content\n          .right= capture(item, \u0026row_details)\n```\n\nAll viewlet options (heading, add_button, etc.) set in `show.html.haml`\nbecome available in `list_section.html.haml` as local variables. None of\nthose options are special and you can make up as many as you want.\n\nNote: If there aren't CSS or JS files you want to keep next to your viewlet\nHTML file you don't need to create a directory for each viewlet; simply\nput them in `app/viewlets` e.g. `app/viewlets/list_section.html.haml`.\n\n### Special HAML syntax\n\nIf you are using HAML you can use special syntax to output a viewlet:\n\n```haml\n%list_section_viewlet # viewlet name suffixed with '_viewlet'\n  heading \"Group members\"\n  empty_description \"No members in this group\"\n\n  collapse_button false\n  add_button do\n    = link_to \"Invite Members\", new_group_member_path(@group)\n\n  items @group.members\n\n  row_title do |member|\n    .name= member.name\n    .summary= member.summary\n\n  row_details do |member|\n    = render :partial =\u003e \"some_other_partial\", :locals =\u003e {:member =\u003e member}\n\n%password_strength_viewlet{:levels =\u003e %w(none weak good)}\n\n%password_strength_viewlet\n  - levels %w(none weak good) # notice optional dash at the beginning\n```\n\n### CSS \u0026 JS\n\nYou can also add other types of files to `app/viewlets/list_section/`.\nIdea here is that your viewlet is self-contained and\nencapsulates all needed parts - HTML, CSS, and JS.\n\nIn `app/viewlets/list_section/plugin.css.scss`:\n\n```scss\n.list_section {\n  width: 300px;\n\n  ul {\n    margin: 0;\n  }\n\n  li {\n    border: 1px solid #ccc;\n    margin-bottom: -1px;\n    padding: 10px;\n    list-style-type: none;\n    overflow: hidden;\n  }\n\n  .left {\n    float: left;\n  }\n\n  .right {\n    float: right;\n  }\n}\n```\n\nTo include list_section viewlet CSS in your application add\n\n    *= require list_section/plugin\n\nto your `application.css`\n\nIn `app/viewlets/list_section/plugin.js`:\n\n```javascript\n// Probably define listSection() jQuery plugin\n```\n\nTo include list_section viewlet JS in your application add\n\n```javascript\n//= require list_section/plugin\n```\n\nto your `application.js`\n\n## Tips\n\n* You do not have to provide a block to `viewlet`:\n\n```haml\n= viewlet(:password_strength)\n```\n\n* You can use hash syntax (and block syntax):\n\n```haml\n= viewlet(:password_strength, :levels =\u003e %w(none weak good))\n\n= viewlet(:password_strength, :levels =\u003e %w(none weak good)) do |ps|\n  - ps.levels %w(none weak good excellent) # overrides levels\n```\n\n* Let's say we decide to make our list_section viewlet use\nthird-party list re-ordering library (e.g. `orderable-list.js`).\nYou can add `orderable-list.js` javascript file to\n`app/viewlets/list_section` and require it from `plugin.js`:\n\n```javascript\n//= require ./orderable-list\n```\n\n* Let's say our `plugin.js` defined jQuery plugin `listSection`\nso that in our `application.js` we can do something like this:\n\n```javascript\n$(document).ready(function(){\n  $(\".list_section\").listSection();\n});\n```\n\nThis is fine; however, that means that our component is not\nreally functional until we add that javascript piece somewhere.\nAlternatively you can put it right after HTML so everytime\nlist_section is rendered it will be automatically initialized.\n\nFor example in `list_section.html.haml`:\n\n```haml\n.list_section{:id =\u003e unique_id}\n  %h2= heading\n  ...\n\n- unless defined?(no_script)\n  :javascript\n    $(document).ready(function(){\n      $(\"##{unique_id}\").listSection();\n    });\n```\n\nEvery viewlet has a predefined local variable `unique_id`\nthat could be used as HTML id.\n\n* It's trivial to subclass `Viewlet::Base` to add new functionality.\n`class_name` option lets you set custom viewlet class:\n\n```haml\n= viewlet(:list_section, {}, :class_name =\u003e \"CustomListSectionViewlet\") do\n  ...\n```\n\n## Todo\n\n* come up with a better name for main files - *plugin* doesn't sound that good\n* `lib/viewlets/` as fallback viewlet lookup path\n* automatically load custom Viewlet::Base subclass from `some_viewlet/plugin.rb`\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcppforlife%2Fviewlet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcppforlife%2Fviewlet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcppforlife%2Fviewlet/lists"}