Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 1.14.0 #250

Merged
merged 29 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
854a30f
Feat: Adding the ability to deregister event listeners
AndresMonk Dec 9, 2024
594d21d
Feat: Improvement of naming & expanding testing
AndresMonk Dec 11, 2024
de0a8ec
Merge pull request #241 from AndresMonk/feat/remove_listeners
michielvandergeest Dec 11, 2024
066f6ab
Added autosize of Elements with a src attribute, without width and he…
michielvandergeest Dec 11, 2024
6b54aeb
Remove transition end callbacks on destroy
suresh-gangumalla Dec 12, 2024
937017e
Fixes effects issues with direct obj assingment
suresh-gangumalla Dec 17, 2024
9b67e3e
Added method to dynamically set the width and height of a component …
michielvandergeest Dec 18, 2024
ea0247e
Added functionality to intercept key pres in the root App component.
michielvandergeest Dec 18, 2024
f20b3c0
Added intercept input method to ts definitions.
michielvandergeest Dec 18, 2024
614a25c
Added documentation for intercept input method.
michielvandergeest Dec 18, 2024
0aec9af
Added functionality to use query string parameters in router hash.
michielvandergeest Dec 20, 2024
35f9fa6
Added documentation about route query params.
michielvandergeest Dec 20, 2024
71cfeb3
Adding unknown for Index signature return type
suresh-gangumalla Dec 23, 2024
bd9fb29
Make Sure Arrays Are Not Part Of Object.assign
suresh-gangumalla Dec 26, 2024
c252796
Added instant stop of transitions when Element is destroyed.
michielvandergeest Jan 5, 2025
df623de
Fixed direct object reactivity in combination with arrays.
michielvandergeest Jan 6, 2025
fe1e07f
Merge pull request #246 from lightning-js/feature/router-query-params
michielvandergeest Jan 6, 2025
e58f70b
Merge pull request #245 from suresh-gangumalla/fix/object-assing-oper…
michielvandergeest Jan 6, 2025
1562f24
Optimized removing prop from scheduled transitions.
michielvandergeest Jan 6, 2025
accaf99
Added canceled check to transition stop callback.
michielvandergeest Jan 6, 2025
9e4e2d7
Merge pull request #243 from suresh-gangumalla/fix/clearing-transitio…
michielvandergeest Jan 6, 2025
134a04c
Merge pull request #249 from suresh-gangumalla/fix/intercept-type-def
michielvandergeest Jan 6, 2025
d88298c
Added note about intercept being only available on root app component.
michielvandergeest Jan 6, 2025
a198aae
Changed name of prop from height to h.
michielvandergeest Jan 6, 2025
8fcb08c
Added documentation about setting size of components in a layout tag.
michielvandergeest Jan 6, 2025
356253c
Merge pull request #248 from lightning-js/feature/component-size
michielvandergeest Jan 6, 2025
5b276c3
Fixed some typos in docs.
michielvandergeest Jan 6, 2025
161ec33
Merge pull request #247 from lightning-js/feature/intercept-key-handler
michielvandergeest Jan 6, 2025
ec89549
Bumped version to 1.14.0 and updated changelog.
michielvandergeest Jan 6, 2025
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
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