{"id":22120372,"url":"https://github.com/maxpleaner/socket_helpers","last_synced_at":"2025-07-25T12:33:14.220Z","repository":{"id":62558828,"uuid":"48895111","full_name":"MaxPleaner/socket_helpers","owner":"MaxPleaner","description":"websocket helpers for rails, http://maxpleaner.github.io/socket_helpers","archived":false,"fork":false,"pushed_at":"2016-12-19T00:18:16.000Z","size":259,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-07-08T19:12:51.137Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/MaxPleaner.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-01-02T01:37:56.000Z","updated_at":"2016-11-04T10:55:58.000Z","dependencies_parsed_at":"2022-11-03T10:15:14.463Z","dependency_job_id":null,"html_url":"https://github.com/MaxPleaner/socket_helpers","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MaxPleaner/socket_helpers","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxPleaner%2Fsocket_helpers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxPleaner%2Fsocket_helpers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxPleaner%2Fsocket_helpers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxPleaner%2Fsocket_helpers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MaxPleaner","download_url":"https://codeload.github.com/MaxPleaner/socket_helpers/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxPleaner%2Fsocket_helpers/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267007598,"owners_count":24020261,"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","status":"online","status_checked_at":"2025-07-25T02:00:09.625Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-12-01T14:23:06.973Z","updated_at":"2025-07-25T12:33:13.896Z","avatar_url":"https://github.com/MaxPleaner.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\n# SocketHelpers\n\n### Usage /Installation\n\n_(these instructions can be seen implemented in the [socket_helpers_example](http://github.com/maxpleaner/socket_helpers_example) repo_ or seen [on a live site](http://socket-helpers-example.herokuapp.com)\n\n---\n\n####\n**create rails app** `rails new App; cd App;`\n\n**create a model** `rails g scaffold Todo content:string; rake db:migrate;`\n\n**add gems** `gem 'socket_helpers'` and `gem 'websocket-rails'`\n\n**add javascript requires to application.js**\n\n- `//= require websocket_rails/main`\n- `//= require socket_helpers`\n\n**add jquery initializer** for whatever models you need websocket resources for (singular, snake case).\n\n ```javascript\n   $(function(){\n    SocketHelpers.initialize([\"todo\"], \"http://localhost:3000/websocket\")\n   })\n ```\n- the default websocket url (from the websocket-rails gem) is \"/websocket\"\n\n**include the controller helpers to application_controller**\n \n ```ruby\n   class ApplicationController \u003c ActionController::Base\n     include SocketHelpers::ControllerHelpers\n   end\n ```\n\n**Remove the default scaffold routes** (`resources :todos`). This gem supports only query parameters, not path parameters. This limitation only applies to `websocket_response` endpoints. Other endpoints can use path parameters.\n\n- i.e. parameters are never declared in the routes.rb file, but they are declared in controllers. For example, routes like `DELETE /todos/MY_TODO_ID` are not supported, but `DELETE /todos?id=MY_TODO_ID` are.\n\n**Create a HTML-serving endpoint** `rails g controller HtmlPages root`\n\n**Create websocket API endpoints and write routes**\n \n ```ruby\n   # routes.rb\n   get \"/\", to: \"html_pages#root\"\n   post \"todos\", to: \"todos#create\"\n   delete \"todos\", to: \"todos#destroy\"\n ```\n\n ```ruby\n   # app/controllers/todos_controller.rb\n   # all the default scaffold stuff can be deleted\n   class TodosController \u003c ApplicationController\n     def create\n       todo = Todo.create(todo_params)\n       websocket_response(todo, \"create\")\n       return false\n     end\n     def destroy\n       todo = Todo.find_by(id: params[:id])\n       todo.destroy\n       websocket_response(todo, \"destroy\")\n       return false\n     end\n     def todo_params\n       params.permit(:content)\n     end\n   end\n ```\n- the first argument of `websocket_response` can be a single record or an array. _It cannot be a query_. The second can be either `create`, `destroy`, or `update` (these values hard-coded into the app. The receiver-hooks for these events are automatically created by the javascript client. \n\n- make sure to add a 'return' or 'render' after `websocket_response` to avoid \"template not found\" errors.\n\n**use the DSL for HTML** in html_pages/root.html.erb. See below for a list of HTML components available.\n\n  ```html\n  \n    \u003ch3\u003eCreate todo\u003c/h3\u003e\n    \n    \u003c%# This form will submit via AJAX %\u003e\n    \u003c%# all forms do this by default. use \u003cform skip-sockets\u003e to prevent it.%\u003e\n    \u003cform action=\"todos\" method=\"POST\"\u003e\n      \u003cinput type=\"text\" name=\"content\" placeholder=\"content\"\u003e\n      \u003cinput type=\"submit\" value=\"submit\"\u003e\n    \u003c/form\u003e\n    \n    \u003c%# a class value of \"model_name-index\" is special %\u003e\n    \u003c%# and sets up this section as a container for a list of records %\u003e\n    \u003cdiv class=\"todo-index\"\u003e\n      \u003ch3\u003eTodos\u003c/h3\u003e\n      \u003cp template\u003e \u003c%# special attr defines this as the template for added records %\u003e\n        \u003cspan template-attr=\"content\"\u003e\u003c/span\u003e\u003c%# two-way databinding for 'content %\u003e\n        \u003cform action=\"/todos\" method=\"POST\"\n          \u003cinput type=\"hidden\" name=\"_method\" value=\"DELETE\"\n          \u003cinput type=\"hidden\" name=\"id\" template-attr=\"id\"\n          \u003cinput type=\"submit\" value=\"remove\"\u003e\u003c/input\u003e\n        \u003c/form\u003e\n        \u003cbr\u003e\n      \u003c/p\u003e\n      \u003c/ul\u003e\n    \u003c/div\u003e\n    \n    \u003c%# define some todos which will initially appear on the page %\u003e\n    \u003c%# This serialization is done automatically during 'websocket_response' %\u003e\n    \u003c% @todos = Todo.limit(1).map do |todo| %\u003e\n    \u003c%   todo.attributes.merge('record_class' =\u003e 'todo') %\u003e\n    \u003c% end %\u003e\n    \n    \u003c%# initial data for the page %\u003e\n    \u003c%# update and delete listeners are set up for these ids %\u003e\n    \u003cdiv init=\"todo\"\u003e\n      \u003c%= Oj.dump @todos.to_a %\u003e\n      \u003c%# make sure not to dump a query %\u003e\n    \u003c/div\u003e\n  ```\n\n- This provides working 'index, 'create', and 'destroy' websocket functionality in quite few lines of HTML, which is mainly the point of this gem. 'update' is automatic as well. When a record is added to the page, a `record-id` attribute is automatically set to `\u003crecord_class\u003e,\u003cid\u003e` on the newly-added template. This is used to lookup records. \n\n**remove CSRF token check**\n\ncomment out the `protect_from_forgery with: :exception` line in application_controller\n\n**start rails server** `rails s;`, open [localhost:3000](http://localhost:3000)\n\nIt is a working todo-app with websockets. Try opening two browser windows at once. \n\n---\n\n### **List of HTML components**\n\n- elements with a class of `\u003cmodel_name\u003e-index` become lists, with elements auto-removed and added in response to websocket events. For example, `\u003cdiv class=\"todo-index\"\u003e\u003c/div\u003e`. These sections correspond to a single ActiveRecord class (underscore, singular i.e. `todo_list_item` for `TodoListItem`)\n\n- inside a `\u003cmodel_name\u003e-index` element, an element with a `template` attribute becomes the template for added records. For example, `\u003cdiv template\u003e\u003c/div\u003e`\n\n- inside a `[template]` element, the `template-attr` attribute is used to establish two-way databinding on an element. Its value is the name of the attribute. This can be used to set the value of form inputs or to change text nodes. For example,\n\n```html\n  \u003cinput type=\"text\" name=\"content\" template-attr=\"content\"\u003e\n  \u003c!-- or alternatively --\u003e\n  \u003cspan template-attr=\"content\"\u003e\n```\n\n- **all form submits are intercepted** by event listeners by default. To override this, add the \"skip-sockets\" attribute to the form element. They submit AJAX requests using the url in the form's `action` attribute and the method in the form's `method` attribute (i.e. `action=\"/todos\" method=\"POST\"`). This works for `GET` and `POST` only, but `PUT` and `DELETE` can be used by adding a hidden input method i.e. `input type=\"hidden\" name=\"_method\" value=\"PUT\"`. This is the default Rails behavior anyway.\n\n- To submit an id with a form, bind a hidden attribute i.e. `\u003cinput type=\"hidden\" name=\"id\" template-attr='id'\u003e`\n\n- Outside of `[template]`s, binding tags are a bit more verbose. `\u003cspan binding-tag='todos,1,content'\u003e\u003c/span\u003e` where the three comma-separated arguments are `\u003cmodel_class\u003e`, `\u003cid\u003e`, and `\u003cattribute\u003e`. `template-attr` tags are automatically converted to `binding-tag` once new records are added to the page. \n\n---\n\n### **Other notes**\n\n#### **Changing a classes' published class name\n\n- Say I created a `LocationCategorization` scaffold but\n  realized that I would rather publish the data  using \n  a `record_class` value of `category` instead of `location_categorization`.\n  I don't want to undo the scaffold, so I add a method to the `LocationCategorization` class:\n\n  ```ruby\n    class LocationCategorization \u003c ActiveRecord::Base\n      def published_class\n        \"category\"\n      end\n    end\n  ```\n\nThis particular method name is used as an optional override\nfor the default published class name (`record.class.to_s.underscore`)\n\n#### **Loading initial data on the page**\n\nWithout doing this, the page will be empty every time it is refreshed. The page needs to start out with a list of records loaded.\n\nCreate an html element with an `init` attribute set to a model class, i.e. `todo`. This element will be auto-hidden. In the html-serving controller method, make an instance variable for whatever data is going to be included (expects an array, not a single object or query). On the html page, use ERB to set the content of the `[init]` element to a JSON stringified version of your instance variable. For example, `\u003cdiv init=\"todo\"\u003e\u003c%= Oj.dump([User.first]) %\u003e\u003c/div\u003e`\n\n#### **How to do links with params**\n\ni.e. how to do\n\n```html\n\u003ca href=\"/my_link?with=params\"\u003eMy Link \u003c/a\u003e\n```\n\nThe way to do this is by building a form and disguising it as a link. Basically come up with some CSS style so the form looks like a link. I don't really know how to do the CSS, but the form HTML code is below. This has the effect of creating a button on the page with the desired link follow-through when clicked. In this example, the 'link-style' class has to be externally implemented.\n\n```html\n\u003cform skip-sockets class=\"link-style\" action=\"/notepad\" method=\"GET\"\u003e\n  \u003cinput type=\"hidden\" name=\"name\" template-attr='name'\u003e\n  \u003cinput type=\"submit\" template-attr='name'\u003e\n\u003c/form\u003e\n\n```\n\n\n\n### **Additional Helpers**\n\nyou can make one html element toggle another open / close very easily.\n\nJust make them 'siblings (share the same parent element) and give the trigger a `toggles` attribute with a value set to the CSS selector of the target. The target will be initially closed. \n\n---\n\n### **Use of OJ gem for JSON**\n\n- I use the OJ gem here and `Oj.dump` because of an unsolved recursion bug in `to_json` I encountered.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxpleaner%2Fsocket_helpers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxpleaner%2Fsocket_helpers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxpleaner%2Fsocket_helpers/lists"}