Skip to content

Commit

Permalink
Merge pull request #478 from studiometa/feature/option-default-value-…
Browse files Browse the repository at this point in the history
…callback

[Feature] Add support for defining an option default value with a function
  • Loading branch information
titouanmathis committed Jul 5, 2024
2 parents 558d03a + 9dad846 commit c6c1a7c
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 24 deletions.
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ All notable changes to this project will be documented in this file. The format

## [Unreleased]

### Fixed

- Fix code coverage reports ([#474](https://github.com/studiometa/js-toolkit/pull/474))

### Added

- Add support for defining an option default value with a function ([#478](https://github.com/studiometa/js-toolkit/pull/478))
- Add shorthand props on the scroll service for easier destructuring ([#432](https://github.com/studiometa/js-toolkit/pull/432))

### Fixed

- Fix code coverage reports ([#474](https://github.com/studiometa/js-toolkit/pull/474))

## [v3.0.0-alpha.3](https://github.com/studiometa/js-toolkit/compare/3.0.0-alpha.2..3.0.0-alpha.3) (2023-04-17)

### Added
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ And its accompanying HTML would be sprinkled with `data-…` attributes to bind
<div data-component="Counter">
<button data-ref="add">Add</button>
<button data-ref="remove">Remove</button>
<input data-ref="count" type="number" value="0">
<input data-ref="count" type="number" value="0" />
</div>
```

Expand Down Expand Up @@ -129,7 +129,6 @@ export default createApp(App);

Visit our ["Getting Started" guide](https://js-toolkit.studiometa.dev/guide/) to learn more and try the above component by visiting [the playground](https://ui.studiometa.dev/-/play/#script=eNqVkjFPwzAQhff%2BijcguRVtEGurSpTuDKyIwdiXYprYkX2pQFX%2BO7bjlg6A1EiJndO97873bNrOecYRjzLQHMqTZNp0HQbU3rUQD4F7bVxLLO8%2BwoKda%2FaGxWoyUY0MAVvXWyYP%2BmSyOmQOjhMgsGSjoJytzQ7rHAOsbGkJUVRinoOe6rDEi5BaizmEp9YdKO1UShOvY5br2DgbE0dSqkDdzx%2FAX11kP%2FXtG%2FlRkh5NtewbXuL%2BFBvGzRC%2FQzwHsCOOfeaOprMC9MS9t%2BB3E6qb1GCVM6qDbHrahLHKKiESIVwQcsYJ87s%2BjiOvZ72zG623jVH7cwNZWZi4XRdSGUKVzn6hfs4j%2Bwew%2BBsQEaOVyfbrbIyKYqFy8SJZsnzhTzG5TDstcdyp2umSTeM7W30DtazCdQ%3D%3D&html=eNptjkEKwyAQRfc9hcw%2BpHStQukNegOTmYJQZ8SOQm9fUzcJZPX5vLd4FmMzGDRMq6QsTKwOHlJZqcAAkjUKTx%2Bl7OAG%2FmKMXaqq8OCFXg4CIvg7op0HOrcKJWkE%2Fvnfgxs5V92p69YARr%2BZHHBNy9bTwrv2e%2B0Rdu7l%2Fgd%2BLEBJ&style=eNpLyk%2BpVKjmUlAoSExJycxLt1IwLErNteaqBQBpsgf8).


## Contributing

This projects follows the [Git Flow](https://github.com/petervanderdoes/gitflow-avh) methodology to manage its branches and features. The packages and their dependencies are managed with NPM workspaces. The files are linted with ESLint, type checked with TypeScript and formatted with Prettier.
Expand Down
12 changes: 12 additions & 0 deletions packages/docs/api/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,26 @@ class Component extends Base {
options: {
stringOption: String, // default to ''
stringWithDefault: { type: String, default: 'Default value' },
stringWithDefaultAsAFunction: {
type: String,
default: (component) => component.$el.id,
},
numberOption: Number, // default to 0
numberWithDefault: { type: Number, default: 10 },
numberWithDefaultAsAFunction: {
type: Number,
default: (component) => component.$el.childElementCount,
},
booleanOption: Boolean, // default to false
// default to true, can be negated with the `data-option-no-boolean-with-default` attribute
booleanWithDefault: {
type: Boolean,
default: true,
},
booleanWithDefaultAsAFunction: {
type: Boolean,
default: (component) => component.$el.classList.has('bool'),
},
arrayOption: Array, // default to []
arrayWithDefault: { type: Array, default: () => [1, 2] },
objectOption: Object, // default to {}
Expand Down
27 changes: 27 additions & 0 deletions packages/docs/guide/introduction/managing-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ class VideoPlayer extends Base {
}
```

You can also use a callback function for the default value, it will be called with the current instance as only parameter:

```js {9-18}
import { Base } from '@studiometa/js-toolkit';

class Player extends Base {
static config = {
name: 'Player',
options: {
type: {
type: String,
default(player) {
switch (player.$el.constructor) {
case HTMLVideoElement:
return 'video';
case HTMLAudioElement:
return 'audio';
default:
return 'unknown';
}
},
},
},
};
}
```

### Merging options

When working with `Array` or `Object` as option type, it can be useful to merge the values from the `data-option-...` attribute with the default ones. You can use the `merge` property to enable merge with [`deepmerge`](https://github.com/TehShrike/deepmerge):
Expand Down
19 changes: 10 additions & 9 deletions packages/js-toolkit/Base/managers/OptionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,29 @@ type OptionType =

type ArrayOption = {
type: ArrayConstructor;
default?: () => unknown[];
default?: (instance: Base) => unknown[];
merge?: boolean | DeepmergeOptions;
};

type ObjectOption = {
type: ObjectConstructor;
default?: () => unknown;
default?: (instance: Base) => unknown;
merge?: boolean | DeepmergeOptions;
};

type StringOption = {
type: StringConstructor;
default?: string;
default?: string | ((instance: Base) => string);
};

type NumberOption = {
type: NumberConstructor;
default?: number;
default?: number | ((instance: Base) => number);
};

type BooleanOption = {
type: BooleanConstructor;
default?: boolean;
default?: boolean | ((instance: Base) => boolean);
};

export type OptionObject = ArrayOption | ObjectOption | StringOption | NumberOption | BooleanOption;
Expand Down Expand Up @@ -180,7 +180,8 @@ export class OptionsManager extends AbstractManager {
* Get an option value.
*/
get(name: string, config: OptionObject) {
const { type, default: defaultValue } = config;
const { type } = config;
const defaultValue = isFunction(config.default) ? config.default(this.__base) : config.default;
const propertyName = __getPropertyName(name);
const hasProperty = isDefined(this.__element.dataset[propertyName]);

Expand All @@ -206,12 +207,12 @@ export class OptionsManager extends AbstractManager {
config = type === Array ? (config as ArrayOption) : (config as ObjectOption);

if (!this.__values[name]) {
let val = hasProperty ? JSON.parse(value) : config.default();
let val = hasProperty ? JSON.parse(value) : defaultValue;

if (isDefined(config.merge)) {
val = isBoolean(config.merge)
? deepmerge(config.default(), val)
: deepmerge(config.default(), val, config.merge);
? deepmerge(defaultValue, val)
: deepmerge(defaultValue, val, config.merge);
}

this.__values[name] = val;
Expand Down
23 changes: 14 additions & 9 deletions packages/tests/Base/managers/OptionsManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('The Options class', () => {
foo: Map,
});
}).toThrow(
'The "foo" option has an invalid type. The allowed types are: String, Number, Boolean, Array and Object.'
'The "foo" option has an invalid type. The allowed types are: String, Number, Boolean, Array and Object.',
);
});

Expand Down Expand Up @@ -84,12 +84,9 @@ describe('The Options class', () => {
});

it('should get and set boolean options', () => {
const instance = componentWithOptions(
'<div data-option-foo></div>',
{
foo: Boolean,
},
);
const instance = componentWithOptions('<div data-option-foo></div>', {
foo: Boolean,
});

expect(instance.$options.foo).toBe(true);
instance.$options.foo = false;
Expand Down Expand Up @@ -154,7 +151,7 @@ describe('The Options class', () => {
default: () => [1, 2],
merge: true,
},
}
},
);

expect(instance.$options.foo).toEqual({ foo: 'foo', key: 'key' });
Expand All @@ -169,9 +166,13 @@ describe('The Options class', () => {
array: Array,
object: Object,
stringWithDefault: { type: String, default: 'foo' },
stringWithDefaultFn: { type: String, default: (i) => i.$id },
numberWithDefault: { type: Number, default: 10 },
numberWithDefaultFn: { type: Number, default: () => 10 },
booleanWithDefault: { type: Boolean, default: true },
booleanWithDefaultFn: { type: Boolean, default: (i) => i.$isMounted },
arrayWithDefault: { type: Array, default: () => [1, 2, 3] },
arrayWithDefaultFn: { type: Array, default: (i) => i.$options.arrayWithDefault },
objectWithDefault: { type: Object, default: () => ({ foo: 'foo' }) },
});

Expand All @@ -183,18 +184,22 @@ describe('The Options class', () => {
expect(instance.$options.object).toEqual({});

expect(instance.$options.stringWithDefault).toBe('foo');
expect(instance.$options.stringWithDefaultFn).toBe(instance.$id);
expect(instance.$options.numberWithDefault).toBe(10);
expect(instance.$options.numberWithDefaultFn).toBe(10);
expect(instance.$options.booleanWithDefault).toBe(true);
expect(instance.$options.booleanWithDefaultFn).toBe(instance.$isMounted);
expect(instance.$el.hasAttribute('data-option-boolean-with-default')).toBe(false);
expect(instance.$options.arrayWithDefault).toEqual([1, 2, 3]);
expect(instance.$options.arrayWithDefaultFn).toEqual(instance.$options.arrayWithDefault);
expect(instance.$options.objectWithDefault).toEqual({ foo: 'foo' });
});

it('should throw an error when default values for types Object or Array are not functions', () => {
expect(() =>
componentWithOptions('<div></div>', {
array: { type: Array, default: [1, 2, 3] },
})
}),
).toThrow('The default value for options of type "Array" must be returned by a function.');
});
});

0 comments on commit c6c1a7c

Please sign in to comment.