Skip to content

Commit e4907b9

Browse files
authored
feat: simplify slots
1 parent c2eba4e commit e4907b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+558
-437
lines changed

lib/documentation/templates/api-slots-section.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = {
44
<h3 class="comment-api-title space-top" >Children</h3>
55
<p class="small-space-top" >
66
This Web Component accepts other HTML Elements as children.
7-
Use the <code>data-ui5-slot</code> attribute to define the category of each child, if more than one category is accepted.
7+
Use the <code>slot</code> attribute to define the category of each child, if more than one category is accepted.
88
You can provide multiple children for the categories marked with <code>[0..n]</code> or just one otherwise.
99
</p>
1010
@@ -24,9 +24,5 @@ module.exports = {
2424
{{/each}}
2525
2626
</div>
27-
{{/if}}
28-
{{#if usesTextContent}}
29-
<h3 class="comment-api-title space-top" >Children</h3>
30-
<p class="small-space-top" >The content of this Web Component is interpreted as text and shown to the user. All HTML Elements inside the Web Component are ignored - only their text is used.</p>
3127
{{/if}}`
32-
};
28+
};

lib/jsdoc/plugin.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
*
2828
* slot
2929
*
30-
* usestextcontent
31-
*
3230
* appenddocs
3331
*
3432
* customtag
@@ -2067,13 +2065,6 @@ exports.defineTags = function(dictionary) {
20672065
}
20682066
});
20692067

2070-
dictionary.defineTag('usestextcontent', {
2071-
mustNotHaveValue: true,
2072-
onTagged: function(doclet, tag) {
2073-
doclet.usestextcontent = true;
2074-
}
2075-
});
2076-
20772068
dictionary.defineTag('appenddocs', {
20782069
mustHaveValue: false,
20792070
onTagged: function(doclet, tag) {

lib/jsdoc/template/publish.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2657,9 +2657,6 @@ function createAPIJSON4Symbol(symbol, omitDefaults) {
26572657
if (symbol.appenddocs) {
26582658
attrib("appenddocs", symbol.appenddocs);
26592659
}
2660-
if (symbol.usestextcontent) {
2661-
attrib("usesTextContent");
2662-
}
26632660
if ( symbol.__ui5.resource ) {
26642661
attrib("resource", symbol.__ui5.resource);
26652662
}

packages/base/src/State.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,6 @@ class State {
117117
},
118118
});
119119
}
120-
121-
Object.defineProperty(proto, "_nodeText", {
122-
get() {
123-
return this._data._nodeText;
124-
},
125-
set(value) {
126-
this._data._nodeText = value;
127-
this._control._invalidate("_nodeText", value);
128-
},
129-
});
130120
}
131121

132122
static generateDefaultState(MetadataClass) {

packages/base/src/UI5Element.js

Lines changed: 100 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,13 @@ class UI5Element extends HTMLElement {
126126

127127
_startObservingDOMChildren() {
128128
const shouldObserveChildren = this.constructor.getMetadata().hasSlots();
129-
const shouldObserveText = this.constructor.getMetadata().usesNodeText();
130-
if (!shouldObserveChildren && !shouldObserveText) {
129+
if (!shouldObserveChildren) {
131130
return;
132131
}
133132
const mutationObserverOptions = {
134133
childList: true,
135-
subtree: shouldObserveText,
136-
characterData: shouldObserveText,
134+
subtree: true,
135+
characterData: true,
137136
};
138137
DOMObserver.observeDOMNode(this, this._processChildren.bind(this), mutationObserverOptions);
139138
}
@@ -146,56 +145,87 @@ class UI5Element extends HTMLElement {
146145
}
147146

148147
_processChildren(mutations) {
149-
const usesNodeText = this.constructor.getMetadata().usesNodeText();
150-
const hasChildren = this.constructor.getMetadata().hasSlots();
151-
if (usesNodeText) {
152-
this._updateNodeText();
153-
} else if (hasChildren) {
148+
const hasSlots = this.constructor.getMetadata().hasSlots();
149+
if (hasSlots) {
154150
this._updateSlots();
155151
}
156152
this.onChildrenChanged(mutations);
157153
}
158154

159-
_updateNodeText() {
160-
this._state._nodeText = this.textContent;
161-
}
162-
163155
_updateSlots() {
164-
const domChildren = Array.from(this.children);
165-
166156
const slotsMap = this.constructor.getMetadata().getSlots();
157+
const defaultSlot = this.constructor.getMetadata().getDefaultSlot();
158+
const canSlotText = slotsMap[defaultSlot] !== undefined && slotsMap[defaultSlot].type === Node;
159+
160+
let domChildren;
161+
if (canSlotText) {
162+
domChildren = Array.from(this.childNodes);
163+
} else {
164+
domChildren = Array.from(this.children);
165+
}
166+
167+
// Init the _state object based on the supported slots
167168
for (const [prop, propData] of Object.entries(slotsMap)) { // eslint-disable-line
168169
if (propData.multiple) {
169170
this._state[prop] = [];
170171
} else {
171172
this._state[prop] = null;
172173
}
173174
}
175+
174176
const autoIncrementMap = new Map();
175177
domChildren.forEach(child => {
176-
const slot = child.getAttribute("data-ui5-slot") || this.constructor.getMetadata().getDefaultSlot();
177-
if (slotsMap[slot] === undefined) {
178+
// Determine the type of the child (mainly by the slot attribute)
179+
const childType = this._getChildType(child);
180+
181+
// Check if the childType is supported
182+
if (slotsMap[childType] === undefined) {
178183
const validValues = Object.keys(slotsMap).join(", ");
179-
console.warn(`Unknown data-ui5-slot value: ${slot}, ignoring`, child, `Valid data-ui5-slot values are: ${validValues}`); // eslint-disable-line
184+
console.warn(`Unknown childType: ${childType}, ignoring`, child, `Valid values are: ${validValues}`); // eslint-disable-line
180185
return;
181186
}
182-
let slotName;
183-
if (slotsMap[slot].multiple) {
184-
const nextId = (autoIncrementMap.get(slot) || 0) + 1;
185-
slotName = `${slot}-${nextId}`;
186-
autoIncrementMap.set(slot, nextId);
187-
} else {
188-
slotName = slot;
187+
188+
// For children that need individual slots, calculate them
189+
if (slotsMap[childType].individualSlots) {
190+
const nextId = (autoIncrementMap.get(childType) || 0) + 1;
191+
autoIncrementMap.set(childType, nextId);
192+
child._individualSlot = `${childType}-${nextId}`;
189193
}
190-
child._slot = slotName;
191-
if (slotsMap[slot].multiple) {
192-
this._state[slot] = [...this._state[slot], child];
194+
195+
// Distribute the child in the _state object
196+
if (slotsMap[childType].multiple) {
197+
this._state[childType] = [...this._state[childType], child];
193198
} else {
194-
this._state[slot] = child;
199+
this._state[childType] = child;
195200
}
196201
});
197202
}
198203

204+
_getChildType(child) {
205+
const defaultSlot = this.constructor.getMetadata().getDefaultSlot();
206+
207+
// Text nodes can only go to the default slot
208+
if (!(child instanceof HTMLElement)) {
209+
return defaultSlot;
210+
}
211+
212+
// Check for explicitly given logical slot
213+
const ui5Slot = child.getAttribute("data-ui5-slot");
214+
if (ui5Slot) {
215+
return ui5Slot;
216+
}
217+
218+
// Discover the slot based on the real slot name (f.e. footer => footer, or content-32 => content)
219+
const slot = child.getAttribute("slot");
220+
if (slot) {
221+
const match = slot.match(/^([^-]+)-\d+$/);
222+
return match ? match[1] : slot;
223+
}
224+
225+
// Use default slot as a fallback
226+
return defaultSlot;
227+
}
228+
199229
static get observedAttributes() {
200230
const observedProps = this.getMetadata().getObservedProps();
201231
return observedProps.map(camelToKebabCase);
@@ -417,9 +447,43 @@ class UI5Element extends HTMLElement {
417447
}
418448

419449
_assignSlotsToChildren() {
450+
const defaultSlot = this.constructor.getMetadata().getDefaultSlot();
420451
const domChildren = Array.from(this.children);
421-
domChildren.filter(child => child._slot).forEach(child => {
422-
child.setAttribute("slot", child._slot);
452+
453+
domChildren.forEach(child => {
454+
const childType = this._getChildType(child);
455+
const slot = child.getAttribute("slot");
456+
const hasSlot = !!slot;
457+
458+
// Assign individual slots, f.e. items => items-1
459+
if (child._individualSlot) {
460+
child.setAttribute("slot", child._individualSlot);
461+
return;
462+
}
463+
464+
// If the user set a slot equal to the default slot, f.e. slot="content", remove it
465+
// Otherwise, stop here
466+
if (childType === defaultSlot) {
467+
if (hasSlot) {
468+
child.removeAttribute("slot");
469+
}
470+
return;
471+
}
472+
473+
// Compatibility - for the ones with "data-ui5-slot"
474+
// If they don't have a slot yet, and are not of the default child type, set childType as slot
475+
if (!hasSlot) {
476+
child.setAttribute("slot", childType);
477+
}
478+
}, this);
479+
480+
481+
domChildren.filter(child => child._compatibilitySlot).forEach(child => {
482+
const hasSlot = !!child.getAttribute("slot");
483+
const needsSlot = child._compatibilitySlot !== defaultSlot;
484+
if (!hasSlot && needsSlot) {
485+
child.setAttribute("slot", child._compatibilitySlot);
486+
}
423487
});
424488
}
425489

@@ -533,19 +597,14 @@ class UI5Element extends HTMLElement {
533597
}
534598

535599
getSlottedNodes(slotName) {
536-
const getSlottedElement = el => {
537-
if (el.tagName.toUpperCase() !== "SLOT") {
538-
return el;
539-
}
540-
541-
const nodes = el.assignedNodes();
542-
543-
if (nodes.length) {
544-
return getSlottedElement(nodes[0]);
600+
const reducer = (acc, curr) => {
601+
if (curr.tagName.toUpperCase() !== "SLOT") {
602+
return acc.concat([curr]);
545603
}
604+
return acc.concat(curr.assignedElements({ flatten: true }));
546605
};
547606

548-
return this[slotName].map(getSlottedElement);
607+
return this[slotName].reduce(reducer, []);
549608
}
550609

551610
/**
@@ -602,16 +661,6 @@ class UI5Element extends HTMLElement {
602661
},
603662
});
604663
}
605-
606-
// Node Text
607-
Object.defineProperty(proto, "_nodeText", {
608-
get() {
609-
return this._state._nodeText;
610-
},
611-
set() {
612-
throw new Error("Cannot set node text directly, use the DOM APIs");
613-
},
614-
});
615664
}
616665
}
617666
const kebabToCamelCase = string => toCamelCase(string.split("-"));

packages/base/src/UI5ElementMetadata.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ class UI5ElementMetadata {
1414
return this.metadata.noShadowDOM;
1515
}
1616

17-
usesNodeText() {
18-
return !!this.metadata.usesNodeText;
19-
}
20-
2117
getDefaultSlot() {
2218
return this.metadata.defaultSlot || "content";
2319
}
@@ -92,14 +88,28 @@ const validateSingleProperty = (value, propData) => {
9288
};
9389

9490
const validateSingleSlot = (value, propData) => {
95-
const getSlottedElement = el => {
96-
return el.tagName.toUpperCase() !== "SLOT" ? el : getSlottedElement(el.assignedNodes()[0]);
91+
if (value === null) {
92+
return value;
93+
}
94+
95+
const getSlottedNodes = el => {
96+
const isTag = el instanceof HTMLElement;
97+
const isSlot = isTag && el.tagName.toUpperCase() === "SLOT";
98+
99+
if (isSlot) {
100+
return el.assignedElements({ flatten: true });
101+
}
102+
103+
return [el];
97104
};
98105
const propertyType = propData.type;
99106

100-
if (value !== null && !(getSlottedElement(value) instanceof propertyType)) {
101-
throw new Error(`${value} is not of type ${propertyType}`);
102-
}
107+
const slottedNodes = getSlottedNodes(value);
108+
slottedNodes.forEach(el => {
109+
if (!(el instanceof propertyType)) {
110+
throw new Error(`${el} is not of type ${propertyType}`);
111+
}
112+
});
103113

104114
return value;
105115
};

packages/main/src/Button.hbs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
></ui5-icon>
1717
{{/if}}
1818

19-
{{#if ctr._nodeText}}
19+
{{#if ctr.text.length}}
2020
<span id="{{ctr._id}}-content" dir="{{dir}}" class="{{classes.text}}">
21-
<bdi>{{ctr._nodeText}}</bdi>
21+
<bdi>
22+
<slot></slot>
23+
</bdi>
2224
</span>
2325
{{/if}}
2426
</button>

packages/main/src/Button.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import "./ThemePropertiesProvider.js";
1919
*/
2020
const metadata = {
2121
tag: "ui5-button",
22-
usesNodeText: true,
2322
properties: /** @lends sap.ui.webcomponents.main.Button.prototype */ {
2423

2524
/**
@@ -106,6 +105,21 @@ const metadata = {
106105

107106
_iconSettings: { type: Object },
108107
},
108+
slots: /** @lends sap.ui.webcomponents.main.Button.prototype */ {
109+
/**
110+
* Defines the text of the <code>ui5-button</code>.
111+
* <br><b>Note:</b> Аlthough this slot accepts HTML Elements, it is strongly recommended that you only use text in order to preserve the intended design.
112+
*
113+
* @type {Node[]}
114+
* @slot
115+
* @public
116+
*/
117+
text: {
118+
type: Node,
119+
multiple: true,
120+
},
121+
},
122+
defaultSlot: "text",
109123
events: /** @lends sap.ui.webcomponents.main.Button.prototype */ {
110124

111125
/**
@@ -154,7 +168,6 @@ const metadata = {
154168
* @alias sap.ui.webcomponents.main.Button
155169
* @extends UI5Element
156170
* @tagname ui5-button
157-
* @usestextcontent
158171
* @public
159172
*/
160173
class Button extends UI5Element {

packages/main/src/ButtonTemplateContext.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ButtonTemplateContext {
2222
sapMBtn: true,
2323
sapMBtnActive: state._active,
2424
sapMBtnWithIcon: state.icon,
25-
sapMBtnNoText: !state._nodeText,
25+
sapMBtnNoText: !state.text.length,
2626
sapMBtnDisabled: state.disabled,
2727
sapMBtnIconEnd: state.iconEnd,
2828
[`sapMBtn${state.type}`]: true,

0 commit comments

Comments
 (0)