Lecture 6 - CS50's Web Programming with Python and JavaScript
cdn.cs50.net/web/2020/spring/lectures/6/lang/en/lecture6.txt
User Interfaces
User Interfaces
How visitors to a web page interact with that page.
Single Page Applications
- Previously, if we wanted a website with multiple pages, we would accomplish that using different routes in our Django application.
> We have the ability to load just a single page and then use JavaScript to manipulate the DOM.
- One major advantage is that we only need to modify the part of the page that is actually changing.
- For example, if we have a Nav Bar that doesn't change based on your current page, we wouldn't want to have to re-render that Nav Bar every time we switch to a new part of the page.
> How could simulate page switching in JavaScript
#singlepage.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Single Page</title>
<style>
div {
display: none;
}
</style>
<script src="singlepage.js"></script>
</head>
<body>
<button data-page="page1">Page 1</button>
<button data-page="page2">Page 2</button>
<button data-page="page3">Page 3</button>
<div id="page1">
<h1>This is page 1</h1>
</div>
<div id="page2">
<h1>This is page 2</h1>
</div>
<div id="page3">
<h1>This is page 3</h1>
</div>
</body>
</html>
- display: none;: All of my divs should have a display property set to none. They're not visible, not displayed on the screen.
- <button data-page="page1">Page 1</button>: The data-page attribute to say that when you click on this button, you should let me see the div whose ID is page1.
- In the HTML above, there's three buttons and three divs.
- At the moment, the divs contain only a small bit of text, but we could imagine each div containing the contents of one page on our site.
> Now, add some JavaScript that allows us to use the buttons to toggle between pages.
// Shows one page and hides the other two
function showPage(page) {
// Hide all of the divs:
document.querySelectorAll('div').forEach(div => {
div.style.display = 'none';
});
// Show the div provided in the argument
document.querySelector(`#${page}`).style.display = 'block';
}
// Wait for page to loaded:
document.addEventListener('DOMContentLoaded', function() {
// Select all buttons
document.querySelectorAll('button').forEach(button => {
// When a button is clicked, switch to that page
button.onclick = function() {
showPage(this.dataset.page);
}
})
});
- document.querySelector(`#${page}`).style.display = 'block'; : Get me the ID of page, whatever element has that particular ID. Change its display property. Instead of none, which was the default(don't show it at all), the other option for a div is block meaning it shows up as just a block that is on the page that is actually visible.
=> Now, I see three buttons that don't do anything yet. I can run the showPage function in the console. But when page 3 is visible, so is page 1. So I want to hide the other pages if I ever show a page.
- document.querySelectorAll('div').forEach(div => {div.style.display= 'none';}): Get all the divs, which is what I'm using to enclose the pages. Use forEach to looping over each of the divs. For each div, set the div.style.display equal to none.
=> showPage function is first querying for all of the divs, which are simulating my pages inside of this singlepage application. For each one of the divs, we're going to pass it as input into this function, which is the argument to forEach. Modify its style property, setting display equal to none, meaning don't show any of the divs, and then show only the div that was requested.
- document.addEventListener('DOMContentLoaded', function() : Wait until all of the content on the page has been loaded.
- document.querySelectorAll('button').forEach(button => { : For all of the buttons, and for each one of those buttons, attach an event listener to them.
- button.onclick = function(){showPage(this.dataset.page);: When the button is clicked on, run this function. I wan to show whatever page is in the page part of the buttons data set. To get it the current button, the button that has been clicked on, use JavaScript keyword 'this' which refers to whatever element has received the event.
=> Take the button that received the event, access its data properties, access its data page attribute and call the showPage function.
(11:15)
> In many cases, it will be inefficient to load the entire contents of every page when we first visit a site, so we'll need to use a server to access new data.
- For example, when you visit a news site, it would take far too long for the site to load if it had to load every single article it has available when you first visit the page.
- We can avoid this problem using a strategy similar to the one we used while loading currency exchange rates in the previous lecture.
- This time, use Django to send and receive information from our single page application.
#urls.py
urlpatterns = [
path("", views.index, name="index"),
path("sections/<int:num>", views.section, name="section")
]
- The 'section' route takes in an integer, and then returns a string of text based on that integer as an HTTP Response.
#views.py
from django.http import Http404, HttpResponse
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request, "singlepage/index.html")
# The texts are much longer in reality, but have
# been shortened here to save space
texts = ["Text 1", "Text 2", "Text 3"]
def section(request, num):
if 1 <= num <= 3:
return HttpResponse(texts[num - 1])
else:
raise Http404("No such section")
- We'll take advantage of AJAX, to make a request to the server to gain the text of a particular section and display it on the screen.
#index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Single Page</title>
<style>
</style>
<script>
// Shows given section
function showSection(section) {
// Find section text from server
fetch(`/sections/${section}`)
.then(response => response.text())
.then(text => {
// Log text and display on page
console.log(text);
document.querySelector('#content').innerHTML = text;
});
}
document.addEventListener('DOMContentLoaded', function() {
// Add button functionality
document.querySelectorAll('button').forEach(button => {
button.onclick = function() {
showSection(this.dataset.section);
};
});
});
</script>
</head>
<body>
<h1>Hello!</h1>
<button data-section="1">Section 1</button>
<button data-section="2">Section 2</button>
<button data-section="3">Section 3</button>
<div id="content">
</div>
</body>
</html>
- showSection: This is going to fetch what text I should display on the page from my own web server. When I get the response, we can convert the response into plain text if it's unstructured data. Then I take that text and colsole.log it so we can see it in the log output. Query select for the content of the page, something that has an ID of content, update its inner HTML and set it equal to that text.
=> Reach out to my server, figure out what text content belongs in the new section, and fill in the part of my page accordingly with the text that comes back from that HTTP request.
- Now, we've created a site where we can load new data from a server without reloading our entire HTML page.
> One disadvantage of our site is that the URL is now less informative.
- The URL remains the same even when we switch from section to section.
- We can solve this problem using the JavaScript History API.
- JavaScript History API: Allows us to push information to our browser history and update the URL manually.
- Alter our script to be employ the history API:
// When back arrow is clicked, show previous section
window.onpopstate = function(event) {
console.log(event.state.section);
showSection(event.state.section);
}
function showSection(section) {
fetch(`/sections/${section}`)
.then(response => response.text())
.then(text => {
console.log(text);
document.querySelector('#content').innerHTML = text;
});
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('button').forEach(button => {
button.onclick = function() {
const section = this.dataset.section;
// Add the current state to the history
history.pushState({section: section}, "", `section${section}`);
showSection(section);
};
});
});
- In the 'showSection', we employ the 'history.pushState' function. This adds a new element to our browsing history based on three arguments:
1. Any data associated with the state
2. A title parameter ignored by most web browsers
3. What should be displayed in the URL (Ex. section followed by section number - I can go to /section1, /section2.. for instance, and those will appear in the URL bar when I click on a different page. )
- The other change is in setting the 'onpopstate' parameter, which specifies what we should do when the user clicks the back arrow(<-).
- In this case, we want to show the previous section when the button is pressed.
- When I pop something off of the history, like go back in my history, we have the ability to take some event as an argument.
- event.state.section: What state was stored associated with that part of the user's history
=> When I click on one of those buttons, not only do I see text, but I also see in the URL bar that I'm not on /section1. That has been pushed onto my history, and I've updated the URL to reflect that too. Whatever section is clicked, URL will update. I've associated some state with them so that I can go back if I ever need to.
If I open up the JavaScript console and I go back to section 2 from section 3, I'll see what gets logged is the number 2. When I print out what is the current section that's associated with this URL, it's saving that state, that I should be loading section number 2.
Scroll
> In order to update and access the browser history, we used an JavaScript object 'window'. It represents the physiccal window on the computer screen that displays all of their web content. There are other properties of the window.
- window.innerWidth: Width of window in pixels
- window.innerHeight: Height of window in pixels
> While the window represents what is currently visible to the user, the document refers to the entire web page, which is often much larger than the window, forcing the user to scroll up and down to see the page's contents.
- window.scrollY: How many pixels we have scrolled from the top of the page
- document.body.offsetHeight: The height in pixels of the entire document
+ '=='가 아닌 '>='를 쓰는 이유: scrollY나 offsetHeight 등이 소수점 단위로 계산되기도 하고, 브라우저마다 미세한 차이가 생김. 유저가 너무 빨리 스크롤하면 scrollY값이 한 번에 offsetHeight 보다 더 커지는 경우도 있음. 컨텐츠가 동적으로 추가되어 높이가 조금씩 달라지거나 페이지 길이가 늘어날 수도 있음.
- Let's change the background colour to green when we reach the bottom of a page
#scroll.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Scroll</title>
<script>
// Event listener for scrolling
window.onscroll = () => {
// Check if we're at the bottom
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
// Change color to green
document.querySelector('body').style.background = 'green';
} else {
// Change color to white
document.querySelector('body').style.background = 'white';
}
};
</script>
</head>
<body>
<p>1</p>
<p>2</p>
<!-- More paragraphs left out to save space -->
<p>99</p>
<p>100</p>
</body>
</html>
- window.onscroll = () =>{ : onscroll: An event that listens for when I'm scrolling through the window
- if (window.innerHeight + window.scrollY >= document.body.offsetHeight) : If I've reached to the end of the page
- document.querySelector('body').style.background = 'blue'; : Change the background colour to blue.
- Otherwise, if we haven't reached the end of the page, take the body of the page and change its background colour to white.
(26:30)
Infinite Scroll
> We may want to detect that we're at the end of the page if we want to implement infinite scroll.
- If you're on a social media site, you don't want to have to load all posts at once, you might want to load the first ten, and then when the user reaches the bottom, load the next ten.
(scroll(app) - posts(app))
> This app has two paths in #urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("posts", views.posts, name="posts")
]
- A default URL that loads an index route, a posts route that loads this posts view.
> Two corresponding views in #views.py
import time
from django.http import JsonResponse
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request, "posts/index.html")
def posts(request):
# Get start and end points
start = int(request.GET.get("start") or 0)
end = int(request.GET.get("end") or (start + 9))
# Generate list of posts
data = []
for i in range(start, end + 1):
data.append(f"Post #{i}")
# Artificially delay speed of response
time.sleep(1)
# Return list of posts
return JsonResponse({
"posts": data
})
- def index(request): Load a file called index.html.
- def posts(request): 'start' for what post I want to start with, and 'end' for what post I want to end with.
- data.append(f"Post #{i}"): Generate some sample posts
> The posts view requires two arguments: start & end point
- In this view, we've created our own API, which we can test out by visiting the url 'localhost:8000/posts?start=10&end=15', which returns the following JSON:
{
"posts": [
"Post #10",
"Post #11",
"Post #12",
"Post #13",
"Post #14",
"Post #15"
]
}
> In the 'index.html' template that the site loads, we start out with only an empty 'div' in the body and some styling. We load our static files at the beginning, and then we reference a JavaScript file within our 'static' folder.
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>My Webpage</title>
<style>
.post {
background-color: #77dd11;
padding: 20px;
margin: 10px;
}
body {
padding-bottom: 50px;
}
</style>
<script scr="{% static 'posts/script.js' %}"></script>
</head>
<body>
<div id="posts">
</div>
</body>
</html>
- The body has a div for all the posts that initially is going to be empty.
> Wait until a user scrolls to the end of the page and then load more posts using our API
// Start with first post
let counter = 1;
// Load posts 20 at a time
const quantity = 20;
// When DOM loads, render the first 20 posts
document.addEventListener('DOMContentLoaded', load);
// If scrolled to bottom, load the next 20 posts
window.onscroll = () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
load();
}
};
// Load next set of posts
function load() {
// Set start and end post numbers, and update counter
const start = counter;
const end = start + quantity - 1;
counter = end + 1;
// Get new posts and add posts
fetch(`/posts?start=${start}&end=${end}`)
.then(response => response.json())
.then(data => {
data.posts.forEach(add_post);
})
};
// Add a new post with given contents to DOM
function add_post(contents) {
// Create new post
const post = document.createElement('div');
post.className = 'post';
post.innerHTML = contents;
// Add post to DOM
document.querySelector('#posts').append(post);
};
- let counter = 1;: Keep track of what post we need to load next. Start by loading post number 1.
- const quantity = 20; : How many posts are we going to load at a time.
- document.addEventListener('DOMContentLoaded', load); : When DOM content has loaded, call the load function.
- function load() : Figures out what the start and end should be, it fetches all the new posts. We're asynchronously asking for new posts.
- function add_post(contents) : Creates a new div, populates the post inside of it, and adds it to the DOM.
- window.onscroll = (): If I have scrolled to the end of the page, call the load function.
=> Every time I scroll to the bottom, more posts are going to load after that, allowing me to effectively implement this idea of infinite scrolling by taking advantage of some JavaScript techniques, where I can check for when I've got to the end of the page and then dynamically do something as a result of that.
> ERROR
(CS50Lec6 > scroll > scroll > urls.py )
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('posts.urls')),
]
(CS50Lec6 > scroll > scroll > settings.py)
INSTALLED_APPS = [
'posts'
Animation
Add some animation to a site. It turns out that in addition to providing styling, CSS makes it easy for us to animate HTML elements.
> To create an animation in CSS, use the format below, where the animation specifies can include starting and ending styles (to & from) or styles at different stages in the duration (anywhere from 0% to 100%)
@keyframes animation_name {
from {
/* Some styling for the start */
}
to {
/* Some styling for the end */
}
}
or
@keyframes animation_name {
0% {
/* Some styling for the start */
}
75% {
/* Some styling after 3/4 of animation */
}
100% {
/* Some styling for the end */
}
}
> Then, to apply an animation to an element, include the 'animation-name', the 'animation-curation' (in seconds), and the 'animation-fill-mode'(typically forwards).
Ex) A page where a title grows when we first enter the page
#animate.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Animate</title>
<style>
@keyframes grow {
from {
font-size: 20px;
}
to {
font-size: 100px;
}
}
h1 {
animation-name: grow;
animation-duration: 2s;
animation-fill-mode: forwards;
}
</style>
</head>
<body>
<h1>Welcome!</h1>
</body>
</html>
- @keyframe: Where should the element start, what should its style properties be, and then at the end, what should its style properties be. CSS is going to take care of the process of figuring out what needs to happen in all those intermediary fractions of seconds, for example.
- animation-fill-mode: What direction should the animation move in.
- We're going to animate all of our headings using an animation called grow.
=> I would like to apply an animation called grow to all of my heading. This animation should last 2 seconds and go forwards. The grow animation will start with a font size of 20 pixels, and at the end, it'll grow to a font size of 100 pixels.
: Over the course of 2 seconds, it goes from smaller to larger by obeying those keyframes.
- How we can change the position or a heading just by changing a few lines:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Animate</title>
<style>
@keyframes move {
from {
left: 0%;
}
to {
left: 50%;
}
}
h1 {
position: relative;
animation-name: move;
animation-duration: 2s;
animation-fill-mode: forwards;
}
</style>
</head>
<body>
<h1>Welcome!</h1>
</body>
</html>
- position: relative; : Its position should be relative to other elements or other things than its parent
- from{left: 0%}to{left: 50%} : Change position from being 0% away from the left side of the screen to being 50%
> Set some intermediate CSS properties, specify the style at any percentage of the way through an animation.
Ex) We'll move the title from left to right, and then back to left by altering only the animation from above
@keyframes move {
0% {
left: 0%;
}
50% {
left: 50%;
}
100% {
left: 0%;
}
}
- animation-iteration-count: 2; : Rather than just do the animation once and then stop, do the animation twice and then stop. (Or 'infinite' to never stop performing)
> If we want to repeat an animation multiple time, we can change the 'animation-iteration-count' to a number higher than one (or even 'infinite' for endless animation).
- Animation properties: We can set in order to change different aspects of our animation.
- In addition to CSS, we can use JavaScript to further control our animations.
- Use our moving header example (with infinite repetition) to show how we can create a button that starts and stops the animation.
- Assuming we already have an animation, button and heading, we can add the following script to start and pause the animation:
document.addEventListener('DOMContentLoaded', function() {
// Find heading
const h1 = document.querySelector('h1');
// Pause Animation by default
h1.style.animationPlayState = 'paused';
// Wait for button to be clicked
document.querySelector('button').onclick = () => {
// If animation is currently paused, begin playing it
if (h1.style.animationPlayState == 'paused') {
h1.style.animationPlayState = 'running';
}
// Otherwise, pause the animation
else {
h1.style.animationPlayState = 'paused';
}
}
})
- 'animationPlayState' should start out as paused, by first getting the h1 element and then modifying the 'animationPlayState' property of that particular element.
- function(): Get me the heading, pause this initially, and every time the button is clicked, run this function where the function says if we're paused, start running the animation. Otherwise, pause the animation by modifying that 'animationPlayState' property of the heading.
- Initially, there's a button and everything is paused. When I click the button, the animation begins. I can control when to start and when to pause that animation.
> How we can apply our new knowledge of animations to the posts page we made earlier
- We want the ability to hide posts once we're done reading them.
- Let's imagine a Django project identical to the one we just created, but with some slightly different HTML and JavaScript.
- Make 'add_post' function, also add a button to the right side of the post:
// Add a new post with given contents to DOM
function add_post(contents) {
// Create new post
const post = document.createElement('div');
post.className = 'post';
post.innerHTML = `${contents} <button class="hide">Hide</button>`;
// Add post to DOM
document.querySelector('#posts').append(post);
};
> Hide a post when the 'hide' button is clicked.
- To do this, add an event listener that is triggered whenever a user clicks anywhere on the page.
- Then write a function that takes in the 'event' as an argument, which is useful because we can use the 'event.target' attribute to access what was clicked on.
- Also use the 'parentElement' class to find the parent of a given element in the DOM.
// If hide button is clicked, delete the post
document.addEventListener('click', event => {
// Find what was clicked on
const element = event.target;
// Check if the user clicked on a hide button
if (element.className === 'hide') {
element.parentElement.remove()
}
});
- document.addEventListener('click', event => {: The event listener takes in can take as an optional argument the event itself, which is a JavaScript object that contains information about the event that happened.
- const elemtent = event.target;: Save event.target inside of a variable called element, where the idea now is that whatever gets clicked on, that's the event's target.
- if (element.className === 'hide'){element.parentElement.remove(): What was clicked on is something with the class of hide, take the element and remove its parent.
- The post is a div and its child element is the Hide button. If you want to remove the post as well, you need to remove not the element, but the element's parent.
> We've implemented the hide button, but it doesn't look nice.
- We want to have the post fade away and shrink before we remove it.
- In order to do this, first create a CSS animation.
- The animation below will spend 75% of its time changing the 'opacity' from 1 to 0, which esentially makes the post fade out slowly. It then spends the rest of the time moving all of its 'height' related attributes to 0, effectively shrinking the post to nothing.
@keyframes hide {
0% {
opacity: 1;
height: 100%;
line-height: 100%;
padding: 20px;
margin-bottom: 10px;
}
75% {
opacity: 0;
height: 100%;
line-height: 100%;
padding: 20px;
margin-bottom: 10px;
}
100% {
opacity: 0;
height: 0px;
line-height: 0px;
padding: 0px;
margin-bottom: 0px;
}
}
- Opacity: A CSS property that just controls how opaque or how transparent an HTML element happens to be. / Initially, we can fully see the element. And at the end, the element is totally transparent.
- For the first 75% of the animation the only thing that changes is the opacity.
- In the last 25% of the animation, the post is already transparent. Can't see it, but it's still taking up physical space on the page.
> Add this animation to our post's CSS.
- We initially set the 'animation-play-state' to 'paused', meaning the post will not be hidden by default.
.post {
background-color: #77dd11;
padding: 20px;
margin-bottom: 10px;
animation-name: hide;
animation-duration: 2s;
animation-fill-mode: forwards;
animation-play-state: paused;
}
- animation-play-state: paused; : Initially, I don't want the animation to be running, because I don't want to hide all the posts immediately.
> Finally, we want to be able to start the animation once the 'hide' button has been clicked, and then remove the post.
- Edit our JavaScript from above:
// If hide button is clicked, delete the post
document.addEventListener('click', event => {
// Find what was clicked on
const element = event.target;
// Check if the user clicked on a hide button
if (element.className === 'hide') {
element.parentElement.style.animationPlayState = 'running';
element.parentElement.addEventListener('animationend', () => {
element.parentElement.remove();
});
}
});
- element.parentElement.style.animationPlayState = 'running'; : Instead of removing the element right away, just take the parentElement(the post itself) and set its animationPlayState equal to running - meaning when I click the Hide button, run the animation that will change the opacity from 1 to 0.
- element.parentElement.addEventListener('animationend', () =>{element.parentElement.remove(); : When the animation is over, remove the element.
React
We need to write by employing a JavaScript framework, just as we employed Bootstrap as a CSS framework to cut down on the amount of CSS we actually had to write.
> React: One of the most popular JavaScript framework. A library.
- Based on the idea of declarative programming
> In this course, we've been using imperative programming methods where we give the computer a set of statements to execute.
Ex) Update the counter in an HTML page
View:
<h1>0</h1>
Logic:
let num = parseInt(document.querySelector("h1").innerHTML);
num += 1;
document.querySelector("h1").innerHTML = num;
- React allows us to use declarative programming, which will allow us to simply write code explaining what we wish to display and not worry about how we're displaying it.
Ex) In React, a counter might look a bit more like this
View:
<h1>{num}</h1>
- Fill in the number here
Logic:
num += 1;
> The React framework is built around the idea of components, each of which can have an underlying state.
- A component would be something you can see on a web page like a post or a navigation bar, and a state is a set of variables associated with that component.
- When the state changes, React will automatically change the DOM accordingly.
> Start directly in an HTML file. To do this, import three JavaScript Packages
- React: Defines components and their behaviour
- ReactDOM: Takes React components and inserts them into the DOM
- Babel: Translates from JSX, the language in which we'll write in React, to plain JavaScript that our browsers can interpret. JSX is very similar to JavaScript with some additional features, including the ability to represent HTML inside of our code.
#react.html
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/react@17/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<title>Hello</title>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
function App() {
return (
<div>
Hello!
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#app"));
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://unpkg.com/react@18/umd/react.production.min.js"crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<title>React</title>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
function App(){
return (
<div>
Hello!
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector("#app"));
root.render(<App />);
</script>
</body>
</html>
- Import the latest versions of React, ReactDOM, and Babel.
- body: Include a single 'div' with an 'id' of 'app'. Leave this empty, and fill it in our react code.
- Include a script tag where we specify that 'type="text/babel". This signals to the browser that the following script needs to be translated using Babel.
- Create a component called 'App'. Components in React can be represented by JavaScript functions.
- Component returns what we would like to render to the DOM. In this case, '<div>Hello!</div>.
- Employ the 'ReactDOM.render' function, which takes two arguments: A component to render, an element in the DOM inside of which the component should be rendered.
+ The React component must be a name that begins with a capital letter. If you write it in lowercase, React considers it an HTML tag.
+ 2025y: Use React 18 and 'ReactDOM.createRoot()'
- Inside of the div, rather than just render some text, use curly braces to say plug in the value of some JavaScript expression.
> Render components within other components.
function Hello(props) {
return (
<h1>Hello</h1>
);
}
> Render three 'Hello' components inside of our 'App' component:
function App() {
return (
<div>
<Hello />
<Hello />
<Hello />
</div>
);
}
- <Hello />: function Hello()
> Make these components more flexible by adding additional properties to them.
- For example, say hello to three different people. We can provide those people's names in a method that looks similar to HTML attributes:
function App() {
return (
<div>
<Hello name="Harry" />
<Hello name="Ron" />
<Hello name="Hermione" />
</div>
);
}
- Hello component is accepting this prop, this property called name, which is different for all three of these components.
> Access those props using 'props.PROP_NAME'. Then insert this into our JSX using curly braces:
function Hello(props) {
return (
<h1>Hello, {props.name}!</h1>
);
}
- Whatever the name prop is, plug that in right inside of the hello component.
function Hello(props){
return (
<h1>Hello, {props.name}</h1>
);
}
function App(){
return (
<div>
<Hello name="Minerva" />
<Hello name="Severus" />
<Hello name="Lucius" />
</div>
);
}
> State means any kind of data that we want to store inside of the component itself.
> How we can use React to re-implement the counter page we built when first working with JavaScript.
- Inside of our 'App' component, we'll use React's 'useState' hook to add state to our component.
- The argument to 'useState' is the initial value of the state, which we'll set to '0'.
- The function returns both a variable representing the state and a function that allows us to update the state.
const [count, setCount] = React.useState(0);
> Work on what the function will render, where we'll specify a header and a button.
- Also add an event listener for when the button is clicked, which React handles using the 'onClick' attribute(capital C: A common React convention when defining event handlers):
return (
<div>
<h1>{count}</h1>
<button onClick={updateCount}>Count</button>
</div>
);
> Finally, define the 'updateCount' function.
- Use the 'setCount' function, which can take as argument a new value for the state.
function updateCount() {
setCount(count + 1);
}
<script type="text/babel">
function App(){
const [count, setCount] = React.useState(0);
function updateCount() {
setCount(count+1);
}
return (
<div>
<div>{count}</div>
<button onClick={updateCount}>Count</button>
</div>
);
}
Addition
> Build a game-like site where users will solve addition problems.
- Create a new file with the same setup as our other React pages.
- We should include anything that we think might change while a user is on our page.
- num1: The first number to be added
- num2: The second number to be added
- response: What the user has typed in
- score: How many questions the user has answered correctly
> Our state can be a JavaScript object that includes all of this information:
const [state, setState] = React.useState({
num1: 1,
num2: 1,
response: "",
score: 0
});
- A JavaScript object that has keys and values. And I can call that state and have a variable and have a function called setState that is going to update the value of that state.
- Add a third part of the state called response that initially will just be the empty string.
> Using the values in the state, we can render a basic a basic user interface.
return (
<div>
<div>{state.num1} + {state.num2}</div>
<input value={state.response} />
<div>Score: {state.score}</div>
</div>
);
- Instead of rendering literally the number 1, using curly braces {state.num1} to decide what it is going to appear inside of the user interface.
- Give a value to the input field which is going to be 'state.response'. Whatever the user typed in as the response, that's stored inside of the state, and that is going to be the value of what shows up in the input field. Whatever is in the input field will have access to it inside of this 'state.response' variable.
- Figure the score out from the state and display it in the user interface.
*ERROR
- If you use {} to return JSX from React's component function, it is recognised as a JavaScript object, causing problems. JSX should be wrapped in () to be correctly recognised.
> At this point, the user cannot type anything in the input box because its value is fixed as 'state.response' which is currently the empty string.
- To fix this, add an 'onchange' attribute to the input element, and set it equal to a function called 'updateResponse'.
onChange={updateResponse}
> We'll have to define the 'updateResponse' function, which takes in the event that triggered the function, and sets the 'response' to the current value of the input.
- This allows the user to type, and stores whatever has been typed in the 'state'.
function updateResponse(event) {
setState({
...state,
response: event.target.value
});
}
- Because it's an event handler, it can accept an argument, which is the event itself.
- If I wan to figure out what it is the user has typed into the input field, I can get at that with 'event.target.value'.
- A response to no longer be the empty string, but to be event.target.value. And that is going to be the new value for response.
- ...state : Spread operator: Use the existing values of the state for everything else, like num1. The only thing to override is the new value for the response.
=> I'd like to update the state. Everything should stay the same except for response, which is now going to be 'event.target.value' -whatever it is the user typed in into that input field.
> Add the ability for a user to submit a function.
- First add another event listener and link it to a function.
onKeyPress={inputKeyPress}
> Define the 'inputKeyPress' function.
- First check whether the 'Enter' key was pressed, and then check to see if the answer is correct.
- When the answer is correct, increase the score by 1, choose random numbers for the next problem, and clear the response.
- If the answer is incorrect, decrease the score by 1 and clear the response.
function inputKeyPress(event) {
if (event.key === "Enter") {
const answer = parseInt(state.response);
if (answer === state.num1 + state.num2) {
// User got question right
setState({
...state,
score: state.score + 1,
response: "",
num1: Math.ceil(Math.random() * 10),
num2: Math.ceil(Math.random() * 10)
});
} else {
// User got question wrong
setState({
...state,
score: state.score - 1,
response: ""
})
}
}
}
- This event is going to happen any time a key is pressed, regardless of whether it's a letter or a number or the Enter key. So I want to check to make sure that the key is actually the Enter key.
- 'state.response' is a string. It's possible the user is going to type in some letters or other characters. => Convert the response into an integer. Define a variable called answer using the JS function parseInt that takes a string and tries to convert it into an integer.
- ...state, : All of the state is going to be the same.
- When the user gets the question right, let's update num1 and num2 also.
- Math.random(): Generates a random number between 0 and 1. Multiply it by 10 to get a number between 0 and 10.
- Math.ceil: We don't want any decimals to appear in the number. Take the ceiling of the number to say if the number was like 5.7, round that up to 6.
- response:"" : Clear out the response.
> <input autoFocus={true} : The input field automatically focuses when I load the page for the first time. The input field will already highlighted, and immediately I can start to try to play this.
> To put some finishing touches on the application, add some style to the page.
- Center everything in the app, and then make the problem larger by adding an 'id' of 'problem' to the div containing the problem, then adding the following CSS to a style tag:
#app {
text-align: center;
font-family: sans-serif;
}
#problem {
font-size: 72px;
}
> <div className="incorrect" id="problem"> ,.incorrect {color: red;} : Give the problem a class of 'incorrect'.When the user has just gotten the question wrong, turn all incorrect text to be red.
- incorrect: false : I want to keep track of did the user just answer a question incorrectly or not. Add 'incorrect' to 'const[state, setState]' and initially, it'll be false.
- <div className={state.incorrect ? "incorrect" : ""}: Rather than have it be incorrect all the time, add in curly braces an expression. If 'state.incorrect' is true, using the ternary operator with a question mark, then the class should be incorrect. But otherwise, it shouldn't have a class of incorrect, just be the empty string.
- incorrect: false, incorrect:true: Add these in 'setState' call. Set 'incorrect' equal to false or true based on whether the user got the question right or wrong.
> Finally, add the ability to win the game after gaining 10 points,
- Add a condition to the 'render' function, returning something completely different different once we have 10 points.
if (state.score === 10) {
return (
<div id="winner">You won!</div>
);
}
- Each React component can be a JS function. And this function is returning this div, but it's a function so I can add addition logic to it.
> To make the win more exciting, add some style to the alternative div.
#winner {
font-size: 72px;
color: green;
}
댓글 없음:
댓글 쓰기