(also available at frmwrk.lol in docsify's format)
A JavaScript framework emphasizing simplicity by using JavaScript objects for templating and events for data handling.
frmwrk aims to provide a lightweight and efficient interface for creating dynamic websites and applications. By blending simplicity with maintainability, it offers a robust solution for modern web development.
import { render } from "@nenadg/frmwrk";
render({
div: {
innerText: "hello world",
},
});
- Introduction
- Core Features
- Getting Started
- Components and Models
- Template Notation
- Event Handling
- Advanced Features
- General state management
- API Reference
frmwrk is a minimalist JavaScript framework designed for building dynamic web applications. Its focus on performance and simplicity makes it ideal for projects ranging from small websites to complex applications.
- Lightweight: Minimalistic, ensuring fast load times and execution.
- Component-Based: Build reusable, stateful UI components.
- Reactive Data Binding: Keeps UI synchronized with application state.
- Modularity: Easy integration with custom components and models.
- Dynamic Component Placement: Fine-grained control over DOM.
Install frmwrk with Webpack for a basic setup:
npm install --save @nenadg/frmwrk webpack webpack-cli webpack-dev-server
Basic Project Structure
frmwrk.hello/
├── src/
│ └── index.js
├── public/
│ └── index.html
├── package.json
Update package.json
to use Webpack's development server:
"scripts": {
"start": "webpack-dev-server --mode development --progress"
}
import { render } from "@nenadg/frmwrk";
render({
div: {
innerText: "hello world",
},
});
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frmwrk Example</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
Run the example using:
npm start
Components and Models are the primary building blocks in frmwrk. Both are JS objects containing name
, type
and config
properties. Components are used to express your HTML, while models are used to express your driving logic using event listeners.
Components define the HTML structure as JavaScript objects.
const HelloComponent = {
type: "component",
name: "Hello",
config: {
div: {
innerText: "I'm a component",
},
},
};
export default HelloComponent;
Models manage state and handle events for components.
const HelloModel = {
type: "model",
name: "Hello",
config: {
oninitelement: (model) => model.update({ text: "My first component" }),
},
};
export default HelloModel;
const HelloComponent = {
type: "component",
name: "Hello",
config: {
div: {
model: "Hello",
oninitelement: "Hello::oninitelement",
innerText: "{*:text}",
},
},
};
frmwrk binds data to the DOM using:
- Direct Matching:
{*:property}
binds directly to a model property. - Linear Matching: Loops through arrays with
@{*:array}
. - Cross Matching: Handles nested loops for complex data binding.
frmwrk also binds events using:
- Direct event assignment:
Model::Event
bindsEvent
to theModel
(eg.onclick: 'Model::OnClick
). Can bind multiple events this way (eg.onclick: "Model::OnClick,Model::OnAnotherEvent"
) - Data-bound event assignment: Events are bind as Direct matched data
{*:onClick}
frmwrk can also load components based on data it has:
- Floating components: Components can be expressed using direct match expression as
{*:someComponent}
that will change based on it's state in the model.
frmwrk has built-in mechanism that allows reusability expressed through meta
concept.
Direct matching is the simplest way to bind a single property from the model to an element's attribute or content. For instance, to bind a text
property:
Model:
const DirectModel = {
type: "model",
name: "DirectExample",
config: {
oninitelement: (model) => model.update({ text: "Hello, World!" }),
},
};
Component:
const DirectComponent = {
type: "component",
name: "DirectExample",
config: {
div: {
innerText: "{*:text}",
},
},
};
This will render a <div>
with the text "Hello, World!".
Linear matching allows you to iterate over an array in the model and render elements for each item.
Model:
const LinearModel = {
type: "model",
name: "LinearExample",
config: {
oninitelement: (model) =>
model.update({
todos: [{ text: "Buy groceries" }, { text: "Walk the dog" }],
}),
},
};
Component:
const LinearComponent = {
type: "component",
name: "LinearExample",
config: {
ul: {
children: [
{
"@{*:todos}": {
li: {
innerText: "{*:todos[*:todos.text]}",
},
},
},
],
},
},
};
This will render:
<ul>
<li>Buy groceries</li>
<li>Walk the dog</li>
</ul>
Cross matching enables rendering elements by correlating data from multiple arrays.
Basic Project Structure
frmwrk.table/
├── src/
│ └── index.js
│ └───Table/
│ └──────table.model.js
│ └──────table.header.row.js
│ └──────table.header.js
│ └──────table.body.row
│ └──────table.js
├── public/
│ └── index.html
├── package.json
Model:
// table.model.js
const TableModel = {
name: "Table",
type: "model",
config: {
oninitelement: async (model) => {
model.update({
header: [
{
header_text: 'Todo name',
index: 'name'
},
{
header_text: 'Due date',
index: 'due_date'
},
{
header_text: 'Status',
index: 'completed'
}
...
],
todos: [
{
name: 'Buy groceries',
due_date: '01/02/2025',
completed: 'not-completed',
},
{
name: 'Sell crypto',
due_date: '02/02/2025',
completed: 'not-completed'
},
...
]
})
}
}
};
export default TableModel;
Components:
// table.header.row.js
const TableHeaderRow = {
name: "TableHeaderRow",
type: "component",
config: {
tr: {
children: [
{
"@{*:header}": {
th: {
innerText: "{*:header[*:header.header_text}",
},
},
},
],
},
},
};
export default TableHeaderRow;
// table.header.js
import TableHeaderRow from "./table.header.row.js";
const TableHeader = {
name: "TableHeader",
type: "component",
config: {
thead: {
children: [TableHeaderRow],
},
},
};
export default TableHeader;
// table.body.row.js
const TableBodyRow = {
name: "TableBodyRow",
type: "component",
config: {
tr: {
children: [
{
"@{*:header}": {
td: {
innerHTML: "{*:todos.?[*:header.index]}",
},
},
},
],
},
},
};
export default TableBodyRow;
// table.body.js
import TableBodyRow from "./table.body.row.js";
const TableBody = {
name: "TableBody",
type: "component",
config: {
tbody: {
model: "Table",
oninitelement: "Table::oninitelement",
children: [
{
"@{*:todos}": "TableBodyRow",
},
],
},
},
};
export default TableBody;
// table.js
import { bundle } from "@nenadg/frmwrk";
import TableModel from "./table.model.js";
import TableHeader from "./table.header.js";
import TableBody from "./table.body.js";
const Table = {
name: "Table",
type: "component",
config: {
table: {
model: "Table",
oninitelement: "Table::oninitelement",
children: [
TableHeader,
TableBody,
//, TableFooter
],
},
},
};
bundle([TableModel]);
export default Table;
// index.js
import { render } from "@nenadg/frmwrk";
import Table from "./Table/table.js";
render(Table);
This will render:
<table>
<thead>
<tr>
<th>Task</th>
<th>Due date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Buy groceries</td>
<td>01/02/2025</td>
<td>Pending</td>
</tr>
<tr>
<td>Walk the dog</td>
<td>02/02/2025</td>
<td>Completed</td>
</tr>
</tbody>
</table>
To assign an event to the template logic, we need to give it a proper name containing the name of the model that handles such an event, and the name of event, joined together like this Model::Event
.
To follow this example, we are going to add sorting to our todos HTML table. Let's modify the TableHeaderRow
component by adding event to the header click action:
// table.header.row.js
const TableHeaderRow = {
name: "TableHeaderRow",
type: "component",
config: {
tr: {
children: [
{
"@{*:header}": {
th: {
innerText: "{*:header[*:header.header_text}",
onclick: "Table::OnSort",
},
},
},
],
},
},
};
export default TableHeaderRow;
Template was extended by adding onclick: "Table::OnSort"
, now we have to add that event to our model configuration:
// table.model.js
const TableModel = {
name: "Table",
type: "model",
config: {
oninitelement: async (model) => {
...
},
OnSort: async (model, state, e) => {
// this event fires when header row is clicked
...
}
}
};
export default TableModel;
Every event, direct or bound have the same interface that we are going to cover in Event handling section of this documentation.
Events can be assigned directly using Model::Event
notation, or assigned as a value to be bound to the current model configuration as {*:someEvent}
.
To have an event that can change over time and model configuration we are simply going to treat it as any value and change our template configuration to handle it like that:
const TableHeaderRow = {
name: "TableHeaderRow",
type: "component",
config: {
tr: {
children: [
{
"@{*:header}": {
th: {
innerText: "{*:header[*:header.header_text}",
onclick: "{*:onHeaderClick}",
},
},
},
],
},
},
};
Now we can set what it does in our model:
// table.model.js
const TableModel = {
name: "Table",
type: "model",
config: {
oninitelement: async (model) => {
model.update({
header: [ ... ],
todos: [ ... ],
onHeaderClick: "Table::OnSort"
})
},
OnSort: async (model, state, e) => {
// let's not fire this event but 'OnSomethingElse'
model.update({onHeaderClick: "Table::OnSomethingElse"});
},
OnSomethingElse: async (model, state, e) => {
// this event is fired after the 'OnSort' has changed it
},
}
};
export default TableModel;
Let's say that we are not going to display todo list all the time, and we need to show some other screens too. For example, we are going to display a landing page before one can enter todo list.
{
ul: {
className: 'todo-list',
children: [
'{*:landingOrTodo}'
]
}
}
and then set the initial state of todo's model to show bundled component:
{
oninitelement: (model) => model.update({ ... landingOrTodo: 'Landing', ... }),
}
and later changing it at some point:
{
onclick: (model) => model.update({ ... landingOrTodo: 'Todo', ... }),
}
This will render separate views, based solely on the template (component) and model configuration.
The concept of using meta
property is to facilitate component reuse in frmwrk. It enhances modularity and flexibility by allowing the same component configuration to be used across different contexts with different models. Here's a breakdown of how this works and why it's beneficial:
Purpose: The meta property acts as a bridge between a component's configuration and different data models. It maps component properties to different model properties, allowing the same component to adapt to various data contexts without rewriting the component logic or structure.
Functionality: By specifying meta in a component's configuration, you can redefine which model properties should be bound to the component's properties. This is particularly useful when you want to use the same component structure with different data sources.
Example
We are going to reuse our Table
component in different data context (using different model):
let SomeOtherModel = {
type: "model",
name: "SomeOtherModel",
config: {
oninitelement: (model, state, e) => {
model.update({
otherHeader: [
/* different data */
],
otherTodos: [
/* different data */
],
onOtherHeaderClick: "SomeOtherModel::OnSort",
});
},
},
};
while our Table
is going to be reconfigured to use meta
property to translate original context to some new:
import Table from "./Table/table.js";
render({
Table: {
model: "SomeOtherModel",
oninitelement: "SomeOtherModel::oninitelement",
meta: {
header: "otherHeader",
todos: "otherTodos",
onHeaderClick: "onOtherHeaderClick",
},
},
});
In this setup:
meta Mapping: The meta property maps the header
, todos
and onHeaderClick
from Table to otherHeader
, otherTodos
and onOtherHeaderClick
of SomeOtherModel. This mapping tells the framework to bind these new model properties to the existing component properties when rendering.
Flexibility: The component remains the same, but the data it displays is dynamically sourced from SomeOtherModel thanks to the meta property.
Incorporating the parent property within a frmwrk component configuration provides an efficient way to specify exactly where in the DOM the component should be rendered. This allows for more precise control over the UI and helps integrate frmwrk components smoothly into existing web applications or pages with specific layout requirements.
export const App = {
type: "component",
name: "App",
config: {
div: {
className: "app-container",
parent: "#app", // Specifies the ID of the DOM element where the component should render
children: [TodoComponent, TodoCounterComponent],
},
},
};
Models propagate their data in a top-down manner. This means that every component that has model: "SomeModel"
assigned to a parent component will inherit it's data context. This can be overriden using implicit
property.
const HelloComponent = {
type: "component",
name: "Hello",
config: {
div: {
model: "Hello",
oninitelement: "Hello::oninitelement",
children: [
{
p: {
innerText: "{*:text}",
},
},
{
p: {
model: "SomeOtherModel",
implicit: true,
innerText: "{*:someOtherText}",
},
},
],
},
},
};
Using implicit
keyword while assigning different model will have the {*:someOtherModel}
source it's value from that other model instead of the one configured upstream.
frmwrk handles positioning of the elements out of the box without the need to provide keys, indexes or things like that. HTML elements will appear in the order they are expressed in the template, while looping elements will appear in the order they are assigned in their respective arrays in model's configuration. The position property in frmwrk is a helpful attribute that provides precise control over the placement of components within their parent container in the DOM adding to flexibility.
export const App = {
type: "component",
name: "App",
config: {
div: {
className: "app-container",
parent: "#app", // Parent container's ID
children: [
{
div: {
className: "header",
innerText: "Application Header",
position: 0, // Ensures this is always the first element
},
},
TodoComponent,
TodoCounterComponent,
{
div: {
className: "footer",
innerText: "Application Footer",
position: 3, // Ensures this is always the last element, assuming there are 4 elements total
},
},
],
},
},
};
The data-alive
property allows components to be conditionally rendered based on a specific state. This feature is akin to conditional rendering in other frameworks but directly influences the DOM
presence of the element.
Usage: When {*:isSomePropertyAlive}
evaluates to false, the corresponding element is not just hidden but removed from the DOM. When it evaluated to true, element will be rendered back to it's place. This can be beneficial for performance, especially in applications with potentially many dynamic elements, as it reduces the load on the browser's rendering engine.
{
div: {
'data-alive': '{*:isVisible}',
className: 'dynamic-content',
innerText: 'This content is conditionally rendered.'
}
}
frmwrk employs HTML native data-* attribute to store additional state data if needed. As in the example with data-alive property, similarly you can assign data-whatever if you want it's value to be represented in the state argument for event handling.
{
button: {
'data-alive': '{*:isVisible}',
'data-whatever': '{*:whatever}',
onclick: "Model::OnClick",
textContent: 'Get 2 state values'
}
}
OnClick: (model, state) => {
// in this example 'state' argument will provide an array of
// two state objects directly assigned to the template,
// { prop: 'isVisible', value: true, set: { isVisible: true, whatever: 'something' }}
// { prop: 'whatever', value: 'something', set: { isVisible: true, whatever: 'something' }}
// we are going to use 'index' which is the second member of this array as defined in the template
...
};
Events in frmwrk pass three arguments:
- model: The instance of the model where the event is being handled.
- state: Snapshot of the current element’s data.
- event: The native event object.
state provides array of elements state objects containing these properties:
- prop: The name of the property in the model's data collection.
- value: The value of the property.
- set: A larger data context to which the property belongs.
Extend the table.header.row.js
:
{
th: {
innerText: "{*:header[*:header.header_text}",
"data-prop": "{*:header[*:header.index}",
onclick: "Table::OnSort",
},
}
OnSort: (model, state) => {
// in this example 'state' argument will provide an array of
// two state objects directly assigned to the template,
// { prop: 'header_text', value: 'Todo name', set: { header_text: 'Todo name', index: 'name' }}
// { prop: 'index', value: 'name', set: { header_text: 'Todo name', index: 'name' }}
// we are going to use 'index' which is the second member of this array as defined in the template
let prop = state[1].value;
// get the initial data set in 'oninitelement'
let data = model.getData(true);
let todos = data.todos;
model.update({
todos: todos.sort((a, b) =>
b[prop].toLowerCase().localeCompare(a[prop].toLowerCase()),
),
});
};
Let's dive deeper into some concepts shown in the example above.
The intricacies of the (model, state, event) trio in frmwrk, play a crucial role in managing component behavior and interactions. Here's a deeper dive into how each part of this trio works and interacts within the framework:
Represents the data model assigned to a specific component. It acts as the central management point for the data related to that component.
Data Handling: The model is responsible for storing, updating, and retrieving the component's data. It supports operations like model.update(...)
, model.append(...)
, model.unload()
, and model.persist()
.
Event Initiation: Initializes the component's state using the oninitelement
event, setting up the initial data state when a component is rendered.
Data Retrieval: Offers methods like model.getData(true) for fetching the initial state and model.getData(-N) for accessing historical states, facilitating undo-like features or debugging.
Represents the current snapshot of data related to the component at the moment an event is handled. It facilitates direct interaction with the data relevant to a particular event.
Data Snapshot: Provides a snapshot of the data at the time of the event, allowing the event handler to access and modify relevant data based on user interactions.
Data Structure: The state is typically an array of objects, each containing properties like prop (property name), value (current value), and set (a larger data set to which the property belongs). This structure helps in pinpointing the exact data affected by an event.
Complex Data Handling: In scenarios where set is complex, it can map to an object or an array, providing a deeper level of interaction with the data structure.
Represents the browser-generated event (like click, input, etc.) that triggers the model and state functions.
Interaction Handling: Captures user interactions and triggers the associated event handlers in the model.
Default Behavior: Can be used within the framework to prevent default actions (like form submission) or to stop propagation, offering more control over the event lifecycle.
Customization: Developers can define custom events within the framework to handle more specific behaviors tailored to their application needs.
Event Driven: When an event occurs, it triggers an event handler that uses the model and state to respond appropriately. For example, a click event on a button might use the state to check which button was clicked and the model to update the data accordingly.
Data Flow: The model updates influence what is stored in the state, and the state provides the context for what the event is acting upon. This interplay allows for a dynamic yet controlled data flow within components.
Lifecycle Management: The lifecycle events like oninitelement
and onunload
help in managing the setup and teardown of components, ensuring data consistency and component integrity throughout the application lifecycle.
Understanding and utilizing the (model, state, event) trio effectively allows for sophisticated component and data management in frmwrk, making it a powerful tool for building interactive and responsive web applications.
Using async/await for lazy loading of event logic in frmwrk is an innovative approach that enhances the efficiency and scalability of applications. This technique allows for components to be lighter and more responsive by only loading the code necessary for specific interactions when those interactions occur. Here’s an in-depth look at this concept:
Lazy Loading: The essence of lazy loading in this context is to defer the loading of event handlers until they are actually needed. This is particularly useful for large applications with many features that may not be immediately required on the initial load.
Async/Await: Using async/await with dynamic imports (import()) allows you to fetch the event handlers asynchronously from separate files only when the related events are triggered.
In example, SomeModelWithLazyEvents
is defined with several asynchronous actions that are loaded only when invoked. Here’s how it works:
Model Configuration
let SomeModelWithLazyEvents = {
type: "model",
name: "SomeModelWithLazyEvents",
config: {
oninitelement: (...) => { ... },
SomeAction: async (model, state, e) =>
await import('./Events/SomeAction.js')
.then(module => module.default(model, state, e))
.catch(e => console.log('[e] can\'t load SomeAction.', e)),
SomeOtherAction: async (model, state, e) =>
await import('./Events/SomeOtherAction.js')
.then(module => module.default(model, state, e))
.catch(e => console.log('[e] can\'t load SomeOtherAction.', e))
}
};
Dynamic Import: Event handlers like SomeAction and SomeOtherAction are loaded dynamically using import(). This import is triggered only when the event occurs.
Handling Failures: The .catch() method ensures that any issues during the load (such as network errors) are gracefully handled, preventing the application from breaking.
External Event Handler File
/* ./Events/SomeAction.js */
export default async (model, state, event) => {
// Event-specific logic here
};
Separation of Concerns: By moving the event logic to separate files, you not only reduce the initial load size but also organize your code better, making it easier to manage and update.
Benefits
Performance: Improves initial load time by reducing the size of the initial JavaScript bundle.
Scalability: Makes it easier to scale applications by adding more features without bogging down the initial load.
Maintainability: Helps keep the codebase more organized and manageable by separating event logic into different files.
Considerations
Network Dependence: Relies on the user's network speed and reliability since event logic needs to be fetched in real-time. Good error handling and fallback mechanisms are crucial.
Caching: Proper caching strategies should be implemented to avoid re-fetching the same code repeatedly.
This lazy loading technique is a sophisticated use of modern JavaScript features that can significantly enhance the user experience and efficiency of applications built with frmwrk.
Persisters in frmwrk are an advanced feature designed to enable actions to be automatically performed after model data updates. This mechanism adds a layer of functionality that can enhance data handling by executing custom logic every time the model is updated. Here's a detailed breakdown of how persisters work and how they can be effectively used:
Purpose: Persisters serve as hooks or middleware that are triggered after the model.update(...) function has successfully updated the model's state. This allows for additional operations to be performed in a controlled manner.
Usage: Persisters are specified as additional arguments to the model.update(...) function, which means they are flexible and can be customized per update operation.
// Define persisters
let persister1 = (model) => {
// Logic that needs to be executed after the model is updated
};
let persister2 = (model) => {
// Another set of operations post-update
};
// Usage in model.update
model.update(
{
key: "value",
},
persister1,
persister2,
);
Serial Execution: Persisters are executed one after the other in the order they are passed to model.update(...)
. This ensures a predictable execution flow and helps manage dependencies between operations.
Argument Passing: Each persister function receives the model as its only argument, giving it access to the updated state and the ability to perform further actions based on that state.
Avoiding Infinite Loops: It's crucial to avoid calling model.update(...)
from within a persister because it would trigger the persisters again, potentially creating infinite loops. Instead, model.append(...)
is used for making further updates from within a persister. This function behaves like model.update(...)
but without triggering additional persister executions.
Benefits
Automation: Automates related tasks that need to occur right after a data update, such as logging, validation, or syncing with external systems.
Consistency: Ensures that all subsequent actions dependent on the updated state are consistently executed.
Flexibility: Provides the ability to tailor post-update behaviors specific to different update scenarios by selectively attaching different persisters.
Considerations
Performance: Although useful, adding many complex persisters can impact performance due to additional processing after each update. It's important to ensure that persisters are as efficient as possible.
Complexity: The use of persisters increases the complexity of the data update mechanism. Proper documentation and understanding of their flow are essential to prevent hard-to-trace bugs.
Persisters are a powerful tool within frmwrk that can significantly enhance the capability to manage side effects and additional operations tied to model state changes. This feature promotes cleaner and more organized code by segregating primary update logic from post-update operations.
Model listeners in frmwrk provide a powerful mechanism for cross-model communication, allowing models to react to changes in other models. This feature facilitates a more reactive architecture where components and models can stay synchronized with each other's state changes. Here’s how this feature is structured and its implications:
Purpose: Listeners are designed to enable a model to react to changes in another model's properties. This is akin to a publish-subscribe pattern where a model subscribes to changes in another model and executes specified logic in response.
Structure: Listeners are defined within a listen object in the model's configuration. This object maps models by their names and specifies arrays of properties to listen to, along with associated callback functions.
{
config: {
// Configuration for the model itself
},
listen: {
SomeModel: [
{
headerTitleText: async (model, someModel) => {
// Logic to execute when 'headerTitleText' in 'SomeModel' changes
}
}
]
}
}
Model-to-Model Binding: In this example, the model is set up to listen to changes in the headerTitleText
property of SomeModel
. Whenever headerTitleText
is updated, the specified asynchronous function is triggered.
Callback Function Parameters:
model: The model that owns the listener.
someModel: The model being listened to, in this case, SomeModel
.
Benefits
Reactivity: Enhances the reactivity of the application by allowing models to respond to changes in other parts of the application, ensuring data consistency across components.
Decoupling: Helps decouple components by allowing them to react to changes without needing to directly invoke methods on other models, which can simplify dependencies and interactions.
Flexibility: Provides flexibility in handling complex interdependencies between models, useful in scenarios where changes in one part of the application need to reflect in another without tightly coupling their implementations.
Use Cases
Synchronized Updates: Useful in dashboard-like interfaces where changes in one widget need to reflect in another.
Conditional Logic: Executes specific logic conditionally based on changes in another model, such as enabling or disabling form inputs based on the state of other components.
Data Validation: Validates or transforms data in one model when another model changes, ensuring data integrity across the system.
Considerations
Performance Implications: Care must be taken to ensure that listeners do not lead to performance bottlenecks, especially in cases where many models are interlinked and updates are frequent.
Complexity in Debugging: Debugging can become more complex due to the indirect nature of interactions initiated by listeners. It's important to maintain clear documentation and possibly implement debugging aids to trace interactions.
Model listeners add a significant layer of interactivity and reactivity within frmwrk, aligning it with more complex application architectures that require dynamic interactions across different components.
frmwrk employs a highly structured approach to state management, where each model's properties are stored independently with their own version history. This approach enhances the ability to track changes over time and provides an efficient mechanism for retrieving current and past states.
Store Structure: The state is stored as a large object (called Data
) where each key corresponds to a model-property combination (e.g., SomeModel--headerTitleText
). Each key maps to an object where each property version is stored.
Versioning and Immutability:
Immutability: When data is updated via model.update(...)
, a new version of the data is created in the state store, preserving the previous versions. This ensures that the state objects are immutable, preventing unintended side effects from changes.
Retrieving Data: Using model.getData()
retrieves the clone of the latest snapshot of the model’s data. However, because of the versioning, it’s possible to access previous states, making debugging and data tracking more manageable.
Performance: This method of storing data ensures quick access to any model’s current and historical data without needing to traverse a complex nested structure. Each property is indexed and retrieved independently, which can significantly speed up lookups in applications with heavy state manipulation.
let originalSnapshot = mode.getData(true); // Retrieves initial state
let stateSnapshot = model.getData(); // Retrieves current state
let historicalState = model.getData(-1); // Retrieves one version back
frmwrk's rendering logic works by creating internal state representing a DOM tree. Such tree (called Tree
) is nothing more than a list of all DOM nodes containing objects called leaves
. Individual leaf
carries it's own information about it's current (and previous) state, HTML representation, model to which it belongs (if any) and position in the DOM throughout it's lifecycle. Data stored in central store called Data
is used for referencing and versioning with the individual states each leaf
maintains for itself.
Here's the API reference what frmwrk exposes and where.
These are native functions that are exposed and available for import {...} from '@nenadg/frmwrk'
getModel(modelName)
: Retrieves a model's configuration by its name.getModel('ModelState').getInstance()
- returns the instance of current model, providing the interface to the model's context outside an event.render(config)
: Renders a component based on the given configuration.define(config)
: Defines or extends a component.bundle([configs])
: Same as define but array.clone(object)
: Creates an immutable clone of a given object.onrenderend()
: Event triggered when the rendering cycle is complete.waitUntil(asyncFunction, ms)
: Pauses execution until the asynchronous function resolves or the timeout expires.wait(ms)
: Pauses execution for the specified duration in milliseconds.
-
oninitelement: Triggered before rendering, used to initialize the model's data.
Example:
function initializeComponent(model, state, event) { model.update({ initialData: "value" }); }
-
onunload: Fires when
model.unload()
completes, allowing cleanup or additional logic.Example:
function cleanupComponent(lastDataState) { console.log("Cleanup with data:", lastDataState); }
model.getData(true | -N)
: Fetches either the original dataset (true
) or the dataset N updates prior (-N
).model.update(data, ...persisters)
: Updates the dataset and optionally triggers persisters.model.append(data)
: Adds data without triggering persisters.model.unload()
: Clears the model's data and triggers theonunload
event.model.persist()
: Manually triggers persisters without modifying the dataset.
This comprehensive API allows developers to manage components and their data with precision and flexibility.