smartlayout-elsolya
v0.1.4
Published
Framework-agnostic layout rules engine with adapters for React/Vue/Next/Nuxt/Angular/Node
Maintainers
Readme
SmartLayout-elsolya Documentation
Overview
SmartLayout-elsolya is a Rules Engine library for dynamically selecting Layouts based on different conditions such as path, roles, permissions, and flags.
The library is designed to be Framework-agnostic - one Core that works with any environment, with ready-made Adapters for:
- React
- Next.js
- Vue 3
- Nuxt 3
- Angular
- Node.js
Installation
npm install smartlayout-elsolyaCore Concepts
1. Layout Engine
The core engine that contains the rules and uses them to determine the appropriate Layout based on the context.
2. Layout Context
The object that contains current information such as:
path: Current pathroles: Current user rolespermissions: Permissionsflags: Custom flagsmeta: Additional dataparams,query: URL parametersuser: User data
3. Rules
Each rule contains:
when: Conditions that must be metuse: Layout name or a function that returns a Layout namepriority: Priority (optional) - Rules with higher priority are evaluated first
Core API (Framework-agnostic)
Creating a Layout Engine
import { createLayoutEngine } from "smartlayout-elsolya";
const engine = createLayoutEngine({
defaultLayout: "default", // Default Layout
rules: [
// Rule: use "admin" layout for paths starting with /admin/ and users with admin role
{
when: {
path: "/admin/**",
roles: ["admin"]
},
use: "admin"
},
// Rule: use "auth" layout for all paths starting with /auth/
{
when: {
path: "/auth/**"
},
use: "auth"
},
// Rule: use a dynamic function to determine the Layout
{
when: {
path: "/dashboard/**"
},
use: (ctx) => {
// If there's a flag named beta with value true, use dashboard-beta
return ctx.flags?.beta ? "dashboard-beta" : "dashboard";
}
},
],
});Using the Engine
// Determine the Layout based on context
const layoutName = engine.resolve({
path: "/admin/users",
roles: ["admin"]
});
// Result: "admin"
const layoutName2 = engine.resolve({
path: "/auth/login"
});
// Result: "auth"Managing Rules Dynamically
// Add new rules
engine.addRules(
{ when: { path: "/blog/**" }, use: "blog" },
{ when: { path: "/shop/**" }, use: "shop" }
);
// Replace all rules
engine.setRules([
{ when: { path: "/new/**" }, use: "new-layout" }
]);
// Get all rules
const rules = engine.getRules();
// Get the default Layout
const defaultLayout = engine.getDefaultLayout();When Clause Conditions
1. Path
You can use:
- String patterns: with support for
*and**"/admin/**"- any path starting with/admin/"/user/*/profile"-/user/anything/profile"**"- any path
- RegExp: Regular expression pattern
- Array: Multiple patterns
{
when: {
path: "/admin/**" // or ["/admin/**", "/dashboard/**"] or /^\/admin/
},
use: "admin"
}2. Roles
{
when: {
roles: ["admin", "moderator"] // User must have one of these roles
},
use: "admin"
}3. Permissions
{
when: {
permissions: ["read:users", "write:users"] // Must have all these permissions
},
use: "admin"
}4. Flags
{
when: {
flags: {
beta: true,
premium: true
}
},
use: "premium-layout"
}5. Meta Data
{
when: {
meta: {
requiresAuth: true,
layout: "custom"
}
},
use: "custom-layout"
}Combining Conditions
You can combine all conditions together:
{
when: {
path: "/admin/**",
roles: ["admin"],
permissions: ["manage:users"],
flags: { beta: true }
},
use: "admin-beta"
}Priority
Rules with higher priority are evaluated first:
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{
when: { path: "/admin/**" },
use: "admin",
priority: 10 // High priority
},
{
when: { path: "/**" }, // Matches any path
use: "default",
priority: 1 // Low priority
}
]
});React
Import
import { LayoutProvider, LayoutRenderer, useLayout } from "smartlayout-elsolya/react";
import { createLayoutEngine } from "smartlayout-elsolya";Complete Example
import React from "react";
import { LayoutProvider, LayoutRenderer } from "smartlayout-elsolya/react";
import { createLayoutEngine } from "smartlayout-elsolya";
// Create the Engine
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{ when: { path: "/auth/**" }, use: "auth" },
{ when: { path: "/admin/**", roles: ["admin"] }, use: "admin" },
{ when: { path: "/dashboard/**" }, use: "dashboard" },
],
});
// Define the Layouts
const layouts = {
default: ({ children }: { children: React.ReactNode }) => (
<div className="default-layout">
<header>Default Header</header>
<main>{children}</main>
<footer>Default Footer</footer>
</div>
),
auth: ({ children }: { children: React.ReactNode }) => (
<div className="auth-layout">
<div className="auth-container">{children}</div>
</div>
),
admin: ({ children }: { children: React.ReactNode }) => (
<div className="admin-layout">
<nav>Admin Navigation</nav>
<main>{children}</main>
</div>
),
dashboard: ({ children }: { children: React.ReactNode }) => (
<div className="dashboard-layout">
<aside>Sidebar</aside>
<main>{children}</main>
</div>
),
};
// Function to get context
function getContext() {
return {
path: window.location.pathname,
roles: ["admin"], // Can be fetched from state management
flags: { beta: false },
};
}
// Main component
export function App() {
return (
<LayoutProvider engine={engine} getContext={getContext}>
<LayoutRenderer layouts={layouts}>
<div>Page Content</div>
</LayoutRenderer>
</LayoutProvider>
);
}Using the Hook
import { useLayout } from "smartlayout-elsolya/react";
function MyComponent() {
const { layout, ctx } = useLayout();
return (
<div>
<p>Current Layout: {layout}</p>
<p>Current Path: {ctx.path}</p>
</div>
);
}With React Router
import { useLocation } from "react-router-dom";
function getContext() {
const location = useLocation();
return {
path: location.pathname,
roles: getCurrentUserRoles(), // Your custom function
};
}Next.js
Import
import { resolveLayoutForNext, useNextLayout } from "smartlayout-elsolya/next";
import { createLayoutEngine } from "smartlayout-elsolya";Usage in _app.tsx or app/layout.tsx
// app/layout.tsx (App Router)
import { createLayoutEngine } from "smartlayout-elsolya";
import { resolveLayoutForNext } from "smartlayout-elsolya/next";
import { headers } from "next/headers";
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{ when: { path: "/admin/**" }, use: "admin" },
{ when: { path: "/auth/**" }, use: "auth" },
],
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
const headersList = headers();
const pathname = headersList.get("x-pathname") || "/";
const layoutName = resolveLayoutForNext(engine, {
path: pathname,
roles: getRolesFromCookies(), // Your custom function
});
const Layout = layouts[layoutName] || layouts.default;
return <Layout>{children}</Layout>;
}Using Hook in Client Components
"use client";
import { useNextLayout } from "smartlayout-elsolya/next";
import { usePathname } from "next/navigation";
export function ClientLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { layout } = useNextLayout(engine, () => ({
path: pathname,
}));
const Layout = layouts[layout] || layouts.default;
return <Layout>{children}</Layout>;
}Vue 3
Import
import { createSmartLayoutVuePlugin, useSmartLayout } from "smartlayout-elsolya/vue";
import { createLayoutEngine } from "smartlayout-elsolya";Plugin Setup
// main.ts
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createSmartLayoutVuePlugin } from "smartlayout-elsolya/vue";
import { createLayoutEngine } from "smartlayout-elsolya";
import App from "./App.vue";
const router = createRouter({
history: createWebHistory(),
routes: [
// routes...
],
});
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{ when: { path: "/admin/**" }, use: "admin" },
{ when: { path: "/auth/**" }, use: "auth" },
],
});
const app = createApp(App);
app.use(router);
app.use(
createSmartLayoutVuePlugin({
engine,
router,
getContext: (to) => ({
path: to.path,
roles: getCurrentUserRoles(), // Your custom function
}),
})
);
app.mount("#app");Usage in Components
<template>
<component :is="currentLayout">
<slot />
</component>
</template>
<script setup lang="ts">
import { useSmartLayout } from "smartlayout-elsolya/vue";
import { computed } from "vue";
const { layout } = useSmartLayout();
const layouts = {
default: () => import("./layouts/DefaultLayout.vue"),
admin: () => import("./layouts/AdminLayout.vue"),
auth: () => import("./layouts/AuthLayout.vue"),
};
const currentLayout = computed(() => layouts[layout.value] || layouts.default);
</script>Nuxt 3
Import
import { installNuxtSmartLayout } from "smartlayout-elsolya/nuxt";
import { createLayoutEngine } from "smartlayout-elsolya";Plugin Setup
// plugins/smartlayout.client.ts
import { installNuxtSmartLayout } from "smartlayout-elsolya/nuxt";
import { createLayoutEngine } from "smartlayout-elsolya";
export default defineNuxtPlugin((nuxtApp) => {
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{ when: { path: "/admin/**" }, use: "admin" },
{ when: { path: "/auth/**" }, use: "auth" },
],
});
installNuxtSmartLayout({
nuxtApp,
engine,
setPageLayout: (name) => {
setPageLayout(name); // Nuxt function
},
getContext: () => ({
path: useRoute().path,
roles: getCurrentUserRoles(), // Your custom function
}),
});
});Usage in Pages
<!-- pages/admin/users.vue -->
<template>
<div>Admin Users Page</div>
</template>
<script setup>
// Layout will be applied automatically based on rules
</script>Angular
Import
import { SmartLayoutService, SmartLayoutModule } from "smartlayout-elsolya/angular";
import { createLayoutEngine } from "smartlayout-elsolya";Service Setup
// app.component.ts
import { Component, OnInit } from "@angular/core";
import { Router, NavigationEnd } from "@angular/router";
import { SmartLayoutService } from "smartlayout-elsolya/angular";
import { createLayoutEngine } from "smartlayout-elsolya";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
constructor(
private layoutService: SmartLayoutService,
private router: Router
) {
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{ when: { path: "/admin/**" }, use: "admin" },
{ when: { path: "/auth/**" }, use: "auth" },
],
});
this.layoutService.init(engine);
}
ngOnInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.layoutService.setContext({
path: event.url,
roles: this.getCurrentUserRoles(), // Your custom function
});
}
});
}
}Usage in Template
<!-- app.component.html -->
<ng-container [ngSwitch]="layout$ | async">
<app-default-layout *ngSwitchCase="'default'">
<router-outlet></router-outlet>
</app-default-layout>
<app-admin-layout *ngSwitchCase="'admin'">
<router-outlet></router-outlet>
</app-admin-layout>
<app-auth-layout *ngSwitchCase="'auth'">
<router-outlet></router-outlet>
</app-auth-layout>
</ng-container>// app.component.ts
export class AppComponent {
layout$ = this.layoutService.layout$;
constructor(private layoutService: SmartLayoutService) {}
}Node.js (Express/Connect)
Import
import { createNodeMiddleware } from "smartlayout-elsolya/node";
import { createLayoutEngine } from "smartlayout-elsolya";Middleware Usage
import express from "express";
import { createNodeMiddleware } from "smartlayout-elsolya/node";
import { createLayoutEngine } from "smartlayout-elsolya";
const app = express();
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{ when: { path: "/admin/**" }, use: "admin" },
{ when: { path: "/api/**" }, use: "api" },
],
});
// Add Middleware
app.use(
createNodeMiddleware(engine, (req) => ({
path: req.path,
roles: req.user?.roles || [],
}))
);
// Use req.layoutName in Routes
app.get("/admin/users", (req, res) => {
const layoutName = req.layoutName; // "admin"
res.render(layoutName, { /* data */ });
});
app.listen(3000);Advanced Examples
Example 1: Dynamic Layout Based on Flags
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{
when: { path: "/dashboard/**" },
use: (ctx) => {
if (ctx.flags?.newDesign) return "dashboard-v2";
if (ctx.flags?.beta) return "dashboard-beta";
return "dashboard";
},
},
],
});Example 2: Layout Based on Permissions
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{
when: {
path: "/admin/**",
permissions: ["admin:full"],
},
use: "admin-full",
priority: 10,
},
{
when: {
path: "/admin/**",
permissions: ["admin:read"],
},
use: "admin-readonly",
priority: 5,
},
],
});Example 3: Layout Based on Meta Data
const engine = createLayoutEngine({
defaultLayout: "default",
rules: [
{
when: {
meta: { layout: "custom" },
},
use: (ctx) => ctx.meta?.layout || "default",
},
],
});Example 4: Adding Rules Dynamically
// In React Component
function AdminPanel() {
useEffect(() => {
// Add a temporary rule
engine.addRules({
when: { path: "/admin/**", flags: { debug: true } },
use: "admin-debug",
priority: 20,
});
return () => {
// Remove the rule on unmount
engine.setRules(engine.getRules().filter(/* ... */));
};
}, []);
}API Reference
createLayoutEngine(config: LayoutEngineConfig): LayoutEngine
Create a new Layout Engine.
Parameters:
config.defaultLayout: Default Layout nameconfig.rules: Array of rules (optional)
Returns: LayoutEngine object
LayoutEngine.resolve(ctx: LayoutContext): LayoutName
Determine the Layout based on context.
Parameters:
ctx: LayoutContext object
Returns: Layout name
LayoutEngine.setRules(rules: LayoutRule[]): void
Replace all rules.
LayoutEngine.addRules(...rules: LayoutRule[]): void
Add new rules.
LayoutEngine.getRules(): LayoutRule[]
Get all rules.
LayoutEngine.getDefaultLayout(): LayoutName
Get the default Layout.
TypeScript Types
LayoutContext
type LayoutContext = {
path: string;
routeName?: string;
meta?: Record<string, any>;
roles?: string[];
permissions?: string[];
flags?: Record<string, boolean>;
params?: Record<string, any>;
query?: Record<string, any>;
user?: any;
};LayoutRule
type LayoutRule = {
when: WhenClause;
use: LayoutName | ((ctx: LayoutContext) => LayoutName);
priority?: number;
};WhenClause
type WhenClause = {
path?: string | RegExp | Array<string | RegExp>;
roles?: string[];
permissions?: string[];
flags?: Record<string, boolean>;
meta?: Record<string, any>;
};Best Practices
- Use Priority wisely: More specific rules should have higher priority
- Separate rules by function: Organize rules in separate files
- Use dynamic
usefunction for complex conditions: Instead of creating many rules - Keep Layout Context updated: Make sure to update context when path or roles change
- Use TypeScript: To help catch errors early
License
MIT License - Ahmed Alsolya
Support and Contribution
For contributions or reporting issues, please open an Issue or Pull Request in the repository.
