Tagged: web development Toggle Comment Threads | Keyboard Shortcuts

  • kmitov 10:10 am on December 10, 2020 Permalink |
    Tags: , , web development   

    Send array params to a rails server from a JavaScript code – URL and URLSearchParams 

    The goal was to filter building instructions on buildin3d.com by brand. We had to parse and create the URL on the client so I played around with URL and URLSearchParams (again). I will try to summarize the implementation here in the hope that you could help understand how URL and URLSearchParams work and how to use them with a Ruby on Rails app.

    Rails Server side request with array param

    The server accepts a request in the form:

    https://platform.buildin3d.com/instructions?in_categories[]=1&in_categories[]=2&in_categories[]=13

    This means we could pass an array with the ids of the categories. The result will return only the 3D assembly instructions that are for Brands in these categories.

    How to send the request on the client side

    On the client side we have an <ul> element with some <li> elements representing the categories.

    The brands filter is on the left.

    When we click on the brand we would like to add the brand to the URL and redirect the user to the new URL.

    So if the current url is

    https://platform.buildin3d.com/instructions?in_categories[]=1&in_categories[]=2

    and we select a new brand I would like to send the user to

    https://platform.buildin3d.com/instructions?in_categories[]=1&in_categories[]=2&in_categories[]=13

    How to add array params to the URL with JavaScript

    Here is the whole code of the Stimulus JS controller

    import { Controller } from "stimulus";
    
    /**
     * This is a controller used for filtering by brands [categories] on the materials index page
     *
     * @author Kiril Mitov
     */
    export default class extends Controller {
      static targets = ["tree"];
    
      connect() {
        console.log("connect");
        const scope = this;
        this.setFromUrl();
        this.treeTarget.addEventListener("click", e => {
          e.preventDefault();
          const li = e.target.closest("li");
          const input = li.querySelector("input");
          input.checked = !input.checked;
          scope.goToNewLocation();
        });
      }
    
      setFromUrl() {
        const url = new URL(window.location);
        const categoryIds = new URL(window.location).searchParams.getAll("in_categories[]")
    
        Array.from(this.treeTarget.querySelectorAll("li[data-category-id]"))
          .forEach(li => {
            const input = li.querySelector("input");
            const selected = categoryIds.indexOf(li.dataset["categoryId"]) != -1
            input.checked = selected;
          });
      }
    
      goToNewLocation() {
        const url = new URL(window.location)
        const searchParams = url.searchParams;
        searchParams.delete("in_categories[]");
        Array.from(this.treeTarget.querySelectorAll("li[data-category-id]"))
          .filter(li => li.querySelector("input").checked)
          .forEach(li => searchParams.append("in_categories[]",li.dataset["categoryId"]))
        searchParams.sort();
        window.location = url.toString();
      }
    }
    

    There are a few important things in the code

    Opening the page on a new location

    window.location = url.toString()

    This will open the new page for the user

    Adding the selected brands to the url search query

    We listen for an event of click from the user and we get a list of all the checked brands.

    goToNewLocation() {
        const url = new URL(window.location)
        const searchParams = url.searchParams;
        searchParams.delete("in_categories[]");
        Array.from(this.treeTarget.querySelectorAll("li[data-category-id]"))
          .filter(li => li.querySelector("input").checked)
          .forEach(li => searchParams.append("in_categories[]",li.dataset["categoryId"]))
        searchParams.sort();
        window.location = url.toString();
      }

    First we delete the param “in_categories[]”. We use URLSearchParams.delete. This removes the param from the searchParam and if we then call .toString() we would receive the new search query without this param.

    Then we call this.treeTarget.querySelectorAll to filter all the checkboxes and then only the check once and we append “in_categories[]” param for every selected checkbox. This is what the server requires.

    searchParams.append("in_categories[]",li.dataset["categoryId"])
    

    As a result with have the query

    https://platform.buildin3d.com/instructions?in_categories[]=1&in_categories[]=2

    Because we use only URLSearchParam.append and URLSearchParam.delete all the other params are still in the search query.

    As a summary:

    We have a Ruby on Rails server that accepts an array param and we form this array param in a Stimulus JS controller. We use URLSearchParams method to append and delete params, as this will preserve the other params that are already in the url.

     
  • kmitov 6:18 am on November 30, 2020 Permalink |
    Tags: , , , web development   

    Testing an index page in a web application where we depend on the order. 

    [Everyday Code]

    Today the specs for an index page failed and I had to improve it. I decided to share a little trick for when we depend on the order of the objects.

    The specs is for the /subscriptions page where we show an index of all the subscriptions. We order the subscriptions by created_at in DESC. There are 20 subscriptions on the page. Then you must got to the next page. In the spec we open the page and check that there is a link to our subscription.

    visit "/admin/user/subscriptions"
    
    expect(page).to have_link subscription.random_id.to_s, href: "/admin/user/subscriptions/#{subscription.to_param}/edit"

    The problem was that there are other specs which for some reason create subscriptions into the future. This means that at a certain point in time when more than 20 subscriptions are created into the future in the DB, then our spec will fail. It will fail because the newly created subscription is on the second page.

    All the subscriptions on this page are into the future as today is 2020-11-30. So our newly created subscription is not here.

    What are our options?

    Move to the correct page in the spec

    This is an option. It will require us to have a loop in the spec that would more to the next page in the index and search for each subscription. This is too much logic for a spec.

    Delete all the future subscriptions before starting the spec

    Could be done. But is more logic for the spec. It actually needs to delete subscriptions and this is not the job of this specs.

    Create a subscription that is with the most recent created_at

    It is simple.

      let(:subscription) {FactoryBot.create(:subscription, created_at: subscription_created_at_time)}
    
      def subscription_created_at_time
        (Subscription.order(:created_at).last.try(:created_at) || Time.now)+1.second
      end
    
    

    It is the simpler change for this spec. Just create a subscription that’s last. In this way we know it will appear on the first page even when other specs have created subscriptions into the future.

     
  • kmitov 9:39 pm on November 28, 2020 Permalink |
    Tags: , , , , web development   

    Why we should never clear our DB before/after running specs. 

    One common “mistake” I’ve seen a couple of times is to clean the Database before/after specs are run. It seems to be a common practice with reasonable arguments. I think this is a bad idea. Here is why and what we should do instead.

    Why is the DB cleared before/after the specs

    When running specs that need access to a DB we might have to create a User or an Article or a Project model, then connect them in a certain way and test the business logic of our spec. After the spec is finished it is not wise to delete these objects from the DB directly in the spec. Sometimes it takes additional time, sometimes it executes additional logic. In most cases you don’t clear the DB after each and every spec.

    It is a good idea to clean the db before all the specs or after all the specs if they are successful. In this way we reset the DB only once, it saves some time and is much cleaner because you can plug in this behavior if you want to.

    Why the DB should not be cleared before/after the specs

    The simple answer is that our code will never, absolutely never work on a clean db in a production. If we have a test procedure that runs the specs against a clean and empty db they might pass when the db is clean. But what use do we have from code that could work in a clean environment, but could not work in a real production environment. The answer is – non.

    We don’t clean our db before/after each spec. In this way we’ve been able to track some really nasty bugs. Like slow queries that are slow only when you have too many users. Other cases involve special relations that are built in time. Like users that are part of an organization and the organization was once having one check for uniqueness of the user and now it has another check. Because the db is not cleared every time we make sure that it is properly migrated with all the needed migrations.

    We found out that a 7 years out test db that is not cleared is closer to a 7 years old production db.

    The test db is not the production db

    The test db is not the production db. It might have the same scheme, that is for sure, but the amount of data in them and the complexity of this data is different. What we need is code that could run on a production db. There is no use of any code that could run only in test environment.

    So here is what we do:

    We export the production db, we change some data like user emails, names and any other sensitive data and we import it as a test db. We run the specs on this db.

    In this way we make sure that the code could actually run on a real db before deploying it.

     
c
compose new post
j
next post/next comment
k
previous post/previous comment
r
reply
e
edit
o
show/hide comments
t
go to top
l
go to login
h
show/hide help
shift + esc
cancel