Omawialiśmy już nieco bardziej zaawansowany projekt własnego jQuery. Tutaj mamy coś podobnego, choć pewne rzeczy są inaczej, dlatego warto będzie to omówić.
Ok, funkcja dolar:
function $(param) {
if (typeof param === "string" || param instanceof String) {
return new ElementCollection(...document.querySelectorAll(param))
} else {
return new ElementCollection(param)
}
}
Jak ktoś nie rozumie tego ifa, niech przypisze do String.prototype jakąś funkcję, która wyloguje this, this instanceof String, typeof this === „string” i pomyśli chwilę nad tym.
Dalej mamy klasę element collection, ona dziedziczy z array. Co oznacza, że ma swój konstruktor, np. zrób new Array(1,2,3). Jest tak, jakbyś użył literału, masz te same metody i tak dalej.
Ok, to teraz ta klasa, metoda ready:
class ElementCollection extends Array {
ready(cb) {
const isReady = this.some(e => {
return e.readyState != null && e.readyState != "loading"
})
if (isReady) {
cb()
} else {
this.on("DOMContentLoaded", cb)
}
return this
}
Param to może być document. Używamy this.some, bo $(document) to jest w zasadzie [document]. Sprawdzamy ready state i wywołujemy callback. Ważne, my to 1 raz sprawdzamy, co jak nie jest jeszcze ready?
Wtedy trzeba przypisać event listener na domcontentloaded z tym callbackiem. To mamy w metodzie on, return this to fluent interface, bez tego nie da się chainować.
Ok, metoda on:
on(event, cbOrSelector, cb) {
if (typeof cbOrSelector === "function") {
this.forEach(e => e.addEventListener(event, cbOrSelector))
} else {
this.forEach(elem => {
elem.addEventListener(event, e => {
if (e.target.matches(cbOrSelector)) cb(e)
})
})
}
return this
}
Pierwszy arg to zawsze nazwa eventu, drugi może być callbackiem albo selektorem. Jak cb, to robimy forEacha na tablicy i dodajemy do każdego elementu event listener z tym callbackiem.
Jak nie jest funkcją, to jest selektorem do filtrowania a cb jest trzeci. Czyli tak – mamy elementy, ale chcemy nadać event elementom spełniającym selektor (tylko im) z określonym callbackiem.
I to się dzieje, iterujemy po kolekcji, dodajemy event listener, sprawdzamy, czy jest match, wykonujemy callback. No, ja bym może sprawdził przed dodaniem listenera, koniec końców wiadomo o co chodzi.
Zwracamy this fluent interface czyli można chainować dalej.
Reszta metod nie powinna nas dziwić przesadnie:
next() {
return this.map(e => e.nextElementSibling).filter(e => e != null)
}
prev() {
return this.map(e => e.previousElementSibling).filter(e => e != null)
}
removeClass(className) {
this.forEach(e => e.classList.remove(className))
return this
}
addClass(className) {
this.forEach(e => e.classList.add(className))
return this
}
css(property, value) {
const camelProp = property.replace(/(-[a-z])/, g => {
return g.replace("-", "").toUpperCase()
})
this.forEach(e => (e.style[camelProp] = value))
return this
}
}
Cały trik polega na tym, że dziedziczymy z array. W związku z tym to, co przekazujemy do konstruktora to tak jakby ktoś new Array(…args) zrobił. I cały ten array jest pod this, razem ze wszystkimi metodami. Po prostu zamiast this.arr = [] mamy tak trochę this = [] + dodatkowe metody naszej klasy.
Ok, to teraz klasa Ajax:
class AjaxPromise {
constructor(promise) {
this.promise = promise
}
done(cb) {
this.promise = this.promise.then(data => {
cb(data)
return data
})
return this
}
fail(cb) {
this.promise = this.promise.catch(cb)
return this
}
always(cb) {
this.promise = this.promise.finally(cb)
return this
}
}
No spoko, przypisujemy promise do this promise, done to then, fail to catch, always to finally. Return this pozwala na chainowanie. Może być.
Dalej, funkcja get, ta jest ciekawa:
$.get = function ({ url, data = {}, success = () => {}, dataType }) {
const queryString = Object.entries(data)
.map(([key, value]) => {
return `${key}=${value}`
})
.join("&")
return new AjaxPromise(
fetch(`${url}?${queryString}`, {
method: "GET",
headers: {
"Content-Type": dataType,
},
})
.then(res => {
if (res.ok) {
return res.json()
} else {
throw new Error(res.status)
}
})
.then(data => {
success(data)
return data
})
)
}
Na query stringa mamy inną klasę (URLSearchParams), ale niech będzie. Teraz autor zwraca nową promisę, która przyjmuje fetch jako argument.
Jeszcze raz – fetch zwraca promisę. Funkcja AjaxPromise przyjmuje jeden argument. Funkcja get przyjmuje funkcję success.
I teraz tak – fetch zwraca promise, trzeba zrobić then na res.json (w async-await to jest podwójny await, bo obie operacje są asynchroniczne). Potem trzeba zrobić then kolejny i wykonać success callback na data i zwrócić data (do then). I cały czas wisi promisa nam.
I ta promisa ląduje w konstruktorze i tam możemy dopisywać kolejne theny przez done, łapać błędy przez fail oraz robić finally przez always.
Możemy tego nie zauważyć, ale autor tutaj tak pochainował i nie wyskakiwał ze swoimi catchami (ani drugim argumentem to then) a nawet nam błąd wyrzucił, jeśli res.json się nie uda, że ten kod jest lepszy, niż na pierwszy rzut oka się wydaje.
Jak nie kumamy, to pobawmy się tym. Jest dobrze, szkoda, że autorowi nie chciało się dalej pociągnąć tej biblioteczki, bo to niezłe kody są, pomysłowe.