mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-25 22:08:06 +02:00
examples refactoring
This commit is contained in:
parent
c35d5ae61d
commit
31e9a55e6c
261
docs/API.ru.html
261
docs/API.ru.html
File diff suppressed because one or more lines are too long
@ -88,7 +88,8 @@
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://twirl.github.io/The-API-Book/#sdk-decomposing"
|
<a
|
||||||
|
href="https://twirl.github.io/The-API-Book/API.en.html#chapter-44"
|
||||||
>Chapter 43: Problems of Introducing UI Components</a
|
>Chapter 43: Problems of Introducing UI Components</a
|
||||||
>. Find the source code of the components and additional tasks
|
>. Find the source code of the components and additional tasks
|
||||||
for self-study at the
|
for self-study at the
|
||||||
@ -112,25 +113,19 @@
|
|||||||
</li>
|
</li>
|
||||||
<li id="example-01">
|
<li id="example-01">
|
||||||
<a href="javascript:showExample('01')"
|
<a href="javascript:showExample('01')"
|
||||||
>Example #1: A <code>SearchBox</code> with a custom
|
>Example #1: A <code>SearchBox</code> with a map</a
|
||||||
icon</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li id="example-02">
|
<li id="example-02">
|
||||||
<a href="javascript:showExample('02')"
|
<a href="javascript:showExample('02')"
|
||||||
>Example #2: A <code>SearchBox</code> with a map</a
|
>Example #2: A <code>SearchBox</code> with in-place
|
||||||
|
actions</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li id="example-03">
|
<li id="example-03">
|
||||||
<a href="javascript:showExample('03')"
|
<a href="javascript:showExample('03')"
|
||||||
>Example #3: A <code>SearchBox</code> with in-place
|
>Example #3: A <code>SearchBox</code> with custom
|
||||||
actions</a
|
buttons</a
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li id="example-04">
|
|
||||||
<a href="javascript:showExample('04')"
|
|
||||||
>Example #4: A <code>SearchBox</code> with a dynamic
|
|
||||||
set of buttons</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 50,
|
"printWidth": 60,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "none"
|
"trailingComma": "none"
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,101 @@
|
|||||||
const buildCustomOrderButton = function (
|
const {
|
||||||
offer,
|
SearchBox,
|
||||||
container
|
SearchBoxComposer,
|
||||||
) {
|
DummyMapApi,
|
||||||
return ourCoffeeSdk.OfferPanelComponent.buildCreateOrderButton(
|
dummyCoffeeApi,
|
||||||
offer,
|
util
|
||||||
container,
|
} = ourCoffeeSdk;
|
||||||
{
|
|
||||||
createOrderButtonUrl:
|
class CustomOfferList {
|
||||||
offer && offer.createOrderButtonIcon,
|
constructor(context, container, offerList) {
|
||||||
createOrderButtonText:
|
this.context = context;
|
||||||
(offer &&
|
this.container = container;
|
||||||
`Buy now for just ${offer.price.formattedValue}`) ||
|
this.events = new util.EventEmitter();
|
||||||
"Place an Order"
|
this.offerList = null;
|
||||||
|
this.map = null;
|
||||||
|
|
||||||
|
this.onMarkerClick = (markerId) => {
|
||||||
|
this.events.emit("offerSelect", {
|
||||||
|
offerId: markerId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.setOfferList = ({ offerList: newOfferList }) => {
|
||||||
|
if (this.map) {
|
||||||
|
this.map.destroy();
|
||||||
|
this.map = null;
|
||||||
}
|
}
|
||||||
|
this.offerList = newOfferList;
|
||||||
|
if (newOfferList) {
|
||||||
|
this.map = new DummyMapApi(this.container, [
|
||||||
|
[16.355, 48.2],
|
||||||
|
[16.375, 48.214]
|
||||||
|
]);
|
||||||
|
for (const offer of newOfferList) {
|
||||||
|
this.map.addMarker(
|
||||||
|
offer.offerId,
|
||||||
|
offer.location,
|
||||||
|
this.onMarkerClick
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
this.setOfferList({ offerList });
|
||||||
generateOfferPreviews(offerList) {
|
this.contextListeners = [
|
||||||
const result = super.generateOfferPreviews(
|
context.events.on(
|
||||||
offerList
|
"offerPreviewListChange",
|
||||||
|
this.setOfferList
|
||||||
|
),
|
||||||
|
context.events.on(
|
||||||
|
"offerFullViewToggle",
|
||||||
|
({ offer }) => {
|
||||||
|
this.map.selectSingleMarker(
|
||||||
|
offer && offer.offerId
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.map) {
|
||||||
|
this.map.destroy();
|
||||||
|
}
|
||||||
|
for (const listener of this.contextListeners) {
|
||||||
|
listener.off();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomComposer extends SearchBoxComposer {
|
||||||
|
buildOfferListComponent(
|
||||||
|
context,
|
||||||
|
container,
|
||||||
|
offerList,
|
||||||
|
contextOptions
|
||||||
|
) {
|
||||||
|
return new CustomOfferList(
|
||||||
|
context,
|
||||||
|
container,
|
||||||
|
this.generateOfferPreviews(offerList, contextOptions),
|
||||||
|
this.generateOfferListComponentOptions(contextOptions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOfferPreviews(offerList) {
|
||||||
|
const result = super.generateOfferPreviews(offerList);
|
||||||
return result === null
|
return result === null
|
||||||
? result
|
? result
|
||||||
: result.map((preview, index) => ({
|
: result.map((preview, index) => ({
|
||||||
...preview,
|
...preview,
|
||||||
imageUrl: offerList[index].place.icon
|
location: offerList[index].place.location
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCurrentOfferFullView(offer, options) {
|
|
||||||
return offer === null
|
|
||||||
? offer
|
|
||||||
: {
|
|
||||||
...super.generateCurrentOfferFullView(
|
|
||||||
offer,
|
|
||||||
options
|
|
||||||
),
|
|
||||||
createOrderButtonIcon: offer.place.icon
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -59,15 +106,10 @@ class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
|||||||
|
|
||||||
const searchBox = new CustomSearchBox(
|
const searchBox = new CustomSearchBox(
|
||||||
document.getElementById("search-box"),
|
document.getElementById("search-box"),
|
||||||
ourCoffeeSdk.dummyCoffeeApi,
|
dummyCoffeeApi,
|
||||||
{
|
{
|
||||||
offerPanel: {
|
offerPanel: {
|
||||||
buttonBuilders: [
|
transparent: true
|
||||||
buildCustomOrderButton,
|
|
||||||
ourCoffeeSdk.OfferPanelComponent
|
|
||||||
.buildCloseButton
|
|
||||||
],
|
|
||||||
closeButtonText: "❌Not Now"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -1,56 +1,76 @@
|
|||||||
class CustomOfferList {
|
class CustomOfferList extends ourCoffeeSdk.OfferListComponent {
|
||||||
constructor(context, container, offerList) {
|
constructor(
|
||||||
this.context = context;
|
context,
|
||||||
this.container = container;
|
container,
|
||||||
this.events =
|
offerList,
|
||||||
new ourCoffeeSdk.util.EventEmitter();
|
options
|
||||||
this.offerList = null;
|
) {
|
||||||
this.map = null;
|
super(context, container, offerList, options);
|
||||||
|
this.onOfferButtonClickListener = (e) => {
|
||||||
this.onMarkerSelect = (markerId) => {
|
const action = e.target.dataset.action;
|
||||||
this.events.emit("offerSelect", {
|
const offerId = e.target.dataset.offerId;
|
||||||
offerId: markerId
|
if (action === "offerSelect") {
|
||||||
});
|
this.events.emit(action, { offerId });
|
||||||
};
|
} else if (action === "createOffer") {
|
||||||
this.setOfferList = ({
|
const offer =
|
||||||
offerList: newOfferList
|
this.context.findOfferById(offerId);
|
||||||
}) => {
|
if (offer) {
|
||||||
if (this.map) {
|
this.context.events.emit(
|
||||||
this.map.destroy();
|
"createOrder",
|
||||||
this.map = null;
|
{
|
||||||
|
offer
|
||||||
}
|
}
|
||||||
this.offerList = newOfferList;
|
);
|
||||||
if (newOfferList) {
|
}
|
||||||
this.map = new ourCoffeeSdk.DummyMapApi(
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOfferHtml(offer) {
|
||||||
|
return ourCoffeeSdk.util.html`<li
|
||||||
|
class="custom-offer"
|
||||||
|
>
|
||||||
|
<aside><button
|
||||||
|
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
||||||
|
offer.offerId
|
||||||
|
)}"
|
||||||
|
data-action="createOffer">Buy now for ${
|
||||||
|
offer.price.formattedValue
|
||||||
|
}</button><button
|
||||||
|
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
||||||
|
offer.offerId
|
||||||
|
)}"
|
||||||
|
data-action="offerSelect">View details
|
||||||
|
</button></aside>
|
||||||
|
<div><strong>${offer.title}</strong></div>
|
||||||
|
<div>${offer.subtitle}</div>
|
||||||
|
<div>${offer.bottomLine}</div>
|
||||||
|
</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,
|
this.container,
|
||||||
[
|
"button"
|
||||||
[16.355, 48.206],
|
|
||||||
[16.375, 48.214]
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
for (const offer of newOfferList) {
|
for (const button of buttons) {
|
||||||
this.map.addMarker(
|
button.removeEventListener(
|
||||||
offer.offerId,
|
"click",
|
||||||
offer.location,
|
this.onOfferButtonClickListener
|
||||||
this.onMarkerSelect
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
this.setOfferList({ offerList });
|
|
||||||
this.contextListener = context.events.on(
|
|
||||||
"offerPreviewListChange",
|
|
||||||
this.setOfferList
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if (this.map) {
|
|
||||||
this.map.destroy();
|
|
||||||
}
|
|
||||||
this.contextListener.off();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
||||||
@ -72,19 +92,6 @@ class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateOfferPreviews(offerList) {
|
|
||||||
const result = super.generateOfferPreviews(
|
|
||||||
offerList
|
|
||||||
);
|
|
||||||
return result === null
|
|
||||||
? result
|
|
||||||
: result.map((preview, index) => ({
|
|
||||||
...preview,
|
|
||||||
location:
|
|
||||||
offerList[index].place.location
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
||||||
|
@ -1,106 +1,189 @@
|
|||||||
class CustomOfferList extends ourCoffeeSdk.OfferListComponent {
|
const {
|
||||||
constructor(
|
SearchBox,
|
||||||
context,
|
SearchBoxComposer,
|
||||||
|
OfferPanelComponent,
|
||||||
|
OfferPanelButton,
|
||||||
|
util,
|
||||||
|
dummyCoffeeApi
|
||||||
|
} = ourCoffeeSdk;
|
||||||
|
|
||||||
|
const { buildCreateOrderButton, buildCloseButton } =
|
||||||
|
OfferPanelComponent;
|
||||||
|
|
||||||
|
const buildCustomOrderButton = function (offer, container) {
|
||||||
|
return OfferPanelComponent.buildCreateOrderButton(
|
||||||
|
offer,
|
||||||
|
container,
|
||||||
|
{
|
||||||
|
createOrderButtonUrl:
|
||||||
|
offer && offer.createOrderButtonIcon,
|
||||||
|
createOrderButtonText:
|
||||||
|
(offer &&
|
||||||
|
`Buy now for just ${offer.price.formattedValue}`) ||
|
||||||
|
"Place an Order"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildCallButton = function (
|
||||||
|
offer,
|
||||||
container,
|
container,
|
||||||
offerList,
|
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
super(context, container, offerList, options);
|
return new OfferPanelButton("call", container, {
|
||||||
this.onOfferButtonClickListener = (e) => {
|
text: util.html`<a href="tel:${util.attrValue(
|
||||||
const action = e.target.dataset.action;
|
offer.phone
|
||||||
const offerId = e.target.dataset.offerId;
|
)}" style="color: inherit; text-decoration: none;">${
|
||||||
if (action === "offerSelect") {
|
options.callButtonText
|
||||||
this.events.emit(action, { offerId });
|
}</a>`
|
||||||
} else if (action === "createOffer") {
|
});
|
||||||
const offer =
|
|
||||||
this.context.findOfferById(offerId);
|
|
||||||
if (offer) {
|
|
||||||
this.context.events.emit(
|
|
||||||
"createOrder",
|
|
||||||
{
|
|
||||||
offer
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
generateOfferHtml(offer) {
|
const buildPreviousOfferButton = function (
|
||||||
return ourCoffeeSdk.util.html`<li
|
offer,
|
||||||
class="custom-offer"
|
container
|
||||||
>
|
) {
|
||||||
<aside><button
|
return new NavigateButton(
|
||||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
"left",
|
||||||
offer.offerId
|
offer.previousOfferId,
|
||||||
)}"
|
container
|
||||||
data-action="createOffer">Buy now for ${
|
|
||||||
offer.price.formattedValue
|
|
||||||
}</button><button
|
|
||||||
data-offer-id="${ourCoffeeSdk.util.attrValue(
|
|
||||||
offer.offerId
|
|
||||||
)}"
|
|
||||||
data-action="offerSelect">View details
|
|
||||||
</button></aside>
|
|
||||||
<div><strong>${offer.title}</strong></div>
|
|
||||||
<div>${offer.subtitle}</div>
|
|
||||||
<div>${offer.bottomLine}</div>
|
|
||||||
</li>`.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDomListeners() {
|
|
||||||
const buttons =
|
|
||||||
this.container.querySelectorAll("button");
|
|
||||||
for (const button of buttons) {
|
|
||||||
button.addEventListener(
|
|
||||||
"click",
|
|
||||||
this.onOfferButtonClickListener
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNextOfferButton = function (offer, container) {
|
||||||
|
return new NavigateButton(
|
||||||
|
"right",
|
||||||
|
offer.nextOfferId,
|
||||||
|
container
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
class NavigateButton {
|
||||||
|
constructor(direction, offerId, container) {
|
||||||
|
this.action = "navigate";
|
||||||
|
this.offerId = offerId;
|
||||||
|
this.events = new util.EventEmitter();
|
||||||
|
const button = (this.button =
|
||||||
|
document.createElement("button"));
|
||||||
|
button.innerHTML = direction === "left" ? "⟨" : "⟩";
|
||||||
|
button.className = direction;
|
||||||
|
container.classList.add("custom-control");
|
||||||
|
this.container = container;
|
||||||
|
this.listener = () =>
|
||||||
|
this.events.emit("press", {
|
||||||
|
target: this
|
||||||
|
});
|
||||||
|
button.addEventListener("click", this.listener);
|
||||||
|
container.appendChild(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.button.removeEventListener("click", this.listener);
|
||||||
|
this.button.parentElement.removeChild(this.button);
|
||||||
|
this.container.classList.remove("custom-control");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
teardownDomListeners() {
|
class CustomOfferPanel extends OfferPanelComponent {
|
||||||
const buttons = document.querySelectorAll(
|
show() {
|
||||||
this.container,
|
const buttons = [];
|
||||||
"button"
|
const offer = this.currentOffer;
|
||||||
);
|
if (offer.previousOfferId) {
|
||||||
for (const button of buttons) {
|
buttons.push(buildPreviousOfferButton);
|
||||||
button.removeEventListener(
|
|
||||||
"click",
|
|
||||||
this.onOfferButtonClickListener
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
buttons.push(buildCreateOrderButton);
|
||||||
|
if (offer.phone) {
|
||||||
|
buttons.push(buildCallButton);
|
||||||
|
}
|
||||||
|
buttons.push(buildCloseButton);
|
||||||
|
if (offer.nextOfferId) {
|
||||||
|
buttons.push(buildNextOfferButton);
|
||||||
|
}
|
||||||
|
this.options.buttonBuilders = buttons;
|
||||||
|
super.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomComposer extends ourCoffeeSdk.SearchBoxComposer {
|
class CustomComposer extends SearchBoxComposer {
|
||||||
buildOfferListComponent(
|
buildOfferPanelComponent(
|
||||||
context,
|
context,
|
||||||
container,
|
container,
|
||||||
offerList,
|
currentOffer,
|
||||||
contextOptions
|
contextOptions
|
||||||
) {
|
) {
|
||||||
return new CustomOfferList(
|
return new CustomOfferPanel(
|
||||||
context,
|
context,
|
||||||
container,
|
container,
|
||||||
this.generateOfferPreviews(
|
this.generateCurrentOfferFullView(
|
||||||
offerList,
|
currentOffer,
|
||||||
contextOptions
|
contextOptions
|
||||||
),
|
),
|
||||||
this.generateOfferListComponentOptions(
|
{
|
||||||
|
...CustomComposer.DEFAULT_OPTIONS,
|
||||||
|
...this.generateOfferPanelComponentOptions(
|
||||||
contextOptions
|
contextOptions
|
||||||
)
|
)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateOfferPreviews(offerList) {
|
||||||
|
const result = super.generateOfferPreviews(offerList);
|
||||||
|
return result === null
|
||||||
|
? result
|
||||||
|
: result.map((preview, index) => ({
|
||||||
|
...preview,
|
||||||
|
imageUrl: offerList[index].place.icon
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCurrentOfferFullView(offer, options) {
|
||||||
|
const offerFullView =
|
||||||
|
super.generateCurrentOfferFullView(offer, options);
|
||||||
|
if (offer) {
|
||||||
|
if (offer.place.phone) {
|
||||||
|
offerFullView.phone = offer.place.phone;
|
||||||
|
}
|
||||||
|
if (offer.place.icon) {
|
||||||
|
offerFullView.createOrderButtonIcon =
|
||||||
|
offer.place.icon;
|
||||||
|
}
|
||||||
|
if (this.offerList) {
|
||||||
|
const offers = this.offerList;
|
||||||
|
const index = offers.findIndex(
|
||||||
|
({ offerId }) => offerId === offer.offerId
|
||||||
|
);
|
||||||
|
if (index > 0) {
|
||||||
|
offerFullView.previousOfferId =
|
||||||
|
offers[index - 1].offerId;
|
||||||
|
}
|
||||||
|
if (index < offers.length - 1 && index >= 0) {
|
||||||
|
offerFullView.nextOfferId =
|
||||||
|
offers[index + 1].offerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return offerFullView;
|
||||||
|
}
|
||||||
|
|
||||||
|
performAction(event) {
|
||||||
|
if (event.action === "navigate") {
|
||||||
|
this.selectOffer(event.target.offerId);
|
||||||
|
} else {
|
||||||
|
super.performAction(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomSearchBox extends ourCoffeeSdk.SearchBox {
|
CustomComposer.DEFAULT_OPTIONS = {
|
||||||
|
createOrderButtonText: "🛒Place an Order",
|
||||||
|
callButtonText: "☎️ Make a Call",
|
||||||
|
closeButtonText: "❌Not Now"
|
||||||
|
};
|
||||||
|
|
||||||
|
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 +194,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");
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
const {
|
|
||||||
SearchBox,
|
|
||||||
SearchBoxComposer,
|
|
||||||
OfferPanelComponent,
|
|
||||||
OfferPanelButton,
|
|
||||||
util,
|
|
||||||
dummyCoffeeApi
|
|
||||||
} = ourCoffeeSdk;
|
|
||||||
|
|
||||||
const buildCallButton = function (
|
|
||||||
offer,
|
|
||||||
container,
|
|
||||||
options
|
|
||||||
) {
|
|
||||||
return new OfferPanelButton("call", container, {
|
|
||||||
text: util.html`<a href="tel:${util.attrValue(
|
|
||||||
offer.phone
|
|
||||||
)}" style="color: inherit; text-decoration: none;">${
|
|
||||||
options.callButtonText
|
|
||||||
}</a>`
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildCreateOrderButton =
|
|
||||||
OfferPanelComponent.buildCreateOrderButton;
|
|
||||||
const buildCloseButton =
|
|
||||||
OfferPanelComponent.buildCloseButton;
|
|
||||||
|
|
||||||
class CustomOfferPanel extends OfferPanelComponent {
|
|
||||||
show() {
|
|
||||||
this.options.buttonBuilders = this
|
|
||||||
.currentOffer.phone
|
|
||||||
? [
|
|
||||||
buildCreateOrderButton,
|
|
||||||
buildCallButton,
|
|
||||||
buildCloseButton
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
buildCreateOrderButton,
|
|
||||||
buildCloseButton
|
|
||||||
];
|
|
||||||
return super.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CustomComposer extends SearchBoxComposer {
|
|
||||||
buildOfferPanelComponent(
|
|
||||||
context,
|
|
||||||
container,
|
|
||||||
currentOffer,
|
|
||||||
contextOptions
|
|
||||||
) {
|
|
||||||
return new CustomOfferPanel(
|
|
||||||
context,
|
|
||||||
container,
|
|
||||||
this.generateCurrentOfferFullView(
|
|
||||||
currentOffer,
|
|
||||||
contextOptions
|
|
||||||
),
|
|
||||||
this.generateOfferPanelComponentOptions(
|
|
||||||
contextOptions
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCurrentOfferFullView(offer, options) {
|
|
||||||
const offerFullView =
|
|
||||||
super.generateCurrentOfferFullView(
|
|
||||||
offer,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
if (offer && offer.place.phone) {
|
|
||||||
offerFullView.phone = offer.place.phone;
|
|
||||||
}
|
|
||||||
return offerFullView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CustomSearchBox extends SearchBox {
|
|
||||||
buildComposer(context, container, options) {
|
|
||||||
return new CustomComposer(
|
|
||||||
context,
|
|
||||||
container,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createOrder(offer) {
|
|
||||||
alert(`Isn't actually implemented (yet)`);
|
|
||||||
return super.createOrder(offer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchBox = new CustomSearchBox(
|
|
||||||
document.getElementById("search-box"),
|
|
||||||
dummyCoffeeApi,
|
|
||||||
{
|
|
||||||
offerPanel: {
|
|
||||||
createOrderButtonText: "🛒Place an Order",
|
|
||||||
callButtonText: "☎️ Make a Call",
|
|
||||||
closeButtonText: "❌Not Now"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
searchBox.search("Lungo");
|
|
@ -18,7 +18,7 @@ export class OfferPanelButton implements IButton {
|
|||||||
protected readonly options: IButtonOptions
|
protected readonly options: IButtonOptions
|
||||||
) {
|
) {
|
||||||
this.container.innerHTML = html`<button
|
this.container.innerHTML = html`<button
|
||||||
class="our-coffee-sdk-sdk-offer-panel-button"
|
class="our-coffee-sdk-offer-panel-button"
|
||||||
>
|
>
|
||||||
${this.options.iconUrl
|
${this.options.iconUrl
|
||||||
? html`<img src="${attrValue(this.options.iconUrl)}" />`
|
? html`<img src="${attrValue(this.options.iconUrl)}" />`
|
||||||
@ -27,7 +27,7 @@ export class OfferPanelButton implements IButton {
|
|||||||
</button>`.toString();
|
</button>`.toString();
|
||||||
this.button = $<HTMLButtonElement>(
|
this.button = $<HTMLButtonElement>(
|
||||||
this.container,
|
this.container,
|
||||||
'.our-coffee-sdk-sdk-offer-panel-button'
|
'.our-coffee-sdk-offer-panel-button'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.button.addEventListener('click', this.onClickListener, false);
|
this.button.addEventListener('click', this.onClickListener, false);
|
||||||
|
@ -97,7 +97,9 @@ export class OfferPanelComponent implements IOfferPanelComponent {
|
|||||||
this.container,
|
this.container,
|
||||||
'.our-coffee-sdk-offer-panel-buttons'
|
'.our-coffee-sdk-offer-panel-buttons'
|
||||||
);
|
);
|
||||||
|
if (!this.options.transparent) {
|
||||||
this.container.classList.add('our-coffee-sdk-cover-blur');
|
this.container.classList.add('our-coffee-sdk-cover-blur');
|
||||||
|
}
|
||||||
this.setupButtons();
|
this.setupButtons();
|
||||||
this.shown = true;
|
this.shown = true;
|
||||||
}
|
}
|
||||||
@ -150,11 +152,12 @@ export class OfferPanelComponent implements IOfferPanelComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
protected onButtonPress = ({ target: { action } }: IButtonPressEvent) => {
|
protected onButtonPress = ({ target }: IButtonPressEvent) => {
|
||||||
if (this.currentOffer !== null) {
|
if (this.currentOffer !== null) {
|
||||||
this.events.emit('action', {
|
this.events.emit('action', {
|
||||||
action,
|
action: target.action,
|
||||||
offerId: this.currentOffer.offerId
|
target,
|
||||||
|
currentOfferId: this.currentOffer.offerId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// TBD
|
// TBD
|
||||||
@ -165,6 +168,7 @@ export class OfferPanelComponent implements IOfferPanelComponent {
|
|||||||
export interface OfferPanelComponentOptions
|
export interface OfferPanelComponentOptions
|
||||||
extends IOfferPanelComponentOptions {
|
extends IOfferPanelComponentOptions {
|
||||||
buttonBuilders?: ButtonBuilder[];
|
buttonBuilders?: ButtonBuilder[];
|
||||||
|
transparent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonBuilder = (
|
export type ButtonBuilder = (
|
||||||
|
@ -87,6 +87,46 @@ export class SearchBoxComposer<ExtraOptions extends IExtraFields = {}>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public selectOffer(offerId: string) {
|
||||||
|
const offer = this.findOfferById(offerId);
|
||||||
|
// Offer may be missing for a variety of reasons,
|
||||||
|
// most notably of `OfferListComponent` renders
|
||||||
|
// offers asynchronously
|
||||||
|
if (offer !== null) {
|
||||||
|
this.currentOffer = offer;
|
||||||
|
this.events.emit('offerFullViewToggle', {
|
||||||
|
offer: this.generateCurrentOfferFullView(
|
||||||
|
this.currentOffer,
|
||||||
|
this.contextOptions
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// TDB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public performAction({
|
||||||
|
action,
|
||||||
|
currentOfferId: offerId
|
||||||
|
}: IOfferPanelActionEvent) {
|
||||||
|
switch (action) {
|
||||||
|
case 'createOrder':
|
||||||
|
const offer = this.findOfferById(offerId);
|
||||||
|
// Offer may be missing if `OfferPanelComponent`
|
||||||
|
// renders offers asynchronously
|
||||||
|
if (offer !== null) {
|
||||||
|
this.events.emit('createOrder', { offer });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'close':
|
||||||
|
if (this.currentOffer !== null) {
|
||||||
|
this.currentOffer = null;
|
||||||
|
this.events.emit('offerFullViewToggle', { offer: null });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
for (const disposer of this.listenerDisposers) {
|
for (const disposer of this.listenerDisposers) {
|
||||||
disposer.off();
|
disposer.off();
|
||||||
@ -120,45 +160,12 @@ export class SearchBoxComposer<ExtraOptions extends IExtraFields = {}>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
protected onOfferPanelAction = ({
|
protected onOfferPanelAction = (event: IOfferPanelActionEvent) => {
|
||||||
action,
|
this.performAction(event);
|
||||||
offerId
|
|
||||||
}: IOfferPanelActionEvent) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'createOrder':
|
|
||||||
const offer = this.findOfferById(offerId);
|
|
||||||
// Offer may be missing if `OfferPanelComponent`
|
|
||||||
// renders offers asynchronously
|
|
||||||
if (offer !== null) {
|
|
||||||
this.events.emit('createOrder', { offer });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'close':
|
|
||||||
if (this.currentOffer !== null) {
|
|
||||||
this.currentOffer = null;
|
|
||||||
this.events.emit('offerFullViewToggle', { offer: null });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
protected onOfferListOfferSelect = ({ offerId }: IOfferSelectedEvent) => {
|
protected onOfferListOfferSelect = ({ offerId }: IOfferSelectedEvent) =>
|
||||||
const offer = this.findOfferById(offerId);
|
this.selectOffer(offerId);
|
||||||
// Offer may be missing for a variety of reasons,
|
|
||||||
// most notably of `OfferListComponent` renders
|
|
||||||
// offers asynchronously
|
|
||||||
if (offer !== null) {
|
|
||||||
this.currentOffer = offer;
|
|
||||||
this.events.emit('offerFullViewToggle', {
|
|
||||||
offer: this.generateCurrentOfferFullView(
|
|
||||||
this.currentOffer,
|
|
||||||
this.contextOptions
|
|
||||||
)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// TDB
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected buildOfferListComponent(
|
protected buildOfferListComponent(
|
||||||
context: ISearchBoxComposer,
|
context: ISearchBoxComposer,
|
||||||
|
@ -18,5 +18,6 @@ export interface IOfferPanelComponentEvents {
|
|||||||
|
|
||||||
export interface IOfferPanelActionEvent {
|
export interface IOfferPanelActionEvent {
|
||||||
action: string;
|
action: string;
|
||||||
offerId: string;
|
currentOfferId: string;
|
||||||
|
target?: any;
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 40px 32px 30px;
|
padding: 0 32px;
|
||||||
margin: 2px 12px;
|
margin: 2px 12px;
|
||||||
border: 1px solid #d8d8d8;
|
border: 1px solid #d8d8d8;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
@ -149,9 +149,10 @@
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.our-coffee-sdk-offer-panel-buttons button {
|
.our-coffee-sdk-offer-panel-button {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
background: #3a3a3c;
|
background: #3a3a3c;
|
||||||
@ -167,14 +168,13 @@
|
|||||||
mix-blend-mode: luminosity;
|
mix-blend-mode: luminosity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.our-coffee-sdk-offer-panel-buttons button img {
|
.our-coffee-sdk-offer-panel-button img {
|
||||||
max-height: 22px;
|
max-height: 22px;
|
||||||
max-width: 22px;
|
max-width: 22px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.our-coffee-sdk-offer-panel-buttons
|
.our-coffee-sdk-offer-panel-close-button {
|
||||||
button.our-coffee-sdk-offer-panel-close-button {
|
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
@ -189,7 +189,7 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
letter-spacing: -0.24px;
|
letter-spacing: -0.24px;
|
||||||
margin: 5px 0;
|
margin: 5px 23px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-offer > aside {
|
.custom-offer > aside {
|
||||||
@ -213,3 +213,31 @@
|
|||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
mix-blend-mode: luminosity;
|
mix-blend-mode: luminosity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-control {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-control > button {
|
||||||
|
color: rgba(60, 60, 67, 0.6);
|
||||||
|
font-size: 60px;
|
||||||
|
line-height: 30px;
|
||||||
|
margin-top: -15px;
|
||||||
|
height: 60px;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-control .right {
|
||||||
|
float: right;
|
||||||
|
margin-right: -30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-control .left {
|
||||||
|
float: left;
|
||||||
|
margin-left: -30px;
|
||||||
|
}
|
||||||
|
@ -137,8 +137,9 @@ describe('OfferPanelComponent', () => {
|
|||||||
});
|
});
|
||||||
expect(events).toEqual([
|
expect(events).toEqual([
|
||||||
{
|
{
|
||||||
offerId: CAFEE_CHAMOMILE_LUNGO_OFFER_FULL_VIEW.offerId,
|
currentOfferId: CAFEE_CHAMOMILE_LUNGO_OFFER_FULL_VIEW.offerId,
|
||||||
action: MOCK_ACTION
|
action: MOCK_ACTION,
|
||||||
|
target: mockButton
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
offerPanel.destroy();
|
offerPanel.destroy();
|
||||||
|
@ -161,7 +161,7 @@ describe('SearchBoxComposer', () => {
|
|||||||
'createOrder',
|
'createOrder',
|
||||||
() => {
|
() => {
|
||||||
mockOfferPanel.events.emit('action', {
|
mockOfferPanel.events.emit('action', {
|
||||||
offerId: CAFEE_CHAMOMILE_LUNGO_OFFER.offerId,
|
currentOfferId: CAFEE_CHAMOMILE_LUNGO_OFFER.offerId,
|
||||||
action: 'createOrder'
|
action: 'createOrder'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ describe('SearchBoxComposer', () => {
|
|||||||
event = e;
|
event = e;
|
||||||
});
|
});
|
||||||
mockOfferPanel.events.emit('action', {
|
mockOfferPanel.events.emit('action', {
|
||||||
offerId: 'fakeOfferId',
|
currentOfferId: 'fakeOfferId',
|
||||||
action: 'createOrder'
|
action: 'createOrder'
|
||||||
});
|
});
|
||||||
expect(event).toEqual(null);
|
expect(event).toEqual(null);
|
||||||
@ -200,7 +200,7 @@ describe('SearchBoxComposer', () => {
|
|||||||
'offerFullViewToggle',
|
'offerFullViewToggle',
|
||||||
() => {
|
() => {
|
||||||
mockOfferPanel.events.emit('action', {
|
mockOfferPanel.events.emit('action', {
|
||||||
offerId: CAFEE_CHAMOMILE_LUNGO_OFFER.offerId,
|
currentOfferId: CAFEE_CHAMOMILE_LUNGO_OFFER.offerId,
|
||||||
action: 'close'
|
action: 'close'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ export class DummyMapApi {
|
|||||||
'background-size: cover',
|
'background-size: cover',
|
||||||
'position: relative',
|
'position: relative',
|
||||||
'width: 100%',
|
'width: 100%',
|
||||||
'height: 100%'
|
'height: 100%',
|
||||||
|
'overflow: hidden'
|
||||||
].join(';');
|
].join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ export class DummyMapApi {
|
|||||||
(this.bounds[1][1] - this.bounds[0][1])
|
(this.bounds[1][1] - this.bounds[0][1])
|
||||||
) - 30;
|
) - 30;
|
||||||
const marker = document.createElement('div');
|
const marker = document.createElement('div');
|
||||||
|
marker.dataset.markerId = markerId;
|
||||||
marker.style.cssText = [
|
marker.style.cssText = [
|
||||||
'position: absolute',
|
'position: absolute',
|
||||||
'width: 30px',
|
'width: 30px',
|
||||||
@ -44,7 +46,8 @@ export class DummyMapApi {
|
|||||||
'align: center',
|
'align: center',
|
||||||
'line-height: 30px',
|
'line-height: 30px',
|
||||||
'font-size: 30px',
|
'font-size: 30px',
|
||||||
'cursor: pointer'
|
'cursor: pointer',
|
||||||
|
'mix-blend-mode: luminosity'
|
||||||
].join(';');
|
].join(';');
|
||||||
marker.innerHTML =
|
marker.innerHTML =
|
||||||
'<a href="javascript:void(0)" style="text-decoration: none;">📍</a>';
|
'<a href="javascript:void(0)" style="text-decoration: none;">📍</a>';
|
||||||
@ -58,6 +61,17 @@ export class DummyMapApi {
|
|||||||
this.container.appendChild(marker);
|
this.container.appendChild(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public selectSingleMarker(markerId: string) {
|
||||||
|
for (const marker of this.container.childNodes) {
|
||||||
|
const element = <HTMLElement>marker;
|
||||||
|
if (element.dataset?.markerId === markerId) {
|
||||||
|
element.style.mixBlendMode = 'unset';
|
||||||
|
} else {
|
||||||
|
element.style.mixBlendMode = 'luminosity';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
for (const dispose of this.listenerDisposers) {
|
for (const dispose of this.listenerDisposers) {
|
||||||
dispose();
|
dispose();
|
||||||
|
@ -2,7 +2,10 @@ const express = require('express');
|
|||||||
const port = process.argv[2];
|
const port = process.argv[2];
|
||||||
|
|
||||||
express()
|
express()
|
||||||
.use('/', express.static('docs'))
|
.get('/', (req, res) => {
|
||||||
|
res.redirect('/The-API-Book');
|
||||||
|
})
|
||||||
|
.use('/The-API-Book', express.static('docs'))
|
||||||
.listen(port, () => {
|
.listen(port, () => {
|
||||||
console.log(`Docs Server is listening port ${port}`);
|
console.log(`Docs Server is listening port ${port}`);
|
||||||
});
|
});
|
||||||
|
BIN
src/img/mockups/07.png
Normal file
BIN
src/img/mockups/07.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
@ -10,15 +10,19 @@
|
|||||||
* иллюстрирует проблемы неоднозначности иерархий наследования и разделяемых ресурсов (иконки кнопки), как описано в предыдущей главе;
|
* иллюстрирует проблемы неоднозначности иерархий наследования и разделяемых ресурсов (иконки кнопки), как описано в предыдущей главе;
|
||||||
2. Замена списочного представления предложений, например, на представление в виде карты с метками:
|
2. Замена списочного представления предложений, например, на представление в виде карты с метками:
|
||||||
* иллюстрирует проблему изменения дизайна одного субкомпонента (списка заказов) при сохранении поведения и дизайна остальных частей системы;
|
* иллюстрирует проблему изменения дизайна одного субкомпонента (списка заказов) при сохранении поведения и дизайна остальных частей системы;
|
||||||
3. Добавление кнопки быстрого заказа в каждое предложение в списке:
|
|
||||||
* иллюстрирует проблему разделяемых ресурсов (см. более подробные пояснения ниже) и изменения UX системы в целом при сохранении существующего дизайна, поведения и бизнес-логики.
|
|
||||||
4. Добавление новой кнопки в панель создания заказа с дополнительной функцией (скажем, позвонить по телефону в кофейню):
|
|
||||||
* иллюстрирует проблему изменения бизнес-логики компонента при сохранении внешнего вида и UX компонента, а также неоднозначности иерархий наследования (поскольку новая кнопка должна располагать своими отдельными настройками).
|
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
|
3. Добавление кнопки быстрого заказа в каждое предложение в списке:
|
||||||
|
* иллюстрирует проблему разделяемых ресурсов (см. более подробные пояснения ниже) и изменения UX системы в целом при сохранении существующего дизайна, поведения и бизнес-логики.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
|
4. Динамическое добавление новой кнопки в панель создания заказа с дополнительной функцией (скажем, позвонить по телефону в кофейню):
|
||||||
|
* иллюстрирует проблему динамического изменения бизнес-логики компонента при сохранении внешнего вида и UX компонента, а также неоднозначности иерархий наследования (поскольку новая кнопка должна располагать своими отдельными настройками).
|
||||||
|
|
||||||
|
[]()
|
||||||
|
|
||||||
Решение, которое напрашивается в данной ситуации — это выделение двух дополнительных компонентов, отвечающих за представление списка предложений и за панель показа конкретного предложения, которые можно будет кастомизировать под конкретную задачу. Например, мы можем реализовать связь между ними следующим образом:
|
Решение, которое напрашивается в данной ситуации — это выделение двух дополнительных компонентов, отвечающих за представление списка предложений и за панель показа конкретного предложения, которые можно будет кастомизировать под конкретную задачу. Например, мы можем реализовать связь между ними следующим образом:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -67,13 +71,13 @@ class SearchBox {
|
|||||||
* единственный способ сделать заказ — клик внутри элемента «панель предложения»;
|
* единственный способ сделать заказ — клик внутри элемента «панель предложения»;
|
||||||
* заказ не может быть сделан, если предложение не было предварительно выбрано.
|
* заказ не может быть сделан, если предложение не было предварительно выбрано.
|
||||||
|
|
||||||
Из поставленных нами задач при такой декомпозиции мы можем условно решить только вторую (замена списка на карту), потому что к решению первой проблемы (замена иконки в кнопке заказа) мы не продвинулись вообще, а третью (кнопки быстрых действий в списке заказов) можно решить только следующим образом:
|
Из поставленных нами задач при такой декомпозиции мы можем условно решить только вторую (замена списка на карту), потому что к решению первой и четвёртой проблемы (настройка кнопок в панели) мы не продвинулись вообще, а третью (кнопки быстрых действий в списке заказов) можно решить только следующим образом:
|
||||||
* сделать панель заказа невидимой / перенести её за границы экрана;
|
* сделать панель заказа невидимой / перенести её за границы экрана;
|
||||||
* после события `"click"` на кнопке создания заказа дождаться окончания отрисовки невидимой панели и сгенерировать на ней фиктивное событие `"click"`.
|
* после события `"click"` на кнопке создания заказа дождаться окончания отрисовки невидимой панели и сгенерировать на ней фиктивное событие `"click"`.
|
||||||
|
|
||||||
Думаем, излишне уточнять, что подобные решения ни в каком случае не могут считать разумным API для UI-компонента. Но как же нам всё-таки *сделать* этот интерфейс расширяемым?
|
Думаем, излишне уточнять, что подобные решения ни в каком случае не могут считать разумным API для UI-компонента. Но как же нам всё-таки *сделать* этот интерфейс расширяемым?
|
||||||
|
|
||||||
Первый очевидный шаг заключается в том, чтобы `SearchBox` перестал реагировать на низкоуровневые события типа `click`, а стал только лишь контекстом для нижележащих сущностей и работал в терминах своего уровня абстракции. А для этого нам нужно в первую очередь установить, что же он из себя представляет *логически*, какова его область ответственности как компонента?
|
Первая очевидная проблема заключается в том, что `SearchBox` должен реагировать на низкоуровневые события типа `click`. Согласно рекомендациям, данным нами в главе «[Слабая связность](#back-compat-weak-coupling)», мы должны сделать его контекстом для нижележащих сущностей, а для этого нам нужно в первую очередь установить, что же он из себя представляет *логически*, какова его область ответственности как компонента?
|
||||||
|
|
||||||
Предположим, что мы определим `SearchBox` концептуально как конечный автомат, находящийся в одном из трёх состояний:
|
Предположим, что мы определим `SearchBox` концептуально как конечный автомат, находящийся в одном из трёх состояний:
|
||||||
1. Пуст (ожидает запроса пользователя и получения списка предложений).
|
1. Пуст (ожидает запроса пользователя и получения списка предложений).
|
||||||
|
Loading…
x
Reference in New Issue
Block a user