-
Notifications
You must be signed in to change notification settings - Fork 60
[6팀 조영민] Chapter 1-2. 프레임워크 없이 SPA 만들기 #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
0miiii
wants to merge
22
commits into
hanghae-plus:main
Choose a base branch
from
0miiii:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
ce3594d
feat: createVNode
jym0421 bb0370d
feat: normalizeVNode
jym0421 5ebd7b9
Merge pull request #1 from ciel-youngmin/main
0miiii 1c46331
feat: normalizeVNode
jym0421 107bd39
feat: createElement
jym0421 49ab2c6
feat: createElement
jym0421 c5835a4
feat: setupEventListeners 수정
jym0421 23f47e4
feat: renderElement
jym0421 e6649d5
feat: renderEl 추가
jym0421 6aae3e1
feat: update el
jym0421 ec2e269
feat: 404 page 추가
jym0421 e6ec12f
feat: event manager 수정
jym0421 eb2b89b
Merge pull request #2 from ciel-youngmin/main
0miiii be98d3c
feat: render 수정
jym0421 9c6ade0
feat: update el 수정
jym0421 67c3547
Merge pull request #3 from ciel-youngmin/main
0miiii ed54b83
feat: event manager 수정
jym0421 f2a5314
feat: event manager 수정
jym0421 d79cd2b
feat: boolean 처리
jym0421 7cb9d5f
Merge pull request #4 from ciel-youngmin/main
0miiii 2de624b
feat: boolean 처리
jym0421 f1542d1
Merge pull request #5 from ciel-youngmin/main
0miiii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <!doctype html> | ||
| <html lang="ko"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>상품 쇼핑몰</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> | ||
| <link rel="stylesheet" href="/src/styles.css"> | ||
| <script> | ||
| tailwind.config = { | ||
| theme: { | ||
| extend: { | ||
| colors: { | ||
| primary: "#3b82f6", | ||
| secondary: "#6b7280" | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| </script> | ||
| </head> | ||
| <body class="bg-gray-50"> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,26 @@ | ||
| <!doctype html> | ||
| <html lang="ko"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>상품 쇼핑몰</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> | ||
| <link rel="stylesheet" href="/src/styles.css"> | ||
| <script> | ||
| tailwind.config = { | ||
| theme: { | ||
| extend: { | ||
| colors: { | ||
| primary: "#3b82f6", | ||
| secondary: "#6b7280" | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| </script> | ||
| </head> | ||
| <body class="bg-gray-50"> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>상품 쇼핑몰</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> | ||
| <link rel="stylesheet" href="/src/styles.css" /> | ||
| <script> | ||
| tailwind.config = { | ||
| theme: { | ||
| extend: { | ||
| colors: { | ||
| primary: "#3b82f6", | ||
| secondary: "#6b7280", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| </script> | ||
| </head> | ||
| <body class="bg-gray-50"> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,40 @@ | ||
| import { addEvent } from "./eventManager"; | ||
|
|
||
| export function createElement(vNode) {} | ||
| export function createElement(vNode) { | ||
| if (vNode === null || vNode === undefined || typeof vNode === "boolean") { | ||
| return document.createTextNode(""); | ||
| } | ||
|
|
||
| function updateAttributes($el, props) {} | ||
| if (typeof vNode === "string" || typeof vNode === "number") { | ||
| return document.createTextNode(String(vNode)); | ||
| } | ||
|
|
||
| if (Array.isArray(vNode)) { | ||
| const fragment = document.createDocumentFragment(); | ||
|
|
||
| vNode.forEach((node) => fragment.appendChild(createElement(node))); | ||
|
|
||
| return fragment; | ||
| } | ||
|
|
||
| const el = document.createElement(vNode.type); | ||
| updateAttributes(el, vNode.props ?? {}); | ||
|
|
||
| el.append(...vNode.children.map(createElement)); | ||
|
|
||
| return el; | ||
| } | ||
|
|
||
| function updateAttributes($el, props) { | ||
| Object.entries(props).forEach(([attribute, value]) => { | ||
| if (/^on[A-Z]/.test(attribute) && typeof value === "function") { | ||
| addEvent($el, attribute.toLowerCase().substring(2), value); | ||
| } else if (attribute === "className") { | ||
| $el.setAttribute("class", value); | ||
| } else if (typeof value === "boolean") { | ||
| $el[attribute] = value; | ||
| } else { | ||
| $el.setAttribute(attribute, value); | ||
| } | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,9 @@ | ||
| export function createVNode(type, props, ...children) { | ||
| return {}; | ||
| const flatChildren = children.flat(Infinity).filter((child) => child === 0 || Boolean(child)); | ||
|
|
||
| return { | ||
| type, | ||
| props, | ||
| children: flatChildren, | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,41 @@ | ||
| export function setupEventListeners(root) {} | ||
| const eventMap = new WeakMap(); | ||
| const delegatedEvents = new Set(); | ||
|
|
||
| export function addEvent(element, eventType, handler) {} | ||
| export function setupEventListeners(root) { | ||
| delegatedEvents.forEach((eventType) => { | ||
| root.removeEventListener(eventType, handleDelegatedEvent); | ||
| root.addEventListener(eventType, handleDelegatedEvent); | ||
| }); | ||
| } | ||
|
|
||
| export function removeEvent(element, eventType, handler) {} | ||
| export function addEvent(element, eventType, handler) { | ||
| if (!eventMap.has(element)) { | ||
| eventMap.set(element, new Map()); | ||
| } | ||
|
|
||
| const elementEvents = eventMap.get(element); | ||
| elementEvents.set(eventType, handler); | ||
|
|
||
| delegatedEvents.add(eventType); | ||
| } | ||
|
|
||
| export function removeEvent(element, eventType) { | ||
| const elementEvents = eventMap.get(element); | ||
| elementEvents.delete(eventType); | ||
| } | ||
|
|
||
| function handleDelegatedEvent(event) { | ||
| let target = event.target; | ||
|
|
||
| // NOTE: cancelBubble 속성은 deprecated이므로 다른 방법으로 구현해야 함 | ||
| while (target && !event.cancelBubble) { | ||
| const elementEvents = eventMap.get(target); | ||
|
|
||
| if (elementEvents?.has(event.type)) { | ||
| const handler = elementEvents.get(event.type); | ||
| handler(event); | ||
| } | ||
|
|
||
| target = target.parentNode; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,18 @@ | ||
| export function normalizeVNode(vNode) { | ||
| return vNode; | ||
| if (vNode === null || vNode === undefined || typeof vNode === "boolean") { | ||
| return ""; | ||
| } | ||
|
|
||
| if (typeof vNode === "string" || typeof vNode === "number") { | ||
| return String(vNode); | ||
| } | ||
|
|
||
| if (typeof vNode.type === "function") { | ||
| return normalizeVNode(vNode.type({ ...vNode.props, children: vNode.children })); | ||
| } | ||
|
|
||
| return { | ||
| ...vNode, | ||
| children: vNode.children.map(normalizeVNode).filter(Boolean), | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,113 @@ | ||
| import { addEvent, removeEvent } from "./eventManager"; | ||
| import { createElement } from "./createElement.js"; | ||
|
|
||
| function updateAttributes(target, originNewProps, originOldProps) {} | ||
| export function updateAttributes(target, originNewProps, originOldProps) { | ||
| if (!originNewProps && !originOldProps) return; | ||
|
|
||
| export function updateElement(parentElement, newNode, oldNode, index = 0) {} | ||
| if (originOldProps) { | ||
| Object.entries(originOldProps).forEach(([key, value]) => { | ||
| if (key === "children") return; | ||
|
|
||
| if (key.startsWith("on")) { | ||
| const eventType = key.substring(2).toLowerCase(); | ||
| removeEvent(target, eventType, originOldProps[key]); | ||
| } else if (!originNewProps || !(key in originNewProps)) { | ||
| if (key === "className") { | ||
| target.removeAttribute("class"); | ||
| } else if (typeof value === "boolean") { | ||
| target[key] = false; | ||
| target.removeAttribute(key); | ||
| } else { | ||
| target.removeAttribute(key); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if (originNewProps) { | ||
| Object.entries(originNewProps).forEach(([key, value]) => { | ||
| if (key === "children") return; | ||
|
|
||
| if (key === "className") { | ||
| if (value) { | ||
| target.setAttribute("class", value); | ||
| } else { | ||
| target.removeAttribute("class"); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (key.startsWith("on")) { | ||
| const eventType = key.substring(2).toLowerCase(); | ||
| addEvent(target, eventType, value); | ||
| return; | ||
| } | ||
|
|
||
| if (typeof value === "boolean") { | ||
| target[key] = value; | ||
| return; | ||
| } | ||
|
|
||
| if (value != null && (!originOldProps || originOldProps[key] !== value)) { | ||
| target.setAttribute(key, String(value)); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export function updateElement(parentElement, newNode, oldNode, index = 0) { | ||
| if (!newNode && oldNode) { | ||
| if (parentElement.childNodes[index]) { | ||
| parentElement.removeChild(parentElement.childNodes[index]); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (newNode && !oldNode) { | ||
| parentElement.appendChild(createElement(newNode)); | ||
| return; | ||
| } | ||
|
|
||
| if (typeof newNode === "string" || typeof newNode === "number") { | ||
| if (newNode !== oldNode) { | ||
| const newTextNode = document.createTextNode(String(newNode)); | ||
| if (parentElement.childNodes[index]) { | ||
| parentElement.replaceChild(newTextNode, parentElement.childNodes[index]); | ||
| } else { | ||
| parentElement.appendChild(newTextNode); | ||
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (newNode.type !== oldNode.type) { | ||
| if (parentElement.childNodes[index]) { | ||
| parentElement.replaceChild(createElement(newNode), parentElement.childNodes[index]); | ||
| } else { | ||
| parentElement.appendChild(createElement(newNode)); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const childNode = parentElement.childNodes[index]; | ||
| if (childNode) { | ||
| updateAttributes(childNode, newNode.props || {}, oldNode.props || {}); | ||
|
|
||
| const newChildren = newNode.children || []; | ||
| const oldChildren = oldNode.children || []; | ||
| const maxLength = Math.max(newChildren.length, oldChildren.length); | ||
|
|
||
| for (let i = 0; i < maxLength; i++) { | ||
| updateElement(childNode, newChildren[i], oldChildren[i], i); | ||
| } | ||
|
|
||
| if (oldChildren.length > newChildren.length) { | ||
| for (let i = oldChildren.length - 1; i >= newChildren.length; i--) { | ||
| const extraChild = childNode.childNodes[i]; | ||
| if (extraChild) { | ||
| childNode.removeChild(extraChild); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleDelegatedEvent 함수를 setupEventListeners 내부에 선언하면 다른 테스트 코드는 다 통과되나,
"동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다" 테스트만 통과되지 않습니다. 왜 내부에 선언하지 않고 외부에 선언해야 테스트가 통과되는지 이유가 궁금합니다