Coder Social home page Coder Social logo

invoicing3's Introduction

README

Introduction

Application uses RoR for the backend and Svelte.js for the frontend. Designed to test out some new ideas on how to build a modern SPA using these. In order to make these work together it also uses Vite and Inertia.js. The approach was originally described Setting up a new Rails 7 app with Vite, Inertia, and Svelte. Some ideas, particularly around the structure of the Svelte setup within the Rails, are lifted from here: Creating your first Inertia Rails app. Adding Authentication to Inertia Rails app shows how to use Devise with Svelte for authentication. This shows how to use Devise for authentication. Once that is working, best to switch to using Pundit to perform authorization. This removes the need for authenication calls to Devise. Details in Adding Authorization and Flash Messages to Inertia Rails app.

In order to keep the Javascript side as easy as possible, we use npm in preferrence to yarn.

We use Docker to run the database. This allows us to have different databases for different branches.

Gotchas Found

Top level CSS file

Originally, I started with main.css being in the stylesheets directory. While this worked fine for development, when running in test mode, I was getting an error saying that main.css could not be found. Much Googling and experimentation later, discovered that Vite.js, when pre-building, requires the entry level css file (in my case main.css) to be pre-packed by vite.js, it needs to be in the entrypoints directory instead.

CSRF tokens in testing

Another issue I ran into while developing tests for the controllers (since replaced with system tests) is that I was getting errors. Have much digging, I discovered that CSRF generation must be enabled in config/environments/test.rb:

  # Enable request forgery protection in test environment as required by Svelte
  config.action_controller.allow_forgery_protection = true

Feature vs System tests

I was struggling to get complete end-to-end testing working with Devise. After much researching, I discovered that feature tests and system tests are distinct and different. Feature tests were introduced as a method in Rspec when Rails did not yet support the concept. System tests were introduced in the core Rails 5 system.

Most of the on-line guides about how to get Devise working using RSpec for doing end-to-end tests describe it using Feature test. However, feature tests are much more fidely to setup. By contrast, system tests generally work well out-of-the-box. Devise and Warden also provides some nice test helpers for system tests.

Forms

This has been a difficult area. I am using Bootstrap for styling. There are a few component libraries that provide nice styling with Bootstrap of forms - particularly to highlight errors. Sveltestrap and mdbsvelte are two I had a go at using. However, both expect Svelte to work in a specific way that is different from how Inertia.js does. In the end, I found that the best approach has been to use Svelte natively and use standard Bootstrap styling for showing errors.

Another confusing area around validation is which classes to apply where. Key conclusions from several hours of experimentation:

  • the form needs to include the novalidate tag. OTherwise, the browser may insert extra stuff in.
  • the form must not include the class 'was-validated' as this seems to set all fields by default to show as having passed validation, even when individual fields are flagged as failing validation
  • any field failing validation needs to have the class is-invalid set. This makes the surrounding box red and puts a warning in the box.
  • the error feedback <div> needs to have the class invalid-feedback

I have created my own component FormInput.svelte to provide error display. The ClientForm.svelte component now has two slot elements: navigation-elements that is a place to put buttons at the top of the form, and form-elements that is used to place extra fields into the form when required.

To make sure the correct info is passed from frontend to the backend, I make good use of Interia.js transform() callback ahead of sending the data.

DateTime and Date fields

Ruby (including RoR) has a class for Dates. Likewise, Javascript has a Date class. They also have corresponding DateTime classes. Sadly, neither the DOM nor JSON play nicely with these. In both cases, dates are represented as ISO8601 strings.

In the RoR backend we use the standard behaviour of the JSON Serializer to convert it to a string. When this is received by the frontend Svelte component (*Form.svelte) it is turned into a Javascript Date or DateTime object.

Because I didn't like the standard way that the browser form renders the selector for DateTime fields, I have created my own component DateTimeInput.svelte. This is passed the field holding the Javascript DateTime object. It unpacks the object into two fields: a Date field and a drop down for the Time field. Any changes in either field causes the Javascript object in the $form object to be updated.

On submitting the form, the whole $form object is simply sent to the backend.

My Capybara test was failing for the #new_rate_from field. Frustratingly, running in dev mode, it all seemed to work. After much experimentation, found that fill_in 'new_rate_from', with: '2028-02-26' was not actually leading to the correct element being updated in the $form object. Not sure why, but now I use a DOM Javascript query to extract the info before sending on to the backend.

Handling Currency types between frontend and backend

We use currency in the app. As everyone knows, it is bad to use a float to represent currency. Ruby has the Money gem which does a good job of managing currency. Javascript has a similar challenge, and there are a few good packages to allow manipulation of currency. After a few tries, I have alighted on the Dinero package. Dinero represents currency in an almost identical way to the Money gem (amount stored as integer in pence/cents, a currency field using ISO string). Sadly, the name of the fields differ between Money and Dinero.

To address this, the JSON generated by the backend uses the Dinero field names. Likewise, on the Post/Put from the frontend, the fields use the Dinero names. These are mapped into Money gem fields in the backend.

Similar to Date fields above, the $form object holds currencies using Dinero objects. The CurrencyFormInput.svelte converts between String representation with a '.' at the right position and the Dinero object that is stored in the $form.

SCSS/SASS Usage

I decided I wanted to use SCSS/SASS in the application. The primary driver was that I wanted to apply the same margins to all form labels on a specific page. This is not possible with standard CSS, but is relatively easy using the @extend operator in SCSS/SASS.

However, it turned into a bit of a nightmare. To be fare to both Svelte and Vite, they do provide easy mechanisms for getting SCSS integrated, just the documentation (and particularly Stackoverflow answers) are both out-of-date. I would see one of three outcomes:

  • under certain configurations the system would flag the @extend keyword would be flagged, telling me I needed to configue the SCCS pre-processor,
  • under other configurations the pre-processor would see the @extend but did not recognise the py-3 css class,
  • under yet another configuration the preprocessor would run, the py-3 css class, but when the page was rendered in the browser, no bootstrap styling had been applied.

My conclusions are that you almost need to treat the pre-complitation stage as a completely separate process to the serving the pages to the browser for rendering.

So here is how I got it working:

  • We need a svelte.config.js file to instruct Svelte to use the Vite preprocessor. This naturally adds the tools needed for SCSS/SASS preprocessing.
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

const config = {
  preprocess: [vitePreprocess({})],
};

export default config;
  • We need to make the following changes to the vite.config.js to provide options to the preprocessing engine so that the precessor knows about Bootstrap and can therefore @extend the css style.
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import 'bootstrap/scss/bootstrap'; `,
      },
    },
  },
  • Finally, in order to ensure that Bootstrap is served to the broswer, we need the application.html.erb to include the main.css ahead of serving the Svelte app itself:
    <%= vite_client_tag %>
    <%= vite_stylesheet_tag 'main.scss' %>
    <%= vite_javascript_tag 'application' %>

We are now able to do the following in the main css file:

@import "bootstrap/scss/bootstrap.scss";
@import '@fortawesome/fontawesome-free/css/all.min.css';

label {
  @extend .py-2;
}

What I was not able to get working was to apply the @extend at the individual page. I suspect, if I included the relevant Bootstrap style this would overcome the issue.

Dynamic Content

Here's the scenario: You have a page with a selector of the client on it, that modifies the content shown on the page. However, determining the content is an expensive operation such that you don't want to push content down for all clients but instead waint until the specific client has been selected.

In classical Svelte (if such a thing exists), you would use a combination of Javascript functions in the <script> section, combined with {@await} in the HTML. This is where Inertia.js approach is much more like that of a classical Rails app. So you define what parameters you will feed into the page in the script page.

<script>
  export let clients = undefined;                       // provided by backend.  List of clients with id, name, etc. 
  export let details_for_specific_client = undefined;   // to be provided on demand by the backend
  
  let client;                                           // set by the page
</script>

And in the main HTML section:

    <select>
      <select name="client" bind:value={client} on:change={change_client}>
        {#each clients as client}
          <option value={client}>
            {client}
          </option>
        {/each}
      </select>
    </select>

The above code will allow the controller to provide a list of clients and details for a specific client. However, they can be left undefined by the backend as well. The above code will invoke the function change_client() whenever the user selects a different client.

It is in this function that the magic of Inertia comes to the fore:

    function change_client() {
        router.get(window.location.href, { client: client }, { only: ["details_for_specific_client"] } );
    }

This asks Inertia to go back to the server, and request the page, but only load the information that will go into the details_for_specific_client variable. Note, the second argument of router.get() allows us to pass in the parameter of the client. The rest is classical Svelte. We will have HTML to render on the page the details_for_specific_client data. Svelte and Inertia between them will work out what parts of the page need re-rendering.

On the server side, we do something like:

  def new
    props = { clients: ClientSerializer.render_as_json(Client.order(:name), view: :short_details) }

    client = params.fetch(:client, nil)  // allows for invocation without client set.
    if client
      details_for_specific_client = get_the_relevant_client_details(); 
      props[:details_for_specific_client] = ClientDetailsSerializer.render_as_json(details_for_specific_client)
    end

    render inertia: 'Invoices/New', props: {client:, details_for_specific_client:}
  end

Selector for Child Relationships

For the Invoice, I want to be able to show a table of Client Sessions that have not yet been invoiced and allow the user to choose which ones to apply to an invoice. In implementing this, I learnt the power of a number of great Svelte features.

First is bind:group= which provides a simple way to tie a group of HTML input elements to an array. It looks something like this:

   {#each client_sessions as client_session}
        <tr>
            <td>
                <input type="checkbox" value="{client_session.id}" bind:group={client_session_ids} />
            </td>
            ... other elements
        </tr>
    {/each}

The other strong feature is the use of $: as a very simple way to make the Javascript reactive. I use this to create a Select All checkbox. The HTML looks like this:

    <input type="checkbox" id="select_all_client_sessions" bind:checked={all_checked}/>

With my Select All checkbox bound to the all_checked variable, a few lines of Javascript does the rest:

$: all_checked, change_client_sessions();
$: client_session_ids, reset_client_sessions();

function change_client_sessions() {
  client_session_ids = [];

  if (all_checked) {
    client_sessions.forEach( (client_session) => client_session_ids.push(client_session.id));
  }

  console.log(client_session_ids);
}

function reset_client_sessions() {
  all_checked = (client_session_ids.length===client_sessions.length);
}

Now, when I uncheck a Client Session the Select All is also unticked. And, if I select all Client Sessions individually, the Select All will become ticked.

Confirm Window for deletes

Its very standard to use a Browser Alert window to ask to confirm deleting a record, before you actually delete it. Unfortunately, there were no ready examples of Svelte/Inertia to show to do this, so I pieced this together from various bits of info.

Although, the normal recommendation is to use the Inertia Link component, I found I had to go back to use a standard HTML button element:

  <button class="btn btn-primary" type="button" on:click={() => confirmDelete({client_session})}>Delete</button>

This will invoke confirmDelete() when the button is clicked. The code for the event handler is:

function confirmDelete(client_session) {
  if (window.confirm('Confirm Delete?')) {
    router.delete('/client_sessions/'+client_session.client_session.id);
  }
}

router is an Inertia variable that provides direct access to the Inertia protocol. Note the need to use clientsession.clientsession to access the object. Not sure, why, but clearly something to do with Svelte/Inertia.

invoicing3's People

Contributors

peterbell215 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.