Skip to content
Merged
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
9 changes: 5 additions & 4 deletions client/client.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import router from './router';
import rerender from './rerender';
import context, { generateContext } from './context';

import generateTree from '../shared/generateTree';
import { loadPlugins } from '../shared/plugins';
import context, { generateContext } from './context';
import rerender from './rerender';
import router from './router';


const client = {};

Expand Down Expand Up @@ -53,6 +53,7 @@ client.processLifecycleQueues = async function () {
for (const instance of initiationQueue) {
instance.initiate && await instance.initiate();
instance._self.initiated = true;
instance.launch && instance.launch()
}
if (initiationQueue.length) {
client.update();
Expand Down
3 changes: 2 additions & 1 deletion server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class Nullstack {
_self = {
prerendered: true,
initiated: false,
hydrated: false
hydrated: false,
terminated: false,
}

constructor(scope) {
Expand Down
7 changes: 7 additions & 0 deletions shared/generateTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,17 @@ async function generateBranch(parent, node, depth, scope) {
if (scope.memory) {
memory = scope.memory[key];
if (memory) {
instance._self.prerendered = true;
instance._self.initiated = true;
Object.assign(instance, memory);
delete scope.memory[key];
}
}
let shouldHydrate = false;
const shouldLaunch = instance._self.initiated && (
!instance._self.prerendered ||
(instance._self.persistent && instance._self.terminated)
)
if (instance._self.terminated) {
shouldHydrate = true;
instance._self.terminated = false;
Expand All @@ -69,6 +74,7 @@ async function generateBranch(parent, node, depth, scope) {
if (scope.context.environment.server) {
instance.initiate && await instance.initiate();
instance._self.initiated = true;
instance.launch && instance.launch();
} else {
scope.initiationQueue.push(instance);
}
Expand All @@ -77,6 +83,7 @@ async function generateBranch(parent, node, depth, scope) {
}
if (scope.hydrationQueue) {
if (shouldHydrate) {
shouldLaunch && instance.launch && instance.launch();
scope.hydrationQueue.push(instance);
} else if (instance._self.initiated == true) {
instance.update && instance.update();
Expand Down
1 change: 1 addition & 0 deletions tests/src/Application.njs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Application extends Nullstack {
<a href="/static-this"> static this </a>
<a href="/routes-and-params/a"> router with params </a>
<a href="/undefined-nodes"> undefined nodes </a>
<a href="/full-stack-lifecycle"> lifecycle </a>
</div>
<RenderableComponent route="/renderable-component" />
<StatefulComponent route="/stateful-component" />
Expand Down
31 changes: 20 additions & 11 deletions tests/src/FullStackLifecycle.njs
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,45 @@ class FullStackLifecycle extends Nullstack {

prepared = false;
initiated = false;
launched = false;
hydrated = false;
updated = false;

prepare() {
prepare({ environment }) {
this.prepared = true;
this.prepareEnv = environment.client ? 'client' : 'server'
}

async initiate() {
this.initiated = true;
async initiate({ environment }) {
this.initiated = this.prepared;
this.initiateEnv = environment.client ? 'client' : 'server'
}

launch({ environment }) {
this.launched = this.initiated;
this.launchEnv = environment.client ? 'client' : 'server'
}

hydrate() {
this.hydrated = true;
this.hydrated = this.launched;
}

update() {
if(!this.updated) {
this.updated = true;
if (!this.updated) {
this.updated = this.hydrated;
}
}
async terminate({params}) {
params.terminated = true;

async terminate({ params }) {
params.terminated = this.updated;
}

render() {
return (
<div class="FullStackLifecycle">
<div data-prepared={this.prepared} />
<div data-initiated={this.initiated} />
<div data-prepared={this.prepared} data-prepare-env={this.prepareEnv} />
<div data-initiated={this.initiated} data-initiate-env={this.initiateEnv} />
<div data-launched={this.launched} data-launch-env={this.launchEnv} />
<div data-hydrated={this.hydrated} />
<div data-updated={this.updated} />
<a href="/"> Terminate </a>
Expand Down
103 changes: 99 additions & 4 deletions tests/src/FullStackLifecycle.test.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,123 @@
const puppeteer = require('puppeteer');

let browser;
let page;

beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
await page.goto('http://localhost:6969/full-stack-lifecycle');
});

describe('FullStackLifecycle', () => {
describe('FullStackLifecycle ssr', () => {

let page;

beforeAll(async () => {
page = await browser.newPage();
await page.goto('http://localhost:6969/full-stack-lifecycle');
});

test('prepare should run', async () => {
await page.waitForSelector('[data-prepared]');
const element = await page.$('[data-prepared]');
expect(element).toBeTruthy();
});

test('prepare should run in the server for ssr', async () => {
await page.waitForSelector('[data-prepare-env="server"]');
const element = await page.$('[data-prepare-env="server"]');
expect(element).toBeTruthy();
});

test('initiate should run', async () => {
await page.waitForSelector('[data-initiated]');
const element = await page.$('[data-initiated]');
expect(element).toBeTruthy();
});

test('initiate should run in the server for ssr', async () => {
await page.waitForSelector('[data-initiate-env="server"]');
const element = await page.$('[data-initiate-env="server"]');
expect(element).toBeTruthy();
});

test('launch should run', async () => {
await page.waitForSelector('[data-launched]');
const element = await page.$('[data-launched]');
expect(element).toBeTruthy();
});

test('launch should run in the server for ssr', async () => {
await page.waitForSelector('[data-launch-env="server"]');
const element = await page.$('[data-launch-env="server"]');
expect(element).toBeTruthy();
});

test('hydrate should run', async () => {
await page.waitForSelector('[data-hydrated]');
const element = await page.$('[data-hydrated]');
expect(element).toBeTruthy();
});

test('update should run', async () => {
await page.waitForSelector('[data-updated]');
const element = await page.$('[data-updated]');
expect(element).toBeTruthy();
});

test('terminate should run', async () => {
await page.click('a[href="/"]');
await page.waitForFunction(() => location.search == '?terminated=true');
const element = await page.$('.FullStackLifecycle');
expect(element).toBeFalsy();
});

});

describe('FullStackLifecycle spa', () => {

let page;

beforeAll(async () => {
page = await browser.newPage();
await page.goto('http://localhost:6969/');
await page.click('a[href="/full-stack-lifecycle"]')
});

test('prepare should run', async () => {
await page.waitForSelector('[data-prepared]');
const element = await page.$('[data-prepared]');
expect(element).toBeTruthy();
});

test('prepare should run in the client for spa', async () => {
await page.waitForSelector('[data-prepare-env="client"]');
const element = await page.$('[data-prepare-env="client"]');
expect(element).toBeTruthy();
});

test('initiate should run', async () => {
await page.waitForSelector('[data-initiated]');
const element = await page.$('[data-initiated]');
expect(element).toBeTruthy();
});

test('initiate should run in the client for spa', async () => {
await page.waitForSelector('[data-initiate-env="client"]');
const element = await page.$('[data-initiate-env="client"]');
expect(element).toBeTruthy();
});

test('launch should run', async () => {
await page.waitForSelector('[data-launched]');
const element = await page.$('[data-launched]');
expect(element).toBeTruthy();
});

test('launch should run in the client for spa', async () => {
await page.waitForSelector('[data-launch-env="client"]');
const element = await page.$('[data-launch-env="client"]');
expect(element).toBeTruthy();
});

test('hydrate should run', async () => {
await page.waitForSelector('[data-hydrated]');
const element = await page.$('[data-hydrated]');
Expand Down
24 changes: 22 additions & 2 deletions tests/src/PersistentComponent.njs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ import Nullstack from 'nullstack';
class PersistentComponent extends Nullstack {

count = 0
launchCount = 0

prepare() {
this.count = -1;
}

initiate() {
async initiate() {
this.count = -1;
}

launch({ self }) {
if (self.initiated) {
this.launchCount++
}
}

async hydrate() {
this.count++
}
Expand All @@ -20,10 +27,23 @@ class PersistentComponent extends Nullstack {
this.count++
}

self({ self }) {
return self
}

render({ self, instances }) {
const aCount = instances['PersistentComponent/0-0-33/persistent-component/a']?.count
const aTerminated = instances['PersistentComponent/0-0-33/persistent-component/a']?.self?.()?.terminated
return (
<div data-count={this.count} data-key={self.key} data-a-count={aCount}>
<div
data-count={this.count}
data-key={self.key}
data-a-count={aCount}
data-launch-count={this.launchCount}
data-persistent={self.persistent}
data-prerendered={self.prerendered}
data-a-terminated={aTerminated}
>
<a href="/persistent-component/a"> a </a>
<a href="/persistent-component/b"> b </a>
</div>
Expand Down
36 changes: 36 additions & 0 deletions tests/src/PersistentComponent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ describe('PersistentComponent instantiated', () => {
expect(element).toBeTruthy();
});

test('persistent components self should have a key persistent', async () => {
await page.waitForSelector('[data-persistent]');
const element = await page.$('[data-persistent]');
expect(element).toBeTruthy();
});

test('persistent components should only launch once when prerendered', async () => {
await page.waitForSelector('[data-launch-count="1"]');
const element = await page.$('[data-launch-count="1"]');
expect(element).toBeTruthy();
});

});

describe('PersistentComponent terminated', () => {
Expand All @@ -48,6 +60,12 @@ describe('PersistentComponent terminated', () => {
expect(element).toBeTruthy();
});

test('persistent instanes self should have a terminated key when off dom', async () => {
await page.waitForSelector('[data-a-terminated]');
const element = await page.$('[data-a-terminated]');
expect(element).toBeTruthy();
});

});

describe('PersistentComponent reinstantiated', () => {
Expand Down Expand Up @@ -88,6 +106,24 @@ describe('PersistentComponent reinstantiated', () => {
expect(element).toBeTruthy();
});

test('persistent components should launch again when reinstantiated', async () => {
await page.waitForSelector('[data-launch-count="2"]');
const element = await page.$('[data-launch-count="2"]');
expect(element).toBeTruthy();
});

test('persistent components self should have a key persistent when reinstantiated', async () => {
await page.waitForSelector('[data-persistent]');
const element = await page.$('[data-persistent]');
expect(element).toBeTruthy();
});

test('persistent components self should have a key prerendered when prerendered then reinstantiated', async () => {
await page.waitForSelector('[data-prerendered]');
const element = await page.$('[data-prerendered]');
expect(element).toBeTruthy();
});

});

afterAll(async () => {
Expand Down
2 changes: 2 additions & 0 deletions types/Self.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export type NullstackSelf = {

hydrated: boolean,

terminated: boolean,

prerendered: boolean,

/**
Expand Down