Skip to content

Commit

Permalink
Merge pull request #250 from lightning-js/dev
Browse files Browse the repository at this point in the history
Release 1.14.0
  • Loading branch information
michielvandergeest authored Jan 6, 2025
2 parents 40f6771 + ec89549 commit a014e20
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 17 deletions.
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## v1.14.0

_6 jan 2025_

- Added ability to deregister listeners (`this.$unlisten()`)
- Added autosize to images without `w` and `h` attributes
- Fixed cleanup of transitions and end-callbacks when Elements are destroyed
- Added support for reactively updating an entire object (instead of having to update each object key individually)
- Added `this.$size()` method to set the dimensions of a Component
- Added `intercept` input method to handle key presses before they reach the currently focused Component
- Added support for query-parameters in routes (in addition to regular query params)


## v1.13.1

_9 dec 2024_
Expand Down Expand Up @@ -56,7 +69,7 @@ _8 nov 2024_
- Fixed issue with watching nested state variables and global state
- Upgraded to renderer 2.6.2
- Fixed issue with white background for Elements with falsy src attribute
- Fixed issue with calling focus on component that already is focussed
- Fixed issue with calling focus on component that already is focused


## v1.9.2
Expand Down
32 changes: 32 additions & 0 deletions docs/built-in/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ The Layout component also uses this event, to execute its calculation and positi

When the children of the Layout-component have _reactive_ dimensions (i.e. `<Element :w="$mywidth" :h="$myheight" />), the Layout component ensures that all child elements are properly re-positioned whenever a dimension changes.

### Components inside a Layout

It is also possible to place Components inside of a Layout, but there is a small _gotcha_ there. By default a Component does not have any dimensions - it has a width and height of `0`, regardless of the contents of the Component. Normally, when using absolute positioning, this isn't a problem. But in the context of a Layout, each child needs to have dimensions.

If the Component has fixed dimensions, you can just add a `w` and a `h` attribute to the Component tag (i.e. `<MyButton w="100" h="40" />`). This is the most performant way to supply dimensions to a Component and should be used whenever possible.

If the Component has dynamic dimensions that are not known upfront, you can dynamically update the dimensions from inside the Component by calling the `this.$size()`-method. This method accepts an object as its argument with a `w` property for setting the _width_ and a `h` property for setting the _height_.

```js
export default Blits.Component('MyButton', {
template: ``,
props: ['type'],
hooks: {
ready() {
if(this.type === 'large') {
this.$size({
w: 200,
h: 80
})
} else {
this.$size({
w: 100,
h: 40
})
}

}
}
})
```
At this moment Blits does not have support for automatically growing the dimensions of a Component based on it's contents, because of the performance impact that this functionality has.

### Nesting Layouts

It's also possible to nest layouts. Simply place a new `<Layout>`-tag, with it's own children in between the children of another Layout-component. The Layout-component itself will grow automatically with the dimensions of it's children. In other words, it's not required to specify a width (`w`) or height (`h`) on the `<Layout>`-tag itself.
Expand Down
10 changes: 10 additions & 0 deletions docs/components/user_input.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ When a component handles a key press by having a corresponding function specifie
}
```
## Intercepting key input
In addition to the Event handling chain explained above. Blits offers the option to _intercept_ key presses at the root level of the Application, before they reach the currently focused Component. This can be useful in certain situation where you want to globally disable all key presses, or when implementing an override key press handler.
The `intercept()` input-method can only be implemented in the `Blits.Application`-component. When present, the method acts as a _catch-all_ method, and will be executed for _all_ key presses. It receives the `KeyboardEvent` as its argument, allowing you to execute logic based on the key being pressed.
Only when the `intercept()` input-method returns the `KeyboardEvent` (possibly modified), the keypress will continue to be handled (by the currently focused Component).
The `intercept`-method can also be an asynchronous method.
## Key-up handling
The functions specified in the `input` configuration are invoked when a key is _pressed down_ (i.e. the `keydown` event listener). But sometimes you may also want to execute some logic when a key is _released_ (i.e. the `keyup` event listener).
Expand Down
36 changes: 36 additions & 0 deletions docs/components/utility_methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,42 @@ export default Blits.Component('MyComponent', {
})
```

### $unlisten

The `$unlisten` utility method (which is available on each Blits Component) is designed to remove event listeners that were previously registered using `$listen()`. This method helps in managing event listeners manually when needed.

The first argument of the `$unlisten()` method is the event `name` to stop listening for. This ensures that all listeners for the specified event are removed from the component.

Generally, you might not need to call `$unlisten()` manually, as Blits automatically deregisters listeners when a Component is destroyed. However, it can be useful in scenarios where you need to stop listening for events before the Component is destroyed.

```js
export default Blits.Component('MyComponent', {
hooks: {
init() {
this.activateListener = () => {
console.log('activated')
this.text = 'We are active!'
}

this.changeBackgroundListener = (data) => {
setTimeout(() => {
this.backgroundImage = data.img
}, data.delay)
}

// Register listeners
this.$listen('activate', this.activateListener)
this.$listen('changeBackground', this.changeBackgroundListener)
},
unfocus() {
// Remove listeners when Component is unfocused
this.$unlisten('activate')
this.$unlisten('changeBackground')
}
}
})
```

## Timeouts

Setting a timeout is a typical way in JS to execute functionality with a delay. But timeouts can also be a common cause of
Expand Down
13 changes: 11 additions & 2 deletions docs/router/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ Each route in this array, is an Object literal that includes the following key p

Besides static routes such as `/account` and `/settings/wifi/advanced`, the Blits router also supports dynamic routes where URI parts can contain params.

To define a param in a route path prefix a string with a colon (`:`), followed by the name of the param, e.g. `/movies/:genre/:id`. This route path will match a URI hash such as `#/movies/sci-fi/65281918`. The router will take the params and inject them as a `prop` into the Page that's being navigated to.
To define a param in a route path, prefix a string with a colon (`:`), followed by the name of the param, e.g. `/movies/:genre/:id`. This route path will match a URI hash such as `#/movies/sci-fi/65281918`. The router will take the params and inject them as a `prop` into the Page that's being navigated to.

To access these props in the Page component, they must first be defined in the Component first (i.e. `props: ['genre', 'id']`). Once defined, the values (`sci-fi` and `65281918`) in this case, will be available on the `this` scope, as with any regular Component.
To access these props in the Page component, they must first be defined in the Page component as _props_ first (i.e. `props: ['genre', 'id']`). Once defined, the values (`sci-fi` and `65281918`) in this case, will be available on the `this` scope as `this.genre` and `this.id` (or as `$genre` and `$id` when used in the template), as with any regular Component.

#### Using query parameters in routes

According to the browser specifications, query parameters are not part of a URL hash. This means that query parameters should be placed _before_ the URL hash (i.e. `http://localhost:5173?id=100&name=john#/my/page/hash`) and implies that "real" query parameters are _not_ part of the route paths.

In this case `id` and `name` won't automatically be made available as props inside a Blits component. They can however be retrieved using `document.location.search` in combination with `new URLSearchParams`, and can be used in an App's `index.js` to set dynamic launch settings for example.

Sometimes you may actually want pass custom data into a Blits component, without it being part of the dynamic route path or `data`-object during navigation. For these cases Blits allows you to use query parameters as part of a route (i.e. `#/series/simpsons/5/10?id=100&name=john`).

This URL hash will match the route `/series/:show/:season/:episode` and it will pass `id` and `name` as additional props into the Page component. Note: similar to dynamic path params, route query params should also be be defined in the Page component as _props_ first (i.e. `props: ['id', 'name']`) in order to be accessed on the `this`-scope.

## Router view

Expand Down
34 changes: 30 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,26 @@ declare module '@lightningjs/blits' {
}

export interface Input {
[key: string]: (event: KeyboardEvent) => void | undefined,
[key: string]: (event: KeyboardEvent) => void | undefined | unknown,
/**
* Catch all input function
*
* Will be invoked when there is no dedicated function for a certain key
*/
// @ts-ignore
any?: (event: KeyboardEvent) => void
any?: (event: KeyboardEvent) => void,
/**
* Intercept key presses on the root Application component before being handled
* by the currently focused component.
*
* Only when a KeyboardEvent (the original one, or a modified one) is returned from the
* intercept function, the Input event is passed on to the Component with focus.
*
* The intercept function can be asynchronous.
*
* Note: the intercept input handler is only available on the Root App component (i.e. Blits.Application)
*/
intercept?: (event: KeyboardEvent) => KeyboardEvent | Promise<KeyboardEvent | any> | any
}

export interface Log {
Expand Down Expand Up @@ -153,7 +165,9 @@ declare module '@lightningjs/blits' {

export type ComponentBase = {
/**
* Check if a component has focus
* Indicates whether the component currently has focus
*
* @returns Boolean
*/
hasFocus: boolean,

Expand Down Expand Up @@ -244,11 +258,23 @@ declare module '@lightningjs/blits' {
* Deprecated: use `this.$trigger()` instead
*/
trigger: (key: string) => void

/**
* Router instance
*/
$router: Router
/**
* Dynamically set the size of a component holder node
*/
$size: (dimensions: {
/**
* Component width
*/
w: number,
/**
* Component height
*/
h: number
}) => void
}

/**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lightningjs/blits",
"version": "1.13.1",
"version": "1.14.0",
"description": "Blits: The Lightning 3 App Development Framework",
"bin": "bin/index.js",
"exports": {
Expand Down
9 changes: 8 additions & 1 deletion src/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ const Application = (config) => {
config.hooks[symbols.init] = function () {
const keyMap = { ...defaultKeyMap, ...Settings.get('keymap', {}) }

keyDownHandler = (e) => {
keyDownHandler = async (e) => {
const key = keyMap[e.key] || keyMap[e.keyCode] || e.key || e.keyCode
// intercept key press if specified in main Application component
if (this[symbols.inputEvents].intercept !== undefined) {
e = await this[symbols.inputEvents].intercept.call(this, e)
// only pass on the key press to focused component when keyboard event is returned
if (e instanceof KeyboardEvent === false) return
}

Focus.input(key, e)
clearTimeout(holdTimeout)
holdTimeout = setTimeout(() => {
Expand Down
8 changes: 8 additions & 0 deletions src/component/base/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ export default {
enumerable: true,
configurable: false,
},
$unlisten: {
value: function (event) {
eventListeners.deregisterListener(this, event)
},
writable: false,
enumerable: true,
configurable: false,
},
}
9 changes: 9 additions & 0 deletions src/component/base/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ import symbols from '../../lib/symbols.js'
import { renderer } from '../../launch.js'

export default {
$size: {
value: function (dimensions = { w: 0, h: 0 }) {
this[symbols.holder].set('w', dimensions.w || 0)
this[symbols.holder].set('h', dimensions.h || 0)
},
writable: false,
enumerable: true,
configurable: false,
},
[symbols.renderer]: {
value: () => renderer,
writable: false,
Expand Down
24 changes: 22 additions & 2 deletions src/engines/L3/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ const propsTransformer = {
if (this.raw['color'] === undefined) {
this.props['color'] = this.props['src'] ? 0xffffffff : 0x00000000
}
// apply auto sizing when no width or height specified
if (!('w' in this.raw) && !('w' in this.raw) && !('h' in this.raw) && !('height' in this.raw)) {
this.props['autosize'] = true
}
},
set texture(v) {
this.props['texture'] = v
Expand Down Expand Up @@ -536,12 +540,18 @@ const Element = {
}

f.once('stopped', () => {
// remove the prop from scheduled transitions
this.scheduledTransitions[prop] = undefined
if (
this.scheduledTransitions[prop] !== undefined &&
this.scheduledTransitions[prop].canceled === true
) {
return
}
// fire transition end callback when animation ends (if specified)
if (transition.end && typeof transition.end === 'function') {
transition.end.call(this.component, this, prop, this.node[prop])
}
// remove the prop from scheduled transitions
delete this.scheduledTransitions[prop]
})

// start animation
Expand All @@ -550,6 +560,16 @@ const Element = {
destroy() {
Log.debug('Deleting Node', this.nodeId)
this.node.destroy()

// Clearing transition end callback functions
const transitionProps = Object.keys(this.scheduledTransitions)
for (let i = 0; i < transitionProps.length; i++) {
const transition = this.scheduledTransitions[transitionProps[i]]
if (transition !== undefined) {
transition.canceled = true
if (transition.f !== undefined) transition.f.stop()
}
}
},
get nodeId() {
return this.node && this.node.id
Expand Down
13 changes: 13 additions & 0 deletions src/lib/eventListeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ export default {
callbackCache.delete(event) // Invalidate the callbackCache when a new callback is added
},

deregisterListener(component, event) {
let componentsMap = eventsMap.get(event)
if (componentsMap === undefined) {
return;
}

if (componentsMap.contains(component)) {
componentsMap.delete(component);
eventsMap.set(event, componentsMap)
callbackCache.delete(event)
}
},

executeListeners(event, params) {
const componentsMap = eventsMap.get(event)
if (componentsMap === undefined || componentsMap.size === 0) {
Expand Down
Loading

0 comments on commit a014e20

Please sign in to comment.