From 9bf2cbeff711ea559b005c7d48ef18ce98d75d1e Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 16 May 2023 14:05:47 -0300 Subject: [PATCH 1/9] :bug: Resolve `webpack/hot/poll` import path for pnpm --- webpack/entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/entry.js b/webpack/entry.js index cfa6f024..afca360d 100644 --- a/webpack/entry.js +++ b/webpack/entry.js @@ -16,7 +16,7 @@ function server(options) { return options.entry } return [ - 'webpack/hot/poll?100', + `${require.resolve('webpack/hot/poll')}?100`, path.posix.join(options.configFolder, 'shared', 'accept.js'), options.entry ] From 2d4a19433072d28974c83179fd9975e84696b0a4 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Wed, 17 May 2023 22:17:50 -0300 Subject: [PATCH 2/9] :bookmark: version 0.18.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9e7e4ba1..af7102a2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nullstack", - "version": "0.18.0", - "description": "Full Stack Javascript Components for one-dev armies", + "version": "0.18.1", + "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", "repository": "github:nullstack/nullstack", From 96923933f2af946d8db854d47d0dc081eae7e9ff Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Sat, 20 May 2023 09:06:36 -0300 Subject: [PATCH 3/9] :construction_worker: Switch to pnpm at `pr-tests` CI --- .github/workflows/pr-tests.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 30df7a2e..75248b85 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -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 }} @@ -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 \ No newline at end of file + run: pnpm test \ No newline at end of file From 5cef82d7fe704e36e0b17be3bd503cb540f0baa9 Mon Sep 17 00:00:00 2001 From: Maycon Sousa Date: Sat, 20 May 2023 16:57:04 -0300 Subject: [PATCH 4/9] general type improvements --- types/ClientContext.d.ts | 4 ++-- types/JSX.d.ts | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/types/ClientContext.d.ts b/types/ClientContext.d.ts index f780bb89..ea3f62a6 100644 --- a/types/ClientContext.d.ts +++ b/types/ClientContext.d.ts @@ -1,5 +1,5 @@ import { NullstackEnvironment } from './Environment' -import { NullstackNode } from './JSX' +import { NullstackFragment } from './JSX' import { NullstackPage } from './Page' import { NullstackParams } from './Params' import { NullstackProject } from './Project' @@ -98,7 +98,7 @@ export type NullstackClientContext = TProps & { * * @see https://nullstack.app/renderable-components#components-with-children */ - children: NullstackNode + children: NullstackFragment /** * Bind object. diff --git a/types/JSX.d.ts b/types/JSX.d.ts index c8bcc4fd..8995bee4 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -577,10 +577,14 @@ type AriaRole = | 'treeitem' | (string & {}) +type Falsy = false | 0 | '' | null | undefined +type CssClass = string | Falsy | Array +type CssStyle = string | Falsy | Array + export interface HTMLAttributes extends AriaAttributes, DOMAttributes { // Standard HTML Attributes accesskey?: string - class?: string | string[] + class?: CssClass contenteditable?: Booleanish | 'inherit' contextmenu?: string dir?: string @@ -591,7 +595,7 @@ export interface HTMLAttributes extends AriaAttributes, DOMAttributes { placeholder?: string slot?: string spellcheck?: Booleanish - style?: string + style?: CssStyle tabindex?: number | string title?: string translate?: 'yes' | 'no' @@ -1188,7 +1192,7 @@ export interface VideoHTMLAttributes extends MediaHTMLAttributes { // - union of string literals export interface SVGAttributes extends AriaAttributes, DOMAttributes { // Attributes which also defined in HTMLAttributes - class?: string | string[] + class?: CssClass color?: string height?: number | string id?: string @@ -1198,7 +1202,7 @@ export interface SVGAttributes extends AriaAttributes, DOMAttributes { method?: string min?: number | string name?: string - style?: string + style?: CssStyle target?: string type?: string width?: number | string @@ -1426,5 +1430,9 @@ declare global { } interface IntrinsicElements extends ExoticElements, AllElements {} + + interface ElementChildrenAttribute { + children: NullstackNode + } } } From 31e2e57a9371df5c7708fa47a7a9071f027c5e5c Mon Sep 17 00:00:00 2001 From: Maycon Sousa Date: Sat, 20 May 2023 16:57:20 -0300 Subject: [PATCH 5/9] upgrade eslint-plugin-nullstack version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af7102a2..3659ca77 100644 --- a/package.json +++ b/package.json @@ -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", From 713398f0f01801b8fb349f01d899c0ac993db32f Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Sun, 21 May 2023 07:12:59 -0300 Subject: [PATCH 6/9] :sparkles: window and html --- client/client.js | 4 +- client/events.js | 21 +++++++---- client/index.js | 4 +- client/render.js | 4 +- client/rerender.js | 11 ++++-- server/prerender.js | 4 +- server/template.js | 6 +-- shared/generateTree.js | 35 ++++++++++------- tests/src/Application.njs | 8 +++- tests/src/HtmlFragment.njs | 57 ++++++++++++++++++++++++++++ tests/src/HtmlFragment.test.js | 60 ++++++++++++++++++++++++++++++ tests/src/ObjectEventScope.njs | 43 +++++++++++++++++++++ tests/src/ObjectEventScope.test.js | 38 +++++++++++++++++++ tests/src/WindowFragment.njs | 42 +++++++++++++++++++++ tests/src/WindowFragment.test.js | 32 ++++++++++++++++ 15 files changed, 333 insertions(+), 36 deletions(-) create mode 100644 tests/src/HtmlFragment.njs create mode 100644 tests/src/HtmlFragment.test.js create mode 100644 tests/src/ObjectEventScope.njs create mode 100644 tests/src/ObjectEventScope.test.js create mode 100644 tests/src/WindowFragment.njs create mode 100644 tests/src/WindowFragment.test.js diff --git a/client/client.js b/client/client.js index 18c993fa..a95b8e5b 100644 --- a/client/client.js +++ b/client/client.js @@ -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 diff --git a/client/events.js b/client/events.js index cab01448..874e0788 100644 --- a/client/events.js +++ b/client/events.js @@ -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) { @@ -67,10 +74,10 @@ export function generateCallback(selector, name) { 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 }) } }) } diff --git a/client/index.js b/client/index.js index 60660942..202868fc 100644 --- a/client/index.js +++ b/client/index.js @@ -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) diff --git a/client/render.js b/client/render.js index 14673b10..7fee3434 100644 --- a/client/render.js +++ b/client/render.js @@ -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) { @@ -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 diff --git a/client/rerender.js b/client/rerender.js index 34f93f4d..cd788f67 100644 --- a/client/rerender.js +++ b/client/rerender.js @@ -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' @@ -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) } } @@ -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 = [] } diff --git a/server/prerender.js b/server/prerender.js index b6507080..be6f8ea2 100644 --- a/server/prerender.js +++ b/server/prerender.js @@ -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) @@ -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] diff --git a/server/template.js b/server/template.js index 8d9277b7..88a954b4 100644 --- a/server/template.js +++ b/server/template.js @@ -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) @@ -39,7 +39,7 @@ export default function ({ head, body, nextBody, context, instances }) { context: environment.mode === 'spa' ? {} : serializableContext, } return ` - + @@ -75,7 +75,7 @@ export default function ({ head, body, nextBody, context, instances }) { integrities['client.js'] || '' }" defer crossorigin="anonymous"> - + ${environment.mode === 'spa' ? '
' : body} ` diff --git a/shared/generateTree.js b/shared/generateTree.js index c084133a..fae3986d 100644 --- a/shared/generateTree.js +++ b/shared/generateTree.js @@ -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] } } } diff --git a/tests/src/Application.njs b/tests/src/Application.njs index f79960d4..58691b9e 100644 --- a/tests/src/Application.njs +++ b/tests/src/Application.njs @@ -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' @@ -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 { @@ -140,6 +143,8 @@ class Application extends Nullstack { + + @@ -150,6 +155,7 @@ class Application extends Nullstack { + ) @@ -157,4 +163,4 @@ class Application extends Nullstack { } -export default Application +export default Application \ No newline at end of file diff --git a/tests/src/HtmlFragment.njs b/tests/src/HtmlFragment.njs new file mode 100644 index 00000000..e782469b --- /dev/null +++ b/tests/src/HtmlFragment.njs @@ -0,0 +1,57 @@ +import Nullstack from 'nullstack' + +class HtmlFragment extends Nullstack { + + count = 0 + visible = false + objected = false + + increment() { + this.count++ + } + + reveal() { + this.visible = !this.visible + } + + countDataKeys({ data }) { + this.hasDataKeys = Object.keys(data).length > 0 + } + + render() { + return ( +
+ + + HtmlFragment + + {this.visible && ( + + HtmlFragment2 + + )} + home + +
+ ) + } + +} + +export default HtmlFragment diff --git a/tests/src/HtmlFragment.test.js b/tests/src/HtmlFragment.test.js new file mode 100644 index 00000000..6c67128f --- /dev/null +++ b/tests/src/HtmlFragment.test.js @@ -0,0 +1,60 @@ +beforeEach(async () => { + await page.goto('http://localhost:6969/html-fragment') +}) + +describe('HtmlFragment', () => { + test('the html behaves as a fragment and creates no markup', async () => { + const element = await page.$('[data-html-parent] > [data-html-child]') + expect(element).toBeTruthy() + }) + + test('when the html is nested regular attributes are overwritten by the last one in the tree', async () => { + const element = await page.$('html[data-chars="b"]') + expect(element).toBeTruthy() + }) + + test('when the html is nested classes are merged togheter', async () => { + const element = await page.$('html[class="class-one class-two class-three class-four"]') + expect(element).toBeTruthy() + }) + + test('when the html is nested styles are merged togheter', async () => { + const element = await page.$('html[style="background-color: black; color: white;"]') + expect(element).toBeTruthy() + }) + + test('when the html is nested events are invoked sequentially', async () => { + await page.waitForSelector('html[data-hydrated]') + await page.click('html') + await page.waitForSelector('[data-keys][data-objected][data-visible]') + const element = await page.$('[data-keys][data-objected][data-visible]') + expect(element).toBeTruthy() + }) + + test('when a html is added to the vdom attributes are added', async () => { + await page.waitForSelector('html[data-hydrated]') + await page.click('html') + await page.waitForSelector('html[data-visible]') + const element = await page.$('html[data-visible]') + expect(element).toBeTruthy() + }) + + test('when a html is removed from the vdom attributes are removed', async () => { + await page.waitForSelector('html[data-hydrated]') + await page.click('html') + await page.waitForSelector('html[data-visible]') + await page.click('html') + await page.waitForSelector('html:not([data-visible])') + const element = await page.$('html:not([data-visible])') + expect(element).toBeTruthy() + }) + + test('the html removes events when the fragment leaves the tree', async () => { + await page.waitForSelector('html[data-hydrated]') + await page.click('[href="/"]') + await page.waitForSelector('[data-application-hydrated]:not([data-count])') + await page.click('html') + const element = await page.$('[data-application-hydrated]:not([data-count])') + expect(element).toBeTruthy() + }) +}) diff --git a/tests/src/ObjectEventScope.njs b/tests/src/ObjectEventScope.njs new file mode 100644 index 00000000..f63510ff --- /dev/null +++ b/tests/src/ObjectEventScope.njs @@ -0,0 +1,43 @@ +import Nullstack from 'nullstack'; + +class NestedComponent extends Nullstack { + + boolean = false + + render({ source, onchange, id }) { + return ( +
+ +
+ ) + } + +} + +class ObjectEventScope extends Nullstack { + + boolean = false + visible = false + + render() { + return ( +
+ + +

----

+ + {this.visible && +
+ + +
+ } +

----

+

Outter: {this.boolean.toString()}

+
+ ) + } + +} + +export default ObjectEventScope; \ No newline at end of file diff --git a/tests/src/ObjectEventScope.test.js b/tests/src/ObjectEventScope.test.js new file mode 100644 index 00000000..043325e9 --- /dev/null +++ b/tests/src/ObjectEventScope.test.js @@ -0,0 +1,38 @@ +describe('OptimizedEvents', () => { + beforeEach(async () => { + await page.goto('http://localhost:6969/object-event-scope') + await page.waitForSelector('[data-hydrated]') + }) + + test('single object events run in declaration scope on render', async () => { + await page.click('#render-single-object') + await page.waitForSelector('[data-boolean]') + const element = await page.$('[data-boolean]') + expect(element).toBeTruthy() + }) + + test('array object events run in declaration scope on render', async () => { + await page.click('#render-object-array') + await page.waitForSelector('[data-boolean]') + const element = await page.$('[data-boolean]') + expect(element).toBeTruthy() + }) + + test('single object events run in declaration scope on rerender', async () => { + await page.click('#rerender') + await page.waitForSelector('#rerender-single-object') + await page.click('#rerender-single-object') + await page.waitForSelector('[data-boolean]') + const element = await page.$('[data-boolean]') + expect(element).toBeTruthy() + }) + + test('array object events run in declaration scope on rerender', async () => { + await page.click('#rerender') + await page.waitForSelector('#rerender-object-array') + await page.click('#render-object-array') + await page.waitForSelector('[data-boolean]') + const element = await page.$('[data-boolean]') + expect(element).toBeTruthy() + }) +}) diff --git a/tests/src/WindowFragment.njs b/tests/src/WindowFragment.njs new file mode 100644 index 00000000..4e135937 --- /dev/null +++ b/tests/src/WindowFragment.njs @@ -0,0 +1,42 @@ +import Nullstack from 'nullstack' + +class WindowFragment extends Nullstack { + + count = 0 + visible = false + objected = false + + increment() { + this.count++ + } + + reveal() { + this.visible = !this.visible + } + + render() { + return ( +
+ console.log('hi'), {objected: true}]} + > + {this.hydrated &&
} + + WindowFragment + + {this.visible && ( + +
+ WindowFragment2 +
+
+ )} + home + +
+ ) + } + +} + +export default WindowFragment diff --git a/tests/src/WindowFragment.test.js b/tests/src/WindowFragment.test.js new file mode 100644 index 00000000..ff3d399e --- /dev/null +++ b/tests/src/WindowFragment.test.js @@ -0,0 +1,32 @@ +beforeEach(async () => { + await page.goto('http://localhost:6969/window-fragment') +}) + +describe('WindowFragment', () => { + test('the window behaves as a fragment and creates no markup', async () => { + const element = await page.$('[data-window-parent] > [data-window-child]') + expect(element).toBeTruthy() + }) + + test('the window ignores non attribute events', async () => { + const element = await page.$('[data-window-parent] > [data-window-child]') + expect(element).toBeTruthy() + }) + + test('when the window is nested events are invoked sequentially', async () => { + await page.waitForSelector('[data-hydrated]') + await page.click('body') + await page.waitForSelector('[data-count="1"][data-objected]') + const element = await page.$('[data-count="1"][data-objected]') + expect(element).toBeTruthy() + }) + + test('the window removes events when the fragment leaves the tree', async () => { + await page.waitForSelector('[data-hydrated]') + await page.click('[href="/"]') + await page.waitForSelector('[data-application-hydrated]:not([data-count])') + await page.click('body') + const element = await page.$('[data-application-hydrated]:not([data-count])') + expect(element).toBeTruthy() + }) +}) From 16ea9cb4a14d35ccf5c998e46ddd379144486790 Mon Sep 17 00:00:00 2001 From: Maycon Sousa Date: Mon, 22 May 2023 11:48:30 -0300 Subject: [PATCH 7/9] replace ts types for interfaces --- types/ClientContext.d.ts | 7 +++++-- types/Environment.d.ts | 2 +- types/Instances.d.ts | 1 + types/Page.d.ts | 2 +- types/Plugin.d.ts | 4 ++-- types/Project.d.ts | 2 +- types/Request.d.ts | 3 +++ types/Response.d.ts | 3 +++ types/Router.d.ts | 2 +- types/Secrets.d.ts | 2 +- types/Server.d.ts | 2 +- types/ServerContext.d.ts | 10 +++++++--- types/Settings.d.ts | 2 +- types/Worker.d.ts | 2 +- types/index.d.ts | 3 +++ 15 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 types/Instances.d.ts create mode 100644 types/Request.d.ts create mode 100644 types/Response.d.ts diff --git a/types/ClientContext.d.ts b/types/ClientContext.d.ts index ea3f62a6..9caace1b 100644 --- a/types/ClientContext.d.ts +++ b/types/ClientContext.d.ts @@ -1,4 +1,5 @@ import { NullstackEnvironment } from './Environment' +import { NullstackInstances } from './Instances' import { NullstackFragment } from './JSX' import { NullstackPage } from './Page' import { NullstackParams } from './Params' @@ -10,7 +11,7 @@ import { NullstackWorker } from './Worker' /** * @see https://nullstack.app/context */ -export type NullstackClientContext = TProps & { +interface BaseNullstackClientContext { /** * Callback function that bootstrap the context for the application. */ @@ -53,7 +54,7 @@ export type NullstackClientContext = TProps & { * @scope client * @see https://nullstack.app/context-instances */ - instances: Record + instances: NullstackInstances /** * It gives you information about the current environment. @@ -129,3 +130,5 @@ export type NullstackClientContext = TProps & { element?: HTMLElement } + +export type NullstackClientContext = BaseNullstackClientContext & TProps \ No newline at end of file diff --git a/types/Environment.d.ts b/types/Environment.d.ts index db962150..f761598d 100644 --- a/types/Environment.d.ts +++ b/types/Environment.d.ts @@ -1,4 +1,4 @@ -export type NullstackEnvironment = { +export interface NullstackEnvironment { client: boolean server: boolean diff --git a/types/Instances.d.ts b/types/Instances.d.ts new file mode 100644 index 00000000..1b5d7dc9 --- /dev/null +++ b/types/Instances.d.ts @@ -0,0 +1 @@ +export interface NullstackInstances extends Record {} \ No newline at end of file diff --git a/types/Page.d.ts b/types/Page.d.ts index c78e5db6..0d3a0747 100644 --- a/types/Page.d.ts +++ b/types/Page.d.ts @@ -1,4 +1,4 @@ -export type NullstackPage = { +export interface NullstackPage { /** * Current page title * diff --git a/types/Plugin.d.ts b/types/Plugin.d.ts index bf680ac2..dbdcefb6 100644 --- a/types/Plugin.d.ts +++ b/types/Plugin.d.ts @@ -1,7 +1,7 @@ import { NullstackClientContext } from './ClientContext' import { AllHTMLAttributes, NullstackAttributes } from './JSX' -type NullstackPluginNode = { +interface NullstackPluginNode { type: string | boolean attributes: AllHTMLAttributes & NullstackAttributes children: NullstackClientContext['children'] @@ -11,7 +11,7 @@ interface NullstackNodeContext extends NullstackClientContext { node: NullstackPluginNode } -export type NullstackPlugin = { +export interface NullstackPlugin { /** * Runs transformation to node element * @param context Context with node attributes diff --git a/types/Project.d.ts b/types/Project.d.ts index a20b04d9..cd282da4 100644 --- a/types/Project.d.ts +++ b/types/Project.d.ts @@ -1,4 +1,4 @@ -export type NullstackProject = { +export interface NullstackProject { domain: string /** diff --git a/types/Request.d.ts b/types/Request.d.ts new file mode 100644 index 00000000..ea864563 --- /dev/null +++ b/types/Request.d.ts @@ -0,0 +1,3 @@ +import { Request } from 'express' + +export interface NullstackRequest extends Request {} \ No newline at end of file diff --git a/types/Response.d.ts b/types/Response.d.ts new file mode 100644 index 00000000..15458b80 --- /dev/null +++ b/types/Response.d.ts @@ -0,0 +1,3 @@ +import { Response } from 'express' + +export interface NullstackResponse extends Response {} \ No newline at end of file diff --git a/types/Router.d.ts b/types/Router.d.ts index 216b36f4..38dab493 100644 --- a/types/Router.d.ts +++ b/types/Router.d.ts @@ -1,4 +1,4 @@ -export type NullstackRouter = { +export interface NullstackRouter { /** * The router path including query params. * Does not contain the domain and port. diff --git a/types/Secrets.d.ts b/types/Secrets.d.ts index 349e2f5c..d1b8ecb5 100644 --- a/types/Secrets.d.ts +++ b/types/Secrets.d.ts @@ -1 +1 @@ -export type NullstackSecrets = Record +export interface NullstackSecrets extends Record {} diff --git a/types/Server.d.ts b/types/Server.d.ts index b6470951..56007c19 100644 --- a/types/Server.d.ts +++ b/types/Server.d.ts @@ -1,4 +1,4 @@ -export type NullstackServer = { +export interface NullstackServer { get(...args) post(...args) diff --git a/types/ServerContext.d.ts b/types/ServerContext.d.ts index 8ec90589..5a84f660 100644 --- a/types/ServerContext.d.ts +++ b/types/ServerContext.d.ts @@ -1,5 +1,7 @@ import { NullstackEnvironment } from './Environment' import { NullstackProject } from './Project' +import { NullstackRequest } from './Request' +import { NullstackResponse } from './Response' import { NullstackSecrets } from './Secrets' import { NullstackServer } from './Server' import { NullstackSettings } from './Settings' @@ -8,7 +10,7 @@ import { NullstackWorker } from './Worker' /** * @see https://nullstack.app/context */ -export type NullstackServerContext = TProps & { +interface BaseNullstackServerContext { /** * Callback function that bootstrap the context for the application. */ @@ -49,7 +51,7 @@ export type NullstackServerContext = TProps & { * @scope server * @see https://nullstack.app/server-request-and-response */ - request?: Record + request?: NullstackRequest /** * Original `response` object from [Express](https://expressjs.com/) @@ -57,7 +59,7 @@ export type NullstackServerContext = TProps & { * @scope server * @see https://nullstack.app/server-request-and-response */ - response?: Record + response?: NullstackResponse /** * You can assign any key with any type of public information. @@ -83,3 +85,5 @@ export type NullstackServerContext = TProps & { */ secrets: NullstackSecrets } + +export type NullstackServerContext = BaseNullstackServerContext & TProps \ No newline at end of file diff --git a/types/Settings.d.ts b/types/Settings.d.ts index efe88216..cf699bcc 100644 --- a/types/Settings.d.ts +++ b/types/Settings.d.ts @@ -1 +1 @@ -export type NullstackSettings = Record +export interface NullstackSettings extends Record {} diff --git a/types/Worker.d.ts b/types/Worker.d.ts index b4999345..77ac02e2 100644 --- a/types/Worker.d.ts +++ b/types/Worker.d.ts @@ -1,4 +1,4 @@ -export type NullstackWorker = { +export interface NullstackWorker { /** * - keys: server functions names * - values: array of these functions arguments diff --git a/types/index.d.ts b/types/index.d.ts index 1a46f414..b2243c41 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -5,10 +5,13 @@ import { NullstackServerContext } from './ServerContext' export * from './ClientContext' export * from './Environment' +export * from './Instances' export * from './Page' export * from './Params' export * from './Plugin' export * from './Project' +export * from './Request' +export * from './Response' export * from './Router' export * from './Secrets' export * from './Server' From eb2fd96045d780d7f132fb699be0be4b8ef31c5b Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 23 May 2023 16:04:55 -0300 Subject: [PATCH 8/9] :sparkles: bind date --- client/events.js | 6 ++++++ plugins/bindable.js | 5 +++++ tests/src/TwoWayBindings.njs | 9 +++++++++ tests/src/TwoWayBindings.test.js | 15 +++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/client/events.js b/client/events.js index 874e0788..718c2850 100644 --- a/client/events.js +++ b/client/events.js @@ -67,6 +67,12 @@ 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] } diff --git a/plugins/bindable.js b/plugins/bindable.js index b3424084..1f8b27c7 100644 --- a/plugins/bindable.js +++ b/plugins/bindable.js @@ -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] ?? '' } diff --git a/tests/src/TwoWayBindings.njs b/tests/src/TwoWayBindings.njs index 64a77596..a7a4d352 100644 --- a/tests/src/TwoWayBindings.njs +++ b/tests/src/TwoWayBindings.njs @@ -9,6 +9,7 @@ class TwoWayBindings extends Nullstack { boolean = true character = 'a' text = 'aaaa' + date = new Date() object = { count: 1 } array = ['a', 'b', 'c'] @@ -143,6 +144,14 @@ class TwoWayBindings extends Nullstack { > rebounce +
) } diff --git a/tests/src/TwoWayBindings.test.js b/tests/src/TwoWayBindings.test.js index 885486f9..26991660 100644 --- a/tests/src/TwoWayBindings.test.js +++ b/tests/src/TwoWayBindings.test.js @@ -257,4 +257,19 @@ describe('TwoWayBindings', () => { const element = await page.$('[data-render-clicked-and-inputed]') expect(element).toBeTruthy() }) + + test('dates can be bound to date inputs', async () => { + await page.$eval('[data-instanceof-date]', (element) => { + element.focus() + element.value = '1992-10-16' + element.blur() + const event = new Event('input') + element.dispatchEvent(event) + }) + await page.waitForSelector('[data-date="16"]') + await page.waitForSelector('[data-month="10"]') + await page.waitForSelector('[data-year="1992"]') + const element = await page.$('[data-date="16"]') + expect(element).toBeTruthy() + }) }) From 91fcd977a49f05b01835966bf0cb32b0fcd63e28 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 30 May 2023 11:54:00 -0300 Subject: [PATCH 9/9] :bookmark: version 0.19.0 --- package.json | 4 ++-- tests/src/Head.jsx | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/src/Head.jsx diff --git a/package.json b/package.json index 3659ca77..f8178938 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.18.1", + "version": "0.19.0", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", @@ -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", diff --git a/tests/src/Head.jsx b/tests/src/Head.jsx new file mode 100644 index 00000000..11f41af7 --- /dev/null +++ b/tests/src/Head.jsx @@ -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 ( + + + {page.title} - {project.title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file