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

Stage 2 Specification

Open ije opened this issue 4 years ago • 12 comments

This stage makes Alef Component to have the power to build a self-contained application.

Import Alef Component

You can import other components in Alef Component. The props of a component should be declared with type in Prop<T> as constant which can be understand in AOT compiler and LSP.

// Hello.alef

const name: Prop<string> = 'World'

<p>Hello {name}!</p>
// App.alef

import Hello from './Hello.alef'

let name = 'World'

<Hello name={name} />

Slots

Slots allows you to pass child nodes into an Alef Component. To use slots in the child component you will need to declare a prop constant with type in Prop<Slots>.

// Logo.alef

const slots: Prop<Slots> = null

<div className="logo">{slots}</div>
// App.alef

import Logo from './Logo.alef'

let name = 'World'

<Logo>
  <img src="..." />
</Logo>

Reuse Pattern

Reuse Pattern allows you to reuse common logics like react-use. A Reuse Pattern Component should have export default ... expression which includes reactive states.

// use-mouse-position.alef

export function $mousePosition() {
  let x: number = 0
  let y: number = 0

  $: () => {
    const { body } = document
    const onMousemove = (e) => {
      x = e.clientX
      y = e.clientY
    }
    body.addEventListener('mousemove', onMousemove)
    return () => body.removeEventListener('mousemove', onMousemove)
  }

  return {x, y}
}
// App.alef

import { $mousePosition } from './use-mouse-position.alef'

let {x, y} = $mousePosition()

<p>current mouse position: {x}, {y}</p>

Inline Component

With FC<Props> typing, you can define an inline component in Alef Component.

// App.alef

type IMember = {
  name: string
  profile: string
}

let members: IMember[] = [...]

const Member: FC<IMember> = ({name, profile}) => (
 <li>
    <h3>{name}</h3>
    <p>{profile}</p>
  </li>
)

<ul>{members.map(member => <Member {...member}  />)}</ul>

Context

Context allows you to share states in child component tree. A specify JSX element context provided to create a context with props in Alef Component, then you can access the context values in the child component with Context<N, T = any> types declared as constant.

// context.alef

import { createContext } from 'alef'

export const [$timer, TimerContextProvidor] = createContext({
  now: Date.now()
})
// Component.alef

import { $timer } from './context.alef'

let { now } = $timer()

<p>now: {now}</p>
// App.alef

import { TimerContextProvidor } from './context.alef'
import Component from './Component.alef'

let now: number = Date.now()

$: () => {
  const ticker = setInterval(() => {
    now = Date.now()
  }, 1000)
  return () => clearInterval(ticker)
}

<TimerContextProvidor value={{now}}>
  <Component />
</TimerContextProvidor>

Styling

Alef Component supports inline CSS with scope by $style label expression, and the AOT compiler can tree-shake the unused CSS.

// App.alef

let n: number = 0

<p onClick={() => n++}>current count is {n}</p>

style: `
  h1 {
    font-size: 200%;
  }
  p {
    font-size: 200%;
    color: ${n >= 10 ? 'red' : 'green'}    
  }
`
// App.output.js

class App extends Component {
  constructor() {
    super()

    // strip types
    let n = 0

    // create styles
    const $style_frag = id => `
/* unused h1 */
/*
  h1 {
    font-size: 200%;
  }
*/
p.${id} {
  font-size: 200%;
}`
    const $style_frag2 = Memo(id => `
p.${id} {
  color: ${Math.abs(n) >= 10 ? 'red' : 'green'}    
}`, [0])

    // create nodes
    const nodes = [
      Style([$style_frag, $style_frag2]),
      Element(
        'p',
        {
          onClick: Dirty(() => n++, [0])
        },
        'current count is ',
        Memo(() => n, [0])
      )
    ]

    // register nodes
    this.register(nodes)
  }
}

And nest expression is supported in inline CSS:

$style: `
  p {
    &:hover {
      font-weight: bold;
    }
    span {
      color: green
    }
  }
`

Transition

Alef Component supports transition animation for view changes.

// App.alef

let ok = false

function toggle() {
  ok = !ok
}

function transIn(node) {
  node.style.opacity = '0'
  node.style.transition = 'opacity 1s easeIn'
  const timer = setTimeout(()=>{
    node.style.opacity = '1'
    node.style.transition = ''
  }, 1000)
  return () => {
   clearTimeout(timer)
  }
}

function transOut(node) {
  node.style.opacity = '1'
  node.style.transition = 'opacity 1s easeOut'
  const timer = setTimeout(()=>{
    node.style.opacity = '0'
    node.style.transition = ''
  }, 1000)
  return () => {
   clearTimeout(timer)
  }
}

<transition in={transIn} out={transOut}>
  {ok &&  (
    <button onClick={toggle}>OFF</button>
  )}
  {!ok &&  (
    <button onClick={toggle}>ON</button>
  )}
</transition>

Alef Comonent provides some preset transtion effects like fade.

// App.alef

let ok = false

function toggle() {
  ok = !ok
}

<transition.fade duration={600} timing="easeInOut">
  {ok &&  (
    <button onClick={toggle}>OFF</button>
  )}
  {!ok &&  (
    <button onClick={toggle}>ON</button>
  )}
</transition.fade>

Asynchronous Component

An asynchronous component need to wait data fetching or component lazy importing finished before render, a specify JSX element await provided in Alef Component that will render the callback UI instead of the component tree until all asynchronous child components ready.

// Component.alef

const Hello = await import("./Hello.alef") // lazy import component

let data = await fetch("https://...").then(resp => resp.json())

<Hello name={data.name} />
// App.alef

import Component from "./Component.alef" // asynchronous component

<await fallback={<p>Loading...</p>}>
  <Component />
</await>

Error Boundary

A specify JSX element error provided in Alef Component that can catch errors anywhere in their child component tree and display a fallback UI instead of the component tree that crashed.

// App.alef

import Component from "./Component.alef" // asynchronous component

<error fallback={props => <p>Error: {props.error.message}</p>}>
  <await fallback={<p>Loading...</p>}>
    <Component />
  </await>
</error>

ije avatar Dec 08 '20 08:12 ije

@ije This kind of goes along with slots and children, do you think we could do dynamic usage of components?

const btn: Props<Children> = null;

$t: <btn onClick={() => {}} />

shadowtime2000 avatar Dec 08 '20 22:12 shadowtime2000

If we are going to tree shake unused CSS we should also minify the string.

shadowtime2000 avatar Dec 08 '20 23:12 shadowtime2000

@shadowtime2000 better not, should use import syntax:

import Button form "./button.alef"

ije avatar Dec 09 '20 00:12 ije

in production mode, output will be minified including CSS

ije avatar Dec 09 '20 00:12 ije

One feature I would suggest which I think fits in stage 2 the best is allowing to have a container DOMElement.

$: () => {
	const members = container.querySelectorAll("[class='member']");
	members.forEach(member => member.textContent += " Member");
}

$t: {members.map(member => <Member {...member} />}

shadowtime2000 avatar Dec 21 '20 08:12 shadowtime2000

how about this:

$: (el) => {
 const members = el.querySelectorAll("[class='member']");
 members.forEach(member => member.textContent += " Member");
}

ije avatar Dec 21 '20 08:12 ije

since the side effect will be triggered(asynchronously) after dom mounted, that can get the container element very easy.

ije avatar Dec 21 '20 08:12 ije

@shadowtime2000 but if we have multiple $t or conditional expr, that can not work...

ije avatar Dec 21 '20 10:12 ije

@ije Do you think it would be possible to split the styles based on their dependencies so only certain styles are updated on changes?

const dynamicStyle = DynamicStyle(id => `
	p.${id} {
		color: ${Math.abs(n) >= 10 ? "red" : "green"}
	}
`, [n]);

const staticStyle = StaticStyle(id => `
	button.${id} {
		display: inline-block;
		width: 24px;
		height: 24px;
		font-weight: bold;
	}
`);

const style = Style([dynamicStyle, staticStyle]);

shadowtime2000 avatar Dec 22 '20 21:12 shadowtime2000

@shadowtime2000 that makes sense!

ije avatar Dec 23 '20 00:12 ije

@shadowtime2000 the output of styles will be:

let n = 0

const $style_frag = id => `
/* unused h1 */
/*
  h1 {
    font-size: 200%;
  }
*/
p.${id} {
  font-size: 200%;
}
`

const $style_frag2 = Memo(id => `
p.${id} {
  color: ${Math.abs(n) >= 10 ? 'red' : 'green'}    
}
`, [0])

// real style
Style([$style_frag, $style_frag2])

Memo(() => n, [0]) means watch and use the first state(n), Dirty(() => n++, [0]) means update the first state(n).

ije avatar Dec 27 '20 14:12 ije

@ije We could also look into using CSS custom properties for style manipulation, could theoretically be faster because the browser does the style updating which is extremely fast because it has extremely optimized algorithms.

shadowtime2000 avatar Mar 04 '21 00:03 shadowtime2000