1
0
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:
Sergey Konstantinov 2023-08-04 10:49:50 +03:00
parent 31e9a55e6c
commit 131b99e5df
7 changed files with 166 additions and 106 deletions

File diff suppressed because one or more lines are too long

View File

@ -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">&gt;</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");

View File

@ -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);
} }

View File

@ -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();

View File

@ -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
};
};

View File

@ -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;

View File

@ -1,25 +1,23 @@
### [Декомпозиция UI-компонентов][sdk-decomposing] ### [Декомпозиция UI-компонентов][sdk-decomposing]
Перейдём к более предметному разговору и попробуем объяснить, почему требование возможности замены одной из подсистем компонента (внешнего вида, бизнес-логики или поведения) приводит к кратному усложнению интерфейсов. Продолжим рассматривать пример компонента `SearchBox` из предыдущей главы. Напомним, что мы выделили следующие факторы, осложняющие декомпозицию визуальных компонентов: Перейдём к более предметному разговору и попробуем объяснить, почему требование возможности замены одной из подсистем компонента (внешнего вида, бизнес-логики или поведения) приводит к кратному усложнению интерфейсов. Продолжим рассматривать пример компонента `SearchBox` из предыдущей главы. Напомним, что мы выделили следующие факторы, осложняющие проектирование API визуальных компонентов:
* объединение в одном объекте разнородной функциональности, а именно — бизнес-логики, настройки внешнего вида и поведения компонента; * объединение в одном объекте разнородной функциональности, а именно — бизнес-логики, настройки внешнего вида и поведения компонента;
* появление разделяемых ресурсов, т.е. состояния объекта, которое могут одновременно читать и модифицировать разные акторы (включая конечного пользователя); * появление разделяемых ресурсов, т.е. состояния объекта, которое могут одновременно читать и модифицировать разные акторы (включая конечного пользователя);
* неоднозначность иерархий наследования свойств компонентов. * неоднозначность иерархий наследования свойств и опций компонентов.
Сделаем задачу более конкретной, и попробуем разработать наш `SearchBox` так, чтобы он допускал следующие модификации: Сделаем задачу более конкретной, и попробуем разработать наш `SearchBox` так, чтобы он допускал следующие модификации:
1. Брендирование предложения и кнопки заказа иконкой сети кофеен: 1. Замена списочного представления предложений, например, на представление в виде карты с подсвечиваемыми метками:
* иллюстрирует проблемы неоднозначности иерархий наследования и разделяемых ресурсов (иконки кнопки), как описано в предыдущей главе; * иллюстрирует проблему полной замены одного субкомпонента (списка заказов) при сохранении поведения и дизайна остальных частей системы, а также сложности имплементации разделяемого состояния;
2. Замена списочного представления предложений, например, на представление в виде карты с метками:
* иллюстрирует проблему изменения дизайна одного субкомпонента (списка заказов) при сохранении поведения и дизайна остальных частей системы;
[![APP](/img/mockups/05.png "Результаты поиска на карте")]() [![APP](/img/mockups/05.png "Результаты поиска на карте")]()
3. Добавление кнопки быстрого заказа в каждое предложение в списке: 2. Добавление кнопки быстрого заказа в каждое предложение в списке:
* иллюстрирует проблему разделяемых ресурсов (см. более подробные пояснения ниже) и изменения UX системы в целом при сохранении существующего дизайна, поведения и бизнес-логики. * иллюстрирует проблему полного удаления одного из субкомпонентов при сохранении бизнес-логики и UX:
[![APP](/img/mockups/06.png "Список результатов поиска с кнопками быстрых действий")]() [![APP](/img/mockups/06.png "Список результатов поиска с кнопками быстрых действий")]()
4. Динамическое добавление новой кнопки в панель создания заказа с дополнительной функцией (скажем, позвонить по телефону в кофейню): 3. Брендирование предложения и кнопки заказа иконкой сети кофеен:
* иллюстрирует проблему динамического изменения бизнес-логики компонента при сохранении внешнего вида и UX компонента, а также неоднозначности иерархий наследования (поскольку новая кнопка должна располагать своими отдельными настройками). * иллюстрирует проблемы неоднозначности иерархий наследования и разделяемых ресурсов (иконки кнопки), как описано в предыдущей главе;
[![APP](/img/mockups/07.png "Дополнительная кнопка «Позвонить»")]() [![APP](/img/mockups/07.png "Дополнительная кнопка «Позвонить»")]()