I'm developing a Svelte frontend app which communicates with rails API server. I like the rails way of abstraction (esp. with simple_form gem) and I'd like to bring some ideas to Svelte. The particular problem I met is how to associate errors from the rails server with inputs on the svelte side
First, I tuned up error responses from the server; in general, they look like this:
{ errors:
{ generic: [<messages without association with particular field>],
email: ["Already taken", <msg_2>, ...],
password: ["Too short", ...],
field_N: [...]
}
}
generic errors can be "Invalid Login Credentials", "Your account is locked", "Internal Server Error", etc. Other errors must be related to the form fields and must be displayed next to them.
Here is my current approach signup.svelte:
<script>
let email;
let password;
function registerRequest() {
let regURL = baseURL+'/api/users/';
let data = {"user": {"email": email, "password": password}};
fetch(regURL, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
.then(function(response) {
if (!response.ok) {
return response.json()
}
})
// if errors present then iterate through errors object
.then(function(error) {
// find id of the message field and insert actual messages
for (let [key, value] of Object.entries(error.errors)) {
document.getElementById(key).insertAdjacentHTML('beforeend',value);
}
});
}
</script>
<style> .errormessage { color: red; } </style>
<form class="col-sm-12 col-lg-6" on:submit|preventDefault={registerRequest}>
<!-- here I need to create errormessage container for each field. -->
<!-- first one is generic to display generic errors -->
<div class="errormessage" id="generic"></div>
<label for="UserEmail">Email address</label>
<input type="email" class="form-control" id="UserEmail" aria-describedby="emailHelp" placeholder="Enter your email here..." bind:value={email}>
<div class="errormessage" id="email"></div>
<label for="UserPassword">Password</label>
<input type="password" class="form-control" id="UserPassword" placeholder="...and your password here" bind:value={password}>
<div class="errormessage" id="password"></div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
I want to unify the code above with frontend validation. If the errors
object is presented - show messages next to the fields with errors. It seems to me that using getElementById
is overkill. Why should I use it if my DOM never reloading? Every time on input
it will search for ids. Maybe it should be listener that listens changes of the errors object? I tried to bind the custom event
with Svelte
import { createEventDispatcher } from 'svelte';
, but with no luck.
Please help and share your thoughts.
Your intuition is right that using getElementById
in a Svelte component feels off - generally Svelte wants you to make you code declarative, where the DOM is a function of the state, rather than imperative where you're manually changing the DOM.
For the error messages, I'd suggest having an errors
variable in your component. When server errors come back, you can assign them to errors
, which will cause a reactive update and re-render the component. Then the markup can know to render errors when they exist, without you having to explicitly change the DOM. Something like this:
<script>
let email;
let password;
// New `errors` variable.
let errors = {};
function registerRequest() {
// Reset the errors when the user submits a new request.
errors = {};
let regURL = baseURL+'/api/users/';
let data = {"user": {"email": email, "password": password}};
fetch(regURL, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
.then(function(response) {
if (!response.ok) {
return response.json()
}
})
.then(function(error) {
// Just save the errors
errors = e.errors;
});
}
</script>
<style> .errormessage { color: red; } </style>
<form class="col-sm-12 col-lg-6" on:submit|preventDefault={registerRequest}>
<!-- first one is generic to display generic errors -->
<!-- note the `|| []` part, which makes sure that
`#each` won't throw an error when `errors.generic` doesn't exist -->
{#each (errors.generic || []) as error}
<div class="errormessage" id="generic">{error}</div>
{/each}
<label for="UserEmail">Email address</label>
<input type="email" class="form-control" id="UserEmail" aria-describedby="emailHelp" placeholder="Enter your email here..." bind:value={email}>
<!-- email errors -->
{#each (errors.email || []) as error}
<div class="errormessage" id="email">{error}</div>
{/each}
<label for="UserPassword">Password</label>
<input type="password" class="form-control" id="UserPassword" placeholder="...and your password here" bind:value={password}>
<!-- password errors -->
{#each (errors.password || []) as error}
<div class="errormessage" id="password">{error}</div>
{/each}
<button type="submit" class="btn btn-primary">Register</button>
</form>
Here's a Svelte REPL with that code working. Hope that helps!