Skip to content
Merged

Next #345

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ jobs:

steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: 7
# cache the dependencies from any node_modules directory
- name: Cache dependencies
uses: actions/cache@v2
with:
path: |
**/node_modules
**/package-lock.json
**/pnpm-lock.yaml
key: node_modules-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }}

- name: Use Node.js ${{ matrix.node-version }}
Expand All @@ -37,15 +40,15 @@ jobs:

- name: Install and link main deps
run: |
npm install
npm link
pnpm install
pnpm link --global

- name: Install deps at tests folder
working-directory: ./tests
run: |
npm link nullstack
npm install
pnpm link nullstack --global
pnpm install

- name: Run tests
working-directory: ./tests
run: npm test
run: pnpm test
4 changes: 2 additions & 2 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ client.selector = null
client.events = {}
client.generateContext = generateContext
client.renderQueue = null
client.currentBody = {}
client.nextBody = {}
client.currentMeta = { body: {}, html: {}, window: {} }
client.nextMeta = { body: {}, html: {}, window: {} }
client.currentHead = []
client.nextHead = []
client.head = document.head
Expand Down
27 changes: 20 additions & 7 deletions client/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ export const eventCallbacks = new WeakMap()
export const eventSubjects = new WeakMap()
export const eventDebouncer = new WeakMap()

function executeEvent(callback, subject, event, data) {
if (typeof callback === 'object') {
Object.assign(subject.source, callback)
} else {
callback({ ...subject, event, data })
export function generateSubject(selector, attributes, name) {
if (Array.isArray(attributes[name])) {
for (let i = 0; i < attributes[name].length; i++) {
if (typeof attributes[name][i] === 'object') {
let changeset = attributes[name][i]
attributes[name][i] = () => Object.assign(attributes.source, changeset)
}
}
} else if (typeof attributes[name] === 'object') {
let changeset = attributes[name]
attributes[name] = () => Object.assign(attributes.source, changeset)
}
eventSubjects.set(selector, attributes)
}

function debounce(selector, name, time, callback) {
Expand Down Expand Up @@ -60,17 +67,23 @@ export function generateCallback(selector, name) {
object[property] = event.target[valueName] === 'true'
} else if (typeof object[property] === 'number') {
object[property] = +event.target[valueName] || 0
} else if (object[property] instanceof Date) {
const [yyyy, mm, dd] = event.target[valueName].split('-')
object[property].setFullYear(yyyy)
object[property].setMonth(+mm - 1)
object[property].setDate(dd)
object[property] = object[property]
} else {
object[property] = event.target[valueName]
}
}
if (subject[name] === noop) return
if (Array.isArray(subject[name])) {
for (const subcallback of subject[name]) {
executeEvent(subcallback, subject, event, data)
subcallback({ ...subject, event, data })
}
} else {
executeEvent(subject[name], subject, event, data)
subject[name]({ ...subject, event, data })
}
})
}
Expand Down
4 changes: 2 additions & 2 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ export default class Nullstack {
} else {
client.virtualDom = await generateTree(client.initializer(), scope)
hydrate(client.selector, client.virtualDom)
client.currentBody = client.nextBody
client.currentMeta = client.nextMeta
client.currentHead = client.nextHead
client.nextBody = {}
client.nextMeta = { body: {}, html: {}, window: {} }
client.nextHead = []
context.environment = environment
scope.plugins = loadPlugins(scope)
Expand Down
4 changes: 2 additions & 2 deletions client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { sanitizeInnerHtml } from '../shared/sanitizeString'
import generateTruthyString from '../shared/generateTruthyString'
import { isFalse, isText } from '../shared/nodes'
import { anchorableElement } from './anchorableNode'
import { eventSubjects, generateCallback } from './events'
import { generateCallback, generateSubject } from './events'
import { ref } from './ref'

export default function render(node, options) {
Expand Down Expand Up @@ -36,7 +36,7 @@ export default function render(node, options) {
const eventName = name.substring(2)
const callback = generateCallback(node.element, name)
node.element.addEventListener(eventName, callback)
eventSubjects.set(node.element, node.attributes)
generateSubject(node.element, node.attributes, name)
}
} else {
let nodeValue
Expand Down
11 changes: 7 additions & 4 deletions client/rerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import generateTruthyString from '../shared/generateTruthyString'
import { isFalse, isText, isUndefined } from '../shared/nodes'
import { anchorableElement } from './anchorableNode'
import client from './client'
import { eventCallbacks, eventSubjects, generateCallback } from './events'
import { eventCallbacks, eventSubjects, generateCallback, generateSubject } from './events'
import { reref } from './ref'
import render from './render'

Expand Down Expand Up @@ -36,6 +36,7 @@ function updateAttributes(selector, currentAttributes, nextAttributes) {
if (!callback) {
selector.addEventListener(eventName, generateCallback(selector, name))
}
generateSubject(selector, nextAttributes, name)
eventSubjects.set(selector, nextAttributes)
}
}
Expand Down Expand Up @@ -156,12 +157,14 @@ function _rerender(current, next) {

export default function rerender() {
_rerender(client.virtualDom, client.nextVirtualDom)
updateAttributes(client.body, client.currentBody, client.nextBody)
updateAttributes(client.body, client.currentMeta.body, client.nextMeta.body)
updateAttributes(window, client.currentMeta.window, client.nextMeta.window)
updateAttributes(client.body.parentElement, client.currentMeta.html, client.nextMeta.html)
updateHeadChildren(client.currentHead, client.nextHead)
client.virtualDom = client.nextVirtualDom
client.nextVirtualDom = null
client.currentBody = client.nextBody
client.nextBody = {}
client.currentMeta = client.nextMeta
client.nextMeta = { body: {}, html: {}, window: {} }
client.currentHead = client.nextHead
client.nextHead = []
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nullstack",
"version": "0.18.0",
"description": "Full Stack Javascript Components for one-dev armies",
"version": "0.19.0",
"description": "Feature-Driven Full Stack JavaScript Components",
"main": "./types/index.d.ts",
"author": "Mortaro",
"repository": "github:nullstack/nullstack",
Expand Down Expand Up @@ -32,7 +32,7 @@
"css-loader": "6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"dotenv": "16.0.3",
"eslint-plugin-nullstack": "0.0.12",
"eslint-plugin-nullstack": "0.0.26",
"express": "4.18.2",
"fs-extra": "11.1.0",
"lightningcss": "^1.19.0",
Expand All @@ -43,7 +43,7 @@
"sass-loader": "13.2.0",
"style-loader": "^3.3.1",
"swc-loader": "0.2.3",
"swc-plugin-nullstack": "0.1.2",
"swc-plugin-nullstack": "0.1.3",
"terser-webpack-plugin": "5.3.6",
"time-analytics-webpack-plugin": "^0.1.20",
"webpack": "^5.0.0",
Expand Down
5 changes: 5 additions & 0 deletions plugins/bindable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ function transform({ node, environment }) {
node.children = [object[property] ?? '']
} else if (node.type === 'input' && node.attributes.type === 'checkbox') {
node.attributes.checked = object[property]
} else if (node.type === 'input' && node.attributes.type === 'date' && object[property] instanceof Date) {
const yyyy = object[property].getFullYear()
const mm = (object[property].getMonth() + 1).toString().padStart(2, '0')
const dd = object[property].getDate().toString().padStart(2, '0')
node.attributes.value = `${yyyy}-${mm}-${dd}`
} else {
node.attributes.value = object[property] ?? ''
}
Expand Down
4 changes: 2 additions & 2 deletions server/prerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function prerender(request, response) {
scope.body = ''
scope.context = context
scope.generateContext = generateContext(context)
scope.nextBody = {}
scope.nextMeta = { body: {}, html: {}, window: {} }
scope.nextHead = []
scope.plugins = loadPlugins(scope)

Expand All @@ -48,7 +48,7 @@ export async function prerender(request, response) {
context.page.status = 500
} finally {
if (context.page.status !== 200) {
scope.nextBody = {}
scope.nextMeta = {body: {}, html: {}, window: {}}
scope.nextHead = []
for (const key in context.router._routes) {
delete context.router._routes[key]
Expand Down
6 changes: 3 additions & 3 deletions server/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import project from './project'
import renderAttributes from './renderAttributes'
import settings from './settings'

export default function ({ head, body, nextBody, context, instances }) {
export default function ({ head, body, nextMeta, context, instances }) {
const { page, router, worker, params } = context
const canonical = absolute(page.canonical || router.url)
const image = cdnOrAbsolute(page.image)
Expand Down Expand Up @@ -39,7 +39,7 @@ export default function ({ head, body, nextBody, context, instances }) {
context: environment.mode === 'spa' ? {} : serializableContext,
}
return `<!DOCTYPE html>
<html lang="${page.locale || ''}">
<html lang="${page.locale || ''}" ${renderAttributes(nextMeta.html)}>
<head>
<meta charset="utf-8">
<meta name="generator" content="Created with Nullstack - https://nullstack.app" />
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function ({ head, body, nextBody, context, instances }) {
integrities['client.js'] || ''
}" defer crossorigin="anonymous"></script>
</head>
<body ${renderAttributes(nextBody)}>
<body ${renderAttributes(nextMeta.body)}>
${environment.mode === 'spa' ? '<div id="application"></div>' : body}
</body>
</html>`
Expand Down
35 changes: 22 additions & 13 deletions shared/generateTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,27 +105,36 @@ async function generateBranch(siblings, node, depth, scope) {
return
}

if (node.type === 'body') {
if (node.type === 'body' || node.type === 'html' || node.type === 'window') {
const tagName = node.type
node.type = fragment
for (const attribute in node.attributes) {
if (attribute === 'children' || attribute.startsWith('_')) continue
if (attribute === 'class' || attribute === 'style') {
if (!scope.nextBody[attribute]) {
scope.nextBody[attribute] = []
}
scope.nextBody[attribute].push(node.attributes[attribute])
} else if (attribute.startsWith('on')) {
if (attribute.startsWith('on')) {
if (scope.context.environment.server) continue
if (!scope.nextBody[attribute]) {
scope.nextBody[attribute] = []
if (!scope.nextMeta[tagName][attribute]) {
scope.nextMeta[tagName][attribute] = []
}
if (Array.isArray(node.attributes[attribute])) {
scope.nextBody[attribute].push(...node.attributes[attribute])
for(const callback of node.attributes[attribute]) {
if (typeof callback === 'object') {
scope.nextMeta[tagName][attribute].push(() => Object.assign(node.attributes.source, callback))
} else {
scope.nextMeta[tagName][attribute].push(callback)
}
}
} else {
scope.nextBody[attribute].push(node.attributes[attribute])
scope.nextMeta[tagName][attribute].push(node.attributes[attribute])
}
} else if (tagName !== 'window') {
if (attribute === 'class' || attribute === 'style') {
if (!scope.nextMeta[tagName][attribute]) {
scope.nextMeta[tagName][attribute] = []
}
scope.nextMeta[tagName][attribute].push(node.attributes[attribute])
} else {
scope.nextMeta[tagName][attribute] = node.attributes[attribute]
}
} else {
scope.nextBody[attribute] = node.attributes[attribute]
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion tests/src/Application.njs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Nullstack from 'nullstack'
import AnchorModifiers from './AnchorModifiers'
import ArrayAttributes from './ArrayAttributes'
import BodyFragment from './BodyFragment'
import HtmlFragment from './HtmlFragment'
import WindowFragment from './WindowFragment'
import CatchError from './CatchError'
import ChildComponent from './ChildComponent'
import ComponentTernary from './ComponentTernary'
Expand Down Expand Up @@ -60,6 +62,7 @@ import LazyComponent from './LazyComponent'
import LazyComponentLoader from './LazyComponentLoader'
import NestedFolder from './nested/NestedFolder'
import ChildComponentWithoutServerFunctions from './ChildComponentWithoutServerFunctions'
import ObjectEventScope from './ObjectEventScope'
import './Application.css'

class Application extends Nullstack {
Expand Down Expand Up @@ -140,6 +143,8 @@ class Application extends Nullstack {
<DynamicHead route="/dynamic-head" />
<TextObserver route="/text-observer" />
<BodyFragment route="/body-fragment" />
<HtmlFragment route="/html-fragment" />
<WindowFragment route="/window-fragment" />
<ArrayAttributes route="/array-attributes" />
<RouteScroll route="/route-scroll/*" key="routeScroll" />
<IsomorphicImport route="/isomorphic-import" />
Expand All @@ -150,11 +155,12 @@ class Application extends Nullstack {
<NestedFolder route="/nested/folder" />
<LazyComponent route="/lazy-importer" prop="works" />
<ChildComponentWithoutServerFunctions route="/child-component-without-server-functions" />
<ObjectEventScope route="/object-event-scope" />
<ErrorPage route="*" />
</body>
)
}

}

export default Application
export default Application
41 changes: 41 additions & 0 deletions tests/src/Head.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// deprecate project.type
// deprecate project.viewport
// deprecate page.schema

export default function Head({ router, project, page }) {
const image = page.image
const canonical = page.canonical || router.url
const favicon = project.favicon // cdn
return (
<html lang={page.locale}>
<head>
<title>{page.title} - {project.title}</title>
<meta name="description" content={page.description} />
<link rel="icon" href={favicon} type="image/png" />
<link rel="canonical" href={canonical} />
<meta name="robots" content={page.robots} />

<meta property="og:title" content={page.title} />
<meta property="og:description" content={page.description} />
<meta property="og:image" content={image} />
<meta property="og:site_name" content={project.name} />
<meta property="og:locale" content={page.locale} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />

<meta name="application-name" content={project.name} />
<link rel="shortcut icon" href={favicon} type="image/png" />
<meta name="mobile-web-app-capable" content="yes" />

<meta name="apple-mobile-web-app-title" content={project.name} />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" sizes="180x180" href={cdn(project.icons['180'])} />

<meta name="msapplication-starturl" content="/" />
<meta name="msapplication-TileColor" content={project.backgroundColor || project.color} />

<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
</head>
</html>
)
}
Loading