Showing posts with label NodeJS. Show all posts
Showing posts with label NodeJS. Show all posts

Wednesday, 8 October 2025

Building Real-Time Collaborative Editor with Operational Transforms & Node.js

October 08, 2025 0

Building a Real-Time Collaborative Editor with Operational Transforms (OT) and Node.js

Real-time collaborative code editor built with Operational Transforms and Node.js - LK-TECH Academy tutorial on building Google Docs-like collaborative editing

Imagine multiple users editing the same document simultaneously without conflicts, version control nightmares, or data loss. This isn't magic—it's Operational Transform (OT), the same technology powering Google Docs, Notion, and other collaborative applications. In this comprehensive guide, you'll learn how to build your own real-time collaborative editor from scratch using Node.js, Socket.IO, and the Operational Transform algorithm. By the end, you'll have a fully functional collaborative text editor that handles concurrent edits gracefully.

🚀 Understanding Operational Transforms

Operational Transform is a conflict resolution algorithm designed specifically for real-time collaborative editing. When multiple users edit the same document simultaneously, their operations (insertions, deletions) need to be transformed to maintain consistency across all clients.

The core problem OT solves: if User A inserts text at position 5, and User B deletes text at position 3, how do we ensure both operations are applied correctly without breaking the document state?

📋 Core OT Operations

  • Insert(position, text): Adds text at specified position
  • Delete(position, length): Removes text starting from position
  • Retain(position, length): Keeps text unchanged

💻 Project Architecture

Our collaborative editor will consist of:

  • Node.js backend with Express and Socket.IO
  • Frontend with vanilla JavaScript and Socket.IO client
  • OT algorithm implementation for conflict resolution
  • Document versioning and operation history

🔧 Setting Up the Server

Let's start by setting up our Node.js server with the necessary dependencies.

💻 Server Setup Code


// package.json dependencies
{
  "name": "collaborative-editor",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2",
    "socket.io": "^4.7.2",
    "uuid": "^9.0.0"
  }
}

// server.js - Basic setup
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const { v4: uuidv4 } = require('uuid');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
});

// Store documents and their operations
const documents = new Map();

app.use(express.static('public'));

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  socket.on('join-document', (docId) => {
    socket.join(docId);
    
    if (!documents.has(docId)) {
      documents.set(docId, {
        content: '',
        operations: [],
        version: 0
      });
    }
    
    const doc = documents.get(docId);
    socket.emit('document-state', {
      content: doc.content,
      version: doc.version
    });
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

  

🔍 Implementing Operational Transform Algorithm

The heart of our collaborative editor is the OT algorithm. Let's implement the core transformation functions.

💻 OT Algorithm Implementation


// ot.js - Operational Transform implementation
class OperationalTransform {
  static transform(op1, op2) {
    // Transform op1 against op2
    let transformedOp = [...op1];
    
    for (let i = 0; i < op2.length; i++) {
      const component = op2[i];
      
      if (component.insert) {
        transformedOp = this.transformAgainstInsert(transformedOp, component);
      } else if (component.delete) {
        transformedOp = this.transformAgainstDelete(transformedOp, component);
      } else if (component.retain) {
        transformedOp = this.transformAgainstRetain(transformedOp, component);
      }
    }
    
    return transformedOp;
  }
  
  static transformAgainstInsert(op, insertOp) {
    const transformed = [];
    const insertPos = insertOp.insert.position;
    const insertLength = insertOp.insert.text.length;
    
    for (const component of op) {
      if (component.insert) {
        let pos = component.insert.position;
        if (pos >= insertPos) {
          pos += insertLength;
        }
        transformed.push({ insert: { position: pos, text: component.insert.text } });
      } else if (component.delete) {
        let start = component.delete.position;
        let end = start + component.delete.length;
        
        if (start >= insertPos) {
          start += insertLength;
          end += insertLength;
        } else if (end > insertPos) {
          end += insertLength;
        }
        
        transformed.push({ delete: { position: start, length: end - start } });
      } else if (component.retain) {
        let start = component.retain.position;
        let end = start + component.retain.length;
        
        if (start >= insertPos) {
          start += insertLength;
          end += insertLength;
        } else if (end > insertPos) {
          end += insertLength;
        }
        
        transformed.push({ retain: { position: start, length: end - start } });
      }
    }
    
    return transformed;
  }
  
  static transformAgainstDelete(op, deleteOp) {
    const transformed = [];
    const deleteStart = deleteOp.delete.position;
    const deleteEnd = deleteStart + deleteOp.delete.length;
    
    for (const component of op) {
      if (component.insert) {
        let pos = component.insert.position;
        if (pos > deleteEnd) {
          pos -= (deleteEnd - deleteStart);
        } else if (pos > deleteStart) {
          pos = deleteStart;
        }
        transformed.push({ insert: { position: pos, text: component.insert.text } });
      } else if (component.delete) {
        let start = component.delete.position;
        let end = start + component.delete.length;
        
        // Handle overlapping deletions
        if (start >= deleteEnd) {
          start -= (deleteEnd - deleteStart);
          end -= (deleteEnd - deleteStart);
        } else if (end <= deleteStart) {
          // No change needed
        } else if (start < deleteStart && end > deleteEnd) {
          end -= (deleteEnd - deleteStart);
        } else if (start >= deleteStart && end <= deleteEnd) {
          continue; // This deletion is completely covered
        } else if (start < deleteStart) {
          end = deleteStart;
        } else if (end > deleteEnd) {
          start = deleteStart;
          end -= (deleteEnd - deleteStart);
        }
        
        if (end > start) {
          transformed.push({ delete: { position: start, length: end - start } });
        }
      } else if (component.retain) {
        let start = component.retain.position;
        let end = start + component.retain.length;
        
        if (start >= deleteEnd) {
          start -= (deleteEnd - deleteStart);
          end -= (deleteEnd - deleteStart);
        } else if (end <= deleteStart) {
          // No change needed
        } else if (start < deleteStart && end > deleteEnd) {
          end -= (deleteEnd - deleteStart);
        } else if (start >= deleteStart && end <= deleteEnd) {
          continue; // This retain is completely covered by deletion
        } else if (start < deleteStart) {
          end = deleteStart;
        } else if (end > deleteEnd) {
          start = deleteStart;
          end -= (deleteEnd - deleteStart);
        }
        
        if (end > start) {
          transformed.push({ retain: { position: start, length: end - start } });
        }
      }
    }
    
    return transformed;
  }
  
  static transformAgainstRetain(op, retainOp) {
    // Retain operations don't change other operations
    return op;
  }
  
  static applyOperation(content, operation) {
    let result = content;
    
    // Sort operations by position to apply correctly
    const sortedOps = [...operation].sort((a, b) => {
      const posA = a.insert ? a.insert.position : a.delete ? a.delete.position : a.retain.position;
      const posB = b.insert ? b.insert.position : b.delete ? b.delete.position : b.retain.position;
      return posA - posB;
    });
    
    let offset = 0;
    
    for (const component of sortedOps) {
      if (component.insert) {
        const pos = component.insert.position + offset;
        result = result.slice(0, pos) + component.insert.text + result.slice(pos);
        offset += component.insert.text.length;
      } else if (component.delete) {
        const pos = component.delete.position + offset;
        result = result.slice(0, pos) + result.slice(pos + component.delete.length);
        offset -= component.delete.length;
      }
      // Retain operations don't change the content
    }
    
    return result;
  }
}

module.exports = OperationalTransform;

  

🔗 Socket.IO Event Handling

Now let's implement the real-time communication between clients and server.

💻 Enhanced Server with OT Logic


// enhanced-server.js
const OperationalTransform = require('./ot');

// Enhanced socket event handling
io.on('connection', (socket) => {
  console.log('User connected:', socket.id);
  
  socket.on('join-document', (docId) => {
    socket.join(docId);
    
    if (!documents.has(docId)) {
      documents.set(docId, {
        content: '',
        operations: [],
        version: 0,
        clients: new Map()
      });
    }
    
    const doc = documents.get(docId);
    doc.clients.set(socket.id, {
      version: doc.version,
      pendingOps: []
    });
    
    socket.emit('document-state', {
      content: doc.content,
      version: doc.version
    });
  });
  
  socket.on('operation', (data) => {
    const { docId, operation, version } = data;
    const doc = documents.get(docId);
    
    if (!doc) return;
    
    const clientState = doc.clients.get(socket.id);
    if (!clientState) return;
    
    // Check if client is behind
    if (version < doc.version) {
      // Client needs to catch up
      const missingOps = doc.operations.slice(version);
      let transformedOp = operation;
      
      for (const missingOp of missingOps) {
        transformedOp = OperationalTransform.transform(transformedOp, missingOp);
      }
      
      // Apply the transformed operation
      doc.content = OperationalTransform.applyOperation(doc.content, transformedOp);
      doc.operations.push(transformedOp);
      doc.version++;
      
      // Update all clients
      socket.to(docId).emit('remote-operation', {
        operation: transformedOp,
        version: doc.version
      });
      
      // Send acknowledgment to originating client
      socket.emit('operation-ack', {
        version: doc.version
      });
    } else if (version === doc.version) {
      // Client is up to date
      doc.content = OperationalTransform.applyOperation(doc.content, operation);
      doc.operations.push(operation);
      doc.version++;
      
      // Broadcast to other clients
      socket.to(docId).emit('remote-operation', {
        operation: operation,
        version: doc.version
      });
      
      // Acknowledge to originating client
      socket.emit('operation-ack', {
        version: doc.version
      });
    } else {
      // Client is ahead (shouldn't happen) - send sync
      socket.emit('document-state', {
        content: doc.content,
        version: doc.version
      });
    }
    
    // Update client state
    clientState.version = doc.version;
  });
  
  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
    
    // Clean up client states from all documents
    for (const [docId, doc] of documents) {
      if (doc.clients.has(socket.id)) {
        doc.clients.delete(socket.id);
        
        // If no clients, consider cleaning up the document
        if (doc.clients.size === 0) {
          // Optional: persist document before cleanup
          // documents.delete(docId);
        }
      }
    }
  });
});

  

⚡ Key Takeaways

  1. Operational Transform is fundamental for conflict-free collaborative editing by mathematically transforming concurrent operations
  2. Version control is crucial for maintaining consistency across multiple clients with different network latencies
  3. Real-time communication using WebSockets (Socket.IO) enables instant updates across all connected clients
  4. Conflict resolution happens automatically through the OT algorithm without user intervention
  5. Advanced features like presence indicators and remote cursors significantly enhance user experience

❓ Frequently Asked Questions

How does Operational Transform differ from Conflict-free Replicated Data Types (CRDT)?
OT requires a central server to transform operations and maintain consistency, while CRDTs are designed for peer-to-peer systems where any replica can merge changes without coordination. OT is generally more efficient for text editing but requires more complex server logic.
What happens when network connectivity is lost?
The client continues to work offline, queueing operations locally. When connectivity is restored, the client sends all pending operations to the server, which transforms them against any missed server operations and updates the document state accordingly.
How scalable is this implementation for large numbers of users?
The basic implementation can handle dozens of concurrent users on a single document. For larger scale, you'd need to implement operation compression, consider using Redis for shared state across multiple server instances, and potentially partition documents across different server nodes.
Can this handle rich text formatting and embedded objects?
Yes, but it requires extending the OT algorithm to handle different operation types. For rich text, you'd need operations for applying formatting ranges, inserting images, creating tables, etc. Each new content type requires defining how its operations transform against other operations.
How do you handle security and access control in a collaborative editor?
You can implement authentication middleware for Socket.IO connections, document-level permissions (read-only, comment-only, edit), operation validation to prevent malicious edits, and audit logging for compliance requirements. Always validate operations server-side before applying them.

💬 Found this article helpful? Please leave a comment below or share it with your network to help others learn! Have you implemented collaborative features in your projects? Share your experiences and challenges!

About LK-TECH Academy — Practical tutorials & explainers on software engineering, AI, and infrastructure. Follow for concise, hands-on guides.

Saturday, 23 January 2021

How to use Angular HttpClient and RxJS to consume REST API

January 23, 2021 0
Angular HttpClient and RxJS


Today I am going to discuss how you can interact with REST API services to load and manage data. 

Prerequisites

Before getting started, you need to have the below software installed on your development machine:

Node.js and npm. You can install both of them from the Node.js

Angular CLI  (You can install it from npm using: npm install -g @angular/cli)


Frontend applications needs to call backend services over http/https protocol to manage dynamic data. Best place to access data from backend APIs are service component. Once you defined your service class you can inject it in to component or another service to use the service methods. 

Angular Related Articles:

Angular provides the HttpClient service class in @angular/common/http module to interact with backend REST API services.

Using HttpClient request call you can easily handle your response and you can intercept your request and response. By intercepting the request, you can inject your security token or any other requested headers to all the service inside the one place. By intercepting the response, you can handle all the errors in a single place. I will explain interceptor concept in another chapter with more details. 

Today we will check how you can use HttpClient methods to do get data from service API. Get method of HTTP Client returns RxJS observable type and you can subscribe to the RxJS observable inside the component where you call the service method.

If you look at my flower store code in GitHub, you can see I have hard coded flower objects and put them into an array as below. 


  mySellingFlowers(){
    let rose = new flower();
    rose.name = "Rose";
    rose.price = 100;
    rose.availableQuantity = 1000;
    rose.isChecked = false;
    this. myFlowerList.push(rose);

    let lily = new flower();
    lily.name = "Lilly";
    lily.price = 80;
    lily.availableQuantity = 2000;
    lily.isChecked = false;
    this. myFlowerList.push(lily);

    let tulip = new flower();
    tulip.name = "Tulip";
    tulip.price = 100;
    tulip.availableQuantity = 2300;
    lily.isChecked = false;

    this. myFlowerList.push(tulip);

    let carnation = new flower();
    carnation.name = "Carnation";
    carnation.price = 50;
    carnation.availableQuantity = 1500;
    lily.isChecked = false;

    this. myFlowerList.push(carnation);

    let gerbera = new flower();
    gerbera.name = "Gerbera";
    gerbera.price = 50;
    gerbera.availableQuantity = 1500;
    lily.isChecked = false;

    this. myFlowerList.push(gerbera);

    let orchid = new flower();
    orchid.name = "Orchid";
    orchid.price = 50;
    orchid.availableQuantity = 1500;
    lily.isChecked = false;

    this. myFlowerList.push(orchid);

  }
  
Today we will check how you can read flowers from backend REST API call. I am planning to use designer.mocky.io to mock my API call. To access list of flowers through API and to read it from Angular side that API should return an JOSN array. Therefor I will make a JSON array to define a response in my mock API as below.

{"flowers":[  
    {"name":"Rose", "price":"100", "availableQuantity":"1000","isChecked":false},    
    {"name":"Lilly", "price":"80", "availableQuantity":"2000","isChecked":false},  		
    {"name":"Tulip", "price":"100", "availableQuantity":"2300","isChecked":false},  	   
    {"name":"Carnation", "price":"80", "availableQuantity":"1500","isChecked":false}, 
    {"name":"Gerbera", "price":"50", "availableQuantity":"1500","isChecked":false},   
    {"name":"Orchid", "price":"200", "availableQuantity":"1500","isChecked":false}   
]
}
]}

https://designer.mocky.io/ to mock my API call

Click on the generate my http response button to get the access URL. In my case it is as below.

Click on the generate my http response button to get the access run.mocky.io/v3

Now let us see how you can access this URL and display data in html. As I said earlier best place to access the data layer is service class.

To generate the service, please run below CLI command in your command prompt. 

ng g s flower
ng g s flower command

Above command generate the default flower service as below.


import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class FlowerService {

  constructor() { }
}

  
The @Injectable() decorator specifies that Angular can use this class with the Dependency Injection.

The metadata, providedIn: 'root', means that the FlowerService is visible throughout the application.
Now we will write a new method in a FlowerService class to access our API end point.

The HttpClient service in Angular 4.3+ is the successor to Angular 2's Http service. Instead of returning a Promise, its http.get() method returns an RxJS Observable object.

Therefore, to call our get API method I will import the HttpClient from Angualr/common/http and Observable module from RxJS. and then injected that to our service class as below.


import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class FlowerService {

  constructor(private http:HttpClient) {}

    getFlowerList():Observable<any>{
      return this.http.get('https://run.mocky.io/v3/cee1c6e9-1491-4191-9054-ce7df1c1a500');
   }
}

  
getFlowerLIst() methods returns RxJS observable type and later in the landing component you can subscribe to access data.

To consume getFlowerList() method in our landing component we need to inject our service in to landing component through constructor and need to subscribe to the method.


constructor(private flowerService:FlowerService) { }
I have commented out my array with hard coded data and add the below codes to read data from service method.


this.flowerService.getFlowerList().subscribe(response=>{
      this.myFlowerList = response.flowers
    },
    err => console.error(err),
    () => console.log('done loading foods')
   )
I have added full code for landing component for you to refer. 


import { Component, EventEmitter, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { flower } from '../../domain/flower';
import { DataService } from 'src/app/services/data.service';
import { FlowerService } from 'src/app/services/flower.service';

@Component({
  selector: 'app-landing',
  templateUrl: './landing.component.html',
  styleUrls: ['./landing.component.scss'],
 
})
export class LandingComponent implements OnInit {

  myFlowerList:flower[]=[];
  selectedFlowers: string[] =[];

  checkedList:string[]=[];
  searchText:string='';
  constructor(private dataService:DataService, private flowerService:FlowerService) { }

  ngOnInit() {
    this.mySellingFlowers();
    this.dataService.getSearchText().subscribe(response => {
      this.printSearchtext(response);
    })
  }

  printSearchtext(searchText){
    this.searchText = searchText;
  }

  printOrder(flowerName){
    if(this.selectedFlowers.indexOf(flowerName)<0){
      this.selectedFlowers.push(flowerName)
    }
    else{
      let index = this.selectedFlowers.indexOf(flowerName);
      this.selectedFlowers.splice(index,1);
    }

  }

  mySellingFlowers(){
    // let rose = new flower();
    // rose.name = "Rose";
    // rose.price = 100;
    // rose.availableQuantity = 1000;
    // rose.isChecked = false;
    // this. myFlowerList.push(rose);

    // let lily = new flower();
    // lily.name = "Lilly";
    // lily.price = 80;
    // lily.availableQuantity = 2000;
    // lily.isChecked = false;
    // this. myFlowerList.push(lily);

    // let tulip = new flower();
    // tulip.name = "Tulip";
    // tulip.price = 100;
    // tulip.availableQuantity = 2300;
    // lily.isChecked = false;

    // this. myFlowerList.push(tulip);

    // let carnation = new flower();
    // carnation.name = "Carnation";
    // carnation.price = 50;
    // carnation.availableQuantity = 1500;
    // lily.isChecked = false;

    // this. myFlowerList.push(carnation);

    // let gerbera = new flower();
    // gerbera.name = "Gerbera";
    // gerbera.price = 50;
    // gerbera.availableQuantity = 1500;
    // lily.isChecked = false;

    // this. myFlowerList.push(gerbera);

    // let orchid = new flower();
    // orchid.name = "Orchid";
    // orchid.price = 50;
    // orchid.availableQuantity = 1500;
    // lily.isChecked = false;

    // this. myFlowerList.push(orchid);

    this.flowerService.getFlowerList().subscribe(response=>{
      this.myFlowerList = response.flowers
    },
    err => console.error(err),
    () => console.log('done loading foods')
   )

  }

  trackFlowers(index,flower){
    return flower?flower.name:undefined
  }
}

  
The subscribe() method takes three arguments which are event handlers. They are called onNext, onError, and onCompleted. 

The onNext method will receive the HTTP response data

The onError event handler is called if the HTTP request returns an error code such as a 500. 

The onCompleted event handler executes after the Observable has finished returning all its data. 

myFlowerList array contains data return from the API call and using *ngFor you can iterate and display it in a HTML as below.


<div *ngFor="let flower of myFlowerList;trackBy:trackFlowers">
      <flower-card [title]="flower.name" (selectedFlower)="printOrder($event)"></flower-card>
</div>
To use the Angular HttpClient, we need to inject it into our app's dependencies in app.module.ts file as below.


imports: [
    BrowserModule,
    AppRoutingModule,
    CardModule,
    CheckboxModule,
    CommonModule,
    FormsModule,
    InputTextModule,
    HttpClientModule
    ],
Unless it gives you below error.

core.js:7187 ERROR Error: Uncaught (in promise): NullInjectorError: StaticInjectorError(AppModule)[HttpClient]: 
  StaticInjectorError(Platform: core)[HttpClient]: 
    NullInjectorError: No provider for HttpClient!
NullInjectorError: StaticInjectorError(AppModule)[HttpClient]:
I have added app.module.ts file code after adding the dependency and you can check full code to the app by accessing GitHub.


import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LandingComponent } from './modules/landing/landing.component';
import { HomeComponent } from './modules/home/home.component';
import { CardModule } from 'primeng/card';
import {CheckboxModule} from 'primeng/checkbox';
import { CommonModule } from '@angular/common';
import { FormsModule }    from '@angular/forms';
import {InputTextModule} from 'primeng/inputtext';
import { FlowerCardComponent } from './modules/cards/flower-card/flower-card.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
    LandingComponent,
    HomeComponent,
    FlowerCardComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    CardModule,
    CheckboxModule,
    CommonModule,
    FormsModule,
    InputTextModule,
    HttpClientModule
    
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
Once you load the app it will show same data as before by getting the data from backend API call.

My Flower Store Angular App

Conclusion

In this tutorial, we used HttpClient and RxJS modules to retrieves data from a REST API using the get() method of HttpClient. First I have explained how you can generate service component using the CLI command. Then I have described how  to subscribe to the RxJS Observable returned by the get() method and how to use the *ngFor directive to iterate over fetched data in the template. 

Thursday, 26 November 2020

Adding styles to your Angular App

November 26, 2020 0


  How to set up Angular environment

             How to create Angular project

             How to generate component and define routings in Angular


Let’s make our app little bit interactive.

You can put all your common style of your application to style.css file which applies globally.

Copy and paste below code in your style.css file

/* You can add global styles to this file, and also import other style files */

@import url('https://fonts.googleapis.com/css?family=Nunito:400,700&display=swap');

$primary: rgb(216, 172, 78);

body {

    margin: 0;

    font-family: 'Nunito', 'sans-serif';

    font-size: 18px;

}

.container {

    width: 80%;

    margin: 0 auto;

}

Sunday, 22 November 2020

Setting up the react native development environment

November 22, 2020 0

 

Setting up the react native development environment

This tutorial I am going to focus on how to install and build your first React Native application.


There are two methods you can follow to develop React Native app. If you are very new to mobile development and not familiar with setting up mobile development environment, I would suggest to start with Expo CLI. Expo is a set of tools to build React Native application. It has may features; you have to select suitable features to develop your app in minutes. You need only a recent version of Node.js and a phone or emulator. If you want to test your React Native application on your web browser before installing any tools, you may use Snack.

 

If you are familiar mobile developer from other languages and want to try out React Native, you may try out React Native CLI. For that you need to install either Xcode for iOS or Android Studio for Android OS.

 

If you are a beginner, my personal suggestion is to go for Expo CLI and familiar with the React Native features and concepts. Actually, using Expo CLI, you can develop production level application. But when you move forward there might have some limitation when your application grow to access very hardware specific features. For example, if your application wants to read NFC card, get some data from Bluetooth enabled devices…etc you might want to go for React Native CLI rather Expo CLI. In my own experience I have developed Expo application for many projects which read mobile device network connection, camera, Wi-Fi connection and many more. Most important feature is if you write your Expo app in a such way that you may use same code for iOS and Android OS as it is. But in React Native CLI most of the time you need to some slight modifications to build for iOS and Android OS.

 

Method 1: Expo CLI

 

Prerequisite is to install Node 12 LTS or greater on your machine. You can use npm or yarn command to install the Expo CLI command line utility:

 

npm install -g expo-cli

 or

yarn global add expo-cli

 

Saturday, 21 November 2020

Introduction to Angular

November 21, 2020 0
Introduction to Angular



Google release AngularJS in year 2010. It got popular immediately because it made static HTML interactive. However, with other latest web development technologies developers started to see drawbacks of AngularJS.

Google then start to rewrite framework again and decided to shift from Java Script to Type Script. Type Script is helpful to avoid drawback of AngularJS

Angular 2 was then introduced in 2016 and it evolves up to Angular 9.


How to install Angular


To setting up the angular framework you need 

1. Node

To install node go to official site of node to download the version you need (nodejs)

Run the downloaded Node.js .msi Installer 

If you want to know the node version installed in your computer run following command

 node -v


node -v