Hey there, fellow developer! Ready to spice up your web development journey? Say hello to Ajo, your new best friend in building dynamic, efficient, and beautifully responsive user interfaces. Whether you're a seasoned pro or just starting out, Ajo is here to make your coding experience smoother, faster, and dare we say, more fun!
Imagine a world where your UI components are as fresh and vibrant as a perfectly seasoned dish. That's what Ajo brings to the table! It's a lightweight, powerful library that combines the best ideas from Incremental DOM and Crank.js, served with a dash of innovation.
With Ajo, you can:
- 🚀 Create lightning-fast UIs with efficient in-place DOM updates
- 🧠 Manage state effortlessly using JavaScript Generators
- 📝 Write intuitive code with JSX syntax
- 🔄 Control component lifecycles with precision
- 🌈 Build flexible and adaptable applications
- Speedy and Efficient: Ajo's in-place DOM updates mean your apps run faster with less memory overhead.
- Generator-Powered: Harness the power of JavaScript Generators for smooth state management and effects.
- JSX Friendly: Feel right at home with JSX syntax, making your transition from React or similar libraries a breeze.
- Lightweight Champion: Keep your project nimble with Ajo's minimal footprint.
- SSR Ready: Seamlessly integrate server-side rendering for SEO-friendly, fast-loading pages.
Ready to add some flavor to your project? Let's start by getting Ajo into your kitchen... err, development environment!
Fire up your terminal and run:
npm install ajo
That's it! Ajo is now ready to spice up your project.
Let's whip up a quick example to see Ajo in action:
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment, render } from 'ajo'
// A simple, stateless component
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>
// A stateful component with a dash of interactivity
function* Counter() {
let count = 0
const increment = () => {
count++
this.render()
}
while (true) {
yield (
<>
<p>Count: {count}</p>
<button set:onclick={increment}>Increment</button>
</>
)
}
}
// Let's bring it all together
function* App() {
while (true) {
yield (
<>
<Greeting name="Ajo Developer" />
<Counter />
</>
)
}
}
// Serve it hot to the DOM!
render(<App />, document.body)
And voilà! You've just created your first Ajo application. A warm greeting and an interactive counter, all served up in a few lines of code.
Notice how our stateful components (Counter
and App
) use fragments (<>...</>
) as their root elements. This is a best practice in Ajo to avoid creating unnecessary DOM nodes, as stateful components always render a wrapper element.
This is just a taste of what Ajo can do. As you explore further, you'll discover how Ajo makes complex UI interactions simple and performant. From managing application state to creating reusable components, Ajo has got your back.
Ready to dive deeper? Let's explore Ajo's core concepts and APIs in the following sections. Trust us, it's going to be a delicious journey! 🌶️
Happy coding, and welcome to the Ajo family!
Welcome to the world of Ajo rendering! Let's dive into how Ajo efficiently updates your UI, keeping things fast and responsive. No chef's hat required, but do bring your curiosity! 🚀
Ajo takes a unique approach to rendering that sets it apart:
-
Lightweight Virtual Representation: Ajo creates a minimal virtual representation of your components, reducing memory overhead.
-
In-Place DOM Updates: Instead of recreating the entire DOM tree, Ajo updates only what has changed, leading to efficient renders.
-
Minimal Overhead: By avoiding full virtual DOM diffing, Ajo keeps things speedy and memory-friendly.
Let's break these down and see how they work in practice!
The render
function is your main tool for getting Ajo components onto the page:
import { h, render } from 'ajo'
const App = () => <div>Welcome to Ajo!</div>
render(<App />, document.body)
This function takes your virtual DOM tree and efficiently updates the real DOM to match.
Stateful components in Ajo are where things get really interesting. They're implemented as generator functions, allowing for powerful state management:
function* Counter() {
let count = 0
const increment = () => {
count++
this.render() // Trigger a re-render
}
while (true) {
yield (
<>
<p>Count: {count}</p>
<button set:onclick={increment}>Increment</button>
</>
)
}
}
render(<Counter />, document.body)
Key points to remember:
- Use generator functions for stateful components.
- Call
this.render()
to trigger a re-render when state changes. - The
while (true)
loop allows the component to yield new UI representations indefinitely.
Ajo handles lists with impressive efficiency. Here's how you can leverage this:
function* TodoList() {
let todos = ['Learn Ajo', 'Build an app']
const addTodo = () => {
todos = [...todos, 'New todo']
this.render()
}
while (true) {
yield (
<>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button set:onclick={addTodo}>Add Todo</button>
</>
)
}
}
Note the use of the key
prop on list items. This helps Ajo track individual items efficiently across renders.
Ajo's rendering strategy offers several benefits:
- Performance: Minimal DOM manipulation leads to faster updates.
- Scalability: The approach works well for both small and large applications.
- Simplicity: Write declarative code and let Ajo handle the DOM updates.
Let's break down what happens when you call this.render()
:
- The generator function is advanced to its next
yield
point. - Ajo creates a lightweight virtual representation of the new UI state.
- This representation is compared with the current DOM state.
- Only the necessary changes are applied to the DOM.
This process ensures that updates are fast and efficient, regardless of the complexity of your UI.
Understanding Ajo's rendering process is key to building efficient, responsive UIs. By leveraging stateful components, efficient list rendering, and judicious use of this.render()
, you can create applications that are both powerful and performant.
Remember, the more you work with Ajo, the more intuitive this process will become. So dive in, experiment, and watch as your UIs come to life with lightning-fast updates!
Happy coding, and may your renders always be swift! 🚀✨
Welcome, Ajo chefs! Let's dive into the special attributes - the secret ingredients that give your components that extra zing! These attributes are like the spices in your code kitchen, each adding its own unique flavor to your Ajo dish. Let's spice things up! 👩🍳
Just as every great chef needs to identify their signature dishes, Ajo needs to identify items in a list. Enter the key
attribute - your component's name tag at the rendering party!
function* TodoList() {
let todos = [
{ id: 1, text: 'Learn Ajo' },
{ id: 2, text: 'Build an app' }
]
const addTodo = () => {
todos = [...todos, { id: Date.now(), text: 'New Todo' }]
this.render()
}
while (true) {
yield (
<>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<button set:onclick={addTodo}>Add Todo</button>
</>
)
}
}
🌶️ Spice Tip: Always use unique key
s for list items. It helps Ajo keep track of which item is which, like a good chef knowing each ingredient in the pantry!
Sometimes, you want to tell Ajo, "Hey, hands off this part!" That's where skip
comes in handy. It's like putting a "Do Not Disturb" sign on a hotel door, but for your component's children.
function* ChartComponent() {
let chart
const initChart = (canvas) => {
if (canvas == null) {
chart?.destroy()
return
}
chart ??= new Chart(canvas, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3]
}]
}
})
}
while (true) {
yield (
<canvas ref={initChart} skip></canvas>
)
}
}
🌶️ Spice Tip: Use skip
when you're working with third-party ingredients that don't play well with Ajo's cooking style. It tells Ajo, "I've got this part covered, chef!"
Why recook a dish if the taste hasn't changed? That's the philosophy behind memo
. It's like vacuum-sealing your component's flavor, preserving it until the ingredients actually change.
function* ExpensiveComponent({ data }) {
const expensiveCalculation = () => {
// Imagine this is a costly operation
return data.reduce((acc, val) => acc + val, 0)
}
while (true) {
yield (
<div memo={[data]}>
<p>Result: {expensiveCalculation()}</p>
</div>
)
}
}
🌶️ Spice Tip: Use memo
for components with expensive computations. It's like meal prepping - do the work once, enjoy the results many times!
Sometimes you need to reach out and touch a DOM element directly. That's where ref
comes in - it's like having a kitchen assistant who can hand you any tool you need, exactly when you need it.
function* FormWithFocus() {
let inputRef
const focusInput = () => {
inputRef.focus()
}
while (true) {
yield (
<>
<input ref={el => inputRef = el} />
<button set:onclick={focusInput}>Focus Input</button>
</>
)
}
}
🌶️ Spice Tip: Use ref
when you need direct access to DOM elements. It's like having X-ray vision in your kitchen!
When you want to sprinkle some HTML attributes onto your component's root element, attr:
is your go-to seasoning shaker!
function* CustomButton(args) {
while (true) yield (
<>
<i class="fa-solid fa-hand-pointer"></i>
{args.children}
</>
)
}
CustomButton.attrs = { class: 'btn btn-primary' }
CustomButton.is = 'button'
// Usage
<CustomButton attr:id="submit-btn" attr:aria-label="Submit">
Click Me
</CustomButton>
🌶️ Spice Tip: Use attr:
to add any HTML attribute to your component's root element. It's like adding the final garnish to your gourmet component dish!
These special attributes are the secret spices in your Ajo cookbook:
- Use
key
to help Ajo identify your list items - like name tags at a potluck. - Apply
skip
when you need to tell Ajo "hands off" - it's the "I'll handle this dish myself" of attributes. - Sprinkle
memo
to preserve expensive computations - think of it as your component's flavor-saver. - Reach for
ref
when you need direct DOM access - it's your kitchen assistant, always ready to hand you the right tool. - Shake some
attr:
to add HTML attributes - the final seasoning for your component masterpiece.
By mastering these special attributes, you'll be able to cook up Ajo components that are not just functional, but Michelin-star worthy! Your UI will be so responsive, efficient, and well-managed, it'll make Gordon Ramsay weep tears of joy.
Now go forth and create your Ajo masterpieces. May your components be tasty and your renders be swift! Bon appétit! 👨🍳🎉
Welcome to the world of stateless components in Ajo! These are the building blocks of your UI that focus purely on presentation. Let's dive into what makes them tick and how to use them effectively. 🧱✨
In Ajo, stateless components are simple function wrappers that return JSX. They're pure functions of their args, meaning they always render the same output for a given input, without any internal state or side effects.
Here's the simplest form of a stateless component:
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>
}
In Ajo, we typically refer to the arguments passed to components as "args". Here's how you can use them:
function UserCard({ name, email, avatar }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
</div>
)
}
// Usage
<UserCard
name="Alice Smith"
email="[email protected]"
avatar="https://example.com/alice.jpg"
/>
One of the strengths of stateless components is how easily they can be composed to create more complex UIs:
function Header({ title }) {
return <header><h1>{title}</h1></header>
}
function Footer({ copyright }) {
return <footer>© {copyright}</footer>
}
function Page({ title, content, year }) {
return (
<>
<Header title={title} />
<main>{content}</main>
<Footer copyright={year} />
</>
)
}
// Usage
<Page
title="Welcome to Ajo"
content={<p>Ajo makes building UIs efficient and fun!</p>}
year={2024}
/>
-
Keep It Simple: Stateless components should focus on presentation. If you need state, consider using a stateful component instead.
-
Args Types: While Ajo doesn't enforce args types, it's a good practice to use TypeScript to define the expected args:
interface UserCardArgs { name: string; email: string; avatar: string; } function UserCard({ name, email, avatar }: UserCardArgs) { // Component body }
-
Default Args: You can provide default values for args to make your components more flexible:
function Button({ text = 'Click me', onClick }) { return <button set:onclick={onClick}>{text}</button> }
-
Destructuring Args: For better readability, destructure your args in the function parameters:
function Profile({ name, bio, isVerified }) { return ( <div> <h2>{name} {isVerified && '✓'}</h2> <p>{bio}</p> </div> ) }
Stateless components in Ajo are powerful in their simplicity. They're easy to write, test, and reason about, making them an essential tool in your Ajo toolkit. By focusing on creating small, reusable stateless components, you can build complex UIs that are both performant and maintainable.
Remember, the art of great Ajo development often lies in knowing when to use stateless components and when to reach for stateful ones. As you continue your Ajo journey, you'll develop an intuition for this balance. 🚀📦
Stateful components in Ajo are powerful constructs that form the backbone of dynamic, interactive UIs. Let's dive into how they work and how to use them effectively.
In Ajo, stateful components are implemented using generator functions. This unique approach allows components to maintain state across renders and react to changes over time.
Here's the fundamental structure of a stateful component in Ajo:
import { Component } from 'ajo'
const Counter: Component<{ initial?: number }> = function* (args) {
let count = args.initial ?? 0
while (true) {
yield (
<div>
<p>Count: {count}</p>
<button set:onclick={() => { count++; this.render(); }}>Increment</button>
</div>
)
}
}
Key points:
- Stateful components are generator functions.
- They use a
while (true)
loop to yield JSX indefinitely. this.render()
triggers re-renders when state changes.
- Initialization: The generator function is called, and runs until the first
yield
. - Rendering: Ajo renders the yielded JSX.
- Updates: When
this.render()
is called, the generator advances to the nextyield
. - Cleanup: When the component is unmounted, the generator's
finally
block (if present) is executed.
Unlike React's useState, state in Ajo components is managed using regular variables:
const Timer: Component = function* () {
let seconds = 0
let intervalId: number | null = null
const start = () => {
if (intervalId === null) {
intervalId = setInterval(() => {
seconds++
this.render()
}, 1000)
}
}
const stop = () => {
if (intervalId !== null) {
clearInterval(intervalId)
intervalId = null
}
}
try {
while (true) {
yield (
<div>
<p>Time: {seconds} seconds</p>
<button set:onclick={start}>Start</button>
<button set:onclick={stop}>Stop</button>
</div>
)
}
} finally {
stop() // Cleanup on unmount
}
}
In Ajo, we use "args" instead of "props". It's crucial not to destructure args in the generator function, as they always point to the same object and get updated on re-renders:
const Greeter: Component<{ name: string }> = function* (args) {
while (true) {
yield <h1>Hello, {args.name}!</h1>
// args.name will always be up-to-date, even if changed by a parent
}
}
To pass HTML attributes to a stateful component's root element, use the attr:
prefix:
<Greeter name="Alice" attr:class="greeting" attr:id="main-greeting" />
Ajo provides two special properties for customizing components:
const CustomButton: Component<{ children: any }> = function* (args) {
const handleClick = () => {
console.log('Button clicked!')
}
this.addEventListener('click', handleClick)
try {
while (true) yield (
<>
<i class="icon-click"></i>
{args.children}
</>
)
} finally {
this.removeEventListener('click', handleClick)
}
}
CustomButton.attrs = { class: 'btn btn-primary' }
CustomButton.is = 'button'
// Usage
<CustomButton>Click Me</CustomButton>
// Renders as: <button class="btn btn-primary"><i class="icon-click"></i>Click Me</button>
Side effects in Ajo are managed directly within the generator function:
const DataFetcher: Component<{ url: string }> = function* (args) {
let data = null
let error = null
const fetchData = async () => {
try {
const response = await fetch(args.url)
data = await response.json()
} catch (e) {
error = e
}
this.render()
}
fetchData() // Initial fetch
while (true) {
if (error) {
yield <div>Error: {error.message}</div>
} else if (data) {
yield <div>Data: {JSON.stringify(data)}</div>
} else {
yield <div>Loading...</div>
}
}
}
Refs in Ajo allow parent components to interact with child components:
import { Ref, Component } from 'ajo'
type ChildComponentElement = ThisParameterType<typeof ChildComponent> | null
const ChildComponent: Ref<Component<{ value: number }>> = function* (props) {
props.ref(this)
try {
while (true) {
yield <div>Value: {props.value}</div>
}
} finally {
props.ref(null)
}
}
const ParentComponent: Component = function* () {
let childRef: ChildComponentElement = null
let value = 0
const incrementChild = () => {
if (childRef) {
value++
childRef.render()
}
}
while (true) {
yield (
<>
<ChildComponent ref={el => childRef = el} value={value} />
<button set:onclick={incrementChild}>Increment Child</button>
</>
)
}
}
- Use the
finally
block for cleanup operations. - Avoid destructuring args to ensure you always have the latest values.
- Use TypeScript for better type safety and editor support.
- Keep your components focused and consider breaking large components into smaller ones.
- Use refs judiciously, preferring props and composition when possible.
Stateful components in Ajo offer a powerful way to create dynamic, interactive UIs. By leveraging generator functions, you can create components that are both expressive and efficient. As you continue to work with Ajo, you'll discover even more ways to harness the power of stateful components. 🎛️✨
Let's dive into the lifecycle methods that give your components their pulse and responsiveness. These methods are your secret weapons for creating dynamic, efficient UIs that respond to change like a well-choreographed dance.
Think of this.render()
as the conductor of your component orchestra. It's your go-to method when you want your component to reflect its latest state.
function* DynamicGreeter(args) {
let greeting = "Hello"
const changeGreeting = () => {
greeting = greeting === "Hello" ? "Bonjour" : "Hello"
this.render() // 🎭 Cue the UI update!
}
while (true) {
yield (
<>
<h1>{greeting}, {args.name}!</h1>
<button set:onclick={changeGreeting}>Switch Language</button>
</>
)
}
}
🎵 Pro Tip: this.render()
ensures your component doesn't try to sing two songs at once. If it's already in the middle of a performance (execution), it won't start another until the current one is done.
While this.next()
is the behind-the-scenes hero, it's rarely in the spotlight of your code. It's called automatically by this.render()
and Ajo's rendering process.
function* AsyncGreeter(args) {
let greeting = "Hello"
const fetchGreeting = async () => {
greeting = await getRandomGreeting()
this.render() // 🌟 We use render() here, not next()
}
fetchGreeting()
while (true) {
yield <h1>{greeting}, {args.name}!</h1>
}
}
🎭 Stage Whisper: You'll rarely need to call this.next()
directly. Let Ajo handle this backstage work!
this.return()
is like the "reset" button on your component. Use it to start fresh or clean up when your component leaves the stage!
function* ColorCycler(args) {
const colors = ['red', 'green', 'blue']
let index = 0
const cycleColor = () => {
index = (index + 1) % colors.length
this.render()
}
const reset = () => {
this.return() // 🔄 Reset the generator state
this.render() // 🎬 Start fresh!
}
while (true) {
yield (
<>
<div style={`background-color: ${colors[index]}; padding: 20px;`}>
Current Color: {colors[index]}
</div>
<button set:onclick={cycleColor}>Next Color</button>
<button set:onclick={reset}>Reset</button>
</>
)
}
}
🎭 Director's Note: Clicking "Reset" will call this.return()
, resetting index
to 0 on the next render. It's like giving your component a fresh start!
When things go off-script, this.throw()
is your error-handling superhero. But Ajo has some built-in error handling tricks up its sleeve too!
function* PotentiallyErrorProneComponent(args) {
while (true) {
if (Math.random() < 0.5) {
throw new Error("Oops! Something went wrong.")
}
yield <div>Everything's fine!</div>
}
}
function* ErrorBoundary(args) {
try {
while (true) {
yield <PotentiallyErrorProneComponent />
}
} catch (error) {
yield <div style="color: red;">Error caught: {error.message}</div>
}
}
🎭 Safety Net: Ajo automatically propagates errors thrown in the generator function during rendering. You don't need to explicitly use this.throw()
for errors in the main component body!
Now that we've explored each lifecycle method, let's recap how they work in harmony to create dynamic, efficient Ajo components:
-
this.render(): Your trusty conductor 🎭
- Use it to update your component's UI when state changes.
- It's smart enough to avoid overlapping renders.
- Example:
this.render()
after updating a counter or fetching new data.
-
this.next(): The behind-the-scenes hero 🎬
- Rarely called directly in your code.
- Automatically used by
this.render()
and Ajo's internal rendering process. - Advances the generator to its next yield point.
-
this.return(): The grand finale (or intermission) 🎟️
- Automatically called by Ajo when the component is unmounted from the DOM.
- Can be called manually to reset the component's state.
- Ensures proper cleanup of resources.
- Example: Resetting a form or clearing intervals before unmounting.
-
this.throw(): The plot-twist handler 🎭
- Use it to manually propagate errors up the component tree.
- Allows parent components to catch and handle errors from children.
- Ajo automatically propagates errors thrown in the generator function during rendering.
- Example: Bubbling up network errors or validation issues.
Remember, these methods work together to create the lifecycle of your Ajo components:
- Use
this.render()
for most UI updates. - Let Ajo handle
this.next()
for you. this.return()
will clean up automatically on unmount, but you can also use it for manual resets.- Errors in your generator function will automatically propagate, but use
this.throw()
for manual error handling when needed.
By mastering these lifecycle methods, you'll be able to create Ajo components that are not just reactive, but proactive – anticipating and handling all the twists and turns of user interaction, data flow, and even unexpected errors. Your components will perform like seasoned actors, ready for any scenario, from opening night jitters to surprise plot twists!
So go forth and compose your Ajo symphonies with confidence. Break a leg! 🌟🎭
Now, we're diving deep into Ajo's context - a mechanism as layered and nuanced as a perfectly crafted tiramisu. It's all about the JavaScript prototype chain, folks! Let's see how this chain of inheritance flavors our component tree. 🍯
First, let's create our base context flavor:
import { context } from 'ajo'
const ThemeContext = context('light') // Our base flavor
🍯 Flavor Note: This 'light' theme is like the bottom layer of our tiramisu - it's the foundation everything else builds upon.
Let's see how components can taste this flavor:
function* ThemedButton() {
while (true) {
const theme = ThemeContext() // Sampling the current flavor
yield (
<button class={`btn-${theme}`}>
I'm a {theme} themed button!
</button>
)
}
}
🍯 Flavor Note: ThemeContext()
is like dipping a spoon into our layered dessert - you taste the most immediate flavor.
Now, let's see how we can layer different flavors down our component tree:
function* App() {
ThemeContext('dark') // Changing the base flavor to 'dark'
while (true) {
yield (
<>
<ThemedButton />{/* This will be 'dark' */}
<NestedTheme />
</>
)
}
}
function* NestedTheme() {
ThemeContext('neon') // Adding a new flavor layer
while (true) {
yield (
<>
<ThemedButton />{/* This will be 'neon' */}
<DeeplyNestedTheme />
</>
)
}
}
function* DeeplyNestedTheme() {
const theme = ThemeContext() // Still tasting 'neon' here
while (true) {
yield (
<>
<p>Current theme: {theme}</p>
<ThemedButton />{/* This will also be 'neon' */}
</>
)
}
}
🍯 Flavor Note: Each call to ThemeContext(value)
is like adding a new layer to our dessert. Components further down the tree taste the most recently added flavor, thanks to the prototype chain!
Here's the kicker - changing the flavor down the tree doesn't affect the taste up above:
function* App() {
ThemeContext('dark')
while (true) {
const appTheme = ThemeContext() // Still 'dark'
yield (
<>
<p>App theme: {appTheme}</p>{/* Shows 'dark' */}
<ThemedButton />{/* Uses 'dark' theme */}
<NestedTheme />{/* Overrides 'dark' with 'neon'*/}
</>
)
}
}
🍯 Flavor Note: The App
component and its direct children still taste 'dark', even though NestedTheme
changed the flavor to 'neon'. It's like each layer of our dessert maintains its own distinct taste!
Context in Ajo leverages JavaScript's prototype chain, creating a delicious layer cake of data:
- Create your base context with
const MyContext = context(defaultValue)
. - Access the current context value in components with
MyContext()
. - Set a new context value with
MyContext(newValue)
in stateful components. This creates a new "layer" in the prototype chain. - Child components inherit context from their parents but can override it without affecting their ancestors.
- Each component tastes the most immediate flavor in its prototype chain.
By understanding this prototype-based approach, you can create Ajo applications with sophisticated, hierarchical data flow. Your components will inherit and override context as needed, creating a rich, multi-layered experience.
Now go forth and layer your contexts! May your components be flavorful and your prototype chains be long and prosperous. Happy coding, chefs! 👩🍳🎉
Welcome, directors of the digital stage! Today, we're exploring the art of Server-Side Rendering (SSR) and hydration in Ajo. It's like preparing a Broadway show where the server sets the stage, and the client brings the performance to life. Let's raise the curtain on this spectacular process!
In SSR, your server is like a meticulous stage manager, preparing everything before the audience (users) arrives. Let's see how to set up this initial scene:
import express from 'express'
import { render } from 'ajo/html'
import { App } from './components'
const app = express()
app.get('/', (req, res) => {
const html = render(<App />)
res.send(`
<!DOCTYPE html>
<html>
<head><title>Ajo Theater</title></head>
<body>
<div id="stage">${html}</div>
<script src="/app.js"></script>
</body>
</html>
`)
})
app.listen(3000, () => console.log('The theater is open on port 3000!'))
🎭 Director's Note: render
from 'ajo/html' is our stage designer. It creates a static HTML representation of our App, like a beautifully painted backdrop.
Now that our stage is set, it's time for the client to breathe life into our static scenery. This is where hydration comes in:
import { render } from 'ajo'
import { App } from './components'
// Hydrate the #stage element with our live App performance
render(<App />, document.getElementById('stage'))
🎭 Director's Note: The same render
function we use for normal rendering also handles hydration. It's like our actors stepping into the pre-set stage and beginning their performance!
The beauty of Ajo's approach is in its simplicity. The same render
function works for both initial renders and updates. It's like having actors who can smoothly transition from static mannequins to living, breathing performers without missing a beat!
function* DynamicScene(props) {
let mood = 'happy'
const changeMood = () => {
mood = mood === 'happy' ? 'sad' : 'happy'
this.render()
}
while (true) {
yield (
<div>
<p>The protagonist is feeling {mood}!</p>
<button set:onclick={changeMood}>Change Mood</button>
</div>
)
}
}
🎭 Director's Note: Whether this scene was initially rendered on the server or the client, the changeMood
function will work seamlessly after hydration.
Ajo even supports streaming, allowing your server to send your page in chunks, like a play unfolding act by act:
import { html } from 'ajo/html'
app.get('/stream', (req, res) => {
res.write('<!DOCTYPE html><html><body>')
for (const chunk of html(<App />)) {
res.write(chunk)
}
res.write('</body></html>')
res.end()
})
🎭 Director's Note: Streaming is like revealing your play scene by scene, allowing the audience to start watching before the entire performance is ready!
By mastering SSR and hydration in Ajo, you're creating a seamless experience for your users. It's like giving them a glimpse of the stage (fast initial load with SSR), and then magically bringing everything to life (hydration) without them even noticing the transition.
Remember:
- Use
render
from 'ajo/html' on the server to set your static stage. - Use the same
render
(but from 'ajo') on the client to bring your live performance to life. - Your components work the same way whether they're rendered on the server or client.
- Consider streaming for an even faster initial page load.
Now go forth and create web experiences so smooth, they'll make Broadway jealous! May your servers render swiftly and your hydration be seamless. Break a leg! 🎭✨
Hey there, Ajo enthusiasts! Ready to add some type-safe zest to your components? Let's dive into the world of TypeScript with Ajo and see how it can make your development experience even more delightful!
First things first, let's get our TypeScript ingredients ready:
npm install --save-dev typescript @types/node
Ajo comes with its own special blend of type definitions. These are like the perfect spice mix for your TypeScript dishes!
For our stateless components, think of them as simple, no-fuss appetizers:
import { h } from 'ajo'
interface GreetingProps {
name: string;
age?: number;
}
const Greeting = (props: GreetingProps) => (
<div>
<h1>Hello, {props.name}!</h1>
{props.age && <p>You are {props.age} years old.</p>}
</div>
)
Now, for our stateful components, we're cooking with gas! Remember, we don't destructure args here - it's like keeping all our ingredients in one pot:
import { Component } from 'ajo'
interface CounterProps {
initialCount?: number;
}
const Counter: Component<CounterProps> = function* (args) {
let count = args.initialCount ?? 0
const increment = () => {
count++
this.render()
}
while (true) {
yield (
<div>
<p>Count: {count}</p>
<button set:onclick={increment}>Spice it up!</button>
</div>
)
}
}
Let's add some context to our dish:
import { Component, context } from 'ajo'
interface Theme {
primaryColor: string;
secondaryColor: string;
}
const ThemeContext = context<Theme>({
primaryColor: '#FF6347', // Tomato red, spicy!
secondaryColor: '#FFA500' // Orange, for a zesty kick
})
const ThemedButton: Component = function* () {
while (true) {
const theme = ThemeContext()
yield (
<button style={`background-color: ${theme.primaryColor}; color: ${theme.secondaryColor};`}>
Spicy Button
</button>
)
}
}
In the Ajo kitchen, we have two types of refs - think of them as two different garnishes that can add that perfect finishing touch to your dish. Let's explore both!
When you need to interact directly with DOM elements, use refs like this:
import { Component } from 'ajo'
const InputWithFocus: Component = function* () {
let inputRef: HTMLInputElement | null = null
const focusInput = () => {
inputRef?.focus()
}
while (true) {
yield (
<>
<input ref={el => inputRef = el} />
<button set:onclick={focusInput}>Focus Input</button>
</>
)
}
}
Here, we're using a ref to get a handle on the input element, allowing us to focus it programmatically. It's like having a direct line to that specific ingredient in your dish!
Now, for the more advanced flavor - refs with stateful components. This is like creating a special connection between your main dish and a complex side dish:
import { Component } from 'ajo'
// First, let's define the type for our component's "this" context
type ComponentElement = ThisParameterType<typeof StatefulComponent> | null
// Now, let's create our stateful component
const StatefulComponent: Component<{ name: string }> = function* (props) {
while (true) {
yield <div>Hello, {props.name}!</div>
}
}
// Here's how we use our component with a ref
function* ParentComponent() {
let componentRef: ComponentElement = null
while (true) {
yield (
<StatefulComponent
name="Ajo Chef"
ref={(el) => (componentRef = el)}
/>
)
}
}
In this gourmet recipe:
- We define
ComponentElement
as the type of our component instance. - We use
Component<Props>
to type our component, which has aref
property added to its props. - In the component, we can now use the
ref
prop to get a reference to the component instance.
This approach allows for type-safe refs that play nicely with Ajo's component lifecycle. It's like having a perfectly coordinated multi-course meal where each dish complements the others!
- Use DOM node refs when you need to interact directly with HTML elements.
- Use stateful component refs when you need to access or control a component instance.
- Use TypeScript's type inference and the provided types (
Component<Props>
) to ensure type safety when working with refs.
By mastering these two types of refs, you'll be able to create more interactive and dynamic Ajo applications. It's like having two secret ingredients that can elevate your code cuisine to new heights! 🚀🍽️
Let's make our event handlers type-safe and tasty:
import { Component } from 'ajo'
const SpicyForm: Component = function* () {
let spiceLevel = ''
const handleInput = (e: Event) => {
if (e.target instanceof HTMLInputElement) {
spiceLevel = e.target.value
this.render()
}
}
const handleSubmit = (e: Event) => {
e.preventDefault()
console.log(`Cooking with ${spiceLevel} spiciness!`)
}
while (true) {
yield (
<form onSubmit={handleSubmit}>
<input type="text" value={spiceLevel} set:oninput={handleInput} />
<button type="submit">Bring the heat!</button>
</form>
)
}
}
- Always season your components with prop types, even if they're as plain as flour (use
{}
for no props). - Use the
Component
type from Ajo to properly type your stateful components - it's the secret ingredient! - Let TypeScript's type inference do the heavy lifting when it can, but don't be shy about adding explicit types when they make your code more flavorful.
- Turn up the heat with
strictNullChecks
in yourtsconfig.json
to catch any bland null or undefined errors. - When using refs, always check if they're fully cooked (not null) before digging in.
By following these recipes, you'll be cooking up type-safe, delicious Ajo applications that are a feast for the eyes (and the linter)! Now go forth and create some TypeScript masterpieces that would make any code chef proud! 👨🍳🎉