Skip to content

Commit d8b6f19

Browse files
committed
move more code to typescript
1 parent 120db7b commit d8b6f19

19 files changed

Lines changed: 663 additions & 450 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## LiveView JavaScript Client
2+
3+
This is the documentation for the LiveView JavaScript client. It is a more low-level API documentation for advanced users. For a higher-level overview, [see the page on JavaScript interoperability](https://hexdocs.pm/phoenix_live_view/js-interop.html) instead.
Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ import {
2828

2929
import { logError } from "./utils";
3030

31+
type FormInputLike = HTMLElement & {
32+
readonly form?: HTMLFormElement | null;
33+
readonly type?: string;
34+
readonly validity?: ValidityState;
35+
readonly name?: string;
36+
};
37+
3138
const DOM = {
3239
byId(id) {
3340
return document.getElementById(id) || logError(`no id found for ${id}`);
@@ -40,7 +47,11 @@ const DOM = {
4047
}
4148
},
4249

43-
all(node, query, callback) {
50+
all(
51+
node: Element | Document | DocumentFragment,
52+
query: string,
53+
callback?: (el: Element) => void,
54+
): Element[] {
4455
if (!node) {
4556
return [];
4657
}
@@ -57,7 +68,7 @@ const DOM = {
5768
return template.content.childElementCount;
5869
},
5970

60-
isUploadInput(el) {
71+
isUploadInput(el): el is HTMLInputElement {
6172
return el.type === "file" && el.getAttribute(PHX_UPLOAD_REF) !== null;
6273
},
6374

@@ -208,7 +219,7 @@ const DOM = {
208219
).forEach((parent) => {
209220
parentCids.add(cid);
210221
this.all(parent, `[${PHX_VIEW_REF}="${viewId}"][${PHX_COMPONENT}]`)
211-
.map((el) => parseInt(el.getAttribute(PHX_COMPONENT)))
222+
.map((el) => parseInt(el.getAttribute(PHX_COMPONENT)!))
212223
.forEach((childCID) => childrenCids.add(childCID));
213224
});
214225
});
@@ -376,7 +387,7 @@ const DOM = {
376387
}
377388
},
378389

379-
triggerCycle(el, key, currentCycle) {
390+
triggerCycle(el, key, currentCycle?) {
380391
const [cycle, trigger] = this.private(el, key);
381392
if (!currentCycle) {
382393
currentCycle = cycle;
@@ -491,7 +502,7 @@ const DOM = {
491502
return null;
492503
},
493504

494-
dispatchEvent(target, name, opts = {}) {
505+
dispatchEvent(target, name, opts: { bubbles?: boolean; detail?: any } = {}) {
495506
let defaultBubble = true;
496507
const isUploadTarget =
497508
target.nodeName === "INPUT" && target.type === "file";
@@ -524,7 +535,11 @@ const DOM = {
524535
// merge attributes from source to target
525536
// if an element is ignored, we only merge data attributes
526537
// including removing data attributes that are no longer in the source
527-
mergeAttrs(target, source, opts = {}) {
538+
mergeAttrs(
539+
target,
540+
source,
541+
opts: { exclude?: string[]; isIgnored?: boolean } = {},
542+
) {
528543
const exclude = new Set(opts.exclude || []);
529544
const isIgnored = opts.isIgnored;
530545
const sourceAttrs = source.attributes;
@@ -611,21 +626,27 @@ const DOM = {
611626
}
612627
},
613628

614-
isFormInput(el) {
615-
if (el.localName && customElements.get(el.localName)) {
616-
// Custom Elements may be form associated. This allows them
617-
// to participate within a form's lifecycle, including form
618-
// validity and form submissions.
619-
// The spec for Form Associated custom elements requires the
620-
// custom element's class to contain a static boolean value of `formAssociated`
621-
// which identifies this class as allowed to associate to a form.
622-
// See https://html.spec.whatwg.org/dev/custom-elements.html#custom-elements-face-example
623-
// for details.
624-
return customElements.get(el.localName)[`formAssociated`];
629+
isFormInput(el: Element | EventTarget | null): el is FormInputLike {
630+
if (!(el instanceof HTMLElement)) return false;
631+
if (el.localName) {
632+
const customEl = customElements.get(el.localName);
633+
if (customEl) {
634+
// Custom Elements may be form associated. This allows them
635+
// to participate within a form's lifecycle, including form
636+
// validity and form submissions.
637+
// The spec for Form Associated custom elements requires the
638+
// custom element's class to contain a static boolean value of `formAssociated`
639+
// which identifies this class as allowed to associate to a form.
640+
// See https://html.spec.whatwg.org/dev/custom-elements.html#custom-elements-face-example
641+
// for details.
642+
return (
643+
(customEl as { formAssociated?: boolean }).formAssociated === true
644+
);
645+
}
625646
}
626-
627647
return (
628-
/^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== "button"
648+
/^(?:input|select|textarea)$/i.test(el.tagName) &&
649+
(el as HTMLInputElement).type !== "button"
629650
);
630651
},
631652

@@ -650,21 +671,22 @@ const DOM = {
650671
);
651672
},
652673

653-
cleanChildNodes(container, phxUpdate) {
674+
cleanChildNodes(container: Element, phxUpdate: string) {
654675
if (
655676
DOM.isPhxUpdate(container, phxUpdate, ["append", "prepend", PHX_STREAM])
656677
) {
657-
const toRemove = [];
678+
const toRemove: Array<ChildNode> = [];
658679
container.childNodes.forEach((childNode) => {
659-
if (!childNode.id) {
680+
if (!("id" in childNode) || !childNode.id) {
660681
// Skip warning if it's an empty text node (e.g. a new-line)
661682
const isEmptyTextNode =
662683
childNode.nodeType === Node.TEXT_NODE &&
684+
childNode.nodeValue &&
663685
childNode.nodeValue.trim() === "";
664686
if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) {
665687
logError(
666688
"only HTML element tags with an id are allowed inside containers with phx-update.\n\n" +
667-
`removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}"\n\n`,
689+
`removing illegal node: "${(("outerHTML" in childNode && (childNode.outerHTML as string)) || childNode.nodeValue || "").trim()}"\n\n`,
668690
);
669691
}
670692
toRemove.push(childNode);
@@ -674,7 +696,11 @@ const DOM = {
674696
}
675697
},
676698

677-
replaceRootContainer(container, tagName, attrs) {
699+
replaceRootContainer(
700+
container: HTMLElement,
701+
tagName: string,
702+
attrs: Record<string, string>,
703+
) {
678704
const retainedAttrs = new Set([
679705
"id",
680706
PHX_SESSION,
@@ -697,9 +723,12 @@ const DOM = {
697723
Object.keys(attrs).forEach((attr) =>
698724
newContainer.setAttribute(attr, attrs[attr]),
699725
);
700-
retainedAttrs.forEach((attr) =>
701-
newContainer.setAttribute(attr, container.getAttribute(attr)),
702-
);
726+
retainedAttrs.forEach((attr) => {
727+
const value = container.getAttribute(attr);
728+
if (value !== null) {
729+
newContainer.setAttribute(attr, value);
730+
}
731+
});
703732
newContainer.innerHTML = container.innerHTML;
704733
container.replaceWith(newContainer);
705734
return newContainer;

assets/js/phoenix_live_view/dom_patch.js

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,14 @@ export default class DOMPatch {
506506
this.view.portalElementIds.forEach((id) => {
507507
const el = document.getElementById(id);
508508
if (el) {
509-
const source = document.getElementById(
510-
el.getAttribute(PHX_TELEPORTED_SRC),
511-
);
512-
if (!source) {
513-
el.remove();
514-
this.onNodeDiscarded(el);
515-
this.view.dropPortalElementId(id);
509+
const srcId = el.getAttribute(PHX_TELEPORTED_SRC);
510+
if (srcId) {
511+
const source = document.getElementById(srcId);
512+
if (!source) {
513+
el.remove();
514+
this.onNodeDiscarded(el);
515+
this.view.dropPortalElementId(id);
516+
}
516517
}
517518
}
518519
});
@@ -696,15 +697,17 @@ export default class DOMPatch {
696697

697698
maybeLimitStream(el) {
698699
const { limit } = this.getStreamInsert(el);
699-
const children = limit !== null && Array.from(el.parentElement.children);
700-
if (limit && limit < 0 && children.length > limit * -1) {
701-
children
702-
.slice(0, children.length + limit)
703-
.forEach((child) => this.removeStreamChildElement(child));
704-
} else if (limit && limit >= 0 && children.length > limit) {
705-
children
706-
.slice(limit)
707-
.forEach((child) => this.removeStreamChildElement(child));
700+
if (limit !== null) {
701+
const children = Array.from(el.parentElement.children);
702+
if (limit < 0 && children.length > limit * -1) {
703+
children
704+
.slice(0, children.length + limit)
705+
.forEach((child) => this.removeStreamChildElement(child));
706+
} else if (limit >= 0 && children.length > limit) {
707+
children
708+
.slice(limit)
709+
.forEach((child) => this.removeStreamChildElement(child));
710+
}
708711
}
709712
}
710713

@@ -860,7 +863,7 @@ export default class DOMPatch {
860863
template.innerHTML = source;
861864
nonce = template.content
862865
.querySelector(`script[${PHX_RUNTIME_HOOK}="${CSS.escape(name)}"]`)
863-
.getAttribute("nonce");
866+
?.getAttribute("nonce");
864867
}
865868
const script = document.createElement("script");
866869
script.textContent = el.textContent;

assets/js/phoenix_live_view/entry_uploader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default class EntryUploader {
2121
}
2222
this.uploadChannel.leave();
2323
this.errored = true;
24-
clearTimeout(this.chunkTimer);
24+
this.chunkTimer != null && clearTimeout(this.chunkTimer);
2525
this.entry.error(reason);
2626
}
2727

@@ -44,11 +44,11 @@ export default class EntryUploader {
4444
this.chunkSize + this.offset,
4545
);
4646
reader.onload = (e) => {
47-
if (e.target.error === null) {
47+
if (e.target?.error === null) {
4848
this.offset += /** @type {ArrayBuffer} */ (e.target.result).byteLength;
4949
this.pushChunk(/** @type {ArrayBuffer} */ (e.target.result));
5050
} else {
51-
return logError("Read error: " + e.target.error);
51+
return logError("Read error: " + e.target?.error);
5252
}
5353
};
5454
reader.readAsArrayBuffer(blob);

0 commit comments

Comments
 (0)