You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-07-12 22:50:21 +02:00
Comments and mockups
This commit is contained in:
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* In this example, we replace the standard offer list
|
||||
* with an alternative implementation that shows offers
|
||||
* as markers on a map
|
||||
*/
|
||||
const {
|
||||
SearchBox,
|
||||
SearchBoxComposer,
|
||||
@ -6,6 +12,12 @@ const {
|
||||
util
|
||||
} = ourCoffeeSdk;
|
||||
|
||||
/**
|
||||
* A custom offer list component that
|
||||
* renders data on the map instead of a static
|
||||
* list. This class implements the `IOfferListComponent`
|
||||
* interface from scratch.
|
||||
*/
|
||||
class CustomOfferList {
|
||||
constructor(context, container, offerList) {
|
||||
this.context = context;
|
||||
@ -14,11 +26,21 @@ class CustomOfferList {
|
||||
this.offerList = null;
|
||||
this.map = null;
|
||||
|
||||
/**
|
||||
* We listen to the map events (marker selection)
|
||||
* and translate it as an offer selection event.
|
||||
* This is the requirement from the `IOfferListComponent`
|
||||
* interface
|
||||
*/
|
||||
this.onMarkerClick = (markerId) => {
|
||||
this.events.emit("offerSelect", {
|
||||
offerId: markerId
|
||||
});
|
||||
};
|
||||
/**
|
||||
* We are free to implement the business logic in
|
||||
* any that suits our needs
|
||||
*/
|
||||
this.setOfferList = ({ offerList: newOfferList }) => {
|
||||
if (this.map) {
|
||||
this.map.destroy();
|
||||
@ -26,6 +48,9 @@ class CustomOfferList {
|
||||
}
|
||||
this.offerList = newOfferList;
|
||||
if (newOfferList) {
|
||||
// We're displaying data on a map (a dummy one),
|
||||
// using the additional data we pass through the
|
||||
// customized composer (see below)
|
||||
this.map = new DummyMapApi(this.container, [
|
||||
[16.355, 48.2],
|
||||
[16.375, 48.214]
|
||||
@ -46,6 +71,21 @@ class CustomOfferList {
|
||||
"offerPreviewListChange",
|
||||
this.setOfferList
|
||||
),
|
||||
// We listen to the
|
||||
// 'offerFullViewToggle' event on
|
||||
// the parent composer context
|
||||
// to select or deselect the corresponding
|
||||
// marker.
|
||||
//
|
||||
// Note the important pattern:
|
||||
// when the marker is clicked, we DO NOT
|
||||
// mark it as selected, but only emit an
|
||||
// event. This is because the offer list
|
||||
// does not own the logic of selecting
|
||||
// offers.
|
||||
// It is the composer's responsibility
|
||||
// to decide, whether this event should
|
||||
// result in opening a panel or not
|
||||
context.events.on(
|
||||
"offerFullViewToggle",
|
||||
({ offer }) => {
|
||||
@ -57,6 +97,9 @@ class CustomOfferList {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* As required in the `IOfferListComponent` interface
|
||||
*/
|
||||
destroy() {
|
||||
if (this.map) {
|
||||
this.map.destroy();
|
||||
@ -67,6 +110,14 @@ class CustomOfferList {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to subclass a standard `SearchBoxComposer`
|
||||
* to achieve to important goals:
|
||||
* * Use the custom offer list we created instead
|
||||
* of the standard component
|
||||
* * Enrich the preview data with the geographical
|
||||
* coordinates of the coffee shop
|
||||
*/
|
||||
class CustomComposer extends SearchBoxComposer {
|
||||
buildOfferListComponent(
|
||||
context,
|
||||
@ -93,6 +144,10 @@ class CustomComposer extends SearchBoxComposer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We're subclassing `SearchBox` to use our
|
||||
* enhanced composer
|
||||
*/
|
||||
class CustomSearchBox extends SearchBox {
|
||||
buildComposer(context, container, options) {
|
||||
return new CustomComposer(context, container, options);
|
||||
|
@ -1,3 +1,9 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* In this example, we change the composition logic:
|
||||
* there is no “offer full view” (panel) component, only
|
||||
* a offer list with additional actions
|
||||
*/
|
||||
const {
|
||||
SearchBox,
|
||||
SearchBoxComposer,
|
||||
@ -6,10 +12,22 @@ const {
|
||||
util
|
||||
} = ourCoffeeSdk;
|
||||
|
||||
/**
|
||||
* A customized version of the standard `OfferListComponent`.
|
||||
* As we're okay with its logic, we reuse it with two modifications:
|
||||
* * List items could be expanded (and then collapsed back)
|
||||
* * List items contain the 'Place an order' button
|
||||
*/
|
||||
class CustomOfferList extends OfferListComponent {
|
||||
constructor(context, container, offerList, options) {
|
||||
super(context, container, offerList, options);
|
||||
|
||||
/**
|
||||
* This is a custom DOM event listener to make
|
||||
* other than selecting an offer actions on the item
|
||||
* click event. This is the shortcut we took (see
|
||||
* the explanations in the `OfferPanelComponent.ts`
|
||||
* file).
|
||||
*/
|
||||
this.onClickListener = (e) => {
|
||||
const { target, value: action } = util.findDataField(
|
||||
e.target,
|
||||
@ -31,7 +49,7 @@ class CustomOfferList extends OfferListComponent {
|
||||
this.collapse(container);
|
||||
break;
|
||||
case "createOrder":
|
||||
this.context.createOrder(offerId);
|
||||
this.events.emit("createOrder", { offerId });
|
||||
break;
|
||||
}
|
||||
};
|
||||
@ -45,6 +63,10 @@ class CustomOfferList extends OfferListComponent {
|
||||
item.classList.remove("expanded");
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a redefined function that returns
|
||||
* the offer “preview” markup in the list
|
||||
*/
|
||||
generateOfferHtml(offer) {
|
||||
return util.html`<li
|
||||
class="custom-offer"
|
||||
@ -66,22 +88,96 @@ class CustomOfferList extends OfferListComponent {
|
||||
}
|
||||
}
|
||||
|
||||
class CustomComposer extends SearchBoxComposer {
|
||||
buildOfferListComponent(
|
||||
context,
|
||||
container,
|
||||
offerList,
|
||||
contextOptions
|
||||
) {
|
||||
return new CustomOfferList(
|
||||
context,
|
||||
/**
|
||||
* This is a custom implementation of the
|
||||
* `ISearchBoxComposer` interface from scratch.
|
||||
* As there is no offer panel in this particular
|
||||
* UI, we don't need all the associated logic,
|
||||
* so we replace the standard implementation
|
||||
* with this new one. However, we re-use the
|
||||
* implementation of the offer list subcomponent
|
||||
*/
|
||||
class CustomComposer {
|
||||
constructor(searchBox, container) {
|
||||
this.events = new util.EventEmitter();
|
||||
this.offerList = null;
|
||||
this.container = container;
|
||||
// This is our enhanced offer list
|
||||
this.offerList = new CustomOfferList(
|
||||
this,
|
||||
container,
|
||||
this.generateOfferPreviews(offerList, contextOptions),
|
||||
this.generateOfferListComponentOptions(contextOptions)
|
||||
this.offerList
|
||||
);
|
||||
this.eventDisposers = [
|
||||
searchBox.events.on(
|
||||
"offerListChange",
|
||||
({ offerList }) => this.onOfferListChange(offerList)
|
||||
),
|
||||
// What we need is to listen to an additional event
|
||||
// the custom offer list emits, and convert it into
|
||||
// the order creation request
|
||||
this.offerList.events.on(
|
||||
"createOrder",
|
||||
({ offerId }) => {
|
||||
const offer = this.findOfferById(offerId);
|
||||
if (offer) {
|
||||
this.events.emit("createOrder", {
|
||||
offer
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
/**
|
||||
* This is the `ISearchBoxComposer` interface
|
||||
* method we must implement
|
||||
*/
|
||||
findOfferById(refOfferId) {
|
||||
return this.offerList
|
||||
? this.offerList.find(
|
||||
({ offerId }) => offerId == refOfferId
|
||||
)
|
||||
: null;
|
||||
}
|
||||
/**
|
||||
* This is the `ISearchBoxComposer` interface
|
||||
* method we must implement
|
||||
*/
|
||||
destroy() {
|
||||
for (const disposer of this.eventDisposers) {
|
||||
disposer.off();
|
||||
}
|
||||
this.offerList.destroy();
|
||||
}
|
||||
onOfferListChange(offerList) {
|
||||
this.offerList = offerList;
|
||||
this.events.emit("offerPreviewListChange", {
|
||||
// This is our custom offer preview generator
|
||||
// function. As we don't plan to customize
|
||||
// it further, we don't bother with exposing
|
||||
// overridable methods, etc.
|
||||
offerList:
|
||||
offerList !== null
|
||||
? offerList.map((offer) => ({
|
||||
offerId: offer.offerId,
|
||||
title: offer.place.title,
|
||||
subtitle: offer.description,
|
||||
bottomLine:
|
||||
SearchBoxComposer.generateOfferBottomLine(
|
||||
offer
|
||||
),
|
||||
price: offer.price
|
||||
}))
|
||||
: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We're subclassing `SearchBox` to use our
|
||||
* custom composer
|
||||
*/
|
||||
class CustomSearchBox extends SearchBox {
|
||||
buildComposer(context, container, options) {
|
||||
return new CustomComposer(context, container, options);
|
||||
|
@ -1,3 +1,11 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* In this example, we enhance the standard offer list with
|
||||
* icons of the coffee shops and the offer view panel,
|
||||
* with an additional business logic, exposing several additional
|
||||
* controls and customizing the existing ones
|
||||
*/
|
||||
|
||||
const {
|
||||
SearchBox,
|
||||
SearchBoxComposer,
|
||||
@ -9,7 +17,16 @@ const {
|
||||
|
||||
const { buildCloseButton } = OfferPanelComponent;
|
||||
|
||||
const buildCustomOrderButton = function (offer, container) {
|
||||
/**
|
||||
* This is the factory method to create a customized
|
||||
* “Place an order” button that augments the button
|
||||
* look depending on the additional data fields
|
||||
* in the assiciated offer
|
||||
*/
|
||||
const buildCustomCreateOrderButton = function (
|
||||
offer,
|
||||
container
|
||||
) {
|
||||
return OfferPanelComponent.buildCreateOrderButton(
|
||||
offer,
|
||||
container,
|
||||
@ -24,6 +41,10 @@ const buildCustomOrderButton = function (offer, container) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the factory method to create a customized
|
||||
* button that allows for making a phone call
|
||||
*/
|
||||
const buildCallButton = function (
|
||||
offer,
|
||||
container,
|
||||
@ -38,6 +59,11 @@ const buildCallButton = function (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the factory method to create a customized
|
||||
* button that allows for navigating back to the
|
||||
* previous offer
|
||||
*/
|
||||
const buildPreviousOfferButton = function (
|
||||
offer,
|
||||
container
|
||||
@ -49,6 +75,10 @@ const buildPreviousOfferButton = function (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the factory method to create a customized
|
||||
* button that allows for navigating to the next offer
|
||||
*/
|
||||
const buildNextOfferButton = function (offer, container) {
|
||||
return new NavigateButton(
|
||||
"right",
|
||||
@ -57,10 +87,17 @@ const buildNextOfferButton = function (offer, container) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a new implementation of the `IButton` interface
|
||||
* from scratch. As “Back” and “Forward” buttons share little
|
||||
* logic with the standard button (they do not have
|
||||
* text or icon, feature a different design, etc.) it's
|
||||
* more convenient to make a new class.
|
||||
*/
|
||||
class NavigateButton {
|
||||
constructor(direction, offerId, container) {
|
||||
constructor(direction, targetOfferId, container) {
|
||||
this.action = "navigate";
|
||||
this.offerId = offerId;
|
||||
this.targetOfferId = targetOfferId;
|
||||
this.events = new util.EventEmitter();
|
||||
const button = (this.button =
|
||||
document.createElement("button"));
|
||||
@ -83,6 +120,16 @@ class NavigateButton {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the customization of the standard `OfferPanelComponent`
|
||||
* class. In this custom implementation, the array of
|
||||
* buttons is contructed dynamically depending on the data
|
||||
* shown in the pannel.
|
||||
*
|
||||
* This is a bit of a shortcut (we should have a separate
|
||||
* composer between a panel and its buttons). The full solution
|
||||
* is left as an exercise for the reader.
|
||||
*/
|
||||
class CustomOfferPanel extends OfferPanelComponent {
|
||||
show() {
|
||||
const buttons = [];
|
||||
@ -90,7 +137,7 @@ class CustomOfferPanel extends OfferPanelComponent {
|
||||
if (offer.previousOfferId) {
|
||||
buttons.push(buildPreviousOfferButton);
|
||||
}
|
||||
buttons.push(buildCustomOrderButton);
|
||||
buttons.push(buildCustomCreateOrderButton);
|
||||
if (offer.phone) {
|
||||
buttons.push(buildCallButton);
|
||||
}
|
||||
@ -103,6 +150,18 @@ class CustomOfferPanel extends OfferPanelComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To work with the augmented panel we need
|
||||
* an augmented composer:
|
||||
* * Add the coffee chain icon to the
|
||||
* “preview” data for the offer list
|
||||
* * Use the enhanced offer panel instead
|
||||
* of the standard one
|
||||
* * Enrich the data for the panel needs
|
||||
* with additional fields, such as
|
||||
* the custom icon, phone, and the identifiers
|
||||
* of the previous and next offers
|
||||
*/
|
||||
class CustomComposer extends SearchBoxComposer {
|
||||
buildOfferPanelComponent(
|
||||
context,
|
||||
@ -167,7 +226,12 @@ class CustomComposer extends SearchBoxComposer {
|
||||
|
||||
performAction(event) {
|
||||
if (event.action === "navigate") {
|
||||
this.selectOffer(event.target.offerId);
|
||||
// NB: `event` itself contains an `offerId`
|
||||
// However, this is the identifier of a currently
|
||||
// displayed offer. With `navigate` buttons
|
||||
// we need a different offer, the one we
|
||||
// need to navigate ro
|
||||
this.selectOffer(event.target.targetOfferId);
|
||||
} else {
|
||||
super.performAction(event);
|
||||
}
|
||||
@ -180,6 +244,10 @@ CustomComposer.DEFAULT_OPTIONS = {
|
||||
closeButtonText: "❌Not Now"
|
||||
};
|
||||
|
||||
/**
|
||||
* We're subclassing `SearchBox` to use our
|
||||
* enhanced composer
|
||||
*/
|
||||
class CustomSearchBox extends SearchBox {
|
||||
buildComposer(context, container, options) {
|
||||
return new CustomComposer(context, container, options);
|
||||
|
Reference in New Issue
Block a user