mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-02-22 18:42:09 +02:00
Examples refactoring
This commit is contained in:
parent
31e9a55e6c
commit
131b99e5df
File diff suppressed because one or more lines are too long
@ -1,79 +1,72 @@
|
|||||||
class CustomOfferList extends ourCoffeeSdk.OfferListComponent {
|
const {
|
||||||
constructor(
|
SearchBox,
|
||||||
context,
|
SearchBoxComposer,
|
||||||
container,
|
OfferListComponent,
|
||||||
offerList,
|
dummyCoffeeApi,
|
||||||
options
|
util
|
||||||
) {
|
} = ourCoffeeSdk;
|
||||||
|
|
||||||
|
class CustomOfferList extends OfferListComponent {
|
||||||
|
constructor(context, container, offerList, options) {
|
||||||
super(context, container, offerList, options);
|
super(context, container, offerList, options);
|
||||||
this.onOfferButtonClickListener = (e) => {
|
|
||||||
const action = e.target.dataset.action;
|
this.onClickListener = (e) => {
|
||||||
const offerId = e.target.dataset.offerId;
|
const { target, value: action } = util.findDataField(
|
||||||
if (action === "offerSelect") {
|
e.target,
|
||||||
this.events.emit(action, { offerId });
|
"action"
|
||||||
} else if (action === "createOffer") {
|
);
|
||||||
const offer =
|
if (target === null || action === null) {
|
||||||
this.context.findOfferById(offerId);
|
return;
|
||||||
if (offer) {
|
}
|
||||||
this.context.events.emit(
|
const { target: container, value: offerId } =
|
||||||
"createOrder",
|
util.findDataField(target, "offerId");
|
||||||
{
|
if (container === null || offerId === null) {
|
||||||
offer
|
return;
|
||||||
}
|
}
|
||||||
);
|
switch (action) {
|
||||||
}
|
case "expand":
|
||||||
|
this.expand(container);
|
||||||
|
break;
|
||||||
|
case "collapse":
|
||||||
|
this.collapse(container);
|
||||||
|
break;
|
||||||
|
case "createOrder":
|
||||||
|
this.context.createOrder(offerId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expand(item) {
|
||||||
|
item.classList.add("expanded");
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse(item) {
|
||||||
|
item.classList.remove("expanded");
|
||||||
|
}
|
||||||
|
|
||||||
generateOfferHtml(offer) {
|
generateOfferHtml(offer) {
|
||||||
return ourCoffeeSdk.util.html`<li
|
return util.html`<li
|
||||||
class="custom-offer"
|
class="custom-offer"
|
||||||
|
data-offer-id="${util.attrValue(offer.offerId)}"
|
||||||
>
|
>
|
||||||
<aside><button
|
<button data-action="expand" class="preview"><aside class="expand-action">></aside><strong>${
|
||||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
offer.title
|
||||||
offer.offerId
|
}</strong> · ${offer.price.formattedValue}</button>
|
||||||
)}"
|
<div class="fullview">
|
||||||
data-action="createOffer">Buy now for ${
|
<button data-action="collapse" class="collapse-action">∧</button>
|
||||||
offer.price.formattedValue
|
<div><strong>${offer.title}</strong> · ${
|
||||||
}</button><button
|
offer.price.formattedValue
|
||||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
}</div>
|
||||||
offer.offerId
|
<div>${offer.subtitle}</div>
|
||||||
)}"
|
<div>${offer.bottomLine}</div>
|
||||||
data-action="offerSelect">View details
|
<button data-action="createOrder" class="action-button">Place an Order</button>
|
||||||
</button></aside>
|
</div>
|
||||||
<div><strong>${offer.title}</strong></div>
|
|
||||||
<div>${offer.subtitle}</div>
|
|
||||||
<div>${offer.bottomLine}</div>
|
|
||||||
</li>`.toString();
|
</li>`.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDomListeners() {
|
|
||||||
const buttons =
|
|
||||||
this.container.querySelectorAll("button");
|
|
||||||
for (const button of buttons) {
|
|
||||||
button.addEventListener(
|
|
||||||
"click",
|
|
||||||
this.onOfferButtonClickListener
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
teardownDomListeners() {
|
|
||||||
const buttons = document.querySelectorAll(
|
|
||||||
this.container,
|
|
||||||
"button"
|
|
||||||
);
|
|
||||||
for (const button of buttons) {
|
|
||||||
button.removeEventListener(
|
|
||||||
"click",
|
|
||||||
this.onOfferButtonClickListener
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
class CustomComposer extends SearchBoxComposer {
|
||||||
buildOfferListComponent(
|
buildOfferListComponent(
|
||||||
context,
|
context,
|
||||||
container,
|
container,
|
||||||
@ -83,24 +76,15 @@ class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
|||||||
return new CustomOfferList(
|
return new CustomOfferList(
|
||||||
context,
|
context,
|
||||||
container,
|
container,
|
||||||
this.generateOfferPreviews(
|
this.generateOfferPreviews(offerList, contextOptions),
|
||||||
offerList,
|
this.generateOfferListComponentOptions(contextOptions)
|
||||||
contextOptions
|
|
||||||
),
|
|
||||||
this.generateOfferListComponentOptions(
|
|
||||||
contextOptions
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
class CustomSearchBox extends SearchBox {
|
||||||
buildComposer(context, container, options) {
|
buildComposer(context, container, options) {
|
||||||
return new CustomComposer(
|
return new CustomComposer(context, container, options);
|
||||||
context,
|
|
||||||
container,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createOrder(offer) {
|
createOrder(offer) {
|
||||||
@ -111,6 +95,6 @@ class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
|||||||
|
|
||||||
const searchBox = new CustomSearchBox(
|
const searchBox = new CustomSearchBox(
|
||||||
document.getElementById("search-box"),
|
document.getElementById("search-box"),
|
||||||
ourCoffeeSdk.dummyCoffeeApi
|
dummyCoffeeApi
|
||||||
);
|
);
|
||||||
searchBox.search("Lungo");
|
searchBox.search("Lungo");
|
||||||
|
@ -7,8 +7,7 @@ const {
|
|||||||
dummyCoffeeApi
|
dummyCoffeeApi
|
||||||
} = ourCoffeeSdk;
|
} = ourCoffeeSdk;
|
||||||
|
|
||||||
const { buildCreateOrderButton, buildCloseButton } =
|
const { buildCloseButton } = OfferPanelComponent;
|
||||||
OfferPanelComponent;
|
|
||||||
|
|
||||||
const buildCustomOrderButton = function (offer, container) {
|
const buildCustomOrderButton = function (offer, container) {
|
||||||
return OfferPanelComponent.buildCreateOrderButton(
|
return OfferPanelComponent.buildCreateOrderButton(
|
||||||
@ -91,7 +90,7 @@ class CustomOfferPanel extends OfferPanelComponent {
|
|||||||
if (offer.previousOfferId) {
|
if (offer.previousOfferId) {
|
||||||
buttons.push(buildPreviousOfferButton);
|
buttons.push(buildPreviousOfferButton);
|
||||||
}
|
}
|
||||||
buttons.push(buildCreateOrderButton);
|
buttons.push(buildCustomOrderButton);
|
||||||
if (offer.phone) {
|
if (offer.phone) {
|
||||||
buttons.push(buildCallButton);
|
buttons.push(buildCallButton);
|
||||||
}
|
}
|
||||||
|
@ -111,12 +111,7 @@ export class SearchBoxComposer<ExtraOptions extends IExtraFields = {}>
|
|||||||
}: IOfferPanelActionEvent) {
|
}: IOfferPanelActionEvent) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'createOrder':
|
case 'createOrder':
|
||||||
const offer = this.findOfferById(offerId);
|
this.createOrder(offerId);
|
||||||
// Offer may be missing if `OfferPanelComponent`
|
|
||||||
// renders offers asynchronously
|
|
||||||
if (offer !== null) {
|
|
||||||
this.events.emit('createOrder', { offer });
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'close':
|
case 'close':
|
||||||
if (this.currentOffer !== null) {
|
if (this.currentOffer !== null) {
|
||||||
@ -127,6 +122,15 @@ export class SearchBoxComposer<ExtraOptions extends IExtraFields = {}>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public createOrder(offerId: string) {
|
||||||
|
const offer = this.findOfferById(offerId);
|
||||||
|
// Offer may be missing if `OfferPanelComponent`
|
||||||
|
// renders offers asynchronously
|
||||||
|
if (offer !== null) {
|
||||||
|
this.events.emit('createOrder', { offer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
for (const disposer of this.listenerDisposers) {
|
for (const disposer of this.listenerDisposers) {
|
||||||
disposer.off();
|
disposer.off();
|
||||||
|
@ -93,3 +93,27 @@ export const hrefValue = (
|
|||||||
? httpHrefEscape(str)
|
? httpHrefEscape(str)
|
||||||
: hrefEscapeBuilder(allowedProtocols)(str)
|
: hrefEscapeBuilder(allowedProtocols)(str)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const findDataField = (
|
||||||
|
target: HTMLElement,
|
||||||
|
fieldName: string
|
||||||
|
): {
|
||||||
|
target: HTMLElement | null;
|
||||||
|
value: string | null;
|
||||||
|
} => {
|
||||||
|
let node = target;
|
||||||
|
while (node) {
|
||||||
|
if (node.dataset && node.dataset[fieldName] !== undefined) {
|
||||||
|
return {
|
||||||
|
target: node,
|
||||||
|
value: node.dataset[fieldName]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
node = node.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
target: null,
|
||||||
|
value: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -185,20 +185,71 @@
|
|||||||
|
|
||||||
.custom-offer {
|
.custom-offer {
|
||||||
clear: right;
|
clear: right;
|
||||||
border-bottom: 0.5px solid rgba(60, 60, 67, 0.36);
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
letter-spacing: -0.24px;
|
letter-spacing: -0.24px;
|
||||||
margin: 5px 23px 0 0;
|
margin: 5px 23px 0 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-offer > aside {
|
.custom-offer .preview,
|
||||||
float: right;
|
.custom-offer .fullview {
|
||||||
width: 120px;
|
display: block;
|
||||||
text-align: right;
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 22px;
|
||||||
|
letter-spacing: -0.408px;
|
||||||
|
padding: 7px 8px;
|
||||||
|
background: rgba(118, 118, 128, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding-left: 22px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: local-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-offer > aside > button {
|
.custom-offer .preview {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer .preview .expand-action,
|
||||||
|
.custom-offer .fullview .collapse-action {
|
||||||
|
float: left;
|
||||||
|
margin-left: -22px;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
padding: 0 1px;
|
||||||
|
width: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer .fullview .collapse-action {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer .fullview {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer .fullview button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-family: local-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer.expanded .preview {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer.expanded .fullview {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-offer .fullview .action-button {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
background: #3a3a3c;
|
background: #3a3a3c;
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
### [Декомпозиция UI-компонентов][sdk-decomposing]
|
### [Декомпозиция UI-компонентов][sdk-decomposing]
|
||||||
|
|
||||||
Перейдём к более предметному разговору и попробуем объяснить, почему требование возможности замены одной из подсистем компонента (внешнего вида, бизнес-логики или поведения) приводит к кратному усложнению интерфейсов. Продолжим рассматривать пример компонента `SearchBox` из предыдущей главы. Напомним, что мы выделили следующие факторы, осложняющие декомпозицию визуальных компонентов:
|
Перейдём к более предметному разговору и попробуем объяснить, почему требование возможности замены одной из подсистем компонента (внешнего вида, бизнес-логики или поведения) приводит к кратному усложнению интерфейсов. Продолжим рассматривать пример компонента `SearchBox` из предыдущей главы. Напомним, что мы выделили следующие факторы, осложняющие проектирование API визуальных компонентов:
|
||||||
* объединение в одном объекте разнородной функциональности, а именно — бизнес-логики, настройки внешнего вида и поведения компонента;
|
* объединение в одном объекте разнородной функциональности, а именно — бизнес-логики, настройки внешнего вида и поведения компонента;
|
||||||
* появление разделяемых ресурсов, т.е. состояния объекта, которое могут одновременно читать и модифицировать разные акторы (включая конечного пользователя);
|
* появление разделяемых ресурсов, т.е. состояния объекта, которое могут одновременно читать и модифицировать разные акторы (включая конечного пользователя);
|
||||||
* неоднозначность иерархий наследования свойств компонентов.
|
* неоднозначность иерархий наследования свойств и опций компонентов.
|
||||||
|
|
||||||
Сделаем задачу более конкретной, и попробуем разработать наш `SearchBox` так, чтобы он допускал следующие модификации:
|
Сделаем задачу более конкретной, и попробуем разработать наш `SearchBox` так, чтобы он допускал следующие модификации:
|
||||||
1. Брендирование предложения и кнопки заказа иконкой сети кофеен:
|
1. Замена списочного представления предложений, например, на представление в виде карты с подсвечиваемыми метками:
|
||||||
* иллюстрирует проблемы неоднозначности иерархий наследования и разделяемых ресурсов (иконки кнопки), как описано в предыдущей главе;
|
* иллюстрирует проблему полной замены одного субкомпонента (списка заказов) при сохранении поведения и дизайна остальных частей системы, а также сложности имплементации разделяемого состояния;
|
||||||
2. Замена списочного представления предложений, например, на представление в виде карты с метками:
|
|
||||||
* иллюстрирует проблему изменения дизайна одного субкомпонента (списка заказов) при сохранении поведения и дизайна остальных частей системы;
|
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
3. Добавление кнопки быстрого заказа в каждое предложение в списке:
|
2. Добавление кнопки быстрого заказа в каждое предложение в списке:
|
||||||
* иллюстрирует проблему разделяемых ресурсов (см. более подробные пояснения ниже) и изменения UX системы в целом при сохранении существующего дизайна, поведения и бизнес-логики.
|
* иллюстрирует проблему полного удаления одного из субкомпонентов при сохранении бизнес-логики и UX:
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
4. Динамическое добавление новой кнопки в панель создания заказа с дополнительной функцией (скажем, позвонить по телефону в кофейню):
|
3. Брендирование предложения и кнопки заказа иконкой сети кофеен:
|
||||||
* иллюстрирует проблему динамического изменения бизнес-логики компонента при сохранении внешнего вида и UX компонента, а также неоднозначности иерархий наследования (поскольку новая кнопка должна располагать своими отдельными настройками).
|
* иллюстрирует проблемы неоднозначности иерархий наследования и разделяемых ресурсов (иконки кнопки), как описано в предыдущей главе;
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user