{"id":13877929,"url":"https://github.com/thoughtbot/props_template","last_synced_at":"2025-04-08T06:36:04.753Z","repository":{"id":56888986,"uuid":"372931174","full_name":"thoughtbot/props_template","owner":"thoughtbot","description":"A very fast json builder for Rails","archived":false,"fork":false,"pushed_at":"2025-01-04T03:04:40.000Z","size":962,"stargazers_count":162,"open_issues_count":5,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-01T04:53:01.330Z","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":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thoughtbot.png","metadata":{"files":{"readme":"README.md","changelog":"NEWS.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"thoughtbot"}},"created_at":"2021-06-01T18:49:10.000Z","updated_at":"2025-03-13T08:17:01.000Z","dependencies_parsed_at":"2023-02-18T02:50:14.348Z","dependency_job_id":"3a701ce4-f696-4c71-942c-c8fbaf6e3d73","html_url":"https://github.com/thoughtbot/props_template","commit_stats":{"total_commits":18,"total_committers":1,"mean_commits":18.0,"dds":0.0,"last_synced_commit":"cba0c1c5f824e28a5e5426b79100e4284224aa97"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fprops_template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fprops_template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fprops_template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fprops_template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thoughtbot","download_url":"https://codeload.github.com/thoughtbot/props_template/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247792880,"owners_count":20996891,"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-08-06T08:01:35.200Z","updated_at":"2025-04-08T06:36:04.725Z","avatar_url":"https://github.com/thoughtbot.png","language":"Ruby","funding_links":["https://github.com/sponsors/thoughtbot"],"categories":["Ruby"],"sub_categories":[],"readme":"# PropsTemplate\n\nPropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON. It has\nsupport for Russian-Doll caching, layouts, and can be queried by giving the\nroot a key path.\n\n[![Build\nStatus](https://circleci.com/gh/thoughtbot/props_template.svg?style=shield)](https://circleci.com/gh/thoughtbot/props_template)\n\nIt's fast.\n\nPropsTemplate bypasses the steps of hash building and serializing\nthat other libraries perform by using Oj's `StringWriter` in `rails` mode.\n\n![benchmarks](docs/benchmarks.png)\n\nCaching is fast too.\n\nWhile other libraries spend time unmarshaling,\nmerging hashes, and serializing to JSON; PropsTemplate simply takes\nthe cached string and uses Oj's [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).\n\n## Example:\n\nPropsTemplate is very similar to JBuilder, and selectively retains some\nconveniences and magic.\n\n```ruby\njson.flash flash.to_h\n\njson.menu do\n  json.currentUser do\n    json.email current_user.email\n    json.avatar current_user.avatar\n    json.inbox current_user.messages.count\n  end\nend\n\njson.dashboard(defer: :auto) do\n  sleep 5\n  json.complexPostMetric 500\nend\n\njson.posts do\n  page_num = params[:page_num]\n  paged_posts = @posts.page(page_num).per(20)\n\n  json.list do\n    json.array! paged_posts, key: :id do |post|\n      json.id post.id\n      json.description post.description\n      json.commentsCount post.comments.count\n      json.editPath edit_post_path(post)\n    end\n  end\n\n  json.paginationPath posts_path\n  json.current pagedPosts.current_page\n  json.total @posts.count\nend\n\njson.footer partial: 'shared/footer' do\nend\n```\n\n## Installation\n\n```\ngem 'props_template'\n```\n\nand run `bundle`.\n\nOptionally add the [core ext](#array-core-extension) to an initializer if you\nwant to [dig](#digging) into your templates.\n\n```ruby\nrequire 'props_template/core_ext'\n```\n\n\nAnd create a file in your `app/views` folder like so:\n\n```ruby\n# app/views/posts/index.json.props\n\njson.greetings \"hello world\"\n```\n\nYou can also add a [layout](#layouts).\n\n## API\n\n### json.set! or json.\\\u003cyour key here\\\u003e\n\nDefines the attribute or structure. All keys are not formatted by default. See [Change Key Format](#change-key-format) to change this behavior.\n\n```ruby\njson.set! :authorDetails, {...options} do\n  json.set! :firstName, 'David'\nend\n\n# or\n\njson.authorDetails, {...options} do\n  json.firstName 'David'\nend\n\n\n# =\u003e {\"authorDetails\": { \"firstName\": \"David\" }}\n```\n\nThe inline form defines key and value\n\n| Parameter | Notes |\n| :--- | :--- |\n| key | A json object key|\n| value | A value |\n\n```ruby\n\njson.set! :firstName, 'David'\n\n# or\n\njson.firstName 'David'\n\n# =\u003e { \"firstName\": \"David\" }\n```\n\nThe block form defines key and structure\n\n| Parameter | Notes |\n| :--- | :--- |\n| key | A json object key|\n| options | Additional [options](#options)|\n| block | Additional `json.set!`s or `json.array!`s|\n\n```ruby\njson.set! :details do\n  # ...\nend\n\nor\n\njson.details do\n  # ...\nend\n```\n\nThe difference between the block form and inline form is\n  1. The block form is an internal node. Functionality such as Partials,\n  Deferment and other [options](#options) are only available on the\n  block form.\n  2. The inline form is considered a leaf node, and you can only [dig](#digging)\n  for internal nodes.\n\n### json.extract!\nExtracts attributes from object or hash in 1 line\n\n```ruby\n# without extract!\njson.id user.id\njson.email user.email\njson.firstName user.first_name\n\n# with extract!\njson.extract! user, :id, :email, :first_name\n\n# =\u003e {\"id\" =\u003e 1, \"email\" =\u003e \"email@gmail.com\", \"first_name\" =\u003e \"user\"}\n\n# with extract! with key transformation\njson.extract! user, :id, [:first_name, :firstName], [:last_name, :lastName]\n\n# =\u003e {\"id\" =\u003e 1, \"firstName\" =\u003e \"user\", \"lastName\" =\u003e \"last\"}\n```\n\nThe inline form defines object and attributes\n\n| Parameter | Notes |\n| :--- | :--- |\n| object | An object |\n| attributes | A list of attributes |\n\n### json.array!\nGenerates an array of json objects.\n\n```ruby\ncollection = [ {name: 'john'}, {name: 'jim'} ]\n\njson.details do\n  json.array! collection, {...options} do |person|\n    json.firstName person[:name]\n  end\nend\n\n# =\u003e {\"details\": [{\"firstName\": 'john'}, {\"firstName\": 'jim'} ]}\n```\n\n| Parameter | Notes |\n| :--- | :--- |\n| collection | A collection that optionally responds to `member_at` and `member_by` |\n| options | Additional [options](#options)|\n\nTo support [digging](#digging), any list passed\nto `array!` MUST implement `member_at(index)` and `member_by(attr, value)`.\n\nFor example, if you were using a delegate:\n\n```ruby\nclass ObjectCollection \u003c SimpleDelegator\n  def member_at(index)\n    at(index)\n  end\n\n  def member_by(attr, val)\n    find do |ele|\n      ele[attr] == val\n    end\n  end\nend\n```\n\nThen in your template:\n\n```ruby\ndata = ObjectCollection.new([\n  {id: 1, name: 'foo'},\n  {id: 2, name: 'bar'}\n])\n\njson.array! data do\n  # ...\nend\n```\n\nSimilarly for ActiveRecord:\n\n```ruby\nclass ApplicationRecord \u003c ActiveRecord::Base\n  def self.member_at(index)\n    offset(index).limit(1).first\n  end\n\n  def self.member_by(attr, value)\n    find_by(Hash[attr, val])\n  end\nend\n```\n\nThen in your template:\n\n```ruby\njson.array! Post.all do\n  # ...\nend\n```\n\n#### **Array core extension**\n\nFor convenience, PropsTemplate includes a core\\_ext that adds these methods to\n`Array`. For example:\n\n```ruby\nrequire 'props_template/core_ext'\ndata = [\n  {id: 1, name: 'foo'},\n  {id: 2, name: 'bar'}\n]\n\njson.posts\n  json.array! data do\n    # ...\n  end\nend\n```\n\nPropsTemplate does not know what the elements are in your collection. The\nexample above will be fine for [digging](#digging)\nby index, but will raise a `NotImplementedError` if you query by attribute. You\nmay still need to implement `member_by`.\n\n### json.deferred!\nReturns all deferred nodes used by the [deferment](#deferment) option.\n\n**Note** This is a [SuperglueJS][1] specific functionality and is used in\n`application.json.props` when first running `rails superglue:install:web`\n\n\n```ruby\njson.deferred json.deferred!\n\n# =\u003e [{url: '/some_url?props_at=outer.inner', path: 'outer.inner', type: 'auto'}]\n```\n\nThis method provides metadata about deferred nodes to the frontend ([SuperglueJS][1])\nto fetch missing data in a second round trip.\n\n### json.fragments!\nReturns all fragment nodes used by the [partial fragments](#partial-fragments)\noption.\n\n```ruby json.fragments json.fragments!  ```\n\n**Note** This is a [SuperglueJS][1] specific functionality and is used in\n`application.json.props` when first running `rails superglue:install:web`\n\n## Options\nOptions Functionality such as Partials, Deferments, and Caching can only be\nset on a block. It is normal to see empty blocks.\n\n```ruby\njson.post(partial: 'blog_post') do\nend\n```\n\n### Partials\n\nPartials are supported. The following will render the file\n`views/posts/_blog_posts.json.props`, and set a local variable `post` assigned\nwith @post, which you can use inside the partial.\n\n```ruby\njson.one_post partial: [\"posts/blog_post\", locals: {post: @post}] do\nend\n```\n\nUsage with arrays:\n\n```ruby\n# The `as:` option is supported when using `array!`\n# Without `as:` option you can use blog_post variable (name is based on partial's name) inside partial\n\njson.posts do\n  json.array! @posts, partial: [\"posts/blog_post\", locals: {foo: 'bar'}, as: 'post'] do\n  end\nend\n```\n\nRendering partials without a key is also supported using `json.partial!`, but use\nsparingly! `json.partial!` is not optimized for collection rendering and may\ncause performance problems. It's best used for things like a shared header or footer.\n\nDo:\n\n```ruby\njson.partial! partial: \"header\", locals: {user: @user} do\nend\n```\n\nor\n\n```ruby\njson.posts do\n  json.array! @posts, partial: [\"posts/blog_post\", locals: {post: @post}] do\n  end\nend\n```\n\nDo NOT:\n\n```ruby\n@post.each do |post|\n  json.partial! partial: \"post\", locals: {post: @post} do\n  end\nend\n```\n\n### Partial Fragments\n**Note** This is a [SuperglueJS][1] specific functionality.\n\nA fragment identifies a partial output across multiple pages. It can be used to\nupdate cross cutting concerns like a header bar.\n\n```ruby\n# index.json.props\njson.header partial: [\"profile\", fragment: \"header\"] do\nend\n\n# _profile.json.props\njson.profile do\n  json.address do\n    json.state \"New York City\"\n  end\nend\n```\n\nWhen using fragments with Arrays, the argument **MUST** be a lamda:\n\n```ruby\nrequire 'props_template/core_ext'\n\njson.array! ['foo', 'bar'], partial: [\"footer\", fragment: -\u003e(x){ x == 'foo'}] do\nend\n```\n\n### Caching\nCaching is supported on internal nodes only. This limitation is what makes it\npossible to for props_template to forgo marshalling/unmarshalling and simply\nuse [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).\n\nUsage:\n\n```ruby\njson.author(cache: \"some_cache_key\") do\n  json.firstName \"tommy\"\nend\n\n# or\n\njson.profile(cache: \"cachekey\", partial: [\"profile\", locals: {foo: 1}]) do\nend\n\n# or nest it\n\njson.author(cache: \"some_cache_key\") do\n  json.address(cache: \"some_other_cache_key\") do\n    json.zip 11214\n  end\nend\n```\n\nWhen used with arrays, PropsTemplate will use `Rails.cache.read_multi`.\n\n```ruby\nrequire 'props_template/core_ext'\n\nopts = { cache: -\u003e(i){ ['a', i] } }\n\njson.array! [4,5], opts do |x|\n  json.top \"hello\" + x.to_s\nend\n\n# or on arrays with partials\n\nopts = { cache: (-\u003e(d){ ['a', d.id] }), partial: [\"blog_post\", as: :blog_post] }\n\njson.array! @options, opts do\nend\n```\n\n### Deferment\n\nYou can defer rendering of expensive nodes in your content tree using the\n`defer: :manual` option. Behind the scenes PropsTemplates will no-op the block\nentirely and replace the value with a placeholder. A common use case would be\ntabbed content that does not load until you click the tab.\n\nWhen your client receives the payload, you may issue a second request to the\nsame endpoint to fetch any missing nodes. See [digging](#digging)\n\nThere is also a `defer: :auto` option that you can use with [SuperglueJS][1]. [SuperglueJS][1]\nwill use the metadata from `json.deferred!` to issue a `remote` dispatch to fetch\nthe missing node and immutably graft it at the appropriate keypath in your Redux\nstore.\n\nUsage:\n\n```ruby\njson.dashboard(defer: :manual) do\n  sleep 10\n  json.someFancyMetric 42\nend\n\n\n# or you can explicitly pass a placeholder\n\njson.dashboard(defer: [:manual, placeholder: {}]) do\n  sleep 10\n  json.someFancyMetric 42\nend\n```\n\nA auto option is available:\n\n**Note** This is a [SuperglueJS][1] specific functionality.\n\n```ruby\njson.dashboard(defer: :auto) do\n  sleep 10\n  json.someFancyMetric 42\nend\n```\n\nFinally in your `application.json.props`:\n\n```ruby\njson.defers json.deferred!\n```\n\n#### Working with arrays\nThe default behavior for deferments is to use the index of the collection to\nidentify an element.\n\n**Note** If you are using this library with [SuperglueJS][1], the `:auto` option will\ngenerate `?props_at=a.b.c.0.title` for `json.deferred!`.\n\nIf you wish to use an attribute to identify the element. You must:\n\n1. Use the `:key` option on `json.array!`. This key refers to an attribute on\nyour collection item, and is used for `defer: :auto` to generate a keypath for\n[SuperglueJS][1]. If you are NOT using SuperglueJS, you do not need to do this.\n\n2. Implement `member_at`, on the [collection](#jsonarray). This will be called\nby PropsTemplate to when [digging](#digging)\n\nFor example:\n\n```ruby\nrequire 'props_template/core_ext'\ndata = [\n  {id: 1, name: 'foo'},\n  {id: 2, name: 'bar'}\n]\n\njson.posts\n  json.array! data, key: :some_id do |item|\n    # By using :key, props_template will append `json.some_id item.some_id`\n    # automatically\n\n    json.contact(defer: :auto) do\n      json.address '123 example drive'\n    end\n  end\nend\n```\n\nIf you are using [SuperglueJS][1], it will automatically kick off\n`remote(?props_at=posts.some_id=1.contact)` and `remote(?props_at=posts.some_id=2.contact)`.\n\n## Digging\n\nPropsTemplate has the ability to walk the tree you build, skipping execution of\nuntargeted nodes. This feature is useful for selectively updating your frontend\nstate.\n\n```ruby\ntraversal_path = ['data', 'details', 'personal']\n\njson.data(dig: traversal_path) do\n  json.details do\n    json.employment do\n      # ...more stuff\n    end\n\n    json.personal do\n      json.name 'james'\n      json.zipCode 91210\n    end\n  end\nend\n\njson.footer do\n  # ...\nend\n```\n\nPropsTemplate will walk depth first, walking only when it finds a matching key,\nthen executes the associated block, and repeats until the node is found.\nThe above will output:\n\n```json\n{\n  \"data\": {\n    \"name\": 'james',\n    \"zipCode\": 91210\n  },\n  \"footer\": {\n    ...\n  }\n}\n```\n\nDigging only works with blocks, and will NOT work with Scalars\n(\"leaf\" values). For example:\n\n```ruby\ntraversal_path = ['data', 'details', 'personal', 'name'] # \u003c- not found\n\njson.data(dig: traversal_path) do\n  json.details do\n    json.personal do\n      json.name 'james'\n    end\n  end\nend\n```\n\n## Nodes that do not exist\n\nNodes that are not found will remove the branch where digging was enabled on.\n\n```ruby\ntraversal_path = ['data', 'details', 'does_not_exist']\n\njson.data(dig: traversal_path) do\n  json.details do\n    json.personal do\n      json.name 'james'\n    end\n  end\nend\n\njson.footer do\n  # ...\nend\n```\n\nThe above will render:\n\n```json\n{\n  \"footer\": {\n    ...\n  }\n}\n```\n\n## Layouts\nA single layout is supported. To use, create an `application.json.props` in\n`app/views/layouts`. Here's an example:\n\n```ruby\njson.data do\n  # template runs here.\n  yield json\nend\n\njson.header do\n  json.greeting \"Hello\"\nend\n\njson.footer do\n  json.greeting \"Hello\"\nend\n\njson.flash flash.to_h\n```\n\n**NOTE** PropsTemplate inverts the usual Rails rendering flow. PropsTemplate\nwill render Layout first, then the template when `yield json` is used.\n\n## Change key format\nBy default, keys are not formatted. This is intentional. By being explicit with your keys,\nit makes your views quicker and more easily diggable when working in JavaScript land.\n\nIf you must change this behavior, override it in an initializer and cache the value:\n\n```ruby\n# default behavior\nProps::BaseWithExtensions.class_eval do\n  # json.firstValue \"first\"\n  # json.second_value \"second\"\n  #\n  # -\u003e { \"firstValue\" =\u003e \"first\", \"second_value\" =\u003e \"second\" }\n  def key_format(key)\n    key.to_s\n  end\nend\n\n# camelCased behavior\nProps::BaseWithExtensions.class_eval do\n  # json.firstValue \"first\"\n  # json.second_value \"second\"\n  #\n  # -\u003e { \"firstValue\" =\u003e \"first\", \"secondValue\" =\u003e \"second\" }\n  def key_format(key)\n    @key_cache ||= {}\n    @key_cache[key] ||= key.to_s.camelize(:lower)\n    @key_cache[key]\n  end\n\n  def result!\n    result = super\n    @key_cache = {}\n    result\n  end\nend\n\n# snake_cased behavior\nProps::BaseWithExtensions.class_eval do\n  # json.firstValue \"first\"\n  # json.second_value \"second\"\n  #\n  # -\u003e { \"first_value\" =\u003e \"first\", \"second_value\" =\u003e \"second\" }\n  def key_format(key)\n    @key_cache ||= {}\n    @key_cache[key] ||= key.to_s.underscore\n    @key_cache[key]\n  end\n\n  def result!\n    result = super\n    @key_cache = {}\n    result\n  end\nend\n```\n\n## Escape mode\n\nPropsTemplate runs OJ with `mode: :rails`, which escapes HTML and XML characters\nsuch as `\u0026` and `\u003c`.\n\n## Contributing\n\nSee the [CONTRIBUTING] document. Thank you, [contributors]!\n\n  [CONTRIBUTING]: CONTRIBUTING.md\n  [contributors]: https://github.com/thoughtbot/props_template/graphs/contributors\n\n## Special Thanks\n\nThanks to [turbostreamer], [oj], and [jbuilder] for the inspiration.\n\n[1]: https://github.com/thoughtbot/superglue\n[turbostreamer]: https://github.com/malomalo/turbostreamer\n[jbuilder]: https://github.com/rails/jbuilder\n[oj]: https://github.com/ohler55/oj/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtbot%2Fprops_template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthoughtbot%2Fprops_template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtbot%2Fprops_template/lists"}