Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Last active September 19, 2021 16:52
Show Gist options
  • Save mindplay-dk/40d83f6fc607e9ae277d1bbc4676af93 to your computer and use it in GitHub Desktop.
Save mindplay-dk/40d83f6fc607e9ae277d1bbc4676af93 to your computer and use it in GitHub Desktop.
Flip Malina syntax?

What if, instead of starting with HTML-like syntax and using script-blocks, we flipped the whole thing on it's head: start with JSX and use HTML blocks?

Let's walk through the examples in the Malina REPL.

Starting with "Hello World":

<script>
    let name = 'world';
</script>

<h1>Hello {name.toUpperCase()}!</h1>

And porting this to TypeScript:

let name = 'world';

<html>
    <h1>Hello {name.toUpperCase()}!</h1>
</html>

If you're used to "normal" JSX (React, etc.) this looks immediately weird - it's actually valid TypeScript, it just isn't meaningful without a compiler to pick up the <html> JSX expression. The static coupling between the name variable and the reference in the {name.toUpperCase()} expression is there though.

Let's look at the "Binding" example:

<script>
    let name = 'world';
    let active = false;
</script>

Enter name: <input type="text" bind:value={name} />
Hello {name}!
<br/>
<input type="checkbox" bind:checked={active} /> {active}

And porting this to TypeScript:

let name = 'world';
let active = false;

<html>
    Enter name: <input type="text" bind:value={name} />
    Hello {name}!
    <br/>
    <input type="checkbox" bind:checked={active} /> {active}
</html>

Again, completely valid, and the static bindings are there. I've kept the bind:value and bind:checked syntax from Malina - these are valid JSX attribute-names, but of course the compiler has to do some magic, same as it does in Malina today.

On to the "Events" example:

<script>
    let name = '';
    let event = '';
    const click = (e) => {
        event = e.x + 'x' + e.y;
    }
</script>
  
Enter word and press enter:
<input bind:value={name} on:keydown|enter={name=''} /> {name}
<br />
<button on:click={click($event)}>Click!</button>
<a href on:click|preventDefault={event=$element.textContent}>Click</a>
{event}

This one will look a little different in TypeScript:

import { preventDefault, onKeyDown } from "malina";

let name = '';
let event = '';
const click = (e) => {
    event = e.x + 'x' + e.y;
}

<html>  
  Enter word and press enter:
  <input bind:value={name} {...onKeyDown("enter", () => name='')} /> {name}
  <br />
  <button on:click={click}>Click!</button>
  <a href on:click={preventDefault(e => event = e.target.textContent)}>Click</a>
  {event}
</html>

Here, I introduced a couple of runtime helper-functions: preventDefault augments an event-handler, and onKeyDown generates a filtered keydown event-listener.

I used JSX {...} spread syntax for the onKeyDown call, just to avoid something more verbose - alternatively, we could have used something like on:keydown={filterKey("enter", () => name='')}, where filterKey would just wrap the event-handler on a key filter. (I'm using the {...onKeyDown(...)} pattern/function in the rest of these examples.)

Next, the "Class and Style" example:

<script>
    let value;
</script>

<input type="checkbox" bind:checked={value} /> switch

<div class:blue={value} class:red={!value}>Linux</div>
<div class="{value?'red':'blue'}">MacOS</div>
<b style="color: {value?'green':'red'};">Windows</b>

<style>
    .blue {background-color: cornflowerblue;}
    .red {background-color: tomato;}
</style>

And the TypeScript version:

import { css } from "malina";

let value;

<html>
  <input type="checkbox" bind:checked={value} /> switch

  <div class:blue={value} class:red={!value}>Linux</div>
  <div class={value?'red':'blue'}>MacOS</div>
  <b style:color={value?'green':'red'}>Windows</b>
</html>

css`
  .blue {background-color: cornflowerblue;}
  .red {background-color: tomato;}
`

Here, I chose a template literal function - and, like the <html> element, this is something the compiler has to pick up.

Because of the curly braces in CSS declarations, something like <style>...</style> wouldn't work - and, in addition, there are already code editors/plugins that can syntax highlight CSS in template literal strings, as this is a popular choice for CSS-in-JS frameworks.

JSX has a bit of an advantage here - where Malina uses quoted string syntax like <div class="{value?'red':'blue'}">, with JSX, we don't have the somewhat confusing "code in strings" syntax.

Now the "If-else Block Example":

<script>
    let value;
</script>
  
<input type="checkbox" bind:checked={value} /> switch
<br/>
  
{#if value}
    checkbox is on
{:else}
    [else] block, switch checkbox
{/if}

And the TypeScript version:

let value;

<html>  
  <input type="checkbox" bind:checked={value} /> switch
  <br/>
    
  {value
    ? "checkbox is on"
    : "[else] block, switch checkbox"}
</html>

Here, JSX loses out a bit - with expression support only, we need to use the arguably less-readable ternary operator.

The same goes for the "Each/Repeat Block Example":

<script>
    let items = [{name: 'first task'}, {name: 'second task'}];
    let name = '';

    const add = () => {
        items.push({name: name});
        name = '';
    };
    const remove = i => {
        items.splice(i, 1);
    }
    const reverse = (item) => {
        item.name = item.name.split('').reverse().join('');
    }
</script>

Enter text and press enter:
<input type="text" bind:value={name} on:keydown|enter={add()} />

<ul>
    {#each items as item}
    <li>
        {$index}: {item.name}
        <a href on:click|preventDefault={reverse(item)}>reverse</a>
        <a href on:click|preventDefault={remove($index)}>remove</a>
    </li>
    {/each}
</ul>

Where the TypeScript version looks like this:

import { preventDefault, onKeyDown } from "malina";

let items = [{name: 'first task'}, {name: 'second task'}];
let name = '';

const add = () => {
    items.push({name: name});
    name = '';
};
const remove = i => {
    items.splice(i, 1);
}
const reverse = (item) => {
    item.name = item.name.split('').reverse().join('');
}

<html>
  Enter text and press enter:
  <input type="text" bind:value={name} {...onKeyDown("enter", add)} />

  <ul>
      {items.map((item, index) => 
          <li>
              {index}: {item.name}
              <a href on:click={preventDefault(() => reverse(item))}>reverse</a>
              <a href on:click={preventDefault(() => remove(index))}>remove</a>
          </li>
      )}
  </ul>
</html>

So yeah, here we have to use items.map with extra parens and curly braces.

On the up-side though, the magical $index and $element variables have been replaced by nice, explicit callback parameters, with type-safety and static coupling to the expressions that use them.

Moving on the "Directive 'use'" example:

<script>
    let visible = false;
    const setStyle = el => {
        el.style.color = 'red';
    }
    let boldElement;
</script>

<input type="checkbox" bind:checked={visible} />
<span use={setStyle($element)}>set focus input on render</span>

<div>
    {#if visible}
        <input type="text" use={$element.focus()} />
    {/if}
</div>

save element in <b use={boldElement=$element}>variable</b>

And the TypeScript version:

let visible = false;
const setStyle = el => {
    el.style.color = 'red';
}
let boldElement;

<html>
    <input type="checkbox" bind:checked={visible} />
    <span use={setStyle}>set focus input on render</span>

    <div>
        { visible && <input type="text" use={el => el.focus()} /> }
    </div>

    save element in <b use={el => boldElement = el}>variable</b>
</html>

This seems intuitive enough, and I might even suggest considering a rename from use to ref, as this works just like ref in React and some other JSX based frameworks.

JSX again has a small advantage here, with Malina's <span use={setStyle($element)}> being simplified as merely <span use={setStyle}>. (It's actually not clear to me why this wouldn't work just the same in Malina? At the time of writing, it silenty failed to do anything.)

And finally, the "Todo app" example:

<script>
    let name = '';
    let todos = [{name: 'first task'}, {name: 'second task', done: true}];
    let active;

    function add() {
        if(!name) return;
        todos.push({name: name});
        name = '';
    }

    const remove = i => todos.splice(i, 1);
    const numDone = () => todos.filter(t => t.done).length;
</script>

{#if active}
    Edit: <input type="text" on:keydown|enter={active=null} bind:value={active.name} use={$element.focus()} />
{:else}
    <input type="text" on:keydown|enter={add()} bind:value={name} />
{/if}

<ul>
    {#each todos as todo}
        <li class:active={todo == active} class:inactive={todo.done}>
            <input type="checkbox" bind:checked={todo.done} />
            <span on:click={active=todo}>{$index}:  {todo.name}</span>
            <a href on:click|preventDefault={remove($index)}>remove</a>
        </li>
    {/each}
</ul>

Total done: {numDone()} of {todos.length}

<style>
    li {cursor: pointer;}
    .active {background-color: #cfc;}
    .inactive {text-decoration-line: line-through; color: gray;}
</style>

And the TypeScript port:

import { css, onKeyDown, preventDefault } from "malina";

let name = '';
let todos = [{name: 'first task'}, {name: 'second task', done: true}];
let active;

function add() {
    if(!name) return;
    todos.push({name: name});
    name = '';
}

const remove = i => todos.splice(i, 1);
const numDone = () => todos.filter(t => t.done).length;

<html>
    {active
        ? <>Edit: <input type="text" {...onKeyDown("enter", () => active=null)} bind:value={active.name} use={el => el.focus()} /></>
        : <input type="text" {...onKeyDown("enter", add)} bind:value={name} /> }

    <ul>
        {todos.map((todo, index) => 
            <li class:active={todo == active} class:inactive={todo.done}>
                <input type="checkbox" bind:checked={todo.done} />
                <span on:click={() => active=todo}>{index}:  {todo.name}</span>
                <a href on:click={preventDefault(() => remove(index))}>remove</a>
            </li>
        )}
    </ul>

    Total done: {numDone()} of {todos.length}
</html>

css`
    li {cursor: pointer;}
    .active {background-color: #cfc;}
    .inactive {text-decoration-line: line-through; color: gray;}
`

There's not much to say about this example, it just shows everything else put together. I didn't like having to add <> and </> around the "Edit" label and input, but this is JSX, so everything is an expression. I do like having the static coupling between todo in the loop, and type-checking for todo.name and todo.done is definitely a plus.

There is much more ground to cover, of course - this is just a quick thought experiment, but seems like it might be an avenue worth exploring.

@mindplay-dk
Copy link
Author

I wish this was happening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment