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
96 changes: 66 additions & 30 deletions docs/components/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,91 @@

Components in Blits have their own [internal state](./component_state.md) and logic, and as such a Component is self-contained. However, as each component is part of a larger App scope, they may need to display different behaviors or appearances based on the rest of the app.

To achieve this, components can receive `props` from their parent component.
To achieve this, components can receive `props` from their parent component. Using props, a parent can pass information into a child Component.

## Defining and passing props

In the Component configuration object, you can specify exactly which props a component accepts. These props are passed to the component via attributes in the parent component's template. Any attributes that are not explicitly defined as a prop will be ignored.

The `props` key in the Component configuration object should be an `Array`, where each item corresponds to a prop that the component can accept.
> **Deprecation notice**:
>
> Previously the props key in the Component configuration object was an `Array`, where each item corresponded to a prop accepted by the component. The array item could either be a `String` (with simply the name of the prop), or an advanced `Object` with a default value for when the prop was not provided by the parent.
>
> Starting Blits 2.0, the array syntax for props has been **deprecated**. The syntax still works at runtime (with the exception of _casting_ and marking props as _required_ - this functionality has been dropped overall), but it is _strongly_ recommended to move to the new _object-based_ notation as soon as possible.
>
> See also the migration path below.

The `props` key in the Component configuration object should now be an `Object`, where each key corresponds to a prop that the component can accept. The value passed to the prop key is used as the default value, when no value for a prop is provided by the parent:

The simplest way to define props is to just list their names within the `props` array:

```js
{
props: ['x', 'color', 'index', 'alpha']
props: {
position: 1,
color: 'red',
index: undefined,
alpha: 1
}
}
```

Once specified, you can refer to these props inside the template of your component using the `$` sign, similar to how you would reference variables defined within the component's [internal state](./component_state.md) (i.e. `<Element color="$color" />`).
Once props are specified in the configuration object, they can be referred to inside the template of a component by using the `$` sign (i.e. `<Element color="$color" />`), similar to how variables defined within the component's [internal state](./component_state.md) are referenced.

You can also access a prop inside a component's code using `this.color` (without a dollar sign!). And similar to component `state` variables, there is no need to specifically reference the `props`-key. Blits automatically maps all props directly on the `this`-scope, for easy access.

Since props are used to pass information from a parent to a child, it's important to not _modify_ props inside your child component. If a value passed as a prop needs modification, then a [computed property](./computed_properties.md), using the prop value passed by the parent, is more appropriate.

### Prop types

You can also access a prop inside a component's code using `this.color` (without a dollar sign!). And similar to component `state` variables,
there is no need to specifically reference the `props`-key. Blits automatically maps all props directly on the `this`-scope, for easy access.
The object notation for `props` facilitates autocompletion of available props and automatic type checking.

Types are inferred based on the default value of a prop. Types can also be added explicitly via TypeScript types or JSDoc comments:


```js
props: {
bgColor: 'red' // type string is inferred
/**
* Height of the element
* @type {number|undefined}
*/
height: undefined,
/**
* Size of the component, passed as a numeric value or a predefined value
* @type {number|presetSizes}
*/
size: 'small'
}
```

Since props are used to pass information from a parent to a child, it's important not to attempt to _modify_ props inside your child component. If changes based on the prop from the parent are needed, you should probably use the prop in a so called [computed property](./computed_properties.md).
```ts
props: {
bgColor: 'red' as string,
height: undefined as number,
size: 'small' as number|presetSizes,
}
```

## Advanced usage
## Migrating from Array Syntax to Object Syntax

For more advanced usage, you can define props using an array with an `object` for each prop, instead of just a string with the accepted name. Within each prop object, you can:
When migrating from the old array-based props to the new object-based props, simply convert the list of prop names into object keys, assigning an appropriate default value.

- Specify a _default value_ for the prop if it's omitted.
- _Validate_ the value of the prop based on certain criteria.
- Mark the prop as _required_.
- Apply a `cast` function to modify the value passed as a prop.
### Before (Array syntax):

As you can see in the following example, you can mix and match the simple string notation with the advanced object notation within the same `props` array.
```js
props: ['bgColor', 'primaryColor', 'height', 'index', 'size']
```

### After (Object syntax):

```js
export default Blits.Component('MyComponent', {
// ...
props: [
'color',
{
key: 'alpha',
default: 0.5,
required: true,
validate(v) {
return v <= 1 && v >= 0;
},
cast: Number
}
]
})
props: {
bgColor: undefined, // or provide a default bgColor
primaryColor: undefined,
height: undefined,
index: undefined,
size: undefined
}
```

You can further enhance type checking by adding JSDoc or TypeScript type annotations where needed.
24 changes: 13 additions & 11 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,20 +372,22 @@ declare module '@lightningjs/blits' {
cast?: () => any
};

// Props Array
export type Props = (string | PropObject)[];
export type Props = Record<string, any>

// Extract the prop names from the props array
type ExtractPropNames<P extends Props> = {
readonly [K in P[number] as K extends string ? K : K extends { key: infer Key } ? Key : never]: any;
};
type InferProp<T> = T extends (...args: any[]) => any ? ReturnType<T> : T

// Update the PropsDefinition to handle props as strings or objects
export type PropsDefinition<P extends Props> = ExtractPropNames<P>;
type InferProps<T extends Record<string, any>> = {
[K in keyof T]: InferProp<T[K]>
}

export type ComponentContext<P extends Props, S, M, C> = ThisType<PropsDefinition<P> & S & M & C & ComponentBase>
export type ComponentContext<
P extends Record<string, any>,
S,
M,
C
> = ThisType<Readonly<InferProps<P>> & S & M & Readonly<C> & ComponentBase>

export interface ComponentConfig<P extends Props, S, M, C, W> {
export interface ComponentConfig<P extends Props = {}, S, M, C, W> {
components?: {
[key: string]: ComponentFactory,
},
Expand Down Expand Up @@ -441,7 +443,7 @@ declare module '@lightningjs/blits' {
* }
* ```
*/
state?: (this: PropsDefinition<P>) => S;
state?: (this: InferProps<P>) => S;
/**
* Methods for abstracting more complex business logic into separate function
*/
Expand Down
59 changes: 31 additions & 28 deletions src/component/setup/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,47 @@
*/

import { Log } from '../../lib/log.js'

import symbols from '../../lib/symbols.js'

const baseProp = {
cast: (v) => v,
required: false,
const normalizeProps = (props) => {
Log.warn(
'Defining props as an Array has been deprecated and will stop working in future versions. Please use the new notation instead (an object with key values pairs).'
)
const out = {}
const propLength = props.length
for (let i = 0; i < propLength; i++) {
const prop = props[i]
if (typeof prop === 'string') {
out[prop] = undefined
} else {
out[prop.key] = prop.default
}
}
return out
}

export default (component, props = []) => {
if (props.indexOf('ref') === -1) {
props.push('ref')
export default (component, props = {}) => {
if (Array.isArray(props) === true) {
props = normalizeProps(props)
}
if (!('ref' in props)) {
props.ref = undefined
}
component[symbols.propKeys] = []
const keys = Object.keys(props)
component[symbols.propKeys] = keys

const propsLength = props.length
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const prop = props[key]

for (let i = 0; i < propsLength; i++) {
const prop = { ...baseProp, ...(typeof props[i] === 'object' ? props[i] : { key: props[i] }) }
component[symbols.propKeys].push(prop.key)
Object.defineProperty(component, prop.key, {
Object.defineProperty(component, key, {
get() {
const value = prop.cast(
this[symbols.props] !== undefined && prop.key in this[symbols.props]
? this[symbols.props][prop.key]
: 'default' in prop
? prop.default
: undefined
)

if (prop.required === true && value === undefined) {
Log.warn(`${prop.key} is required`)
}

return value
if (this[symbols.props] === undefined) return undefined
return key in this[symbols.props] ? this[symbols.props][key] : prop
},
set(v) {
Log.warn(`Warning! Avoid mutating props directly (${prop.key})`)
this[symbols.props][prop.key] = v
Log.warn(`Warning! Avoid mutating props directly (${key})`)
this[symbols.props][key] = v
},
})
}
Expand Down