Skip to content

Commit

Permalink
feat: simplify slots
Browse files Browse the repository at this point in the history
  • Loading branch information
vladitasev authored May 20, 2019
1 parent c2eba4e commit e4907b9
Show file tree
Hide file tree
Showing 86 changed files with 558 additions and 437 deletions.
8 changes: 2 additions & 6 deletions lib/documentation/templates/api-slots-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
<h3 class="comment-api-title space-top" >Children</h3>
<p class="small-space-top" >
This Web Component accepts other HTML Elements as children.
Use the <code>data-ui5-slot</code> attribute to define the category of each child, if more than one category is accepted.
Use the <code>slot</code> attribute to define the category of each child, if more than one category is accepted.
You can provide multiple children for the categories marked with <code>[0..n]</code> or just one otherwise.
</p>
Expand All @@ -24,9 +24,5 @@ module.exports = {
{{/each}}
</div>
{{/if}}
{{#if usesTextContent}}
<h3 class="comment-api-title space-top" >Children</h3>
<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>
{{/if}}`
};
};
9 changes: 0 additions & 9 deletions lib/jsdoc/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
*
* slot
*
* usestextcontent
*
* appenddocs
*
* customtag
Expand Down Expand Up @@ -2067,13 +2065,6 @@ exports.defineTags = function(dictionary) {
}
});

dictionary.defineTag('usestextcontent', {
mustNotHaveValue: true,
onTagged: function(doclet, tag) {
doclet.usestextcontent = true;
}
});

dictionary.defineTag('appenddocs', {
mustHaveValue: false,
onTagged: function(doclet, tag) {
Expand Down
3 changes: 0 additions & 3 deletions lib/jsdoc/template/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -2657,9 +2657,6 @@ function createAPIJSON4Symbol(symbol, omitDefaults) {
if (symbol.appenddocs) {
attrib("appenddocs", symbol.appenddocs);
}
if (symbol.usestextcontent) {
attrib("usesTextContent");
}
if ( symbol.__ui5.resource ) {
attrib("resource", symbol.__ui5.resource);
}
Expand Down
10 changes: 0 additions & 10 deletions packages/base/src/State.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,6 @@ class State {
},
});
}

Object.defineProperty(proto, "_nodeText", {
get() {
return this._data._nodeText;
},
set(value) {
this._data._nodeText = value;
this._control._invalidate("_nodeText", value);
},
});
}

static generateDefaultState(MetadataClass) {
Expand Down
151 changes: 100 additions & 51 deletions packages/base/src/UI5Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,13 @@ class UI5Element extends HTMLElement {

_startObservingDOMChildren() {
const shouldObserveChildren = this.constructor.getMetadata().hasSlots();
const shouldObserveText = this.constructor.getMetadata().usesNodeText();
if (!shouldObserveChildren && !shouldObserveText) {
if (!shouldObserveChildren) {
return;
}
const mutationObserverOptions = {
childList: true,
subtree: shouldObserveText,
characterData: shouldObserveText,
subtree: true,
characterData: true,
};
DOMObserver.observeDOMNode(this, this._processChildren.bind(this), mutationObserverOptions);
}
Expand All @@ -146,56 +145,87 @@ class UI5Element extends HTMLElement {
}

_processChildren(mutations) {
const usesNodeText = this.constructor.getMetadata().usesNodeText();
const hasChildren = this.constructor.getMetadata().hasSlots();
if (usesNodeText) {
this._updateNodeText();
} else if (hasChildren) {
const hasSlots = this.constructor.getMetadata().hasSlots();
if (hasSlots) {
this._updateSlots();
}
this.onChildrenChanged(mutations);
}

_updateNodeText() {
this._state._nodeText = this.textContent;
}

_updateSlots() {
const domChildren = Array.from(this.children);

const slotsMap = this.constructor.getMetadata().getSlots();
const defaultSlot = this.constructor.getMetadata().getDefaultSlot();
const canSlotText = slotsMap[defaultSlot] !== undefined && slotsMap[defaultSlot].type === Node;

let domChildren;
if (canSlotText) {
domChildren = Array.from(this.childNodes);
} else {
domChildren = Array.from(this.children);
}

// Init the _state object based on the supported slots
for (const [prop, propData] of Object.entries(slotsMap)) { // eslint-disable-line
if (propData.multiple) {
this._state[prop] = [];
} else {
this._state[prop] = null;
}
}

const autoIncrementMap = new Map();
domChildren.forEach(child => {
const slot = child.getAttribute("data-ui5-slot") || this.constructor.getMetadata().getDefaultSlot();
if (slotsMap[slot] === undefined) {
// Determine the type of the child (mainly by the slot attribute)
const childType = this._getChildType(child);

// Check if the childType is supported
if (slotsMap[childType] === undefined) {
const validValues = Object.keys(slotsMap).join(", ");
console.warn(`Unknown data-ui5-slot value: ${slot}, ignoring`, child, `Valid data-ui5-slot values are: ${validValues}`); // eslint-disable-line
console.warn(`Unknown childType: ${childType}, ignoring`, child, `Valid values are: ${validValues}`); // eslint-disable-line
return;
}
let slotName;
if (slotsMap[slot].multiple) {
const nextId = (autoIncrementMap.get(slot) || 0) + 1;
slotName = `${slot}-${nextId}`;
autoIncrementMap.set(slot, nextId);
} else {
slotName = slot;

// For children that need individual slots, calculate them
if (slotsMap[childType].individualSlots) {
const nextId = (autoIncrementMap.get(childType) || 0) + 1;
autoIncrementMap.set(childType, nextId);
child._individualSlot = `${childType}-${nextId}`;
}
child._slot = slotName;
if (slotsMap[slot].multiple) {
this._state[slot] = [...this._state[slot], child];

// Distribute the child in the _state object
if (slotsMap[childType].multiple) {
this._state[childType] = [...this._state[childType], child];
} else {
this._state[slot] = child;
this._state[childType] = child;
}
});
}

_getChildType(child) {
const defaultSlot = this.constructor.getMetadata().getDefaultSlot();

// Text nodes can only go to the default slot
if (!(child instanceof HTMLElement)) {
return defaultSlot;
}

// Check for explicitly given logical slot
const ui5Slot = child.getAttribute("data-ui5-slot");
if (ui5Slot) {
return ui5Slot;
}

// Discover the slot based on the real slot name (f.e. footer => footer, or content-32 => content)
const slot = child.getAttribute("slot");
if (slot) {
const match = slot.match(/^([^-]+)-\d+$/);
return match ? match[1] : slot;
}

// Use default slot as a fallback
return defaultSlot;
}

static get observedAttributes() {
const observedProps = this.getMetadata().getObservedProps();
return observedProps.map(camelToKebabCase);
Expand Down Expand Up @@ -417,9 +447,43 @@ class UI5Element extends HTMLElement {
}

_assignSlotsToChildren() {
const defaultSlot = this.constructor.getMetadata().getDefaultSlot();
const domChildren = Array.from(this.children);
domChildren.filter(child => child._slot).forEach(child => {
child.setAttribute("slot", child._slot);

domChildren.forEach(child => {
const childType = this._getChildType(child);
const slot = child.getAttribute("slot");
const hasSlot = !!slot;

// Assign individual slots, f.e. items => items-1
if (child._individualSlot) {
child.setAttribute("slot", child._individualSlot);
return;
}

// If the user set a slot equal to the default slot, f.e. slot="content", remove it
// Otherwise, stop here
if (childType === defaultSlot) {
if (hasSlot) {
child.removeAttribute("slot");
}
return;
}

// Compatibility - for the ones with "data-ui5-slot"
// If they don't have a slot yet, and are not of the default child type, set childType as slot
if (!hasSlot) {
child.setAttribute("slot", childType);
}
}, this);


domChildren.filter(child => child._compatibilitySlot).forEach(child => {
const hasSlot = !!child.getAttribute("slot");
const needsSlot = child._compatibilitySlot !== defaultSlot;
if (!hasSlot && needsSlot) {
child.setAttribute("slot", child._compatibilitySlot);
}
});
}

Expand Down Expand Up @@ -533,19 +597,14 @@ class UI5Element extends HTMLElement {
}

getSlottedNodes(slotName) {
const getSlottedElement = el => {
if (el.tagName.toUpperCase() !== "SLOT") {
return el;
}

const nodes = el.assignedNodes();

if (nodes.length) {
return getSlottedElement(nodes[0]);
const reducer = (acc, curr) => {
if (curr.tagName.toUpperCase() !== "SLOT") {
return acc.concat([curr]);
}
return acc.concat(curr.assignedElements({ flatten: true }));
};

return this[slotName].map(getSlottedElement);
return this[slotName].reduce(reducer, []);
}

/**
Expand Down Expand Up @@ -602,16 +661,6 @@ class UI5Element extends HTMLElement {
},
});
}

// Node Text
Object.defineProperty(proto, "_nodeText", {
get() {
return this._state._nodeText;
},
set() {
throw new Error("Cannot set node text directly, use the DOM APIs");
},
});
}
}
const kebabToCamelCase = string => toCamelCase(string.split("-"));
Expand Down
28 changes: 19 additions & 9 deletions packages/base/src/UI5ElementMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ class UI5ElementMetadata {
return this.metadata.noShadowDOM;
}

usesNodeText() {
return !!this.metadata.usesNodeText;
}

getDefaultSlot() {
return this.metadata.defaultSlot || "content";
}
Expand Down Expand Up @@ -92,14 +88,28 @@ const validateSingleProperty = (value, propData) => {
};

const validateSingleSlot = (value, propData) => {
const getSlottedElement = el => {
return el.tagName.toUpperCase() !== "SLOT" ? el : getSlottedElement(el.assignedNodes()[0]);
if (value === null) {
return value;
}

const getSlottedNodes = el => {
const isTag = el instanceof HTMLElement;
const isSlot = isTag && el.tagName.toUpperCase() === "SLOT";

if (isSlot) {
return el.assignedElements({ flatten: true });
}

return [el];
};
const propertyType = propData.type;

if (value !== null && !(getSlottedElement(value) instanceof propertyType)) {
throw new Error(`${value} is not of type ${propertyType}`);
}
const slottedNodes = getSlottedNodes(value);
slottedNodes.forEach(el => {
if (!(el instanceof propertyType)) {
throw new Error(`${el} is not of type ${propertyType}`);
}
});

return value;
};
Expand Down
6 changes: 4 additions & 2 deletions packages/main/src/Button.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
></ui5-icon>
{{/if}}

{{#if ctr._nodeText}}
{{#if ctr.text.length}}
<span id="{{ctr._id}}-content" dir="{{dir}}" class="{{classes.text}}">
<bdi>{{ctr._nodeText}}</bdi>
<bdi>
<slot></slot>
</bdi>
</span>
{{/if}}
</button>
Expand Down
17 changes: 15 additions & 2 deletions packages/main/src/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import "./ThemePropertiesProvider.js";
*/
const metadata = {
tag: "ui5-button",
usesNodeText: true,
properties: /** @lends sap.ui.webcomponents.main.Button.prototype */ {

/**
Expand Down Expand Up @@ -106,6 +105,21 @@ const metadata = {

_iconSettings: { type: Object },
},
slots: /** @lends sap.ui.webcomponents.main.Button.prototype */ {
/**
* Defines the text of the <code>ui5-button</code>.
* <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.
*
* @type {Node[]}
* @slot
* @public
*/
text: {
type: Node,
multiple: true,
},
},
defaultSlot: "text",
events: /** @lends sap.ui.webcomponents.main.Button.prototype */ {

/**
Expand Down Expand Up @@ -154,7 +168,6 @@ const metadata = {
* @alias sap.ui.webcomponents.main.Button
* @extends UI5Element
* @tagname ui5-button
* @usestextcontent
* @public
*/
class Button extends UI5Element {
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/ButtonTemplateContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ButtonTemplateContext {
sapMBtn: true,
sapMBtnActive: state._active,
sapMBtnWithIcon: state.icon,
sapMBtnNoText: !state._nodeText,
sapMBtnNoText: !state.text.length,
sapMBtnDisabled: state.disabled,
sapMBtnIconEnd: state.iconEnd,
[`sapMBtn${state.type}`]: true,
Expand Down
Loading

0 comments on commit e4907b9

Please sign in to comment.