Categories
JavaScript Nodejs

How To Send Tweets From Your App With the Twitter API

Using the twit module to send tweets and get tweet history

If you’ve followed the steps to build the Twitter sign in to your app, you can sign in to your Twitter account and get the OAuth request token and secret.

Now we can use it to do a lot of great things, like sending tweets and getting the messages you tweeted in the past.

The back end is built with Express and the front end is built with Angular. We built the basic parts of our Twitter app in that tutorial. It has sign up, log in, and Twitter sign-in functionality.

Now, we need to add Twitter functionality so the app is useful.

To achieve this functionality, we can use the twit Node.js module. We download it by running npm i twit.

As Twitter tweets have a 280 character limit, we also need to use a link shortener to shrink our links. We do that with the Bitly Node package, which we add by running npm i bitly.

As our existing app uses Sequelize as our ORM, we can run database migrations to change the structure of our database. Here’s an article on how to use Sequelize including making models and table relations.

To make things easy, if you’re continuing from the app you built before, we remove a few things and start over. If you’re in production, you have to make migrations and add or change to achieve the same database structure.

We make a migration and add the following:

'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userName: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
hashedPassword: {
type: Sequelize.STRING
},
hashedOauthRequestToken: {
type: Sequelize.STRING
},
hashOauthRequestTokenSecret: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
}
};

Make another migration and add:

'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Tweets', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
content: {
type: Sequelize.TEXT
},
usersId: {
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Tweets');
}
};

Then, in the models, which should exist if we follow the tutorial on how to build the app with Twitter sign in, we put this in models/users.js:

'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
userName: DataTypes.STRING,
email: DataTypes.STRING,
hashedPassword: DataTypes.STRING,
hashedOauthAccessToken: DataTypes.STRING,
hashedOauthAccessTokenSecret: DataTypes.STRING,
activated: DataTypes.BOOLEAN,
passwordResetToken: DataTypes.STRING
}, {});
User.associate = function (models) {
User.hasMany(models.Tweet, {
foreignKey: 'userId',
sourceKey: 'id'
});
};
return User;
};

And put the following in tweet.js:

'use strict';
module.exports = (sequelize, DataTypes) => {
const Tweet = sequelize.define('Tweet', {
content: DataTypes.TEXT,
userId: DataTypes.INTEGER,
scheduled: DataTypes.BOOLEAN
}, {});
Tweet.associate = function (models) {
Tweet.hasMany(models.TweetRecord, {
foreignKey: 'tweetId',
sourceKey: 'id'
});
  Tweet.belongsTo(models.User, {
foreignKey: 'userId',
targetKey: 'id'
});
};
  return Tweet;
};

We have the models and the database structure in place to build our app after running npx sequelize db:migrate.

We make a new controller to store the Twitter tweet logic. Create a file called tweetsController.js and put in the following:

const express = require('express');
const models = require('../models');
const jwt = require('jsonwebtoken');
const Twit = require('twit');
const Op = require('sequelize').Op;
const CryptoJS = require("crypto-js");
const shortUrl = require('node-url-shortener');
const router = express.Router();
import { authCheck } from '../middlewares/authCheck';
import { saveTweetRecord, saveTweet } from '../services/tweetService';
import { BitlyClient } from 'bitly';
const bitly = new BitlyClient(process.env.BITLY_TOKEN, {});
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
router.get('/get/:id', authCheck, async (req, res) => {
try {
const id = req.params.id;
const tweets = await models.Tweet.findAll({ where: { id } });
return res.send(tweets[0]);
}
catch (ex) {
return res.send(ex, 500);
}
});
router.get('/list', authCheck, async (req, res) => {
const page = req.params.page;
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const tweets = await models.Tweet.findAll({
where: {
userId,
[Op.or]: [
{ deleted: false },
{ deleted: null }
]
}
});
res.send({
tweets,
count: tweets.length
});
});
router.post('/post', authCheck, async (req, res) => {
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
try {
let content = req.body.content;
let urls = content.match(urlRegex) || [];
urls = urls.filter(u => u && u.startsWith('http') && !u.includes('bit.ly'));
for await (const u of urls) {
const urlResult = await bitly.shorten(u);
content = content.replace(u, urlResult.url);
}
await models.Tweet.create({
...req.body,
content,
userId
})
return res.send({ message: 'Successfully created tweet' });
}
catch (ex) {
return res.send(ex, 500);
}
});
router.put('/update/:tweetId', authCheck, async (req, res) => {
try {
const token = req.headers.authorization;
const tweetId = req.params.tweetId;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
let content = req.body.content;
let urls = content.match(urlRegex) || [];
urls = urls.filter(u => u && u.startsWith('http') && !u.includes('bit.ly'));
for await (const u of urls) {
const urlResult = await bitly.shorten(u);
content = content.replace(u, urlResult.url);
}
await models.Tweet.update({
...req.body,
content,
userId
}, {
where: {
id: tweetId
}
})
return res.send({ message: 'Successfully updated tweet' });
}
catch (ex) {
console.log(ex);
return res.send(ex, 500);
}
});
router.delete('/delete/:id', authCheck, async (req, res) => {
const id = req.params.id;
try {
await models.Tweet.update(
{
deleted: true
}, {
where: { id }
}
)
return res.send({ message: 'Successfully deleted tweet' });
}
catch (ex) {
return res.send(ex, 500);
}
});
router.post('/sendTweet/:id', authCheck, async (req, res) => {
const id = req.params.id;
const token = req.headers.authorization;
try {
const tweets = await models.Tweet.findAll({
where: { id }
})
const tweet = tweets[0];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const users = await models.User.findAll({
where: { id: userId }
})
const user = users[0];
const accessTokenBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessToken, process.env.OAUTH_SECRET);
const oauthAccessToken = accessTokenBytes.toString(CryptoJS.enc.Utf8);
const accessTokenSecretBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessTokenSecret, process.env.OAUTH_SECRET);
const oauthAccessTokenSecret = accessTokenSecretBytes.toString(CryptoJS.enc.Utf8);
const T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: oauthAccessToken,
access_token_secret: oauthAccessTokenSecret,
timeout_ms: 60 * 1000,
strictSSL: true
})
T.post('statuses/update', { status: tweet.content }, (err, data, response) => {
saveTweetRecord(id);
res.send({ message: 'Successfully sent tweet' });
})
}
catch (ex) {
return res.send(ex, 500);
}
});
router.get('/importOldTweets', authCheck, async (req, res) => {
const token = req.headers.authorization;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const users = await models.User.findAll({
where: { id: userId }
})
const user = users[0];
const accessTokenBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessToken, process.env.OAUTH_SECRET);
const oauthAccessToken = accessTokenBytes.toString(CryptoJS.enc.Utf8);
const accessTokenSecretBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessTokenSecret, process.env.OAUTH_SECRET);
const oauthAccessTokenSecret = accessTokenSecretBytes.toString(CryptoJS.enc.Utf8);
         const T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: oauthAccessToken,
access_token_secret: oauthAccessTokenSecret,
timeout_ms: 60 * 1000,
strictSSL: true
})
T.get('statuses/user_timeline', {}, (err, data, response) =>
res.send({ message: 'Successfully imported tweets' });
})
}
catch (ex) {
return res.send(ex, 500);
}
});
module.exports = router;

The ’/get/:id’ route is where we get the tweets stored in our local database. findAll is the function for getting items from a table with Sequelize.

The ‘/post’ route is where we send our tweets to our Twitter account via the Twitter API.

We use the authCheck in our previous tutorial to verify the token before allowing users to send tweets. Then, we get the token from the authorization header from the front end. Then, we get the user ID.

In the try block, we shorten our links through Bit.ly. To use the bitly module, we have to sign up for a bit.ly account and get an API token. Then, we shorten everything that isn’t a Bit.ly URL with the Bit.ly API.

We can update existing tweets with the ’/update/:tweetId’ route. It is very similar to the post route except that you’re retrieving an existing tweet with the tweetId , and updating the tweet in our database.

The '/sendTweet/:id’ is where tweeting magic happens. We use the twit module which talks to the Twitter API to send the tweet.

The delete route does soft delete in case we need the data back. importOldTweets route gets our past tweet history and saves it to our database.

In this block:

const accessTokenBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessToken, process.env.OAUTH_SECRET);
const oauthAccessToken = accessTokenBytes.toString(CryptoJS.enc.Utf8);
const accessTokenSecretBytes = CryptoJS.AES.decrypt(user.hashedOauthAccessTokenSecret, process.env.OAUTH_SECRET);
const oauthAccessTokenSecret = accessTokenSecretBytes.toString(CryptoJS.enc.Utf8);

We get the encrypted OAuth access token and secret which identifies your Twitter account and in the code below we instantiate our Twitter client:

const T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: oauthAccessToken,
access_token_secret: oauthAccessTokenSecret,
timeout_ms: 60 * 1000,
strictSSL: true
})

Then in:

T.post('statuses/update', { status: tweet.content }, (err, data, response) => {
res.send({ message: 'Successfully sent tweet' });
})

We send the tweet content to our Twitter account, letting the world see what you tweeted.

To hash encrypt our OAuth access token and secret, we replace the saveAccessTokens controller with the following:

router.get('/saveAccessTokens', authCheck, (req, res) => {
  consumer.getOAuthAccessToken(
req.query.oauth_token,
req.session.oauthRequestTokenSecret,
req.query.oauth_verifier,
(error, oauthAccessToken, oauthAccessTokenSecret, results) => {
if (error) {
logger.error(error);
res.send(error, 500);
}
else {
const token = req.headers.authorization;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.data.userId;
const hashedOauthAccessToken = CryptoJS.AES
.encrypt(oauthAccessToken, process.env.OAUTH_SECRET)
.toString();
const hashedOauthAccessTokenSecret = CryptoJS.AES
.encrypt(oauthAccessTokenSecret, process.env.OAUTH_SECRET)
.toString();
models.User.update({
hashedOauthAccessToken,
hashedOauthAccessTokenSecret
},
{ where: { id: userId } })
.then((result) => {
return res.send({ message: 'token saved' });
})
.catch((ex) => {
logger.error(ex);
return res.send({ error: ex }, 500);
})
}
});
});

In app.js, we have to add two lines to access our routes that we added:

const tweets = require('./controllers/tweetsController');

Then add:

app.use('/tweets', tweets);

With your other app.use lines.

In the previous tutorial, we didn’t encrypt the confidential access tokens. But, if we want to make a secure app, we must do it. This saves the encrypted credentials instead of the raw ones.

In our Angular app, which we built previously, we have to add one page to see our front-end app.

We need to store our tweets in the store for easy, centralized access. We make a file called tweets-reducer.ts in our reducers folder, then we put the following in:

const SET_TWEETS = 'SET_TWEETS';
function tweetsReducer(state, action) {
switch (action.type) {
case SET_TWEETS:
state = action.payload;
return state;
default:
return state
}
}
export { tweetsReducer, SET_TWEETS };

Then, in reducers/index.ts, we put:

import { menuReducer } from './menu-reducer';
import { tweetsReducer } from './tweets-reducer';
export const reducers = {
menu: menuReducer,
tweets: tweetsReducer
};

In the imports array of app.module.ts, make sure we have the following added:

MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatInputModule,
MatMenuModule,
MatSidenavModule,
MatToolbarModule,
MatTableModule,
FormsModule,
HttpClientModule,
MatDialogModule,
MatDatepickerModule,
MatMomentDateModule,
MatSelectModule,
MatCardModule

We run ng g component tweetForm. This is the dialog for entering tweets. In the newly generated tweet-form.component.ts, we add:

import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { TweetsService } from '../tweets.service';
import { SET_TWEETS } from '../reducers/tweets-reducer';
import { NgForm } from '@angular/forms';
import * as moment from 'moment';
@Component({
selector: 'app-tweet-form',
templateUrl: './tweet-form.component.html',
styleUrls: ['./tweet-form.component.scss']
})
export class TweetFormComponent implements OnInit {
tweetData: any = {};
edit: boolean;
  constructor(
public dialogRef: MatDialogRef<TweetFormComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private tweetsService: TweetsService,
private store: Store<any>
) { }
  ngOnInit() {
if (this.data) {
this.edit = this.data.edit;
this.tweetData = this.data.tweet;
}
}
  getTweets(page: number = 1) {
this.tweetsService.listTweets(page)
.subscribe(res => {
this.store.dispatch({ type: SET_TWEETS, payload: res });
this.dialogRef.close();
})
}
  save(tweetForm: NgForm) {
if (tweetForm.invalid) {
return;
}
const data = Object.assign({}, this.tweetData);
if (this.tweetData.scheduleDate) {
data.scheduleDate = moment(this.tweetData.scheduleDate).format('YYYY-MM-DD');
}
if (!this.edit) {
this.tweetsService.postTweet(data)
.subscribe(res => {
this.getTweets();
})
}
else {
this.tweetsService.updateTweet(this.tweetData.id, data)
.subscribe(res => {
this.getTweets();
})
}
}
}

In tweet-form.component.html, we have:

<h2 mat-dialog-title>{{data.edit ? 'Edit' :'Add'}} Tweet</h2>
<mat-dialog-content>
<form #tweetForm='ngForm' (ngSubmit)='save(tweetForm)'>
<mat-form-field>
<input matInput placeholder="Content" required #content='ngModel' name='content'
[(ngModel)]='tweetData.content' maxlength="140">
<mat-error *ngIf="content.invalid && (content.dirty || content.touched)">
<div *ngIf="content.errors.required">
Content is required.
</div>
</mat-error>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button type='submit' (click)='save(tweetForm)'>Save</button>
</mat-dialog-actions>

The dialog will update the tweets store after saving is successful. It is used for both adding and editing tweets.

Add an entryComponents field to AppModule in app.module.ts. We add the following:

TweetFormComponent

This allows our module to use the dialog that we just created.

We have to make a page to see a table of tweets. We run ng g component tweetsPage.

Then, in the newly created tweets-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { MatDialogRef, MatDialog } from '@angular/material';
import { TweetFormComponent } from '../tweet-form/tweet-form.component';
import { TweetsService } from '../tweets.service';
import { Store, select } from '@ngrx/store';
import { SET_TWEETS } from '../reducers/tweets-reducer';
import { TweetHistoryDialogComponent } from '../tweet-history-dialog/tweet-history-dialog.component';
import { SessionService } from '../session.service';
@Component({
selector: 'app-tweets-page',
templateUrl: './tweets-page.component.html',
styleUrls: ['./tweets-page.component.scss']
})
export class TweetsPageComponent implements OnInit {
tweets: any[] = [];
displayedColumns: string[] = [
'content',
'edit',
'tweet',
'delete'
];
currentTwitterUser = {};
  constructor(
public dialog: MatDialog,
private tweetsService: TweetsService,
private store: Store<any>,
private sessionService: SessionService
) {
store.pipe(select('tweets'))
.subscribe(tweets => {
if (!tweets || !Array.isArray(tweets.tweets)) {
return;
}
this.tweets = tweets.tweets;
})
}
  ngOnInit() {
this.getTweets();
this.getCurrentTwitterAccount();
}
  openDialog(edit: boolean, index: number = null) {
const dialogRef = this.dialog.open(TweetFormComponent, {
width: '70vw',
data: {
edit,
tweet: edit ? this.tweets[index] : {}
}
})
  dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
  getTweets() {
this.tweetsService.listTweets()
.subscribe(res => {
this.store.dispatch({ type: SET_TWEETS, payload: res });
})
}
  deleteTweet(id: number) {
this.tweetsService.deleteTweet(id)
.subscribe(res => {
this.getTweets();
})
}
  sendTweet(id: number) {
this.tweetsService.sendTweet(id)
.subscribe(res => {
alert('Tweet sent');
})
}
  openTweetHistory(index: number) {
const dialogRef = this.dialog.open(TweetHistoryDialogComponent, {
width: '70vw',
data: this.tweets[index] || {}
})
    dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
  getCurrentTwitterAccount() {
this.sessionService.getCurrentTwitterAccount()
.subscribe(res => {
this.currentTwitterUser = res;
})
}
  importOldTweets() {
this.tweetsService.importOldTweets()
.subscribe(res => {
this.getTweets();
alert('Tweets successfully imported.');
})
}
}

To add a table to display our tweets, we put the following in tweet-page.component.html:

<div class="center">
<h1>Tweets</h1>
</div>
<br>
<button mat-raised-button (click)='openDialog(false)'>Add Tweet</button>
<button mat-raised-button (click)='importOldTweets()'>Import Old Tweets</button>
<div id='table'>
<table mat-table [dataSource]="tweets" class="mat-elevation-z8">
       <ng-container matColumnDef="content">
<th mat-header-cell *matHeaderCellDef>
Content
</th>
<td mat-cell *matCellDef="let element">{{element.content}}</td>
</ng-container>
       <ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element; let i = index">
<button mat-raised-button (click)='openDialog(true, i)'>Edit</button>
</td>
</ng-container>
        <ng-container matColumnDef="tweet">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button (click)='sendTweet(element.id)' [disabled]='!currentTwitterUser.id'>
Tweet
</button>
</td>
</ng-container>
        <ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button (click)='deleteTweet(element.id)'>Delete</button>
</td>
</ng-container>
        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>

We add a service for making the HTTP requests. We run ng g service tweets to make tweets.service.ts.

We put in:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class TweetsService {
  constructor(
private http: HttpClient
) { }
  getTweets(id: number) {
return this.http.get(`${environment.apiUrl}/tweets/get/${id}`)
}
  listTweets() {
return this.http.get(`${environment.apiUrl}/tweets/list`)
}
  postTweet(data) {
return this.http.post(`${environment.apiUrl}/tweets/post`, data)
}
  updateTweet(id, data) {
return this.http.put(`${environment.apiUrl}/tweets/update/${id}`, data)
}
  deleteTweet(id: number) {
return this.http.delete(`${environment.apiUrl}/tweets/delete/${id}`)
}
  sendTweet(id: number) {
return this.http.post(`${environment.apiUrl}/tweets/sendTweet/${id}`, {})
}
  getTweetRecord(tweetId: number) {
return this.http.get(`${environment.apiUrl}/tweets/tweetRecord/${tweetId}`)
}
  importOldTweets() {
return this.http.get(`${environment.apiUrl}/tweets/importOldTweets`)
}
}

In the end, we have:

Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

Categories
JavaScript

Using JavaScript’s Object Constructor — Part 1

In JavaScript, the Object constructor lets us create object wrapper with the given values. It will create an empty object if null or undefined is passed into the Object constructor. If the value passed into the constructor is an object already then, it will return the object.

The Object constructor has 2 properties. It has a length property that is always 1, and like all other objects, the Object constructor has a prototype to get all the property additions to the type Object.

The Object constructor has many useful methods that can be used without constructor a new object. Part 1 of this list is below:

Object.assign()

The Object.assign() method makes a shallow copy of an object. The first parameter is a target object that you copy the object to, and the second parameter accepts an object that you want to copy. Note that if the source and target objects have the same properties, the source object’s property value to overwrite the one in the target object. For example, we can write:

const target = { a: 1, b: 2 };  
const source = { b: 3, c: 4};
const newObj = Object.assign(target, source);  
console.log(newObj)

If we run the code, we will get { a: 1, b: 3, c: 4} . We can also copy arrays. For example, we can write:

const targetArr = [1,2];  
const sourceArr = [2,3];
const newArr = Object.assign(targetArr, sourceArr);  
console.log(newArr)

We get [2,3] logged when we run the code above. For arrays, it will overwrite the whole target array with the source array.

Object.create()

The Object.create() method creates a new object with the object you pass in as the prototype of the new object. For example, we can write:

const obj = { a: 1, b: 2, c: 3 };  
const newObj = Object.create(obj);  
console.log(newObj);

In the code above, when we log newObj , it doesn’t have its own properties that weren’t inherited from obj . This is because we only passed in the first argument to the constructor, which is the prototype for the object that’s returned. If we want to add properties that are available only in the returned object, we pass in an object with the property names as keys and the properties writable , configurable , enumerable and value as properties of the property name keys, which is called the property descriptor. For example, we can write:

const obj = {  
  a: 1,  
  b: 2,  
  c: 3  
};

const childObj = {  
  a: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  }  
}  
const newObj = Object.create(obj, childObj);  
console.log(newObj);

In the code above, writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object. The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property.

If we want to create an object that has properties that can’t be changed, then we set writable to false , like in the following code:

const obj = {  
  a: 1,  
  b: 2,  
  c: 3  
};

const childObj = {  
  a: {  
    writable: false,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  }  
}  
const newObj = Object.create(obj, childObj);  
newObj.a = 1;  
newObj.d = 1;  
console.log(newObj);

Notice that the assignment operator has no effect. If we have strict mode enabled, then a TyepError will be thrown. We have {a: “hello”, d: 1} logged in the console.log . This means that writable set to false is working for the property a and writable value set to true is working for the property d .

If we pass in null to the constructor, we get an empty object:

const nullObj = Object.create(null)  
console.log(nullObj)

We will get an error ‘Object prototype may only be an Object or null: undefined’ is we pass in undefined as the prototype of an object like in the code below:

const undefinedObj = Object.create(undefined);  
console.log(undefinedObj)

Object.defineProperty()

The Object.defineProperty() method defines a new property on an object. The first parameter is the object that you want to add the property to. The second parameter is the name of the property you want to add passed in as a string, and the last parameter is the property descriptor included in the Object.create() method when we try to add properties to the returned object. The property descriptor should have the properties writable , configurable , enumerable and value as properties of the property name keys. The writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object. The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property. For example, we can write:

let obj = {};

Object.defineProperty(obj, 'a', {  
  writable: false,  
  configurable: true,  
  value: 'hello'  
})

console.log(obj.a)  
obj.a  = 1;  
console.log(obj.a)

As we can see, the property descriptor acts the same way as in Object.create() . When writable is false , assignment to the property has no effect. If we have strict mode enabled, then a TyepError will be thrown.

We can also define getters and setter function for properties called get and set respectively for a property:

let obj = {};  
let value;

Object.defineProperty(obj, 'a', {  
  get() {  
    return value;  
  },  
  set(a) {  
    value = a;  
  }  
});

console.log(obj.a)  
obj.a = 1;  
console.log(obj.a)

As we can see, in the code above, we defined the property a for the object obj with the get and set functions to get the value of the property and set the value respectively.

Accessor properties are set on the prototype if we defined it on the prototype, but value properties are set on the current object. If an object inherits non-writable properties, it will still be non-writable on the current object. For example, if we have:

let ObjClass = function() {};  
ObjClass.prototype.a = 1;
Object.defineProperty(ObjClass.prototype, "b", {  
  writable: false,  
  value: 1  
});

const obj = new ObjClass();  
ObjClass.prototype.a = 3  
obj.a = 2  
ObjClass.prototype.b = 3  
obj.b = 2  
console.log(obj);

Then assigning to property b is no effect. If we have strict mode enabled, then a TyepError will be thrown.

Object.defineProperties()

The Object.defineProperties method let us define more than one property on an object. The first parameter of the method is the object that you want to define the properties on, and the second object contains the property names as key and the corresponding property descriptors as values. The property descriptor should have the properties writable , configurable , enumerable and value as properties of the property name keys. The writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object. The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property. For example, we can write:

let obj = {}  
Object.defineProperties(obj, {  
  a: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: false,  
    configurable: true,  
    value: 'hello'  
  }  
})  
obj.a = 1;  
obj.d = 1;  
console.log(obj);

To add the a and d properties where a ‘s value can change and d ‘s value can’t. The console.log will show {a: 1, d: “hello”} since the value assignment to d fails because the writable property of property d ‘s descriptor is set to false . We can also set configurable to false to prevent it from being deleted or having its property descriptor changed. For example, we can write:

let obj = {}  
Object.defineProperties(obj, {  
  a: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: false,  
    configurable: false,  
    value: 'hello'  
  }  
})  
obj.a = 1;  
delete obj.d;  
console.log(obj);

If we run the code above, then we can see that the delete operator had no effect on property d of obj .

Object.entries()

The Object.entries() method returns an array of key-value pairs of an object that are enumerable by the for...in loop. They are returned in the same order as they are in if they are iterated with the for...in loop. Each entry of the array will have the key as the first element and the value of the corresponding key as the second element. For example, we can use it to loop through the properties of an object in the following code:

const obj = {  
  a: 1,  
  b: 2  
};

for (let [key, value] of Object.entries(obj)) {  
  console.log(key, value);  
}

When we run the code above, we get a 1 and b 2 . These are the key-value pairs of this object.

Object.freeze()

The Object.freeze() method freezes an object. This means that all properties’ values in an object can’t be changed. Also, new properties can’t be added to it, and existing property descriptors for the frozen object can’t be changed. The object is frozen in place. This method doesn’t return a new frozen object. Instead, it returns the original object before it’s frozen. The frozen object’s prototype also can’t be changed. For example, if we run the following to freeze an object:

const obj = {  
  a: 1  
};
Object.freeze(obj);
obj.a = 2;
console.log(obj.a);

We get that obj.a is still 1. This is because the object’s properties’ values can’t be changed. If we have strict mode enabled, a TypeError will be raised. It’s important to note that values that are objects can still be modified. For example, if we have the following code:

const obj = {  
  a: 1,  
  b: {  
    c: 2  
  }  
};
Object.freeze(obj);
obj.b.c = 3;
console.log(obj.b.c);

Then obj.b.c is 3 since Object.freeze doesn’t freeze properties that are inside of nested objects unless they’re frozen explicitly. So if we have:

const obj = {  
  a: 1,  
  b: {  
    c: 2  
  }  
};

Object.freeze(obj);  
Object.freeze(obj.b);
obj.b.c = 3;
console.log(obj.b.c);

Then obj.b.c is 2 since we frozen obj.b so that we can’t modify the value of obj.b .

The Object constructor has many more methods for constructing objects from an array of arrays with key and value of properties, and also methods to get property descriptors from objects, property names, property symbols, gets the keys of objects and prevent properties from being added or deleted or modify their property descriptors.

Categories
JavaScript Nodejs

How To Create a Simple Front End With Authenticated Routes

Creating pages that require authentication is simple in Angular. A guard is a piece of Angular code that is designed to control access to routes.

In app-routing.module.ts, we have an array of Route objects and in each entry, you can add a canActivate property where you can pass an array of guards.

The guard allows you to check for the requirements to access your routes. You return true, or a promise that resolves to true, to allow people to access your route. Otherwise, return false or return your promise to false.

In this story, we use the API we wrote in a previous piece.

To build the Angular app, you need the Angular CLI. To install it, run npm i -g @angular/cli in your Node.js command prompt. Then, run ng new frontend to generate the skeleton code for your front-end app.

Also, install @angular/material according to the Angular documentation to make our UI look pretty.

After that, replace the default app.module.ts with the following:

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import {  
  MatButtonModule,  
  MatCheckboxModule,  
  MatInputModule,  
  MatMenuModule,  
  MatSidenavModule,  
  MatToolbarModule,  
  MatTableModule,  
  MatDialogModule,  
  MAT_DIALOG_DEFAULT_OPTIONS,  
  MatDatepickerModule,  
  MatSelectModule,  
  MatCardModule  
} from '@angular/material';  
import { MatFormFieldModule } from '@angular/material/form-field';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { FormsModule } from '@angular/forms';  
import { TopBarComponent } from './top-bar/top-bar.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
import { SessionService } from './session.service';  
import { HttpReqInterceptor } from './http-req-interceptor';  
import { UserService } from './user.service';  
import { CapitalizePipe } from './capitalize.pipe';

@NgModule({  
  declarations: [  
    AppComponent,  
    TopBarComponent,  
    HomePageComponent,  
    LoginPageComponent,  
    SignUpPageComponent,  
    SettingsPageComponent,  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatCheckboxModule,  
    MatFormFieldModule,  
    MatInputModule,  
    MatMenuModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatTableModule,  
    FormsModule,  
    HttpClientModule,  
    MatDialogModule,  
    MatDatepickerModule,  
    MatMomentDateModule,  
    MatSelectModule,  
    MatCardModule,  
    NgxMaterialTimepickerModule  
  ],  
  providers: [  
    SessionService,  
    {  
      provide: HTTP_INTERCEPTORS,  
      useClass: HttpReqInterceptor,  
      multi: true  
    },  
    UserService,  
    {  
      provide: MAT_DIALOG_DEFAULT_OPTIONS,  
      useValue: { hasBackdrop: false }  
    },  
  ],  
  bootstrap: [AppComponent],  
})  
export class AppModule { }

This creates all our dependencies and components that we’ll add. To make authenticated requests easy with our token, we create an HTTP request interceptor by creating http-req-interceptor.ts:

import { Injectable } from '@angular/core';  
import {  
    HttpEvent,  
    HttpInterceptor,  
    HttpHandler,  
    HttpResponse,  
    HttpErrorResponse,  
    HttpRequest  
} from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { environment } from '../environments/environment'  
import { map, filter, tap } from 'rxjs/operators';  
import { Router } from '@angular/router';

@Injectable()  
export class HttpReqInterceptor implements HttpInterceptor {  
    constructor(  
        public router: Router  
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        let modifiedReq = req.clone({});if (localStorage.getItem('token')) {  
            modifiedReq = modifiedReq.clone({  
                setHeaders: {  
                    authorization: localStorage.getItem('token')  
                }  
            });  
        }return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {  
            if (event instanceof HttpResponse) {}  
        });  
    }  
}

We set the token in all requests, except the login request.

In our environments/environment.ts, we have:

export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:8080'
};

This points to our back end’s URL.

Now, we need to make our side nav. We want to add @ngrx/store to store the side nav’s state. We install the package by running npm install @ngrx/store --save.

We add our reducer by running ng add @ngrx/store to add our reducers.

We add menu-reducers.ts to set state centrally in our flux store and in that file we enter:

const TOGGLE_MENU = 'TOGGLE_MENU';

function menuReducer(state, action) {  
    switch (action.type) {  
        case TOGGLE_MENU:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { menuReducer, TOGGLE_MENU };

In index.ts of the same folder, we put:

import { menuReducer } from './menu-reducer';  
import { tweetsReducer } from './tweets-reducer';export const reducers = {  
  menu: menuReducer,  
};

To link our reducer to other parts of the app.

In style.css, to get our Material Design look, we put:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";  
body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

In index.html, we add the following in between the head tags:

<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Then, we add a service for the user functions by running ng g service user. That will create user.service.ts. We then put:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';  
import { Router } from '@angular/router)';  
import { JwtHelperService } from "@auth0/angular-jwt";

const helper = new JwtHelperService();
@Injectable({  
  providedIn: 'root'  
})  
export class UserService {

  constructor(  
    private http: HttpClient,  
    private router: Router  
  ) { }

  signUp(data) {  
    return this.http.post(`${environment.apiUrl}/user/signup`, data);  
  }

  updateUser(data) {  
    return this.http.put(`${environment.apiUrl}/user/updateUser`, data);  
  }

  updatePassword(data) {  
    return this.http.put(`${environment.apiUrl}/user/updatePassword`, data);  
  }

  login(data) {  
    return this.http.post(`${environment.apiUrl}/user/login`, data);  
  }

  logOut() {  
    localStorage.clear();  
    this.router.navigate(['/']);  
  }

  isAuthenticated() {  
    try {  
      const token = localStorage.getItem('token');  
      const decodedToken = helper.decodeToken(token);  
      const isExpired = helper.isTokenExpired(token);  
      return !!decodedToken && !isExpired;  
    }  
    catch (ex) {  
      return false;  
    }  
  }}

Each function requests a subscription for an HTTP request, except for the isAuthenticated function which is used to check for the token’s validity.

We also need routing for our app so we can see the pages when we go to the URLs listed below.

In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { TweetsPageComponent } from './tweets-page/tweets-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component';  
import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component';  
import { IsAuthenticatedGuard } from './is-authenticated.guard';

const routes: Routes = [  
  { path: 'login', component: LoginPageComponent },  
  { path: 'signup', component: SignUpPageComponent },  
  { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] },  
  { path: '**', component: HomePageComponent }];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

Now, we create the parts that are referenced in the file above.

We need to prevent people from accessing authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated. This generates is-authenticated.guard.ts.

We put the following in is-authenticated.guard.ts:

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';  
import { Observable } from 'rxjs';  
import { UserService } from './user.service';

@Injectable({  
  providedIn: 'root'  
})  
export class IsAuthenticatedGuard implements CanActivate {  
  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    const isAuthenticated = this.userService.isAuthenticated();  
    if (!isAuthenticated) {  
      localStorage.clear();  
      this.router.navigate(['/']);  
    }  
    return isAuthenticated;  
  }}

This uses our isAuthenticated function from UserService to check for a valid token. If it’s not valid, we clear it and redirect back to home page.

Now, we create the forms for logging in setting the user data after logging in.

We run ng g component homePage, ng g component loginPage, ng g component topBar, ng g component signUpPage, and ng g component settingsPage. These are for the forms and the top bar components.

The home page is just a static page. We should have home-page.component.html and home-page.component.ts generated after running the commands in our last paragraph.

In home-page.component.html, we put:

<div class="center">  
    <h1>Home Page</h1>  
</div>

Now we make our login page. In login-page.component.ts, we put:

<div class="center">  
    <h1>Log In</h1>  
</div>  
<form #loginForm='ngForm' (ngSubmit)='login(loginForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='loginData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='loginData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Log In</button>  
    <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a>  
</form>

In login-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';
@Component({  
  selector: 'app-login-page',  
  templateUrl: './login-page.component.html',  
  styleUrls: ['./login-page.component.scss']  
})  
export class LoginPageComponent implements OnInit {  
  loginData: any = <any>{};

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  }

  login(loginForm: NgForm) {  
    if (loginForm.invalid) {  
      return;  
    }  
    this.userService.login(this.loginData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/settings']);  
      }, err => {  
        alert('Invalid username or password');  
      })  
  }  
}

We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise an error alert will be displayed.

In our sign-up page, sign-up-page.component.html, we put:

<div class="center">  
    <h1>Sign Up</h1>  
</div>  
<br>  
<form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='signUpData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='signUpData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='signUpData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Sign Up</button>  
</form>

And, in sign-up-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';  
import _ from 'lodash';

@Component({  
  selector: 'app-sign-up-page',  
  templateUrl: './sign-up-page.component.html',  
  styleUrls: ['./sign-up-page.component.scss']  
})  
export class SignUpPageComponent implements OnInit {  
  signUpData: any = <any>{};

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  }

  signUp(signUpForm: NgForm) {  
    if (signUpForm.invalid) {  
      return;  
    }  
    this.userService.signUp(this.signUpData)  
      .subscribe(res => {  
        this.login();  
      }, err => {  
        console.log(err);  
        if (  
          _.has(err, 'error.error.errors') &&  
          Array.isArray(err.error.error.errors) &&  
          err.error.error.errors.length > 0  
        ) {  
          alert(err.error.error.errors[0].message);  
        }  
      })  
  }

  login() {  
    this.userService.login(this.signUpData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/tweets']);  
      })  
  }  
}

The two pieces of code get the sign-up data and send it to the back end which will save the file if they are all valid.

Similarly, in the settings-page.component.html:

<div class="center">  
    <h1>Settings</h1>  
</div>  
<br>  
<div>  
    <h2>Update User Info</h2>  
</div>  
<br>  
<form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='updateUserData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='updateUserData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update User Info</button>  
</form>  
<br><div>  
    <h2>Update Password</h2>  
</div>  
<br>  
<form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='updatePasswordData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update Password</button>  
</form>  
<br>

<div *ngIf='currentTwitterUser.id' class="title">  
    <h2>Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button>  
    </div>  
</div>  
<div *ngIf='!currentTwitterUser.id' class="title">  
    <h2>Not Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button>  
    </div>  
</div>

In settings-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { ActivatedRoute, Router } from '@angular/router';  
import { SessionService } from '../session.service';  
import { NgForm } from '@angular/forms';  
import { UserService } from '../user.service';
@Component({  
  selector: 'app-settings-page',  
  templateUrl: './settings-page.component.html',  
  styleUrls: ['./settings-page.component.scss']  
})  
export class SettingsPageComponent implements OnInit {  
  currentTwitterUser: any = <any>{};  
  elements: any[] = [];  
  displayedColumns: string[] = ['key', 'value'];  
  updateUserData: any = <any>{};  
  updatePasswordData: any = <any>{};

  constructor(  
    private sessionService: SessionService,  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() { }

  updateUser(updateUserForm: NgForm) {  
    if (updateUserForm.invalid) {  
      return;  
    }  
    this.userService.updateUser(this.updateUserData)  
      .subscribe(res => {  
        alert('Updated user info successful.');  
      }, err => {  
        alert('Updated user info failed.');  
      })  
  }

  updatePassword(updatePasswordForm: NgForm) {  
    if (updatePasswordForm.invalid) {  
      return;  
    }  
    this.userService.updatePassword(this.updatePasswordData)  
      .subscribe(res => {  
        alert('Updated password successful.');  
      }, err => {  
        alert('Updated password failed.');  
      })  
  }  
}

Similar to other pages, this sends a request payload for changing user data and passwords to our back end.

Finally, to make our top bar, we put the following in top-bar.component.html:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Twitter Automator  
</mat-toolbar>

And in top-bar.component.ts:

import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from '../reducers/menu-reducer';
@Component({  
  selector: 'app-top-bar',  
  templateUrl: './top-bar.component.html',  
  styleUrls: ['./top-bar.component.scss']  
})  
export class TopBarComponent implements OnInit {  
  menuOpen: boolean;

  constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  }

  ngOnInit() {  
  }

  toggleMenu() {  
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });  
  }  
}

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from './reducers/menu-reducer';  
import { UserService } from './user.service';
@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean;

  constructor(  
    private store: Store<any>,  
    private userService: UserService  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  }

  isAuthenticated() {  
    return this.userService.isAuthenticated();  
  }

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

  logOut() {  
    this.userService.logOut();  
  }  
}

And in app.component.html, we have:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    Twitter Automator  
                </b>  
            </li>  
            <li>  
                <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a>  
            </li>  
            <li>  
                <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a>  
            </li>  
            <li>  
                <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a>  
            </li>  
            <li>  
                <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a>  
            </li>  
            <li>  
                <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a>  
            </li>  
        </ul></mat-sidenav>  
    <mat-sidenav-content>  
        <app-top-bar></app-top-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This allows us to toggle our side nav menu. Note that we have:

@HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

To detect clicks outside the side nav. If we click outside, i.e. we’re not clicking on any element with those classes, then we close the menu.

this.store.dispatch propagates the closed state to all components.

Categories
JavaScript

Using JavaScript’s Object Constructor — Part 2

In JavaScript, the object constructor lets us create object wrapper with the given values. It will create an empty object if null or undefined is passed into the object constructor. If the value passed into the constructor is an object already, then it will return the object.

The object constructor has two properties. It has a length property that is always 1, and like all other objects, the object constructor has a prototype to get all the property additions to the type object.

Continuing from Part 1, the object constructor has many useful methods that can be used without constructing a new object.


Object.fromEntries()

The Object.fromEntries() method accepts an array with arrays of key-value pairs with the key as the first element and the value of the corresponding key as the second element. We can also pass in other iterables with the same kind of arrays into the method.

For example, we can write the following code to pass in an array of key-value pairs to create an object:

const entries = [  
  [  
    ['a', 1],  
    ['b', 2]  
  ]  
];

const obj = Object.fromEntries(entries);
console.log(obj);

The resulting object obj should be {a: 1, b: 2} as we can see from the console.log output when the code above is run. We can pass in other iterables, such as maps:

const entries = new Map([  
  ['a', 1],  
  ['b', 2]  
]);

const obj = Object.fromEntries(entries);
console.log(obj);

We should see the same thing logged. Also, we can convert arrays to objects with the following code:

const arr = [1,2,3];  
const entries = arr.map((value, index) => [index, value]);
const obj = Object.fromEntries(entries);
console.log(obj);

When the code above is run, we get {0: 1, 1: 2, 2: 3} since we mapped the index of each array entry to the key and the value of each array entry to the value.


Object.getOwnPropertyDescriptor()

The Object.getOwnPropertyDescriptor() method gets the property descriptor of a property in the object and returns it. As the name suggests, it only gets the property descriptor of the object that’s in the object itself and not up the prototype chain.

A property descriptor is an object with the property names as keys and the properties writable, configurable, enumerable, and value as properties of the property name keys.

writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object.

The enumerable property means that the property shows up during enumeration of the properties with the for...in loop and value is the value of the property.

For example, if we log a property descriptor of an object with:

const obj = {  
  a: 1  
}const descriptor = Object.getOwnPropertyDescriptor(obj, 'a');  
console.log(descriptor);

We get {value: 1, writable: true, enumerable: true, configurable: true} . The value is the value of the property and the rest of the properties are the property descriptor’s properties. If we have property getters and setters, they are also returned with the method call:

let obj = {};  
let value;  
Object.defineProperty(obj, 'a', {  
  get() {  
    return value;  
  },  
  set(a) {  
    value = a;  
  }  
});  
  
const descriptor = Object.getOwnPropertyDescriptor(obj, 'a');  
console.log(descriptor);

When the code above is run, we get the get and set functions back in the descriptor object.


Object.getOwnPropertyDescriptors()

While the Object.getOwnPropertyDescriptor() gets the property descriptor for a single object, the Object.getOwnPropertyDescriptors() gets all the property descriptors of an object in one object—once again without the properties that are inherited by this object up the prototype chain, with the property names as the keys and the property descriptor of the corresponding property name key as the value.

For example, if we have

const obj = {  
  a: 1,  
  b: 2  
}  
const descriptors = Object.getOwnPropertyDescriptors(obj);  
console.log(descriptors);

then we get

{  
  "a": {  
    "value": 1,  
    "writable": true,  
    "enumerable": true,  
    "configurable": true  
  },  
  "b": {  
    "value": 2,  
    "writable": true,  
    "enumerable": true,  
    "configurable": true  
  }  
}

We get all the values and the property descriptor attributes of each property. Like with Object.getOwnPropertyDescriptor(), this method has the same definitions for the property descriptors.

A property descriptor is an object with the property names as keys and the properties writable, configurable, enumerable, and value as properties of the property name keys.

The writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object.

The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property.


Object.getOwnPropertyNames()

The Object.getOwnPropertyNames() method returns an array of property names that are defined in the object itself and not in any object up the prototype chain. Non-enumerable properties are also returned except for those that are symbols. For example, if we have:

let obj = {  
  a: 1,  
  b: 2  
}

Object.defineProperty(obj, 'c', {  
  "value": 2,  
  "writable": true,  
  "enumerable": false,  
  "configurable": true  
})

const names = Object.getOwnPropertyNames(obj);  
console.log(names);

If we run the code above, we get [“a”, “b”, “c”] returned since all properties defined except ones identified with Symbols are returned. If we have Symbols in our object, we won’t see it in the returned array. For example, if we have

let obj = {  
  a: 1,  
  b: 2,  
  [Symbol('foo')]: 3  
}  
Object.defineProperty(obj, 'c', {  
  "value": 4,  
  "writable": true,  
  "enumerable": false,  
  "configurable": true  
})  
const names = Object.getOwnPropertyNames(obj);  
console.log(names);

we still see [“a”, “b”, “c”] if we run the code above since symbols aren’t included in the array. We have the Object.getOwnPropertySymbols() to get properties that are identified with symbols.


Object.getOwnPropertySymbols()

The Object.getOwnPropertySymbols() returns an array of symbols that are used as identifiers in the properties of an object.

It only gets the property identifiers that are named with symbols and nothing else.

Also, it doesn’t traverse up to the prototype chain to get properties of objects up the prototype chain. For example, if we have the following code:

let obj = {  
  a: 1,  
  b: 2,  
  [Symbol('foo')]: 3  
}  
Object.defineProperty(obj, 'c', {  
  "value": 4,  
  "writable": true,  
  "enumerable": false,  
  "configurable": true  
})  
const symbols = Object.getOwnPropertySymbols(obj);  
console.log(symbols);

we get [Symbol(foo)] logged as we expected.


Object.getPrototypeOf()

The Object.getPrototypeOf() method gets the prototype of an object, which is the same as the [[Prototype]] property of the specified object. For example, if we have

const prototype1 = {};  
const obj = Object.create(prototype1);  
console.log(Object.getPrototypeOf(obj) === prototype1);

the console.log will output true since the first argument of Object.create is the prototype object for the object that it returns; likewise, if we use Object.setPrototypeOf() method to set the prototype of an existing object.

const prototype2 = {};  
let obj2 = {};  
Object.setPrototypeOf(obj2, prototype2);  
console.log(Object.getPrototypeOf(obj2) === prototype2);

The console.log will also output true since set the prototype of obj2 explicitly.


Object.is()

The Object.is() method compares whether two objects passed in its argument are the same value. Two values are the same if they’re:

  • both undefined
  • both null
  • both true or both false
  • both strings with the same length and same characters in the same order
  • both objects having the same references
  • both numbers and both +0 , or both -0 or both NaN or both non-zero and both not NaN and they both have the same value

It doesn’t convert types like the == operator to compare objects and don’t convert truthy or falsy values to booleans. Also, it’s not the same as comparing objects with the === operator because with the === operator, -0 and +0 are equal and NaN is not the same as itself.

For example, we can make the following comparisons:

Object.is('a', 'a');             // true  
Object.is(document, document);   // true  
  
Object.is('a', 'b');             // false  
Object.is([], []);               // false  
  
const obj = { a: 1 };  
const obj2 = { a: 1 };  
Object.is(obj, obj);             // true  
Object.is(obj, obj2);            // false  
  
Object.is(null, null);           // true  
  
Object.is(0, -0);                // false  
Object.is(-0, -0);               // true  
Object.is(NaN, 0/0);             // true

As we can see, only object references and values are compared. The object’s content isn’t compared, so even if two objects have the same content, they’re still considered different since their references in memory are different, as they’re defined by two different variables.


Object.isExtensible()

The Object.isExtensible() method checks whether an object’s property is extensible. That is if an object can have new properties added to it. For example, if we have:

const obj = {};  
const obj2 = {};console.log(Object.isExtensible(obj));Object.preventExtensions(obj2);console.log(Object.isExtensible(obj2));

Then console.log(Object.isExtensible(obj)); will log true since we didn’t explicitly prevent adding new properties to obj . However, console.log(Object.isExtensible(obj2)); will log false since we called Object.preventExtensions(obj2); to prevent new properties from being added to obj2 .

If we run the following code, we also get false logged in both console.log statements because we explicitly prevented the objects obj and obj2 from having new properties added with the Object.freeze() and Object.seal() methods respectively:

const obj = {};  
const obj2 = {};  
Object.freeze(obj);  
console.log(Object.isExtensible(obj));Object.seal(obj2);  
console.log(Object.isExtensible(obj2));

Object.isFrozen()

The Object.isFrozen() method determines if an object is frozen. Frozen means that all properties’ values in an object can’t be changed. Also, new properties can’t be added to it, and existing property descriptors for the frozen object can’t be changed. The object is frozen in place. This method doesn’t return a new frozen object. Instead, it returns the original object before it’s frozen. The frozen object’s prototype also can’t be changed. For example, if we have:

const obj = {  
  a: 1  
};console.log(Object.isFrozen(obj));

We get false from the console.log statement because we didn’t explicitly freeze the object by calling Object.freeze(obj). On the other hand, we have frozen the object, then Object.isFrozen() will return true. For example:

const obj = {  
  a: 1  
};Object.freeze(obj);  
console.log(Object.isFrozen(obj));

Then we get true from the console.log statement because we froze the object by calling Object.freeze(obj). If primitive values are passed in, Object.isFrozen() will return true since they’re immutable. For example, if we have:

console.log(Object.isFrozen(1));  
console.log(Object.isFrozen('string'));  
console.log(Object.isFrozen(null));  
console.log(Object.isFrozen(undefined));  
console.log(Object.isFrozen(NaN));  
console.log(Object.isFrozen(true));  
console.log(Object.isFrozen(Symbol('a')));

Then all the console.log statement will be true since they’re all immutable.

The Object constructor has many more methods for constructing objects from an array of an array with key and value of properties and also methods to get property descriptors from objects, property names, property symbols, gets the keys of an object and prevent properties from being added or deleted or modify their property descriptors.

Categories
JavaScript

How to Use the Array indexOf Method in JavaScript

The JavaScript array’s indexOf method returns the index of the first instance of a given entry when starting the search from the beginning of the array. Otherwise -1 is returned.

It takes up to 2 arguments. The first is the item to search for. The 2nd argument is an optional argument with the array index to start searching from.

The search is done by comparing the entry by using the === operator, which does comparison without type coercion beforehand.

For instance, we can use it as follows:

const index = [1,2,3].indexOf(2);

In the code above, we called indexOf on the array [1,2,3] with argument 2. This should return 1 and assign that to index since the first instance of 2 appears as the 2nd entry of the array.

We can also pass in an index number to the 2nd argument. For instance, we can write the following:

const index = [1, 2, 3].indexOf(2, 2);

index should be -1 in this case since we specified that we start searching from index 2 and beyond. 2 doesn’t exist in index 2 or beyond so -1 is returned.

Conclusion

indexOf lets search for an item in the array with either starting from the beginning or from a starting index.

It uses the === operator to compare each entry to find a match.