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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@types/semver": "7.3.9",
"@typescript-eslint/eslint-plugin": "5.27.0",
"@typescript-eslint/parser": "5.27.0",
"brotli": "^1.3.3",
"concurrently": "7.2.1",
"cross-spawn": "7.0.3",
"esbuild": "0.14.42",
Expand Down
103 changes: 66 additions & 37 deletions packages/docs/src/pages/docs/components/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ For a web application to be interactive, there needs to be a way to respond to u
const Counter = component$(() => {
const store = useStore({ count: 0 });

return <button onClick$={() => store.count++}>{store.count}</button>;
return (
<button
onClick$={() => store.count++}
>
{store.count}
</button>
);
});
```

Expand All @@ -21,7 +27,7 @@ Notice that `onClick$` ends with `$`. This is a hint to both the Qwik Optimizer
In the above example, the `click` listener is trivial in implementation. But in real applications, the listener may refer to complex code. By creating a lazy-loaded boundary, Qwik can tree-shake all of the code behind the click listener and delay its loading until the user clicks the button.


# Events and Components
## Events and Components

So far, we have discussed how one can listen to DOM events in JSX. A similar mechanism exists with components. Let's assume we have two kinds of buttons to aid the discussion. An HTML button (`<button>`) and a component button `<CmpButton>`.

Expand Down Expand Up @@ -59,7 +65,64 @@ Notice that both `<button>` and `<CmpButton>` use the same syntax for registerin

The main point here is that while the syntax of the events is consistent between HTML elements and Components, the resulting HTML only has `on:<event>` attributes for the DOM events, not for the component props.

## Declaring Component Events
## Prevent default

Because of the async nature of Qwik, event's handler execution might be delayed because the implementation is not downloaded yet. This introduces a problem when the event's handler needs to prevent the default behavior of the event. Tradicional `event.preventDefault()` will not work, instead use the qwik's `preventdefault:{eventName}` attribute:

```tsx
const Counter = component$(() => {
return (
<a
href="/about"
preventdefault:click // This will prevent the default behavior of the "click" event.
onClick$={(event) => {
// PreventDefault will not work here, because handle is dispatched asynchronously.
// event.preventDefault();
singlePageNavigate('/about');
}}
>
Go to about page
</a>
);
});
```

## onWindow and onDocument

So far, we have discussed how to listen to events that originate at elements. There are events (for example, `scroll` and `mousemove`) that require that we listen to them on `window` or `document`. For this reason, Qwik allows for the `onWindow` and `onDocument` prefixes when listening for events.

```tsx
const EventExample = component$(() => {
const store = useStore({
scroll: 0,
mouse: { x: 0, y: 0 },
clickCount: 0,
});

return (
<button
window:onScroll$={(e) => (store.scroll = window.scrollY)}
document:onMouseMove$={(e) => {
store.mouse.x = e.x;
store.mouse.y = e.y;
}}
onClick$={() => store.clickCount++}
>
scroll: {store.scroll}
mouseMove: {store.mouse.x}, {store.mouse.y}
click: {store.clickCount}
</button>
);
});
```

The purpose of the `onWindow`/`onDocument` is to register the event at a current DOM location of the component but have it receive events from the `window`/`document`. There are two advantages to it:

1. The events can be registered declaratively in your JSX
2. The events get automatically cleaned up when the component is destroyed. (No explicit bookkeeping and cleanup is needed.)


## Declaring Custom Component Events

So far, we have ignored the implementation detail of `<CmpButton>` because we wanted to talk about its usage only. Now let's look at how one declares a child component that can be used with events.

Expand Down Expand Up @@ -151,40 +214,6 @@ Notice that we can pass the `props.onClickQrl` directly to the `onDblclickQrl` a
However, it is not possible to pass `props.onClickQrl` to `onClick$` because the types don't match. (This would result in type error: `onClick$={props.onClickQrl}`.) Instead, the `$` is reserved for inlined closures. In our example, we would like to print the `console.log("clicked")` after we process the `props.onClickQrl` callback. We can do so with the `props.onClickQrl.invoke()` method. This 1) lazy-loads the code, 2) restores the closure state, and 3) invokes the closure. The operation is asynchronous and therefore returns a promise, which we can resolve using the `await` statement.


# onWindow and onDocument

So far, we have discussed how to listen to events that originate at elements. There are events (for example, `scroll` and `mousemove`) that require that we listen to them on `window` or `document`. For this reason, Qwik allows for the `onWindow` and `onDocument` prefixes when listening for events.

```tsx
const EventExample = component$(() => {
const store = useStore({
scroll: 0,
mouse: { x: 0, y: 0 },
clickCount: 0,
});

return (
<button
window:onScroll$={(e) => (store.scroll = window.scrollY)}
document:onMouseMove$={(e) => {
store.mouse.x = e.x;
store.mouse.y = e.y;
}}
onClick$={() => store.clickCount++}
>
scroll: {store.scroll}
mouseMove: {store.mouse.x}, {store.mouse.y}
click: {store.clickCount}
</button>
);
});
```

The purpose of the `onWindow`/`onDocument` is to register the event at a current DOM location of the component but have it receive events from the `window`/`document`. There are two advantages to it:

1. The events can be registered declaratively in your JSX
2. The events get automatically cleaned up when the component is destroyed. (No explicit bookkeeping and cleanup is needed.)


## Advanced: Events and qwikloader

Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/utils/useLocation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useHostElement } from '@builder.io/qwik';
import { useDocument } from '@builder.io/qwik';

export const useLocation = () => {
const doc = useHostElement().ownerDocument;
const doc = useDocument();
return doc.location;
};
6 changes: 3 additions & 3 deletions packages/qwik-city/src/runtime/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ export const QwikCityContext = createContext<PageHandler>('qwikcity-page');
* @alpha
*/
export const useQwikCity = () => {
const [value, setValue] = useSequentialScope();
if (value) {
const { get, set } = useSequentialScope<boolean>();
if (get) {
return;
}
setValue(true);
set(true);

const href = useLocation().href;

Expand Down
10 changes: 6 additions & 4 deletions packages/qwik/src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export interface QRL<TYPE = any> {
}

// @alpha
export const qrl: <T = any>(chunkOrFn: string | (() => Promise<any>), symbol: string, lexicalScopeCapture?: any[] | null) => QRL<T>;
export const qrl: <T = any>(chunkOrFn: string | (() => Promise<any>), symbol: string, lexicalScopeCapture?: any[]) => QRL<T>;

// @public (undocumented)
export interface QwikDOMAttributes extends DOMAttributes<any> {
Expand Down Expand Up @@ -544,13 +544,13 @@ export const useMount$: (first: ServerFn) => void;
export const useMountQrl: (mountQrl: QRL<ServerFn>) => void;

// @alpha
export const useOn: (event: string, eventFn: QRL<() => void>) => void;
export const useOn: (event: string, eventQrl: QRL<() => void>) => void;

// @alpha
export const useOnDocument: (event: string, eventQrl: QRL<() => void>) => void;

// @alpha
export const useOnWindow: (event: string, eventFn: QRL<() => void>) => void;
export const useOnWindow: (event: string, eventQrl: QRL<() => void>) => void;

// Warning: (ae-incompatible-release-tags) The symbol "useRef" is marked as @public, but its signature references "Ref" which is marked as @alpha
//
Expand All @@ -569,8 +569,10 @@ export const useScopedStyles$: (first: string) => void;
// @alpha (undocumented)
export const useScopedStylesQrl: (styles: QRL<string>) => void;

// Warning: (ae-forgotten-export) The symbol "SequentialScope" needs to be exported by the entry point index.d.ts
//
// @alpha (undocumented)
export const useSequentialScope: () => [any, (prop: any) => void, number];
export const useSequentialScope: <T>() => SequentialScope<T>;

// Warning: (ae-incompatible-release-tags) The symbol "useServerMount$" is marked as @public, but its signature references "ServerFn" which is marked as @alpha
//
Expand Down
31 changes: 13 additions & 18 deletions packages/qwik/src/core/assert/assert.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { logError } from '../util/log';
import { logErrorAndStop } from '../util/log';
import { qDev } from '../util/qdev';
import { isString } from '../util/types';

export const assertDefined = (value: any, text?: string) => {
if (qDev) {
if (value != null) return;
throw newError(text || 'Expected defined value');
throw logErrorAndStop(text || 'Expected defined value');
}
};

export const assertNotPromise = (value: any, text?: string) => {
if (qDev) {
if (!(value instanceof Promise)) return;
throw newError(text || 'Expected defined value.');
throw logErrorAndStop(text || 'Expected defined value.');
}
};

Expand All @@ -26,7 +26,7 @@ export const assertDefinedAndNotPromise = (value: any, text?: string) => {
export const assertInstanceOf = (value: any, type: any, text?: string) => {
if (qDev) {
if (value instanceof type) return;
throw newError(
throw logErrorAndStop(
text || `Expected value '${value}' to be instance of '${type}' but was '${typeOf(value)}'.`
);
}
Expand All @@ -35,49 +35,51 @@ export const assertInstanceOf = (value: any, type: any, text?: string) => {
export const assertString = (value: any, text?: string) => {
if (qDev) {
if (isString(value)) return;
throw newError(text || `Expected value '${value}' to be 'string' but was '${typeOf(value)}'.`);
throw logErrorAndStop(
text || `Expected value '${value}' to be 'string' but was '${typeOf(value)}'.`
);
}
};

export const assertNotEqual = (value1: any, value2: any, text?: string) => {
if (qDev) {
if (value1 !== value2) return;
throw newError(text || `Expected '${value1}' !== '${value2}'.`);
throw logErrorAndStop(text || `Expected '${value1}' !== '${value2}'.`);
}
};

export const assertEqual = (value1: any, value2: any, text?: string) => {
if (qDev) {
if (value1 === value2) return;
throw newError(text || `Expected '${value1}' === '${value2}'.`);
throw logErrorAndStop(text || `Expected '${value1}' === '${value2}'.`);
}
};

export const assertLessOrEqual = (value1: any, value2: any, text?: string) => {
if (qDev) {
if (value1 <= value2) return;
throw newError(text || `Expected '${value1}' <= '${value2}'.`);
throw logErrorAndStop(text || `Expected '${value1}' <= '${value2}'.`);
}
};

export const assertLess = (value1: any, value2: any, text?: string) => {
if (qDev) {
if (value1 < value2) return;
throw newError(text || `Expected '${value1}' < '${value2}'.`);
throw logErrorAndStop(text || `Expected '${value1}' < '${value2}'.`);
}
};

export const assertGreaterOrEqual = (value1: any, value2: any, text?: string) => {
if (qDev) {
if (value1 >= value2) return;
throw newError(text || `Expected '${value1}' >= '${value2}'.`);
throw logErrorAndStop(text || `Expected '${value1}' >= '${value2}'.`);
}
};

export const assertGreater = (value1: any, value2: any, text?: string) => {
if (qDev) {
if (value1 > value2) return;
throw newError(text || `Expected '${value1}' > '${value2}'.`);
throw logErrorAndStop(text || `Expected '${value1}' > '${value2}'.`);
}
};

Expand All @@ -90,10 +92,3 @@ const typeOf = (value: any) => {
return type;
}
};

const newError = (text: string) => {
debugger; // eslint-disable-line no-debugger
const error = new Error(text);
logError(error); // eslint-disable-line no-console
return error;
};
4 changes: 4 additions & 0 deletions packages/qwik/src/core/error/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const QError_missingRenderCtx = 15;
export const QError_missingDoc = 16;
export const QError_immutableProps = 17;
export const QError_hostCanOnlyBeAtRoot = 18;
export const QError_immutableJsxProps = 19;
export const QError_useInvokeContext = 20;

export const qError = (code: number, ...parts: any[]): Error => {
const text = codeToText(code);
Expand Down Expand Up @@ -49,6 +51,8 @@ export const codeToText = (code: number): string => {
'Cant access document for existing context', // 16
'props are inmutable', // 17
'<Host> component can only be used at the root of a Qwik component$()', // 18
'Props are immutable by default.', // 19
'use- method must be called only at the root level of a component$()',
];
return `Code(${code}): ${MAP[code] ?? ''}`;
} else {
Expand Down
10 changes: 8 additions & 2 deletions packages/qwik/src/core/import/qrl-class.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { qError, QError_qrlIsNotFunction } from '../error/error';
import { verifySerializable } from '../object/q-object';
import { InvokeContext, newInvokeContext, useInvoke } from '../use/use-core';
import { then } from '../util/promises';
import { qDev } from '../util/qdev';
import { isFunction, ValueOrPromise } from '../util/types';
import { qrlImport, QRLSerializeOptions, stringifyQRL } from './qrl';
import type { QRL as IQRL } from './qrl.public';
Expand All @@ -21,8 +23,12 @@ class QRL<TYPE = any> implements IQRL<TYPE> {
public $symbolRef$: null | ValueOrPromise<TYPE>,
public $symbolFn$: null | (() => Promise<Record<string, any>>),
public $capture$: null | string[],
public $captureRef$: null | any[]
) {}
public $captureRef$: any[] | null
) {
if (qDev) {
verifySerializable($captureRef$);
}
}

setContainer(el: Element) {
if (!this.$el$) {
Expand Down
Loading