Link to this page: https://goo.gl/p5sZJz
Aloha CUNY Hackathon 2017!!! This has been updated for the November 2017 CUNY Hackathon.
tl;dr
- I'm Leo Rudberg. I ❤️ hackathons!
- Thanks to Shaun for helping me debug this!
- I'll show you how to get an Angular (Version ≥ 2) app up and running.
- For the purposes of this guide, assume we are using Angular 5 (it's a lot like 2).
- I'm going to write terminal commands in the "UNIX" style, but this should be very similar for Windows
- PLEASE let me know if you have questions (find me or comment below).
- I'm going to assume that you have a small amount of programming knowledge (like at least one college-level CS class). Also, some basic HTML and CSS experience, but I'll explain those parts, anyway.
- I'm also going to assume that someone (like me) has given a brief intro on Angular and why it is great!
- LET'S MAKE SOMETHING AWESOME!
- Getting Started
- Initial launch and explanation
- Milestones
- Conclusion
Choose your path...
For the purposes of the CUNY Hackathon, we are taking the second path.
### I wanna hack now!
Ok, go to the Plunker seed which can be seen from the QuickStart page
There does not seem to be an offical Angular Plunker seed. Let's use the Angular CLI instead.
¯\_(ツ)_/¯
Follow along with the directions from the Angular QuickStart guide.
Here's a summary:
First, we need to install a few things!
- Node
- Node (and
npm
) is the server-side (i.e. non-browser) tooling we need to get off the ground. - Official downloads
- There are other ways to download (e.g.
homebrew
); choose your favorite flavor of installation method.
- Node (and
- Editor
- I totally recommend VSCode as it has great TypeScript support.
- Other editors are ok, as well.
- Angular CLI
- The Angular CLI (
ng
) makes our lives easier by giving us a pre-built setup and codegen. - Install info here
- The Angular CLI (
- Finally, make sure you have a modern browser and
git
ready if you want to use it. - Generate a new project following the steps here
So, you should see an Angular app before you. An Angular app can be thought of as a tree of Components, which change and interact via Service, Pipes, Directives, Observables and other fancy tools.
Although this is (fairly) correct description of Angular, it is not very concrete or beginner friendly.
My concrete description of Angular is that is a framework for building scalable front-end web apps by giving power to HTML through custom Components. Think of Components as handmade, superpowered DOM elements.
With vanilla HTML, we are pretty much stuck with things like <div>
, <input>
,
<img>
, and a few others. As web apps get more complex, we need more
powerful, abstract components that allow for larger-scale interoperability -- this is where
Angular really shines.
Let's start hacking!
You should see "Hello Angular!" in the Plunker demo. To the left are the files
we'll be working with. We really only care about the files in the app/
directory.
The Plunker app is an app made of one Component: AppComponent
. A Component
is just a TypeScript class
with a @Component
decorator on it.
Inside the decorator, we put:
selector
: the kabob-case/HTML-like name of the Componenttemplate
: the HTML template for the componenttemplateUrl
: the path to the HTML template (recommended overtemplate
)styleUrls
: a list of.css
files to apply to the Component
Inside the class, we put all of the scope logic for the Component ($scope
for Angular 1 users).
In the example, we see that the Component has a field called name
which is
set to 'Angular'
. Try mutating that string and see what happens.
Voila! The magic of Angular!
Next, we will begin working on our own version of hackertyper.com.
You will need to add a CSS file (e.g. app/app.component.css
) and put it in the component's styleUrls
list if it's not there already.
Use height: 100vh;
to make the app take the full screen. Make the background-color: black
, the font-family: monospace
,
and the color: lime
or limegreen
for the true hacker experience. Adding white-space: pre-line
can also make your content behave like its
in a <pre>
, without needing the actual element.
We will put all of our code in a simple <div>
container with the main-content
class in our component, so we'll use the .main-content
selector to style.
After making all of those changes, app/app.component.css
should look something like:
.main-content {
/* Even without any text, our div occupies the full height of the screen. */
min-height: 100vh;
/* Our main content should always take up the full height and width. */
height: 100%;
width: 100%;
/* Center the content naturally. */
margin: 0 auto;
/* Preserve the newlines and whitespace in the code. */
white-space: pre-line;
/* Finally, make the content look like an old terminal. */
background-color: black;
color: limegreen;
font-family: monospace;
}
Similarly, create an app/app.component.html
file and make it the value of the templateUrl
if it's not already.
Right now, we just need a container element and the content on the page. Like I mentioned earlier, a simple <div>
can contain our main content.
After making the edits, your entire app/app.component.html
file should look something like:
<div class='main-content'></div>
First, I recommend creating an offset
field of the AppComponent
class that keeps track of where we are in the other string field, allContent
, which contains the complete content that will be written to the screen.
We can add these like so:
// ...
export class AppComponent {
offset: number = 0;
allContent: string = '';
// ...
}
Let's add a HostListener
that will handle our keypresses on the component.
We can add it like so:
@HostListener('document:keydown', ['$event'])
onKeyPress(event: KeyboardEvent) {
// Add your logic here.
}
What we want to do is, on every keypress, get a random number of characters (~1-5) sliced off from the content, and add it to the view. In otherworks, increment our offset
value by a (random) amount.
How does the content get written to the screen though?
We can add the following:
// ... somewhere in the AppComponent class
get viewContent() {
return this.allContent.slice(0, this.offset);
}
This is a special syntax in both JavaScript and TypeScript that makes it seem like AppComponent
has a field call viewContent
, but it is secretly a method that uses our other two fields. Accessors like these are useful for when you have data that can be purely computed from other data, but needs to be present in the view.
Finally, we can make our viewContent
appear by adding the following in our HTML. Behold, the magic of Angular:
<div class='main-content'>{{viewContent}}</div>
Try typing and see what happens! Be sure to check the Developer Console for any errors that Angular throws.
To make it look like we're actually hacking, let's scroll to the bottom of the container on every keypress.
Accomplish this through setting a footer: HTMLElement
variable in the ngAfterViewInit
lifecycle hook. Then call scrollIntoView
on it in your keypress handler.
Here's what I did to accomplish this:
First, I added the AfterViewInit
interface to the class, so that Angular knows that we are using a lifecycle hook.
export class AppComponent implements AfterViewInit {
Then, I added a ViewChild to the class. This allows our AppComponent to affect a child element in its DOM.
// ... at the top of the AppComponent class
@ViewChild('footer') footer: HTMLElement;
The string inside the ViewChild decorator means that we want to attach to a footer
element.
Next, I set up the element in ngAfterViewInit
, the corresponding lifecycle method:
ngAfterViewInit() {
this.footer = document.getElementById('footer');
}
In our handler for keypresses, just add the following to update the positioning in the view:
this.footer.scrollIntoView(false);
Finally, add the footer to the view:
<div ...>...</div>
<footer id='footer'></footer>
You may need to add a lot of content (I recommend setting allContent
to something like:
'Hello Angular!!!!!\n'.repeat(500);
.
Let's create an AppService
to fetch code from a (bonus points for randomized) repo from GitHub. You'll need to create a service that uses Http
, add it to the AppModule
's providers
list, and add any other necessary modules.
My AppService
looks like this:
import 'rxjs/add/operator/map';
import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
@Injectable()
export class AppService {
constructor(private http: Http) {}
getCode(link: string) {
return this.http.get(link).map(resp => resp.text());
}
}
We then add the service to the AppModule
(app.module.ts
):
import {NgModule} from '@angular/core';
import {HttpModule} from '@angular/http';
import {BrowserModule} from '@angular/platform-browser';
import {AppComponent} from './app.component';
import {AppService} from './app_service';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpModule],
providers: [AppService],
bootstrap: [AppComponent]
})
export class AppModule {}
Then, add it to the constructor to properly inject it.
Use another lifecycle hook, ngOnInit
, to fetch the content and assign it to your allContent
.
Here's how you inject the service into your component:
constructor(private appService: AppService) {}
Finally, you should have something like this:
ngOnInit() {
this.appService.getCode(myURL).subscribe(content => this.allContent = content);
}
Now, my component (app.component.ts
) looks like:
import {AfterViewInit, Component, HostListener, OnInit, ViewChild} from '@angular/core';
import {AppService} from './app_service';
// I like this file because it has 'batman' in it.
// Feel free to use whatever code you like!
const myURL =
'https://raw.githubusercontent.com/torvalds/linux/master/net/batman-adv/distributed-arp-table.c';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
offset: number = 0;
allContent: string = 'Hello world, and welcome to Angular!!!!!\n'.repeat(500);
@ViewChild('footer') footer: HTMLElement;
constructor(private appService: AppService) {}
ngOnInit() {
this.appService.getCode(myURL).subscribe(
content => this.allContent = content);
}
ngAfterViewInit() {
this.footer = document.getElementById('footer');
}
@HostListener('document:keydown', ['$event'])
onKeyPress(event: KeyboardEvent) {
console.log(`got keypress: ${event.key}`);
const val = this.randomValue();
if (this.offset + val > this.allContent.length) {
return;
}
this.offset += val;
this.footer.scrollIntoView(false);
}
private randomValue(max = 5) {
return Math.floor(Math.random() * max);
}
get viewContent() {
return this.allContent.slice(0, this.offset);
}
}
Let's talk about how the app bootstraps itself.
index.html
has<my-app>
, which is the selector of the AppComponent.main.ts
bootstraps the AppModule as the root of the tree.- AppComponent's selector becomes visible to the App, and then it is drawn to the page.
(Rough) solution here: https://goo.gl/XqbgIR