{"id":15725876,"url":"https://github.com/ndrean/dynamic-ajax-forms","last_synced_at":"2026-02-06T10:03:34.473Z","repository":{"id":40121123,"uuid":"261913997","full_name":"ndrean/Dynamic-Ajax-forms","owner":"ndrean","description":"Rails app featuring Dynamic nested forms, all AJAX, Drag \u0026 Drop, advanced PG_search","archived":false,"fork":false,"pushed_at":"2023-01-19T18:46:28.000Z","size":109499,"stargazers_count":2,"open_issues_count":30,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-11T13:32:46.297Z","etag":null,"topics":["ajax","ajax-pagination","docker","drag-drop","dynamic-forms","javascript","nginx-proxy","postgresql","rails"],"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/ndrean.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":"2020-05-07T00:51:22.000Z","updated_at":"2025-04-16T23:25:13.000Z","dependencies_parsed_at":"2023-02-11T16:31:00.423Z","dependency_job_id":null,"html_url":"https://github.com/ndrean/Dynamic-Ajax-forms","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ndrean/Dynamic-Ajax-forms","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2FDynamic-Ajax-forms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2FDynamic-Ajax-forms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2FDynamic-Ajax-forms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2FDynamic-Ajax-forms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ndrean","download_url":"https://codeload.github.com/ndrean/Dynamic-Ajax-forms/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndrean%2FDynamic-Ajax-forms/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29157471,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T07:18:23.844Z","status":"ssl_error","status_checked_at":"2026-02-06T07:13:32.659Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["ajax","ajax-pagination","docker","drag-drop","dynamic-forms","javascript","nginx-proxy","postgresql","rails"],"created_at":"2024-10-03T22:24:46.494Z","updated_at":"2026-02-06T10:03:33.401Z","avatar_url":"https://github.com/ndrean.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Overview\n\nA toy Rails app deployed on Heroku: \u003chttps://dynamic-ajax-forms.herokuapp.com/\u003e\n\n- backed by `Postgres`,\n- Puma configured with unix/sockets for speed\n- reverse proxied with `nginx`. Leave the gzip compression to nginx for static files only as BREACH vulnerability...). Note: rake can use gem 'rack-brotli'.\n- using plain Javascript with `Webpacker` (no React).\n- all queries are `Ajax` in multiple forms\n- using search `pg_search`\n- implementing `dynamic forms` (add fields 'on-the-fly')\n- implementing full ajax `Kaminari` pagination\n- implementing dynamic 'Drag-drop' (all ajax)\n\n\u003e No cache strategy implemented (nor fragment/page nor conditional Get nor in model cache.fetch). TODO\n\u003e \u003chttps://medium.com/better-programming/cache-and-serve-rails-static-assets-with-nginx-reverse-proxy-dfcd49319547\u003e\n\n\u003e Nginx: installed via \u003chttps://denji.github.io/homebrew-nginx/#modules\u003e and `nginx brew reinstall nginx-full --with-gzip-static --with-brotli-module` to Brotli compress data and let nginx serve static files (after `rails assets:precompile` with `config.public_file_server.enabled = true`). See below 'nginx.conf' running using tcp/ports (possible unix/socket, configure Puma).\n\n\u003e Unix/socket or TCP/port: modify '/config/puma.rb' and '/nginx.conf':\n\n- TCP/port : in '/config.puma.rb', set\n\n```\nport ENV.fetch(\"PORT\") { 3000 }\n```\n\nonly and in 'nginx.conf', set the directive\n\n```\nupstream app_server {\n  server localhost:3000;\n}\n```\n\n- unix/sockets: in '/config.puma.rb', set\n\n```\napp_dir =  File.expand_path(\"../..\", __FILE__);\nbind \"unix://#{app_dir}/tmp/sockets/nginx.socket\";\n```\n\nand in 'nginx.conf', set\n\n```\nupstream app_server {\n  server unix:///Users/utilisateur/code/rails/dynamic-ajax-forms/tmp/sockets/nginx.socket fail_timeout=0;\n}\n```\n\nwhere 'app_dir = Users/utilisateur/code/rails/dynamic-ajax-forms'\n\n# Nginx.conf\n\nWith the flag `config.public_file_server.enabled = false` in '/config/environment/dev || prod', we can configure Nginx to run as reverse proxy to serve static files (CSS, JPG, JS) from the /public/assets or /public/packs folders\n\n\u003chttps://www.linode.com/docs/web-servers/nginx/slightly-more-advanced-configurations-for-nginx/\u003e\n\n\u003chttps://medium.com/@joatmon08/using-containers-to-learn-nginx-reverse-proxy-6be8ac75a757\u003e\n\n```\nworker_processes  auto; # depend on cpu cores, ram\n\nerror_log  tmp/logs/error.log;\n# error_log  logs/error.log  notice;\n# error_log  logs/error.log  info;\n\n# pid        logs/nginx.pid;\n\n# daemon off;\n\nevents {\n    worker_connections  1024;\n}\n\n\nhttp {\n    include           mime.types;\n    default_type      application/octet-stream;\n    sendfile          on;\n    keepalive_timeout 65;\n    add_header    X-XSS-Protection \"1; mode=block\";\n    add_header    X-Content-Type-Options nosniff;\n    add_header    X-Frame-Options SAMEORIGIN;\n\n    upstream app_server {\n      server          localhost:3000;\n      # server unix:///Users/utilisateur/code/rails/godwd/tmp/sockets/nginx.socket fail_timeout=0;\n    }\n\n    gzip                  on;\n    gzip_comp_level       6;\n    gzip_min_length       512;\n    gzip_static           on;\n    gzip_proxied          no-cache no-store private expired auth;\n    gzip_types\n      #\"application/json;charset=utf-8\" application/json\n      \"application/javascript;charset=utf-8\" application/javascript text/javascript\n      \"application/xml;charset=utf-8\" application/xml text/xml\n      \"text/css;charset=utf-8\" text/css\n      \"text/plain;charset=utf-8\" text/plain;\n\n\n    server {\n      listen          8080;\n      listen          [::]:8080;\n      #server_name localhost;\n\n      root             /public\n\n      # serve static (compiled) assets directly if they exist (for rails monolith production)\n      # if Rails API, do not use !!!\n\n      location ~ ^/(assets|packs) {\n        try_files $uri @rails;\n        access_log off;\n        gzip_static on;\n        # to serve pre-gzipped version\n        expires max;\n        add_header Cache-Control public;\n        add_header Last-Modified \"\";\n        add_header ETag \"\";\n        break;\n      }\n\n      location / {\n        try_files $uri @rails;\n      }\n\n      location @rails {\n      proxy_set_header  X-Real-IP  $remote_addr;\n      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n      proxy_set_header  Host $http_host;\n      proxy_redirect    off;\n      proxy_pass        http://app_server;\n\n      proxy_http_version  1.1;\n      proxy_set_header    Connection ‘’;\n      proxy_buffering     off;\n      proxy_cache         off;\n      chunked_transfer_encoding off;\n      # proxy_set_header    X-Accel-Buffering: no;\n\n   }\n\n      error_page   500 502 503 504  /50x.html;\n      location = /50x.html {\n          root   html;\n      }\n\n      location /favicon.ico {\n        log_not_found off;\n      }\n    }\n\n\n    # another virtual host using mix of IP-, name-, and port-based configuration\n    #\n    #server {\n    #    listen       8000;\n    #    listen       somename:8080;\n    #    server_name  somename  alias  another.alias;\n\n    #    location / {\n    #        root   html;\n    #        index  index.html index.htm;\n    #    }\n    #}\n\n\n    # HTTPS server\n    #\n    #server {\n    #    listen       443 ssl;\n    #    server_name  localhost;\n\n    #    ssl_certificate      cert.pem;\n    #    ssl_certificate_key  cert.key;\n\n    #    ssl_session_cache    shared:SSL:1m;\n    #    ssl_session_timeout  5m;\n\n    #    ssl_ciphers  HIGH:!aNULL:!MD5;\n    #    ssl_prefer_server_ciphers  on;\n\n    #    location / {\n    #        root   html;\n    #        index  index.html index.htm;\n    #    }\n    #}\n    include servers/*;\n}\n```\n\n# Local testing with Nginx reverse-proxy \u0026 'rails s'\n\nThe `database.yml` should be set with `host: localhost`. The Rails app will run on port:3000. To run with nginx, then nginx should be started on the machine (`brew services start nginx`). Modify nginx's config in '/etc/local/nginx/nginx.conf', remove `daemon off`, set `listen 8080` and set `upstream { server localhost:3000 }`.\n\n# Run the app in local Docker container without nginx\n\nSet `host: db` in '/config/database.yml' where `db`is the Postgres.\nRun `docker-compose up -f docker-compose-no-nginx.yml`. Navigate to localhost:3000.\n\n# Run the app in a local container with nginx in it.\n\n- folder structure:\n\n```\n- app\n  - config\n    database.yml #put 'host: db'\n    puma.rb # choose port: 3000\n- db\n- docker\n  - app\n    Dockerfile (rails app with node)\n  - web\n    Dockerfile (nginx)\n    nginx.conf\ndocker-compose.yml\n```\n\n\u003e Note: the gem 'web-console' allows you to create an interactive Ruby session in your browser. Those sessions are launched automatically in case of an error and can also be launched manually in any page. Since we whare The `config.web_console.permissions` lets you control which IP's have access to the console.\u003chttps://stackoverflow.com/questions/29417328/how-to-disable-cannot-render-console-from-on-rails\u003e\n\n## Docker container Ip adress\n\n\u003chttps://www.freecodecamp.org/news/how-to-get-a-docker-container-ip-address-explained-with-examples/\u003e\n`docker network ls`\n\n## Error with ENTRYPOINT\n\nGot error `standard_init_linux.go:211: exec user process caused \"no such file or directory\"`because of this file.\n\n# Heroku deploy\n\nFor Heroku:\n\n- in 'app/config/nginx.conf.erb', set `daemon off`\n- in Procfile, set: `web: bin/start-nginx bundle exec puma --config config/puma.rb`\n\n```\nrails assets:precompile\nrails assets:clobber\n\nheroku ps:scale web=1 --app dynamic-ajax-forms\n\nheroku run rack db:schema:load --app dynamic-ajax-forms\nheroku run rack db:seed --app dynamic-ajax-forms\n```\n\nContinuous update of the logs with `heroku logs --tail` in a terminal.\n\n## To be done: favicon\n\n\u003chttps://github.com/lewagon/product/blob/master/checklist/04_favicon_tag_is_set.md\u003e\n\n# Links:\n\n- [Import JS in js.erb](#import-js-methods-in-js.erb)\n  - [Javascript setup](#javascript-setup)\n- [The models](#the-models)\n- [Dynamic form](#dynamic-forms)\n\n- [Queries](#queries)\n- [Search](#search-pg_Search)\n- [Fetch GET with query string](#fetch-get-with-query-string)\n- [Editable on the fly](#editable-cell-on-the-fly)\n- [Create or Select on the fly](#create-or-select-on-the-fly)\n- [Delete Ajax](#delete-ajax)\n- [Fetch GET iwth response.text()](#fetch-with-response-text)\n- [Drag \u0026 Drop](#drag-drop) with `fetch()` 'POST' and `DELETE` and `csrfToken()`\n\n  - [Fetch POST](#fetch-post)\n  - [Fetch DELETE](#fetch-delete-and-tabindex-attribute) and [tabindex](#tabindex) attribute\n\n- [Error rendering \u0026 form validation](##error-rendering) for browser \u0026 backend\n- [Kaminari](#kaminari-ajax) setup with Ajax rendering pagination\n\n- [Setup](#setup)\n  - [Database model](#database-model)\n  - [Counter cache](#counter-cache) quick setup, child model and parent model\n  - [Fontawsome](#fontawesome) setup with a _gem_ and `@import`\n  - [Bootstrap](#bootstrap) setup with _yarn_ and `@import`\n\n## Import js libraries into _.js.erb_\n\nTo add Erb support in your JS templates, run:\n\n```bash\nbundle exec rails webpacker:install:erb\n```\n\non a Rails app already setup with Webpacker.\n\nWith this setting, we then can create a _.js.erb_ file in the folder _/javascript/packs/_. Then we can use ERB (Ruby parses the file first) and import external libraries with `import { myFunction } from '../components/myJsFile.js`.\n\nIn other words, we can import _.js_ libraries into _.js.erb_ files.\n\n\u003e This can save on _data-attributes_ (the `data-something=\"\u003c%= Post.first.id%\u003e\"` in the HTML file with it's searching `document.querySelector('[data-something]')`can be replaced simply by eg `const id = \u003c%= Post.first.id%\u003e in the _.js.erb_ file)\n\n\u003e Note 1: A 'standard' view rendering file _.js.erb_ located in the views does \u003cstrong\u003enot\u003c/strong\u003e have access to `import`, only those located in the folder _/javascript/packs/_ do (after running `webpacker:install:erb`).\n\n\u003e Note 2: To use a JS library inside a view _.html.erb_ we need to:\n\n- import the library in a _someFile.js.erb_ file in the folder _/javascript/packs/_\n\n- import the _someFile.js.erb_ file in the view with `\u003ct%= javascript_pack_tag 'someFile' %\u003e`\n\n\u003e Note 3: we need to have Turbolinks loaded to access to the DOM, so all the code in the _someFile.js.erb_ file is wrapped as a callback: `document.addEventListener(\"turbolinks:load\", myFunction})`, and declare `const myFunction = ()=\u003e {[...]}` after.\n\n### Javascript setup\n\nStandard _Webpack_ settings:\n\n```ruby\n# views/layout/application\n \u003c%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload', defer: true %\u003e\n```\n\nAn example to ensure that Turbolinks is loaded before calling any JS function:\n\n```js\n# javacsript/packs/applications.js\nimport { createComment } from \"../components/createComment.js\";\n// for Turbolinks to work with Javascript\ndocument.addEventListener(\"turbolinks:load\", () =\u003e {\n  const createCommentButton = document.getElementById(\"newComment\");\n    if (createCommentButton) {\n      createComment();\n    }\n  [... all other methods called here ....]\n});\n```\n\n## The models\n\nWe have a simple three model _one-to-many_ with _Type_, _Restaurant_ and _Client_ and a joint table _Comment_ between _Restaurant_ and _Client_ (with fields resp. _name_, _name_, _name_ and _comment_ ).\n![Database](https://github.com/ndrean/Dynamic-Ajax-forms/blob/master/app/assets/images/db-schema.jpeg?raw=true)\n\n```ruby\nclass Genre \u003c ApplicationRecord\n  has_many :restos, -\u003e { order(name: :asc)}\n    has_many :comments, through: :restos\n    has_many :clients, through: :comments\n    validates :name, uniqueness: true, presence: true\n    accepts_nested_attributes_for :restos\nend\n\nclass Resto \u003c ApplicationRecord\n  belongs_to :genre, optional: true\n  has_many :comments, dependent: :destroy\n  has_many :clients, through: :comments\n  validates :name, uniqueness: true, presence: true\n  accepts_nested_attributes_for :comments\nend\n\nclass Comment \u003c ApplicationRecord\n  belongs_to :resto, counter_cache: true\n  belongs_to :client\n  validates :comment, length: {minimum: 2}\n  accepts_nested_attributes_for :client\n  default_scope {joins(:resto).order('name ASC') } # to sort the table 'comments' with the resto name ASC\nend\n\nclass Client \u003c ApplicationRecord\n    has_many :comments\n    has_many :restos, through: :comments\n    has_many :genres, through: :comments, source: :resto\n\nend\n```\n\n\u003e The method `accept_nested_attributes_for` works with `has_many`and `belongs_to`\n\n## Dynamic forms\n\nWe build a form which permits to add four nested inputs: _genre (1\u003en) restos (1)\u003en) comments (n\u003c1) client_.\n\n- use `accepts_nested_attributes_for` in the models, both for `has_many` and `belongs_to`\n- build nested records in the controller's method `new` with `@genre.restos.build` for a simple nested association or `@genre.restos.build.comments.build` for a triple nested association, and `build_model` for the `belongs_to` association (where _model_ is _client_ here), so we have `@resto.comments.build.build_client` in the _new_ method.\n- use the form builder `fields_for` (both _simple_form_ or _form_with_)\n- adapt the strong params method (see below)\n\nAll this will make Rails accept an array of nested attributes of any length, and the formbuilder will render a block for each element in the association.\n\nThe controller is\n\n```ruby\n#restos_controllers.rb\ndef new4\n  @genre = Genre.new\n  @genre.restos.build.comments.build.build_client\n\n# with strong params method:\n\ndef resto_params\n      params.require(:resto).permit(:name,:genre_id,\n        comments_attributes: [:id, :comment,\n          client_attributes: [:client_id\n          ]\n        ]\n      )\n    end\n```\n\nWe obtain the following hash params for example: (we put `:` instead of `=\u003e`):\n\n```json\n#Parameters:\n{\"genre\":{\n  \"name\":\"German\",\n   \"restos_attributes\":{\n      \"0\":{\n        \"name\":\"The Best\",\n        \"comments_attributes\":{\n          \"0\":{\n            \"comment\":\"Cool\",\n            \"client_attributes\":{\n              \"name\":\"John\"\n            }\n          },\n        \"1\":{\n          \"comment\":\"Bueno\",\n          \"client_attributes\":{\n            \"name\":\"Mary\"\n            }\n          }\n        }\n      }\n    }\n  }, \"commit\":\"Create!\"\n}\n```\n\nThe code written in _/views/genres/new4.html.erb_ calls the partial _/genres/\\_nested_dyn_form.html.erb_:\n\n```ruby\n\u003c%= simple_form_for genre, url: 'create4', remote: true do |f| %\u003e\n    \u003c%= f.error_notification%\u003e\n    \u003c%= f.input :name, label:\"Genre/Type of restaurant\" %\u003e\n    \u003c%= f.simple_fields_for :restos do |r| %\u003e\n        \u003c%= r.input :name, label:\"Restaurant's name\" %\u003e\n        \u003c%= r.simple_fields_for :comments do |c| %\u003e\n        \u003cfieldset data-fields-id=\"\u003c%= c.index %\u003e\"\u003e\n            \u003c%= c.input :comment, label:\"Add a comment\" %\u003e\n            \u003c%= c.simple_fields_for :client do |cl| %\u003e\n                \u003c%= cl.input :name, label: \"Join client's name\"%\u003e\n            \u003c% end %\u003e\n        \u003c/fieldset\u003e\n        \u003c% end %\u003e\n    \u003c% end %\u003e\n    \u003c%= f.button :submit, \"Create!\", class:\"btn btn-primary\", id:\"submit-nested\" %\u003e\n\u003c% end %\u003e\n```\n\nWe have wrapped the dynamic HTML fragment inside a fielset tag to easily select it. Furthermore:\n\n\u003e we added a dataset which fills in with the index of the formbuilder object 'comment', `\u003c%= c.index %\u003e` which is automatically updated by Rails.\n\nThe HTML fragment is:\n\n```html\n# HTML fragment copied from the console\n\u003cdiv id=\"select_comment\"\u003e\n  \u003cfieldset data-fields-id=\"0\"\u003e\n    \u003cdiv class=\"form-group string optional genre_restos_comments_comment\"\u003e\n      \u003clabel\n        class=\"string optional\"\n        for=\"genre_restos_attributes_0_comments_attributes_${newId}_comment\"\n        \u003eAdd a comment\u003c/label\n      \u003e\n      \u003cinput\n        class=\"form-control string optional\"\n        type=\"text\"\n        name=\"genre[restos_attributes][0][comments_attributes][0][comment]\"\n        id=\"genre_restos_attributes_0_comments_attributes_0_comment\"\n      /\u003e\n    \u003c/div\u003e\n\n    \u003cdiv class=\"form-group string optional genre_restos_comments_client_name\"\u003e\n      \u003clabel\n        class=\"string optional\"\n        for=\"genre_restos_attributes_0_comments_attributes_${newId}_client_attributes_name\"\n        \u003eJoin client's name\u003c/label\n      \u003e\n      \u003cinput\n        class=\"form-control string optional\"\n        type=\"text\"\n        name=\"genre[restos_attributes][0][comments_attributes][0][client_attributes][name]\"\n        id=\"genre_restos_attributes_0_comments_attributes_0_client_attributes_name\"\n      /\u003e\n    \u003c/div\u003e\n  \u003c/fieldset\u003e\n\u003c/div\u003e\n```\n\nWe use JS in a _js.erb_ file ot inject the HTML code. We want a button to add new input fields and assign a unique id, and a form _submit_ button. The following Javascript method does the following:\n\n- copies the first HTML fragment wrapped in the tag 'fieldset'\n- finds the last formbuilder index, which is in a dataset,\n- we find and replace by a simple regex the desired indexes (here, it's basically replace one out of two '0' with 'newId' to get the unique Id)\n- finally, inject inot the DOM\n\n```js\n// # restos/new4.js.erb\nfunction dynAddNestedComment() {\n  document.getElementById(\"addNestedComment\").addEventListener(\"click\", (e) =\u003e {\n    e.preventDefault();\n    const lastID = document.querySelector(\"#fieldset: last-child\").dataset\n      .fieldsId;\n    // calculate the new Id\n    // const arrayComments = [...document.querySelectorAll(\"fieldset\")];\n    // const lastId = arrayComments[arrayComments.length - 1].dataset.fieldsId\n    // we have put a dataset in the fieldset tag where data-fieldsid = c.index\n    //  where Rails gives the index of the formbuilder object\n    const newId = parseInt(lastId, 10) + 1;\n\n    // set new ID at special location in the new injected HTML fragment\n    let dynField = document\n      .querySelector('[data-fields-id=\"0\"]')\n      .outerHTML.toString();\n    let nb = 0; // counter\n    dynField = dynField.replace(\n      /0/g, // global flag 'g' to get ALL\n      (matched, offset) =\u003e {\n        // we are going to replace every odd index of '0' to 'newId' (see original fieldset)\n        nb += 1;\n        if ((offset = 0)) {\n          return matched;\n        }\n        if (nb % 2 === 1) {\n          // every odd to change 'xxx-0-xxx-0' to 'xxx-0-xxx-1'\n          return newId;\n        } else {\n          return matched;\n        }\n      }\n    );\n\n    // inject the new updated fragment into the DOM\n    document\n      .querySelector(\"#submit-nested\")\n      .insertAdjacentHTML(\"beforebegin\", dynField);\n  });\n}\n```\n\nWe have another form with dynamical injection. This time, we create a restaurant given the 'genre' and will dynamically add new comments with a given collection of clients. The code below is the HTML fragment of the 'fielset' (this has been created for this purpose) to be injected by Javascript. We passed the index of the form object with the `.index` Ruby method and passed it into a dataset so that the Ruby parsing for the HTML will set the correct value. We grab it with JS and use `outerHTML` to get the serialized HTML fragment of the fieldset including its descendants. Since we wrapped the fieldsets into a div, we can easily grab the last child element to get the last index. Then we replace the index (since it has to have a unique 'name') by a regex `replace(/regex/, new value)` where the new value is given by searching the formbuilder's last index and incrementing it.\n\n```html\n# HTML fragment copied from the console\n\u003cdiv id=\"select\"\u003e\n  \u003cfieldset data-fields-id=\"0\"\u003e\n    \u003cdiv class=\"form-group string optional resto_comments_comment\"\u003e\n      \u003clabel\n        class=\"string optional\"\n        for=\"resto_comments_attributes_${newID}_comment\"\n        \u003eComment\u003c/label\n      \u003e\n      \u003cinput\n        class=\"form-control string optional\"\n        type=\"text\"\n        name=\"resto[comments_attributes][${newID}][comment]\"\n        id=\"resto_comments_attributes_${newID}_comment\"\n      /\u003e\n    \u003c/div\u003e\n  \u003c/fieldset\u003e\n\u003c/div\u003e\n```\n\n```js\nfunction dynAddComment() {\n  const createCommentButton = document.getElementById(\"addComment\");\n  createCommentButton.addEventListener(\"click\", (e) =\u003e {\n    e.preventDefault();\n    const lastId = document.querySelector(\"#select\").lastElementChild.dataset\n      .fieldsId;\n    // const arrayComments = [...document.querySelectorAll(\"fieldset\")];\n    // const lastId = arrayComments[arrayComments.length - 1].dataset.fieldsId\n    const newId = parseInt(lastId, 10) + 1;\n    const changeFieldsetId = document\n      .querySelector(\"[data-fields-id]\")\n      .outerHTML.replace(\"0\", \"${newId}\");\n    document\n      .querySelector(\"#new_resto\")\n      .insertAdjacentHTML(\"beforeend\", changeFieldsetId);\n  });\n}\n```\n\nWhen the button _ create comment_ is clicked, we want to inject by Javascript a new input block used for _comment_ We need a unique id for the input field. Since we have access to the formbuilder index, we save this id in a dataset, namely add it to the fieldset that englobes our label/input block. By JS, we can attribute a unique id to the new input by reading the last block.\n\n## Queries\n\nSome ActiveRecord queries\n\n- WHERE needs **table name** and JOINS needs the **association name**.\n\nGiven a `client = CLIENT.find_by(name: \"myfavorite\")`, we can find the restaurants on which he commented with `client.restos`, and the genres he commented on with `client.genres`.\n\nConversely:\ngiven a `resto = RESTO.find_by('restos.name ILIKE ?', \"%Sweet%\")`, we can\nfind the clients that gave a comment with the equivalent queries:\n\n```ruby\n  Resto.joins(comments: :resto).where('clients.name ILIKE ?', '%coralie%')\n```\n\nGiven a `genre = Genre.find_by(name: \"thai\")`, we can find the clients gave a comment with:\n\n```ruby\nGenre.joins(restos: {comments: :client}).where(clients: {name: \"Coralie Effertz\"}).uniq\n\nGenre.joins(restos: {comments: :client}).merge(Client.where(\"clients.name= ?\",  \"Coralie Effertz\")).uniq\n\nGenre.joins(restos: {comments: :client}).merge(Client.where(\"clients.name ILIKE ?\",  \"%Coralie%\")).uniq\n\nGenre.joins(restos: {comments: :client}).where(\"clients.name ILIKE ?\",\"%Coralie%\").uniq\n```\n\nGiven a `client = Client.first`, we want to render it's comments sorted by restaurant (where `comment: belongs_to :resto, client: has_many :comments`), then we write:\n\n```ruby\n#clients_controller.rb \u003cbr\u003e\n  client.includes(comments: :resto)\n\n#views.clients.html.erb \u003cbr\u003e\nclient.comments do |c|\n  c.resto.name\n```\n\nand if we want to further include the genre (where `resto: belongs_to :genre`) which makes the _bullet_ gem happy:\n\n```ruby\n#clients_controler.rb \u003cbr\u003e\n  client.includes(comments: {resto: :genre})\n\n#views/clients/index.html.erb \u003cbr\u003e\n  c.resto.genre.name\n```\n\n## Search pg_Search\n\nWe implemented only a full-text `pg_search` in the page _comments_ on two columns of associated tables (_restos_ and _comments_).\n\n```ruby\n# /views/restos/index.html.erb\n\u003c%= simple_form_for :search, method: 'GET' do |f| %\u003e (note: a form is 'POST' by default)\n\u003cdiv class=\"input-field\"\u003e\n  \u003c%= f.input_field :g, required: false, placeholder: \"blank or any 'type'\"  %\u003e\n  \u003c%= f.input_field :r, required: false, placeholder: \"blank or any 'type'\"  %\u003e\n  \u003c%= f.input_field :pg, required: false, placeholder: \"blank or any 'type'\"  %\u003e\n  \u003c%= button_tag(type: 'submit', class: \"btn btn-outline-success btn-lg\", style:\"padding: .8rem 1rem\") do %\u003e\n    \u003ci class=\"fas fa-search\" id=\"i-search\"\u003e\u0026lt/i\u003e\n  \u003c% end %\u003e\n\u003c/div\u003e\n\u003c% end %\u003e\n```\n\n```ruby\n# model Comment\nclass Comment \u003c ActiveRecord\n  # Usage of question mark \"?\" to SANITIZE against SQL injection\n  scope :find_by_genre, -\u003e(name) {joins(resto: :genre).where(\"genres.name ILIKE ?\", \"%#{name}%\")}\n  scope :find_by_resto, -\u003e(name) {joins(:resto).where(\"restos.name ILIKE ?\", \"%#{name}%\")}\n\n  include PgSearch::Model\n      multisearchable against: :comment\n\n  pg_search_scope :search_by_word, against: [:comment],\n      associated_against: {\n          resto: :name\n          # !! use the association name\n      },\n      using: {\n          tsearch: { prefix: true }\n      }\n\n  # helper to avoid repeating comments = Comment.find_by_xxx(qurey[:x])\n  def self.sendmethod(m,q)\n    comments = self.send(m, q)\n    return  comments.any? ? comments :  self.all\n  end\n\n\n  def self.search_for_comments(query)\n    # page load\n    return Comment.all if !query.present? || (query.present? \u0026\u0026 query[:r]==\"\" \u0026\u0026 query[:g]==\"\" \u0026\u0026 query[:pg]==\"\")\n\n    if !(query[:r]== \"\")\n      return self.sendmethod(:find_by_resto, query[:r])\n\n    elsif query[:g] != \"\"\n      return self.sendmethod(:find_by_genre, query[:g])\n\n    elsif query[:pg] != \"\"\n      return self.sendmethod(:search_by_word, query[:pg])\n    end\n  end\nend\n```\n\nand the mode _Resto_ needs also:\n\n```ruby\n#model Resto\nclass Resto \u003c ActiveRecord\n[•••]\ninclude PgSearch::Model\n  multisearchable against: :name\n[•••]\nend\n```\n\nThen, the controller's index method includes the search results (and avoids N+1 with 'includes' and uses Kaminari's pagination)\u003c/p\u003e\n\n```ruby\n#comments_controller.rb\ndef index\n  @comments = Comment.includes(:resto).order('restos.name').search_for_comments(params[:search]).page(params[:page])\nend\n```\n\n### Fetch GET with query string\n\nFor a `GET` request, there is no need for `CORS`. We used:\n\n- `new FormData` on `e.target` as we listened to the _submit_ of the form, and then\n- `new URLSearchParams().toString()`\n\nto convert the input of a form into a query string added to the end point `/restos?`.\n\nThis produces for example `/restos?search%5Bg%5D=burgers\u0026search%5Br%5D=\u0026button=` if `params[:search][:g]=\"burgers\",params[:search][:r]=\"\",params[:search][:r]=\"\"`)\n\n```js\nasync function getSearchRestos() {\n  const searchForm = document.querySelector('[action=\"/restos\"]');\n  searchForm.addEventListener(\"submit\", async (e) =\u003e {\n    e.preventDefault();\n\n    const data = new FormData(e.target);\n    const uri = new URLSearchParams(data).toString();\n    console.log(uri);\n    try {\n      const request = await fetch(\"/restos?\" + uri, {\n        method: \"GET\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Accept: \"application/json\",\n        },\n      });\n      const response = await request.json();\n      console.log(response);\n    } catch (error) {\n      console.warn(error);\n    }\n  });\n}\n```\n\n##\n\n## Editable cell on the fly\n\n[Back to Contents](#readme)\n\nWe can edit directly the name of the restaurant and save. Firstly we need to use the attribute `contenteditable = true`. Then, there is a hidden form under the button _submit_. When Rails renders the HTML view, the (hidden) form will be initially populated with the object values, as a standard _edit mode_ form. We use the form helper `form_with` and provide the object `model: @esto`. Then, the save method will automatically be _patch/put_.\n\nWe attached a listener on the body since we have a Kaminari pagination. It captures the event `input`. We build a Javascript function such that every event triggers a copy of the innerText into the input of the hidden form. When we validate, the form is submitted to the database with PATCH / UPDATE, so this happens in the background.\n\nThe JS helper that copies directly from the cell to the form input every change in the cell.\n\n```js\nconst copyActive = (tag) =\u003e {\n  document.querySelectorAll(\"td\").forEach((td) =\u003e {\n    document.body.addEventListener(\"input\", (e) =\u003e {\n      if (e.target.dataset.editable === undefined) {\n        return;\n      } else {\n      const id = e.target.dataset.editable;\n      document.querySelector(tag + id).value = e.target.innerText;\n    });\n  });\n};\n```\n\n## Create or Select on the fly\n\nIn the settings of `Resto, belongs_to :genre` and `Genre, has_many :restos`, when we create a form with `Resto.new`, we can create a new restaurant and select-or-create it's genre with the `belongs_to` method `create_genre` as a `before_save` action in the model).\n\nTo create a new genre or select one for a new restaurant we can create an instance variable `attr_accessor :new_genre_name` in the model _Resto_ and `permits(... :new_genre_name)`in the controller. Then we have 2 possible methods, with `before_save` in the model, or with `find_or_create_by` in the controller:\n\n- just declare `@resto = Resto.new(resto_params)` in controller and set in the model a `before_action :create_genre_from_resto`(the `belongs_to` method makes the following `create_genre` method available (\u003ca herf=\"https://guides.rubyonrails.org/association_basics.html#methods-added-by-belongs-to-create-association-attributes\"\u003e Rails guide\u003c/a\u003e )) (\"The create_association method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through this object's foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object will be saved\").\n\nThe model:\n\n```ruby\n# model Resto\ndef create_genre_from_resto\n  create_genre(name: new_genre ) unless new_genre.blank?\nend\n```\n\nor the controller (where the hash params is available):\n\n```ruby\n@resto = Rest.new(resto_params)\nif params[:resto][:new_genre_name] != \"\"\n  @resto.genre = Genre.find_or_create_by(name: params[:resto][:new_genre_name] )\nend\n\n# Note: ligne 2 is equivalent to the following:\nif params[:resto][:new_genre_name].blank?\n  @genre = Genre.find(params[:resto][:genre_id])\nelse\n  @genre = Genre.create(name: params[:resto][:new_genre_name])\nend\n@resto.genre = @genre\n```\n\nFor the setting of the 'New comment on restaurant by client' query in the same page, the model _Comment_ has 2 foreign keys, and the method `create_client` does not work. The use the controller's method:\n\n```ruby\ndef create\n  @comment = Comment.new(comment_params)\n  if params[:comment][:client_new] != \"\"\n    @comment.client = Client.find_or_create_by(name: params[:comment][:client_new])\n  end\n  @comment.save\n  respond_to :js\nend\n```\n\n## Delete Ajax\n\n[Back to Contents](#readme)\n\nThe Delete method is Ajax rendered. The link calls the _restos#destroy_ method. It reads the query string with the _params hash_, then querries the database with the found _ID_ and delete it from the database.\n\nWe declared `dependent: :destroy` in the model; this is similar to `@resto.comments.destroy_all` so all associated objects will be deleted together with the parent.\n\nThen the link has the attribute `remote: true`, so the method will respond to with _destroy.js.erb_ to render dynamically\nthe view in the browser. To update the view, namely delete a row, we need to select it with Javascript so we need to pass the ID information\nfrom Rails to Javascript to be able to remove the correct row. We use datasets for this. When Rails renders the HTML, Rails will write the IDs given by the database in a dataset for every object, with the HTML.ERB code:\n\n```\n\u003ctr data-resto-id = '\u0026lt%=resto.id%\u003e'\u003e\n```\n\n(we used a `\u003ctable\u003e` to present the data above).\n\nSince we use the file format _js.erb_, this file will be firstly parsed by Rails and then Javascript. The code of this file is:\n\n```js\ndocument.querySelector('[data-resto-id = \u0026lt%= @resto.id %\u003e\"]').remove();\n```\n\nIn the first parse, Rails _restos#destroy_ knows the instance `@resto` and will put the 'real' value for `\u0026lt%= @resto.id %\u003e`, say \"13\" for example. Then Javascript reads the string `data-resto-id = \"13\"`, finds the correct `\u0026lttr\u003e` in the DOM, and acts with `.remove()`. Et voilà.\n\n## fetch with response text\n\nThe list of clients is rendered by the controler _clients#index_ in the view _/views/clients/index.html.erb_. On page load, we ask the controller to return an empty array, and there is a button to display all of them. To do so, we used a dummy query string pointing to `http://localhost:3000/clients` with params `?=c=\" \"` so that the controller can respond with `Client.all.includes(comments: {resto: :genre})` when the button is trigger. We ask the controller to serve the collection of clients in format _text_ with a partial with no layout, with `render partial: 'clients/client', collection: @clients, layout: false` so Rails sends a prefilled text response to the browser. Then we have a Javascript method `fetch()` that reads the response and parses it into _text_ format, and inserts inot the DOM.\n\n\u003e we can use the method `.innerHTML` here (otherwise, only `.insertAdjacentHTML`)\n\n```js\nconst fetchClients = (tag) =\u003e {\n  document.querySelector(tag).addEventListener(\"click\", async (e) =\u003e {\n    e.preventDefault();\n    try {\n      const query = await fetch('/clients?c=\"\"', {\n        method: \"GET\",\n        headers: {\n          \"Content-Type\": \"text/html\",\n          Accept: \"text/html\",\n        },\n        credentials: \"same-origin\", // default value\n      });\n      if (query.ok) {\n        const content = await query.text();\n        return (document.querySelector(\"#client_articles\").innerHTML = content);\n      }\n    } catch (error) {\n      throw error;\n    }\n  });\n};\n```\n\n[Back to Contents](#readme)\n\n## Drag Drop\n\n[Back to Contents](#readme)\n\n- we need to add the _draggable_ attribute to the node we want to make draggable\n- we add a listener on the _dragstart_ event to capture the start of the drag and capture data in the _DataTansfer_ object. The `dataTransfer.setData()` method sets the data type and the value of the dragged data. We can only pass a string in it so we stringify the object we pass.\n\n```js\ndocument.addEventListener(\"dragstart\", (e) =\u003e {\n    // we define the data that wil lbe transfered with the dragged node\n    const draggedObj = {\n      idSpan: e.target.id,\n      resto_id: e.target.dataset.restoId,\n    };\n    e.dataTransfer.setData(\"text\", JSON.stringify(draggedObj));\n```\n\nBy default, data/elements cannot be dropped in other elements. To allow a drop on an element, it needs:\n\n- to listen to the _dragover_ event to prevent the default handling of the element,\n\n```js\ndocument.addEventListener(\"dragover\", (e) =\u003e {\n  e.preventDefault();\n});\n```\n\n- listen to the _drop_ event: here we accept to drop on an element that has the class \"drop-zone\". We can then use the data contained in the _DataTransfer_ object with the `dataTransfer.setData()` method.\n\nThen, for a _drop_ event, we construct an object `data={resto:{genre_id:\"value\", id:\"value}}` and transmit it to the Rails backend with a `fetch()` with _POST_. This will update the dragged element with it's property (_resto_id_ with correct _genre_id_) and persist to the database. This is done by calling a dedicated method of a Rails controller.\n\n```js\ndocument.addEventListener(\"drop\", async (e) =\u003e {\n    e.preventDefault();\n    // permits drop only in elt with class 'drop-zone'\n    if (e.target.classList.contains(\"drop-zone\")) {\n      const transferedData = JSON.parse(e.dataTransfer.getData(\"text\"));\n\n      const data = {\n        resto: {\n          genre_id: e.target.parentElement.dataset.genreId,\n          id: transferedData.resto_id,\n        },\n      };\n      // the method `postGenreToResto` is a `fetch`with a Rails ended-point defined after\n      await postGenreToResto(data).then((data) =\u003e {\n        if (data) {\n          // status: ok\n          e.target.appendChild(document.getElementById(transferedData.idSpan));\n        }\n      });\n    }\n  });\n}\n\n```\n\n### Fetch POST\n\nWe need to get the _csrf token_ from the session given by Rails since it is an internal request and Rails serve as an API.\n\nWe define a custom route that likes to a method that updates the params sent by the `fetch()` Javascript method.\n\n```ruby\n# routes\npatch 'updateGenre', to:'restos#updateGenre'\n```\n\nThe method _updateGenre_ simply reads the _params_ hash (formatted as `{resto:{id: value, name: value}}`) and saves it to the database.\n\n```js\nimport { csrfToken } from \"@rails/ujs\";\n\nconst postGenreToResto = async (obj = {}) =\u003e {\n  try {\n    const response = await fetch(\"/updateGenre\", {\n      method: \"PATCH\",\n      headers: {\n        Accept: \"application/json\",\n        \"X-CSRF-Token\": csrfToken(), // for CORS since it is an internal request\n        //document.getElementsByName(\"csrf-token\")[0].getAttribute(\"content\"),\n        \"Content-Type\": \"application/json\",\n      },\n      credentials: \"same-origin\", //if authnetification with cookie\n      body: JSON.stringify(obj),\n    });\n    return await response.json();\n  } catch (err) {\n    console.log(err);\n  }\n};\n```\n\n### Fetch DELETE and tabindex attribute\n\nTo delete a 'type', we first need to read/find it, and then transmit the data to a Rails end-point by a `fetch()`DELETE method. Then the Rails bakcned will try to delete it (depending upon the validations), and then upon success, the Javascript will remove (or not) the element from the DOM.\n\n\u003e We add a _tabindex_ attribute `tabindex=\"0\"` to make the 'type\" element clickable.\n\n\u003e We can then listen to a _clic_ event with the `document.activeElement`\n\nWe use `activeElement` because the list of clickable items can change, so we couldn't use a _querySelector_ which acts on a fixed list. With an `document.activeElement` that corresponds to a certain 'type' (we added a class _genre_tag_ to find the 'type' items only), we have access to the properties of the item.\n\n```js\nfunction listenToGenres() {\n  document.querySelector(\"#tb-genres\").addEventListener(\"click\", () =\u003e {\n    const item = document.activeElement;\n    if (item.classList.contains(\"genre_tag\")) {\n      document.querySelector(\"#hiddenId\").value =\n        item.parentElement.parentElement.dataset.genreId;\n      document.querySelector(\"#genre_to_delete\").value = item.textContent;\n    }\n  });\n}\n```\n\nThen we save the _id_ in a hidden input. On submit, the `fetch()` DELETE query is triggered.\nTthe Rails end-point is defined by a custom route:\n\n```ruby\ndelete 'deleteFetch/:id', to: 'genres#deleteFetch'\n```\n\nThe _deleteFetch_ method renders: `render json: {status: :ok}` so the `fetch`method will process it and react upon success.\n\n```js\nfunction destroyType() {\n  document\n    .querySelector(\"#genreDeleteForm\")\n    .addEventListener(\"submit\", async (e) =\u003e {\n      e.preventDefault();\n      const id = document.querySelector(\"#hiddenId\").value;\n      try {\n        const query = await fetch(\"/deleteFetch/\" + id, {\n          method: \"DELETE\",\n          headers: {\n            Accept: \"application/json\",\n            \"X-CSRF-Token\": csrfToken(),\n            \"Content-Type\": \"application/json\",\n          },\n          credentials: \"same-origin\",\n        });\n        const response = await query.json();\n        if (response.status === \"ok\") {\n          document.querySelector(`[data-genre-id=\"${id}\"]`).remove();\n          document.querySelector(\"#genreDeleteForm\").reset();\n        }\n      } catch {\n        (err) =\u003e console.log(\"impossible\", err);\n      }\n    });\n```\n\n## Error rendering\n\n[Back to Contents](#readme)\n\nBrowser validation `required: true` with the setup `config.browser_validations = true` used with _simple_form_for_ in _#config/initializers/simple_form.rb_\n\nWith _Simple_Form_, we just need:\n\n```ruby\n\u003c%= simple_form_for @comment do |f|\u003e\n \u003c%= f.error_notification %\u003e\n ....\n```\n\nbut with _form_with_, we may need to add:\n\n```ruby\n\u003c%= form_with model: @comment do |f| %\u003e\n\u003c%= render 'shared/errors', myvar: f.object %\u003e\n...\n```\n\nwhere:\n\n```ruby\n#shared/_erros.html.erb\n\u003c% if myvar.errors.any? %\u003e\n    \u003cdiv\u003e\n      \u003ch2\u003e\u003c%= pluralize(myvar.errors.count, \"error\") %\u003e prohibited this item from being saved:\u003c/h2\u003e\n\n      \u003cul\u003e\n        \u003c% myvar.errors.full_messages.each do |message| %\u003e\n          \u003cli\u003e\u003c%= message %\u003e\u003c/li\u003e\n        \u003c% end %\u003e\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  \u003c% end %\u003e\n```\n\nTo render errors when the form is AJAX submitted, we can do:\n\n```js\nif (\u003c%= @myobject.errors.any? %\u003e) {\n    const myDivAboveTheForm = document.querySelector(\"#myDivAboveTheForm\")\n    myDivAboveTheForm.innerHTML = \"\"\n    myDivAboveTheForm.insertAdjacentHTML('beforeend',`\u003c%= j render 'restos/form' %\u003e`)\n\n} else {\n    ....do something\n}\n```\n\n## Kaminari AJAX\n\n[Back to Contents](#readme)\n\nInstallation: put `gem kaminari` in _gemfile_, `bundle`, and run `rails g kaminari:config`: this generates the default configuration file into _config/initializers_ directory. We set here:\n\n```ruby\n#/config/initializers/kaminari_config.rb\nKaminari.configure do |config|\n  config.default_per_page = 5\nend\n```\n\nWe tweaked the pagination helper with Bootstrap4 template them, running `rails g kaminari:view bootstrap4`.\n\n```ruby\nclass CommentsController \u003c ApplicationController\n\n  # GET /comments\n  def index\n    @comments = Comment.includes([:resto]).page(params[:page])\n    respond_to do |format|\n      format.js\n      format.html\n    end\n  end\n```\n\nIn the _index.html.erb_ views of _Comments_, we add the pagination link and extract a partial of the data that will be paginated (namely the `\u003ctbody\u003e` part)\n\n```\n# views/comments/index.html.erb\n\u003cdiv id=\"paginator\"\u003e\n    \u003c%= paginate(@comments, remote: true)  %\u003e\n\u003c/div\u003e\n\n\u003ctable\u003e\n  \u003cthead\u003e\n  ...\n  \u003ctbody id=\"tb-comments\"\u003e\n    \u003c%= render 'comments/table_comments', comments: @comments %\u003e\n  \u003c/tbody\u003e\n  \u003c/thead\u003e\n\u003c/table\u003e\n```\n\nWe extract in a partial the body of the table that will be paginated\nwhere the partial that iterates oever `@comments = Comment.all`:\n\n```\n# /views/comments/_table_comments.html.erb\n\u003c% comments.each do |comment| %\u003e\n    \u003ctr data-comment-id= \"\u003c%= comment.id %\u003e\" \u003e\n    \u003ctd contenteditable=\"true\" data-editable=\"\u003c%= comment.id %\u003e\"\u003e\u003c%= comment.comment %\u003e \u003c/td\u003e\n    \u003ctd\u003e\u003c%= comment.resto.name %\u003e\u003c/td\u003e\n    ...\n```\n\nAnd we create a file _index.js.erb_ that contains:\n\n```js\ndocument.querySelector(\"#tb-comments\").innerHTML = \"\";\ndocument.querySelector(\n  \"#tb-comments\"\n).innerHTML = `\u003c%= j render 'comments/table_comments', comments: @comments %\u003e`;\ndocument.querySelector(\n  \"#paginator\"\n).innerHTML = `\u003c%= j paginate(@comments, remote: true)%\u003e`;\n```\n\nEt voilà.\n\n## Setup\n\n[Back to Contents](#readme)\n\n### Database model\n\n![Database](app/assets/images/db-schema.jpeg)\n\n```bash\n\u003e rails g model genre name\n\u003e rails g model resto name comments_count:integer genre:references\n\u003e rails g model comment comment resto:references\n\u003e rails g model client name comment:references\n\u003e rails db:create db:migrate\n```\n\n```sql\n#postgresql\nTable Genres as G {\n  id int [pk, increment] // auto-increment\n  name varchar\n  created_at timestamp\n}\n\nTable Restos as R {\n  id int [pk]\n  name varchar\n  comments_count integer\n  genre_id int [ref:\u003e G.id]\n  created_at timestamp // inline relationship (many-to-one)\n}\n\n\n\nTable Comments as Co {\n  id int [pk]\n  comment varchar\n  resto_id int [ref: \u003e R.id]\n  client_id int [ref: \u003e Cl.id]\n  created_at_at timestamp\n }\n\nTable Clients as Cl {\n  id int [pk]\n  name varchar\n  create_at timestamp\n}\n```\n\n### Counter cache\n\nIn the view _#views/restos/index.html.erb_, we have an iteration with a counting output `\u003ctd\u003e \u003c%= resto.comments.size %\u003e\u003c/td\u003e`. If we use `count`, we fire an SQL query. We can use _counter_cache_ to persist the count in the database and Rails will update the counter for us whenever a comment is added or removed.\n\n\u003e Add `counter_cache` in the _child_ model (`Comment`here).\n\n```ruby\nclass Comment \u003c ApplicationRecord\n  belongs_to :resto, counter_cache: true\n  # requires a field comments_count to the Resto model\n  validates :comment, length: {minimum: 2}\nend\n```\n\n\u003e Add a field `comments_count` to the _parent_ model (`Resto`model here).\n\n```\nrails g migration AddCommentsCountToRestos comments_count:integer\nrails db:migrate\n```\n\n\u003e Note: to count the number of comments by restaurant with SQL/Ruby, we do:\n\n```sql\nJOINS( 'restos' )\n.SELECT (\"restos.*, 'COUNT(\"comments.id\") AS comments_count')\n.GROUP('restos.id')\n```\n\n\u003chttps://blog.appsignal.com/2018/06/19/activerecords-counter-cache.html\u003e\n\n### Fontawesome\n\n```ruby\n# gemfile\ngem 'font-awesome-sass', '~\u003e 5.12'\n#application.scss (respect the order)\n@import \"font-awesome-sprockets\";\n@import \"font-awesome\";\n```\n\n### Bootstrap\n\n```bash\nyarn add bootstrap\n```\n\n```ruby\n#application.scss\n@import \"bootstrap/scss/bootstrap\";\n```\n\nand\n\n```bash\nrails generate simple_form:install --bootstrap\n```\n\n### Faker\n\n```ruby\n#gemfile\ngroup :development do\n  gem 'faker', :git =\u003e 'https://github.com/faker-ruby/faker.git', :branch =\u003e 'master'\nend\n```\n\n[Back to Contents](#readme)\n\n# Docker\n\n\u003chttps://www.codewithjason.com/dockerize-rails-application/\u003e\n\n### Misc\n\n- generate Rails new app with:\n\n```bash\nrails new nompapp --webpack --database:postgresql\n```\n\n- change application.css to a SCSS file so that I can use @import directive\n\n- to present a `select` ordered alphabetically:\n\n```ruby\nResto.all.order(name: :asc)\n```\n\n- list of pids using port 5432: `lsof -i :5432`\n- kill this \u003cPID\u003e\n\n```bash\nkill -9 $(lsof -i tcp:3000 -t)\n```\n\n- PostgreSQL\n  Run this command to manually start the server:\n\n```bash\nbrew services start/stop/restart postgresql\npg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start\n```\n\nStart manually:\n\n```bash\npg_ctl -D /usr/local/var/postgres start\n```\n\nStop manually:\n\n```bash\npg_ctl -D /usr/local/var/postgres stop\n```\n\nStart automatically:\n\n\"To have launchd start postgresql now and restart at login:\"\n\n```bash\nbrew services start postgresql\n```\n\n```bash\nRBENV_VERSION=2.6.5 gem install irb\n```\n\n- !!! Prefer PostgresApp, no automatic launch of Postgres.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fdynamic-ajax-forms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fndrean%2Fdynamic-ajax-forms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndrean%2Fdynamic-ajax-forms/lists"}