jops-core 1.0.15 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +356 -29
- package/bin/init.js +138 -20
- package/lib/jops-core.d.ts +251 -15
- package/lib/jops-core.min.js +1 -1
- package/lib/nextjs/JopsApp.d.ts +35 -0
- package/lib/nextjs/JopsApp.js +50 -0
- package/package.json +33 -5
- package/templates/nextjs-jops-spa/README.md +34 -0
- package/templates/nextjs-jops-spa/data/quotes.json +62 -0
- package/templates/nextjs-jops-spa/gitignore +21 -0
- package/templates/nextjs-jops-spa/next.config.ts +15 -0
- package/templates/nextjs-jops-spa/package.json +25 -0
- package/templates/nextjs-jops-spa/public/jops-spa/App.js +10 -0
- package/templates/nextjs-jops-spa/public/jops-spa/Login.js +26 -0
- package/templates/nextjs-jops-spa/public/jops-spa/Quotes.js +102 -0
- package/templates/nextjs-jops-spa/public/jops-spa/api.js +15 -0
- package/templates/nextjs-jops-spa/public/jops-spa/badge.js +8 -0
- package/templates/nextjs-jops-spa/public/jops-spa/format.js +5 -0
- package/templates/nextjs-jops-spa/public/jops-spa/layout/login.html +34 -0
- package/templates/nextjs-jops-spa/public/jops-spa/layout/main.html +17 -0
- package/templates/nextjs-jops-spa/public/jops-spa/layout/quotes.html +50 -0
- package/templates/nextjs-jops-spa/public/jops-spa/layout/quotes_row.html +8 -0
- package/templates/nextjs-jops-spa/public/jops-spa/skeleton.js +7 -0
- package/templates/nextjs-jops-spa/src/app/about/page.tsx +59 -0
- package/templates/nextjs-jops-spa/src/app/api/quotes/route.ts +11 -0
- package/templates/nextjs-jops-spa/src/app/globals.css +6 -0
- package/templates/nextjs-jops-spa/src/app/jops-spa/page.tsx +11 -0
- package/templates/nextjs-jops-spa/src/app/layout.tsx +27 -0
- package/templates/nextjs-jops-spa/src/app/page.tsx +5 -0
- package/templates/nextjs-jops-spa/tsconfig.json +21 -0
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ No Webpack. No Babel. No compiler. Just `<script type="module">` and a browser.
|
|
|
21
21
|
|
|
22
22
|
**Target audience:** Teams building internal tools, dashboards, embedded WebViews, or native mobile hybrid apps who want a structured, class-based frontend architecture without the npm/webpack/babel ecosystem overhead.
|
|
23
23
|
|
|
24
|
+
**Next.js teams:** a JOPS SPA can also run inside a Next.js page, next to React, with Next.js as its backend. See [Next.js Integration](#nextjs-integration).
|
|
25
|
+
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
## JOPS Demos
|
|
@@ -52,7 +54,14 @@ The import map lets your app modules use `import { View } from "jops-core"`. The
|
|
|
52
54
|
|
|
53
55
|
`App.js` is your entry point — a file you provide that instantiates your root `View` and mounts it to the page. Point `data-app` to wherever you place it.
|
|
54
56
|
|
|
55
|
-
Replace `@latest` with a specific version (e.g. `@1.
|
|
57
|
+
Replace `@latest` with a specific version (e.g. `@1.1.1`) to pin. No build step, no npm, no bundler.
|
|
58
|
+
|
|
59
|
+
By default the app renders into `<body>`. To render it into an element instead (keeping the rest of the page), add `data-root` with the element's id:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<div id="app"></div>
|
|
63
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js" data-app="./src/App.js" data-root="app"></script>
|
|
64
|
+
```
|
|
56
65
|
|
|
57
66
|
> **Note:** ES modules require an HTTP server — open your project with `npx serve` (or any static server) rather than double-clicking `index.html`. Browsers block `import` on `file://` URLs.
|
|
58
67
|
|
|
@@ -74,7 +83,14 @@ npm install jops-core
|
|
|
74
83
|
npx jops-core init
|
|
75
84
|
```
|
|
76
85
|
|
|
77
|
-
`npx jops-core init` vendors the library to `/lib/jops-core.min.js`, configures `index.html` with the import map, and injects build and postinstall scripts into `package.json`.
|
|
86
|
+
`npx jops-core init` vendors the library to `/lib/jops-core.min.js`, configures `index.html` with the import map, and injects build and postinstall scripts into `package.json`. It first asks which template to start from:
|
|
87
|
+
|
|
88
|
+
| Choice | Template |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| 1 | Empty JOPS project: the basic skeleton of a JOPS app |
|
|
91
|
+
| 2 | Hello World JOPS project: an example app using every feature |
|
|
92
|
+
| 3 | Next.js JOPS Embedded SPA: a Next.js project hosting a JOPS SPA (see [Next.js Integration](#nextjs-integration)) |
|
|
93
|
+
| 4 | None: only the library, `index.html` and scripts |
|
|
78
94
|
|
|
79
95
|
### Updating the library
|
|
80
96
|
|
|
@@ -277,6 +293,141 @@ A `cordova_plugins.js` "not found" error in the console is expected when the pro
|
|
|
277
293
|
|
|
278
294
|
---
|
|
279
295
|
|
|
296
|
+
## Next.js Integration
|
|
297
|
+
|
|
298
|
+
A JOPS SPA can run inside a page of a Next.js (App Router) app. Next.js keeps what it does best (server rendering, data, Route Handlers, its own routing and React ecosystem) and the JOPS app adds class-based views with an Android-style lifecycle on the pages that need them. Both render on the same page:
|
|
299
|
+
|
|
300
|
+
- **Next.js owns the path** (`/quotes`), **JOPS owns the hash** (`/quotes#details?id=q2`).
|
|
301
|
+
- The JOPS app's files live in `public/` and are served as-is: they are never bundled by Turbopack or webpack.
|
|
302
|
+
- The backend is Next.js: the JOPS app calls Route Handlers with `fetch()`.
|
|
303
|
+
|
|
304
|
+
### Quick start: the Next.js template
|
|
305
|
+
|
|
306
|
+
In an empty folder (or a fresh npm project):
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npx jops-core init
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
and pick **3. Next.js JOPS Embedded SPA**. `init` writes the project into the current folder, merges its dependencies and scripts into an existing `package.json` (existing entries are kept), vendors the library and runs `npm install`. Then:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
npm run dev
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Path | What it is |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| `src/app/about/page.tsx` | A React-only page (no JOPS) |
|
|
321
|
+
| `src/app/jops-spa/page.tsx` | Renders `<JopsApp app="/jops-spa/App.js" />`, which launches the JOPS SPA |
|
|
322
|
+
| `src/app/api/quotes/route.ts` | `GET /api/quotes[?status=…]`: the SPA's backend |
|
|
323
|
+
| `public/jops-spa/` | The JOPS SPA: a Router with a mock `#login` screen and a `#quotes` list with status filters |
|
|
324
|
+
| `public/lib/jops-core.min.js` | The JOPS library, vendored |
|
|
325
|
+
|
|
326
|
+
`init` refuses to run in a folder that already has a Next.js project (`next.config.*`, `tsconfig.json`, `src/app`) and changes nothing there; to add JOPS to an existing Next.js app, follow the manual setup below.
|
|
327
|
+
|
|
328
|
+
### Manual setup in an existing Next.js app
|
|
329
|
+
|
|
330
|
+
**1. Install JOPS and vendor the library** into `public/lib/` (served as `/lib/jops-core.min.js`):
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
npm i jops-core
|
|
334
|
+
npx jops-core update
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`update` detects a Next.js project (`next.config.*`) and copies the library to `public/lib/jops-core.min.js`. Run it again after every `npm i jops-core@latest`; adding `"postinstall": "jops-core update"` to `package.json` does it automatically.
|
|
338
|
+
|
|
339
|
+
**2. Put the JOPS app in `public/`**, e.g. `public/my-app/App.js`, `public/my-app/layout/main.html` and its views. `App.js` is the same entry module as in a standalone app:
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
import { View } from "jops-core";
|
|
343
|
+
|
|
344
|
+
export default class App {
|
|
345
|
+
constructor() {
|
|
346
|
+
const root = new View();
|
|
347
|
+
root.loadLayout("/my-app/layout/main.html").then(() => root.render());
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**3. Render `JopsApp` on the page** that hosts the app:
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
// src/app/my-app/page.tsx
|
|
356
|
+
import JopsApp from "jops-core/nextjs";
|
|
357
|
+
|
|
358
|
+
export default function MyAppPage() {
|
|
359
|
+
return <JopsApp app="/my-app/App.js" id="my-app-root" />;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`JopsApp` is a Client Component (`"use client"` is built in), so it can be used from a Server Component page. It renders an empty `<div>` on the server; in the browser it loads `/lib/jops-core.min.js` once per page, adds the `"jops-core"` import map to `<head>` and mounts the app into its `<div>`. You don't add an import map or script tag to the root layout.
|
|
364
|
+
|
|
365
|
+
| Prop | Description |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| `app` | **Required.** URL of the app's entry module (`/my-app/App.js`); its default export is instantiated |
|
|
368
|
+
| `id` | `id` of the root `<div>` (CSS, tests) |
|
|
369
|
+
| `className` | `className` of the root `<div>` |
|
|
370
|
+
| `lib` | URL of the jops-core library, default `/lib/jops-core.min.js`. The import map uses the same URL, so the page always has a single jops-core instance |
|
|
371
|
+
|
|
372
|
+
The root `<div>` has `position: relative`, which the Router's slide and fade animations need.
|
|
373
|
+
|
|
374
|
+
### App lifecycle inside Next.js
|
|
375
|
+
|
|
376
|
+
- **Leaving the page ends the app completely**, like closing a standalone app: a Next.js link to another page, or Back past the entry the app was launched from. The Router and every view are destroyed, the `<div>` is emptied, `Store` and `EventBus` are cleared, and the history methods JOPS wrapped are restored. Nothing of the app is left running.
|
|
377
|
+
- **Coming back launches it again** (a link, or Forward) and restores the screen from the hash.
|
|
378
|
+
- **Links to the app's own page don't relaunch it.** A Next.js `<Link href="/my-app">` while the app runs shows the default (first) route; `<Link href="/my-app#details?id=7">` shows that route. Stacked `navigate()` views are closed in both cases.
|
|
379
|
+
- Next.js history state (`history.state`) is kept in every entry JOPS writes, so Next.js Back/Forward keep working.
|
|
380
|
+
|
|
381
|
+
### Conventions
|
|
382
|
+
|
|
383
|
+
- **Root-absolute paths** for everything JOPS loads: `src="/my-app/Quotes.js"`, `layout="/my-app/layout/quotes.html"`, `loadLayout("/my-app/…")`. ES module `import` statements inside your `.js` files resolve relative to the file as usual.
|
|
384
|
+
- **Vendored library** (`public/lib/`), not the CDN: with the CDN, JOPS turns `/…` paths into page-relative ones.
|
|
385
|
+
- **Next.js owns the CSS.** `setStyle()` has no effect inside Next.js (a console message says so once). Tailwind CSS v4 also scans the files in `public/`, so Tailwind classes in JOPS layouts and views are generated by the Next.js build. Write class names as complete literal strings (`"bg-sky-50"`, never `` `bg-${color}-50` ``). Put other CSS in `globals.css` or a CSS file imported by the page, scoped under the app's root class (`.my-app table td { … }`), since it applies to the whole site.
|
|
386
|
+
- **Backend calls:** `fetch("/api/…")` to Route Handlers on the same origin; the session cookie is sent automatically. A JOPS app can't call Server Actions.
|
|
387
|
+
- **One JOPS app per page.** `Router`, `Store` and `EventBus` are page-wide singletons.
|
|
388
|
+
- **Always use `JopsApp` inside a Next.js page.** Don't add the jops-core `<script>` or an import map to a page or layout yourself: React doesn't run scripts it renders on soft navigation, and nothing would end the app when the page is left.
|
|
389
|
+
|
|
390
|
+
### Deploying
|
|
391
|
+
|
|
392
|
+
The result is a standard Next.js app: `npm run build` produces `.next/`, and it runs on a Node.js server when it uses server features such as Route Handlers. Deploy it like any Next.js app (see the [Next.js deployment docs](https://nextjs.org/docs/app/getting-started/deploying)). Points specific to JOPS:
|
|
393
|
+
|
|
394
|
+
- **Deploy `public/` with the app**: the whole JOPS app and the library live there. With `output: "standalone"`, copy `public/` (and `.next/static/`) into `.next/standalone/` yourself.
|
|
395
|
+
- **Caching:** files in `public/` are not content-hashed, so a CDN or the browser may keep serving an old `jops-core.min.js` or view after a deploy. Use short cache headers for those paths, or versioned paths.
|
|
396
|
+
- **Library version:** run `npx jops-core update` after updating the package and before `npm run build`.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Embedding JOPS in Other Pages (Hosted Mode)
|
|
401
|
+
|
|
402
|
+
A JOPS app can run inside any page that is not a React component: a plain HTML page, or a page rendered by another server framework (PHP, ASP.NET, Rails, Django, …). This is the mechanism `JopsApp` uses in Next.js; **inside a Next.js page, use `JopsApp`** instead (see [Next.js Integration](#nextjs-integration)).
|
|
403
|
+
|
|
404
|
+
Load the library with `data-host` and no `data-app`: nothing starts automatically, and `window.JOPS` exposes `mount()` and `unmount()` (a `jops:ready` event fires when it is available). The import map and the script `src` must be the same URL, so the page has a single jops-core instance.
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<script type="importmap">
|
|
408
|
+
{ "imports": { "jops-core": "https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js" } }
|
|
409
|
+
</script>
|
|
410
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/jops-core@latest/lib/jops-core.min.js" data-host></script>
|
|
411
|
+
|
|
412
|
+
<div id="app" style="position: relative"></div>
|
|
413
|
+
<script type="module">
|
|
414
|
+
const start = () => window.JOPS.mount({ app: "./my-app/App.js", root: document.getElementById("app") });
|
|
415
|
+
window.JOPS ? start() : window.addEventListener("jops:ready", start, { once: true });
|
|
416
|
+
</script>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
| Function | Description |
|
|
420
|
+
| --- | --- |
|
|
421
|
+
| `mount({ app, root })` | Imports `app` and creates its default export; the root view renders into `root` (an `HTMLElement`) instead of `<body>`. Returns a promise of the App instance |
|
|
422
|
+
| `unmount()` | Ends the app completely: destroys the Router and every view, empties `root`, clears `Store` and `EventBus`, and restores the history methods JOPS wrapped. Safe to call more than once; `mount()` can start an app again afterwards |
|
|
423
|
+
|
|
424
|
+
- **Paths depend on where the library comes from.** With the library from the CDN (as above), JOPS resolves paths relative to the **hosting page**: `./my-app/App.js` on `example.com/admin/users` means `example.com/admin/my-app/App.js`, and root-absolute `/my-app/...` paths are turned page-relative too. With a vendored library on the same origin (`/lib/jops-core.min.js` in both the import map and `src`), write root-absolute paths (`/my-app/App.js`, `src="/my-app/Users.js"`), which work on every page.
|
|
425
|
+
- **Ending the app:** a full page load ends it anyway. A host that removes the app's element without a page load, or navigates with `history.pushState()`, calls `unmount()`; when history moves to another path the Router also ends the app by itself.
|
|
426
|
+
- `mount` and `unmount` are also exported by the module (`import { mount, unmount } from "jops-core"`) for modules running on the same page.
|
|
427
|
+
- In hosted mode `setStyle()` has no effect (the host page provides the CSS) and the Android back button integration is off.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
280
431
|
## Class Reference
|
|
281
432
|
|
|
282
433
|
### `View` — Base Class
|
|
@@ -289,30 +440,106 @@ import { View } from "jops-core";
|
|
|
289
440
|
export default class MyView extends View { ... }
|
|
290
441
|
```
|
|
291
442
|
|
|
443
|
+
#### Constructor
|
|
444
|
+
|
|
445
|
+
| Constructor | Description |
|
|
446
|
+
| --------------------------- | ----------- |
|
|
447
|
+
| `new View(layout?, props?)` | `layout` (optional) is passed to `loadLayout(layout)` when the view is created: a `.js` layout module path, an `.html` file path, or an inline HTML string. An inline string is assigned to `this.layout` immediately. `props` (optional) sets the view's [input props](#tag-props-input-properties) for a view created in code. |
|
|
448
|
+
|
|
449
|
+
Usually the layout comes from the view's `layout=` attribute or from an overridden `loadLayout()`, so subclasses call `super()` with no argument. Pass an inline layout when a view builds its markup from data, for example one row per record:
|
|
450
|
+
|
|
451
|
+
```js
|
|
452
|
+
import { View, layout } from "jops-core";
|
|
453
|
+
|
|
454
|
+
export default class QuoteRow extends View {
|
|
455
|
+
constructor(quote) {
|
|
456
|
+
super(layout`<tr><td>${quote.number}</td><td>${quote.companyName}</td></tr>`);
|
|
457
|
+
this.quote = quote;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// in the parent view:
|
|
462
|
+
await this.addSubView("rows", new QuoteRow(quote));
|
|
463
|
+
```
|
|
464
|
+
|
|
292
465
|
#### Lifecycle (called by the framework in order)
|
|
293
466
|
|
|
294
467
|
| Method | When called |
|
|
295
468
|
| ------------------------- | ------------------------------------------------------------------------------ |
|
|
296
|
-
| `async loadLayout(path?)` | Override to load a layout `.js` module or `.html` file
|
|
469
|
+
| `async loadLayout(path?)` | Override to load a layout `.js` module or `.html` file. When you pass a path, `await` (or `return`) `super.loadLayout(…)` — see [Overriding `loadLayout()`](#overriding-loadlayout). Loads once per view |
|
|
297
470
|
| `inflate()` | Converts `this.layout` string into `this.domNode` |
|
|
298
471
|
| `async onLayout()` | DOM is live — add subviews, read initial data |
|
|
299
|
-
| `async onResume()` | View
|
|
472
|
+
| `async onResume()` | View comes to the front — on first display, on every return navigation, and when the same route is shown again with new parameters. A Router route is resumed only when it is shown |
|
|
300
473
|
| `bindEvents()` | Wires `on*` attributes on `[jops-event-bind]` elements to class methods |
|
|
301
|
-
|
|
474
|
+
|
|
475
|
+
Optional hooks, called by the Router:
|
|
476
|
+
|
|
477
|
+
| Method | When called |
|
|
478
|
+
| ----------------- | ----------- |
|
|
479
|
+
| `onPause()` | A different view comes to the front and this one stays alive: another route is shown, a stacked view is opened on top of it with `navigate()`, or `replace()` shows another view. Always runs before the incoming view's `onResume()`. Not called when the view is destroyed instead (Back from a stacked view, a stack closed by a route link, leaving the app) |
|
|
480
|
+
| `onBackPressed()` | The user navigates back from this view (browser back, `Router.back()`, Android back button in hybrid apps) — never on forward navigation. For a route it runs before `onPause()`; for a stacked view it is the last call before the view is destroyed, so put closing logic here |
|
|
481
|
+
|
|
482
|
+
`onPause()` and `onResume()` are not passed on to child views. The root view (the one whose `render()` is called first) gets the full lifecycle too, after its declared children.
|
|
302
483
|
|
|
303
484
|
#### Instance Methods
|
|
304
485
|
|
|
305
486
|
| Method | Description |
|
|
306
487
|
| ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
307
|
-
| `get(id)` | Returns an `Element` wrapper for the
|
|
308
|
-
| `
|
|
488
|
+
| `get(id)` | Returns an `Element` wrapper for the first element with the given `id`, searching every top-level element of the layout |
|
|
489
|
+
| `find(id)` | Returns an array of `Element` wrappers for every element with the given `id` across the layout (sub-views' DOM included), in page order |
|
|
490
|
+
| `find(condition)` | Depth-first search across full subview subtree; returns flat array of all views matching the predicate |
|
|
491
|
+
| `props` | The view's read-only [input props](#tag-props-input-properties) |
|
|
492
|
+
| `setStyle(style)` | Injects a `.css` file link or inline style tag into `<head>`, deduped. No effect when the app runs inside Next.js (the host owns the CSS) |
|
|
309
493
|
| `addSubView(containerId, view)` | Appends `view` into the child element with `id="containerId"`, runs its full lifecycle |
|
|
310
494
|
| `insertSubView(view, index)` | Inserts `view` at position `index` within `this.domNode`, runs its full lifecycle |
|
|
311
495
|
| `removeSubView(view)` | Removes `view` from DOM and `this.subViews`, calls `view.destroy()` |
|
|
312
|
-
| `
|
|
313
|
-
| `
|
|
314
|
-
| `
|
|
315
|
-
|
|
496
|
+
| `show()` | Shows the view again (removes the inline `display` override) |
|
|
497
|
+
| `hide()` | Hides the view (`display: none`) |
|
|
498
|
+
| `destroy()` | Recursively destroys subtree — calls `destroy()` on all subviews, removes the view's elements from the DOM, removes its owned `EventBus` subscriptions, nulls references |
|
|
499
|
+
|
|
500
|
+
- **`show()` / `hide()` are visual only**: they don't call `onPause()` / `onResume()`. Refresh or pause a view toggled this way yourself.
|
|
501
|
+
- **A removed view is final.** `removeSubView()` destroys it; adding the same instance again logs a warning and does nothing. Keep the data outside the view (the `Store`, the parent's fields, the server) and create a new instance to show it again. `addSubView()` also warns and adds nothing when the container id doesn't exist or the layout failed to load.
|
|
502
|
+
- **The root view's container is borrowed.** The root view uses its container (`<body>`, the `data-root` element, or the element passed to `mount()`) as its `domNode`; destroying the root empties the container instead of removing it.
|
|
503
|
+
|
|
504
|
+
#### Tag props (input properties)
|
|
505
|
+
|
|
506
|
+
Attributes on a declared `jops` tag are passed to its view as **props**, like Android's `AttributeSet` or React's props:
|
|
507
|
+
|
|
508
|
+
```html
|
|
509
|
+
<Card jops src="/src/Card.js" title="Monthly report" item-id="42" variant="compact"></Card>
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```js
|
|
513
|
+
export default class Card extends View {
|
|
514
|
+
constructor() {
|
|
515
|
+
super();
|
|
516
|
+
// props are available right after super(), in loadLayout() and in onLayout()
|
|
517
|
+
this.layout = layout`<article class="card ${this.props.variant}"><h3>${this.props.title}</h3></article>`;
|
|
518
|
+
this.inflate();
|
|
519
|
+
}
|
|
520
|
+
onLayout() {
|
|
521
|
+
console.log(this.props.itemId); // "42"
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
- Names are converted to camelCase (`item-id` → `itemId`); values are always strings.
|
|
527
|
+
- `this.props` is read-only (a frozen object), and `{}` when there are none.
|
|
528
|
+
- `jops`, `type="jops"`, `src`, `layout`, `path` and `jops-event-bind` belong to JOPS and are not props. `id`, `class` and `on*` attributes are props only: they are not copied to the view's element, and an `onclick` prop is not bound.
|
|
529
|
+
- The view's constructor needs no parameters for this: props reach declared views whatever their constructor looks like.
|
|
530
|
+
- A view created in code can get props through the constructor: `new View(layout, { title: "Hi" })`, or `super(undefined, props)` in a subclass.
|
|
531
|
+
- Props come from tags in static layouts processed by `render()`: the root layout, the Router's route declarations, and the layouts of declared views. Tags inside the layout of a view added with `addSubView()`, or inside a route's own layout, are not created as views; add those children in code.
|
|
532
|
+
|
|
533
|
+
#### Layouts with several top-level elements
|
|
534
|
+
|
|
535
|
+
A layout may have one or more top-level elements:
|
|
536
|
+
|
|
537
|
+
```html
|
|
538
|
+
<h2 id="title">Quotes</h2>
|
|
539
|
+
<table><tbody id="rows"></tbody></table>
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`this.domNode` is the first one and `this.domNodes` holds all of them. `get()`, `find(id)`, `bindEvents()`, `show()`, `hide()`, `addSubView()`, `insertSubView()` and `destroy()` act on all of them, in order. Top-level text is ignored. The Router's `slide` and `fade` animations need single-root views: a stacked view with several top-level elements switches instantly.
|
|
316
543
|
|
|
317
544
|
#### Declarative Event Binding
|
|
318
545
|
|
|
@@ -326,22 +553,50 @@ Mark any element with `jops-event-bind` and add `on*` attributes whose values ar
|
|
|
326
553
|
onSave(event) { ... }
|
|
327
554
|
```
|
|
328
555
|
|
|
556
|
+
`bindEvents()` wires the markup of the view's layout when the view is shown. HTML you generate later (with `Element.set()` or `innerHTML`) is not bound — the browser would treat `onclick="onSave"` as an inline script. Wire generated markup yourself, ideally with one listener on its container (event delegation):
|
|
557
|
+
|
|
558
|
+
```js
|
|
559
|
+
// layout: <tbody id="lines" jops-event-bind onclick="onLineAction"></tbody>
|
|
560
|
+
// generated rows: <button data-action="remove" data-index="3">✕</button>
|
|
561
|
+
onLineAction(event) {
|
|
562
|
+
const button = event.target.closest("[data-action]");
|
|
563
|
+
if (button?.dataset.action === "remove") this.removeLine(Number(button.dataset.index));
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
329
567
|
---
|
|
330
568
|
|
|
331
569
|
### `Element` — DOM Accessor (returned by `View.get()`)
|
|
332
570
|
|
|
333
|
-
Returned by `view.get(id)
|
|
571
|
+
Returned by `view.get(id)` (and, as an array, by `view.find(id)`). Wraps a single DOM element for reading and writing.
|
|
572
|
+
|
|
573
|
+
| Method / property | Description |
|
|
574
|
+
| ------------------- | ----------- |
|
|
575
|
+
| `set(html)` | Sets the `innerHTML` of the element (plain text or HTML). Returns the wrapper, for chaining. **Not escaped**: pass values from users or the network through `layout` |
|
|
576
|
+
| `attr(name, value)` | Sets an attribute; `null` or `undefined` removes it, `""` sets a boolean attribute (`disabled`, `hidden`, `required`). Returns the wrapper, for chaining |
|
|
577
|
+
| `value()` | Returns the current `innerHTML` of the element |
|
|
578
|
+
| `element` | The underlying DOM element, for anything else (`classList`, `textContent`, properties). `null` when no element has that id |
|
|
579
|
+
| `Element.from(element, id)` | Static. Wraps an element you already have |
|
|
334
580
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
| `value()` | Returns the current `innerHTML` of the element |
|
|
581
|
+
`set()`, `attr()` and `value()` never throw. When no element has the id, they do nothing (`value()` returns `""`) and log a warning with the id — usually a typo. On a destroyed view (for example after an async request finished when the user had already left), they do nothing silently. Code that uses `element` or `this.domNode` directly has no such guard.
|
|
582
|
+
|
|
583
|
+
> `set()` replaces plain markup. Don't use it to clear a container that holds sub-views: call `removeSubView()` for each of them first, otherwise they stay in the parent's `subViews`.
|
|
339
584
|
|
|
340
585
|
```js
|
|
341
586
|
this.get("username").set("Alice");
|
|
342
587
|
const current = this.get("username").value();
|
|
588
|
+
|
|
589
|
+
// chaining, with escaped data from the API
|
|
590
|
+
this.get("quote_id")
|
|
591
|
+
.set(layout`${quote.number}`)
|
|
592
|
+
.attr("href", `#quote?id=${quote.id}`);
|
|
593
|
+
|
|
594
|
+
this.get("save").attr("disabled", ""); // disable
|
|
595
|
+
this.get("save").attr("disabled", null); // enable again
|
|
343
596
|
```
|
|
344
597
|
|
|
598
|
+
> `attr()` can't inject HTML, but URL attributes (`href`, `src`) still accept `javascript:` URLs. Don't put untrusted values into them unchecked.
|
|
599
|
+
|
|
345
600
|
---
|
|
346
601
|
|
|
347
602
|
### `layout` — Tagged Template Function
|
|
@@ -372,6 +627,33 @@ await root.loadLayout("/src/layout/mainlayout.html");
|
|
|
372
627
|
root.render();
|
|
373
628
|
```
|
|
374
629
|
|
|
630
|
+
#### Overriding `loadLayout()`
|
|
631
|
+
|
|
632
|
+
When a view class overrides `loadLayout()` and passes a **path**, it must `await` (or `return`) `super.loadLayout(…)`:
|
|
633
|
+
|
|
634
|
+
```js
|
|
635
|
+
class QuoteRow extends View {
|
|
636
|
+
loadLayout() {
|
|
637
|
+
return super.loadLayout("/quotes/layout/quote_row.html"); // ✓ returns the promise
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Equivalent; handy when the override does more after loading:
|
|
642
|
+
class QuoteRow extends View {
|
|
643
|
+
async loadLayout() {
|
|
644
|
+
await super.loadLayout("/quotes/layout/quote_row.html"); // ✓ waits, then resolves
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
The framework waits for the promise returned by `loadLayout()` before calling `inflate()`. Loading a path is asynchronous (`fetch()` for `.html`, a dynamic `import()` for `.js`), so without `await` the layout arrives too late and the view renders empty.
|
|
650
|
+
|
|
651
|
+
| Passed to `super.loadLayout()` | Loading | `await` needed |
|
|
652
|
+
| --- | --- | --- |
|
|
653
|
+
| An `.html` path | `fetch()` | **yes** |
|
|
654
|
+
| A `.js` path | dynamic `import()` | **yes** |
|
|
655
|
+
| A string (inline HTML, or a layout module imported at the top of the file) | synchronous | no |
|
|
656
|
+
|
|
375
657
|
`mainlayout.html` is a plain HTML fragment:
|
|
376
658
|
|
|
377
659
|
```html
|
|
@@ -388,6 +670,8 @@ root.render();
|
|
|
388
670
|
|
|
389
671
|
> **Note:** `.html` layouts are fetched at runtime and cached in memory — identical dedup behaviour to the browser's module cache for `.js` layouts. Each unique path is fetched once per page load.
|
|
390
672
|
|
|
673
|
+
`loadLayout()` loads **once per view**: calls made while a load is in progress (with or without a path) wait for it, and once a layout is loaded, later calls return at once. A failed load (for example a 404) leaves the view without a layout, so it can be retried. This also makes `new View("/path/to/layout.html")` followed by `addSubView()` work: `addSubView()` waits for the layout the constructor started loading.
|
|
674
|
+
|
|
391
675
|
---
|
|
392
676
|
|
|
393
677
|
### `raw(html)` — Function
|
|
@@ -427,9 +711,24 @@ Hash-based SPA router. Singleton. Declared in the layout as `<Router jops animat
|
|
|
427
711
|
| `navigate(path, ClassDef?)` | Creates a new view instance for `path`, pushes onto the nav stack, animates forward. Updates address bar to `#path?params`. `ClassDef` required for dynamic routes not in the static route map |
|
|
428
712
|
| `back()` | Navigates back — pops the nav stack with animation if non-empty, otherwise calls `history.back()` |
|
|
429
713
|
| `canGoBack()` | Returns `true` if there is an earlier JOPS history entry to go back to (like checking the back stack in Android) |
|
|
430
|
-
| `replace(path, ClassDef?)` | Clears the nav stack and navigates to `path`. Uses existing flat route instance if available, otherwise creates a new one from `ClassDef`
|
|
714
|
+
| `replace(path, ClassDef?)` | Clears the nav stack and navigates to `path`. Uses existing flat route instance if available, otherwise creates a new one from `ClassDef`. Replaces the current history entry and keeps its existing `history.state` |
|
|
431
715
|
| `getParams()` | Returns the current URL query params as a plain object. Works for both static (`href="#path?k=v"`) and dynamic (`navigate("/path?k=v")`) routes |
|
|
432
716
|
|
|
717
|
+
#### Navigation and the lifecycle
|
|
718
|
+
|
|
719
|
+
| Navigation | Outgoing view | Incoming view |
|
|
720
|
+
| --- | --- | --- |
|
|
721
|
+
| Route link (`href="#users"`), Forward to a route | `onPause()` | `onResume()` |
|
|
722
|
+
| Back to the previous route | `onBackPressed()`, `onPause()` | `onResume()` |
|
|
723
|
+
| Same route with new params (`#details?id=1` → `#details?id=2`) | — | `onResume()` |
|
|
724
|
+
| `navigate()` (stacked view) | `onPause()` | new instance: `onLayout()` → `onResume()` |
|
|
725
|
+
| Back from a stacked view | `onBackPressed()`, then destroyed | `onResume()` |
|
|
726
|
+
| Route link while stacked views are open | stacked views destroyed (no `onPause()`) | `onResume()` |
|
|
727
|
+
|
|
728
|
+
- The first route declared in `<Router>` is the default: it is shown on startup when the URL has no known route hash, and whenever the hash becomes empty (`href="#"`, a link to the page itself).
|
|
729
|
+
- Each route is resumed only when it is shown; at startup only the selected route gets `onResume()`. Every route gets `onLayout()` at startup.
|
|
730
|
+
- **Screen rule:** put screen views in `<Router>`. A route's own layout is not scanned for declared views, so a screen adds its child views in code with `addSubView()`.
|
|
731
|
+
|
|
433
732
|
#### Animation
|
|
434
733
|
|
|
435
734
|
Set via the `animation` attribute on the `<Router>` element:
|
|
@@ -440,7 +739,7 @@ Set via the `animation` attribute on the `<Router>` element:
|
|
|
440
739
|
| `"slide"` | Forward: incoming slides in from right. Back: outgoing slides out to right |
|
|
441
740
|
| `"fade"` | Crossfade between outgoing and incoming |
|
|
442
741
|
|
|
443
|
-
Animation only occurs on `navigate()` (forward) and `back()` when the nav stack is non-empty. Flat tab switching is always instant.
|
|
742
|
+
Animation only occurs on `navigate()` (forward) and `back()` when the nav stack is non-empty. Flat tab switching is always instant, and so is a transition involving a view whose layout has several top-level elements.
|
|
444
743
|
|
|
445
744
|
#### Layout Declaration
|
|
446
745
|
|
|
@@ -498,18 +797,46 @@ Store.singleton().clear();
|
|
|
498
797
|
|
|
499
798
|
Stateless cross-view event broadcasting. All methods are static — no instance needed.
|
|
500
799
|
|
|
501
|
-
| Method
|
|
502
|
-
|
|
|
503
|
-
| `EventBus.subscribe(event, callback)` | Registers a listener
|
|
504
|
-
| `EventBus.
|
|
505
|
-
| `EventBus.
|
|
800
|
+
| Method | Description |
|
|
801
|
+
| ---------------------------------------------- | ---------------------------------------------------------------------- |
|
|
802
|
+
| `EventBus.subscribe(owner, event, callback)` | Registers a listener owned by `owner` (usually a view, `this`). It is removed automatically when the owner is destroyed |
|
|
803
|
+
| `EventBus.subscribe(event, callback)` | Registers a global listener. It stays until you unsubscribe it |
|
|
804
|
+
| `EventBus.send(event, data?)` | Fires the named event to all subscribers with an optional data payload |
|
|
805
|
+
| `EventBus.unsubscribe(event, callback)` | Removes a specific listener (owned or global) by reference |
|
|
806
|
+
| `EventBus.unsubscribeAll(owner)` | Removes every subscription of `owner` (called by `View.destroy()`) |
|
|
807
|
+
| `EventBus.clear()` | Removes every listener of every event |
|
|
506
808
|
|
|
507
809
|
```js
|
|
508
|
-
import { EventBus } from "jops-core";
|
|
810
|
+
import { View, EventBus } from "jops-core";
|
|
811
|
+
|
|
812
|
+
class Header extends View {
|
|
813
|
+
onLayout() {
|
|
814
|
+
// owned: removed when this view is destroyed (removeSubView, Router, unmount)
|
|
815
|
+
EventBus.subscribe(this, "user:login", (user) => this.showUser(user));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
509
818
|
|
|
510
|
-
EventBus.subscribe("user:login", (data) => console.log(data.name));
|
|
511
819
|
EventBus.send("user:login", { name: "Alice" });
|
|
512
|
-
|
|
820
|
+
|
|
821
|
+
// global: keep the function reference to be able to unsubscribe it
|
|
822
|
+
const onLogin = (user) => console.log(user.name);
|
|
823
|
+
EventBus.subscribe("user:login", onLogin);
|
|
824
|
+
EventBus.unsubscribe("user:login", onLogin);
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Prefer the owned form inside views. A global subscription made by a view keeps that view in memory and keeps calling it after it is destroyed, until you unsubscribe it (in an overridden `destroy()` that calls `super.destroy()`, or in `onPause()` with a new subscription in `onResume()`).
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
### `mount()` / `unmount()` — Functions
|
|
832
|
+
|
|
833
|
+
Start and end an app in a host page (hosted mode). Used by `JopsApp` in Next.js; see [Embedding JOPS in Other Pages](#embedding-jops-in-other-pages-hosted-mode). The page must load jops-core with `data-host` (otherwise it starts `data-app` / `./src/App.js` by itself); `window.JOPS.mount` / `window.JOPS.unmount` are the same functions.
|
|
834
|
+
|
|
835
|
+
```js
|
|
836
|
+
import { mount, unmount } from "jops-core";
|
|
837
|
+
|
|
838
|
+
await mount({ app: "/my-app/App.js", root: document.getElementById("app") });
|
|
839
|
+
unmount(); // destroys the Router and all views, empties the root, clears Store and EventBus
|
|
513
840
|
```
|
|
514
841
|
|
|
515
842
|
---
|
|
@@ -554,8 +881,8 @@ t.terminate();
|
|
|
554
881
|
|
|
555
882
|
| Command | Description |
|
|
556
883
|
| ---------------------- | -------------------------------------------------------------------------- |
|
|
557
|
-
| `npx jops-core init` | Vendor library, configure `index.html`, inject scripts into `package.json`. Detects Cordova and Capacitor projects (see [Hybrid Mobile Apps](#hybrid-mobile-apps-cordova-ionic--capacitor)) |
|
|
558
|
-
| `npx jops-core update` | Copy the latest `jops-core.min.js` from `node_modules` to `lib/`
|
|
884
|
+
| `npx jops-core init` | Vendor library, configure `index.html`, inject scripts into `package.json`. Detects Cordova and Capacitor projects (see [Hybrid Mobile Apps](#hybrid-mobile-apps-cordova-ionic--capacitor)). Template 3 creates a Next.js project instead (see [Next.js Integration](#nextjs-integration)) |
|
|
885
|
+
| `npx jops-core update` | Copy the latest `jops-core.min.js` from `node_modules` to `lib/` (`public/lib/` in a Next.js project) |
|
|
559
886
|
| `npx jops-core build` | Generate production-ready `dist/` folder (skipped in Cordova/Capacitor projects) |
|
|
560
887
|
|
|
561
888
|
`init` and `update` accept `--cordova`, `--capacitor` or `--web` to override the detected project type.
|
|
@@ -566,7 +893,7 @@ After each command the CLI checks npm for a newer jops-core and prints a notice
|
|
|
566
893
|
|
|
567
894
|
## Source & Contributing
|
|
568
895
|
|
|
569
|
-
JOPS is distributed as a single minified ES module (`lib/jops-core.min.js`) together with full TypeScript declarations (`lib/jops-core.d.ts`). The source repository is private.
|
|
896
|
+
JOPS is distributed as a single minified ES module (`lib/jops-core.min.js`) together with full TypeScript declarations (`lib/jops-core.d.ts`), plus the `JopsApp` React component for Next.js (`lib/nextjs/`, imported as `jops-core/nextjs`). The source repository is private.
|
|
570
897
|
|
|
571
898
|
This is a deliberate choice. JOPS is maintained by one developer at [UIDB](https://ui-db.com), and keeping the codebase in one pair of hands keeps it small, consistent, and fast to change.
|
|
572
899
|
|