alef-component icon indicating copy to clipboard operation
alef-component copied to clipboard

Stage 1 Specification

Open ije opened this issue 4 years ago • 3 comments

This stage includes the basic features of Alef Component.

Elements Rendering

The core concept of Alef Component is AOT compilation, that means JSX elements in Alef Component will be transpiled to native DOM operations, no Virtual DOM needed, that gives your the vanilla DOM updating preference. A JSX.Element starts with $ label (Side Effect) will add a node to the component, you can have multiple nodes in single component. Any variable declare will be treated as a reactive state in Alef Component which can cause view updates when it changed.

// App.alef

let name: string = 'World'
let n: number = 0

$: <div>
  <p>Hello {name}!</p>
  <p onClick={() => n++}>current count is {n}</p>
</div>
// App.output.js

export default function App {
  const self = new Component()

  // strip types 
  let name = 'World'
  let n = 0

  // create nodes
  const nodes = [
    Element(
      'div', 
      null, 
      Element('p', null , `Hello ${name}!`),
      Element(
          'p',
          {
          onClick: Dirty(() => n++, [0])
          },
          'current count is ',
          Memo(() => n, [0])
      )
    ),
  ]

  // register nodes
  self.register(nodes)
}

Conditional Rendering

Alef Component uses the standard JSX syntax, you can write conditional statements just like React.

// App.alef

let ok: boolean = false

function toggle() {
  ok = !ok
}

$: <p>
  {ok ? (
    <button onClick={toggle}>OFF</button>
  ) : (
    <button onClick={toggle}>ON</button>
  )}
</p>
// App.output.js

export default function App {
  const self = new Component()

  // strip types
  let ok = false
  function toggle() {
    ok = !ok // dirty data: ok
  }

  // create nodes
  const nodes = [
    Element(
      'p',
      null,
      Memo(() => ok ? Element(
        'button',
        { onClick: Dirty(toggle, [0]) },
        'ON'
      ):
      Element(
        'button',
        { onClick: Dirty(toggle, [0]) },
        'OFF'
      ), [0]),
    )
  ]

  // register nodes
  self.register(nodes)
  return self
}

Loop Rendering

To render a list, you can put an array of JSX.Element like React, each JSX.Element should have key property. In Aleh Component, the self-update methods like push, unshift and splice of the array object will trigger the ListBlock update automatically.

// App.alef

let numbers: number[] = [1, 2, 3]

$: <>
  <ul>
    {numbers.map(n => (
      <li key={n}>{n}</li>
    )}
  </ul>
  <button onClick={() => { numbers.push(numbers.length + 1) }}>Add number</button>
</>
// App.output.js

export default function App {
  const self = new Component()
  
  // strip types
  let numbers /* Array */ = Proxy([1, 2, 3], [0])

  // create nodes
  const nodes = [
    Element(
      'ul',
      null,
      Memo(() => numbers.map(n => Element('li', { key: n }, n)), [0])
    ),
    Element(
      'button', {
        onClick: () => { numbers.push(numbers.length + 1) }
      }, 
      'Add number'
    ) 
  ]

  // register nodes
  self.register(nodes)

  return self
}

Memo

A Memo is a computed state which is a reactive constant, that is useful to reuse some computed expressions with states. To create a complex memo you can use IIFE expression.

// App.alef

let n: number = 0

const double: nubmer = 2 * n
const quad: nubmer = (() => 2 * double)()

$: <p onClick={() => n++}>
  double is {double}, quad is {quad}, octuple is {2 * quad}
</p>
// App.output.js

export default function App {
  const self = new Component()

  // strip types 
  let n = 0

  // create memos
  const $double = Memo(() => 2 * n, [0])
  const $quad = Memo(() => 2 * $double.value, [0])

  // create nodes
  const nodes = [
    Element(
      'p',
      { onClick: Dirty(() => n++, [0]) },
      '\n  double is ',
      $double,
      ', quad is ',
      $quad,
      ', octuple is ',
      Memo(() => 2 * $quad.value, [0])
      '\n'
    )
  ]

  // register nodes
  self.register(nodes)
  return self
}

Side Effect

The Side Effect will happen when a component mounted or the dependent states changed. You call return a dispose funtion to clear asynchronous event listeners defined in the side effect callback. JSX expression will be treated as a Side Effect that can react to UI.

// App.alef

let n: number = 0

$: console.log(`current count is ${n}`)

$: () => {
  console.log('mounted')
  () => console.log('unmounted')
}

$: <p onClick={() => n++}>{n}</p>
// App.output.js

export default function App {
  const self = new Component()

  // strip types
  let n = 0

  // create effects
  const $$effect = Effect(() => {
    console.log(`current count is ${n}`)
  }, [0])
  const $$effect2 = Effect(() => {
    console.log(`mounted`)
    () => console.log(`unmounted`)
  }, [])

  // create nodes
  const nodes = [
    Element('p', { onClick: Dirty(() => n++, [0])}, Memo(() => n, [0]))
  ]

  // register nodes
  self.register(nodes)

  // register effects
  self.onMount($$effect, $$effect2)

  return self
}

Events

Aleh Component uses JSX event property name like onClick={CALLBACK}, you can write event callbacks in JSX expression, or create event handles out of JSX to get better readability.

// App.alef

let n: number = 0

function increase() {
  n++
}

$: <div>
  <p>current count is {n}</p>
  <button onClick={() => n-- }>-</button>
  <button onClick={increase}>+</button>
<div>
// App.output.js

export default function App {
  const self = new Component()

  // strip types 
  let n = 0
  function increase() {
    n++
  }

  // create nodes
  const nodes = [
    Element(
      'div', 
      null,
      Element('p', null, 'current count is ', Memo(() => n, [0])),
      Element('button', { onClick: Dirty(() => n--, [0])}, '-'),
      Element('button', { onClick: Dirty(increase, [0])}, '+')
    ),
  ]

  // register nodes
  self.register(nodes)
  return self
}

Alef Component does not support two-way binding in form elements, you will need to update the state manually by the change event:

let value = ''

<input value={value} onChange={e => value = e.target.value} />

A specify JSX namespace window provided to allow you create global event handles in a component, with that you will not need to concern event disposing:

$: <window.self onResize={onResize} />
$: <window.document onKeyUp={onKeyUp} />

or you can listen global DOM/IO events in Side Effect expression $: () => {}, but should be vary careful to return a dispose function that can clear all the listeners to avoid memory leak:

$: () => {
  const onResize = () => { ... }
  window.AddEventListener('resize', onResize)
  return () => window.removeEventListener('resize', onResize)
}

ije avatar Dec 08 '20 08:12 ije

@ije Will the compiler allow using a ternary operator instead of an if statement for a conditional? I find it much more concise

shadowtime2000 avatar Dec 21 '20 08:12 shadowtime2000

@shadowtime2000 yes, please ref #8 , i also think the top if-else is kind of weird...

ije avatar Dec 21 '20 08:12 ije

if you are a rust user, it's vary familiar. but in JS i'm not sure... i'm trying to extend the conditional rendering that is weak in JSX syntax.

ije avatar Dec 21 '20 08:12 ije