PPHP – Reactive JavaScript Framework

A reactive JavaScript framework with declarative templating and component-based architecture.

1. Basic State Management #

Simple State Declaration

<!DOCTYPE html>
<html>
  <body>
    <p>Count: {count}</p>
    <button onclick="setCount(count + 1)">Increment</button>
    <button onclick="setCount(count - 1)">Decrement</button>

    <script type="text/pp">
      const [count, setCount] = pp.state(0);
    </script>
  </body>
</html>

Multiple State Variables

<div>
  <p>Name: {name}</p>
  <p>Age: {age}</p>
  <p>Email: {email}</p>
</div>

<script type="text/pp">
  const [name, setName] = pp.state("John");
  const [age, setAge] = pp.state(25);
  const [email, setEmail] = pp.state("john@example.com");
</script>

2. Text Interpolation #

Basic Interpolation

<div>
  <h1>Welcome, {username}!</h1>
  <p>Today is {new Date().toLocaleDateString()}</p>
  <p>Random number: {Math.floor(Math.random() * 100)}</p>
</div>

<script type="text/pp">
  const [username, setUsername] = pp.state("Alice");
</script>

Complex Expressions

<div>
  <p>Full name: {firstName} {lastName}</p>
  <p>Initials: {firstName.charAt(0)}.{lastName.charAt(0)}.</p>
  <p>Character count: {(firstName + lastName).length}</p>
</div>

<script type="text/pp">
  const [firstName, setFirstName] = pp.state("John");
  const [lastName, setLastName] = pp.state("Doe");
</script>

3. Attribute Binding #

Basic Attribute Binding

<div>
  <img src="{imageUrl}" alt="{imageAlt}" />
  <a href="{linkUrl}" target="{linkTarget}">Click me</a>
  <div class="{cssClass}" style="color: {textColor}">Styled text</div>
</div>

<script type="text/pp">
  const [imageUrl, setImageUrl] = pp.state("https://example.com/image.jpg");
  const [imageAlt, setImageAlt] = pp.state("Sample image");
  const [linkUrl, setLinkUrl] = pp.state("https://example.com");
  const [linkTarget, setLinkTarget] = pp.state("_blank");
  const [cssClass, setCssClass] = pp.state("highlight");
  const [textColor, setTextColor] = pp.state("blue");
</script>

Boolean Attributes

<div>
  <input type="checkbox" checked="{isChecked}" />
  <button disabled="{isDisabled}">Submit</button>
  <input type="text" readonly="{isReadonly}" />
  <details open="{isOpen}">
    <summary>Details</summary>
    <p>Content here</p>
  </details>
</div>

<script type="text/pp">
  const [isChecked, setIsChecked] = pp.state(true);
  const [isDisabled, setIsDisabled] = pp.state(false);
  const [isReadonly, setIsReadonly] = pp.state(false);
  const [isOpen, setIsOpen] = pp.state(true);
</script>

4. Event Handling #

Basic Event Handling

<div>
  <button onclick="setClickCount(clickCount + 1)">Click me {clickCount}</button>
  <button
    onmouseover="setHoverText('Hovering!')"
    onmouseout="setHoverText('Not hovering')"
  >
    Hover: {hoverText}
  </button>
</div>

<script type="text/pp">
  const [clickCount, setClickCount] = pp.state(0);
  const [hoverText, setHoverText] = pp.state("Not hovering");
</script>

Event with Parameters

<div>
  <button onclick="updateMessage('Hello!')">Say Hello</button>
  <button onclick="updateMessage('Goodbye!')">Say Goodbye</button>
  <p>Message: {message}</p>
</div>

<script type="text/pp">
  const [message, setMessage] = pp.state("");

  function updateMessage(text) {
    setMessage(text);
  }
</script>

5. Two-Way Data Binding #

Input Binding

<div>
  <input type="text" value="{inputValue}" placeholder="Type something..." oninput="setInputValue(event.target.value)" />
  <p>You typed: {inputValue}</p>
  <p>Length: {inputValue.length}</p>
</div>

<script type="text/pp">
  const [inputValue, setInputValue] = pp.state("");
</script>

Form Controls

<form>
  <div>
    <label>Name: <input type="text" value="{formData.name}" /></label>
  </div>
  <div>
    <label>
      <input type="checkbox" checked="{formData.subscribe}" onchange="setFormData({ ...formData, subscribe: event.target.checked })" />
      Subscribe to newsletter
    </label>
  </div>
  <div>
    <label>
      Country:
      <select
        value="{formData.country}"
        onchange="setFormData({ ...formData, country: event.target.value })"
      >
        <option value="us">United States</option>
        <option value="ca">Canada</option>
        <option value="uk">United Kingdom</option>
      </select>
    </label>
  </div>
  <p>Form data: {JSON.stringify(formData)}</p>
</form>

<script type="text/pp">
  const [formData, setFormData] = pp.state({
    name: "",
    subscribe: false,
    country: "us"
  });
</script>

6. Conditional Rendering #

Simple Conditionals

<div>
  <button onclick="setIsVisible(!isVisible)">Toggle</button>
  <p hidden="{!isVisible}">This text can be hidden!</p>
  <p>{isVisible ? 'Text is visible' : 'Text is hidden'}</p>
</div>

<script type="text/pp">
  const [isVisible, setIsVisible] = pp.state(true);
</script>

Multiple Conditions

<div>
  <select value="{userType}" onchange="setUserType(event.target.value)">
    <option value="guest">Guest</option>
    <option value="user">User</option>
    <option value="admin">Admin</option>
  </select>

  <div class="{userType === 'admin' ? 'admin-panel' : 'user-panel'}">
    <h3>
      {userType === 'admin' ? 'Admin Dashboard' : userType === 'user' ? 'User
      Dashboard' : 'Welcome'}
    </h3>
    <p hidden="{userType === 'admin' ? false : true}">Admin controls here</p>
    <p hidden="{userType === 'user' ? false : true}">User content here</p>
    <p hidden="{userType === 'guest' ? false : true}">Please log in</p>
  </div>
</div>

<script type="text/pp">
  const [userType, setUserType] = pp.state("guest");
</script>

7. List Rendering #

Basic List

<ul>
  <template pp-for="item in items">
    <li>{item}</li>
  </template>
</ul>

<script type="text/pp">
  const [items, setItems] = pp.state(["Apple", "Banana", "Cherry"]);
</script>

List with Index

<ul>
  <template pp-for="(item, index) in items">
    <li>{index + 1}. {item}</li>
  </template>
</ul>

<script type="text/pp">
  const [items, setItems] = pp.state(["First", "Second", "Third"]);
</script>

Object List with Key

<div>
  <template pp-for="user in users">
    <div key="{user.id}">
      <h4>{user.name}</h4>
      <p>Email: {user.email}</p>
      <p>Age: {user.age}</p>
      <button onclick="removeUser(user.id)">Remove</button>
    </div>
  </template>
</div>

<script type="text/pp">
  const [users, setUsers] = pp.state([
    { id: 1, name: "Alice", email: "alice@example.com", age: 30 },
    { id: 2, name: "Bob", email: "bob@example.com", age: 25 },
    { id: 3, name: "Charlie", email: "charlie@example.com", age: 35 }
  ]);

  function removeUser(userId) {
    setUsers(users.filter(user => user.id !== userId));
  }
</script>

8. Components #

Basic Component

<div>
  <div pp-component="counter">
    <h3>Counter Component</h3>
    <p>Count: {count}</p>
    <button onclick="setCount(count + 1)">+</button>
    <button onclick="setCount(count - 1)">-</button>

    <script type="text/pp">
      const [count, setCount] = pp.state(0);
    </script>
  </div>
</div>

Multiple Components

<div>
  <div pp-component="header">
    <header>
      <h1>{title}</h1>
      <nav>Navigation here</nav>
    </header>

    <script type="text/pp">
      const [title, setTitle] = pp.state("My Website");
    </script>
  </div>

  <div pp-component="sidebar">
    <aside>
      <h3>Sidebar</h3>
      <ul>
        <template pp-for="link in links">
          <li><a href="{link.url}">{link.text}</a></li>
        </template>
      </ul>
    </aside>

    <script type="text/pp">
      const [links, setLinks] = pp.state([
        { url: "/home", text: "Home" },
        { url: "/about", text: "About" },
        { url: "/contact", text: "Contact" }
      ]);
    </script>
  </div>
</div>

9. Effects & Reactivity #

Basic Effect

<div>
  <input type="text" value="{searchTerm}" placeholder="Search..." />
  <p>Search results count: {results.length}</p>
</div>

<script type="text/pp">
  const [searchTerm, setSearchTerm] = pp.state("");
  const [results, setResults] = pp.state([]);

  // Effect that runs when searchTerm changes
  pp.effect(() => {
    if (searchTerm) {
      // Simulate search
      const mockResults = ["Result 1", "Result 2", "Result 3"]
        .filter(item => item.toLowerCase().includes(searchTerm.toLowerCase()));
      setResults(mockResults);
    } else {
      setResults([]);
    }
  }, [searchTerm]);
</script>

Effect with Cleanup

<div>
  <button onclick="startTimer()">Start Timer</button>
  <button onclick="stopTimer()">Stop Timer</button>
  <p>Seconds: {seconds}</p>
</div>

<script type="text/pp">
  const [seconds, setSeconds] = pp.state(0);
  const [isRunning, setIsRunning] = pp.state(false);

  function startTimer() {
    setIsRunning(true);
  }

  function stopTimer() {
    setIsRunning(false);
  }

  pp.effect(() => {
    if (isRunning) {
      const interval = setInterval(() => {
        setSeconds(seconds + 1);
      }, 1000);

      // Cleanup function
      return () => clearInterval(interval);
    }
  }, [isRunning]);
</script>

10. Component Props #

Parent-Child Communication

<div pp-component="app">
  <h1>Parent Component</h1>
  <div
    pp-component="child"
    message="{parentMessage}"
    count="{parentCount}"
    update-count="{updateCount}"
  >
    <!-- Child component content -->
    <h2>Child Component</h2>
    <p>Message from parent: {message}</p>
    <p>Count from parent: {count}</p>
    <button onclick="handleChildClick()">Child Button</button>

    <script type="text/pp">
      function handleChildClick() {
        // Access parent's updateCount function
        updateCount();
      }
    </script>
  </div>

  <button onclick="updateMessage()">Update Message</button>
  <button onclick="updateCount()">Update Count</button>

  <script type="text/pp">
    const [parentMessage, setParentMessage] = pp.state("Hello from parent!");
    const [parentCount, setParentCount] = pp.state(0);

    function updateMessage() {
      setParentMessage("Updated message at " + new Date().toLocaleTimeString());
    }

    function updateCount() {
      setParentCount(parentCount + 1);
    }
  </script>
</div>

Dynamic Props

<div pp-component="app">
  <div
    pp-component="user-card"
    user-name="{selectedUser.name}"
    user-email="{selectedUser.email}"
    user-role="{selectedUser.role}"
    theme="{currentTheme}"
  >
    <div class="user-card {theme}">
      <h3>{userName}</h3>
      <p>{userEmail}</p>
      <span class="role-{userRole}">{userRole}</span>
    </div>
  </div>

  <select value="{selectedUserId}" onchange="changeUser(event.target.value)">
    <template pp-for="user in users">
      <option value="{user.id}">{user.name}</option>
    </template>
  </select>

  <script type="text/pp">
    const [users, setUsers] = pp.state([
      { id: 1, name: "Alice", email: "alice@example.com", role: "admin" },
      { id: 2, name: "Bob", email: "bob@example.com", role: "user" }
    ]);
    const [selectedUserId, setSelectedUserId] = pp.state(1);
    const [currentTheme, setCurrentTheme] = pp.state("light");
    const [selectedUser, setSelectedUser] = pp.state([]);

    pp.effect(() => {
        const user = users.find(u => u.id === selectedUserId);
        setSelectedUser(user);
    }, [selectedUserId]);

    function changeUser(userId) {
      setSelectedUserId(parseInt(userId));
    }
  </script>
</div>

11. Context Management #

Context Switching

<h1>App Component</h1>
<p>App count: {appCount}</p>

<div pp-component="child1" app-count="{appCount}">
  <h2>Child 1 (own context)</h2>
  <p>Child count: {childCount}</p>
  <p>Access by props app count: {appCount || 'undefined'}</p>

  <div
    pp-component="grandchild1"
    app-count="{appCount}"
    child-count="{childCount}"
  >
    <h3>Grandchild</h3>
    <p>App count via props: {appCount}</p>
    <p>Child count via props: {childCount || 'undefined'}</p>
  </div>

  <div>
    <h3>Grandchild (app context)</h3>
    <p>App count via app context: {appCount}</p>
    <button pp-context="app" onclick="incrementAppCount()">
      Increment App Count
    </button>
  </div>

  <script>
    const [childCount, setChildCount] = pp.state(0);
  </script>
</div>

<script>
  const [appCount, setAppCount] = pp.state(0);

  function incrementAppCount() {
    setAppCount(appCount + 1);
  }
</script>

12. Spread Attributes #

Basic Spread

<div>
  <div pp-spread="{...commonAttributes}">Common attributes applied</div>
  <div pp-spread="{...buttonAttributes, {class: 'extra-class'}}">
    Button with spread
  </div>
</div>

<script type="text/pp">
  const [commonAttributes, setCommonAttributes] = pp.state({
    class: "shared-class",
    "data-testid": "common-element",
    style: "padding: 10px; border: 1px solid #ccc;"
  });

  const [buttonAttributes, setButtonAttributes] = pp.state({
    type: "button",
    disabled: false,
    onclick: "handleButtonClick()"
  });

  function handleButtonClick() {
    console.log("Button clicked!");
  }
</script>

Dynamic Spread

<div>
  <input pp-spread="{...inputProps}" value="{inputValue}" />
  <button onclick="toggleRequired()">Toggle Required</button>
  <p>Current props: {JSON.stringify(inputProps)}</p>
</div>

<script type="text/pp">
  const [inputValue, setInputValue] = pp.state("");
  const [isRequired, setIsRequired] = pp.state(false);
  const [inputProps] = pp.state({
      type: "text",
      placeholder: "Enter text...",
      required: isRequired,
      class: isRequired ? "required" : "optional"
  });

  function toggleRequired() {
    setIsRequired(!isRequired);
  }
</script>

13. Advanced Patterns #

Complex State Management – Todo App

Includes localStorage hydration, filters, and live search.
<style>
  .todo-app { max-width: 600px; margin: 0 auto; padding: 20px; }
  .add-todo { display: flex; gap: 10px; margin-bottom: 20px; }
  .add-todo input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
  .filters { display: flex; gap: 10px; margin-bottom: 20px; }
  .filters button { padding: 6px 12px; border: 1px solid #ddd; background: white; cursor: pointer; border-radius: 4px; }
  .filters button.active { background: #007bff; color: white; border-color: #007bff; }
  .todo-list { list-style: none; padding: 0; }
  .todo-list li { display: flex; align-items: center; gap: 10px; padding: 10px; border-bottom: 1px solid #eee; }
  .todo-list li.completed .todo-text { text-decoration: line-through; opacity: 0.6; }
  .todo-text { flex: 1; }
  .remove { background: #dc3545; color: white; border: none; border-radius: 4px; width: 24px; height: 24px; cursor: pointer; }
  .stats { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
  .stats p { margin: 5px 0; }
</style>

<h1>Todo App</h1>

<div class="add-todo">
  <input
    type="text"
    value="{newTodoText}"
    oninput="setNewTodoText(this.value)"
    onkeyup="(event.key === 'Enter') && addTodo()"
    placeholder="Add todo..."
  />
  <button onclick="addTodo()" disabled="{!newTodoText}">Add</button>
</div>

<div class="search" style="display:flex; gap:10px; margin: 10px 0 20px;">
  <input
    type="text"
    value="{search}"
    oninput="setSearch(this.value)"
    placeholder="Search todos…"
    style="flex:1; padding:8px; border:1px solid #ddd; border-radius:4px;"
  />
  <button
    onclick="setSearch('')"
    disabled="{!search}"
    style="padding:6px 12px; border:1px solid #ddd; background:white; border-radius:4px; cursor:pointer;"
  >Clear</button>
</div>

<div class="filters">
  <button onclick="setFilter('all')" class="{filter === 'all' ? 'active' : ''}">All</button>
  <button onclick="setFilter('active')" class="{filter === 'active' ? 'active' : ''}">Active</button>
  <button onclick="setFilter('completed')" class="{filter === 'completed' ? 'active' : ''}">Completed</button>
</div>

<ul class="todo-list">
  <template pp-for="todo in filteredTodos">
    <li key="{todo.id}" class="{todo.completed ? 'completed' : ''}">
      <input type="checkbox" checked="{todo.completed}" onchange="toggleTodo(todo.id)" />
      <span class="todo-text">{todo.text}</span>
      <button onclick="removeTodo(todo.id)" class="remove">×</button>
    </li>
  </template>
</ul>

<div class="stats">
  <p>Total: {todos.length}</p>
  <p>Active: {activeTodos.length}</p>
  <p>Completed: {completedTodos.length}</p>
</div>

<script type="text/pp">
  const [todos, setTodos] = pp.state([]);
  const [newTodoText, setNewTodoText] = pp.state("");
  const [filter, setFilter] = pp.state("all");
  const [nextId, setNextId] = pp.state(1);
  const [search, setSearch] = pp.state("");
  const [activeTodos, setActiveTodos] = pp.state([]);
  const [completedTodos, setCompletedTodos] = pp.state([]);
  const [filteredTodos, setFilteredTodos] = pp.state([]);

  const [hydrated, setHydrated] = pp.state(false);

  function addTodo() {
    const text = newTodoText.trim();
    if (!text) return;

    const newTodo = { id: nextId, text, completed: false };
    setTodos([...todos, newTodo]);
    setNewTodoText("");
    setNextId(nextId + 1);
  }

  function toggleTodo(id) {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }

  function removeTodo(id) {
    setTodos(todos.filter((todo) => todo.id !== id));
  }

  // Hydrate once from localStorage
  pp.effect(() => {
    try {
      const saved = localStorage.getItem("todos");
      if (saved) {
        const parsed = JSON.parse(saved);
        if (Array.isArray(parsed)) {
          setTodos(parsed);
          if (parsed.length > 0) {
            const maxId = Math.max(...parsed.map((t) => Number(t.id) || 0));
            setNextId(maxId + 1);
          }
        }
      }
    } catch (e) {
      console.warn("⚠️ Failed to parse saved todos:", e);
    } finally {
      setHydrated(true);
      console.log("✅ Hydrated from storage");
    }
  }, []);

  // Save whenever todos change (post-hydration)
  pp.effect(() => {
    if (!hydrated) return;
    try {
      localStorage.setItem("todos", JSON.stringify(todos));
      console.log("💾 Saved todos:", todos);
    } catch (e) {
      console.warn("⚠️ Failed to save todos:", e);
    }
  }, [todos, hydrated]);

  // Recompute derived lists when todos OR filter OR search change
  pp.effect(() => {
    const active = todos.filter((t) => !t.completed);
    const completed = todos.filter((t) => t.completed);

    setActiveTodos(active);
    setCompletedTodos(completed);

    // Apply base filter first
    let base =
      filter === "active" ? active : filter === "completed" ? completed : todos;

    // Then apply search (case-insensitive, trims spaces)
    const term = search.trim().toLowerCase();
    if (term) {
      base = base.filter((t) => (t.text || "").toLowerCase().includes(term));
    }

    setFilteredTodos(base);

    console.log("🧮 Derived ->", { filter, search: term, resultCount: base.length });
  }, [todos, filter, search]);
</script>
Prisma PHP V4 Alpha: This documentation covers the alpha version of Prisma PHP V4. Features, APIs, and syntax may change before the stable release. Use for testing and feedback only—do not use in production.