mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-25 01:04:14 -05:00
feat: useAction hook (#261)
This commit is contained in:
@@ -7,11 +7,13 @@ import {
|
||||
teardown,
|
||||
run,
|
||||
runSnaps,
|
||||
observeActions,
|
||||
useState,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
usePromise,
|
||||
useAction,
|
||||
useRect,
|
||||
useModel,
|
||||
useApp,
|
||||
@@ -57,6 +59,7 @@ describe('hooks', () => {
|
||||
run,
|
||||
teardown,
|
||||
runSnaps,
|
||||
observeActions,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,13 +95,23 @@ describe('hooks', () => {
|
||||
c = {
|
||||
__hooks: {
|
||||
list: [{ teardown: spy }],
|
||||
pendingEffects: [],
|
||||
pendingLayoutEffects: [],
|
||||
pendingEffects: ['a'],
|
||||
pendingLayoutEffects: ['a'],
|
||||
actions: ['a'],
|
||||
},
|
||||
};
|
||||
|
||||
teardown(c);
|
||||
expect(spy.callCount).to.equal(1);
|
||||
|
||||
expect(c.__hooks).to.eql({
|
||||
obsolete: true,
|
||||
list: [],
|
||||
pendingEffects: [],
|
||||
pendingLayoutEffects: [],
|
||||
actions: [],
|
||||
dispatchActions: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -422,6 +435,73 @@ describe('hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAction', () => {
|
||||
beforeEach(() => {
|
||||
c = {};
|
||||
initiate(c);
|
||||
});
|
||||
afterEach(() => {
|
||||
teardown(c);
|
||||
});
|
||||
|
||||
it('should execute callback', async () => {
|
||||
let act;
|
||||
const stub = sandbox.stub();
|
||||
const spy = sandbox.spy();
|
||||
stub.returns({
|
||||
action: spy,
|
||||
});
|
||||
c.fn = () => {
|
||||
[act] = useAction(stub, []);
|
||||
};
|
||||
|
||||
run(c);
|
||||
expect(stub.callCount).to.eql(1);
|
||||
expect(spy.callCount).to.eql(0);
|
||||
|
||||
act();
|
||||
expect(spy.callCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('should maintain reference', async () => {
|
||||
const stub = sandbox.stub();
|
||||
stub.returns({
|
||||
action: 'action',
|
||||
icon: 'ic',
|
||||
active: true,
|
||||
enabled: true,
|
||||
});
|
||||
c.fn = () => {
|
||||
useAction(stub, []);
|
||||
};
|
||||
|
||||
run(c);
|
||||
|
||||
const ref = c.__hooks.list[0].value[0];
|
||||
|
||||
expect(ref.active).to.eql(true);
|
||||
expect(ref.getSvgIconShape()).to.eql('ic');
|
||||
expect(ref.enabled).to.eql(true);
|
||||
});
|
||||
|
||||
it('should dispatch actions', async () => {
|
||||
const spy = sandbox.spy();
|
||||
c.fn = () => {
|
||||
useAction(() => ({ key: 'nyckel' }), []);
|
||||
};
|
||||
|
||||
observeActions(c, spy);
|
||||
expect(spy.callCount).to.eql(1);
|
||||
|
||||
run(c);
|
||||
expect(spy.callCount).to.eql(2);
|
||||
|
||||
const actions = spy.getCall(1).args[0];
|
||||
|
||||
expect(actions[0].key).to.eql('nyckel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRect', () => {
|
||||
let element;
|
||||
|
||||
@@ -576,6 +656,7 @@ describe('hooks', () => {
|
||||
run(c);
|
||||
expect(value).to.equal('model');
|
||||
});
|
||||
|
||||
it('useApp', () => {
|
||||
let value;
|
||||
c.fn = () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ const defaultComponent = /** @lends SnComponent */ {
|
||||
getViewState: () => {},
|
||||
|
||||
// temporary
|
||||
observeActions() {},
|
||||
setSnapshotData: snapshot => Promise.resolve(snapshot),
|
||||
};
|
||||
|
||||
@@ -54,13 +55,14 @@ function createWithHooks(generator, opts, env) {
|
||||
console.warn('Detected multiple supernova modules, this might cause problems.');
|
||||
}
|
||||
}
|
||||
const qGlobal = opts.app && opts.app.session ? opts.app.session.getObjectApi({ handle: -1 }) : null;
|
||||
const c = {
|
||||
context: {
|
||||
element: undefined,
|
||||
layout: {},
|
||||
model: opts.model,
|
||||
app: opts.app,
|
||||
global: opts.global,
|
||||
global: qGlobal,
|
||||
selections: opts.selections,
|
||||
},
|
||||
env,
|
||||
@@ -92,8 +94,15 @@ function createWithHooks(generator, opts, env) {
|
||||
return generator.component.runSnaps(this, layout);
|
||||
},
|
||||
destroy() {},
|
||||
observeActions(callback) {
|
||||
generator.component.observeActions(this, callback);
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(c, {
|
||||
selections: opts.selections,
|
||||
});
|
||||
|
||||
return [c, null];
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export function initiate(component) {
|
||||
},
|
||||
list: [],
|
||||
snaps: [],
|
||||
actions: [],
|
||||
pendingEffects: [],
|
||||
pendingLayoutEffects: [],
|
||||
pendingPromises: [],
|
||||
@@ -48,6 +49,8 @@ export function teardown(component) {
|
||||
component.__hooks.list.length = 0;
|
||||
component.__hooks.pendingEffects.length = 0;
|
||||
component.__hooks.pendingLayoutEffects.length = 0;
|
||||
component.__hooks.actions.length = 0;
|
||||
component.__hooks.dispatchActions = null;
|
||||
|
||||
clearTimeout(component.__hooks.micro);
|
||||
cancelAnimationFrame(component.__hooks.macro);
|
||||
@@ -149,6 +152,18 @@ export function runSnaps(component, layout) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function dispatchActions(component) {
|
||||
component._dispatchActions && component._dispatchActions(component.__hooks.actions.slice());
|
||||
}
|
||||
|
||||
export function observeActions(component, callback) {
|
||||
component._dispatchActions = callback;
|
||||
|
||||
if (component.__hooks) {
|
||||
dispatchActions(component);
|
||||
}
|
||||
}
|
||||
|
||||
function getHook(idx) {
|
||||
if (typeof currentComponent === 'undefined') {
|
||||
throw new Error('Invalid nebula hook call. Hooks can only be called inside a supernova component.');
|
||||
@@ -203,6 +218,7 @@ export function hook(cb) {
|
||||
run,
|
||||
teardown,
|
||||
runSnaps,
|
||||
observeActions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -261,7 +277,7 @@ export function useLayoutEffect(cb, deps) {
|
||||
|
||||
export function useMemo(cb, deps) {
|
||||
if (__NEBULA_DEV__) {
|
||||
if (!deps || !deps.length) {
|
||||
if (!deps) {
|
||||
console.warn('useMemo called without dependencies.');
|
||||
}
|
||||
}
|
||||
@@ -335,6 +351,32 @@ export function usePromise(p, deps) {
|
||||
}
|
||||
|
||||
// ---- composed hooks ------
|
||||
export function useAction(fn, deps) {
|
||||
const [ref] = useState({
|
||||
action() {
|
||||
ref._config.action.call(null);
|
||||
},
|
||||
});
|
||||
|
||||
if (!ref.component) {
|
||||
ref.component = currentComponent;
|
||||
currentComponent.__hooks.actions.push(ref);
|
||||
}
|
||||
useMemo(() => {
|
||||
const a = fn();
|
||||
ref._config = a;
|
||||
|
||||
ref.active = a.active || false;
|
||||
ref.enabled = a.enabled !== false;
|
||||
ref.getSvgIconShape = a.icon ? () => a.icon : undefined;
|
||||
|
||||
ref.key = a.key || ref.component.__hooks.actions.length;
|
||||
dispatchActions(ref.component);
|
||||
}, deps);
|
||||
|
||||
return [ref.action];
|
||||
}
|
||||
|
||||
export function useRect() {
|
||||
const element = useElement();
|
||||
const [rect, setRect] = useState(() => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export { useMemo } from './hooks';
|
||||
export { usePromise } from './hooks';
|
||||
|
||||
// composed hooks
|
||||
export { useAction } from './hooks';
|
||||
export { useRect } from './hooks';
|
||||
export { useModel } from './hooks';
|
||||
export { useApp } from './hooks';
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
/* eslint import/no-extraneous-dependencies: 0 */
|
||||
|
||||
import { useState, useEffect, useLayout, useElement, useTheme, useTranslator, usePromise } from '@nebula.js/supernova';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useLayout,
|
||||
useElement,
|
||||
useTheme,
|
||||
useTranslator,
|
||||
usePromise,
|
||||
useAction,
|
||||
} from '@nebula.js/supernova';
|
||||
|
||||
function sn() {
|
||||
return {
|
||||
@@ -11,16 +20,31 @@ function sn() {
|
||||
const theme = useTheme();
|
||||
const layout = useLayout();
|
||||
|
||||
const [acted, setActed] = useState(false);
|
||||
|
||||
const [act] = useAction(
|
||||
() => ({
|
||||
action() {
|
||||
setActed(true);
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
setCount(prev => prev + 1);
|
||||
if (count >= 1) {
|
||||
act();
|
||||
} else {
|
||||
setCount(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
element.addEventListener('click', listener);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('click', listener);
|
||||
};
|
||||
}, [element]);
|
||||
}, [element, count]);
|
||||
|
||||
const [v] = usePromise(
|
||||
() =>
|
||||
@@ -38,6 +62,7 @@ function sn() {
|
||||
<div class="translator">${translator.get('Common.Cancel')}</div>
|
||||
<div class="theme">${theme.getColorPickerColor({ index: 2 })}</div>
|
||||
<div class="promise">${v || 'pending'}</div>
|
||||
<div class="action">${acted}</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
@@ -16,11 +16,13 @@ describe('hooks', () => {
|
||||
});
|
||||
|
||||
it('should render with initial state', async () => {
|
||||
const text = await page.$eval(`${snSelector} .state`, el => el.textContent);
|
||||
expect(text).to.equal('0');
|
||||
const state = await page.$eval(`${snSelector} .state`, el => el.textContent);
|
||||
expect(state).to.equal('0');
|
||||
const action = await page.$eval(`${snSelector} .action`, el => el.textContent);
|
||||
expect(action).to.equal('false');
|
||||
});
|
||||
|
||||
it('should update count state after click', async () => {
|
||||
it('should update count state after first click', async () => {
|
||||
await page.click(snSelector);
|
||||
await page.waitForFunction(
|
||||
selector => document.querySelector(selector).textContent === '1',
|
||||
@@ -31,6 +33,17 @@ describe('hooks', () => {
|
||||
expect(text).to.equal('1');
|
||||
});
|
||||
|
||||
it('should update action state after second click', async () => {
|
||||
await page.click(snSelector);
|
||||
await page.waitForFunction(
|
||||
selector => document.querySelector(selector).textContent === 'true',
|
||||
{},
|
||||
`${snSelector} .action`
|
||||
);
|
||||
const text = await page.$eval(`${snSelector} .action`, el => el.textContent);
|
||||
expect(text).to.equal('true');
|
||||
});
|
||||
|
||||
it('useLayout', async () => {
|
||||
const text = await page.$eval(`${snSelector} .layout`, el => el.textContent);
|
||||
expect(text).to.equal('true');
|
||||
|
||||
Reference in New Issue
Block a user