alef-component
alef-component copied to clipboard
Stage 2 Specification
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 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={() => {}} />
If we are going to tree shake unused CSS we should also minify the string.
@shadowtime2000 better not, should use import syntax:
import Button form "./button.alef"
in production mode, output will be minified including CSS
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} />}
how about this:
$: (el) => {
const members = el.querySelectorAll("[class='member']");
members.forEach(member => member.textContent += " Member");
}
since the side effect will be triggered(asynchronously) after dom mounted, that can get the container element very easy.
@shadowtime2000 but if we have multiple $t or conditional expr, that can not work...
@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 that makes sense!
@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 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.