Skip to content

on

This feature is responsible for initiating requests to the server.

It supports the following request types:

  • XHR requests, including form submissions
  • full page transitions
  • History API navigation

Its only responsibility is configuring and sending the request. What happens after the request is outside the scope of this feature.


All XHR requests include an X-Requested-With header set to XMLHttpRequest, allowing the server to distinguish XHR requests from full page navigation when needed.

KEML sets a permanent cookie named tzo, containing the browser timezone after the first load of any page that includes the KEML runtime, all subsequent requests will include this cookie.

KEML relies heavily on and benefits from the existing web stack, without overriding its semantics or behavior, including the standard browser page caching mechanisms.

Thus, if your server is smart about sending the correct caching response headers at the correct time - that can help speed up KEML even more.

That's right, your application's performance optimizations also live on the server 🤯.


The on attribute is the only required attribute for an element to become capable of initiating requests to the server.

Even though most examples here show on being used on the same element as on:*, that is only for brevity and simplicity's sake. It can appear on any number of elements at once.

on provides a set of optional attributes for customizing its behavior. These are covered in the sections below.


Endpoint+Method Configuration

The default endpoint is an empty string ("").

The default HTTP method is GET.

Given the current URL:

1
https://www.example.com/some/path
Configuration Result
(empty) https://www.example.com/some/path/
list-todo or ./list-todo https://www.example.com/some/path/list-todo/
../list-todo https://www.example.com/some/list-todo/
/list-todo https://www.example.com/list-todo/
/file.html https://www.example.com/file.html

Automatic normalization:

  • paths ending in a file extension → exactly zero trailing slashes
  • paths without a file extension → exactly one trailing slash

get, href, action and src

These attributes override the default endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<button
  type="button"
  class="btn"
  on:click="submitOverrideGetEndpoint"
  on="submitOverrideGetEndpoint"
  action="/endpoint-override"
  result="overrideGetEndpointResult"
>
  click me
</button>

<div render="overrideGetEndpointResult"></div>
1
2
3
4
5
6
7
8
<small class="chip mv3">Request received</small>

<dl class="dl">
  <dt>Endpoint:</dt>
  <dd>{ server.url.pathname }</dd>
  <dt>Method:</dt>
  <dd>{ server.method }</dd>
</dl>

post, put and delete

These attributes override both the default endpoint and the default HTTP method.

The method they set is the same as the attribute name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<button
  type="button"
  class="btn"
  on:click="submitOverridePostEndpoint"
  on="submitOverridePostEndpoint"
  post="/endpoint-override"
  result="overridePostEndpointResult"
>
  click me
</button>

<div render="overridePostEndpointResult"></div>
1
2
3
4
5
6
7
8
<small class="chip mv3">Request received</small>

<dl class="dl">
  <dt>Endpoint:</dt>
  <dd>{ server.url.pathname }</dd>
  <dt>Method:</dt>
  <dd>{ server.method }</dd>
</dl>

method

This attribute overrides just the default HTTP method. The provided value will be converted into all uppercase.

There are no restrictions on the value — it can be anything as long as your server understands the verb.

It has higher precedence than the attributes shown above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<button
  type="button"
  class="btn"
  on:click="submitOverrideMethod"
  on="submitOverrideMethod"
  put="/endpoint-override"
  method="custom"
  result="overrideMethodResult"
>
  click me
</button>

<div render="overrideMethodResult"></div>
1
2
3
4
5
6
7
8
<small class="chip mv3">Request received</small>

<dl class="dl">
  <dt>Endpoint:</dt>
  <dd>{ server.url.pathname }</dd>
  <dt>Method:</dt>
  <dd>{ server.method }</dd>
</dl>

debounce and throttle

These attributes can be used to rate limit the on feature.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<h4 class="mt0">
  The request is only sent when you stop typing for two seconds.
</h4>

<input
  name="message"
  autocomplete="off"
  class="input"
  on:input="submitExpensive"
  on="submitExpensive"
  debounce="2000"
  src="/expensive"
  result="expensiveResult"
>

<div render="expensiveResult"></div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<small class="chip mt3">Complete</small>

<p>Imagine that this value was hard to compute!</p>

<dl class="dl">
  <dt>You said:</dt>
  <dd>{ server.getParam("message") }</dd>
  <dt>Answer:</dt>
  <dd>42</dd>
</dl>

The request is only sent when you stop typing for two seconds.


Request Data

Forms submit their fields the same way they always did in HTML, including native field validation rules.

Custom elements (e.g. Web Components) can also be made validatable; all they must do is implement the standard checkValidity method.

What's more, any element inside the one sending the request, including the element itself, can contribute values to the request data by specifying a name and a value attribute. For non-form fields, both attributes must be provided.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<h4 class="mt0">Enter your name and submit the form</h4>

<form
  action="/form-submit"
  on:submit="submitForm"
  on="submitForm"
  result="formSubmitResult"
>
  <label>
    Name:
    <!-- no KEML-specific attributes on the input -->
    <input
      type="text"
      name="name"
      autocomplete="off"
      required
      class="input"
    >
  </label>

  <button class="btn mt2">submit</button>
</form>

<div render="formSubmitResult"></div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<small class="chip mt3">Received</small>

<p>
  Form submissions may not be exciting, but where would the web be without them?
</p>

<dl class="dl">
  <dt>Hello:</dt>
  <dd>{ server.getParam("name") }</dd>
</dl>

Enter your name and submit the form


Form fields can even submit themselves without a form at all.

This example works exactly the same.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<h4 class="mt0">Enter your name and submit the field</h4>

Name:
<!-- Look Ma, no form! -->
<input
  type="text"
  name="name"
  autocomplete="off"
  required
  class="input"
  action="/form-submit"
  on:keydown="submitField"
  event:keydown="key = Enter"
  on="submitField"
  result="fieldSubmitResult"
>

<button
  type="button"
  class="btn mt2"
  on:click="submitField"
>
  submit
</button>

<div render="fieldSubmitResult"></div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<small class="chip mt3">Received</small>

<p>
  Form submissions may not be exciting, but where would the web be without them?
</p>

<dl class="dl">
  <dt>Hello:</dt>
  <dd>{ server.getParam("name") }</dd>
</dl>

Enter your name and submit the field

Name:

It does not even have to be a form field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<h4 class="mt0">A div huh 🤔</h4>

Name:
<div
  name="name"
  value="Batman"
  class="input"
  action="/form-submit"
  on="submitDiv"
  result="divSubmitResult"
>
  I'm Batman!
</div>

<button
  type="button"
  class="btn mt2"
  on:click="submitDiv"
>
  submit
</button>

<div render="divSubmitResult"></div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<small class="chip mt3">Received</small>

<p>
  Form submissions may not be exciting, but where would the web be without them?
</p>

<dl class="dl">
  <dt>Hello:</dt>
  <dd>{ server.getParam("name") }</dd>
</dl>

A div huh 🤔

Name:
I'm Batman!

Request Headers

Request headers can be set using:

h-<header name>="header value"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<button
  type="button"
  class="btn"
  on:click="sendHeaders"
  on="sendHeaders"
  href="/request-headers"
  h-batman="Bruce Wayne"
  result="headersResult"
>
  Send Message
</button>

<div render="headersResult"></div>
1
2
3
4
5
6
7
8
<small class="chip mt3">Received</small>

<p>Just don't tell anyone 😀</p>

<dl class="dl">
  <dt>Secret identity:</dt>
  <dd>{ server.headers.get("batman") }</dd>
</dl>

credentials

RTFM (just kidding)

This is simply the standard mechanism for including credentials (e.g. cookies) when making requests to another domain, where they would normally not be included for security reasons.

Make sure you know what you're doing if you're using this.

This is a boolean attribute, so its value is ignored and only the presence determines whether or not the credentials are enabled.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<button
  type="button"
  class="btn"
  on:click="sendCredentials"
  on="sendCredentials"
  href="https://www.example.com/nefarious"
  credentials
  result="credentialsResult"
>
  Log In
</button>

<div render="credentialsResult"></div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<small class="chip mt3">Ha!</small>

<p>What?!</p>

<p>You were expecting to see something, weren't you?</p>

<p>But the deed is done.</p>

<dl class="dl">
  <dt>Credentials included:</dt>
  <dd>{ server.withCredentials ? "Yes" : "No" }</dd>
</dl>

redirect

This attribute switches the operational mode of on from sending requests to performing redirects.

The endpoint resolution logic remains the same. Headers and HTTP method are ignored. Form data (excluding file uploads) is applied to the query string.


assign

This option performs a full page navigation.

1
2
3
4
5
6
7
8
<a
  href="/page-b"
  on:click="redirectAssignA"
  on="redirectAssignA"
  redirect="assign"
>
  Go to page B
</a>
1
2
3
4
5
6
7
8
<a
  href="/page-c"
  on:click="redirectAssignB"
  on="redirectAssignB"
  redirect="assign"
>
  Go to page C
</a>
1
2
3
<p>Congrats you have reached the last page 🎉</p>

<p>Now use the browser buttons to go back and forth.</p>
https://www.assign-example.com/page-a/

replace

This option performs a full page navigation, but replaces the current history entry instead of adding a new one.

1
2
3
4
5
6
7
8
<a
  href="/page-b"
  on:click="redirectReplaceA"
  on="redirectReplaceA"
  redirect="assign"
>
  Go to page B
</a>
1
2
3
4
5
6
7
8
<a
  href="/page-c"
  on:click="redirectReplaceB"
  on="redirectReplaceB"
  redirect="replace"
>
  Replace this page with page C
</a>
1
2
3
<p>Congrats you have reached the last page 🎉</p>

<p>Now use the browser buttons to go back and forth.</p>
https://www.replace-example.com/page-a/

pushState and replaceState

These options work exactly the same, but use the History API and do not cause a full page navigation.

Oh, and did I forget to mention that you get SSR for free? It is not some separate thing you have to implement, and it does not impose restrictions on your server infrastructure 🤯.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<a
  href="/page-a"
  class="mr3"
  on:click="redirectPushStateA"
  on="redirectPushStateA"
  redirect="pushState"
>
  Push A
</a>

<a
  href="/page-b"
  class="mr3"
  on:click="redirectPushStateB"
  on="redirectPushStateB"
  redirect="pushState"
>
  Push B
</a>

<a
  href="/page-c"
  class="mr3"
  on:click="redirectPushStateC"
  on="redirectPushStateC"
  redirect="pushState"
>
  Push C
</a>
|
<a
  href="/page-a"
  class="mh3"
  on:click="redirectReplaceStateA"
  on="redirectReplaceStateA"
  redirect="replaceState"
>
  Replace A
</a>

<a
  href="/page-b"
  class="mr3"
  on:click="redirectReplaceStateB"
  on="redirectReplaceStateB"
  redirect="replaceState"
>
  Replace B
</a>

<a
  href="/page-c"
  on:click="redirectReplaceStateC"
  on="redirectReplaceStateC"
  redirect="replaceState"
>
  Replace C
</a>

<div
  on:navigate="showHistoryPage"
  on="showHistoryPage"
  result="historyResult"
  render="historyResult"
>
  { server.partial } <!-- SSR partial render -->
</div>
1
<p>The A page content.</p>
1
<p>The B page content.</p>
1
<p>The C page content.</p>
https://www.history-example.com/page-a/

once

This is a self-destruct instruction for the on attribute. It is removed after the first invocation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<h4 class="mt0">You can only add a single entry to the log.</h4>

<button
  type="button"
  class="btn"
  on:click="sendOnce"
  on="sendOnce"
  href="/once"
  once
  result="onceResult"
>
  Append Log
</button>

<ul
  class="mv0"
  render="onceResult"
  position="append"
></ul>
1
<li class="mt3">Log entry from: { new Date().toLocaleString() }</li>

You can only add a single entry to the log.


    Polling

    Info

    Example only provided for completeness. You are discouraged from doing this. Use SSE if possible.

    KEML does not implement polling as a dedicated feature, but it can still be assembled from the basic building blocks shown above.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    {{ const colors = ["red", "orange", "gold", "yellow", "purple", "pink", "green",
      "navy", "blue"]; }}
    
    <div
      class="w3 h3 bg-{ colors[Math.random() * colors.length | 0] }"
      on:discover="startPolling"
      on="startPolling"
      debounce="5000"
      get="/polling"
      on:result="startPolling"
      result="pollingResult"
      render="pollingResult"
      position="replaceWith"
    ></div>
    

    Changes color every 5 seconds.