Skip to content

Commit ef78b5f

Browse files
committed
Refactor defineComponent and Enhance Config System Documentation #7019
1 parent 65f9c3c commit ef78b5f

3 files changed

Lines changed: 86 additions & 15 deletions

File tree

src/Neo.mjs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,33 @@ Neo = globalThis.Neo = Object.assign({
496496
*/
497497

498498
/**
499-
* Internally used at the end of each class / module definition
499+
* This is the final and most critical step in the Neo.mjs class creation process.
500+
* It is called at the end of every class module definition.
501+
*
502+
* `setupClass` performs several key operations:
503+
* 1. **Configuration Merging:** It traverses the prototype chain to merge `static config`
504+
* objects from parent classes into the current class, creating a unified `config`.
505+
* 2. **Applying Overwrites:** It calls the static `applyOverwrites()` method on the class,
506+
* allowing the global `Neo.overwrites` object to modify the class's default prototype
507+
* configs. This is a key mechanism for external theming and configuration.
508+
* 3. **Reactive Getter/Setter Generation:** For any config ending with an underscore (e.g., `myConfig_`),
509+
* it automatically generates the corresponding public getter and setter. This enables optional
510+
* lifecycle hooks that are called automatically if implemented on the class:
511+
* - `beforeGetMyConfig(value)`
512+
* - `beforeSetMyConfig(newValue, oldValue)`
513+
* - `afterSetMyConfig(newValue, oldValue)`
514+
* 4. **Prototype-based Configs:** Non-reactive configs (without an underscore) are set
515+
* directly on the prototype for memory efficiency.
516+
* 5. **Mixin Application:** It processes the `mixins` config to blend in functionality from
517+
* other classes.
518+
* 6. **Namespace Registration:** It registers the class in the global `Neo` namespace.
519+
* 7. **Singleton Instantiation:** If the class is configured as a singleton, it creates the
520+
* single instance.
521+
*
500522
* @memberOf module:Neo
501523
* @template T
502-
* @param {T} cls
503-
* @returns {T}
524+
* @param {T} cls The class constructor to process.
525+
* @returns {T} The processed and finalized class constructor or singleton instance.
504526
*/
505527
setupClass(cls) {
506528
let baseConfig = null,

src/core/Base.mjs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,26 @@ class Base {
5858
*/
5959
static overwrittenMethods = {}
6060
/**
61-
* Configs will get merged throughout the class hierarchy
61+
* Defines the default configuration properties for the class. These configurations are
62+
* merged throughout the class hierarchy and can be overridden at the instance level.
63+
*
64+
* There are two main types of configs:
65+
*
66+
* 1. **Reactive Configs:** Property names ending with a trailing underscore (e.g., `myConfig_`).
67+
* The framework automatically generates a public getter and setter, removing the underscore
68+
* from the property name (e.g., `this.myConfig`). This system enables powerful, optional
69+
* lifecycle hooks that are called automatically if they are implemented on the class:
70+
* - `beforeGetMyConfig(value)`: Executed before the getter returns. Can be used to dynamically modify the returned value.
71+
* - `beforeSetMyConfig(newValue, oldValue)`: Executed before a new value is set. Can be used for validation or transformation. Returning `undefined` from this hook will cancel the update.
72+
* - `afterSetMyConfig(newValue, oldValue)`: Executed after a new value has been successfully set. Ideal for triggering side effects.
73+
*
74+
* 2. **Non-Reactive (Prototype-based) Configs:** Property names without a trailing underscore.
75+
* These are applied directly to the class's **prototype** during the `Neo.setupClass`
76+
* process. This is highly memory-efficient as the value is shared across all instances.
77+
* It also allows for powerful, application-wide modifications of default behaviors
78+
* by using the `Neo.overwrites` mechanism, which modifies these prototype values at
79+
* load time.
80+
*
6281
* @returns {Object} config
6382
*/
6483
static config = {
@@ -290,9 +309,38 @@ class Base {
290309
}
291310

292311
/**
293-
* Applying overwrites and adding overwrittenMethods to the class constructors
294-
* @param {Object} cfg
312+
* This static method is called by `Neo.setupClass()` during the class creation process.
313+
* It allows for modifying a class's default prototype-based configs from outside the
314+
* class hierarchy, which is a powerful way to avoid boilerplate code.
315+
*
316+
* It looks for a matching entry in the global `Neo.overwrites` object based on the
317+
* class's `className`. If found, it merges the properties from the overwrite object
318+
* into the class's static `config`. This provides a powerful mechanism for theming
319+
* or applying application-wide customizations to framework or library classes without
320+
* needing to extend them.
321+
*
322+
* @example
323+
* // Imagine you have hundreds of buttons in your app, and you want all of them
324+
* // to have `labelPosition: 'top'` instead of the default `'left'`.
325+
* // Instead of configuring each instance, you can define an overwrite.
326+
*
327+
* // inside an Overwrites.mjs file loaded by your app:
328+
* Neo.overwrites = {
329+
* Neo: {
330+
* button: {
331+
* Base: {
332+
* labelPosition: 'top'
333+
* }
334+
* }
335+
* }
336+
* };
337+
*
338+
* // Now, every `Neo.button.Base` (and any class that extends it) will have this
339+
* // new default value on its prototype.
340+
*
341+
* @param {Object} cfg The static `config` object of the class being processed.
295342
* @protected
343+
* @static
296344
*/
297345
static applyOverwrites(cfg) {
298346
let overwrites = Neo.ns(cfg.className, false, Neo.overwrites),

src/functional/defineComponent.mjs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import FunctionalBase from './component/Base.mjs';
1313
* import { useConfig } from 'neo/functional/useConfig.mjs';
1414
*
1515
* const MyComponent = defineComponent({
16-
* // 1. Static properties are defined first for a clear mental model.
17-
* // This object is directly used as the class's `static config`.
18-
* static: {
16+
* // 1. The `config` object directly defines the class's `static config`.
17+
* // This is where you set the className, ntype, and all default values
18+
* // for your component's reactive (e.g., `text_`) and non-reactive (e.g., `className`) configs.
19+
* config: {
1920
* className: 'MyApp.MyFunctionalComponent',
2021
* ntype : 'my-functional-component',
2122
*
@@ -24,7 +25,7 @@ import FunctionalBase from './component/Base.mjs';
2425
* text_: 'Hello World'
2526
* },
2627
*
27-
* // 2. Methods (instance logic) follow the static definition.
28+
* // 2. Methods (instance logic) follow the config definition.
2829
* createVdom(config) {
2930
* const [count, setCount] = useConfig(0);
3031
*
@@ -59,16 +60,16 @@ import FunctionalBase from './component/Base.mjs';
5960
* // });
6061
*/
6162
export function defineComponent(spec) {
62-
const staticSpec = spec.static;
63-
delete spec.static;
63+
const configSpec = spec.config;
64+
delete spec.config;
6465

65-
if (!staticSpec?.className) {
66-
throw new Error('defineComponent requires a static config with a className.');
66+
if (!configSpec?.className) {
67+
throw new Error('defineComponent requires a config object with a className.');
6768
}
6869

6970
class FunctionalComponent extends FunctionalBase {
7071
static config = {
71-
...staticSpec
72+
...configSpec
7273
// We can add our own configurations here
7374
}
7475
}

0 commit comments

Comments
 (0)