Skip to content

Commit 90f280a

Browse files
committed
#5700 vdom example, more content
1 parent 10aba22 commit 90f280a

1 file changed

Lines changed: 116 additions & 17 deletions

File tree

learn/guides/CustomComponents.md

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,15 @@ In the example below, we create `MySpecialButton` by extending `Neo.button.Base`
7575

7676
## Introducing New Configs
7777

78-
You can also add entirely new configuration properties to your custom components. Simply add them to the `static config`
79-
block with a default value. Neo.mjs will automatically generate a getter and a setter for your new config, and you can
80-
use it to control the behavior of your component.
78+
You can also add entirely new configuration properties to your custom components. To make a config "reactive" – meaning
79+
it automatically triggers a lifecycle method when its value changes – you **must** define it with a trailing underscore (`_`).
8180

82-
For example, if we wanted our `MySpecialButton` to have a `specialEffect` config, we could add
83-
`specialEffect: 'glow'` to its config block.
81+
For a reactive config like `myConfig_`, the framework provides this behavior:
82+
- **Reading**: You can access the value directly: `this.myConfig`.
83+
- **Writing**: Assigning a new value (`this.myConfig = 'new value'`) triggers a prototype-based setter. This is the core of Neo.mjs reactivity.
84+
- **Hook**: After a value is set, the `afterSetMyConfig(value, oldValue)` method is automatically called.
85+
86+
If you define a config without the trailing underscore, it will simply be a static property on the class instance and will not trigger any lifecycle methods.
8487

8588
## Example: A Custom Button
8689

@@ -101,17 +104,14 @@ class MySpecialButton extends Button {
101104
iconCls: 'far fa-face-grin-wide',
102105
ui: 'ghost',
103106

104-
// c. Add a new custom config
105-
specialText: 'I am special'
107+
// c. Add a new reactive config (note the trailing underscore)
108+
specialText_: 'I am special'
106109
}
107110

108111
// d. Hook into the component lifecycle
109-
onConstructed() {
110-
// Call the superclass method first
111-
super.onConstructed();
112-
113-
// Access our new config property
114-
console.log(this.specialText);
112+
afterSetSpecialText(value, oldValue) {
113+
// This method is called automatically when `specialText` is changed.
114+
console.log(`specialText changed from "${oldValue}" to "${value}"`);
115115
}
116116
}
117117

@@ -147,9 +147,108 @@ Neo.setupClass(MainView);
147147
1. **Class Definition**: We define `MySpecialButton` which `extends` the framework's `Button` class.
148148
2. **`className`**: We give our new class a unique `className`. This is important for the framework's class system.
149149
3. **Overridden Configs**: We change the default `iconCls` and `ui` to style our button differently.
150-
4. **New Config**: We add a `specialText` config. While this example doesn't use it to change the button's
151-
appearance, the `onConstructed` method shows how you can access its value.
152-
5. **Lifecycle Method**: We use `onConstructed`, which fires after the component is created, to log the value of our
153-
new config. We call `super.onConstructed()` to ensure the parent class's logic is executed.
150+
4. **New Reactive Config**: We add a `specialText_` config. The trailing underscore makes it reactive.
151+
5. **Lifecycle Method**: We use `afterSetSpecialText()` to automatically run logic when the config changes. We also use
152+
`onConstructed()` to run logic once when the component is created.
154153
6. **Usage**: We use `MySpecialButton` in the `items` array of our `MainView` just like any other component, by
155154
referencing its `module`.
155+
156+
## Extending `Component.Base`: Building VDom from Scratch
157+
158+
While extending specialized components like `Button` or `Container` is common for adding features (acting like a
159+
Higher-Order Component), there are times when you need to define a component's HTML structure from the ground up. For
160+
this, you extend the generic `Neo.component.Base`.
161+
162+
When you extend `component.Base`, you are responsible for defining the component's entire virtual DOM (VDom) structure
163+
using the `vdom` config. This gives you complete control over the rendered output.
164+
165+
### Example: A Simple Profile Badge
166+
167+
Let's create a `ProfileBadge` component that displays a user's name and an online status indicator.
168+
169+
```javascript live-preview
170+
import Component from '../component/Base.mjs';
171+
import Container from '../container/Base.mjs';
172+
import NeoArray from '../util/Array.mjs';
173+
import VdomUtil from '../util/Vdom.mjs';
174+
175+
// 1. Extend the generic Component.Base
176+
class ProfileBadge extends Component {
177+
static config = {
178+
className: 'Example.view.ProfileBadge',
179+
ntype : 'profile-badge',
180+
181+
// a. Define the VDom from scratch
182+
vdom: {
183+
cls: ['profile-badge'],
184+
cn: [
185+
{tag: 'span', cls: ['status-indicator'], flag: 'statusNode'},
186+
{tag: 'span', cls: ['username'], flag: 'usernameNode'}
187+
]
188+
},
189+
190+
// b. Add new reactive configs to control the component (note the trailing underscore)
191+
online_: false,
192+
username_: 'Guest'
193+
}
194+
195+
// d. Define getters for easy access to flagged VDom nodes
196+
get statusNode() {
197+
return VdomUtil.getByFlag(this, 'statusNode');
198+
}
199+
200+
get usernameNode() {
201+
return VdomUtil.getByFlag(this, 'usernameNode');
202+
}
203+
204+
// c. Use lifecycle methods to react to config changes
205+
afterSetOnline(value, oldValue) {
206+
// Access the VDom node via the getter
207+
NeoArray.toggle(this.statusNode.cls, 'online', value);
208+
this.update(); // Trigger a VDom update
209+
}
210+
211+
afterSetUsername(value, oldValue) {
212+
this.usernameNode.text = value;
213+
this.update();
214+
}
215+
}
216+
217+
Neo.setupClass(ProfileBadge);
218+
219+
220+
// 2. Use the new component
221+
class MainView extends Container {
222+
static config = {
223+
className: 'Example.view.MainView',
224+
layout : {ntype: 'vbox', align: 'start'},
225+
items : [{
226+
module: ProfileBadge,
227+
username: 'Alice',
228+
online: true
229+
}, {
230+
module: ProfileBadge,
231+
username: 'Bob',
232+
online: false,
233+
style: {marginTop: '10px'}
234+
}]
235+
}
236+
}
237+
238+
Neo.setupClass(MainView);
239+
```
240+
241+
### Key Differences in this Approach:
242+
243+
1. **Base Class**: We extend `Neo.component.Base` because we are not inheriting complex logic like a `Button` or
244+
`Container`.
245+
2. **`vdom` Config**: We define the entire HTML structure inside the `vdom` config. We use `flag`s (`statusNode`,
246+
`usernameNode`) to easily reference these VDom nodes later.
247+
3. **Lifecycle Methods**: We use `afterSet...` methods to react to changes in our custom `online_` and `username_`
248+
configs. Inside these methods, we directly manipulate the properties of our VDom nodes and then call `this.update()`
249+
to apply the changes to the real DOM.
250+
251+
This approach gives you maximum control, but it also means you are responsible for building the structure yourself.
252+
253+
For a deeper dive into advanced VDom manipulation, including performance best practices and security, please refer to the
254+
[Working with VDom guide](guides.WorkingWithVDom).

0 commit comments

Comments
 (0)