Gaussian Direct Coupling analysis for Protein Contacts Predicion

GaussDCA.jl

Gaussian Direct Coupling Analysis for protein contacts predicion 

Overview

This is the code which accompanies the paper "Fast and accurate multivariate Gaussian modeling of protein families: Predicting residue contacts and protein-interaction partners" by Carlo Baldassi, Marco Zamparo, Christoph Feinauer, Andrea Procaccini, Riccardo Zecchina, Martin Weigt and Andrea Pagnani, (2014) PLoS ONE 9(3): e92721. doi:10.1371/journal.pone.0092721

See also this Wikipedia article for a general overview of the Direct Coupling Analysis technique.

This code is released under the GPL version 3 (or later) license; see the LICENSE.md file for details.

The code is written in Julia and requires julia version 1.5 or later; it provides a function which reads a multiple sequence alignment (in FASTA format) and returns a ranking of all pairs of residue positions in the aligned amino-acid sequences.

Since version 2, most of the internal functions used to parse and manipulate the data have been factored out into the package DCAUtils.jl. The code in this module is essentially a wrapper around those utilities.

Installation

To install the package, enter in Pkg mode by pressing the ] key, then in the pkg prompt enter

(@v1.5) pkg> add "https://github.com/carlobaldassi/GaussDCA.jl"

Usage

To load the code, just type using GaussDCA.

This software provides one main function, gDCA(filname::String, ...). This function takes the name of a (possibly gzipped) FASTA file, and returns a predicted contact ranking, in the form of a Vector of triples, each triple containing two indices i and j (with i < j) and a score. The indices start counting from 1, and denote pair of residue positions in the given alignment; pairs which are separated by less than a given number of residues (by default 5) are filtered out. The triples are sorted by score in descending order, such that predicted contacts should come up on top.

For convenience, a utility function is also provided, printrank(output, R), which prints the result of gDCA either in a file or to a stream, given as first argument. If the first argument output is omitted, the standard terminal output will be used.

The gDCA function takes some additional, optional keyword arguments:

  • pseudocount: the value of the pseudo-count parameter, between 0 and 1. the default is 0.8, which gives good results when the Frobenius norm score is used (see below); a good value for the Direct Information score is 0.2.
  • θ: the value of the similarity threshold. By default it is :auto, which means it will be automatically computed (this takes additional time); otherwise, a real value between 0 and 1 can be given.
  • max_gap_fraction: maximum fraction of gap symbols in a sequence; sequences that exceed this threshold are discarded. The default value is 0.9.
  • score: the scoring function to use. There are two possibilities, :DI for the Direct Information, and :frob for the Frobenius norm. The default is :frob. (Note the leading colon: this argument is passed as a symbol).
  • min_separation: the minimum separation between residues in the output ranking. Must be ≥ 1. The default is 5.

The code is multi-threaded: if you start julia with the -t option, for example as julia -t 8, the computations will run in parallel on the given number of threads.

Examples

Here is a basic usage example, assuming an alignment in FASTA format is found in the file "alignment.fasta.gz":

julia> using GaussDCA

julia> FNR = gDCA("alignment.fasta.gz");

julia> printrank("results_FN.txt", FNR)

The above uses the Frobenius norm ranking with default parameters. This is how to get the Direct Information ranking instead:

julia> DIR = gDCA("alignment.fasta.gz", pseudocount = 0.2, score = :DI);

julia> printrank("results_DI.txt", DIR)

Download Details:

Author: Carlobaldassi
Source Code: https://github.com/carlobaldassi/GaussDCA.jl 
License: Unknown, GPL-3.0 licenses found

#julia #contact #prediction 

Gaussian Direct Coupling analysis for Protein Contacts Predicion
Rupert  Beatty

Rupert Beatty

1666089074

Advanced Usage Of UIAlertController & Pickers Based on It: Telegram

Alerts & Pickers

Advanced usage of native UIAlertController with TextField, TextView, DatePicker, PickerView, TableView, CollectionView and MapView.

Features

  •  Custom pickers based on UITextField, UITextView, UIDatePicker, UIPickerView, UITableView, UICollectionView and MKMapView.
  •  Example using a Storyboard.
  •  Easy contentViewController placement.
  •  Attributed title label and message label.
  •  Button customization: image and title color.
  •  Understandable action button placement.
  •  Easy presentation.
  • Pure Swift 4.

actionSheet-.gif 

Usage

actionSheet-simple.gif alert-simple.gif

  • New Alert
let alert = UIAlertController(style: .alert, title: "Title", message: "Message")
// or
let alert = UIAlertController(style: .alert)
  • Set and styling title
alert.set(title: "Title", font: .systemFont(ofSize: 20), color: .black)
// or
alert.setTitle(font: .systemFont(ofSize: 20), color: .black)
  • Set and styling message
alert.set(message: "Message", font: .systemFont(ofSize: 16), color: .black)
// or
alert.setMessage(font: .systemFont(ofSize: 16), color: .black)
  • Add button with image
alert.addAction(image: image, title: "Title", color: .black, style: .default) { action in
    // completion handler
}
  • Show Alert
// show alert
alert.show()

// or show alert with options
alert.show(animated: true, vibrate: true) {
    // completion handler
}

actionSheet-simple-image.gif alert-simple-image.gif

Set Content ViewController

When setting your own custom UIViewController into UIAlertController keep in mind to set prefferedContentSize.height of the controller otherwise it will no effect. You can not set prefferedContentSize.width.

let alert = UIAlertController(style: .alert, title: "Title")
let vc = CustomViewController()
vc.preferredContentSize.height = height
alert.setValue(vc, forKey: "contentViewController")
alert.show()

// or
let alert = UIAlertController(style: .alert, title: "Title")
let vc = CustomViewController()
alert.set(vc: vc, height: height)
alert.show()

Pickers

For UX better to use .actionSheet style in UIAlertController when set picker into contentViewController. If you like you can use .alert style as well, buy .actionSheet style is wider and User can see more as well as action button is placing at bottom that also more convenience for User to touch it.

UITextField In native UIAlertController you can only add UITextField to .alert style with default style and you can not change such properties as .borderColor, .borderWidth, .frame.size and so on. But if you make your own UIViewController with UITextField, it will solve all these problems.

One TextField Picker

You can use both styles .alert and .actionSheet of UIAlertController.

 alert-textField-1.gif

let alert = UIAlertController(style: self.alertStyle, title: "TextField")                  
let config: TextField.Config = { textField in
    textField.becomeFirstResponder()
    textField.textColor = .black
    textField.placeholder = "Type something"
    textField.left(image: image, color: .black)
    textField.leftViewPadding = 12
    textField.borderWidth = 1
    textField.cornerRadius = 8
    textField.borderColor = UIColor.lightGray.withAlphaComponent(0.5)
    textField.backgroundColor = nil
    textField.keyboardAppearance = .default
    textField.keyboardType = .default
    textField.isSecureTextEntry = true
    textField.returnKeyType = .done
    textField.action { textField in
        // validation and so on
    }
}              
alert.addOneTextField(configuration: config)
alert.addAction(title: "OK", style: .cancel)
alert.show()

Two TextFields Picker

You can use both styles .alert and .actionSheet of UIAlertController.

actionSheet-textField-2.gif

let alert = UIAlertController(style: .alert, title: "Login")

let configOne: TextField.Config = { textField in
    textField.left(image: user), color: .black)
    textField.leftViewPadding = 16
    textField.leftTextPadding = 12
    textField.becomeFirstResponder()
    textField.backgroundColor = nil
    textField.textColor = .black
    textField.placeholder = "Name"
    textField.clearButtonMode = .whileEditing
    textField.keyboardAppearance = .default
    textField.keyboardType = .default
    textField.returnKeyType = .done
    textField.action { textField in
        // action with input
    }
}

let configTwo: TextField.Config = { textField in
    textField.textColor = .black
    textField.placeholder = "Password"
    textField.left(image: lock, color: .black)
    textField.leftViewPadding = 16
    textField.leftTextPadding = 12
    textField.borderWidth = 1
    textField.borderColor = UIColor.lightGray.withAlphaComponent(0.5)
    textField.backgroundColor = nil
    textField.clearsOnBeginEditing = true
    textField.keyboardAppearance = .default
    textField.keyboardType = .default
    textField.isSecureTextEntry = true
    textField.returnKeyType = .done
    textField.action { textField in
        // action with input
    }
}
// vInset - is top and bottom margin of two textFields   
alert.addTwoTextFields(vInset: 12, textFieldOne: configOne, textFieldTwo: configTwo)
alert.addAction(title: "OK", style: .cancel)
alert.show()

DatePicker

UIDatePicker does not look very much in .alert style.

 

let alert = UIAlertController(style: .actionSheet, title: "Select date") alert.addDatePicker(mode: .dateAndTime, date: date, minimumDate: minDate, maximumDate: maxDate) { date in    // action with selected date } alert.addAction(title: "OK", style: .cancel) alert.show()

PickerView

Example how to use UIPickerView as contentViewController and change height of the UIAlertController.

 

let alert = UIAlertController(style: .actionSheet, title: "Picker View", message: "Preferred Content Height") let frameSizes: [CGFloat] = (150...400).map { CGFloat($0) } let pickerViewValues: [[String]] = [frameSizes.map { Int($0).description }] let pickerViewSelectedValue: PickerViewViewController.Index = (column: 0, row: frameSizes.index(of: 216) ?? 0) alert.addPickerView(values: pickerViewValues, initialSelection: pickerViewSelectedValue) { vc, picker, index, values in    DispatchQueue.main.async {        UIView.animate(withDuration: 1) {            vc.preferredContentSize.height = frameSizes[index.row]        }    } } alert.addAction(title: "Done", style: .cancel) alert.show()

Locale Pickers

Country Picker

 

let alert = UIAlertController(style: .actionSheet, message: "Select Country") alert.addLocalePicker(type: .country) { info in    // action with selected object } alert.addAction(title: "OK", style: .cancel) alert.show()

Phone Code Picker

 

let alert = UIAlertController(style: .actionSheet, title: "Phone Codes") alert.addLocalePicker(type: .phoneCode) { info in    // action with selected object } alert.addAction(title: "OK", style: .cancel) alert.show()

Currency Picker

 

let alert = UIAlertController(style: .actionSheet, title: "Currencies") alert.addLocalePicker(type: .currency) { info in    alert.title = info?.currencyCode    alert.message = "is selected"    // action with selected object } alert.addAction(title: "OK", style: .cancel) alert.show()

Image Picker

 

  • Horizontal Image Picker with paging and single selection:

let alert = UIAlertController(style: .actionSheet) let photos: [UIImage] = images alert.addImagePicker(    flow: .horizontal,    paging: true,    images: photos,    selection: .single(action: { [unowned self] image in        // action with selected image    })) alert.addAction(title: "OK", style: .cancel) alert.show()

  • Vertical Image Picker w/o paging and with multiple selection:

let alert = UIAlertController(style: .actionSheet) let photos: [UIImage] = images alert.addImagePicker(    flow: .vertical,    paging: false,    height: UIScreen.main.bounds.height,    images: photos,    selection: .multiple(action: { [unowned self] images in        // action with selected images    })) alert.addAction(title: "OK", style: .cancel) alert.show()

PhotoLibrary Picker

 

let alert = UIAlertController(style: .actionSheet) alert.addPhotoLibraryPicker(    flow: .horizontal,    paging: true,    selection: .single(action: { image in        // action with selected image    })) alert.addAction(title: "Cancel", style: .cancel) alert.show()

ColorPicker

Example how to use UIViewController instantiated from Storyboard with Autolayout as contentViewController in the UIAlertController.

 

let alert = UIAlertController(style: .actionSheet) alert.addColorPicker(color: color) { color in    // action with selected color } alert.addAction(title: "Done", style: .cancel) alert.show()

Contacts Picker

 

let alert = UIAlertController(style: .actionSheet) alert.addContactsPicker { contact in    // action with contact } alert.addAction(title: "Cancel", style: .cancel) alert.show()

Location Picker

 

let alert = UIAlertController(style: .actionSheet) alert.addLocationPicker { location in    // action with location } alert.addAction(title: "Cancel", style: .cancel) alert.show()

Telegram Picker

 

let alert = UIAlertController(style: .actionSheet) alert.addTelegramPicker { result in    switch result {      case .photo(let assets):        // action with assets      case .contact(let contact):        // action with contact      case .location(let location):        // action with location    } } alert.addAction(title: "Cancel", style: .cancel) alert.show()

TextViewer

 

let alert = UIAlertController(style: .actionSheet) alert.addTextViewer(text: .attributedText(text)) alert.addAction(title: "OK", style: .cancel) alert.show()

Alerts vs. Action Sheets

There are some things to keep in mind when using .actionSheet and .alert styles:

  • Pickers better to use in .actionSheet style.
  • UITextField can be used in both styles.

Installing

Manually

Download and drop /Source folder in your project.

Requirements

  • Swift 4
  • iOS 11 or higher

Communication

  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request.

Download Details:

Author: Dillidon
Source Code: https://github.com/dillidon/alerts-and-pickers 
License: MIT license

#swift #map #telegram #login #contact 

Advanced Usage Of UIAlertController & Pickers Based on It: Telegram
Lawson  Wehner

Lawson Wehner

1660226525

Contactus: CONTACTUS Is A Flutter Package

Contact Us

The most common functionality added in any commercial app is the Developer's contact details!!
So this package helps the developers to simply add their details.

Now you can also add the contact details as a part of your bottomNavigationBar

Now you can add customizedable fonts for company/individual name, tagline & text along with the custom color for divider and font weights.

Developers can add following details:
 

  • Company Name
  • Phone Number
  • Website
  • Email ID
  • Twitter Handle
  • Instagram ID
  • Facebook ID
  • Linkedin URL
  • Github UserName

Best feature is that, when the user clicks on any detail, respective app/web page will be opened.

Now you can also add the contact details as a part of your bottomNavigationBar

Example Code for creating an entire page

import 'package:contactus/contactus.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.teal,
        body: ContactUs(
          logo: AssetImage('images/crop.jpg'),
          email: 'adoshi26.ad@gmail.com',
          companyName: 'Abhishek Doshi',
          phoneNumber: '+91123456789',
          dividerThickness: 2,
          website: 'https://abhishekdoshi.godaddysites.com',
          githubUserName: 'AbhishekDoshi26',
          linkedinURL: 'https://www.linkedin.com/in/abhishek-doshi-520983199/',
          tagLine: 'Flutter Developer',
          twitterHandle: 'AbhishekDoshi26',
          instagramUserName: '_abhishek_doshi',
        ),
      ),
    );
  }
}

Example Code for adding details in bottomNavigationBar

bottomNavigationBar: ContactUsBottomAppBar(
          companyName: 'Abhishek Doshi',
          textColor: Colors.white,
          backgroundColor: Colors.teal.shade300,
          email: 'adoshi26.ad@gmail.com',
        ),

Output

Output

Installing

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add contactus

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  contactus: ^1.2.0

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:contactus/contactus.dart';

example/lib/main.dart

import 'package:contactus/contactus.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        bottomNavigationBar: ContactUsBottomAppBar(
          companyName: 'Abhishek Doshi',
          textColor: Colors.white,
          backgroundColor: Colors.teal.shade300,
          email: 'adoshi26.ad@gmail.com',
          // textFont: 'Sail',
        ),
        backgroundColor: Colors.teal,
        body: ContactUs(
            cardColor: Colors.white,
            textColor: Colors.teal.shade900,
            logo: AssetImage('images/logo.jpg'),
            email: 'adoshi26.ad@gmail.com',
            companyName: 'Abhishek Doshi',
            companyColor: Colors.teal.shade100,
            dividerThickness: 2,
            phoneNumber: '+917818044311',
            website: 'https://abhishekdoshi.godaddysites.com',
            githubUserName: 'AbhishekDoshi26',
            linkedinURL:
                'https://www.linkedin.com/in/abhishek-doshi-520983199/',
            tagLine: 'Flutter Developer',
            taglineColor: Colors.teal.shade100,
            twitterHandle: 'AbhishekDoshi26',
            instagram: '_abhishek_doshi',
            facebookHandle: '_abhishek_doshi'),
      ),
    );
  }
}

Download Details:

Author: AbhishekDoshi26
Source Code: https://github.com/AbhishekDoshi26/contactus 
License: MIT license

#flutter #dart #contact

Contactus: CONTACTUS Is A Flutter Package
Joshua Yates

Joshua Yates

1652925418

How to Create Your Own Portfolio Website with HTML, CSS & JavaScript

The Ultimate Guide To Create A Complete Portfolio Website Using HTML CSS And JavaScript 🤯

Build Your Personal Portfolio ➡️ Showcase Your Skills ➡️ GET HIRED! 🔥

Your personal portfolio is one of the most important things in order to showcase your work and skills. An excellent portfolio website will immediately outshine you and put you ten steps ahead ✨

Learn how to build an amazing portfolio website using codedamn's playgrounds for absolutely FREE! 

Timestamp
0:00 Teaser
01:34 Starts Building Project for your Portfolio
04:50 Navigation Bar for website
12:46 Download Icons for Navigation Bar from Fontawesome
16:15 Build Homepage Design
24:04 Outro


How to Create a Portfolio Website Using HTML, CSS, JavaScript, and Bootstrap 5

In this blog post, I will discuss some of the benefits of creating a portfolio website. Then I'll show you how to create a beautiful responsive portfolio website for yourself using HTML, CSS, JavaScript and Bootstrap version 5.

Table Of Contents

  • Benefits of having a portfolio website
  • What is Bootstrap?
  • Folder Structure
  • How to Add a Navigation Menu to Your Portfolio
  • How to Add a Hero Header to the Portfolio
  • How to Make the About Section
  • How to Make the Services Section
  • How to Add Dark Background Color to Navbar on Page Scroll
  • How to Build the Portfolio Section
  • How to Build the Contact Section
  • How to Build the Footer Section
  • Adding Final Touches
  • Conclusion

Benefits of having a Portfolio Website

Having a portfolio website has several benefits, including:

  • it provides a platform to showcase your relevant skills and experience
  • it shows your personality
  • it lets hiring managers find you instead of you reaching out to them
  • you are easily searchable on search engines like Google

What is Bootstrap?

Bootstrap is a popular front-end CSS framework which is used to develop responsive and mobile friendly websites. The latest release of Bootstrap is version 5. You can find the official documentation of Bootstrap 5 here.

Folder Structure

We will now start working on creating the portfolio website.

First, let's create the folder structure. You can get the project starter files on GitHub. Also, you can visit here to see the live demo of this project.

Screenshot-from-2022-01-22-19-10-25

Project Folder Structure

The folder structure consists of index.html, style.css, and script.js files and an images folder. We'll write all CSS in the style.css file and the JavaScript in the script.js file .

In the index.html file, you can see the HTML boilerplate code with the Bootstrap CDN, font awesome kit, and a link to the external style sheet and JavaScript.

Here, the script.js file is loaded after loading all the HTML code.

How to Add a Navigation Menu to Your Portfolio

Now, let's work on adding a navigation menu in our project. It will help visitors find the relevant info they're looking for.

We will use Bootstrap's fixed-top class in nav element to keep the navbar at the top of the page. The navbar also has a navbar-brand class where we keep the name of the person as a brand.

<nav class="navbar navbar-expand-lg fixed-top navbarScroll">
        <div class="container">
            <a class="navbar-brand" href="#">Brad</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#home">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#about">About</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#services">Services</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#portfolio">Portfolio</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#contact">Contact</a>
                    </li>
                </ul>
                
            </div>
        </div>
    </nav>

The navbar has the following features:

  • It has six links: home, about, services, portfolio, contact, and footer
  • It has a transparent background. We will add a dark background on page scrolling later.
  • It toggles on smaller devices

You can find more details regarding Bootstrap 5 navbar features here.

However, the navbar has a problem while scrolling. It's fully transparent throughout the page which causes readability issues.  We will fix this issue after we complete the Services section to make you understand the issue properly.

How to Add a Hero Header to the Portfolio

Now, we will be adding a hero image with some text in the center. A hero image is a web design term which refers to a high quality full width image that displays the company or individual's main goals, a representative image, photo, or other eye-catching elements. It helps attract users to your site.

 <!-- main banner -->
    <section class="bgimage" id="home">
        <div class="container-fluid">
            <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 hero-text">
                <h2 class="hero_title">Hi, it's me Brad</h2>
                <p class="hero_desc">I am a professional freelancer in New York City</p>
            </div>
            </div>
        </div>
    </section>

 Also, let's add the CSS for the above code in the style.css file:

/* hero background image */
.bgimage {
    height:100vh;
    background: url('images/heroImage.jpeg');
    background-size:cover;
    position:relative;
}
/* text css above hero image*/
.hero_title {
    font-size: 4.5rem;
}
.hero_desc {
    font-size: 2rem;
}
.hero-text {
    text-align: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}

Here we can see that the section has an id named bgimage which is responsible for displaying the background hero image with full width. It also displays some text in the center above the background image with the help of the above CSS.

This is how the site looks so far with the navbar and the hero section:

Screenshot-from-2022-01-25-10-13-25

Hero Image with Navbar

How to Make the About Section

The About page contains important information about you and your background. Visitors to your portfolio site can get to know you through the information you provide in this page.

We will be adding an image to the left side of the row, and on the right side we will add our quick introduction in this section. Let's demonstrate it using the code below:

<!-- about section-->
    <section id="about">
        <div class="container mt-4 pt-4">
            <h1 class="text-center">About Me</h1>
            <div class="row mt-4">
                <div class="col-lg-4">
                    <img src="images/about.jpeg" class= "imageAboutPage" alt="">
                </div>

                <div class="col-lg-8">
                    <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged
                        
                    </p>
                    <div class="row mt-3">
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                    </div>
                    <div class="row mt-3">
                        <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                        </p>
                    </div>
                </div>
            </div>
    </section>

Let's add some CSS for the left side image:

/* about section image css */
.imageAboutPage {
    width: 100%;
}

This will create an about section. You can modify the content based on your use cases. We have added classes named mt-4 and pt-4 with container class which will set the margin top and padding top to 1.5 rem.

The row has two columns. One has the col-lg-4 class for displaying the image which will occupy the left column with a 4-part grid for large screens.

The next column is assigned a class of col-lg-8 which will occupy the right column with an 8-part grid for larger screens. For medium and small screens they will overlap with each other which we can see in the below GIF file:

about

About Section

How to Make the Services Section

This section helps convert website visitors into potential clients. This is where you explain what specific services you offer, and where you niche down your offered services.

Let's add the code for this section and describe it below:

<!-- services section-->
    <section id="services">
        <div class="container">
            <h1 class="text-center">Services</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class="fas servicesIcon fa-clock"></i>
                            <h4 class="card-title mt-3">Website Development</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-layer-group'></i>
                            <h4 class="card-title mt-3">Website Design</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='far servicesIcon fa-check-circle'></i>
                            <h4 class="card-title mt-3">Website Deployment</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>

            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-search'></i>
                            <h4 class="card-title mt-3">SEO</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-shield-alt'></i>
                            <h4 class="card-title mt-3">DevOps</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-wrench'></i>
                            <h4 class="card-title mt-3">QA</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>
        </div>
    </section>

Since this website is targeted towards web developers and designers, I've included some of the services which a web developer or designer might offer.

We have used bootstrap cards to display services. Our services section has 2 rows and 3 columns each. For large screens with a width greater than or equal to 992px, three cards are displayed in a row. For screens less than 992px wide, only a single card is displayed in a row.

You can find more about bootstrap breakpoints here.

Also, there are fonts added in each card to make them look better.

Without CSS, the services section would look like this :

Screenshot-from-2022-01-23-14-01-00

So, let's add some CSS to increase the font icon font size and card height and add some extra color when a user hovers over a card.

/* services section css */
.servicesText.card {
    height: 280px;
    cursor: pointer;
  }
.servicesIcon {
    font-size: 36px;
    text-align: center;
    width: 100%;
}
.card-title {
    text-align: center;
}
.card:hover .servicesIcon {
    color: #008000;
}
.servicesText:hover {
    border: 1px solid #008000;
}

This is how our services section looks now:

services

Services 

How to Add Dark Background Color to Navbar on Page Scroll

If you look into the above gif properly you will see that the navbar is transparent throughout the page which causes readability issues. So let's work on fixing this issue.

We will write some JavaScript and CSS in order to resolve this problem. We will add a navbarDark class in order to show a dark background color for the navbar on page scroll.

For that we need to go to the script.js file and add the following code:

// add class navbarDark on navbar scroll
const header = document.querySelector('.navbar');

window.onscroll = function() {
    var top = window.scrollY;
    if(top >=100) {
        header.classList.add('navbarDark');
    }
    else {
        header.classList.remove('navbarDark');
    }
}

Now, let's break down the above code:

  • The header holds the value of the nav element since the querySelector method returns the first element that matches the CSS selector (which is .navbar in this case).
  • window.onscroll fires up when the scroll event happens.
  • window.scrollY returns the number of pixels that the document is scrolled vertically and its value is assigned to a variable named top.
  • If the value of top is greater than or equal to 100, it adds a class of navbarDark to the header.

Let's quickly add CSS for the navbarDark class. For that, go to your style.css file and add the following code:

/* display background color black on navbar scroll */
.navbarScroll.navbarDark {
    background-color: black;
}

This is how the navbar will look now:

navbar

Dark background color on Navbar on page scroll

How to Build the Portfolio Section

This section includes your best work. People can see what you are capable of doing, and showcasing strong past work will definitely attract more potential clients or recruiters. So only add your best work in this section.

We will use Bootstrap cards to display the portfolio projects. There will be 2 rows and each row will have 3 columns of cards.

This will be the code for portfolio section:

<!-- portfolio section-->
    <section id="portfolio">
        <div class="container mt-3">
            <h1 class="text-center">Portfolio</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">YouTube Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Quiz App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage3.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Product Landing Page</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <br>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Messaging Service</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Twitter Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Blog App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </section>

Each card has an image, title, description, and link to the projects. Three cards are displayed in a row for large screens which have breakpoints of ≥ 992px wide, but for screens < 992px wide only a single card is displayed in a row.

The GIF below shows how the portfolio section looks now:

portfolio

Portfolio

How to Build the Contact Section

You should include your contact information in this section so that visitors can contact you if they want to hire you.

Our contact section will include 2 columns in a single row: Google maps for location and a contact form.

In order to embed the Google map, you need to follow these steps:

  • go to https://www.embed-map.com
  • enter your location
  • click on the Generate HTML Code button which will provide your Google Map HTML Code

Our code will look like this with the contact form included:

<!-- contact section-->
    <section id="contact">
        <div class="container mt-3 contactContent">
            <h1 class="text-center">Contact Me</h1>

            <div class="row mt-4">
                <div class="col-lg-6">
                    <!-- to edit google map goto https://www.embed-map.com type your location, generate html code and copy the html  -->
                    <div style="max-width:100%;overflow:hidden;color:red;width:500px;height:500px;">
                        <div id="embedmap-canvas" style="height:100%; width:100%;max-width:100%;">
                            <iframe style="height:100%;width:100%;border:0;" frameborder="0" src="https://www.google.com/maps/embed/v1/place?q=new+york&key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8">
                            </iframe>
                        </div>
                        <a class="googlemaps-html" href="https://www.embed-map.com" id="get-data-forembedmap">https://www.embed-map.com</a>
                        <style>#embedmap-canvas img{max-width:none!important;background:none!important;font-size: inherit;font-weight:inherit;}
                        </style>
                    </div>
                </div>

                <div class="col-lg-6">
                    <!-- form fields -->
                    <form>
                        <input type="text" class="form-control form-control-lg" placeholder="Name">
                        <input type="email" class="form-control mt-3" placeholder="Email">
                        <input type="text" class="form-control mt-3" placeholder="Subject">
                        <div class="mb-3 mt-3">
                            <textarea class="form-control" rows="5" id="comment" name="text" placeholder="Project Details"></textarea>
                        </div>
                    </form>
                    <button type="button" class="btn btn-success mt-3">Contact Me</button>
                    
                </div>

            </div>
        </div>
    </section>

The first column will display the Google map and the next one will display the contact form.

The form has four different form fields: name, email, subject and project details. The form doesn't submit the request itself. You will need to connect it with any back-end language. Or, you can simply use Netlify Form or Formspree form for this.

This is how the contact section will appear:

Screenshot-from-2022-01-25-11-31-56

Contact Section

How to Build the Footer Section

Now we have come to the last section of this post, which is the footer section. We have already added a link to the font awesome CDN in the index.html file.

In the Footer, we will add links to our social media through font awesome icons.

 <!-- footer section-->
    <footer id="footer">
        <div class="container-fluid">
            <!-- social media icons -->
            <div class="social-icons mt-4">
                <a href="https://www.facebook.com/" target="_blank"><i class="fab fa-facebook"></i></a>
                <a href="https://www.instagram.com/" target="_blank"><i class="fab fa-instagram"></i></a>
                <a href="https://www.twitter.com/" target="_blank"><i class="fab fa-twitter"></i></a>
                <a href="https://www.linkedin.com/" target="_blank"><i class="fab fa-linkedin"></i></a>
                <a href="https://www.twitch.tv/" target="_blank"><i class="fab fa-twitch"></i></a>
            </div>
        </div>
    </footer>

Without the CSS, our footer will look like this:

Screenshot-from-2022-01-23-17-56-37

footer without styling

So let's add some styling to the footer with this code:

/* social media icons styling */
.social-icons {
    font-size: 36px;
    cursor: pointer;
}
.fa-facebook:hover,.fa-instagram:hover,.fa-twitter:hover,.fa-linkedin:hover, .fa-twitch:hover {
    color: #008000;
}
.fab {
    color: #000000;
}
/* footer styling */
#footer {
    background-color: #808080;
    text-align: center;
}

The icons are now displayed in the center with a hover effect which we can see in the below GIF file.

footer

Footer

Final Touches

In order to add some spacing between all the sections, let's add some more styling:

/* spacing on all sections */
#about, #services, #portfolio, #contact {
    margin-top: 4rem;
    padding-top: 4rem;
}
#contact {
    padding-bottom: 4rem;
}

Now we're done making our complete portfolio website.

You can find the full source code of this project here.

Conclusion

This is how you can create a complete responsive portfolio website using HTML, CSS, JavaScript, and Bootstrap 5 .

In this blog post we saw some of the benefits of creating a portfolio website for web developers and designers. We divided the whole website into different sections and discussed each one individually as we built it.

You can customize this website based on your own use cases.

I hope you found this post useful.

Happy Coding!

Original article source at https://www.freecodecamp.org

#webdesign #webdevelopment #css #html #javascript #programming #webdev 

How to Create Your Own Portfolio Website with HTML, CSS & JavaScript
Web  Dev

Web Dev

1650349069

Create a Responsive Portfolio Website with HTML, CSS, JavaScript & Bootstrap 5

If you are a web developer or a web designer, it is essential for you to have a portfolio website. It lets you provide information about yourself and showcase your best work with your relevant skills and experience.

In this blog post, I will discuss some of the benefits of creating a portfolio website. Then I'll show you how to create a beautiful responsive portfolio website for yourself using HTML, CSS, JavaScript and Bootstrap version 5.

Table Of Contents

  • Benefits of having a portfolio website
  • What is Bootstrap?
  • Folder Structure
  • How to Add a Navigation Menu to Your Portfolio
  • How to Add a Hero Header to the Portfolio
  • How to Make the About Section
  • How to Make the Services Section
  • How to Add Dark Background Color to Navbar on Page Scroll
  • How to Build the Portfolio Section
  • How to Build the Contact Section
  • How to Build the Footer Section
  • Adding Final Touches
  • Conclusion

Benefits of having a Portfolio Website

Having a portfolio website has several benefits, including:

  • it provides a platform to showcase your relevant skills and experience
  • it shows your personality
  • it lets hiring managers find you instead of you reaching out to them
  • you are easily searchable on search engines like Google

What is Bootstrap?

Bootstrap is a popular front-end CSS framework which is used to develop responsive and mobile friendly websites. The latest release of Bootstrap is version 5. You can find the official documentation of Bootstrap 5 here.

Folder Structure

We will now start working on creating the portfolio website.

First, let's create the folder structure. You can get the project starter files on GitHub. Also, you can visit here to see the live demo of this project.

Screenshot-from-2022-01-22-19-10-25

Project Folder Structure

The folder structure consists of index.html, style.css, and script.js files and an images folder. We'll write all CSS in the style.css file and the JavaScript in the script.js file .

In the index.html file, you can see the HTML boilerplate code with the Bootstrap CDN, font awesome kit, and a link to the external style sheet and JavaScript.

Here, the script.js file is loaded after loading all the HTML code.

How to Add a Navigation Menu to Your Portfolio

Now, let's work on adding a navigation menu in our project. It will help visitors find the relevant info they're looking for.

We will use Bootstrap's fixed-top class in nav element to keep the navbar at the top of the page. The navbar also has a navbar-brand class where we keep the name of the person as a brand.

<nav class="navbar navbar-expand-lg fixed-top navbarScroll">
        <div class="container">
            <a class="navbar-brand" href="#">Brad</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#home">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#about">About</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#services">Services</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#portfolio">Portfolio</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#contact">Contact</a>
                    </li>
                </ul>
                
            </div>
        </div>
    </nav>

The navbar has the following features:

  • It has six links: home, about, services, portfolio, contact, and footer
  • It has a transparent background. We will add a dark background on page scrolling later.
  • It toggles on smaller devices

You can find more details regarding Bootstrap 5 navbar features here.

However, the navbar has a problem while scrolling. It's fully transparent throughout the page which causes readability issues.  We will fix this issue after we complete the Services section to make you understand the issue properly.

How to Add a Hero Header to the Portfolio

Now, we will be adding a hero image with some text in the center. A hero image is a web design term which refers to a high quality full width image that displays the company or individual's main goals, a representative image, photo, or other eye-catching elements. It helps attract users to your site.

 <!-- main banner -->
    <section class="bgimage" id="home">
        <div class="container-fluid">
            <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 hero-text">
                <h2 class="hero_title">Hi, it's me Brad</h2>
                <p class="hero_desc">I am a professional freelancer in New York City</p>
            </div>
            </div>
        </div>
    </section>

 Also, let's add the CSS for the above code in the style.css file:

/* hero background image */
.bgimage {
    height:100vh;
    background: url('images/heroImage.jpeg');
    background-size:cover;
    position:relative;
}
/* text css above hero image*/
.hero_title {
    font-size: 4.5rem;
}
.hero_desc {
    font-size: 2rem;
}
.hero-text {
    text-align: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}

Here we can see that the section has an id named bgimage which is responsible for displaying the background hero image with full width. It also displays some text in the center above the background image with the help of the above CSS.

This is how the site looks so far with the navbar and the hero section:

Screenshot-from-2022-01-25-10-13-25

Hero Image with Navbar

How to Make the About Section

The About page contains important information about you and your background. Visitors to your portfolio site can get to know you through the information you provide in this page.

We will be adding an image to the left side of the row, and on the right side we will add our quick introduction in this section. Let's demonstrate it using the code below:

<!-- about section-->
    <section id="about">
        <div class="container mt-4 pt-4">
            <h1 class="text-center">About Me</h1>
            <div class="row mt-4">
                <div class="col-lg-4">
                    <img src="images/about.jpeg" class= "imageAboutPage" alt="">
                </div>

                <div class="col-lg-8">
                    <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged
                        
                    </p>
                    <div class="row mt-3">
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                    </div>
                    <div class="row mt-3">
                        <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                        </p>
                    </div>
                </div>
            </div>
    </section>

Let's add some CSS for the left side image:

/* about section image css */
.imageAboutPage {
    width: 100%;
}

This will create an about section. You can modify the content based on your use cases. We have added classes named mt-4 and pt-4 with container class which will set the margin top and padding top to 1.5 rem.

The row has two columns. One has the col-lg-4 class for displaying the image which will occupy the left column with a 4-part grid for large screens.

The next column is assigned a class of col-lg-8 which will occupy the right column with an 8-part grid for larger screens. For medium and small screens they will overlap with each other which we can see in the below GIF file:

about

About Section

How to Make the Services Section

This section helps convert website visitors into potential clients. This is where you explain what specific services you offer, and where you niche down your offered services.

Let's add the code for this section and describe it below:

<!-- services section-->
    <section id="services">
        <div class="container">
            <h1 class="text-center">Services</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class="fas servicesIcon fa-clock"></i>
                            <h4 class="card-title mt-3">Website Development</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-layer-group'></i>
                            <h4 class="card-title mt-3">Website Design</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='far servicesIcon fa-check-circle'></i>
                            <h4 class="card-title mt-3">Website Deployment</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>

            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-search'></i>
                            <h4 class="card-title mt-3">SEO</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-shield-alt'></i>
                            <h4 class="card-title mt-3">DevOps</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-wrench'></i>
                            <h4 class="card-title mt-3">QA</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>
        </div>
    </section>

Since this website is targeted towards web developers and designers, I've included some of the services which a web developer or designer might offer.

We have used bootstrap cards to display services. Our services section has 2 rows and 3 columns each. For large screens with a width greater than or equal to 992px, three cards are displayed in a row. For screens less than 992px wide, only a single card is displayed in a row.

You can find more about bootstrap breakpoints here.

Also, there are fonts added in each card to make them look better.

Without CSS, the services section would look like this :

Screenshot-from-2022-01-23-14-01-00

So, let's add some CSS to increase the font icon font size and card height and add some extra color when a user hovers over a card.

/* services section css */
.servicesText.card {
    height: 280px;
    cursor: pointer;
  }
.servicesIcon {
    font-size: 36px;
    text-align: center;
    width: 100%;
}
.card-title {
    text-align: center;
}
.card:hover .servicesIcon {
    color: #008000;
}
.servicesText:hover {
    border: 1px solid #008000;
}

This is how our services section looks now:

services

Services 

How to Add Dark Background Color to Navbar on Page Scroll

If you look into the above gif properly you will see that the navbar is transparent throughout the page which causes readability issues. So let's work on fixing this issue.

We will write some JavaScript and CSS in order to resolve this problem. We will add a navbarDark class in order to show a dark background color for the navbar on page scroll.

For that we need to go to the script.js file and add the following code:

// add class navbarDark on navbar scroll
const header = document.querySelector('.navbar');

window.onscroll = function() {
    var top = window.scrollY;
    if(top >=100) {
        header.classList.add('navbarDark');
    }
    else {
        header.classList.remove('navbarDark');
    }
}

Now, let's break down the above code:

  • The header holds the value of the nav element since the querySelector method returns the first element that matches the CSS selector (which is .navbar in this case).
  • window.onscroll fires up when the scroll event happens.
  • window.scrollY returns the number of pixels that the document is scrolled vertically and its value is assigned to a variable named top.
  • If the value of top is greater than or equal to 100, it adds a class of navbarDark to the header.

Let's quickly add CSS for the navbarDark class. For that, go to your style.css file and add the following code:

/* display background color black on navbar scroll */
.navbarScroll.navbarDark {
    background-color: black;
}

This is how the navbar will look now:

navbar

Dark background color on Navbar on page scroll

How to Build the Portfolio Section

This section includes your best work. People can see what you are capable of doing, and showcasing strong past work will definitely attract more potential clients or recruiters. So only add your best work in this section.

We will use Bootstrap cards to display the portfolio projects. There will be 2 rows and each row will have 3 columns of cards.

This will be the code for portfolio section:

<!-- portfolio section-->
    <section id="portfolio">
        <div class="container mt-3">
            <h1 class="text-center">Portfolio</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">YouTube Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Quiz App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage3.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Product Landing Page</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <br>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Messaging Service</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Twitter Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Blog App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </section>

Each card has an image, title, description, and link to the projects. Three cards are displayed in a row for large screens which have breakpoints of ≥ 992px wide, but for screens < 992px wide only a single card is displayed in a row.

The GIF below shows how the portfolio section looks now:

portfolio

Portfolio

How to Build the Contact Section

You should include your contact information in this section so that visitors can contact you if they want to hire you.

Our contact section will include 2 columns in a single row: Google maps for location and a contact form.

In order to embed the Google map, you need to follow these steps:

  • go to https://www.embed-map.com
  • enter your location
  • click on the Generate HTML Code button which will provide your Google Map HTML Code

Our code will look like this with the contact form included:

<!-- contact section-->
    <section id="contact">
        <div class="container mt-3 contactContent">
            <h1 class="text-center">Contact Me</h1>

            <div class="row mt-4">
                <div class="col-lg-6">
                    <!-- to edit google map goto https://www.embed-map.com type your location, generate html code and copy the html  -->
                    <div style="max-width:100%;overflow:hidden;color:red;width:500px;height:500px;">
                        <div id="embedmap-canvas" style="height:100%; width:100%;max-width:100%;">
                            <iframe style="height:100%;width:100%;border:0;" frameborder="0" src="https://www.google.com/maps/embed/v1/place?q=new+york&key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8">
                            </iframe>
                        </div>
                        <a class="googlemaps-html" href="https://www.embed-map.com" id="get-data-forembedmap">https://www.embed-map.com</a>
                        <style>#embedmap-canvas img{max-width:none!important;background:none!important;font-size: inherit;font-weight:inherit;}
                        </style>
                    </div>
                </div>

                <div class="col-lg-6">
                    <!-- form fields -->
                    <form>
                        <input type="text" class="form-control form-control-lg" placeholder="Name">
                        <input type="email" class="form-control mt-3" placeholder="Email">
                        <input type="text" class="form-control mt-3" placeholder="Subject">
                        <div class="mb-3 mt-3">
                            <textarea class="form-control" rows="5" id="comment" name="text" placeholder="Project Details"></textarea>
                        </div>
                    </form>
                    <button type="button" class="btn btn-success mt-3">Contact Me</button>
                    
                </div>

            </div>
        </div>
    </section>

The first column will display the Google map and the next one will display the contact form.

The form has four different form fields: name, email, subject and project details. The form doesn't submit the request itself. You will need to connect it with any back-end language. Or, you can simply use Netlify Form or Formspree form for this.

This is how the contact section will appear:

Screenshot-from-2022-01-25-11-31-56

Contact Section

How to Build the Footer Section

Now we have come to the last section of this post, which is the footer section. We have already added a link to the font awesome CDN in the index.html file.

In the Footer, we will add links to our social media through font awesome icons.

 <!-- footer section-->
    <footer id="footer">
        <div class="container-fluid">
            <!-- social media icons -->
            <div class="social-icons mt-4">
                <a href="https://www.facebook.com/" target="_blank"><i class="fab fa-facebook"></i></a>
                <a href="https://www.instagram.com/" target="_blank"><i class="fab fa-instagram"></i></a>
                <a href="https://www.twitter.com/" target="_blank"><i class="fab fa-twitter"></i></a>
                <a href="https://www.linkedin.com/" target="_blank"><i class="fab fa-linkedin"></i></a>
                <a href="https://www.twitch.tv/" target="_blank"><i class="fab fa-twitch"></i></a>
            </div>
        </div>
    </footer>

Without the CSS, our footer will look like this:

Screenshot-from-2022-01-23-17-56-37

footer without styling

So let's add some styling to the footer with this code:

/* social media icons styling */
.social-icons {
    font-size: 36px;
    cursor: pointer;
}
.fa-facebook:hover,.fa-instagram:hover,.fa-twitter:hover,.fa-linkedin:hover, .fa-twitch:hover {
    color: #008000;
}
.fab {
    color: #000000;
}
/* footer styling */
#footer {
    background-color: #808080;
    text-align: center;
}

The icons are now displayed in the center with a hover effect which we can see in the below GIF file.

footer

Footer

Final Touches

In order to add some spacing between all the sections, let's add some more styling:

/* spacing on all sections */
#about, #services, #portfolio, #contact {
    margin-top: 4rem;
    padding-top: 4rem;
}
#contact {
    padding-bottom: 4rem;
}

Now we're done making our complete portfolio website.

You can find the full source code of this project here.

Conclusion

This is how you can create a complete responsive portfolio website using HTML, CSS, JavaScript, and Bootstrap 5 .

In this blog post we saw some of the benefits of creating a portfolio website for web developers and designers. We divided the whole website into different sections and discussed each one individually as we built it.

You can customize this website based on your own use cases.

I hope you found this post useful.

Happy Coding!

Original article source at https://www.freecodecamp.org

#html #css #javascript #bootstrap #webdev #programming

Create a Responsive Portfolio Website with HTML, CSS, JavaScript & Bootstrap 5
Web  Dev

Web Dev

1645673869

Responsive Portfolio Website using HTML, CSS, JavaScript and Bootstrap 5

How to Create a Portfolio Website Using HTML, CSS, JavaScript, and Bootstrap 5

If you are a web developer or a web designer, it is essential for you to have a portfolio website. It lets you provide information about yourself and showcase your best work with your relevant skills and experience.

In this blog post, I will discuss some of the benefits of creating a portfolio website. Then I'll show you how to create a beautiful responsive portfolio website for yourself using HTML, CSS, JavaScript and Bootstrap version 5.

Table Of Contents

  • Benefits of having a portfolio website
  • What is Bootstrap?
  • Folder Structure
  • How to Add a Navigation Menu to Your Portfolio
  • How to Add a Hero Header to the Portfolio
  • How to Make the About Section
  • How to Make the Services Section
  • How to Add Dark Background Color to Navbar on Page Scroll
  • How to Build the Portfolio Section
  • How to Build the Contact Section
  • How to Build the Footer Section
  • Adding Final Touches
  • Conclusion

Benefits of having a Portfolio Website

Having a portfolio website has several benefits, including:

  • it provides a platform to showcase your relevant skills and experience
  • it shows your personality
  • it lets hiring managers find you instead of you reaching out to them
  • you are easily searchable on search engines like Google

What is Bootstrap?

Bootstrap is a popular front-end CSS framework which is used to develop responsive and mobile friendly websites. The latest release of Bootstrap is version 5. You can find the official documentation of Bootstrap 5 here.

Folder Structure

We will now start working on creating the portfolio website.

First, let's create the folder structure. You can get the project starter files on GitHub. Also, you can visit here to see the live demo of this project.

Screenshot-from-2022-01-22-19-10-25

Project Folder Structure

The folder structure consists of index.html, style.css, and script.js files and an images folder. We'll write all CSS in the style.css file and the JavaScript in the script.js file .

In the index.html file, you can see the HTML boilerplate code with the Bootstrap CDN, font awesome kit, and a link to the external style sheet and JavaScript.

Here, the script.js file is loaded after loading all the HTML code.

How to Add a Navigation Menu to Your Portfolio

Now, let's work on adding a navigation menu in our project. It will help visitors find the relevant info they're looking for.

We will use Bootstrap's fixed-top class in nav element to keep the navbar at the top of the page. The navbar also has a navbar-brand class where we keep the name of the person as a brand.

<nav class="navbar navbar-expand-lg fixed-top navbarScroll">
        <div class="container">
            <a class="navbar-brand" href="#">Brad</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#home">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#about">About</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#services">Services</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#portfolio">Portfolio</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#contact">Contact</a>
                    </li>
                </ul>
                
            </div>
        </div>
    </nav>

The navbar has the following features:

  • It has six links: home, about, services, portfolio, contact, and footer
  • It has a transparent background. We will add a dark background on page scrolling later.
  • It toggles on smaller devices

You can find more details regarding Bootstrap 5 navbar features here.

However, the navbar has a problem while scrolling. It's fully transparent throughout the page which causes readability issues.  We will fix this issue after we complete the Services section to make you understand the issue properly.

How to Add a Hero Header to the Portfolio

Now, we will be adding a hero image with some text in the center. A hero image is a web design term which refers to a high quality full width image that displays the company or individual's main goals, a representative image, photo, or other eye-catching elements. It helps attract users to your site.

 <!-- main banner -->
    <section class="bgimage" id="home">
        <div class="container-fluid">
            <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 hero-text">
                <h2 class="hero_title">Hi, it's me Brad</h2>
                <p class="hero_desc">I am a professional freelancer in New York City</p>
            </div>
            </div>
        </div>
    </section>

 Also, let's add the CSS for the above code in the style.css file:

/* hero background image */
.bgimage {
    height:100vh;
    background: url('images/heroImage.jpeg');
    background-size:cover;
    position:relative;
}
/* text css above hero image*/
.hero_title {
    font-size: 4.5rem;
}
.hero_desc {
    font-size: 2rem;
}
.hero-text {
    text-align: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}

Here we can see that the section has an id named bgimage which is responsible for displaying the background hero image with full width. It also displays some text in the center above the background image with the help of the above CSS.

This is how the site looks so far with the navbar and the hero section:

Screenshot-from-2022-01-25-10-13-25

Hero Image with Navbar

How to Make the About Section

The About page contains important information about you and your background. Visitors to your portfolio site can get to know you through the information you provide in this page.

We will be adding an image to the left side of the row, and on the right side we will add our quick introduction in this section. Let's demonstrate it using the code below:

<!-- about section-->
    <section id="about">
        <div class="container mt-4 pt-4">
            <h1 class="text-center">About Me</h1>
            <div class="row mt-4">
                <div class="col-lg-4">
                    <img src="images/about.jpeg" class= "imageAboutPage" alt="">
                </div>

                <div class="col-lg-8">
                    <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged
                        
                    </p>
                    <div class="row mt-3">
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                    </div>
                    <div class="row mt-3">
                        <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                        </p>
                    </div>
                </div>
            </div>
    </section>

Let's add some CSS for the left side image:

/* about section image css */
.imageAboutPage {
    width: 100%;
}

This will create an about section. You can modify the content based on your use cases. We have added classes named mt-4 and pt-4 with container class which will set the margin top and padding top to 1.5 rem.

The row has two columns. One has the col-lg-4 class for displaying the image which will occupy the left column with a 4-part grid for large screens.

The next column is assigned a class of col-lg-8 which will occupy the right column with an 8-part grid for larger screens. For medium and small screens they will overlap with each other which we can see in the below GIF file:

about

About Section

How to Make the Services Section

This section helps convert website visitors into potential clients. This is where you explain what specific services you offer, and where you niche down your offered services.

Let's add the code for this section and describe it below:

<!-- services section-->
    <section id="services">
        <div class="container">
            <h1 class="text-center">Services</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class="fas servicesIcon fa-clock"></i>
                            <h4 class="card-title mt-3">Website Development</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-layer-group'></i>
                            <h4 class="card-title mt-3">Website Design</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='far servicesIcon fa-check-circle'></i>
                            <h4 class="card-title mt-3">Website Deployment</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>

            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-search'></i>
                            <h4 class="card-title mt-3">SEO</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-shield-alt'></i>
                            <h4 class="card-title mt-3">DevOps</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-wrench'></i>
                            <h4 class="card-title mt-3">QA</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>
        </div>
    </section>

Since this website is targeted towards web developers and designers, I've included some of the services which a web developer or designer might offer.

We have used bootstrap cards to display services. Our services section has 2 rows and 3 columns each. For large screens with a width greater than or equal to 992px, three cards are displayed in a row. For screens less than 992px wide, only a single card is displayed in a row.

You can find more about bootstrap breakpoints here.

Also, there are fonts added in each card to make them look better.

Without CSS, the services section would look like this :

Screenshot-from-2022-01-23-14-01-00

So, let's add some CSS to increase the font icon font size and card height and add some extra color when a user hovers over a card.

/* services section css */
.servicesText.card {
    height: 280px;
    cursor: pointer;
  }
.servicesIcon {
    font-size: 36px;
    text-align: center;
    width: 100%;
}
.card-title {
    text-align: center;
}
.card:hover .servicesIcon {
    color: #008000;
}
.servicesText:hover {
    border: 1px solid #008000;
}

This is how our services section looks now:

services

Services 

How to Add Dark Background Color to Navbar on Page Scroll

If you look into the above gif properly you will see that the navbar is transparent throughout the page which causes readability issues. So let's work on fixing this issue.

We will write some JavaScript and CSS in order to resolve this problem. We will add a navbarDark class in order to show a dark background color for the navbar on page scroll.

For that we need to go to the script.js file and add the following code:

// add class navbarDark on navbar scroll
const header = document.querySelector('.navbar');

window.onscroll = function() {
    var top = window.scrollY;
    if(top >=100) {
        header.classList.add('navbarDark');
    }
    else {
        header.classList.remove('navbarDark');
    }
}

Now, let's break down the above code:

  • The header holds the value of the nav element since the querySelector method returns the first element that matches the CSS selector (which is .navbar in this case).
  • window.onscroll fires up when the scroll event happens.
  • window.scrollY returns the number of pixels that the document is scrolled vertically and its value is assigned to a variable named top.
  • If the value of top is greater than or equal to 100, it adds a class of navbarDark to the header.

Let's quickly add CSS for the navbarDark class. For that, go to your style.css file and add the following code:

/* display background color black on navbar scroll */
.navbarScroll.navbarDark {
    background-color: black;
}

This is how the navbar will look now:

navbar

Dark background color on Navbar on page scroll

How to Build the Portfolio Section

This section includes your best work. People can see what you are capable of doing, and showcasing strong past work will definitely attract more potential clients or recruiters. So only add your best work in this section.

We will use Bootstrap cards to display the portfolio projects. There will be 2 rows and each row will have 3 columns of cards.

This will be the code for portfolio section:

<!-- portfolio section-->
    <section id="portfolio">
        <div class="container mt-3">
            <h1 class="text-center">Portfolio</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">YouTube Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Quiz App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage3.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Product Landing Page</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <br>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Messaging Service</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Twitter Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Blog App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </section>

Each card has an image, title, description, and link to the projects. Three cards are displayed in a row for large screens which have breakpoints of ≥ 992px wide, but for screens < 992px wide only a single card is displayed in a row.

The GIF below shows how the portfolio section looks now:

portfolio

Portfolio

How to Build the Contact Section

You should include your contact information in this section so that visitors can contact you if they want to hire you.

Our contact section will include 2 columns in a single row: Google maps for location and a contact form.

In order to embed the Google map, you need to follow these steps:

  • go to https://www.embed-map.com
  • enter your location
  • click on the Generate HTML Code button which will provide your Google Map HTML Code

Our code will look like this with the contact form included:

<!-- contact section-->
    <section id="contact">
        <div class="container mt-3 contactContent">
            <h1 class="text-center">Contact Me</h1>

            <div class="row mt-4">
                <div class="col-lg-6">
                    <!-- to edit google map goto https://www.embed-map.com type your location, generate html code and copy the html  -->
                    <div style="max-width:100%;overflow:hidden;color:red;width:500px;height:500px;">
                        <div id="embedmap-canvas" style="height:100%; width:100%;max-width:100%;">
                            <iframe style="height:100%;width:100%;border:0;" frameborder="0" src="https://www.google.com/maps/embed/v1/place?q=new+york&key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8">
                            </iframe>
                        </div>
                        <a class="googlemaps-html" href="https://www.embed-map.com" id="get-data-forembedmap">https://www.embed-map.com</a>
                        <style>#embedmap-canvas img{max-width:none!important;background:none!important;font-size: inherit;font-weight:inherit;}
                        </style>
                    </div>
                </div>

                <div class="col-lg-6">
                    <!-- form fields -->
                    <form>
                        <input type="text" class="form-control form-control-lg" placeholder="Name">
                        <input type="email" class="form-control mt-3" placeholder="Email">
                        <input type="text" class="form-control mt-3" placeholder="Subject">
                        <div class="mb-3 mt-3">
                            <textarea class="form-control" rows="5" id="comment" name="text" placeholder="Project Details"></textarea>
                        </div>
                    </form>
                    <button type="button" class="btn btn-success mt-3">Contact Me</button>
                    
                </div>

            </div>
        </div>
    </section>

The first column will display the Google map and the next one will display the contact form.

The form has four different form fields: name, email, subject and project details. The form doesn't submit the request itself. You will need to connect it with any back-end language. Or, you can simply use Netlify Form or Formspree form for this.

This is how the contact section will appear:

Screenshot-from-2022-01-25-11-31-56

Contact Section

How to Build the Footer Section

Now we have come to the last section of this post, which is the footer section. We have already added a link to the font awesome CDN in the index.html file.

In the Footer, we will add links to our social media through font awesome icons.

 <!-- footer section-->
    <footer id="footer">
        <div class="container-fluid">
            <!-- social media icons -->
            <div class="social-icons mt-4">
                <a href="https://www.facebook.com/" target="_blank"><i class="fab fa-facebook"></i></a>
                <a href="https://www.instagram.com/" target="_blank"><i class="fab fa-instagram"></i></a>
                <a href="https://www.twitter.com/" target="_blank"><i class="fab fa-twitter"></i></a>
                <a href="https://www.linkedin.com/" target="_blank"><i class="fab fa-linkedin"></i></a>
                <a href="https://www.twitch.tv/" target="_blank"><i class="fab fa-twitch"></i></a>
            </div>
        </div>
    </footer>

Without the CSS, our footer will look like this:

Screenshot-from-2022-01-23-17-56-37

footer without styling

So let's add some styling to the footer with this code:

/* social media icons styling */
.social-icons {
    font-size: 36px;
    cursor: pointer;
}
.fa-facebook:hover,.fa-instagram:hover,.fa-twitter:hover,.fa-linkedin:hover, .fa-twitch:hover {
    color: #008000;
}
.fab {
    color: #000000;
}
/* footer styling */
#footer {
    background-color: #808080;
    text-align: center;
}

The icons are now displayed in the center with a hover effect which we can see in the below GIF file.

footer

Footer

Final Touches

In order to add some spacing between all the sections, let's add some more styling:

/* spacing on all sections */
#about, #services, #portfolio, #contact {
    margin-top: 4rem;
    padding-top: 4rem;
}
#contact {
    padding-bottom: 4rem;
}

Now we're done making our complete portfolio website.

You can find the full source code of this project here.

Conclusion

This is how you can create a complete responsive portfolio website using HTML, CSS, JavaScript, and Bootstrap 5 .

In this blog post we saw some of the benefits of creating a portfolio website for web developers and designers. We divided the whole website into different sections and discussed each one individually as we built it.

You can customize this website based on your own use cases.

I hope you found this post useful.

Happy Coding!

Original article source at https://www.freecodecamp.org

#html #css #javascript #express #bootstrap #webdev

Responsive Portfolio Website using HTML, CSS, JavaScript and Bootstrap 5

Cómo Crear Un Sitio Web De Cartera Usando HTML, CSS, JavaScript

Si eres un desarrollador web o un diseñador web, es esencial que tengas un sitio web de cartera. Le permite proporcionar información sobre usted y mostrar su mejor trabajo con sus habilidades y experiencia relevantes.

En esta publicación de blog, analizaré algunos de los beneficios de crear un sitio web de cartera. Luego, le mostraré cómo crear un hermoso sitio web de portafolio receptivo para usted usando HTML, CSS, JavaScript y Bootstrap versión 5.

Beneficios de tener un sitio web de cartera

Tener un sitio web de cartera tiene varios beneficios, que incluyen:

  • proporciona una plataforma para mostrar sus habilidades y experiencia relevantes
  • muestra tu personalidad
  • permite que los gerentes de contratación lo encuentren en lugar de que usted se comunique con ellos
  • se le puede buscar fácilmente en motores de búsqueda como Google

¿Qué es Bootstrap?

Bootstrap es un marco CSS front-end popular que se utiliza para desarrollar sitios web receptivos y compatibles con dispositivos móviles. La última versión de Bootstrap es la versión 5. Puede encontrar la documentación oficial de Bootstrap 5 aquí .

Estructura de carpetas

Ahora comenzaremos a trabajar en la creación del sitio web de la cartera.

Primero, vamos a crear la estructura de carpetas. Puede obtener los archivos de inicio del proyecto en GitHub . Además, puede visitar aquí para ver la demostración en vivo de este proyecto.

Captura de pantalla de 2022-01-22-19-10-25

Estructura de carpetas del proyecto

La estructura de carpetas consta de archivos index.html, style.css y script.js y una carpeta de imágenes. Escribiremos todo el CSS en el archivo style.css y el JavaScript en el archivo script.js.

En el archivo index.html, puede ver el código repetitivo HTML con Bootstrap CDN, el kit impresionante de fuentes y un enlace a la hoja de estilo externa y JavaScript.

Aquí, el archivo script.js se carga después de cargar todo el código HTML.

Cómo agregar un menú de navegación a su cartera

Ahora, trabajemos en agregar un menú de navegación en nuestro proyecto. Ayudará a los visitantes a encontrar la información relevante que están buscando.

Usaremos la fixed-topclase de Bootstrap en el elemento de navegación para mantener la barra de navegación en la parte superior de la página. La barra de navegación también tiene una navbar-brandclase donde guardamos el nombre de la persona como marca.

<nav class="navbar navbar-expand-lg fixed-top navbarScroll">
        <div class="container">
            <a class="navbar-brand" href="#">Brad</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#home">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#about">About</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#services">Services</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#portfolio">Portfolio</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#contact">Contact</a>
                    </li>
                </ul>
                
            </div>
        </div>
    </nav>

La barra de navegación tiene las siguientes características:

  • Tiene seis enlaces: inicio, acerca de, servicios, cartera, contacto y pie de página.
  • Tiene un fondo transparente. Agregaremos un fondo oscuro en el desplazamiento de la página más adelante.
  • Se alterna en dispositivos más pequeños.

Puede encontrar más detalles sobre las funciones de la barra de navegación de Bootstrap 5 aquí .

Sin embargo, la barra de navegación tiene un problema al desplazarse. Es totalmente transparente en toda la página, lo que provoca problemas de legibilidad. Solucionaremos este problema después de que completemos la sección Servicios para que comprenda el problema correctamente.

Cómo agregar un encabezado de héroe a la cartera

Ahora, agregaremos una imagen de héroe con algo de texto en el centro. Una imagen de héroe es un término de diseño web que se refiere a una imagen de ancho completo de alta calidad que muestra los objetivos principales de la empresa o del individuo, una imagen representativa, una foto u otros elementos llamativos. Ayuda a atraer usuarios a su sitio.

 <!-- main banner -->
    <section class="bgimage" id="home">
        <div class="container-fluid">
            <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 hero-text">
                <h2 class="hero_title">Hi, it's me Brad</h2>
                <p class="hero_desc">I am a professional freelancer in New York City</p>
            </div>
            </div>
        </div>
    </section>

 Además, agreguemos el CSS para el código anterior en el archivo style.css:

/* hero background image */
.bgimage {
    height:100vh;
    background: url('images/heroImage.jpeg');
    background-size:cover;
    position:relative;
}
/* text css above hero image*/
.hero_title {
    font-size: 4.5rem;
}
.hero_desc {
    font-size: 2rem;
}
.hero-text {
    text-align: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}

Aquí podemos ver que la sección tiene una identificación llamada bgimageque es responsable de mostrar la imagen del héroe de fondo con todo el ancho. También muestra algo de texto en el centro sobre la imagen de fondo con la ayuda del CSS anterior.

Así es como se ve el sitio hasta ahora con la barra de navegación y la sección principal:

Captura de pantalla de 2022-01-25-10-13-25

Imagen destacada con barra de navegación

Cómo hacer la sección Acerca de

La página Acerca de contiene información importante sobre usted y sus antecedentes. Los visitantes del sitio de su cartera pueden conocerlo a través de la información que proporciona en esta página.

Agregaremos una imagen al lado izquierdo de la fila, y en el lado derecho agregaremos nuestra introducción rápida en esta sección. Vamos a demostrarlo usando el siguiente código:

<!-- about section-->
    <section id="about">
        <div class="container mt-4 pt-4">
            <h1 class="text-center">About Me</h1>
            <div class="row mt-4">
                <div class="col-lg-4">
                    <img src="images/about.jpeg" class= "imageAboutPage" alt="">
                </div>

                <div class="col-lg-8">
                    <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged
                        
                    </p>
                    <div class="row mt-3">
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                    </div>
                    <div class="row mt-3">
                        <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                        </p>
                    </div>
                </div>
            </div>
    </section>

Agreguemos algo de CSS para la imagen del lado izquierdo:

/* about section image css */
.imageAboutPage {
    width: 100%;
}

Esto creará una sección acerca de. Puede modificar el contenido en función de sus casos de uso. Hemos agregado clases con nombre mt-4y pt-4clase de contenedor que establecerán el margen superior y el relleno superior en 1,5 rem.

La fila tiene dos columnas. Uno tiene la col-lg-4clase para mostrar la imagen que ocupará la columna izquierda con una cuadrícula de 4 partes para pantallas grandes.

A la siguiente columna se le asigna una clase de col-lg-8la cual ocupará la columna de la derecha con una cuadrícula de 8 partes para pantallas más grandes. Para pantallas medianas y pequeñas, se superpondrán entre sí, lo que podemos ver en el siguiente archivo GIF:

acerca de

Acerca de la sección

Cómo hacer la sección de servicios

Esta sección ayuda a convertir a los visitantes del sitio web en clientes potenciales. Aquí es donde explica qué servicios específicos ofrece y dónde clasifica los servicios ofrecidos.

Agreguemos el código para esta sección y describámoslo a continuación:

<!-- services section-->
    <section id="services">
        <div class="container">
            <h1 class="text-center">Services</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class="fas servicesIcon fa-clock"></i>
                            <h4 class="card-title mt-3">Website Development</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-layer-group'></i>
                            <h4 class="card-title mt-3">Website Design</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='far servicesIcon fa-check-circle'></i>
                            <h4 class="card-title mt-3">Website Deployment</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>

            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-search'></i>
                            <h4 class="card-title mt-3">SEO</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-shield-alt'></i>
                            <h4 class="card-title mt-3">DevOps</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-wrench'></i>
                            <h4 class="card-title mt-3">QA</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>
        </div>
    </section>

Dado que este sitio web está dirigido a desarrolladores y diseñadores web, he incluido algunos de los servicios que un desarrollador o diseñador web podría ofrecer.

Hemos utilizado tarjetas de arranque para mostrar servicios. Nuestra sección de servicios tiene 2 filas y 3 columnas cada una. Para pantallas grandes con un ancho mayor o igual a 992 px, se muestran tres tarjetas seguidas. Para pantallas de menos de 992 px de ancho, solo se muestra una tarjeta en una fila.

Puede encontrar más información sobre los puntos de interrupción de arranque aquí .

Además, se agregan fuentes en cada tarjeta para que se vean mejor.

Sin CSS, la sección de servicios se vería así:

Captura de pantalla de 2022-01-23-14-01-00

Por lo tanto, agreguemos algo de CSS para aumentar el tamaño de fuente del icono de fuente y la altura de la tarjeta y agreguemos un poco de color adicional cuando un usuario pase el mouse sobre una tarjeta.

/* services section css */
.servicesText.card {
    height: 280px;
    cursor: pointer;
  }
.servicesIcon {
    font-size: 36px;
    text-align: center;
    width: 100%;
}
.card-title {
    text-align: center;
}
.card:hover .servicesIcon {
    color: #008000;
}
.servicesText:hover {
    border: 1px solid #008000;
}

Así luce ahora nuestra sección de servicios:

servicios

Servicios 

Cómo agregar un color de fondo oscuro a la barra de navegación en el desplazamiento de página

Si observa correctamente el gif anterior, verá que la barra de navegación es transparente en toda la página, lo que provoca problemas de legibilidad. Entonces, trabajemos para solucionar este problema.

Escribiremos algo de JavaScript y CSS para resolver este problema. Agregaremos una navbarDarkclase para mostrar un color de fondo oscuro para la barra de navegación en el desplazamiento de la página.

Para eso necesitamos ir al archivo script.js y agregar el siguiente código:

// add class navbarDark on navbar scroll
const header = document.querySelector('.navbar');

window.onscroll = function() {
    var top = window.scrollY;
    if(top >=100) {
        header.classList.add('navbarDark');
    }
    else {
        header.classList.remove('navbarDark');
    }
}

Ahora, analicemos el código anterior:

  • El encabezado contiene el valor del elemento de navegación ya que el método querySelector devuelve el primer elemento que coincide con el selector de CSS (que es .navbaren este caso).
  • window.onscroll se activa cuando ocurre el evento de desplazamiento.
  • window.scrollYdevuelve el número de píxeles que el documento se desplaza verticalmente y su valor se asigna a una variable llamada top.
  • Si el valor de topes mayor o igual a 100, agrega una clase de navbarDarkal encabezado.

Agreguemos rápidamente CSS para la navbarDarkclase. Para eso, ve a tu archivo style.css y agrega el siguiente código:

/* display background color black on navbar scroll */
.navbarScroll.navbarDark {
    background-color: black;
}

Así es como se verá la barra de navegación ahora:

barra de navegación

Color de fondo oscuro en la barra de navegación al desplazarse por la página

Cómo construir la sección de cartera

Esta sección incluye tu mejor trabajo. La gente puede ver lo que eres capaz de hacer, y mostrar un trabajo anterior sólido definitivamente atraerá a más clientes o reclutadores potenciales. Así que solo agregue su mejor trabajo en esta sección.

Usaremos tarjetas Bootstrap para mostrar los proyectos de la cartera. Habrá 2 filas y cada fila tendrá 3 columnas de cartas.

Este será el código para la sección de cartera:

<!-- portfolio section-->
    <section id="portfolio">
        <div class="container mt-3">
            <h1 class="text-center">Portfolio</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">YouTube Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Quiz App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage3.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Product Landing Page</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <br>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Messaging Service</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Twitter Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Blog App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </section>

Cada tarjeta tiene una imagen, título, descripción y enlace a los proyectos. Se muestran tres tarjetas en una fila para pantallas grandes que tienen puntos de interrupción de ≥ 992 px de ancho, pero para pantallas < 992 px de ancho solo se muestra una tarjeta en una fila.

El siguiente GIF muestra cómo se ve ahora la sección de cartera:

portafolio

portafolio

Cómo construir la sección de contacto

Debe incluir su información de contacto en esta sección para que los visitantes puedan contactarlo si desean contratarlo.

Nuestra sección de contacto incluirá 2 columnas en una sola fila: mapas de Google para la ubicación y un formulario de contacto.

Para incrustar el mapa de Google, debe seguir estos pasos:

  • vaya a https://www.embed-map.com
  • Ingresa tu ubicación
  • haga clic en el botón Generar código HTML que le proporcionará su código HTML de mapa de Google

Nuestro código se verá así con el formulario de contacto incluido:

<!-- contact section-->
    <section id="contact">
        <div class="container mt-3 contactContent">
            <h1 class="text-center">Contact Me</h1>

            <div class="row mt-4">
                <div class="col-lg-6">
                    <!-- to edit google map goto https://www.embed-map.com type your location, generate html code and copy the html  -->
                    <div style="max-width:100%;overflow:hidden;color:red;width:500px;height:500px;">
                        <div id="embedmap-canvas" style="height:100%; width:100%;max-width:100%;">
                            <iframe style="height:100%;width:100%;border:0;" frameborder="0" src="https://www.google.com/maps/embed/v1/place?q=new+york&key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8">
                            </iframe>
                        </div>
                        <a class="googlemaps-html" href="https://www.embed-map.com" id="get-data-forembedmap">https://www.embed-map.com</a>
                        <style>#embedmap-canvas img{max-width:none!important;background:none!important;font-size: inherit;font-weight:inherit;}
                        </style>
                    </div>
                </div>

                <div class="col-lg-6">
                    <!-- form fields -->
                    <form>
                        <input type="text" class="form-control form-control-lg" placeholder="Name">
                        <input type="email" class="form-control mt-3" placeholder="Email">
                        <input type="text" class="form-control mt-3" placeholder="Subject">
                        <div class="mb-3 mt-3">
                            <textarea class="form-control" rows="5" id="comment" name="text" placeholder="Project Details"></textarea>
                        </div>
                    </form>
                    <button type="button" class="btn btn-success mt-3">Contact Me</button>
                    
                </div>

            </div>
        </div>
    </section>

La primera columna mostrará el mapa de Google y la siguiente mostrará el formulario de contacto.

El formulario tiene cuatro campos de formulario diferentes: nombre, correo electrónico, asunto y detalles del proyecto. El formulario no envía la solicitud en sí. Deberá conectarlo con cualquier idioma de back-end. O simplemente puede usar el formulario Netlify o el formulario Formspree para esto.

Así aparecerá la sección de contacto:

Captura de pantalla de 2022-01-25-11-31-56

Sección de contacto

Cómo construir la sección de pie de página

Ahora hemos llegado a la última sección de esta publicación, que es la sección de pie de página. Ya hemos agregado un enlace a la fuente Awesome CDN en el archivo index.html.

En el pie de página, agregaremos enlaces a nuestras redes sociales a través de íconos impresionantes de fuentes.

 <!-- footer section-->
    <footer id="footer">
        <div class="container-fluid">
            <!-- social media icons -->
            <div class="social-icons mt-4">
                <a href="https://www.facebook.com/" target="_blank"><i class="fab fa-facebook"></i></a>
                <a href="https://www.instagram.com/" target="_blank"><i class="fab fa-instagram"></i></a>
                <a href="https://www.twitter.com/" target="_blank"><i class="fab fa-twitter"></i></a>
                <a href="https://www.linkedin.com/" target="_blank"><i class="fab fa-linkedin"></i></a>
                <a href="https://www.twitch.tv/" target="_blank"><i class="fab fa-twitch"></i></a>
            </div>
        </div>
    </footer>

Sin el CSS, nuestro pie de página se verá así:

Captura de pantalla de 2022-01-23-17-56-37

pie de página sin estilo

Así que agreguemos algo de estilo al pie de página con este código:

/* social media icons styling */
.social-icons {
    font-size: 36px;
    cursor: pointer;
}
.fa-facebook:hover,.fa-instagram:hover,.fa-twitter:hover,.fa-linkedin:hover, .fa-twitch:hover {
    color: #008000;
}
.fab {
    color: #000000;
}
/* footer styling */
#footer {
    background-color: #808080;
    text-align: center;
}

Los íconos ahora se muestran en el centro con un efecto de desplazamiento que podemos ver en el siguiente archivo GIF.

pie de página

Pie de página

Toques finales

Para agregar algo de espacio entre todas las secciones, agreguemos un poco más de estilo:

/* spacing on all sections */
#about, #services, #portfolio, #contact {
    margin-top: 4rem;
    padding-top: 4rem;
}
#contact {
    padding-bottom: 4rem;
}

Ahora hemos terminado de hacer nuestro sitio web de cartera completo.

Puede encontrar el código fuente completo de este proyecto aquí .

Conclusión

Así es como puede crear un sitio web de cartera receptivo completo utilizando HTML, CSS, JavaScript y Bootstrap 5.

En esta publicación de blog, vimos algunos de los beneficios de crear un sitio web de cartera para desarrolladores y diseñadores web. Dividimos todo el sitio web en diferentes secciones y discutimos cada una individualmente a medida que la construíamos.

Puede personalizar este sitio web en función de sus propios casos de uso.

Espero que hayas encontrado útil esta publicación.

¡Feliz codificación!  

Enlace: https://www.freecodecamp.org/news/how-to-create-a-portfolio-website-using-html-css-javascript-and-bootstrap/

#html  #css  #javascript 

Cómo Crear Un Sitio Web De Cartera Usando HTML, CSS, JavaScript
宇野  和也

宇野 和也

1643369460

HTML、CSS、JavaScript、Bootstrap5を使用してポートフォリオWebサイトを作成する

あなたがウェブ開発者またはウェブデザイナーであるならば、あなたがポートフォリオウェブサイトを持っていることはあなたにとって不可欠です。それはあなたがあなた自身についての情報を提供し、あなたの関連するスキルと経験であなたの最高の仕事を紹介することを可能にします。

このブログ投稿では、ポートフォリオWebサイトを作成する利点のいくつかについて説明します。次に、HTML、CSS、JavaScript、およびBootstrapバージョン5を使用して、美しいレスポンシブポートフォリオWebサイトを自分で作成する方法を紹介します。

ポートフォリオウェブサイトを持つことの利点

ポートフォリオWebサイトを持つことには、次のようないくつかの利点があります。

  • 関連するスキルと経験を紹介するためのプラットフォームを提供します
  • それはあなたの性格を示しています
  • 採用担当マネージャーは、あなたが彼らに連絡する代わりにあなたを見つけることができます
  • あなたはグーグルのような検索エンジンで簡単に検索できます

ブートストラップとは何ですか?

Bootstrapは、レスポンシブでモバイルフレンドリーなWebサイトを開発するために使用される人気のあるフロントエンドCSSフレームワークです。Bootstrapの最新リリースはバージョン5です。Bootstrap5の公式ドキュメントはここにあります

フォルダ構造

ポートフォリオWebサイトの作成に取り掛かります。

まず、フォルダ構造を作成しましょう。プロジェクトスターターファイルはGitHubで入手できます。また、ここにアクセスして、このプロジェクトのライブデモを見ることができます。

スクリーンショット-2022-01-22-19-10-25から

プロジェクトフォルダの構造

フォルダ構造は、index.html、style.css、script.jsファイルとimagesフォルダで構成されています。すべてのCSSをstyle.cssファイルに記述し、JavaScriptをscript.jsファイルに記述します。

index.htmlファイルには、Bootstrap CDN、font awesomeキット、および外部スタイルシートとJavaScriptへのリンクを含むHTMLボイラープレートコードが表示されます。

ここでは、すべてのHTMLコードをロードした後にscript.jsファイルがロードされます。

ポートフォリオにナビゲーションメニューを追加する方法

それでは、プロジェクトにナビゲーションメニューを追加してみましょう。それは訪問者が彼らが探している関連情報を見つけるのを助けるでしょう。

nav要素でBootstrapのfixed-topクラスを使用して、navbarをページの上部に保持します。ナビゲーションバーにはnavbar-brand、その人の名前をブランドとして保持するクラスもあります。

<nav class="navbar navbar-expand-lg fixed-top navbarScroll">
        <div class="container">
            <a class="navbar-brand" href="#">Brad</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#home">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#about">About</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#services">Services</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#portfolio">Portfolio</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#contact">Contact</a>
                    </li>
                </ul>
                
            </div>
        </div>
    </nav>

ナビゲーションバーには次の機能があります。

  • ホーム、アバウト、サービス、ポートフォリオ、連絡先、フッターの6つのリンクがあります
  • 背景は透明です。後でページスクロールに暗い背景を追加します。
  • 小さいデバイスで切り替わります

Bootstrap 5ナビゲーションバー機能の詳細については、こちらをご覧ください。

ただし、スクロール中にナビゲーションバーに問題があります。ページ全体が完全に透過的であるため、読みやすさの問題が発生します。問題を正しく理解できるように、サービスセクションを完了した後、この問題を修正します。

ポートフォリオにヒーローヘッダーを追加する方法

次に、中央にテキストを含むヒーロー画像を追加します。ヒーロー画像は、会社または個人の主な目標、代表的な画像、写真、またはその他の人目を引く要素を表示する高品質の全幅画像を指すWebデザイン用語です。それはあなたのサイトにユーザーを引き付けるのに役立ちます。

 <!-- main banner -->
    <section class="bgimage" id="home">
        <div class="container-fluid">
            <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 hero-text">
                <h2 class="hero_title">Hi, it's me Brad</h2>
                <p class="hero_desc">I am a professional freelancer in New York City</p>
            </div>
            </div>
        </div>
    </section>

 また、上記のコードのCSSをstyle.cssファイルに追加しましょう。

/* hero background image */
.bgimage {
    height:100vh;
    background: url('images/heroImage.jpeg');
    background-size:cover;
    position:relative;
}
/* text css above hero image*/
.hero_title {
    font-size: 4.5rem;
}
.hero_desc {
    font-size: 2rem;
}
.hero-text {
    text-align: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}

ここでは、セクションにbgimage、背景のヒーロー画像を全幅で表示するための名前のIDがあることがわかります。また、上記のCSSを使用して、背景画像の上の中央にテキストを表示します。

これは、ナビゲーションバーとヒーローセクションでこれまでのところサイトがどのように見えるかです:

スクリーンショット-2022-01-25-10-13-25から

ナビゲーションバー付きのヒーロー画像

アバウトセクションの作り方

Aboutページには、あなたとあなたの経歴に関する重要な情報が含まれています。あなたのポートフォリオサイトへの訪問者は、あなたがこのページで提供する情報を通してあなたを知ることができます。

行の左側に画像を追加し、右側にこのセクションで簡単な紹介を追加します。以下のコードを使用してそれをデモンストレーションしましょう:

<!-- about section-->
    <section id="about">
        <div class="container mt-4 pt-4">
            <h1 class="text-center">About Me</h1>
            <div class="row mt-4">
                <div class="col-lg-4">
                    <img src="images/about.jpeg" class= "imageAboutPage" alt="">
                </div>

                <div class="col-lg-8">
                    <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged
                        
                    </p>
                    <div class="row mt-3">
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                    </div>
                    <div class="row mt-3">
                        <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                        </p>
                    </div>
                </div>
            </div>
    </section>

左側の画像にCSSを追加しましょう。

/* about section image css */
.imageAboutPage {
    width: 100%;
}

これにより、aboutセクションが作成されます。ユースケースに基づいてコンテンツを変更できます。マージントップとパディングトップを1.5レムに設定するコンテナクラスという名前mt-4のクラスを追加しました。pt-4

行には2つの列があります。1つは、col-lg-4大画面用の4つの部分からなるグリッドで左の列を占める画像を表示するためのクラスです。

次の列にはクラスが割り当てられ、そのクラスはcol-lg-8、より大きな画面用に8つの部分からなるグリッドで右側の列を占めます。中画面と小画面の場合、それらは互いに重なり合います。これは、以下のGIFファイルで確認できます。

約

セクションについて

サービスセクションの作り方

このセクションは、Webサイトの訪問者を潜在的なクライアントに変えるのに役立ちます。ここでは、提供する特定のサービスを説明し、提供するサービスをニッチに絞り込みます。

このセクションのコードを追加して、以下に説明しましょう。

<!-- services section-->
    <section id="services">
        <div class="container">
            <h1 class="text-center">Services</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class="fas servicesIcon fa-clock"></i>
                            <h4 class="card-title mt-3">Website Development</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-layer-group'></i>
                            <h4 class="card-title mt-3">Website Design</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='far servicesIcon fa-check-circle'></i>
                            <h4 class="card-title mt-3">Website Deployment</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>

            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-search'></i>
                            <h4 class="card-title mt-3">SEO</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-shield-alt'></i>
                            <h4 class="card-title mt-3">DevOps</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-wrench'></i>
                            <h4 class="card-title mt-3">QA</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>
        </div>
    </section>

このWebサイトは、Web開発者およびデザイナーを対象としているため、Web開発者またはデザイナーが提供する可能性のあるサービスの一部を含めました。

サービスを表示するためにブートストラップカードを使用しました。サービスセクションには、それぞれ2行3列あります。幅が992px以上の大画面の場合、3枚のカードが連続して表示されます。幅が992px未満の画面の場合、1枚のカードのみが連続して表示されます。

ブートストラップブレークポイントの詳細については、こちらをご覧ください。

また、見栄えを良くするために、各カードにフォントが追加されています。

CSSがない場合、サービスセクションは次のようになります。

スクリーンショット-2022-01-23-14-01-00から

そこで、CSSを追加して、フォントアイコンのフォントサイズとカードの高さを増やし、ユーザーがカードにカーソルを合わせたときに色を追加しましょう。

/* services section css */
.servicesText.card {
    height: 280px;
    cursor: pointer;
  }
.servicesIcon {
    font-size: 36px;
    text-align: center;
    width: 100%;
}
.card-title {
    text-align: center;
}
.card:hover .servicesIcon {
    color: #008000;
}
.servicesText:hover {
    border: 1px solid #008000;
}

これが私たちのサービスセクションが今どのように見えるかです:

サービス

サービス 

ページスクロールのナビゲーションバーに暗い背景色を追加する方法

上記のgifを適切に調べると、ナビゲーションバーがページ全体で透明であることがわかり、読みやすさの問題が発生します。それでは、この問題の修正に取り組みましょう。

この問題を解決するために、JavaScriptとCSSを作成します。navbarDarkページスクロールでナビゲーションバーの背景色を暗くするために、クラスを追加します。

そのためには、script.jsファイルに移動して、次のコードを追加する必要があります。

// add class navbarDark on navbar scroll
const header = document.querySelector('.navbar');

window.onscroll = function() {
    var top = window.scrollY;
    if(top >=100) {
        header.classList.add('navbarDark');
    }
    else {
        header.classList.remove('navbarDark');
    }
}

それでは、上記のコードを分解してみましょう。

  • querySelectorメソッドはCSSセレクター(.navbarこの場合)に一致する最初の要素を返すため、ヘッダーはnav要素の値を保持します。
  • window.onscroll スクロールイベントが発生すると起動します。
  • window.scrollYドキュメントが垂直方向にスクロールされ、その値が。という名前の変数に割り当てられているピクセル数を返しますtop
  • の値がtop100以上の場合navbarDark、ヘッダーにのクラスが追加されます。

navbarDarkクラスのCSSを簡単に追加しましょう。そのためには、style.cssファイルに移動し、次のコードを追加します。

/* display background color black on navbar scroll */
.navbarScroll.navbarDark {
    background-color: black;
}

これがナビゲーションバーの外観です。

ナビゲーションバー

ページスクロールのナビゲーションバーの暗い背景色

ポートフォリオセクションの構築方法

このセクションには、あなたの最高の作品が含まれています。人々はあなたが何ができるかを見ることができ、強力な過去の仕事を紹介することは間違いなくより多くの潜在的なクライアントやリクルーターを引き付けるでしょう。したがって、このセクションには最高の作品のみを追加してください。

Bootstrapカードを使用してポートフォリオプロジェクトを表示します。2行あり、各行には3列のカードがあります。

これはポートフォリオセクションのコードになります。

<!-- portfolio section-->
    <section id="portfolio">
        <div class="container mt-3">
            <h1 class="text-center">Portfolio</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">YouTube Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Quiz App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage3.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Product Landing Page</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <br>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Messaging Service</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Twitter Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Blog App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </section>

各カードには、画像、タイトル、説明、およびプロジェクトへのリンクがあります。ブレークポイントの幅が992px以上の大画面の場合、3枚のカードが連続して表示されますが、幅が992px未満の画面の場合、1枚のカードのみが連続して表示されます。

以下のGIFは、ポートフォリオセクションが現在どのように見えるかを示しています。

ポートフォリオ

ポートフォリオ

連絡先セクションの作成方法

訪問者があなたを雇いたい場合にあなたに連絡できるように、このセクションにあなたの連絡先情報を含める必要があります。

お問い合わせセクションには、1行に2つの列が含まれます。場所のGoogleマップとお問い合わせフォームです。

Googleマップを埋め込むには、次の手順に従う必要があります。

  • https://www.embed-map.comにアクセスします
  • あなたの場所を入力してください
  • GoogleマップのHTMLコードを提供する[ HTMLコードの生成]ボタンをクリックします

コードは次のようになり、お問い合わせフォームが含まれています。

<!-- contact section-->
    <section id="contact">
        <div class="container mt-3 contactContent">
            <h1 class="text-center">Contact Me</h1>

            <div class="row mt-4">
                <div class="col-lg-6">
                    <!-- to edit google map goto https://www.embed-map.com type your location, generate html code and copy the html  -->
                    <div style="max-width:100%;overflow:hidden;color:red;width:500px;height:500px;">
                        <div id="embedmap-canvas" style="height:100%; width:100%;max-width:100%;">
                            <iframe style="height:100%;width:100%;border:0;" frameborder="0" src="https://www.google.com/maps/embed/v1/place?q=new+york&key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8">
                            </iframe>
                        </div>
                        <a class="googlemaps-html" href="https://www.embed-map.com" id="get-data-forembedmap">https://www.embed-map.com</a>
                        <style>#embedmap-canvas img{max-width:none!important;background:none!important;font-size: inherit;font-weight:inherit;}
                        </style>
                    </div>
                </div>

                <div class="col-lg-6">
                    <!-- form fields -->
                    <form>
                        <input type="text" class="form-control form-control-lg" placeholder="Name">
                        <input type="email" class="form-control mt-3" placeholder="Email">
                        <input type="text" class="form-control mt-3" placeholder="Subject">
                        <div class="mb-3 mt-3">
                            <textarea class="form-control" rows="5" id="comment" name="text" placeholder="Project Details"></textarea>
                        </div>
                    </form>
                    <button type="button" class="btn btn-success mt-3">Contact Me</button>
                    
                </div>

            </div>
        </div>
    </section>

最初の列にはGoogleマップが表示され、次の列にはお問い合わせフォームが表示されます。

フォームには、名前、電子メール、件名、プロジェクトの詳細の4つの異なるフォームフィールドがあります。フォームはリクエスト自体を送信しません。バックエンド言語に接続する必要があります。または、 NetlifyフォームまたはFormspreeフォームを使用することもできます

連絡先セクションは次のように表示されます。

スクリーンショット-2022-01-25-11-31-56から

連絡先セクション

フッターセクションの作成方法

これで、この投稿の最後のセクションであるフッターセクションに到達しました。index.htmlファイルにfontawesomeCDNへのリンクを すでに追加しています。

フッターでは、フォントの素晴らしいアイコンを介してソーシャルメディアへのリンクを追加します。

 <!-- footer section-->
    <footer id="footer">
        <div class="container-fluid">
            <!-- social media icons -->
            <div class="social-icons mt-4">
                <a href="https://www.facebook.com/" target="_blank"><i class="fab fa-facebook"></i></a>
                <a href="https://www.instagram.com/" target="_blank"><i class="fab fa-instagram"></i></a>
                <a href="https://www.twitter.com/" target="_blank"><i class="fab fa-twitter"></i></a>
                <a href="https://www.linkedin.com/" target="_blank"><i class="fab fa-linkedin"></i></a>
                <a href="https://www.twitch.tv/" target="_blank"><i class="fab fa-twitch"></i></a>
            </div>
        </div>
    </footer>

CSSがない場合、フッターは次のようになります。

スクリーンショット-2022-01-23-17-56-37から

スタイリングなしのフッター??

それでは、このコードを使用してフッターにスタイルを追加しましょう。

/* social media icons styling */
.social-icons {
    font-size: 36px;
    cursor: pointer;
}
.fa-facebook:hover,.fa-instagram:hover,.fa-twitter:hover,.fa-linkedin:hover, .fa-twitch:hover {
    color: #008000;
}
.fab {
    color: #000000;
}
/* footer styling */
#footer {
    background-color: #808080;
    text-align: center;
}

アイコンが中央に表示され、ホバー効果が表示されます。これは、以下のGIFファイルで確認できます。

フッター

フッター

最後の仕上げ

すべてのセクションの間に間隔を追加するために、さらにスタイルを追加しましょう。

/* spacing on all sections */
#about, #services, #portfolio, #contact {
    margin-top: 4rem;
    padding-top: 4rem;
}
#contact {
    padding-bottom: 4rem;
}

これで、完全なポートフォリオWebサイトの作成が完了しました。

このプロジェクトの完全なソースコードはここにあります

結論

これは、HTML、CSS、JavaScript、およびBootstrap5を使用して完全なレスポンシブポートフォリオWebサイトを作成する方法です。

このブログ投稿では、Web開発者とデザイナーのためのポートフォリオWebサイトを作成することの利点のいくつかを見ました。Webサイト全体をさまざまなセクションに分割し、構築時にそれぞれについて個別に説明しました。

このWebサイトは、独自のユースケースに基づいてカスタマイズできます。

この投稿がお役に立てば幸いです。

ハッピーコーディング!  

リンク:https ://www.freecodecamp.org/news/how-to-create-a-portfolio-website-using-html-css-javascript-and-bootstrap/

#html  #css  #javascript 

HTML、CSS、JavaScript、Bootstrap5を使用してポートフォリオWebサイトを作成する
Kathlyn  Robel

Kathlyn Robel

1638913800

Easily Build A Contact form For Your Project

Learn to easily build a contact form for your project....

#code #contact 

Easily Build A Contact form For Your Project
Dylan  Iqbal

Dylan Iqbal

1638243664

ElectroDB: A DynamoDB Library to Make Single Table Designs Easier

ElectroDB

 ElectroDB is a DynamoDB library to ease the use of having multiple entities and complex hierarchical relationships in a single DynamoDB table.


 

Introducing: The NEW ElectroDB Playground

 

Try out and share ElectroDB Models, Services, and Single Table Design at electrodb.fun


Features


Turn this

tasks
  .patch({ 
    team: "core",
    task: "45-662", 
    project: "backend"
  })
  .set({ status: "open" })
  .add({ points: 5 })
  .append({ 
    comments: [{
      user: "janet",
      body: "This seems half-baked."
    }] 
  })
  .where(( {status}, {eq} ) => eq(status, "in-progress"))
  .go();

Into This

{
    "UpdateExpression": "SET #status = :status_u0, #points = #points + :points_u0, #comments = list_append(#comments, :comments_u0), #updatedAt = :updatedAt_u0, #gsi1sk = :gsi1sk_u0",
    "ExpressionAttributeNames": {
        "#status": "status",
        "#points": "points",
        "#comments": "comments",
        "#updatedAt": "updatedAt",
        "#gsi1sk": "gsi1sk"
    },
    "ExpressionAttributeValues": {
        ":status0": "in-progress",
        ":status_u0": "open",
        ":points_u0": 5,
        ":comments_u0": [
            {
                "user": "janet",
                "body": "This seems half-baked."
            }
        ],
        ":updatedAt_u0": 1630977029015,
        ":gsi1sk_u0": "$assignments#tasks_1#status_open"
    },
    "TableName": "your_table_name",
    "Key": {
        "pk": "$taskapp#team_core",
        "sk": "$tasks_1#project_backend#task_45-662"
    },
    "ConditionExpression": "attribute_exists(pk) AND attribute_exists(sk) AND #status = :status0"
}

Table of Contents


Project Goals

ElectroDB focuses on simplifying the process of modeling, enforcing data constraints, querying across entities, and formatting complex DocumentClient parameters. Three important design considerations we're made with the development of ElectroDB:

  1. ElectroDB should be able to be useful without having to query the database itself [read more].
  2. ElectroDB should be able to be added to a project that already has been established tables, data, and access patterns [read more].
  3. ElectroDB should not require additional design considerations on top of those made for DynamoDB, and therefore should be able to be removed from a project at any time without sacrifice.

Installation

Install from NPM

npm install electrodb --save

Usage

Require/import Entity and/or Service from electrodb:

const {Entity, Service} = require("electrodb");
// or 
import {Entity, Service} from "electrodb";

Entities and Services

To see full examples of ElectroDB in action, go to the Examples section.

Entity allows you to create separate and individual business objects in a DynamoDB table. When queried, your results will not include other Entities that also exist the same table. This allows you to easily achieve single table design as recommended by AWS. For more detail, read Entities.

Service allows you to build relationships across Entities. A service imports Entity Models, builds individual Entities, and creates Collections to allow cross Entity querying. For more detail, read Services.

You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, If you intend to make queries that join or span multiple Entities you will need to use a Service.

Entities

In ElectroDB an Entity is represents a single business object. For example, in a simple task tracking application, one Entity could represent an Employee and or a Task that is assigned to an employee.

Require or import Entity from electrodb:

const {Entity} = require("electrodb");
// or
import {Entity} from "electrodb";

When using TypeScript, for strong type checking, be sure to either add your model as an object literal to the Entity constructor or create your model using const assertions with the as const syntax.

Services

In ElectroDB a Service represents a collection of related Entities. Services allow you to build queries span across Entities. Similar to Entities, Services can coexist on a single table without collision. You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, you do you need to use a Service if you intend make queries that join multiple Entities.

Require:

const {Service} = require("electrodb");
// or
import {Service} from "electrodb";

TypeScript Support

Previously it was possible to generate type definition files (.d.ts) for you Models, Entities, and Services with the Electro CLI. New with version 0.10.0 is TypeScript support for Entities and Services.

As of writing this, this functionality is still a work in progress, and enforcement of some of ElectroDB's query constraints have still not been written into the type checks. Most notably are the following constraints not yet enforced by the type checker, but are enforced at query runtime:

  • Sort Key Composite Attribute order is not strongly typed. Sort Key Composite Attributes must be provided in the order they are defined on the model to build the key appropriately. This will not cause an error at query runtime, be sure your partial Sort Keys are provided in accordance with your model to fully leverage Sort Key queries. For more information about composite attribute ordering see the section on Composite Attributes.
  • Put/Create/Update/Patch/Delete/Create operations that partially impact index composite attributes are not statically typed. When performing a put or update type operation that impacts a composite attribute of a secondary index, ElectroDB performs a check at runtime to ensure all composite attributes of that key are included. This is detailed more in the section Composite Attribute and Index Considerations.
  • Use of the params method does not yet return strict types.
  • Use of the raw or includeKeys query options do not yet impact the returned types.

If you experience any issues using TypeScript with ElectroDB, your feedback is very important, please create a GitHub issue, and it can be addressed.

See the section Exported TypeScript Types to read more about the useful types exported from ElectroDB.

TypeScript Services

New with version 0.10.0 is TypeScript support. To ensure accurate types with, TypeScript users should create their services by passing an Object literal or const object that maps Entity alias names to Entity instances.

const table = "my_table_name";
const employees = new Entity(EmployeesModel, { client, table });
const tasks = new Entity(TasksModel, { client, table });
const TaskApp = new Service({employees, tasks});

The property name you assign the entity will then be "alias", or name, you can reference that entity by through the Service. Aliases can be useful if you are building a service with multiple versions of the same entity or wish to change the reference name of an entity without impacting the schema/key names of that entity.

Services take an optional second parameter, similar to Entities, with a client and table. Using this constructor interface, the Service will utilize the values from those entities, if they were provided, or be passed values to override the client or table name on the individual entities.

Not yet available for TypeScript, this pattern will also accept Models, or a mix of Entities and Models, in the same object literal format.

Join

When using JavaScript, use join to add Entities or Models onto a Service.

NOTE: If using TypeScript, see Joining Entities at Service construction for TypeScript to learn how to "join" entities for use in a TypeScript project.

Independent Models

let table = "my_table_name";
let employees = new Entity(EmployeesModel, { client, table });
let tasks = new Entity(TasksModel, { client, table });

Joining Entity instances to a Service

// Joining Entity instances to a Service
let TaskApp = new Service("TaskApp", { client, table });
TaskApp
    .join(employees) // available at TaskApp.entities.employees
    .join(tasks);    // available at TaskApp.entities.tasks

Joining models to a Service

let TaskApp = new Service("TaskApp", { client, table });
TaskApp
    .join(EmployeesModel) // available at TaskApp.entities.employees (based on entity name in model)
    .join(TasksModel);    // available at TaskApp.entities.tasks (based on entity name in model)

Joining Entities or Models with an alias

let TaskApp = new Service("TaskApp", { client, table });
TaskApp
    .join("personnel", EmployeesModel) // available at TaskApp.entities.personnel
    .join("directives", TasksModel); // available at TaskApp.entities.directives

Joining Entities at Service construction for TypeScript

let TaskApp = new Service({
    personnel: EmployeesModel, // available at TaskApp.entities.personnel
    directives: TasksModel, // available at TaskApp.entities.directives
});

When joining a Model/Entity to a Service, ElectroDB will perform a number of validations to ensure that Entity conforms to expectations collectively established by all joined Entities.

  • Entity names must be unique across a Service.
  • Collection names must be unique across a Service.
  • All Collections map to on the same DynamoDB indexes with the same index field names. See Indexes.
  • Partition Key [Composite Attributes](#composite attribute-arrays) on a Collection must have the same attribute names and labels (if applicable). See Attribute Definitions.
  • The name of the Service in the Model must match the Name defined on the Service instance.
  • Joined instances must be type Model or Entity.
  • If the attributes of an Entity have overlapping names with other attributes in that service, they must all have compatible or matching attribute definitions.
  • All models conform to the same model format. If you created your model prior to ElectroDB version 0.9.19 see section Version 1 Migration.

Model

Create an Entity's schema. In the below example.

const DynamoDB = require("aws-sdk/clients/dynamodb");
const {Entity, Service} = require("electrodb");
const client = new DynamoDB.DocumentClient();
const EmployeesModel = {
    model: {
        entity: "employees",
        version: "1",
        service: "taskapp",
    },
    attributes: {
        employee: {
            type: "string",
            default: () => uuidv4(),
        },
        firstName: {
            type: "string",
            required: true,
        },
        lastName: {
            type: "string",
            required: true,
        },
        office: {
            type: "string",
            required: true,
        },
        title: {
            type: "string",
            required: true,
        },
        team: {
            type: ["development", "marketing", "finance", "product", "cool cats and kittens"],
            required: true,
        },
        salary: {
            type: "string",
            required: true,
        },
        manager: {
            type: "string",
        },
        dateHired: {
            type: "string",
            validate: /^\d{4}-\d{2}-\d{2}$/gi
        },
        birthday: {
            type: "string",
            validate: /^\d{4}-\d{2}-\d{2}$/gi
        },
    },
    indexes: {
        employee: {
            pk: {
                field: "pk",
                composite: ["employee"],
            },
            sk: {
                field: "sk",
                composite: [],
            },
        },
        coworkers: {
            index: "gsi1pk-gsi1sk-index",
            collection: "workplaces",
            pk: {
                field: "gsi1pk",
                composite: ["office"],
            },
            sk: {
                field: "gsi1sk",
                composite: ["team", "title", "employee"],
            },
        },
        teams: {
            index: "gsi2pk-gsi2sk-index",
            pk: {
                field: "gsi2pk",
                composite: ["team"],
            },
            sk: {
                field: "gsi2sk",
                composite: ["title", "salary", "employee"],
            },
        },
        employeeLookup: {
            collection: "assignments",
            index: "gsi3pk-gsi3sk-index",
            pk: {
                field: "gsi3pk",
                composite: ["employee"],
            },
            sk: {
                field: "gsi3sk",
                composite: [],
            },
        },
        roles: {
            index: "gsi4pk-gsi4sk-index",
            pk: {
                field: "gsi4pk",
                composite: ["title"],
            },
            sk: {
                field: "gsi4sk",
                composite: ["salary", "employee"],
            },
        },
        directReports: {
            index: "gsi5pk-gsi5sk-index",
            pk: {
                field: "gsi5pk",
                composite: ["manager"],
            },
            sk: {
                field: "gsi5sk",
                composite: ["team", "office", "employee"],
            },
        },
    },
};

const TasksModel = {
    model: {
        entity: "tasks",
        version: "1",
        service: "taskapp",
    },
    attributes: {
        task: {
            type: "string",
            default: () => uuid(),
        },
        project: {
            type: "string",
        },
        employee: {
            type: "string",
        },
        description: {
            type: "string",
        },
    },
    indexes: {
        task: {
            pk: {
                field: "pk",
                composite: ["task"],
            },
            sk: {
                field: "sk",
                composite: ["project", "employee"],
            },
        },
        project: {
            index: "gsi1pk-gsi1sk-index",
            pk: {
                field: "gsi1pk",
                composite: ["project"],
            },
            sk: {
                field: "gsi1sk",
                composite: ["employee", "task"],
            },
        },
        assigned: {
            collection: "assignments",
            index: "gsi3pk-gsi3sk-index",
            pk: {
                field: "gsi3pk",
                composite: ["employee"],
            },
            sk: {
                field: "gsi3sk",
                composite: ["project", "task"],
            },
        },
    },
};

Model Properties

PropertyDescription
model.serviceName of the application using the entity, used to namespace all entities
model.entityName of the entity that the schema represents
model.version(optional) The version number of the schema, used to namespace keys
attributesAn object containing each attribute that makes up the schema
indexesAn object containing table indexes, including the values for the table's default Partition Key and Sort Key

Service Options

Optional second parameter

PropertyDescription
tableThe name of the dynamodb table in aws.
client(optional) An instance of the docClient from the aws-sdk for use when querying a DynamoDB table. This is optional if you wish to only use the params functionality, but required if you actually need to query against a database.

Attributes

Attributes define an Entity record. The AttributeName represents the value your code will use to represent an attribute.

Pro-Tip: Using the field property, you can map an AttributeName to a different field name in your table. This can be useful to utilize existing tables, existing models, or even to reduce record sizes via shorter field names. For example, you may refer to an attribute as organization but want to save the attribute with a field name of org in DynamoDB.

Simple Syntax

Assign just the type of the attribute directly to the attribute name. Types currently supported options are "string", "number", "boolean", an array of strings representing a fixed set of possible values, or "any" which disables value type checking on that attribute.

attributes: {
    <AttributeName>: "string" | "number" | "boolean" | "list" | "map" | "set" | "any" | string[] | ReadonlyArray<string> 
}

Expanded Syntax

Use the expanded syntax build out more robust attribute options.

attributes: {
    <AttributeName>: {
        type: "string" | "number" | "boolean" | "list" | "map" | "set" | "any" | ReadonlyArray<string>;
        required?: boolean;
        default?: <type> | (() => <type>);
        validate?: RegExp | ((value: <type>) => void | string);
        field?: string;
        readOnly?: boolean;
        label?: string;
        cast?: "number"|"string"|"boolean";
        get?: (attribute: <type>, schema: any) => <type> | void | undefined;
        set?: (attribute?: <type>, schema?: any) => <type> | void | undefined; 
        watch: "*" | string[]
    }
}

NOTE: When using get/set in TypeScript, be sure to use the ?: syntax to denote an optional attribute on set

Attribute Definition

PropertyTypeRequiredTypesDescription
typestring, ReadonlyArray<string>, string[]yesallAccepts the values: "string", "number" "boolean", "map", "list", "set", an array of strings representing a finite list of acceptable values: ["option1", "option2", "option3"], or "any" which disables value type checking on that attribute.
requiredbooleannoallFlag an attribute as required to be present when creating a record. This attribute also acts as a type of NOT NULL flag, preventing it from being removed directly.
hiddenbooleannoallFlag an attribute as hidden to remove the property from results before they are returned.
defaultvalue, () => valuenoallEither the default value itself or a synchronous function that returns the desired value. Applied before set and before required check.
validateRegExp, (value: any) => void, (value: any) => stringnoallEither regex or a synchronous callback to return an error string (will result in exception using the string as the error's message), or thrown exception in the event of an error.
fieldstringnoallThe name of the attribute as it exists in DynamoDB, if named differently in the schema attributes. Defaults to the AttributeName as defined in the schema.
readOnlybooleannoallPrevents an attribute from being updated after the record has been created. Attributes used in the composition of the table's primary Partition Key and Sort Key are read-only by default. The one exception to readOnly is for properties that also use the watch property, read attribute watching for more detail.
labelstringnoallUsed in index composition to prefix key composite attributes. By default, the AttributeName is used as the label.
cast"number", "string", "boolean"noallOptionally cast attribute values when interacting with DynamoDB. Current options include: "number", "string", and "boolean".
set(attribute, schema) => valuenoallA synchronous callback allowing you to apply changes to a value before it is set in params or applied to the database. First value represents the value passed to ElectroDB, second value are the attributes passed on that update/put
get(attribute, schema) => valuenoallA synchronous callback allowing you to apply changes to a value after it is retrieved from the database. First value represents the value passed to ElectroDB, second value are the attributes retrieved from the database.
watchAttribute[], "*"noroot-onlyDefine other attributes that will always trigger your attribute's getter and setter callback after their getter/setter callbacks are executed. Only available on root level attributes.
properties{[key: string]: Attribute}yes*mapDefine the properties available on a "map" attribute, required if your attribute is a map. Syntax for map properties is the same as root level attributes.
itemsAttributeyes*listDefine the attribute type your list attribute will contain, required if your attribute is a list. Syntax for list items is the same as a single attribute.
items"string""number"yes*set

Enum Attributes

When using TypeScript, if you wish to also enforce this type make sure to us the as const syntax. If TypeScript is not told this array is Readonly, even when your model is passed directly to the Entity constructor, it will not resolve the unique values within that array.

This may be desirable, however, as enforcing the type value can require consumers of your model to do more work to resolve the type beyond just the type string.

NOTE: Regardless of using TypeScript or JavaScript, ElectroDB will enforce values supplied match the supplied array of values at runtime.

The following example shows the differences in how TypeScript may enforce your enum value:

attributes: {
  myEnumAttribute1: {
      type: ["option1", "option2", "option3"]        // TypeScript enforces as `string[]`
  },
  myEnumAttribute2: {
    type: ["option1", "option2", "option3"] as const // TypeScript enforces as `"option1" | "option2" | "option3" | undefined`
  },
  myEnumAttribute3: {
    required: true,
    type: ["option1", "option2", "option3"] as const // TypeScript enforces as `"option1" | "option2" | "option3"`
  }
}

Map Attributes

Map attributes leverage DynamoDB's native support for object-like structures. The attributes within a Map are defined under the properties property; a syntax that mirrors the syntax used to define root level attributes. You are not limited in the types of attributes you can nest inside a map attribute.

attributes: {
  myMapAttribute: {
    type: "map",
    properties: {
      myStringAttribute: {
        type: "string"
      },
      myNumberAttribute: {
        type: "number"
      }
    }
  }
}

List Attributes

List attributes model array-like structures with DynamoDB's List type. The elements of a List attribute are defined using the items property. Similar to Map properties, ElectroDB does not restrict the types of items that can be used with a list.

attributes: {
  myStringList: { 
    type: "list",
    items: {
      type: "string"
    },
  },
  myMapList: {
    myMapAttribute: {
      type: "map",
      properties: {
        myStringAttribute: {
          type: "string"
        },
        myNumberAttribute: {
          type: "number"
        }
      }
    }
  }
}

Set Attributes

The Set attribute is arguably DynamoDB's most powerful type. ElectroDB supports String and Number Sets using the items property set as either "string" or "number".

In addition to having the same modeling benefits you get with other attributes, ElectroDB also simplifies the use of Sets by removing the need to use DynamoDB's special createSet class to work with Sets. ElectroDB Set Attributes accept Arrays, JavaScript native Sets, and objects from createSet as values. ElectroDB will manage the casting of values to a DynamoDB Set value prior to saving and ElectroDB will also convert Sets back to JavaScript arrays on retrieval.

NOTE: If you are using TypeScript, Sets are currently typed as Arrays to simplify the type system. Again, ElectroDB will handle the conversion of these Arrays without the need to use client.createSet().

attributes: {
  myStringSet: {
    type: "set",
    items: "string"
  },
  myNumberSet: {
    type: "set",
    items: "number"
  }
}

Attribute Getters and Setters

Using get and set on an attribute can allow you to apply logic before and just after modifying or retrieving a field from DynamoDB. Both callbacks should be pure synchronous functions and may be invoked multiple times during one query.

The first argument in an attribute's get or set callback is the value received in the query. The second argument, called "item", in an attribute's is an object containing the values of other attributes on the item as it was given or retrieved. If your attribute uses watch, the getter or setter of attribute being watched will be invoked before your getter or setter and the updated value will be on the "item" argument instead of the original.

NOTE: Using getters/setters on Composite Attributes is not recommended without considering the consequences of how that will impact your keys. When a Composite Attribute is supplied for a new record via a put or create operation, or is changed via a patch or updated operation, the Attribute's set callback will be invoked prior to formatting/building your record's keys on when creating or updating a record.

ElectroDB invokes an Attribute's get method in the following circumstances:

  1. If a field exists on an item after retrieval from DynamoDB, the attribute associated with that field will have its getter method invoked.
  2. After a put or create operation is performed, attribute getters are applied against the object originally received and returned.
  3. When using ElectroDB's attribute watching functionality, an attribute will have its getter callback invoked whenever the getter callback of any "watched" attributes are invoked. Note: The getter of an Attribute Watcher will always be applied after the getters for the attributes it watches.

ElectroDB invokes an Attribute's set callback in the following circumstances:

  1. Setters for all Attributes will always be invoked when performing a create or put operation.
  2. Setters will only be invoked when an Attribute is modified when performing a patch or update operation.
  3. When using ElectroDB's attribute watching functionality, an attribute will have its setter callback invoked whenever the setter callback of any "watched" attributes are invoked. Note: The setter of an Attribute Watcher will always be applied after the setters for the attributes it watches.

NOTE: As of ElectroDB 1.3.0, the watch property is only possible for root level attributes. Watch is currently not supported for nested attributes like properties on a "map" or items of a "list".

Attribute Watching

Attribute watching is a powerful feature in ElectroDB that can be used to solve many unique challenges with DynamoDB. In short, you can define a column to have its getter/setter callbacks called whenever another attribute's getter or setter callbacks are called. If you haven't read the section on Attribute Getters and Setters, it will provide you with more context about when an attribute's mutation callbacks are called.

Because DynamoDB allows for a flexible schema, and ElectroDB allows for optional attributes, it is possible for items belonging to an entity to not have all attributes when setting or getting records. Sometimes values or changes to other attributes will require corresponding changes to another attribute. Sometimes, to fully leverage some advanced model denormalization or query access patterns, it is necessary to duplicate some attribute values with similar or identical values. This functionality has many uses; below are just a few examples of how you can use watch:

NOTE: Using the watch property impacts the order of which getters and setters are called. You cannot watch another attribute that also uses watch, so ElectroDB first invokes the getters or setters of attributes without the watch property, then subsequently invokes the getters or setters of attributes who use watch.

myAttr: { 
  type: "string",
  watch: ["otherAttr"],
  set: (myAttr, {otherAttr}) => {
    // Whenever "myAttr" or "otherAttr" are updated from an `update` or `patch` operation, this callback will be fired. 
    // Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback
  },
  get: (myAttr, {otherAttr}) => {
    // Whenever "myAttr" or "otherAttr" are retrieved from a `query` or `get` operation, this callback will be fired. 
    // Note: myAttr or otherAttr could be independently undefined because either attribute could have triggered this callback.
  } 
}

Attribute Watching: Watch All

If your attributes needs to watch for any changes to an item, you can model this by supplying the watch property a string value of "*"

myAttr: { 
  type: "string",
  watch: "*", // "watch all"
  set: (myAttr, allAttributes) => {
    // Whenever an `update` or `patch` operation is performed, this callback will be fired. 
    // Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
  },
  get: (myAttr, allAttributes) => {
    // Whenever a `query` or `get` operation is performed, this callback will be fired. 
    // Note: myAttr or the attributes under `allAttributes` could be independently undefined because either attribute could have triggered this callback
  } 
}

Attribute Watching Examples

Example 1 - A calculated attribute that depends on the value of another attribute:

In this example, we have an attribute "fee" that needs to be updated any time an item's "price" attribute is updated. The attribute "fee" uses watch to have its setter callback called any time "price" is updated via a put, create, update, or patch operation.

Try it out!

{
  model: {
    entity: "products",
    service: "estimator",
    version: "1"
  },
  attributes: {
    product: {
      type: "string"
    },
    price: {
      type: "number",
              required: true
    },
    fee: {
      type: "number",
              watch: ["price"],
              set: (_, {price}) => {
        return price * .2;
      }
    }
  },
  indexes: {
    pricing: {
      pk: {
        field: "pk",
                composite: ["product"]
      },
      sk: {
        field: "sk",
                composite: []
      }
    }
  }
}

Example 2 - Making a virtual attribute that never persists to the database:

In this example we have an attribute "displayPrice" that needs its getter called anytime an item's "price" attribute is retrieved. The attribute "displayPrice" uses watch to return a formatted price string based whenever an item with a "price" attribute is queried. Additionally, "displayPrice" always returns undefined from its setter callback to ensure that it will never write data back to the table.

{
  model: {
    entity: "services",
    service: "costEstimator",
    version: "1"
  },
  attributes: {
    service: {
      type: "string"
    },
    price: {
      type: "number",
      required: true
    },
    displayPrice: {
      type: "string",
      watch: ["price"],
      get: (_, {price}) => {
        return "$" + price;  
      },
      set: () => undefined
    }
  },
  indexes: {
    pricing: {
      pk: {
        field: "pk",
        composite: ["service"]
      },
      sk: {
        field: "sk",
        composite: []
      }
    }
  }
}

Example 3 - Creating a more filter-friendly version of an attribute without impacting the original attribute:

In this example we have an attribute "descriptionSearch" which will help our users easily filter for transactions by "description". To ensure our filters will not take into account a description's character casing, descriptionSearch duplicates the value of "description" so it can be used in filters without impacting the original "description" value. Without ElectroDB's watch functionality, to accomplish this you would either have to duplicate this logic or cause permanent modification to the property itself. Additionally, the "descriptionSearch" attribute has used hidden:true to ensure this value will not be presented to the user.

{
  model: {
    entity: "transaction",
    service: "bank",
    version: "1"
  },
  attributes: {
    accountNumber: {
      type: "string"
    },
    transactionId: {
      type: "string"
    },
    amount: {
      type: "number",
    },
    description: {
      type: "string",
    },
    descriptionSearch: {
      type: "string",
      hidden: true,
      watch: ["description"],
      set: (_, {description}) => {
        if (typeof description === "string") {
            return description.toLowerCase();
        }
      }
    }
  },
  indexes: {
    transactions: {
      pk: {
        field: "pk",
        composite: ["accountNumber"]
      },
      sk: {
        field: "sk",
        composite: ["transactionId"]
      }
    }
  }
}

Example 4 - Creating an updatedAt property:

In this example we can easily create both updatedAt and createdAt attributes on our model. createdAt will use ElectroDB's set and readOnly attribute properties, while updatedAt will make use of readOnly, and watch with the "watchAll" syntax: {watch: "*"}. By supplying an asterisk, instead of an array of attribute names, attributes can be defined to watch all changes to all attributes.

Using watch in conjunction with readOnly is another powerful modeling technique. This combination allows you to model attributes that can only be modified via the model and not via the user. This is useful for attributes that need to be locked down and/or strictly calculated.

Notable about this example is that both updatedAt and createdAt use the set property without using its arguments. The readOnly only prevents modification of an attributes on update, and patch. By disregarding the arguments passed to set, the updatedAt and createdAt attributes are then effectively locked down from user influence/manipulation.

{
  model: {
    entity: "transaction",
    service: "bank",
    version: "1"
  },
  attributes: {
    accountNumber: {
      type: "string"
    },
    transactionId: {
      type: "string"
    },
    description: {
      type: "string",
    },
    createdAt: {
      type: "number",
      readOnly: true,
      set: () => Date.now()
    },
    updatedAt: {
      type: "number",
      readOnly: true,
      watch: "*",
      set: () => Date.now()
    },
    
  },
  indexes: {
    transactions: {
      pk: {
        field: "pk",
        facets: ["accountNumber"]
      },
      sk: {
        field: "sk",
        facets: ["transactionId"]
      }
    }
  }
}

Calculated Attributes

See: Attribute Watching (Example 1).

Virtual Attributes

See: Attribute Watching (Example 2).

CreatedAt and UpdatedAt Attributes

See: Attribute Watching (Example 4).

Attribute Validation

The validation property allows for multiple function/type signatures. Here the different combinations ElectroDB supports: signature | behavior ----------------------- | -------- Regexp | ElectroDB will call .test(val) on the provided regex with the value passed to this attribute (value: T) => string | If a string value with length is returned, the text will be considered the reason the value is invalid. It will generate a new exception this text as the message. (value: T) => boolean | If a boolean value is returned, true or truthy values will signify than a value is invalid while false or falsey will be considered valid. (value: T) => void | A void or undefined value is returned, will be treated as successful, in this scenario you can throw an Error yourself to interrupt the query

Indexes

When using ElectroDB, indexes are referenced by their AccessPatternName. This allows you to maintain generic index names on your DynamoDB table, but reference domain specific names while using your ElectroDB Entity. These will often be referenced as "Access Patterns".

All DynamoDB table start with at least a PartitionKey with an optional SortKey, this can be referred to as the "Table Index". The indexes object requires at least the definition of this Table Index Partition Key and (if applicable) Sort Key.

In your model, the Table Index this is expressed as an Access Pattern without an index property. For Secondary Indexes, use the index property to define the name of the index as defined on your DynamoDB table.

Within these AccessPatterns, you define the PartitionKey and (optionally) SortKeys that are present on your DynamoDB table and map the key's name on the table with the field property.

indexes: {
    [AccessPatternName]: {
        pk: {
            field: string; 
            composite: AttributeName[];
            template?: string;
        },
        sk?: {
            field: string;
            composite: AttributesName[];
            template?: string;
        },
        index?: string
        collection?: string | string[]
    }
}
PropertyTypeRequiredDescription
pkobjectyesConfiguration for the pk of that index or table
pk.composite`stringstring[]`yes
pk.templatestringnoA string that represents the template in which attributes composed to form a key (see Composite Attribute Templates below for more on this functionality).
pk.fieldstringyesThe name of the attribute as it exists in DynamoDB, if named differently in the schema attributes.
pk.casingdefaultupperlower
skobjectnoConfiguration for the sk of that index or table
sk.composite`stringstring[]`no
sk.templatestringnoA string that represents the template in which attributes composed to form a key (see Composite Attribute Templates below for more on this functionality).
sk.fieldstringyesThe name of the attribute as it exists in DynamoDB, if named differently in the schema attributes.
pk.casingdefaultupperlower
indexstringnoRequired when the Index defined is a Secondary Index; but is left blank for the table's primary index.
collection`stringstring[]`no

Indexes Without Sort Keys

When using indexes without Sort Keys, that should be expressed as an index without an sk property at all. Indexes without an sk cannot have a collection, see Collections for more detail.

NOTE: It is generally recommended to always use Sort Keys when using ElectroDB as they allow for more advanced query opportunities. Even if your model doesn't need an additional property to define a unique record, having an sk with no defined composite attributes (e.g. an empty array) still opens the door to many more query opportunities like collections.

// ElectroDB interprets as index *not having* an SK.
{
  indexes: {
    myIndex: {
      pk: {
        field: "pk",
        composite: ["id"]
      }
    }
  }
}

Try it out!

Indexes With Sort Keys

When using indexes with Sort Keys, that should be expressed as an index with an sk property. If you don't wish to use the Sort Key in your model, but it does exist on the table, simply use an empty for the composite property. An empty array is still very useful, and opens the door to more query opportunities and access patterns like collections.

// ElectroDB interprets as index *having* SK, but this model doesnt assign any composite attributes to it.
{
  indexes: {
    myIndex: {
      pk: {
        field: "pk",
        composite: ["id"]
      },
      sk: {
        field: "sk",
        composite: []
      }
    }
  }
}

Try it out!

Numeric Keys

If you have an index where the Partition or Sort Keys are expected to be numeric values, you can accomplish this with the template property on the index that requires numeric keys. Define the attribute used in the composite template as type "number", and then create a template string with only the attribute's name.

For example, this model defines both the Partition and Sort Key as numeric:

const schema = {
  model: {
    entity: "numeric",
    service: "example",
    version: "1"
  },
  attributes: {
    number1: {
      type: "number" // defined as number
    },
    number2: {
      type: "number"  // defined as number
    }
  },
  indexes: {
    record: {
      pk: {
        field: "pk",
        template: "${number1}" // will build PK as numeric value 
      },
      sk: {
        field: "sk",
        template: "${number2}" // will build SK as numeric value
      }
    }
  }
}

Try it out!

Index Casing

DynamoDB is a case-sensitive data store, and therefore it is common to convert the casing of keys to uppercase or lowercase prior to saving, updating, or querying data to your table. ElectroDB, by default, will lowercase all keys when preparing query parameters. For those who are using ElectroDB with an existing dataset, have preferences on upper or lowercase, or wish to not convert case at all, this can be configured on an index key field basis.

In the example below, we are configuring the casing ElectroDB will use individually for the Partition Key and Sort Key on the GSI "gis1". For the index's PK, mapped to gsi1pk, we ElectroDB will convert this key to uppercase prior to its use in queries. For the index's SK, mapped to gsi1pk, we ElectroDB will not convert the case of this key prior to its use in queries.

{
  indexes: {
    myIndex: {
      index: "gsi1",
      pk: {
        field: "gsi1pk",
        casing: "upper", // Acct_0120 -> ACCT_0120
        composite: ["organizationId"]
      },
      sk: {
        field: "gsi1sk",
        casing: "none", // Acct_0120 -> Acct_0120 
        composite: ["accountId"]
      }
    }
  }
}

Try it out!

NOTE: Casing is a very important decision when modeling your data in DynamoDB. While choosing upper/lower is largely a personal preference, once you have begun loading records in your table it can be difficult to change your casing after the fact. Unless you have good reason, allowing for mixed case keys can make querying data difficult because it will require database consumers to always have a knowledge of their data's case.

Casing OptionEffect
defaultThe default for keys is lowercase, or lower
lowerWill convert the key to lowercase prior it its use
upperWill convert the key to uppercase prior it its use
noneWill not perform any casing changes when building keys

Facets

As of version 0.11.1, "Facets" have been renamed to "Composite Attributes", and all documentation has been updated to reflect that change.

Composite Attributes

A Composite Attribute is a segment of a key based on one of the attributes. Composite Attributes are concatenated together from either a Partition Key, or a Sort Key key, which define an index.

NOTE: Only attributes with a type of "string", "number", "boolean", or string[] (enum) can be used as composite attributes.

There are two ways to provide composite:

  1. As a Composite Attribute Array
  2. As a Composite Attribute Template

For example, in the following Access Pattern, "locations" is made up of the composite attributes storeId, mallId, buildingId and unitId which map to defined attributes in the model:

// Input
{
    storeId: "STOREVALUE",
    mallId: "MALLVALUE",
    buildingId: "BUILDINGVALUE",
    unitId: "UNITVALUE"
};

// Output:
{
    pk: '$mallstoredirectory_1#storeId_storevalue',
    sk: '$mallstores#mallid_mallvalue#buildingid_buildingvalue#unitid_unitvalue'
}

For PK values, the service and version values from the model are prefixed onto the key.

For SK values, the entity value from the model is prefixed onto the key.

Composite Attribute Arrays

Within a Composite Attribute Array, each element is the name of the corresponding Attribute defined in the Model. The attributes chosen, and the order in which they are specified, will translate to how your composite keys will be built by ElectroDB.

NOTE: If the Attribute has a label property, that will be used to prefix the composite attributes, otherwise the full Attribute name will be used.

attributes: {
    storeId: {
        type: "string",
        label: "sid",
    },
    mallId: {
        type: "string",
        label: "mid",
    },
    buildingId: {
        type: "string",
        label: "bid",
    },
    unitId: {
        type: "string",
        label: "uid",
    }
},
indexes: {
    locations: {
        pk: {
            field: "pk",
            composite: ["storeId"]
        },
        sk: {
            field: "sk",
            composite: ["mallId", "buildingId", "unitId"]
        }
    }
}
    
// Input
{
    storeId: "STOREVALUE",
    mallId: "MALLVALUE",
    buildingId: "BUILDINGVALUE",
    unitId: "UNITVALUE"
};

// Output:
{
    pk: '$mallstoredirectory_1#sid_storevalue',
    sk: '$mallstores#mid_mallvalue#bid_buildingvalue#uid_unitvalue'
}

Try it out!

Composite Attribute Templates

In a Composite Template, you provide a formatted template for ElectroDB to use when making keys. Composite Attribute Templates allow for potential ElectroDB adoption on already established tables and records.

Attributes are identified by surrounding the attribute with ${...} braces. For example, the syntax ${storeId} will match storeId attribute in the model.

Convention for a composing a key use the # symbol to separate attributes, and for labels to attach with underscore. For example, when composing both the mallId and buildingId would be expressed as mid_${mallId}#bid_${buildingId}.

NOTE: ElectroDB will not prefix templated keys with the Entity, Project, Version, or Collection. This will give you greater control of your keys but will limit ElectroDB's ability to prevent leaking entities with some queries.

ElectroDB will continue to always add a trailing delimiter to composite attributes with keys are partially supplied. The section on BeginsWith Queries goes into more detail about how ElectroDB builds indexes from composite attributes.

{
    model: {
        entity: "MallStoreCustom",
        version: "1",
        service: "mallstoredirectory"
    },
  attributes: {
      storeId: {
          type: "string"
      },
      mallId: {
          type: "string"
      },
      buildingId: {
          type: "string"
      },
      unitId: {
          type: "string"
      }
  },
  indexes: {
      locations: {
          pk: {
              field: "pk",
              template: "sid_${storeId}"
          },
          sk: {
              field: "sk",
              template: "mid_${mallId}#bid_${buildingId}#uid_${unitId}"
          }
      }
  }
}


// Input
{
    storeId: "STOREVALUE",
    mallId: "MALLVALUE",
    buildingId: "BUILDINGVALUE",
    unitId: "UNITVALUE"
};

// Output:
{
    pk: 'sid_storevalue',
    sk: 'mid_mallvalue#bid_buildingvalue#uid_unitvalue'
}

Try it out!

Templates and Composite Attribute Arrays

The example above shows indexes defined only with the template property. This property alone is enough to work with ElectroDB, however it can be useful to also include a composite array with the names of the Composite Attributes included in the template string. Doing so achieves the following benefits:

ElectroDB will enforce that the template you have supplied actually resolves to the composite attributes specified in the array.

If you use ElectroDB with TypeScript, supplying the composite array will ensure the indexes' Composite Attributes are typed just the same as if you had not used a composite template.

An example of using template while also using composite:

{
  indexes: {
    locations: {
      pk: {
        field: "pk",
        template: "sid_${storeId}"
        composite: ["storeId"]
      },
      sk: {
        field: "sk",
        template: "mid_${mallId}#bid_${buildingId}#uid_${unitId}",
        composite: ["mallId", "buildingId", "unitId"]
      }
    }
  }
}

Try it out!

Composite Attribute and Index Considerations

As described in the above two sections (Composite Attributes, Indexes), ElectroDB builds your keys using the attribute values defined in your model and provided on your query. Here are a few considerations to take into account when thinking about how to model your indexes:

Your table's primary Partition and Sort Keys cannot be changed after a record has been created. Be mindful of not to use Attributes that have values that can change as composite attributes for your primary table index.

When updating/patching an Attribute that is also a composite attribute for secondary index, ElectroDB will perform a runtime check that the operation will leave a key in a partially built state. For example: if a Sort Key is defined as having the Composite Attributes ["prop1", "prop2", "prop3"], than an update to the prop1 Attribute will require supplying the prop2 and prop3 Attributes as well. This prevents a loss of key fidelity because ElectroDB is not able to update a key partially in place with its existing values.

As described and detailed in [Composite Attribute Arrays](#composite attribute-arrays), you can use the label property on an Attribute shorten a composite attribute's prefix on a key. This can allow trim down the length of your keys.

Attributes as Indexes

It may be the case that an index field is also an attribute. For example, if a table was created with a Primary Index partition key of accountId, and that same field is used to store the accountId value used by the application. The following are a few examples of how to model that schema with ElectroDB:

NOTE: If you have the unique opportunity to use ElectroDB with a new project, it is strongly recommended to use generically named index fields that are separate from your business attributes.

Using composite

When your attribute's name, or field property on an attribute, matches the field property on an indexes' pk or sk ElectroDB will forego its usual index key prefixing.

{
  model: {
    entity: "your_entity_name",
    service: "your_service_name",
    version: "1"
  },
  attributes: {
    accountId: {
      type: "string"
    },
    productNumber: {
      type: "number"
    }
  },
  indexes: {
    products: {
      pk: {
        field: "accountId",
        composite: ["accountId"]
      },
      sk: {
        field: "productNumber",
        composite: ["productNumber"]
      }
    }
  }
}

Try it out!

Using template

Another approach allows you to use the template property, which allows you to format exactly how your key should be built when interacting with DynamoDB. In this case composite is optional when using template, but including it helps with TypeScript typing.

{
  model: {
    entity: "your_entity_name",
    service: "your_service_name",
    version: "1"
  },
  attributes: {
    accountId: {
      type: "string" // string and number types are both supported
    }      
  },
  indexes: {
    "your_access_pattern_name": {
      pk: {
        field: "accountId",
        composite: ["accountId"], // `composite` is optional when using `template` but is required when using TypeScript
        template: "${accountId}"
      },
      sk: {...}
    }
  }
}

Try it out!

Advanced use of template

When your string attribute is also an index key, and using key templates, you can also add static prefixes and postfixes to your attribute. Under the covers, ElectroDB will leverage this template while interacting with DynamoDB but will allow you to maintain a relationship with the attribute value itself.

For example, given the following model:

{
  model: {
    entity: "your_entity_name",
    service: "your_service_name",
    version: "1"
  },
  attributes: {
    accountId: {
      type: "string" // only string types are both supported for this example
    },
    organizationId: {
      type: "string"
    },
    name: {
      type: "string"
    }
  },
  indexes: {
    "your_access_pattern_name": {
      pk: {
        field: "accountId",
        composite: ["accountId"],
        template: "prefix_${accountId}_postfix"
      },
      sk: {
        field: "organizationId",
        composite: ["organizationId"]
      }
    }
  }
}

Try it out!

ElectroDB will accept a get request like this:

await myEntity.get({
  accountId: "1111-2222-3333-4444",
  organizationId: "AAAA-BBBB-CCCC-DDDD"
}).go()

Query DynamoDB with the following params (note the pre/postfix on accountId):

NOTE: ElectroDB defaults keys to lowercase, though this can be configured using Index Casing.

{
  Key: {
    accountId: "prefix_1111-2222-3333-4444_postfix",
    organizationId: `aaaa-bbbb-cccc-dddd`, 
  },
  TableName: 'your_table_name'
}

When returned from a query, however, ElectroDB will return the following and trim the key of it's prefix and postfix:

{
  accountId: "prefix_1111-2222-3333-4444_postfix",
  organizationId: `aaaa-bbbb-cccc-dddd`,
}
name: "your_item_name"

Collections

A Collection is a grouping of Entities with the same Partition Key and allows you to make efficient query across multiple entities. If your background is SQL, imagine Partition Keys as Foreign Keys, a Collection represents a View with multiple joined Entities.

NOTE: ElectroDB Collections use DynamoDB queries to retrieve results. One query is made to retrieve results for all Entities (the benefits of single table design), however like the query method, ElectroDB will paginate through all results for a given query.

Collections are defined on an Index, and the name of the collection should represent what the query would return as a pseudo Entity. Additionally, Collection names must be unique across a Service.

NOTE: A collection name should be unique to a single common index across entities.

const DynamoDB = require("aws-sdk/clients/dynamodb");
const table = "projectmanagement";
const client = new DynamoDB.DocumentClient();

const employees = new Entity({
  model: {
    entity: "employees",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    employeeId: {
      type: "string"
    },
    organizationId: {
      type: "string"
    },
    name: {
      type: "string"
    },
    team: {
      type: ["jupiter", "mercury", "saturn"]
    }
  },
  indexes: {
    staff: {
      pk: {
        field: "pk",
        composite: ["organizationId"]
      },
      sk: {
        field: "sk",
        composite: ["employeeId"]
      }
    },
    employee: {
      collection: "assignments",
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: [],
      },
    }
  }
}, { client, table })

const tasks = new Entity({
  model: {
    entity: "tasks",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    taskId: {
      type: "string"
    },
    employeeId: {
      type: "string"
    },
    projectId: {
      type: "string"
    },
    title: {
      type: "string"
    },
    body: {
      type: "string"
    }
  },
  indexes: {
    project: {
      pk: {
        field: "pk",
        composite: ["projectId"]
      },
      sk: {
        field: "sk",
        composite: ["taskId"]
      }
    },
    assigned: {
      collection: "assignments",
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: ["projectId"],
      },
    }
  }
}, { client, table });

const TaskApp = new Service({employees, tasks});

await TaskApp.collections
    .assignments({employeeId: "JExotic"})
    .go();

// Equivalent Parameters
{
  "TableName": 'projectmanagement',
  "ExpressionAttributeNames": { '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  "ExpressionAttributeValues": { ':pk': '$taskapp_1#employeeid_joeexotic', ':sk1': '$assignments' },
  "KeyConditionExpression": '#pk = :pk and begins_with(#sk1, :sk1)',
  "IndexName": 'gsi2'
}

Try it out!

Collection Queries vs Entity Queries

To query across entities, collection queries make use of ElectroDB's Sort Key structure, which prefixes Sort Key fields with the collection name. Unlike an Entity Query, Collection Queries only leverage Composite Attributes from an access pattern's Partition Key.

To better explain how Collection Queries are formed, here is a juxtaposition of an Entity Query's parameters vs a Collection Query's parameters:

Entity Query

await TaskApp.entities
    .tasks.query
    .assigned({employeeId: "JExotic"})
    .go();

// Equivalent Parameters
{
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  ExpressionAttributeValues: {
    ':pk': '$taskapp#employeeid_jexotic',
    ':sk1': '$assignments#tasks_1'
  },
  IndexName: 'gsi2'
}

Try it out!

Collection Query

await TaskApp.collections
    .assignments({employeeId: "JExotic"})
    .go();

// Equivalent Parameters
{
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  ExpressionAttributeValues: { ':pk': '$taskapp#employeeid_jexotic', ':sk1': '$assignments' },
  IndexName: 'gsi2'
}

Try it out!

The notable difference between the two is how much of the Sort Key is specified at query time.

Entity Query:

ExpressionAttributeValues: { ':sk1': '$assignments#tasks_1' },

Collection Query:

ExpressionAttributeValues: { ':sk1': '$assignments' },

Collection Response Structure

Unlike Entity Queries which return an array, Collection Queries return an object. This object will have a key for every Entity name (or Entity Alias) associated with that Collection, and an array for all results queried that belong to that Entity.

For example, using the "TaskApp" models defined above, we would expect the following response from a query to the "assignments" collection:

let results = await TaskApp.collections
        .assignments({employeeId: "JExotic"})
        .go();

{
    tasks: [...],    // tasks for employeeId "JExotic" 
    employees: [...] // employee record(s) with employeeId "JExotic"
}

Because the Tasks and Employee Entities both associated their index (gsi2) with the same collection name (assignments), ElectroDB is able to associate the two entities via a shared Partition Key. As stated in the collections section, querying across Entities by PK can be comparable to querying across a foreign key in a traditional relational database.

Sub-Collections

Sub-Collections are an extension of Collection functionality that allow you to model more advanced access patterns. Collections and Sub-Collections are defined on Indexes via a property called collection, as either a string or string array respectively.

The following is an example of functionally identical collections, implemented as a string (referred to as a "collection") and then as a string array (referred to as sub-collections):

As a string (collection):

{
  collection: "assignments"
  pk: {
    field: "pk",
    composite: ["employeeId"]
  },
  sk: {
    field: "sk",
    composite: ["projectId"]
  }
}

As a string array (sub-collections):

{
  collection: ["assignments"]
  pk: {
    field: "pk",
            composite: ["employeeId"]
  },
  sk: {
    field: "sk",
            composite: ["projectId"]
  }
}

Both implementations above will create a "collections" method called assignments when added to a Service.

const results = await TaskApp.collections
    .assignments({employeeId: "JExotic"})
    .go();

The advantage to using a string array to define collections is the ability to express sub-collections. Below is an example of three entities using sub-collections, followed by an explanation of their sub-collection definitions:

Sub-Collection Entities

import {Entity, Service} from "electrodb"
import DynamoDB from "aws-sdk/clients/dynamodb";
const table = "projectmanagement";
const client = new DynamoDB.DocumentClient();

const employees = new Entity({
  model: {
    entity: "employees",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    employeeId: {
      type: "string"
    },
    organizationId: {
      type: "string"
    },
    name: {
      type: "string"
    },
    team: {
      type: ["jupiter", "mercury", "saturn"] as const
    }
  },
  indexes: {
    staff: {
      pk: {
        field: "pk",
        composite: ["organizationId"]
      },
      sk: {
        field: "sk",
        composite: ["employeeId"]
      }
    },
    employee: {
      collection: "contributions",
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: [],
      },
    }
  }
}, { client, table })

const tasks = new Entity({
  model: {
    entity: "tasks",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    taskId: {
      type: "string"
    },
    employeeId: {
      type: "string"
    },
    projectId: {
      type: "string"
    },
    title: {
      type: "string"
    },
    body: {
      type: "string"
    }
  },
  indexes: {
    project: {
      collection: "overview",
      pk: {
        field: "pk",
        composite: ["projectId"]
      },
      sk: {
        field: "sk",
        composite: ["taskId"]
      }
    },
    assigned: {
      collection: ["contributions", "assignments"] as const,
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: ["projectId"],
      },
    }
  }
}, { client, table });

const projectMembers = new Entity({
  model: {
    entity: "projectMembers",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    employeeId: {
      type: "string"
    },
    projectId: {
      type: "string"
    },
    name: {
      type: "string"
    },
  },
  indexes: {
    members: {
      collection: "overview",
      pk: {
        field: "pk",
        composite: ["projectId"]
      },
      sk: {
        field: "sk",
        composite: ["employeeId"]
      }
    },
    projects: {
      collection: ["contributions", "assignments"] as const,
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: [],
      },
    }
  }
}, { client, table }); 

const TaskApp = new Service({employees, tasks, projectMembers});

Try it out!

TypeScript Note: Use as const syntax when defining collection as a string array for improved type support

The last line of the code block above creates a Service called TaskApp using the Entity instances created above its declaration. By creating a Service, ElectroDB will identify and validate the sub-collections defined across all three models. The result in this case are four unique collections: "overview", "contributions", and "assignments".

The simplest collection to understand is overview. This collection is defined on the table's Primary Index, composed of a projectId in the Partition Key, and is currently implemented by two Entities: tasks and projectMembers. If another entity were to be added to our service, it could "join" this collection by implementing an identical Partition Key composite (projectId) and labeling itself as part of the overview collection. The following is an example of using the overview collection:

// overview
const results = await TaskApp.collections
    .overview({projectId: "SD-204"})
    .go();

// results 
{ 
  tasks: [...],         // tasks associated with projectId "SD-204
  projectMembers: [...] // employees of project "SD-204"
}

// parameters
{
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#pk': 'pk', '#sk1': 'sk' },
  ExpressionAttributeValues: { ':pk': '$taskapp#projectid_sd-204', ':sk1': '$overview' }
}

Try it out!

Unlike overview, the collections contributions, and assignments are more complex.

In the case of contributions, all three entities implement this collection on the gsi2 index, and compose their Partition Key with the employeeId attribute. The assignments collection, however, is only implemented by the tasks and projectMembers Entities. Below is an example of using these collections:

NOTE: Collection values of collection: "contributions" and collection: ["contributions"] are interpreted by ElectroDB as being the same implementation.

// contributions
const results = await TaskApp.collections
        .contributions({employeeId: "JExotic"})
        .go();

// results 
{
  tasks: [...], // tasks assigned to employeeId "JExotic" 
  projectMembers: [...], // projects with employeeId "JExotic"
  employees: [...] // employee record(s) with employeeId "JExotic"
}

{
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  ExpressionAttributeValues: { ':pk': '$taskapp#employeeid_jexotic', ':sk1': '$contributions' },
  IndexName: 'gsi2'
}

Try it out!

// assignments
const results = await TaskApp.collections
        .assignments({employeeId: "JExotic"})
        .go();

// results 
{
  tasks: [...],          // tasks assigned to employeeId "JExotic" 
  projectMembers: [...], // projects with employeeId "JExotic"
}

{
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  ExpressionAttributeValues: {
    ':pk': '$taskapp#employeeid_jexotic',
    ':sk1': '$contributions#assignments'
  },
  IndexName: 'gsi2'
}

Try it out!

Looking above we can see that the assignments collection is actually a subset of the results that could be queried with the contributions collection. The power behind having the assignments sub-collection is the flexibility to further slice and dice your cross-entity queries into more specific and performant queries.

If you're interested in the naming used in the collection and access pattern definitions above, checkout the section on Naming Conventions

Index and Collection Naming Conventions

ElectroDB puts an emphasis on allowing users to define more domain specific naming. Instead of referring to indexes by their name on the table, ElectroDB allows users to define their indexes as Access Patterns.

Please refer to the Entities defined in the section Sub-Collection Entities as the source of examples within this section.

Index Naming Conventions

The following is an access pattern on the "employees" entity defined here:

staff: {
  pk: {
    field: "pk",
    composite: ["organizationId"]
  },
  sk: {
    field: "sk",
    composite: ["employeeId"]
  }
}

This Access Pattern is defined on the table's Primary Index (note the lack of an index property), is given the name staff, and is composed of an organiztionId and an employeeId.

When deciding on an Access Pattern name, ask yourself, "What would the array of items returned represent if I only supplied the Partition Key". In this example case, the entity defines an "Employee" by its organizationId and employeeId. If you performed a query against this index, and only provided organizationId you would then expect to receive all Employees for that Organization. From there, the name staff was chosen because the focus becomes "What are these Employees to that Organization?".

This convention also becomes evident when you consider Access Pattern name becomes the name of the method you use query that index.

await employee.query.staff({organizationId: "nike"}).go();

Collection Naming Conventions

The following are access patterns on entities defined here:

// employees entity
employee: {
  collection: "contributions",
  index: "gsi2",
  pk: {
    field: "gsi2pk",
    composite: ["employeeId"],
  },
  sk: {
    field: "gsi2sk",
    composite: [],
  },
}

// tasks entity
assigned: {
  collection: ["contributions", "assignments"],
  index: "gsi2",
  pk: {
    field: "gsi2pk",
    composite: ["employeeId"],
  },
  sk: {
    field: "gsi2sk",
    composite: ["projectId"],
  },
}

// projectMembers entity
projects: {
  collection: ["contributions", "assignments"] as const,
  index: "gsi2",
  pk: {
    field: "gsi2pk",
    composite: ["employeeId"],
  },
  sk: {
    field: "gsi2sk",
    composite: [],
  },
}

In the case of the entities above, we see an example of a sub-collection. ElectroDB will use the above definitions to generate two collections: contributions, assignments.

The considerations for naming a collection are nearly identical to the considerations for naming an index: What do the query results from supplying just the Partition Key represent? In the case of collections you must also consider what the results represent across all of the involved entities, and the entities that may be added in the future.

For example, the contributions collection is named such because when given an employeeId we receive the employee's details, the tasks the that employee, and the projects where they are currently a member.

In the case of assignments, we receive a subset of contributions when supplying an employeeId: Only the tasks and projects they are "assigned" are returned.

Filters

Filters are no longer the preferred way to add FilterExpressions. Checkout the Where section to find out about how to apply FilterExpressions and ConditionExpressions.

Building thoughtful indexes can make queries simple and performant. Sometimes you need to filter results down further. By adding Filters to your model, you can extend your queries with custom filters. Below is the traditional way you would add a filter to Dynamo's DocumentClient directly alongside how you would accomplish the same using a Filter function.

{
  "IndexName": "idx2",
  "TableName": "StoreDirectory",
  "ExpressionAttributeNames": {
    "#rent": "rent",
    "#discount": "discount",
    "#pk": "idx2pk",
    "#sk1": "idx2sk"
  },
  "ExpressionAttributeValues": {
    ":rent1": "2000.00",
    ":rent2": "5000.00",
    ":discount1": "1000.00",
    ":pk": "$mallstoredirectory_1#mallid_eastpointe",
    ":sk1": "$mallstore#leaseenddate_2020-04-01#rent_",
    ":sk2": "$mallstore#leaseenddate_2020-07-01#rent_"
  },
  "KeyConditionExpression": ",#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2",
  "FilterExpression": "(#rent between :rent1 and :rent2) AND #discount <= :discount1"
}

Defined on the model

Deprecated but functional with 1.x

Filters can be defined on the model and used in your query chain.

/**
    * Filter by low rent a specific mall or a leaseEnd withing a specific range  
    * @param {Object} attributes - All attributes from the model with methods for each filter operation  
    * @param {...*} values - Values passed when calling the filter in a query chain.
**/
filters: {
    rentPromotions: function(attributes, minRent, maxRent, promotion)  {
        let {rent, discount} = attributes;
        return `
            ${rent.between(minRent, maxRent)} AND ${discount.lte(promotion)}
        `
    }
}


let StoreLocations  =  new Entity(model, {table: "StoreDirectory"});
let maxRent = "5000.00";
let minRent = "2000.00";
let promotion = "1000.00";
let stores = await MallStores.query
    .stores({ mallId: "EastPointe" })
    .between({ leaseEndDate:  "2020-04-01" }, { leaseEndDate:  "2020-07-01" })
    .rentPromotions(minRent, maxRent, promotion)
    .go();

// Equivalent Parameters
{
  IndexName: 'idx2',
  TableName: 'StoreDirectory',
  ExpressionAttributeNames: {
    '#rent': 'rent',
    '#discount': 'discount',
    '#pk': 'idx2pk',
    '#sk1': 'idx2sk'
  },
  ExpressionAttributeValues: {
    ':rent1': '2000.00',
    ':rent2': '5000.00',
    ':discount1': '1000.00',
    ':pk': '$mallstoredirectory_1#mallid_eastpointe',
    ':sk1': '$mallstore#leaseenddate_2020-04-01#rent_',
    ':sk2': '$mallstore#leaseenddate_2020-07-01#rent_'
  },
  KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2',
  FilterExpression: '(#rent between :rent1 and :rent2) AND #discount <= :discount1'
}

Defined via Filter method after query operators

Filters are no longer the preferred way to add FilterExpressions. Checkout the Where section to find out about how to apply FilterExpressions and ConditionExpressions.

The easiest way to use filters is to use them inline in your query chain.

let StoreLocations  =  new Entity(model, {table: "StoreDirectory"});
let maxRent = "5000.00";
let minRent = "2000.00";
let promotion = "1000.00";
let stores  =  await StoreLocations.query
    .leases({ mallId: "EastPointe" })
    .between({ leaseEndDate:  "2020-04-01" }, { leaseEndDate:  "2020-07-01" })
    .filter(({rent, discount}) => `
        ${rent.between(minRent, maxRent)} AND ${discount.lte(promotion)}
    `)
    .go();

// Equivalent Parameters
{
  IndexName: 'idx2',
  TableName: 'StoreDirectory',
  ExpressionAttributeNames: {
    '#rent': 'rent',
    '#discount': 'discount',
    '#pk': 'idx2pk',
    '#sk1': 'idx2sk'
  },
  ExpressionAttributeValues: {
    ':rent1': '2000.00',
    ':rent2': '5000.00',
    ':discount1': '1000.00',
    ':pk': '$mallstoredirectory_1#mallid_eastpointe',
    ':sk1': '$mallstore#leaseenddate_2020-04-01#rent_',
    ':sk2': '$mallstore#leaseenddate_2020-07-01#rent_'
  },
  KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2',
  FilterExpression: '(#rent between :rent1 and :rent2) AND #discount <= :discount1'
}

Filter functions allow you to write a FilterExpression without having to worry about the complexities of expression attributes. To accomplish this, ElectroDB injects an object attributes as the first parameter to all Filter Functions. This object contains every Attribute defined in the Entity's Model with the following operators as methods:

operator | example | result | ----------- | -------------------------------- |
gte | rent.gte(maxRent) | #rent >= :rent1 gt | rent.gt(maxRent) | #rent > :rent1 lte | rent.lte(maxRent) | #rent <= :rent1 lt | rent.lt(maxRent) | #rent < :rent1 eq | rent.eq(maxRent) | #rent = :rent1 ne | rent.ne(maxRent) | #rent <> :rent1 begins | rent.begins(maxRent) | begins_with(#rent, :rent1) exists | rent.exists() | attribute_exists(#rent) notExists | rent.notExists() | attribute_not_exists(#rent) contains | rent.contains(maxRent) | contains(#rent = :rent1) notContains | rent.notContains(maxRent) | not contains(#rent = :rent1) between | rent.between(minRent, maxRent) | (#rent between :rent1 and :rent2) name | rent.name() | #rent value | rent.value(maxRent) | :rent1

This functionality allows you to write the remaining logic of your FilterExpression with ease. Add complex nested and/or conditions or other FilterExpression logic while ElectroDB handles the ExpressionAttributeNames and ExpressionAttributeValues.

Multiple Filters

Filters are no longer the preferred way to add FilterExpressions. Checkout the Where section to find out about how to apply FilterExpressions and ConditionExpressions.

It is possible to chain together multiple filters. The resulting FilterExpressions are concatenated with an implicit AND operator.

let MallStores = new Entity(model, {table: "StoreDirectory"});
let stores = await MallStores.query
    .leases({ mallId: "EastPointe" })
    .between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
    .filter(({ rent, discount }) => `
        ${rent.between("2000.00", "5000.00")} AND ${discount.eq("1000.00")}
    `)
    .filter(({ category }) => `
        ${category.eq("food/coffee")}
    `)
    .go();

// Equivalent Parameters
{
  TableName: 'StoreDirectory',
  ExpressionAttributeNames: {
    '#rent': 'rent',
    '#discount': 'discount',
    '#category': 'category',
    '#pk': 'idx2pk',
    '#sk1': 'idx2sk'
  },
  ExpressionAttributeValues: {
    ':rent1': '2000.00',
    ':rent2': '5000.00',
    ':discount1': '1000.00',
    ':category1': 'food/coffee',
    ':pk': '$mallstoredirectory_1#mallid_eastpointe',
    ':sk1': '$mallstore#leaseenddate_2020-04-01#storeid_',
    ':sk2': '$mallstore#leaseenddate_2020-07-01#storeid_'
  },
  KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2',
  IndexName: 'idx2',
  FilterExpression: '(#rent between :rent1 and :rent2) AND (#discount = :discount1 AND #category = :category1)'
}

Where

The where() method is an improvement on the filter() method. Unlike filter, where will be compatible with upcoming features related to complex types.

Building thoughtful indexes can make queries simple and performant. Sometimes you need to filter results down further or add conditions to an update/patch/put/create/delete/remove action.

FilterExpressions

Below is the traditional way you would add a FilterExpression to Dynamo's DocumentClient directly alongside how you would accomplish the same using the where method.

animals.query
  .exhibit({habitat: "Africa"})
  .where(({isPregnant, offspring}, {exists, eq}) => `
    ${eq(isPregnant, true)} OR ${exists(offspring)}
  `)
  .go()
{
  "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)",
  "TableName": "zoo_manifest",
  "ExpressionAttributeNames": {
    "#isPregnant": "isPregnant",
    "#offspring": "offspring",
    "#pk": "gsi1pk",
    "#sk1": "gsi1sk"
  },
  "ExpressionAttributeValues": {
    ":isPregnant0": true,
    ":pk": "$zoo#habitat_africa",
    ":sk1": "$animals_1#enclosure_"
  },
  "IndexName": "gsi1pk-gsi1sk-index",
  "FilterExpression": "#isPregnant = :isPregnant0 OR attribute_exists(#offspring)"
}

Try it out!

ConditionExpressions

Below is the traditional way you would add a ConditionExpression to Dynamo's DocumentClient directly alongside how you would accomplish the same using the where method.

animals.update({
    animal: "blackbear",
    name: "Isabelle"
  })
  // no longer pregnant because Ernesto was born!
  .set({
    isPregnant: false,
    lastEvaluation: "2021-09-12",
    lastEvaluationBy: "stephanie.adler"
  })
  // welcome to the world Ernesto!
  .append({
    offspring: [{
      name: "Ernesto",
      birthday: "2021-09-12",
      note: "healthy birth, mild pollen allergy"
    }]
  })
  // using the where clause can guard against making
  // updates against stale data
  .where(({isPregnant, lastEvaluation}, {lt, eq}) => `
    ${eq(isPregnant, true)} AND ${lt(lastEvaluation, "2021-09-12")}
  `)
  .go()
{
  "UpdateExpression": "SET #isPregnant = :isPregnant_u0, #lastEvaluation = :lastEvaluation_u0, #lastEvaluationBy = :lastEvaluationBy_u0, #offspring = list_append(#offspring, :offspring_u0)",
  "ExpressionAttributeNames": {
    "#isPregnant": "isPregnant",
    "#lastEvaluation": "lastEvaluation",
    "#lastEvaluationBy": "lastEvaluationBy",
    "#offspring": "offspring"
  },
  "ExpressionAttributeValues": {
    ":isPregnant0": true,
    ":lastEvaluation0": "2021-09-12",
    ":isPregnant_u0": false,
    ":lastEvaluation_u0": "2021-09-12",
    ":lastEvaluationBy_u0": "stephanie.adler",
    ":offspring_u0": [
      {
        "name": "Ernesto",
        "birthday": "2021-09-12",
        "note": "healthy birth, mild pollen allergy"
      }
    ]
  },
  "TableName": "zoo_manifest",
  "Key": {
    "pk": "$zoo#animal_blackbear",
    "sk": "$animals_1#name_isabelle"
  },
  "ConditionExpression": "#isPregnant = :isPregnant0 AND #lastEvaluation < :lastEvaluation0"
}

Try it out!

Where with Complex Attributes

ElectroDB supports using the where() method with DynamoDB's complex attribute types: map, list, and set. When using the injected attributes object, simply drill into the attribute itself to apply your update directly to the required object.

The following are examples on how to filter on complex attributes:

Example 1: Filtering on a map attribute

animals.query
    .farm({habitat: "Africa"})
    .where(({veterinarian}, {eq}) => eq(veterinarian.name, "Herb Peterson"))
    .go()

Try it out!

Example 1: Filtering on an element in a list attribute

animals.query
  .exhibit({habitat: "Tundra"})
  .where(({offspring}, {eq}) => eq(offspring[0].name, "Blitzen"))
  .go()

Try it out!

Attributes and Operations

Where functions allow you to write a FilterExpression or ConditionExpression without having to worry about the complexities of expression attributes. To accomplish this, ElectroDB injects an object attributes as the first parameter to all Filter Functions, and an object operations, as the second parameter. Pass the properties from the attributes object to the methods found on the operations object, along with inline values to set filters and conditions.

NOTE: where callbacks must return a string. All method on the operation object all return strings, so you can return the results of the operation method or use template strings compose an expression.

// A single filter operation 
animals.update({habitat: "Africa", enclosure: "5b"})
  .set({keeper: "Joe Exotic"})
  .where((attr, op) => op.eq(attr.dangerous, true))
  .go();

// A single filter operation w/ destructuring
animals.update({animal: "tiger", name: "janet"})
  .set({keeper: "Joe Exotic"})
  .where(({dangerous}, {eq}) => eq(dangerous, true))
  .go();

// Multiple conditions - `op`
animals.update({animal: "tiger", name: "janet"})
  .set({keeper: "Joe Exotic"})
  .where((attr, op) => `
    ${op.eq(attr.dangerous, true)} AND ${op.notExists(attr.lastFed)}
  `)
  .go();

// Multiple usages of `where` (implicit AND)
animals.update({animal: "tiger", name: "janet"})
  .set({keeper: "Joe Exotic"})
  .where((attr, op) => `
    ${op.eq(attr.dangerous, true)} OR ${op.notExists(attr.lastFed)}
  `)
  .where(({birthday}, {between}) => {
    const today = Date.now();
    const lastMonth = today - 1000 * 60 * 60 * 24 * 30;
    return between(birthday, lastMonth, today);
  })
  .go();

// "dynamic" filtering
function getAnimals(habitat, keepers) {
  const query = animals.query.exhibit({habitat});
  for (const name of keepers) {
    query.where(({keeper}, {eq}) => eq(keeper, name));
  }
  return query.go();
}

const keepers = [
  "Joe Exotic",
  "Carol Baskin"
];

getAnimals("RainForest", keepers);

Try it out!

The attributes object contains every Attribute defined in the Entity's Model. The operations object contains the following methods:

operatorexampleresult
eqeq(rent, maxRent)#rent = :rent1
neeq(rent, maxRent)#rent <> :rent1
gtegte(rent, value)#rent >= :rent1
gtgt(rent, maxRent)#rent > :rent1
ltelte(rent, maxRent)#rent <= :rent1
ltlt(rent, maxRent)#rent < :rent1
beginsbegins(rent, maxRent)begins_with(#rent, :rent1)
existsexists(rent)attribute_exists(#rent)
notExistsnotExists(rent)attribute_not_exists(#rent)
containscontains(rent, maxRent)contains(#rent = :rent1)
notContainsnotContains(rent, maxRent)not contains(#rent = :rent1)
betweenbetween(rent, minRent, maxRent)(#rent between :rent1 and :rent2)
namename(rent)#rent
valuevalue(rent, maxRent):rent1

Multiple Where Clauses

It is possible to include chain multiple where clauses. The resulting FilterExpressions (or ConditionExpressions) are concatenated with an implicit AND operator.

let MallStores = new Entity(model, {table: "StoreDirectory"});
let stores = await MallStores.query
    .leases({ mallId: "EastPointe" })
    .between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
    .where(({ rent, discount }, {between, eq}) => `
        ${between(rent, "2000.00", "5000.00")} AND ${eq(discount, "1000.00")}
    `)
    .where(({ category }, {eq}) => `
        ${eq(category, "food/coffee")}
    `)
    .go();

// Equivalent Parameters
{
  TableName: 'StoreDirectory',
  ExpressionAttributeNames: {
    '#rent': 'rent',
    '#discount': 'discount',
    '#category': 'category',
    '#pk': 'idx2pk',
    '#sk1': 'idx2sk'
  },
  ExpressionAttributeValues: {
    ':rent1': '2000.00',
    ':rent2': '5000.00',
    ':discount1': '1000.00',
    ':category1': 'food/coffee',
    ':pk': '$mallstoredirectory_1#mallid_eastpointe',
    ':sk1': '$mallstore#leaseenddate_2020-04-01#storeid_',
    ':sk2': '$mallstore#leaseenddate_2020-07-01#storeid_'
  },
  KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2',
  IndexName: 'idx2',
  FilterExpression: '(#rent between :rent1 and :rent2) AND (#discount = :discount1 AND #category = :category1)'
}

Parse

The parse method can be given a DocClient response and return a typed and formatted ElectroDB item.

ElectroDB's parse() method accepts results from get, delete, put, update, query, and scan operations, applies all the same operations as though the item was retrieved by ElectroDB itself, and will return null (or empty array for query results) if the item could not be parsed.

const myEntity = new Entity({...});
const getResults = docClient.get({...}).promise();
const queryResults = docClient.query({...}).promise();
const updateResults = docClient.update({...}).promise(); 
const formattedGetResults = myEntity.parse(getResults);
const formattedQueryResults = myEntity.parse(formattedQueryResults);

Parse also accepts an optional options object as a second argument (see the section Query Options to learn more). Currently, the following query options are relevant to the parse() method:

Option | Default | Notes ----------------- : ------- | ----- ignoreOwnership | true | This property defaults to true here, unlike elsewhere in the application when it defaults to false. You can overwrite the default here with your own preference.

Building Queries

For hands-on learners: the following example can be followed along with and executed on runkit: https://runkit.com/tywalch/electrodb-building-queries

ElectroDB queries use DynamoDB's query method to find records based on your table's indexes.

NOTE: By default, ElectroDB will paginate through all items that match your query. To limit the number of items ElectroDB will retrieve, read more about the Query Options pages and limit, or use the ElectroDB Pagination API for fine-grain pagination support.

Forming a composite Partition Key and Sort Key is a critical step in planning Access Patterns in DynamoDB. When planning composite keys, it is crucial to consider the order in which they are composed. As of the time of writing this documentation, DynamoDB has the following constraints that should be taken into account when planning your Access Patterns:

  1. You must always supply the Partition Key in full for all queries to DynamoDB.
  2. You currently only have the following operators available on a Sort Key: begins_with, between, >, >=, <, <=, and Equals.
  3. To act on single record, you will need to know the full Partition Key and Sort Key for that record.

Using composite attributes to make hierarchical keys

Carefully considering your Composite Attribute order will allow ElectroDB to express hierarchical relationships and unlock more available Access Patterns for your application.

For example, let's say you have a StoreLocations Entity that represents Store Locations inside Malls:

Shopping Mall Stores

let schema = {  
    model: {
      service: "MallStoreDirectory",  
      entity: "MallStore",
      version: "1",    
    },  
    attributes: {
        cityId: {
            type: "string",
            required: true,
        }, 
        mallId: {  
            type: "string",  
            required: true,  
        },  
        storeId: {  
            type: "string",  
            required: true,  
        },  
        buildingId: {  
            type: "string",  
            required: true,  
        },  
        unitId: {  
            type: "string",  
            required: true,
        },  
        category: {  
            type: [
                "spite store",
                "food/coffee", 
                "food/meal", 
                "clothing", 
                "electronics", 
                "department", 
                "misc"
            ],  
            required: true  
        },  
        leaseEndDate: {  
            type: "string",  
            required: true  
        },
        rent: {
            type: "string",
            required: true,
            validate: /^(\d+\.\d{2})$/
        },
        discount: {
            type: "string",
            required: false,
            default: "0.00",
            validate: /^(\d+\.\d{2})$/
        }  
    },  
    indexes: {  
        stores: {  
            pk: {
                field: "pk",
                composite: ["cityId", "mallId"]
            }, 
            sk: {
                field: "sk",
                composite: ["buildingId", "storeId"]
            }  
        },  
        units: {  
            index: "gis1pk-gsi1sk-index",  
            pk: {
                field: "gis1pk",
                composite: ["mallId"]
            },  
            sk: {
                field: "gsi1sk",
                composite: ["buildingId", "unitId"]
            }  
        },
        leases: {
            index: "gis2pk-gsi2sk-index",
            pk: {
                field: "gis2pk",
                composite: ["storeId"]
            },  
            sk: {
                field: "gsi2sk",
                composite: ["leaseEndDate"]
            }  
        }
    }
};
const StoreLocations = new Entity(schema, {table: "StoreDirectory"});

Query App Records

Examples in this section using the MallStore schema defined above, and available for interacting with here: https://runkit.com/tywalch/electrodb-building-queries

All queries start from the Access Pattern defined in the schema.

const MallStore = new Entity(schema, {table: "StoreDirectory"}); 
// Each Access Pattern is available on the Entity instance
// MallStore.query.stores()
// MallStore.query.malls()

Partition Key Composite Attributes

All queries require (at minimum) the Composite Attributes included in its defined Partition Key. Composite Attributes you define on the Sort Key can be partially supplied, but must be supplied in the order they are defined.

Important: Composite Attributes must be supplied in the order they are composed when invoking the Access Pattern. This is because composite attributes are used to form a concatenated key string, and if attributes supplied out of order, it is not possible to fill the gaps in that concatenation.

const MallStore = new Entity({
  model: {
    service: "mallmgmt",
    entity: "store", 
    version: "1"
  },
  attributes: {
    cityId: "string",
    mallId: "string",
    storeId: "string",
    buildingId: "string",
    unitId: "string",
    name: "string",
    description: "string",
    category: "string"
  },
  indexes: {
    stores: {
      pk: {
        field: "pk",
        composite: ["cityId", "mallId"]
      },
      sk: {
        field: "sk",
        composite: ["storeId", "unitId"]
      }
    }
  }
}, {table: "StoreDirectory"});

const cityId = "Atlanta1";
const mallId = "EastPointe";
const storeId = "LatteLarrys";
const unitId = "B24";
const buildingId = "F34";

// Good: Includes at least the PK
StoreLocations.query.stores({cityId, mallId});

// Good: Includes at least the PK, and the first SK attribute
StoreLocations.query.stores({cityId, mallId, storeId});

// Good: Includes at least the PK, and the all SK attributes   
StoreLocations.query.stores({cityId, mallId, storeId, unitId});

// Bad: No PK composite attributes specified, will throw
StoreLocations.query.stores();

// Bad: Not All PK Composite Attributes included (cityId), will throw
StoreLocations.query.stores({mallId});

// Bad: Composite Attributes not included in order, will NOT throw, but will ignore `unitId` because `storeId` was not supplied as well
StoreLocations.query.stores({cityId, mallId, unitId});

Sort Key Operations

operatoruse case
beginsKeys starting with a particular set of characters.
betweenKeys between a specified range.
gtKeys less than some value
gteKeys less than or equal to some value
ltKeys greater than some value
lteKeys greater than or equal to some value

Each record represents one Store location. All Stores are located in Malls we manage.

To satisfy requirements for searching based on location, you could use the following keys: Each StoreLocations record would have a Partition Key with the store's storeId. This key alone is not enough to identify a particular store. To solve this, compose a Sort Key for the store's location attribute ordered hierarchically (mall/building/unit): ["mallId", "buildingId", "unitId"].

The StoreLocations entity above, using just the stores Index alone enables four Access Patterns:

  1. All LatteLarrys locations in all Malls
  2. All LatteLarrys locations in one Mall
  3. All LatteLarrys locations inside a specific Mall
  4. A specific LatteLarrys inside of a Mall and Building

Query Chains

Queries in ElectroDB are built around the Access Patterns defined in the Schema and are capable of using partial key Composite Attributes to create performant lookups. To accomplish this, ElectroDB offers a predictable chainable API.

Examples in this section using the StoreLocations schema defined above and can be directly experiment with on runkit: https://runkit.com/tywalch/electrodb-building-queries

The methods: Get (get), Create (put), Update (update), and Delete (delete) *require all composite attributes described in the Entities' primary PK and SK.

Query Method

ElectroDB queries use DynamoDB's query method to find records based on your table's indexes. To read more about queries checkout the section Building Queries

NOTE: By default, ElectroDB will paginate through all items that match your query. To limit the number of items ElectroDB will retrieve, read more about the Query Options pages and limit, or use the ElectroDB Pagination API for fine-grain pagination support.

Get Method

Provide all Table Index composite attributes in an object to the get method. In the event no record is found, a value of null will be returned.

NOTE: As part of ElectroDB's roll out of 1.0.0, a breaking change was made to the get method. Prior to 1.0.0, the get method would return an empty object if a record was not found. This has been changed to now return a value of null in this case.

let results = await StoreLocations.get({
    storeId: "LatteLarrys", 
    mallId: "EastPointe", 
    buildingId: "F34", 
    cityId: "Atlanta1"
}).go();

// Equivalent Params:
// {
//   Key: {
//     pk: "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
//     sk: "$mallstore_1#buildingid_f34#storeid_lattelarrys"
//   },
//   TableName: 'StoreDirectory'
// }

Batch Get

Provide all Table Index composite attributes in an array of objects to the get method to perform a BatchGet query.

NOTE: Performing a BatchGet will return a response structure unique to BatchGet: a two-dimensional array with the results of the query and any unprocessed records. See the example below. Additionally, when performing a BatchGet the .params() method will return an array of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchGet queries larger than the docClient's limit of 100 records.

If the number of records you are requesting is above the BatchGet threshold of 100 records, ElectroDB will make multiple requests to DynamoDB and return the results in a single array. By default, ElectroDB will make these requests in series, one after another. If you are confident your table can handle the throughput, you can use the Query Option concurrent. This value can be set to any number greater than zero, and will execute that number of requests simultaneously.

For example, 150 records (50 records over the DynamoDB maximum):

The default value of concurrent will be 1. ElectroDB will execute a BatchGet request of 100, then after that request has responded, make another BatchGet request for 50 records.

If you set the Query Option concurrent to 2, ElectroDB will execute a BatchGet request of 100 records, and another BatchGet request for 50 records without waiting for the first request to finish.

It is important to consider your Table's throughput considerations when setting this value.

let [results, unprocessed] = await StoreLocations.get([
    {
        storeId: "LatteLarrys", 
        mallId: "EastPointe", 
        buildingId: "F34", 
        cityId: "Atlanta1"
    },
    {
        storeId: "MochaJoes", 
        mallId: "WestEnd", 
        buildingId: "A21", 
        cityId: "Madison2"
    }   
]).go({concurrent: 1}); // `concurrent` value is optional and default's to `1`

// Equivalent Params:
// {
//   "RequestItems": {
//     "electro": {
//       "Keys": [
//         {
//           "pk": "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
//           "sk": "$mallstore_1#buildingid_f34#storeid_lattelarrys"
//         },
//         {
//           "pk": "$mallstoredirectory#cityid_madison2#mallid_westend",
//           "sk": "$mallstore_1#buildingid_a21#storeid_mochajoes"
//         }
//       ]
//     }
//   }
// }

The two-dimensional array returned by batch get most easily used when deconstructed into two variables, in the above case: results and unprocessed.

The results array are records that were returned DynamoDB as Responses on the BatchGet query. They will appear in the same format as other ElectroDB queries.

Elements of the unprocessed array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the query option {unprocessed:"raw"} override this behavior and return the Keys as they came from DynamoDB.

Delete Method

Provide all Table Index composite attributes in an object to the delete method to delete a record.

await StoreLocations.delete({
    storeId: "LatteLarrys", 
    mallId: "EastPointe", 
    buildingId: "F34", 
    cityId: "Atlanta1"
}).go();

// Equivalent Params:
// {
//   Key: {
//     pk: "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
//     sk: "$mallstore_1#buildingid_f34#storeid_lattelarrys"
//   },
//   TableName: 'StoreDirectory'
// }

Batch Write Delete Records

Provide all table index composite attributes in an array of objects to the delete method to batch delete records.

NOTE: Performing a Batch Delete will return an array of "unprocessed" records. An empty array signifies all records were processed. If you want the raw DynamoDB response you can always use the option {raw: true}, more detail found here: Query Options. Additionally, when performing a BatchWrite the .params() method will return an array of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchWrite queries larger than the docClient's limit of 25 records.

If the number of records you are requesting is above the BatchWrite threshold of 25 records, ElectroDB will make multiple requests to DynamoDB and return the results in a single array. By default, ElectroDB will make these requests in series, one after another. If you are confident your table can handle the throughput, you can use the Query Option concurrent. This value can be set to any number greater than zero, and will execute that number of requests simultaneously.

For example, 75 records (50 records over the DynamoDB maximum):

The default value of concurrent will be 1. ElectroDB will execute a BatchWrite request of 25, then after that request has responded, make another BatchWrite request for 25 records, and then another.

If you set the Query Option concurrent to 2, ElectroDB will execute a BatchWrite request of 25 records, and another BatchGet request for 25 records without waiting for the first request to finish. After those two have finished it will execute another BatchWrite request for 25 records.

It is important to consider your Table's throughput considerations when setting this value.

let unprocessed = await StoreLocations.delete([
    {
        storeId: "LatteLarrys", 
        mallId: "EastPointe", 
        buildingId: "F34", 
        cityId: "LosAngeles1"
    },
    {
        storeId: "MochaJoes", 
        mallId: "EastPointe", 
        buildingId: "F35", 
        cityId: "LosAngeles1"
    }
]).go({concurrent: 1}); // `concurrent` value is optional and default's to `1` 

// Equivalent Params:
{
  "RequestItems": {
    "StoreDirectory": [
      {
        "DeleteRequest": {
          "Key": {
            "pk": "$mallstoredirectory#cityid_losangeles1#mallid_eastpointe",
            "sk": "$mallstore_1#buildingid_f34#storeid_lattelarrys"
          }
        }
      },
      {
        "DeleteRequest": {
          "Key": {
            "pk": "$mallstoredirectory#cityid_losangeles1#mallid_eastpointe",
            "sk": "$mallstore_1#buildingid_f35#storeid_mochajoes"
          }
        }
      }
    ]
  }
}

Elements of the unprocessed array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the query option {unprocessed:"raw"} override this behavior and return the Keys as they came from DynamoDB.

Put Record

Provide all required Attributes as defined in the model to create a new record. ElectroDB will enforce any defined validations, defaults, casting, and field aliasing. A Put operation will trigger the default, and set attribute callbacks when writing to DynamoDB. By default, after performing a put() or create() operation, ElectroDB will format and return the record through the same process as a Get/Query. This process will invoke the get callback on all included attributes. If this behaviour is not desired, use the Query Option response:"none" to return a null value.

This example includes an optional conditional expression

await StoreLocations
  .put({
      cityId: "Atlanta1",
      storeId: "LatteLarrys",
      mallId: "EastPointe",
      buildingId: "BuildingA1",
      unitId: "B47",
      category: "food/coffee",
      leaseEndDate: "2020-03-22",
      rent: "4500.00"
  })
  .where((attr, op) => op.eq(attr.rent, "4500.00"))
  .go()

// Equivalent Params:
{
  "Item": {
    "cityId": "Atlanta1",
    "mallId": "EastPointe",
    "storeId": "LatteLarrys",
    "buildingId": "BuildingA1",
    "unitId": "B47",
    "category": "food/coffee",
    "leaseEndDate": "2020-03-22",
    "rent": "4500.00",
    "discount": "0.00",
    "pk": "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_buildinga1#storeid_lattelarrys",
    "gis1pk": "$mallstoredirectory#mallid_eastpointe",
    "gsi1sk": "$mallstore_1#buildingid_buildinga1#unitid_b47",
    "gis2pk": "$mallstoredirectory#storeid_lattelarrys",
    "gsi2sk": "$mallstore_1#leaseenddate_2020-03-22",
    "__edb_e__": "MallStore",
    "__edb_v__": "1"
  },
  "TableName": "StoreDirectory",
  "ConditionExpression": "#rent = :rent_w1",
  "ExpressionAttributeNames": {
    "#rent": "rent"
  },
  "ExpressionAttributeValues": {
    ":rent_w1": "4500.00"
  }
}

Batch Write Put Records

Provide all required Attributes as defined in the model to create records as an array to .put(). ElectroDB will enforce any defined validations, defaults, casting, and field aliasing. Another convenience ElectroDB provides, is accepting BatchWrite arrays larger than the 25 record limit. This is achieved making multiple, "parallel", requests to DynamoDB for batches of 25 records at a time. A failure with any of these requests will cause the query to throw, so be mindful of your table's configured throughput.

NOTE: Performing a Batch Put will return an array of "unprocessed" records. An empty array signifies all records returned were processed. If you want the raw DynamoDB response you can always use the option {raw: true}, more detail found here: Query Options. Additionally, when performing a BatchWrite the .params() method will return an array of parameters, rather than just the parameters for one docClient query. This is because ElectroDB BatchWrite queries larger than the docClient's limit of 25 records.

If the number of records you are requesting is above the BatchWrite threshold of 25 records, ElectroDB will make multiple requests to DynamoDB and return the results in a single array. By default, ElectroDB will make these requests in series, one after another. If you are confident your table can handle the throughput, you can use the Query Option concurrent. This value can be set to any number greater than zero, and will execute that number of requests simultaneously.

For example, 75 records (50 records over the DynamoDB maximum):

The default value of concurrent will be 1. ElectroDB will execute a BatchWrite request of 25, then after that request has responded, make another BatchWrite request for 25 records, and then another.

If you set the Query Option concurrent to 2, ElectroDB will execute a BatchWrite request of 25 records, and another BatchGet request for 25 records without waiting for the first request to finish. After those two have finished it will execute another BatchWrite request for 25 records.

It is important to consider your Table's throughput considerations when setting this value.

let unprocessed = await StoreLocations.put([
    {
        cityId: "LosAngeles1",
        storeId: "LatteLarrys",
        mallId: "EastPointe",
        buildingId: "F34",
        unitId: "a1",
        category: "food/coffee",
        leaseEndDate: "2022-03-22",
        rent: "4500.00"
    },
    {
        cityId: "LosAngeles1",
        storeId: "MochaJoes",
        mallId: "EastPointe",
        buildingId: "F35",
        unitId: "a2",
        category: "food/coffee",
        leaseEndDate: "2021-01-22",
        rent: "1500.00"
    }
]).go({concurrent: 1}); // `concurrent` value is optional and default's to `1`

// Equivalent Params:
{
  "RequestItems": {
    "StoreDirectory": [
      {
        "PutRequest": {
          "Item": {
            "cityId": "LosAngeles1",
            "mallId": "EastPointe",
            "storeId": "LatteLarrys",
            "buildingId": "F34",
            "unitId": "a1",
            "category": "food/coffee",
            "leaseEndDate": "2022-03-22",
            "rent": "4500.00",
            "discount": "0.00",
            "pk": "$mallstoredirectory#cityid_losangeles1#mallid_eastpointe",
            "sk": "$mallstore_1#buildingid_f34#storeid_lattelarrys",
            "gis1pk": "$mallstoredirectory#mallid_eastpointe",
            "gsi1sk": "$mallstore_1#buildingid_f34#unitid_a1",
            "gis2pk": "$mallstoredirectory#storeid_lattelarrys",
            "gsi2sk": "$mallstore_1#leaseenddate_2022-03-22",
            "__edb_e__": "MallStore",
            "__edb_v__": "1"
          }
        }
      },
      {
        "PutRequest": {
          "Item": {
            "cityId": "LosAngeles1",
            "mallId": "EastPointe",
            "storeId": "MochaJoes",
            "buildingId": "F35",
            "unitId": "a2",
            "category": "food/coffee",
            "leaseEndDate": "2021-01-22",
            "rent": "1500.00",
            "discount": "0.00",
            "pk": "$mallstoredirectory#cityid_losangeles1#mallid_eastpointe",
            "sk": "$mallstore_1#buildingid_f35#storeid_mochajoes",
            "gis1pk": "$mallstoredirectory#mallid_eastpointe",
            "gsi1sk": "$mallstore_1#buildingid_f35#unitid_a2",
            "gis2pk": "$mallstoredirectory#storeid_mochajoes",
            "gsi2sk": "$mallstore_1#leaseenddate_2021-01-22",
            "__edb_e__": "MallStore",
            "__edb_v__": "1"
          }
        }
      }
    ]
  }
}

Elements of the unprocessed array are unlike results received from a query. Instead of containing all the attributes of a record, an unprocessed record only includes the composite attributes defined in the Table Index. This is in keeping with DynamoDB's practice of returning only Keys in the case of unprocessed records. For convenience, ElectroDB will return these keys as composite attributes, but you can pass the query option {unprocessed:"raw"} override this behavior and return the Keys as they came from DynamoDB.

Update Record

Update Methods are available after the method update() is called, and allow you to perform alter an item stored dynamodb. The methods can be used (and reused) in a chain to form update parameters, when finished with .params(), or an update operation, when finished with .go(). If your application requires the update method to return values related to the update (e.g. via the ReturnValues DocumentClient parameters), you can use the Query Option {response: "none" | "all_old" | "updated_old" | "all_new" | "updated_new"} with the value that matches your need. By default, the Update operation returns an empty object when using .go().

ElectroDB will validate an attribute's type when performing an operation (e.g. that the subtract() method can only be performed on numbers), but will defer checking the logical validity your update operation to the DocumentClient. If your query performs multiple mutations on a single attribute, or perform other illogical operations given nature of an item/attribute, ElectroDB will not validate these edge cases and instead will simply pass back any error(s) thrown by the Document Client.

Update MethodAttribute TypesParameter
setnumber string boolean enum map list set anyobject
removenumber string boolean enum map list set anyarray
addnumber any setobject
subtractnumberobject
appendany listobject
deleteany setobject
data*callback

Updates to Composite Attributes

ElectroDB adds some constraints to update calls to prevent the accidental loss of data. If an access pattern is defined with multiple composite attributes, then ElectroDB ensure the attributes cannot be updated individually. If an attribute involved in an index composite is updated, then the index key also must be updated, and if the whole key cannot be formed by the attributes supplied to the update, then it cannot create a composite key without overwriting the old data.

This example shows why a partial update to a composite key is prevented by ElectroDB:

{
  "index": "my-gsi",
  "pk": {
    "field": "gsi1pk",
    "composite": ["attr1"]
  },
  "sk": {
    "field": "gsi1sk",
    "composite": ["attr2", "attr3"]
  }
}

The above secondary index definition would generate the following index keys:

{
  "gsi1pk": "$service#attr1_value1",
  "gsi1sk": "$entity_version#attr2_value2#attr3_value6"
}

If a user attempts to update the attribute attr2, then ElectroDB has no way of knowing value of the attribute attr3 or if forming the composite key without it would overwrite its value. The same problem exists if a user were to update attr3, ElectroDB cannot update the key without knowing each composite attribute's value.

In the event that a secondary index includes composite values from the table's primary index, ElectroDB will draw from the values supplied for the update key to address index gaps in the secondary index. For example:

For the defined indexes:

{
  "accessPattern1": {
    "pk": {
      "field": "pk",
      "composite": ["attr1"]
    },
    "sk": {
      "field": "sk",
      "composite": ["attr2"]
    }
  },
  "accessPattern2": {
    "index": "my-gsi",
    "pk": {
      "field": "gsi1pk",
      "composite": ["attr3"]
    },
    "sk": {
      "field": "gsi1sk",
      "composite": ["attr2", "attr4"]
    }
  }
}

A user could update attr4 alone because ElectroDB is able to leverage the value for attr2 from values supplied to the update() method:

entity.update({ attr1: "value1", attr2: "value2" })
  .set({ attr4: "value4" })
  .go();

{
  "UpdateExpression": "SET #attr4 = :attr4_u0, #gsi1sk = :gsi1sk_u0, #attr1 = :attr1_u0, #attr2 = :attr2_u0",
  "ExpressionAttributeNames": {
    "#attr4": "attr4",
    "#gsi1sk": "gsi1sk",
    "#attr1": "attr1",
    "#attr2": "attr2"
  },
  "ExpressionAttributeValues": {
    ":attr4_u0": "value6",
    // This index was successfully built
    ":gsi1sk_u0": "$update-edgecases_1#attr2_value2#attr4_value6",
    ":attr1_u0": "value1",
    ":attr2_u0": "value2"
  },
  "TableName": "test_table",
  "Key": { 
    "pk": "$service#attr1_value1", 
    "sk": "$entity_version#attr2_value2" 
  }
}

Note: Included in the update are all attributes from the table's primary index. These values are automatically included on all updates in the event an update results in an insert.

Update Method: Set

The set() method will accept all attributes defined on the model. Provide a value to apply or replace onto the item.

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .set({category: "food/meal"})
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "SET #category = :category",
  "ExpressionAttributeNames": {
    "#category": "category"
  },
  "ExpressionAttributeValues": {
    ":category_w1": "food/coffee",
    ":category": "food/meal"
  },
  "TableName": "StoreDirectory",
  "Key": {
    "pk": "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_f34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category_w1"
}

Update Method: Remove

The remove() method will accept all attributes defined on the model. Unlike most other update methods, the remove() method accepts an array with the names of the attributes that should be removed.

NOTE that the attribute property required functions as a sort of NOT NULL flag. Because of this, if a property exists as required:true it will not be possible to remove that property in particular. If the attribute is a property is on "map", and the "map" is not required, then the "map" can be removed.

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .remove(["category"])
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "REMOVE #category",
  "ExpressionAttributeNames": {
    "#category": "category"
  },
  "ExpressionAttributeValues": {
    ":category0": "food/coffee"
  },
  "TableName": "StoreDirectory",
  "Key": {
    "pk": "$mallstoredirectory#cityid_atlanta#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_a34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category0"
}

Update Method: Add

The add() method will accept attributes with type number, set, and any defined on the model. In the case of a number attribute, provide a number to add to the existing attribute's value on the item.

If the attribute is defined as any, the syntax compatible with the attribute type set will be used. For this reason, do not use the attribute type any to represent a number.

const newTenant = client.createSet("larry");

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .add({
      rent: 100,         // "number" attribute
      tenant: ["larry"]  // "set" attribute
    })
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "SET #rent = #rent + :rent0 ADD #tenant :tenant0",
  "ExpressionAttributeNames": {
    "#category": "category",
    "#rent": "rent",
    "#tenant": "tenant"
  },
  "ExpressionAttributeValues": {
    ":category0": "food/coffee",
    ":rent0": 100,
    ":tenant0": ["larry"]
  },
  "TableName": "StoreDirectory",
  "Key": {
    "pk": "$mallstoredirectory#cityid_atlanta#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_a34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category0"
}

Update Method: Subtract

The subtract() method will accept attributes with type number. In the case of a number attribute, provide a number to subtract from the existing attribute's value on the item.

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .subtract({deposit: 500})
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "SET #deposit = #deposit - :deposit0",
  "ExpressionAttributeNames": {
    "#category": "category",
    "#deposit": "deposit"
  },
  "ExpressionAttributeValues": {
    ":category0": "food/coffee",
    ":deposit0": 500
  },
  "TableName": "StoreDirectory",
  "Key": {
    "pk": "$mallstoredirectory#cityid_atlanta#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_a34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category0"
}

Update Method: Append

The append() method will accept attributes with type any. This is a convenience method for working with DynamoDB lists, and is notably different that set because it will add an element to an existing array, rather than overwrite the existing value.

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .append({
      rentalAgreement: [{
        type: "ammendment", 
        detail: "no soup for you"
      }]
    })
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "SET #rentalAgreement = list_append(#rentalAgreement, :rentalAgreement0)",
  "ExpressionAttributeNames": {
    "#category": "category",
    "#rentalAgreement": "rentalAgreement"
  },
  "ExpressionAttributeValues": {
    ":category0": "food/coffee",
    ":rentalAgreement0": [
      {
        "type": "ammendment",
        "detail": "no soup for you"
      }
    ]
  },
  "TableName": "StoreDirectory",
  "Key": {
    "pk": "$mallstoredirectory#cityid_atlanta#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_a34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category0"
}

Update Method: Delete

The delete() method will accept attributes with type any or set . This operation removes items from a the contract attribute, defined as a set attribute.

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .delete({contact: ['555-345-2222']})
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "DELETE #contact :contact0",
  "ExpressionAttributeNames": {
    "#category": "category",
    "#contact": "contact"
  },
  "ExpressionAttributeValues": {
    ":category0": "food/coffee",
    ":contact0": "555-345-2222"
  },
  "TableName": "StoreDirectory",
  "Key": {
    "pk": "$mallstoredirectory#cityid_atlanta#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_a34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category0"
}

Update Method: Data

The data() allows for different approach to updating your item, by accepting a callback with a similar argument signature to the where clause.

The callback provided to the data method is injected with an attributes object as the first parameter, and an operations object as the second parameter. All operations accept an attribute from the attributes object as a first parameter, and optionally accept a second value parameter.

As mentioned above, this method is functionally similar to the where clause with one exception: The callback provided to data() is not expected to return a value. When you invoke an injected operation method, the side effects are applied directly to update expression you are building.

operationexampleresultdescription
setset(category, value)#category = :category0Add or overwrite existing value
addadd(tenant, name)#tenant :tenant1Add value to existing set attribute (used when provided attribute is of type any or set)
addadd(rent, amount)#rent = #rent + :rent0Mathematically add given number to existing number on record
subtractsubtract(deposit, amount)#deposit = #deposit - :deposit0Mathematically subtract given number from existing number on record
removeremove(petFee)#petFeeRemove attribute/property from item
appendappend(rentalAgreement, amendment)#rentalAgreement = list_append(#rentalAgreement, :rentalAgreement0)Add element to existing list attribute
deletedelete(tenant, name)#tenant :tenant1Remove item from existing set attribute
deldel(tenant, name)#tenant :tenant1Alias for delete operation
namename(rent)#rentReference another attribute's name, can be passed to other operation that allows leveraging existing attribute values in calculating new values
valuevalue(rent, value):rent1Create a reference to a particular value, can be passed to other operation that allows leveraging existing attribute values in calculating new values
await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .data((a, o) => {
        const newTenant = a.value(attr.tenant, "larry");
        o.set(a.category, "food/meal");   // electrodb "enum"   -> dynamodb "string"
        o.add(a.tenant, newTenant);       // electrodb "set"    -> dynamodb "set"
        o.add(a.rent, 100);               // electrodb "number" -> dynamodb "number"
        o.subtract(a.deposit, 200);       // electrodb "number" -> dynamodb "number"
        o.remove(a.leaseEndDate);         // electrodb "string" -> dynamodb "string"
        o.append(a.rentalAgreement, [{    // electrodb "list"   -> dynamodb "list"
            type: "ammendment",           // electrodb "map"    -> dynamodb "map"
            detail: "no soup for you"
        }]);
        o.delete(a.tags, ['coffee']);     // electrodb "set"    -> dynamodb "set"
        o.del(a.contact, '555-345-2222'); // electrodb "string" -> dynamodb "string"
        o.add(a.fees, op.name(a.petFee)); // electrodb "number" -> dynamodb "number"
        o.add(a.leaseHolders, newTenant); // electrodb "set"    -> dynamodb "set"
    })
    .where((attr, op) => op.eq(attr.category, "food/coffee"))
    .go()

// Equivalent Params:
{
  "UpdateExpression": "SET #category = :category_u0, #rent = #rent + :rent_u0, #deposit = #deposit - :deposit_u0, #rentalAgreement = list_append(#rentalAgreement, :rentalAgreement_u0), #totalFees = #totalFees + #petFee REMOVE #leaseEndDate, #gsi2sk ADD #tenant :tenant_u0, #leaseHolders :tenant_u0 DELETE #tags :tags_u0, #contact :contact_u0",
  "ExpressionAttributeNames": {
  "#category": "category",
    "#tenant": "tenant",
    "#rent": "rent",
    "#deposit": "deposit",
    "#leaseEndDate": "leaseEndDate",
    "#rentalAgreement": "rentalAgreement",
    "#tags": "tags",
    "#contact": "contact",
    "#totalFees": "totalFees",
    "#petFee": "petFee",
    "#leaseHolders": "leaseHolders",
    "#gsi2sk": "gsi2sk"
  },
  "ExpressionAttributeValues": {
    ":category0": "food/coffee",
    ":category_u0": "food/meal",
    ":tenant_u0": ["larry"],
    ":rent_u0": 100,
    ":deposit_u0": 200,
    ":rentalAgreement_u0": [{
      "type": "amendment",
      "detail": "no soup for you"
    }],
    ":tags_u0": ["coffee"], // <- DynamoDB Set
    ":contact_u0": ["555-345-2222"], // <- DynamoDB Set 
    },
  "TableName": "electro",
  "Key": {
    "pk": `$mallstoredirectory#cityid_12345#mallid_eastpointe`,
    "sk": "$mallstore_1#buildingid_a34#storeid_lattelarrys"
  },
  "ConditionExpression": "#category = :category0"
}

Update Method: Complex Data Types

ElectroDB supports updating DynamoDB's complex types (list, map, set) with all of its Update Methods.

When using the chain methods set, add, subtract, remove, append, and delete, you can access map properties, list elements, and set items by supplying the json path of the property as the name of the attribute.

The data() method also allows for working with complex types. Unlike using the update chain methods, the data() method ensures type safety when using TypeScript. When using the injected attributes object, simply drill into the attribute itself to apply your update directly to the required object.

The following are examples on how update complex attributes, using both with chain methods and the data() method.

Example 1: Set property on a map attribute

Specifying a property on a map attribute is expressed with dot notation.

// via Chain Method
await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .set({'mapAttribute.mapProperty':  "value"})
    .go();

// via Data Method 
await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .data(({mapAttribute}, {set}) => set(mapAttribute.mapProperty, "value"))
    .go()

Example 2: Removing an element from a list attribute

Specifying an index on a list attribute is expressed with square brackets containing the element's index number.

// via Chain Method
await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .remove(['listAttribute[0]'])
    .go();

// via Data Method 
await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .data(({listAttribute}, {remove}) => remove(listAttribute[0]))
    .go();

Example 3: Adding an item to a set attribute, on a map attribute, that is an element of a list attribute

All other complex structures are simply variations on the above two examples.

// Set values must use the DocumentClient to create a `set`
const newSetValue = StoreLocations.client.createSet("setItemValue"); 

// via Data Method 
await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .add({'listAttribute[1].setAttribute': newSetValue})
    .go();

await StoreLocations
    .update({cityId, mallId, storeId, buildingId})
    .data(({listAttribute}, {add}) => {
        add(listAttribute[1].setAttribute, newSetValue)
    })
    .go();

Scan Records

When scanning for rows, you can use filters the same as you would any query. For more information on filters, see the Where section.

Note: Scan functionality will be scoped to your Entity. This means your results will only include records that match the Entity defined in the model.

await StoreLocations.scan
    .where(({category}, {eq}) => `
        ${eq(category, "food/coffee")} OR ${eq(category, "spite store")}  
    `)
    .where(({leaseEndDate}, {between}) => `
        ${between(leaseEndDate, "2020-03", "2020-04")}
    `)
    .go()

// Equivalent Params:
{
  "TableName": "StoreDirectory",
  "ExpressionAttributeNames": {
    "#category": "category",
    "#leaseEndDate": "leaseEndDate",
    "#pk": "pk",
    "#sk": "sk",
    "#__edb_e__": "__edb_e__",
    "#__edb_v__": "__edb_v__"
  },
  "ExpressionAttributeValues": {
    ":category_w1": "food/coffee",
    ":category_w2": "spite store",
    ":leaseEndDate_w1": "2020-03",
    ":leaseEndDate_w2": "2020-04",
    ":pk": "$mallstoredirectory#cityid_",
    ":sk": "$mallstore_1#buildingid_",
    ":__edb_e__": "MallStore",
    ":__edb_v__": "1"
  },
  "FilterExpression": "begins_with(#pk, :pk) AND #__edb_e__ = :__edb_e__ AND #__edb_v__ = :__edb_v__ AND begins_with(#sk, :sk) AND (#category = :category_w1 OR #category = :category_w2) AND (#leaseEndDate between :leaseEndDate_w1 and :leaseEndDate_w2)"
}

Remove Method

A convenience method for delete with ConditionExpression that the item being deleted exists. Provide all Table Index composite attributes in an object to the remove method to remove the record.

await StoreLocations.remove({
    storeId: "LatteLarrys", 
    mallId: "EastPointe", 
    buildingId: "F34", 
    cityId: "Atlanta1"
}).go();

// Equivalent Params:
// {
//   Key: {
//     pk: "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
//     sk: "$mallstore_1#buildingid_f34#storeid_lattelarrys"
//   },
//   TableName: 'StoreDirectory'
//   ConditionExpression: 'attribute_exists(pk) AND attribute_exists(sk)'
// }

Patch Record

In DynamoDB, update operations by default will insert a record if record being updated does not exist. In ElectroDB, the patch method will utilize the attribute_exists() parameter dynamically to ensure records are only "patched" and not inserted when updating.

For more detail on how to use the patch() method, see the section Update Record to see all the transferable requirements and capabilities available to patch().

Create Record

In DynamoDB, put operations by default will overwrite a record if record being updated does not exist. In ElectroDB, the patch method will utilize the attribute_not_exists() parameter dynamically to ensure records are only "created" and not overwritten when inserting new records into the table.

A Put operation will trigger the default, and set attribute callbacks when writing to DynamoDB. By default, after writing to DynamoDB, ElectroDB will format and return the record through the same process as a Get/Query, which will invoke the get callback on all included attributes. If this behaviour is not desired, use the Query Option response:"none" to return a null value.

await StoreLocations
  .create({
      cityId: "Atlanta1",
      storeId: "LatteLarrys",
      mallId: "EastPointe",
      buildingId: "BuildingA1",
      unitId: "B47",
      category: "food/coffee",
      leaseEndDate: "2020-03-22",
      rent: "4500.00"
  })
  .where((attr, op) => op.eq(attr.rent, "4500.00"))
  .go()

// Equivalent Params:
{
  "Item": {
    "cityId": "Atlanta1",
    "mallId": "EastPointe",
    "storeId": "LatteLarrys",
    "buildingId": "BuildingA1",
    "unitId": "B47",
    "category": "food/coffee",
    "leaseEndDate": "2020-03-22",
    "rent": "4500.00",
    "discount": "0.00",
    "pk": "$mallstoredirectory#cityid_atlanta1#mallid_eastpointe",
    "sk": "$mallstore_1#buildingid_buildinga1#storeid_lattelarrys",
    "gis1pk": "$mallstoredirectory#mallid_eastpointe",
    "gsi1sk": "$mallstore_1#buildingid_buildinga1#unitid_b47",
    "gis2pk": "$mallstoredirectory#storeid_lattelarrys",
    "gsi2sk": "$mallstore_1#leaseenddate_2020-03-22",
    "__edb_e__": "MallStore",
    "__edb_v__": "1"
  },
  "TableName": "StoreDirectory",
  "ConditionExpression": "attribute_not_exists(pk) AND attribute_not_exists(sk) AND #rent = :rent_w1",
  "ExpressionAttributeNames": {
    "#rent": "rent"
  },
  "ExpressionAttributeValues": {
    ":rent_w1": "4500.00"
  }
}

Find Records

DynamoDB offers three methods to query records: get, query, and scan. In ElectroDB, there is a fourth type: find. Unlike get and query, the find method does not require you to provide keys, but under the covers it will leverage the attributes provided to choose the best index to query on. Provide the find method will all properties known to match a record and ElectroDB will generate the most performant query it can to locate the results. This can be helpful with highly dynamic querying needs. If an index cannot be satisfied with the attributes provided, scan will be used as a last resort.

NOTE: The Find method is similar to the Match method with one exception: The attributes you supply directly to the .find() method will only be used to identify and fulfill your index access patterns. Any values supplied that do not contribute to a composite key will not be applied as query filters. Furthermore, if the values you provide do not resolve to an index access pattern, then a table scan will be performed. Use the where() chain method to further filter beyond keys, or use Match for the convenience of automatic filtering based on the values given directly to that method.

await StoreLocations.find({
    mallId: "EastPointe",
    buildingId: "BuildingA1",
}).go()

// Equivalent Params:
{
  "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)",
  "TableName": "StoreDirectory",
  "ExpressionAttributeNames": {
    "#mallId": "mallId",
    "#buildingId": "buildingId",
    "#pk": "gis1pk",
    "#sk1": "gsi1sk"
  },
  "ExpressionAttributeValues": {
    ":mallId1": "EastPointe",
    ":buildingId1": "BuildingA1",
    ":pk": "$mallstoredirectory#mallid_eastpointe",
    ":sk1": "$mallstore_1#buildingid_buildinga1#unitid_"
  },
  "IndexName": "gis1pk-gsi1sk-index",
}

Match Records

Match is a convenience method based off of ElectroDB's find method. Similar to Find, Match does not require you to provide keys, but under the covers it will leverage the attributes provided to choose the best index to query on.

Match differs from Find in that it will also include all supplied values into a query filter.

await StoreLocations.find({
    mallId: "EastPointe",
    buildingId: "BuildingA1",
    leaseEndDate: "2020-03-22",
    rent: "1500.00"
}).go()

// Equivalent Params:
{
  "KeyConditionExpression": "#pk = :pk and begins_with(#sk1, :sk1)",
  "TableName": "StoreDirectory",
  "ExpressionAttributeNames": {
    "#mallId": "mallId",
    "#buildingId": "buildingId",
    "#leaseEndDate": "leaseEndDate",
    "#rent": "rent",
    "#pk": "gis1pk",
    "#sk1": "gsi1sk"
  },
  "ExpressionAttributeValues": {
    ":mallId1": "EastPointe",
    ":buildingId1": "BuildingA1",
    ":leaseEndDate1": "2020-03-22",
    ":rent1": "1500.00",
    ":pk": "$mallstoredirectory#mallid_eastpointe",
    ":sk1": "$mallstore_1#buildingid_buildinga1#unitid_"
  },
  "IndexName": "gis1pk-gsi1sk-index",
  "FilterExpression": "#mallId = :mallId1 AND#buildingId = :buildingId1 AND#leaseEndDate = :leaseEndDate1 AND#rent = :rent1"
}

After invoking the Access Pattern with the required Partition Key Composite Attributes, you can now choose what Sort Key Composite Attributes are applicable to your query. Examine the table in Sort Key Operations for more information on the available operations on a Sort Key.

Access Pattern Queries

When you define your indexes in your model, you are defining the Access Patterns of your entity. The composite attributes you choose, and their order, ultimately define the finite set of index queries that can be made. The more you can leverage these index queries the better from both a cost and performance perspective.

Unlike Partition Keys, Sort Keys can be partially provided. We can leverage this to multiply our available access patterns and use the Sort Key Operations: begins, between, lt, lte, gt, and gte. These queries are more performant and cost-effective than filters. The costs associated with DynamoDB directly correlate to how effectively you leverage Sort Key Operations.

For a comprehensive and interactive guide to build queries please visit this runkit: https://runkit.com/tywalch/electrodb-building-queries.

Begins With Queries

One important consideration when using Sort Key Operations to make is when to use and not to use "begins".

It is possible to supply partially supply Sort Key composite attributes. Sort Key attributes must be provided in the order they are defined, but it's possible to provide only a subset of the Sort Key Composite Attributes to ElectroDB. By default, when you supply a partial Sort Key in the Access Pattern method, ElectroDB will create a beginsWith query. The difference between that and using .begins() is that, with a .begins() query, ElectroDB will not post-pend the next composite attribute's label onto the query.

The difference is nuanced and makes better sense with an example, but the rule of thumb is that data passed to the Access Pattern method should represent values you know strictly equal the value you want.

The following examples will use the following Access Pattern definition for units:

{
  "units": {
    "index": "gis1pk-gsi1sk-index",
    "pk": {
      "field": "gis1pk",
      "composite attributes": [
        "mallId"
      ]
    },
    "sk": {
      "field": "gsi1sk",
      "composite attributes": [
        "buildingId",
        "unitId"
      ]
    }
  }
}

The names you have given to your indexes on your entity model/schema express themselves as "Access Pattern" methods on your Entity's query object:

// Example #1, access pattern `units`
StoreLocations.query.units({mallId, buildingId}).go();
// -----------------------^^^^^^^^^^^^^^^^^^^^^^

Data passed to the Access Pattern method is considered to be full, known, data. In the above example, we are saying we know the mallId, buildingId and unitId.

Alternatively, if you only know the start of a piece of data, use .begins():

// Example #2
StoreLocations.query.units({mallId}).begins({buildingId}).go();
// ---------------------------------^^^^^^^^^^^^^^^^^^^^^

Data passed to the .begins() method is considered to be partial data. In the second example, we are saying we know the mallId and buildingId, but only know the beginning of unitId.

For the above queries we see two different sort keys:

  1. "$mallstore_1#buildingid_f34#unitid_"
  2. "$mallstore_1#buildingid_f34"

The first example shows how ElectroDB post-pends the label of the next composite attribute (unitId) on the Sort Key to ensure that buildings such as "f340" are not included in the query. This is useful to prevent common issues with overloaded sort keys like accidental over-querying.

The second example allows you to make queries that do include buildings such as "f340" or "f3409" or "f340356346".

For these reasons it is important to consider that attributes passed to the Access Pattern method are considered to be full, known, data.

Collection Chains

Collections allow you to query across Entities. They can be used on Service instance.

const DynamoDB = require("aws-sdk/clients/dynamodb");
const table = "projectmanagement";
const client = new DynamoDB.DocumentClient();

const employees = new Entity({
  model: {
    entity: "employees",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    employeeId: {
      type: "string"
    },
    organizationId: {
      type: "string"
    },
    name: {
      type: "string"
    },
    team: {
      type: ["jupiter", "mercury", "saturn"]
    }
  },
  indexes: {
    staff: {
      pk: {
        field: "pk",
        composite: ["organizationId"]
      },
      sk: {
        field: "sk",
        composite: ["employeeId"]
      }
    },
    employee: {
      collection: "assignments",
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: [],
      },
    }
  }
}, { client, table })

const tasks = new Entity({
  model: {
    entity: "tasks",
    version: "1",
    service: "taskapp",
  },
  attributes: {
    taskId: {
      type: "string"
    },
    employeeId: {
      type: "string"
    },
    projectId: {
      type: "string"
    },
    title: {
      type: "string"
    },
    body: {
      type: "string"
    }
  },
  indexes: {
    project: {
      pk: {
        field: "pk",
        composite: ["projectId"]
      },
      sk: {
        field: "sk",
        composite: ["taskId"]
      }
    },
    assigned: {
      collection: "assignments",
      index: "gsi2",
      pk: {
        field: "gsi2pk",
        composite: ["employeeId"],
      },
      sk: {
        field: "gsi2sk",
        composite: [],
      },
    }
  }
}, { client, table });

const TaskApp = new Service({employees, tasks});

Available on your Service are two objects: entites and collections. Entities available on entities have the same capabilities as they would if created individually. When a Model added to a Service with join however, its Collections are automatically added and validated with the other Models joined to that Service. These Collections are available on collections.

TaskApp.collections.assignments({employeeId: "JExotic"}).params();  

// Results
{
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  ExpressionAttributeValues: { ':pk': '$taskapp_1#employeeid_joeexotic', ':sk1': '$assignments' },
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  IndexName: 'gsi3'
}

Collections do not have the same query functionality and as an Entity, though it does allow for inline filters like an Entity. The attributes available on the filter object include all attributes across entities.

TaskApp.collections
    .assignments({employee: "CBaskin"})
    .filter((attributes) => `
        ${attributes.project.notExists()} OR ${attributes.project.contains("murder")}
    `)

// Results
{
  TableName: 'projectmanagement',
  ExpressionAttributeNames: { '#project': 'project', '#pk': 'gsi2pk', '#sk1': 'gsi2sk' },
  ExpressionAttributeValues: {
    ':project1': 'murder',
    ':pk': '$taskapp_1#employeeid_carolbaskin',
    ':sk1': '$assignments'
  },
  KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
  IndexName: 'gsi2',
  FilterExpression: '\n\t\tattribute_not_exists(#project) OR contains(#project, :project1)\n\t'
}

Execute Queries

Lastly, all query chains end with either a .go() or a .params() method invocation. These will either execute the query to DynamoDB (.go()) or return formatted parameters for use with the DynamoDB docClient (.params()).

Both .params() and .go() take a query configuration object which is detailed more in the section Query Options.

Params

The params method ends a query chain, and synchronously formats your query into an object ready for the DynamoDB docClient.

For more information on the options available in the config object, checkout the section Query Options.

let config = {};
let stores = MallStores.query
    .leases({ mallId })
    .between(
      { leaseEndDate:  "2020-06-01" }, 
      { leaseEndDate:  "2020-07-31" })
    .filter(attr) => attr.rent.lte("5000.00"))
    .params(config);

// Results:
{
  IndexName: 'idx2',
  TableName: 'electro',
  ExpressionAttributeNames: { '#rent': 'rent', '#pk': 'idx2pk', '#sk1': 'idx2sk' },
  ExpressionAttributeValues: {
    ':rent1': '5000.00',
    ':pk': '$mallstoredirectory_1#mallid_eastpointe',
    ':sk1': '$mallstore#leaseenddate_2020-06-01#rent_',
    ':sk2': '$mallstore#leaseenddate_2020-07-31#rent_'
  },
  KeyConditionExpression: '#pk = :pk and #sk1 BETWEEN :sk1 AND :sk2',
  FilterExpression: '#rent <= :rent1'
}

Go

The go method ends a query chain, and asynchronously queries DynamoDB with the client provided in the model.

For more information on the options available in the config object, check out the section Query Options.

let config = {};
let stores = MallStores.query
    .leases({ mallId })
    .between(
        { leaseEndDate:  "2020-06-01" }, 
        { leaseEndDate:  "2020-07-31" })
    .filter(({rent}) => rent.lte("5000.00"))
    .go(config);

Page

NOTE: By Default, ElectroDB queries will paginate through all results with the go() method. ElectroDB's page() method can be used to manually iterate through DynamoDB query results.

The page method ends a query chain, and asynchronously queries DynamoDB with the client provided in the model. Unlike the .go(), the .page() method returns a tuple.

The first element for a page query is the "pager": an object contains the composite attributes that make up the ExclusiveStartKey that is returned by the DynamoDB client. This is very useful in multi-tenant applications where only some composite attributes are exposed to the client, or there is a need to prevent leaking keys between entities. If there is no ExclusiveStartKey this value will be null. On subsequent calls to .page(), pass the results returned from the previous call to .page() or construct the composite attributes yourself.

The "pager" includes the associated entity's Identifiers.

NOTE: It is highly recommended to use the query option pager: "raw"" flag when using .page() with scan operations. This is because when using scan on large tables the docClient may return an ExclusiveStartKey for a record that does not belong to entity making the query (regardless of the filters set). In these cases ElectroDB will return null (to avoid leaking the keys of other entities) when further pagination may be needed to find your records.

The second element is the results of the query, exactly as it would be returned through a query operation.

NOTE: When calling .page() the first argument is reserved for the "page" returned from a previous query, the second parameter is for Query Options. For more information on the options available in the config object, check out the section Query Options.

Entity Pagination

let [next, stores] = await MallStores.query
    .leases({ mallId })
    .page(); // no "pager" passed to `.page()`

let [pageTwo, moreStores] = await MallStores.query
    .leases({ mallId })
    .page(next, {}); // the "pager" from the first query (`next`) passed to the second query

// page:
// { 
//   storeId: "LatteLarrys", 
//   mallId: "EastPointe", 
//   buildingId: "BuildingA1", 
//   unitId: "B47"
//   __edb_e__: "MallStore",
//   __edb_v__: "version" 
// }

// stores
// [{
//   mall: '3010aa0d-5591-4664-8385-3503ece58b1c',
//   leaseEnd: '2020-01-20',
//   sector: '7d0f5c19-ec1d-4c1e-b613-a4cc07eb4db5',
//   store: 'MNO',
//   unit: 'B5',
//   id: 'e0705325-d735-4fe4-906e-74091a551a04',
//   building: 'BuildingE',
//   category: 'food/coffee',
//   rent: '0.00'
// },
// {
//   mall: '3010aa0d-5591-4664-8385-3503ece58b1c',
//   leaseEnd: '2020-01-20',
//   sector: '7d0f5c19-ec1d-4c1e-b613-a4cc07eb4db5',
//   store: 'ZYX',
//   unit: 'B9',
//   id: 'f201a1d3-2126-46a2-aec9-758ade8ab2ab',
//   building: 'BuildingI',
//   category: 'food/coffee',
//   rent: '0.00'
// }]

Service Pagination

NOTE: By Default, ElectroDB will paginate through all results with the query() method. ElectroDB's page() method can be used to manually iterate through DynamoDB query results.

Pagination with services is also possible. Similar to Entity Pagination, calling the .page() method returns a [pager, results] tuple. Also, similar to pagination on Entities, the pager object returned by default is a deconstruction of the returned LastEvaluatedKey.

Pager Query Options

The .page() method also accepts Query Options just like the .go() and .params() methods. Unlike those methods, however, the .page() method accepts Query Options as the second parameter (the first parameter is reserved for the "pager").

A notable Query Option, that is available only to the .page() method, is an option called pager. This property defines the post-processing ElectroDB should perform on a returned LastEvaluatedKey, as well as how ElectroDB should interpret an incoming pager, to use as an ExclusiveStartKey.

NOTE: Because the "pager" object is destructured from the keys DynamoDB returns as the LastEvaluatedKey, these composite attributes differ from the record's actual attribute values in one important way: Their string values will all be lowercase. If you intend to use these attributes in ways where their casing will matter (e.g. in a where filter), keep in mind this may result in unexpected outcomes.

The three options for the query option pager are as follows:

// LastEvaluatedKey
{
  pk: '$taskapp#country_united states of america#state_oregon',
  sk: '$offices_1#city_power#zip_34706#office_mobile branch',
  gsi1pk: '$taskapp#office_mobile branch',
  gsi1sk: '$workplaces#offices_1'
}

"named" (default): By default, ElectroDB will deconstruct the LastEvaluatedKey returned by the DocClient into it's individual composite attribute parts. The "named" option, chosen by default, also includes the Entity's column "identifiers" -- this is useful with Services where destructured pagers may be identical between more than one Entity in that Service.

// {pager: "named"} | {pager: undefined} 
{  
  "city": "power",
  "country": "united states of america",
  "state": "oregon",
  "zip": "34706",
  "office": "mobile branch",
  "__edb_e__": "offices",
  "__edb_v__": "1"
}

"item": Similar to "named", however without the Entity's "identifiers". If two Entities with a service have otherwise identical index definitions, using the "item" pager option can result in errors while paginating a Collection. If this is not a concern with your Service, or you are paginating with only an Entity, this option could be preferable because it has fewer properties.

// {pager: "item"} 
{  
  "city": "power",
  "country": "united states of america",
  "state": "oregon",
  "zip": "34706",
  "office": "mobile branch",
}

"raw": The "raw" option returns the LastEvaluatedKey as it was returned by the DynamoDB DocClient.

// {pager: "raw"} 
{
  pk: '$taskapp#country_united states of america#state_oregon',
  sk: '$offices_1#city_power#zip_34706#office_mobile branch',
  gsi1pk: '$taskapp#office_mobile branch',
  gsi1sk: '$workplaces#offices_1'
}

Pagination Example

Simple pagination example:

async function getAllStores(mallId) {
  let stores = [];
  let pager = null;

  do {
    let [next, results] = await MallStores.query
      .leases({ mallId })
      .page(pager);
    stores = [...stores, ...results]; 
    pager = next;
  } while(pager !== null);
  
  return stores;
} 

Query Examples

For a comprehensive and interactive guide to build queries please visit this runkit: https://runkit.com/tywalch/electrodb-building-queries.

const cityId = "Atlanta1";
const mallId = "EastPointe";
const storeId = "LatteLarrys";
const unitId = "B24";
const buildingId = "F34";
const june = "2020-06";
const july = "2020-07"; 
const discount = "500.00";
const maxRent = "2000.00";
const minRent = "5000.00";

// Lease Agreements by StoreId
await StoreLocations.query.leases({storeId}).go()

// Lease Agreement by StoreId for March 22nd 2020
await StoreLocations.query.leases({storeId, leaseEndDate: "2020-03-22"}).go()

// Lease agreements by StoreId for 2020
await StoreLocations.query.leases({storeId}).begins({leaseEndDate: "2020"}).go()

// Lease Agreements by StoreId after March 2020
await StoreLocations.query.leases({storeId}).gt({leaseEndDate: "2020-03"}).go()

// Lease Agreements by StoreId after, and including, March 2020
await StoreLocations.query.leases({storeId}).gte({leaseEndDate: "2020-03"}).go()

// Lease Agreements by StoreId before 2021
await StoreLocations.query.leases({storeId}).lt({leaseEndDate: "2021-01"}).go()

// Lease Agreements by StoreId before February 2021
await StoreLocations.query.leases({storeId}).lte({leaseEndDate: "2021-02"}).go()

// Lease Agreements by StoreId between 2010 and 2020
await StoreLocations.query
    .leases({storeId})
    .between(
        {leaseEndDate: "2010"}, 
        {leaseEndDate: "2020"})
    .go()

// Lease Agreements by StoreId after, and including, 2010 in the city of Atlanta and category containing food
await StoreLocations.query
    .leases({storeId})
    .gte({leaseEndDate: "2010"})
    .where((attr, op) => `
        ${op.eq(attr.cityId, "Atlanta1")} AND ${op.contains(attr.category, "food")}
    `)
    .go()
    
// Rents by City and Store who's rent discounts match a certain rent/discount criteria
await StoreLocations.query
    .units({mallId})
    .begins({leaseEndDate: june})
    .rentDiscount(discount, maxRent, minRent)
    .go()

// Stores by Mall matching a specific category
await StoreLocations.query
    .units({mallId})
    .byCategory("food/coffee")
    .go()

Query Options

Query options can be added the .params(), .go() and .page() to change query behavior or add customer parameters to a query.

By default, ElectroDB enables you to work with records as the names and properties defined in the model. Additionally, it removes the need to deal directly with the docClient parameters which can be complex for a team without as much experience with DynamoDB. The Query Options object can be passed to both the .params() and .go() methods when building you query. Below are the options available:

{
  params?: object;
  table?: string;
  raw?: boolean;
  includeKeys?: boolean;
  pager?: "raw" | "named" | "item";
  originalErr?: boolean;
  concurrent?: number;
  unprocessed?: "raw" | "item";
  response?: "default" | "none" | "all_old" | "updated_old" | "all_new" | "updated_new";
  ignoreOwnership?: boolean;
  limit?: number;
  pages?: number;
};
OptionDefaultDescription
params{}Properties added to this object will be merged onto the params sent to the document client. Any conflicts with ElectroDB will favor the params specified here.
table(from constructor)Use a different table than the one defined in the Service Options
rawfalseReturns query results as they were returned by the docClient.
includeKeysfalseBy default, ElectroDB does not return partition, sort, or global keys in its response.
pager"named"Used in with pagination (.pages()) calls to override ElectroDBs default behaviour to break apart LastEvaluatedKeys records into composite attributes. See more detail about this in the sections for Pager Query Options.
originalErrfalseBy default, ElectroDB alters the stacktrace of any exceptions thrown by the DynamoDB client to give better visibility to the developer. Set this value equal to true to turn off this functionality and return the error unchanged.
concurrent1When performing batch operations, how many requests (1 batch operation == 1 request) to DynamoDB should ElectroDB make at one time. Be mindful of your DynamoDB throughput configurations
unprocessed"item"Used in batch processing to override ElectroDBs default behaviour to break apart DynamoDBs Unprocessed records into composite attributes. See more detail about this in the sections for BatchGet, BatchDelete, and BatchPut.
response"default"Used as a convenience for applying the DynamoDB parameter ReturnValues. The options here are the same as the parameter values for the DocumentClient except lowercase. The "none" option will cause the method to return null and will bypass ElectroDB's response formatting -- useful if formatting performance is a concern.
ignoreOwnershipfalseBy default, ElectroDB interrogates items returned from a query for the presence of matching entity "identifiers". This helps to ensure other entities, or other versions of an entity, are filtered from your results. If you are using ElectroDB with an existing table/dataset you can turn off this feature by setting this property to true.
limitnoneA target for the number of items to return from DynamoDB. If this option is passed, Queries on entities and through collections will paginate DynamoDB until this limit is reached or all items for that query have been returned.
pagesHow many DynamoDB pages should a query iterate through before stopping. By default ElectroDB paginate through all results for your query.

Errors:

Error CodeDescription
1000sConfiguration Errors
2000sInvalid Queries
3000sUser Defined Errors
4000sDynamoDB Errors
5000sUnexpected Errors

No Client Defined On Model

Code: 1001

Why this occurred: If a DynamoDB DocClient is not passed to the constructor of an Entity or Service (client), ElectroDB will be unable to query DynamoDB. This error will only appear when a query(using go()) is made because ElectroDB is still useful without a DocClient through the use of it's params() method.

What to do about it: For an Entity be sure to pass the DocClient as the second param to the constructor:

new Entity(schema, {client})

For a Service, the client is passed the same way, as the second param to the constructor:

new Service("", {client});

Invalid Identifier

Code: 1002

Why this occurred: You tried to modify the entity identifier on an Entity.

What to do about it: Make sure you have spelled the identifier correctly or that you actually passed a replacement.

Invalid Key Composite Attribute Template

Code: 1003

Why this occurred: You are trying to use the custom Key Composite Attribute Template, and the format you passed is invalid.

What to do about it: Checkout the section on [Composite Attribute Templates](#composite attribute-templates) and verify your template conforms to the rules detailed there.

Duplicate Indexes

Code: 1004

Why this occurred: Your model contains duplicate indexes. This could be because you accidentally included an index twice or even forgot to add an index name on a secondary index, which would be interpreted as "duplicate" to the Table's Primary index.

What to do about it: Double-check the index names on your model for duplicate indexes. The error should specify which index has been duplicated. It is also possible that you have forgotten to include an index name. Each table must have at least one Table Index (which does not include an index property in ElectroDB), but all Secondary and Local indexes must include an index property with the name of that index as defined on the table.

{
  indexes: {
    index1: {
      index: "idx1", // <-- duplicate "idx1"
      pk: {},
      sk: {}
    },
    index2: {
      index: "idx1", // <-- duplicate "idx1"
      pk: {},
      sk: {}
    }
  }
}

Collection Without An SK

Code: 1005

Why this occurred: You have added a collection to an index that does not have an SK. Because Collections are used to help query across entities via the Sort Key, not having a Sort Key on an index defeats the purpose of a Collection.

What to do about it: If your index does have a Sort Key, but you are unsure of how to inform electro without setting composite attributes to the SK, add the SK object to the index and use an empty array for Composite Attributes:

// ElectroDB interprets as index *not having* an SK.
{
  indexes: {
    myIndex: {
      pk: {
        field: "pk",
        composite: ["id"]
      }
    }
  }
}

// ElectroDB interprets as index *having* SK, but this model doesnt attach any composite attributes to it.
{
  indexes: {
    myIndex: {
      pk: {
        field: "pk",
        composite: ["id"]
      },
      sk: {
        field: "sk",
        composite: []
      }
    }
  }
}

Duplicate Collections

Code: 1006

Why this occurred: You have assigned the same collection name to multiple indexes. This is not allowed because collection names must be unique.

What to do about it: Determine a new naming scheme

Missing Primary Index

Code: 1007

Why this occurred: DynamoDB requires the definition of at least one Primary Index on the table. In Electro this is defined as an Index without an index property. Each model needs at least one, and the composite attributes used for this index must ensure each composite represents a unique record.

What to do about it: Identify the index you're using as the Primary Index and ensure it does not have an index property on its definition.

// ElectroDB interprets as the Primary Index because it lacks an `index` property.
{
  indexes: {
    myIndex: {
      pk: {
        field: "pk",
        composite: ["org"]
      },
      sk: {
        field: "sk",
        composite: ["id"]
      }
    }
  }
}

// ElectroDB interprets as a Global Secondary Index because it has an `index` property.
{
  indexes: {
    myIndex: {
      index: "gsi1"
      pk: {
        field: "gsipk1",
        composite: ["org"]
      },
      sk: {
        field: "gsisk1",
        composite: ["id"]
      }
    }
  }
}

Invalid Attribute Definition

Code: 1008

Why this occurred: Some attribute on your model has an invalid configuration.

What to do about it: Use the error to identify which column needs to examined, double-check the properties on that attribute. Checkout the section on Attributes for more information on how they are structured.

Invalid Model

Code: 1009

Why this occurred: Some properties on your model are missing or invalid.

What to do about it: Checkout the section on Models to verify your model against what is expected.

Invalid Options

Code: 1010

Why this occurred: Some properties on your options object are missing or invalid.

What to do about it: Checkout the section on Model/Service Options to verify your model against what is expected.

Duplicate Index Fields

Code: 1014

Why this occurred: An Index in your model references the same field twice across indexes. The field property in the definition of an index is a mapping to the name of the field assigned to the PK or SK of an index.

What to do about it: This is likely a typo, if not double-check the names of the fields you assigned to be the PK and SK of your index, these field names must be unique.

Duplicate Index Composite Attributes

Code: 1015

Why this occurred: Within one index you tried to use the same composite attribute in both the PK and SK. A composite attribute may only be used once within an index. With ElectroDB it is not uncommon to use the same value as both the PK and SK when a Sort Key exists on a table -- this usually is done because some value is required in that column but for that entity it is not necessary. If this is your situation remember that ElectroDB does put a value in the SortKey even if does not include a composite attribute, checkout this section for more information.

What to do about it: Determine how you can change your access pattern to not duplicate the composite attribute. Remember that an empty array for an SK is valid.

Incompatible Key Composite Attribute Template

Code: 1017

Why this occurred: You are trying to use the custom Key Composite Attribute Template, and a Composite Attribute Array on your model, and they do not contain identical composite attributes.

What to do about it: Checkout the section on [Composite Attribute Templates](#composite attribute-templates) and verify your template conforms to the rules detailed there. Both properties must contain the same attributes and be provided in the same order.

Invalid Index With Attribute Name

Code: 1018

Why this occurred: ElectroDB's design revolves around best practices related to modeling in single table design. This includes giving indexed fields generic names. If the PK and SK fields on your table indexes also match the names of attributes on your Entity you will need to make special considerations to make sure ElectroDB can accurately map your data.

What to do about it: Checkout the section Using ElectroDB with existing data to learn more about considerations to make when using attributes as index fields.

Invalid Collection on Index With Attribute Field Names

Code: 1019

Why this occurred: Collections allow for unique access patterns to be modeled between entities. It does this by appending prefixes to your key composites. If an Entity leverages an attribute field as an index key, ElectroDB will be unable to prefix your value because that would result in modifying the value itself.

What to do about it: Checkout the section Collections to learn more about collections, as well as the section Using ElectroDB with existing data to learn more about considerations to make when using attributes as index fields.

Missing Composite Attributes

Code: 2002

Why this occurred: The current request is missing some composite attributes to complete the query based on the model definition. Composite Attributes are used to create the Partition and Sort keys. In DynamoDB Partition keys cannot be partially included, and Sort Keys can be partially include they must be at least passed in the order they are defined on the model.

What to do about it: The error should describe the missing composite attributes, ensure those composite attributes are included in the query or update the model to reflect the needs of the access pattern.

Missing Table

Code: 2003f

Why this occurred: You never specified a Table for DynamoDB to use.

What to do about it: Tables can be defined on the Service Options object when you create an Entity or Service, or if that is not known at the time of creation, it can be supplied as a Query Option and supplied on each query individually. If can be supplied on both, in that case the Query Option will override the Service Option.

Invalid Concurrency Option

Code: 2004

Why this occurred: When performing a bulk operation (Batch Get, Batch Delete Records, Batch Put Records) you can pass a Query Options called concurrent, which impacts how many batch requests can occur at the same time. Your value should pass the test of both, !isNaN(parseInt(value)) and parseInt(value) > 0.

What to do about it:
Expect this error only if you're providing a concurrency option. Double-check the value you are providing is the value you expect to be passing, and that the value passes the tests listed above.

Invalid Pages Option

Code: 2005

Why this occurred: When performing a query Query you can pass a Query Options called pages, which impacts how many DynamoDB pages a query should iterate through. Your value should pass the test of both, !isNaN(parseInt(value)) and parseInt(value) > 0.

What to do about it: Expect this error only if you're providing a pages option. Double-check the value you are providing is the value you expect to be passing, and that the value passes the tests listed above.

Invalid Limit Option

Code: 2006

Why this occurred: When performing a query Query you can pass a Query Options called limit, which impacts how many DynamoDB items a query should return. Your value should pass the test of both, !isNaN(parseInt(value)) and parseInt(value) > 0.

What to do about it: Expect this error only if you're providing a limit option. Double-check the value you are providing is the value you expect to be passing, and that the value passes the tests listed above.

Invalid Attribute

Code: 3001

Why this occurred: The value received for a validation either failed type expectations (e.g. a "number" instead of a "string"), or the user provided "validate" callback on an attribute rejected a value.

What to do about it: Examine the error itself for more precise detail on why the failure occurred. The error object itself should have a property called "fields" which contains an array of every attribute that failed validation, and a reason for each. If the failure originated from a "validate" callback, the originally thrown error will be accessible via the cause property the corresponding element within the fields array.1

Below is the type definition for an ElectroValidationError:

ElectroValidationError<T extends Error = Error> extends ElectroError {
    readonly name: "ElectroValidationError"
    readonly code: number;
    readonly date: number;
    readonly isElectroError: boolean;
    ref: {
        readonly code: number;
        readonly section: string;
        readonly name: string;
        readonly sym: unique symbol;
    }
    readonly fields: ReadonlyArray<{
        /**
         * The json path to the attribute that had a validation error
         */
        readonly field: string;

        /**
         * A description of the validation error for that attribute
         */
        readonly reason: string;

        /**
         * Index of the value passed (present only in List attribute validation errors)
         */
        readonly index: number | undefined;

        /**
         * The error thrown from the attribute's validate callback (if applicable)
         */
        readonly cause: T | undefined;
    }>
}

AWS Error

Code: 4001

Why this occurred: DynamoDB did not like something about your query.

What to do about it: By default ElectroDB tries to keep the stack trace close to your code, ideally this can help you identify what might be going on. A tip to help with troubleshooting: use .params() to get more insight into how your query is converted to DocClient params.

Unknown Errors

Invalid Last Evaluated Key

Code: 5003

Why this occurred: Likely you were calling .page() on a scan. If you weren't please make an issue and include as much detail about your query as possible.

What to do about it: When paginating with scan queries, it is highly recommended that the query option, {pager: "raw"}. This is because when using scan on large tables the docClient may return an ExclusiveStartKey for a record that does not belong to entity making the query (regardless of the filters set). In these cases ElectroDB will return null (to avoid leaking the keys of other entities) when further pagination may be needed to find your records.

// example
myModel.scan.page(null, {pager: "raw"});

No Owner For Pager

Code: 5004

Why this occurred: When using pagination with a Service, ElectroDB will try to identify which Entity is associated with the supplied pager. This error can occur when you supply an invalid pager, or when you are using a different pager option to a pager than what was used when retrieving it. Consult the section on Pagination to learn more.

What to do about it: If you are sure the pager you are passing to .page() is the same you received from .page() this could be an unexpected error. To mitigate the issue use the Query Option {pager: "raw"} and please open a support issue.

Pager Not Unique

Code: 5005

Why this occurred: When using pagination with a Service, ElectroDB will try to identify which Entity is associated with the supplied pager option. This error can occur when you supply a pager that resolves to more than one Entity. This can happen if your entities share the same composite attributes for the index you are querying on, and you are using the Query Option {pager: "item""}.

What to do about it: Because this scenario is possible with otherwise well considered/thoughtful entity models, the default pager type used by ElectroDB is "named". To avoid this error, you will need to use either the "raw" or "named" pager options for any index that could result in an ambiguous Entity owner.

Examples

Want to just play with ElectroDB instead of read about it? Try it out for yourself! https://runkit.com/tywalch/electrodb-building-queries

Employee App

For an example, lets look at the needs of application used to manage Employees. The application Looks at employees, offices, tasks, and projects.

Employee App Requirements

  1. As a Project Manager, I need to find all tasks and details on a specific employee.
  2. As a Regional Manager, I need to see all details about an office and its employees
  3. As an Employee, I need to see all my Tasks.
  4. As a Product Manager, I need to see all the tasks for a project.
  5. As a Client, I need to find a physical office close to me.
  6. As a Hiring manager, I need to find employees with comparable salaries.
  7. As HR, I need to find upcoming employee birthdays/anniversaries
  8. As HR, I need to find all the employees that report to a specific manager

App Entities

const EmployeesModel = {
    model: {
      entity: "employees",
      version: "1",
      service: "taskapp",  
    },
    attributes: {
        employee: "string",
        firstName: "string",
        lastName: "string",
        office: "string",
        title: "string",
        team: ["development", "marketing", "finance", "product"],
        salary: "string",
        manager: "string",
        dateHired: "string",
        birthday: "string",
    },
    indexes: {
        employee: {
            pk: {
                field: "pk",
                composite: ["employee"],
            },
            sk: {
                field: "sk",
                composite: [],
            },
        },
        coworkers: {
            index: "gsi1pk-gsi1sk-index",
            collection: "workplaces",
            pk: {
                field: "gsi1pk",
                composite: ["office"],
            },
            sk: {
                field: "gsi1sk",
                composite: ["team", "title", "employee"],
            },
        },
        teams: {
            index: "gsi2pk-gsi2sk-index",
            pk: {
                field: "gsi2pk",
                composite: ["team"],
            },
            sk: {
                field: "gsi2sk",
                composite: ["title", "salary", "employee"],
            },
        },
        employeeLookup: {
            collection: "assignements",
            index: "gsi3pk-gsi3sk-index",
            pk: {
                field: "gsi3pk",
                composite: ["employee"],
            },
            sk: {
                field: "gsi3sk",
                composite: [],
            },
        },
        roles: {
            index: "gsi4pk-gsi4sk-index",
            pk: {
                field: "gsi4pk",
                composite: ["title"],
            },
            sk: {
                field: "gsi4sk",
                composite: ["salary", "employee"],
            },
        },
        directReports: {
            index: "gsi5pk-gsi5sk-index",
            pk: {
                field: "gsi5pk",
                composite: ["manager"],
            },
            sk: {
                field: "gsi5sk",
                composite: ["team", "office", "employee"],
            },
        },
    },
    filters: {
        upcomingCelebrations: (attributes, startDate, endDate) => {
            let { dateHired, birthday } = attributes;
            return `${dateHired.between(startDate, endDate)} OR ${birthday.between(
                startDate,
                endDate,
            )}`;
        },
    },
};

const TasksModel = {
    model: {
        entity: "tasks",
        version: "1",
        service: "taskapp",  
    }, 
    attributes: {
        task: "string",
        project: "string",
        employee: "string",
        description: "string",
    },
    indexes: {
        task: {
            pk: {
                field: "pk",
                composite: ["task"],
            },
            sk: {
                field: "sk",
                composite: ["project", "employee"],
            },
        },
        project: {
            index: "gsi1pk-gsi1sk-index",
            pk: {
                field: "gsi1pk",
                composite: ["project"],
            },
            sk: {
                field: "gsi1sk",
                composite: ["employee", "task"],
            },
        },
        assigned: {
            collection: "assignements",
            index: "gsi3pk-gsi3sk-index",
            pk: {
                field: "gsi3pk",
                composite: ["employee"],
            },
            sk: {
                field: "gsi3sk",
                composite: ["project", "task"],
            },
        },
    },
};

const OfficesModel = {
    model: {
          entity: "offices",
          version: "1",
          service: "taskapp",  
      }, 
    attributes: {
        office: "string",
        country: "string",
        state: "string",
        city: "string",
        zip: "string",
        address: "string",
    },
    indexes: {
        locations: {
            pk: {
                field: "pk",
                composite: ["country", "state"],
            },
            sk: {
                field: "sk",
                composite: ["city", "zip", "office"],
            },
        },
        office: {
            index: "gsi1pk-gsi1sk-index",
            collection: "workplaces",
            pk: {
                field: "gsi1pk",
                composite: ["office"],
            },
            sk: {
                field: "gsi1sk",
                composite: [],
            },
        },
    },
};

Join models on a new Service called EmployeeApp

const DynamoDB = require("aws-sdk/clients/dynamodb");
const client = new DynamoDB.DocumentClient({region: "us-east-1"});
const { Service } = require("electrodb");
const table = "projectmanagement";
const EmployeeApp = new Service("EmployeeApp", { client, table });

EmployeeApp
    .join(EmployeesModel) // EmployeeApp.entities.employees
    .join(TasksModel)     // EmployeeApp.entities.tasks
    .join(OfficesModel);  // EmployeeApp.entities.tasks

Query Records

All tasks and employee information for a given employee

Fulfilling Requirement #1.

EmployeeApp.collections.assignements({employee: "CBaskin"}).go();

Returns the following:

{
    employees: [{
        employee: "cbaskin",
        firstName: "carol",
        lastName: "baskin",
        office: "big cat rescue",
        title: "owner",
        team: "cool cats and kittens",
        salary: "1,000,000",
        manager: "",
        dateHired: "1992-11-04",
        birthday: "1961-06-06",
    }],
    tasks: [{
        task: "Feed tigers",
        description: "Prepare food for tigers to eat",
        project: "Keep tigers alive",
        employee: "cbaskin"
    }, {
        task: "Fill water bowls",
        description: "Ensure the tigers have enough water",
        project: "Keep tigers alive",
        employee: "cbaskin"
    }]
}

Find all employees and office details for a given office

Fulfilling Requirement #2.

EmployeeApp.collections.workplaces({office: "big cat rescue"}).go()

Returns the following:

{
    employees: [{
        employee: "cbaskin",
        firstName: "carol",
        lastName: "baskin",
        office: "big cat rescue",
        title: "owner",
        team: "cool cats and kittens",
        salary: "1,000,000",
        manager: "",
        dateHired: "1992-11-04",
        birthday: "1961-06-06",
    }],
    offices: [{
        office: "big cat rescue",
        country: "usa",
        state: "florida",
        city: "tampa",
        zip: "12345",
        address: "123 Kitty Cat Lane"
    }]
}

Tasks for a given employee

Fulfilling Requirement #3.

EmployeeApp.entities.tasks.query.assigned({employee: "cbaskin"}).go();

Returns the following:

[
    {
        task: "Feed tigers",
        description: "Prepare food for tigers to eat",
        project: "Keep tigers alive",
        employee: "cbaskin"
    }, {
        task: "Fill water bowls",
        description: "Ensure the tigers have enough water",
        project: "Keep tigers alive",
        employee: "cbaskin"
    }
]

Tasks for a given project

Fulfilling Requirement #4.

EmployeeApp.entities.tasks.query.project({project: "Murder Carol"}).go();

Returns the following:

[
    {
        task: "Hire hitman",
        description: "Find someone to murder Carol",
        project: "Murder Carol",
        employee: "jexotic"
    }
];

Find office locations

Fulfilling Requirement #5.

EmployeeApp.entities.office.locations({country: "usa", state: "florida"}).go()

Returns the following:

[
    {
        office: "big cat rescue",
        country: "usa",
        state: "florida",
        city: "tampa",
        zip: "12345",
        address: "123 Kitty Cat Lane"
    }
]

Find employee salaries and titles

Fulfilling Requirement #6.

EmployeeApp.entities.employees
    .roles({title: "animal wrangler"})
    .lte({salary: "150.00"})
    .go()

Returns the following:

[
    {
        employee: "ssaffery",
        firstName: "saff",
        lastName: "saffery",
        office: "gw zoo",
        title: "animal wrangler",
        team: "keepers",
        salary: "105.00",
        manager: "jexotic",
        dateHired: "1999-02-23",
        birthday: "1960-07-11",
    }
]

Find employee birthdays or anniversaries

Fulfilling Requirement #7.

EmployeeApp.entities.employees
    .workplaces({office: "gw zoo"})
    .upcomingCelebrations("2020-05-01", "2020-06-01")
    .go()

Returns the following:

[
    {
        employee: "jexotic",
        firstName: "joe",
        lastName: "maldonado-passage",
        office: "gw zoo",
        title: "tiger king",
        team: "founders",
        salary: "10000.00",
        manager: "jlowe",
        dateHired: "1999-02-23",
        birthday: "1963-03-05",
    }
]

Find direct reports

Fulfilling Requirement #8.

EmployeeApp.entities.employees
    .reports({manager: "jlowe"})
    .go()

Returns the following:

[
    {
        employee: "jexotic",
        firstName: "joe",
        lastName: "maldonado-passage",
        office: "gw zoo",
        title: "tiger king",
        team: "founders",
        salary: "10000.00",
        manager: "jlowe",
        dateHired: "1999-02-23",
        birthday: "1963-03-05",
    }
]

Shopping Mall Property Management App

For an example, lets look at the needs of application used to manage Shopping Mall properties. The application assists employees in the day-to-day operations of multiple Shopping Malls.

Shopping Mall Requirements

  1. As a Maintenance Worker, I need to know which stores are currently in each Mall down to the Building they are located.
  2. As a Helpdesk Employee, I need to locate related stores in Mall locations by Store Category.
  3. As a Property Manager, I need to identify upcoming leases in need of renewal.

Create a new Entity using the StoreLocations schema defined above

const DynamoDB = require("aws-sdk/clients/dynamodb");
const client = new DynamoDB.DocumentClient();
const StoreLocations = new Entity(model, {client, table: "StoreLocations"});

Access Patterns are accessible on the StoreLocation

PUT Record

Add a new Store to the Mall

await StoreLocations.create({
    mallId: "EastPointe",
    storeId: "LatteLarrys",
    buildingId: "BuildingA1",
    unitId: "B47",
    category: "spite store",
    leaseEndDate: "2020-02-29",
    rent: "5000.00",
}).go();

Returns the following:

{
    "mallId": "EastPointe",
    "storeId": "LatteLarrys",
    "buildingId": "BuildingA1",
    "unitId": "B47",
    "category": "spite store",
    "leaseEndDate": "2020-02-29",
    "rent": "5000.00",
    "discount": "0.00"
}

UPDATE Record

Change the Stores Lease Date

When updating a record, you must include all Composite Attributes associated with the table's primary PK and SK.

let storeId = "LatteLarrys";
let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
await StoreLocations.update({storeId, mallId, buildingId, unitId}).set({
    leaseEndDate: "2021-02-28"
}).go();

Returns the following:

{
    "leaseEndDate": "2021-02-28"
}

GET Record

Retrieve a specific Store in a Mall

When retrieving a specific record, you must include all Composite Attributes associated with the table's primary PK and SK.

let storeId = "LatteLarrys";
let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
await StoreLocations.get({storeId, mallId, buildingId, unitId}).go();

Returns the following:

{
    "mallId": "EastPointe",
    "storeId": "LatteLarrys",
    "buildingId": "BuildingA1",
    "unitId": "B47",
    "category": "spite store",
    "leaseEndDate": "2021-02-28",
    "rent": "5000.00",
    "discount": "0.00"
}

DELETE Record

Remove a Store location from the Mall

When removing a specific record, you must include all Composite Attributes associated with the table's primary PK and SK.

let storeId = "LatteLarrys";
let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
let storeId = "LatteLarrys";
await StoreLocations.delete({storeId, mallId, buildingId, unitId}).go();

Returns the following:

{}

Query Mall Records

All Stores in a particular mall

Fulfilling Requirement #1.


let mallId = "EastPointe";
let stores = await StoreLocations.malls({mallId}).query().go();

All Stores in a particular mall building

Fulfilling Requirement #1.

let mallId = "EastPointe";
let buildingId = "BuildingA1";
let stores = await StoreLocations.malls({mallId}).query({buildingId}).go();

Find the store located in unit B47

Fulfilling Requirement #1.

let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
let stores = await StoreLocations.malls({mallId}).query({buildingId, unitId}).go();

Stores by Category at Mall

Fulfilling Requirement #2.

let mallId = "EastPointe";
let category = "food/coffee";
let stores = await StoreLocations.malls({mallId}).byCategory(category).go();

Stores by upcoming lease

Fulfilling Requirement #3.

let mallId = "EastPointe";
let q2StartDate = "2020-04-01";
let stores = await StoreLocations.leases({mallId}).lt({leaseEndDate: q2StateDate}).go();

Stores will renewals for Q4

Fulfilling Requirement #3.

let mallId = "EastPointe";
let q4StartDate = "2020-10-01";
let q4EndDate = "2020-12-31";
let stores = await StoreLocations.leases(mallId)
    .between (
      {leaseEndDate: q4StartDate}, 
      {leaseEndDate: q4EndDate})
    .go();

Spite-stores with release renewals this year

Fulfilling Requirement #3.

let mallId = "EastPointe";
let yearStarDate = "2020-01-01";
let yearEndDate = "2020-12-31";
let storeId = "LatteLarrys";
let stores = await StoreLocations.leases(mallId)
    .between (
      {leaseEndDate: yearStarDate}, 
      {leaseEndDate: yearEndDate})
    .filter(attr => attr.category.eq("Spite Store"))
    .go();

All Latte Larrys in a particular mall building

let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
let storeId = "LatteLarrys";
let stores = await StoreLocations.malls({mallId}).query({buildingId, storeId}).go();

Exported TypeScript Types

The following types are exported for easier use while using ElectroDB with TypeScript:

EntityRecord Type

The EntityRecord type is an object containing every attribute an Entity's model.

Definition:

type EntityRecord<E extends Entity<any, any, any, any>> =
    E extends Entity<infer A, infer F, infer C, infer S>
        ? Item<A,F,C,S,S["attributes"]>
        : never;

Use:

type EntiySchema = EntityRecord<typeof MyEntity>

EntityItem Type

This type represents an item as it is returned from a query. This is different from the EntityRecord in that this type reflects the required, hidden, default, etc properties defined on the attribute.

Definition:

export type EntityItem<E extends Entity<any, any, any, any>> =
  E extends Entity<infer A, infer F, infer C, infer S>
  ? ResponseItem<A, F, C, S>
  : never;

Use:

type Thing = EntityItem<typeof MyEntityInstance>;

CollectionItem Type

This type represents the value returned from a collection query, and is similar to EntityItem.

Use:

type CollectionResults = CollectionItem<typeof MyServiceInstance, "collectionName">

CreateEntityItem Type

This type represents an item that you would pass your entity's put or create method

Definition:

export type CreateEntityItem<E extends Entity<any, any, any, any>> =
  E extends Entity<infer A, infer F, infer C, infer S>
  ? PutItem<A, F, C, S>
  : never;

Use:

type NewThing = CreateEntityItem<typeof MyEntityInstance>;

UpdateEntityItem Type

This type represents an item that you would pass your entity's set method when using create or update.

Definition:

export type UpdateEntityItem<E extends Entity<any, any, any, any>> =
  E extends Entity<infer A, infer F, infer C, infer S>
  ? SetItem<A, F, C, S>
  : never;

Use:

type UpdateProperties = UpdateEntityItem<typeof MyEntityInstance>;

UpdateAddEntityItem Type

This type represents an item that you would pass your entity's add method when using create or update.

Definition:

export type UpdateAddEntityItem<E extends Entity<any, any, any, any>> =
    E extends Entity<infer A, infer F, infer C, infer S>
        ? AddItem<A, F, C, S>
        : never;

UpdateSubtractEntityItem Type

This type represents an item that you would pass your entity's subtract method when using create or update.

Definition:

export type UpdateSubtractEntityItem<E extends Entity<any, any, any, any>> =
    E extends Entity<infer A, infer F, infer C, infer S>
        ? SubtractItem<A, F, C, S>
        : never;

UpdateAppendEntityItem Type

This type represents an item that you would pass your entity's append method when using create or update.

Definition:

export type UpdateAppendEntityItem<E extends Entity<any, any, any, any>> =
    E extends Entity<infer A, infer F, infer C, infer S>
        ? AppendItem<A, F, C, S>
        : never;

UpdateRemoveEntityItem Type

This type represents an item that you would pass your entity's remove method when using create or update.

Definition:

export type UpdateRemoveEntityItem<E extends Entity<any, any, any, any>> =
    E extends Entity<infer A, infer F, infer C, infer S>
        ? RemoveItem<A, F, C, S>
        : never;

UpdateDeleteEntityItem Type

This type represents an item that you would pass your entity's delete method when using create or update.

Definition:

export type UpdateDeleteEntityItem<E extends Entity<any, any, any, any>> =
    E extends Entity<infer A, infer F, infer C, infer S>
        ? DeleteItem<A, F, C, S>
        : never;

Using ElectroDB With Existing Data

When using ElectroDB with an existing table and/or data model, there are a few configurations you may need to make to your ElectroDB model. Read the sections below to see if any of the following cases fits your particular needs.

Whenever using ElectroDB with existing tables/data, it is best to use the Query Option ignoreOwnership. ElectroDB leaves some meta-data on items to help ensure data queried and returned from DynamoDB does not leak between entities. Because your data was not made by ElectroDB, these checks could impede your ability to return data.

// when building params
.params({ignoreOwnership: true})
// when querying the table
.go({ignoreOwnership: true})
// when using pagination
.page(null, {ignoreOwnership: true})

Your existing index fields have values with mixed case:

DynamoDB is case-sensitive, and ElectroDB will lowercase key values by default. In the case where you modeled your data with uppercase, or did not apply case modifications, ElectroDB can be configured to match this behavior. Checkout the second on Index Casing to read more.

You have index field names that match attribute names:

With Single Table Design, it is encouraged to give index fields a generic name, like pk, sk, gsi1pk, etc. In reality, it is also common for tables to have index fields that are named after the domain itself, like accountId, organizationId, etc.

ElectroDB tries to abstract away your when working with DynamoDB, so instead of defining pk or sk in your model's attributes, you define them as indexes and map other attributes onto those fields as a composite. Using separate item fields for keys, then for the actual attributes you use in your application, you can leverage more advanced modeling techniques in DynamoDB.

If your existing table uses non-generic fields that also function as attributes, checkout the section Attributes as Indexes to learn more about how ElectroDB handles these types of indexes.

Electro CLI

NOTE: The ElectroCLI is currently in a beta phase and subject to change.

Electro is a CLI utility toolbox for extending the functionality of ElectroDB. Current functionality of the CLI allows you to:

  1. Execute queries against your Entities, Services, Models directly from the command line.
  2. Dynamically stand up an HTTP Service to interact with your Entities, Services, Models.

For usage and installation details you can learn more here.

Version 1 Migration

This section is to detail any breaking changes made on the journey to a stable 1.0 product.

New schema format/breaking key format change

It became clear when I added the concept of a Service that the "version" paradigm of having the version in the PK wasn't going to work. This is because collection queries use the same PK for all entities and this would prevent some entities in a Service to change versions without impacting the service as a whole. The better more is the place the version in the SK after the entity name so that all version of an entity can be queried. This will work nicely into the migration feature I have planned that will help migrate between model versions.

To address this change, I decide it would be best to change the structure for defining a model, which is then used as heuristic to determine where to place the version in the key (PK or SK). This has the benefit of not breaking existing models, but does increase some complexity in the underlying code.

Additionally, a change was made to the Service class. New Services would take a string of the service name instead of an object as before.

In the old scheme, version came after the service name (see ^).

pk: $mallstoredirectory_1#mall_eastpointe
                        ^
sk: $mallstores#building_buildinga#store_lattelarrys

In the new scheme, version comes after the entity name (see ^).

pk: $mallstoredirectory#mall_eastpointe

sk: $mallstores_1#building_buildinga#store_lattelarrys
                ^

In practice the change looks like this for use of Entity:

const  DynamoDB  =  require("aws-sdk/clients/dynamodb");
const {Entity} = require("electrodb");
const client = new DynamoDB.DocumentClient();
const table = "dynamodb_table_name";

// old way
let old_schema = {
  entity: "model_name",
  service: "service_name",
  version: "1",
  table: table,
  attributes: {...},
  indexes: {...}
};
new Entity(old_schema, {client});

// new way
let new_schema = {
  model: {
    entity: "model_name",
    service: "service_name",
    version: "1",
  },
  attributes: {...},
  indexes: {...}
};
new Entity(new_schema, {client, table});

Changes to usage of Service would look like this:

const  DynamoDB  =  require("aws-sdk/clients/dynamodb");
const {Service} = require("electrodb");
const client = new DynamoDB.DocumentClient();
const table = "dynamodb_table_name";

// old way
new Service({
  service: "service_name",
  version: "1",
  table: table,
}, {client});

// new way
new Service("service_name", {client, table});

// new way (for better TypeScript support)
new Service({entity1, entity2, ...})

The renaming of index property Facets to Composite and Template

In preparation of moving the codebase to version 1.0, ElectroDB will now accept the facets property as either the composite and/or template properties. Using the facets property is still accepted by ElectroDB but will be deprecated sometime in the future (tbd).

This change stems from the fact the facets is already a defined term in the DynamoDB space and that definition does not fit the use-case of how ElectroDB uses the term. To avoid confusion from new developers, the facets property shall now be called composite (as in Composite Attributes) when supplying an Array of attributes, and template while supplying a string. These are two independent fields for two reasons:

ElectroDB will validate the Composite Attributes provided map to those in the template (more validation is always nice).

Allowing for the composite array to be supplied independently will allow for Composite Attributes to remained typed even when using a Composite Attribute Template.

Get Method to Return null

1.0.0 brings back a null response from the get() method when a record could not be found. Prior to 1.0.0 ElectroDB returned an empty object.

Coming Soon

  • Default query options defined on the model to give more general control of interactions with the Entity.

Download Details:
Author: tywalch
Official Website: https://github.com/tywalch/electrodb 
License: MIT
 

#electrodb #dynamodb

ElectroDB: A DynamoDB Library to Make Single Table Designs Easier
Raleigh  Hayes

Raleigh Hayes

1636783200

How to Adjust Ringing Settings Full Stack Clojure Contact Book

This full stack clojure project is going to be a start of a new video series where I learn and teach how to create a simple contact book

Timestamps
00:00 - Intro
00:46 - Prerequisites
01:10 - Manage Dependencies With deps.edn
03:33 - Set Up http-kit Server
07:06 - Reitit Routes in Depth
10:28 - Adding Ring Middlewares
12:34 - Testing with Http Client
13:18 - Outro

#contact 

How to Adjust Ringing Settings Full Stack Clojure Contact Book

Sveltekit HTTP Requests Can Be Used in The Same Endpoint.

In this video, I will be talking about Sveltekit HTTP requests which can be used in the same endpoint.

#sveltekit  #contact 

Sveltekit HTTP Requests Can Be Used in The Same Endpoint.

Flutter Plugin to Create, Update, Delete and Observe Native Contacts

flutter_contacts

Flutter plugin to read, create, update, delete and observe native contacts on Android and iOS, with vCard support.

For a minimalistic example, take a look at example/. You can write a full-fledged contacts app with it – see example_full/ to see how.

Quick start

// See installation notes below regarding AndroidManifest.xml and Info.plist
import 'package:flutter_contacts/flutter_contacts.dart';

// Request contact permission
if (await FlutterContacts.requestPermission()) {
  // Get all contacts (lightly fetched)
  List<Contact> contacts = await FlutterContacts.getContacts();

  // Get all contacts (fully fetched)
  contacts = await FlutterContacts.getContacts(
      withProperties: true, withPhoto: true);

  // Get contact with specific ID (fully fetched)
  Contact contact = await FlutterContacts.getContact(contacts.first.id);

  // Insert new contact
  final newContact = Contact()
    ..name.first = 'John'
    ..name.last = 'Smith'
    ..phones = [Phone('555-123-4567')];
  await newContact.insert();

  // Update contact
  contact.name.first = 'Bob';
  await contact.update();

  // Delete contact
  await contact.delete();

  // Open external contact app to view/edit/pick/insert contacts.
  await FlutterContacts.openExternalView(contact.id);
  await FlutterContacts.openExternalEdit(contact.id);
  final contact = await FlutterContacts.openExternalPick();
  final contact = await FlutterContacts.openExternalInsert();

  // Listen to contact database changes
  FlutterContacts.addListener(() => print('Contact DB changed'));

  // Export contact to vCard
  String vCard = contact.toVCard();

  // Import contact from vCard
  contact = Contact.fromVCard('BEGIN:VCARD\n'
      'VERSION:3.0\n'
      'N:;Joe;;;\n'
      'TEL;TYPE=HOME:123456\n'
      'END:VCARD');
}

Simplified contact model

See code for complete data model.

class Contact {
    String id;
    String displayName;
    Uint8List? photo;
    Uint8List? thumbnail;
    Name name;
    List<Phone> phones;
    List<Email> emails;
    List<Address> addresses;
    List<Organization> organizations;
    List<Website> websites;
    List<SocialMedia> socialMedias;
    List<Event> events;
    List<Note> notes;
}
class Name { String first; String last; }
class Phone { String number; PhoneLabel label; }
class Email { String address; EmailLabel label; }
class Address { String address; AddressLabel label; }
class Organization { String company; String title; }
class Website { String url; WebsiteLabel label; }
class SocialMedia { String userName; SocialMediaLabel label; }
class Event { int? year; int month; int day; EventLabel label; }
class Note { String note; }

Demo

demo

Installation

  1. Add the following key/value pair to your app's Info.plist (for iOS):
  2. Add the following <uses-permissions> tags to your app's AndroidManifest.xml (for Android):

Notes

  • On iOS13+ you can only access notes if your app is entitled by Apple so notes are disabled by default. If you get entitlement, enable them via
  • On both iOS and Android there is a concept of raw and unified contacts. A single person might have two raw contacts (for example from Gmail and from iCloud) but will be merged into a single view called a unified contact. In a contact app you typically want unified contacts, so this is what's returned by default. You can get raw contacts instead viaHowever, for now, raw contacts cannot be inserted, updated or deleted.

Feature requests

These features have been requested and will be available soon.

  • Read/write groups ("labels" on Android, "groups" on iOS) #29
  • Read/write custom ringtones #22
  • Block contacts #28
  • Support for contacts stored in SIM card #26 #23
  • More raw account information on Android #5 #8
FlutterContacts.config.returnUnifiedContacts = false;
FlutterContacts.config.includeNotesOnIos13AndAbove = true;
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
     <uses-permission android:name="android.permission.READ_CONTACTS"/>
     <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
     <application ...>
     ...
 <plist version="1.0">
 <dict>
     ...
     <key>NSContactsUsageDescription</key>
     <string>Reason we need access to the contact list</string>
 </dict>
 </plist>

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add flutter_contacts

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  flutter_contacts: ^1.1.0+4

Alternatively, your editor might support or flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:flutter_contacts/flutter_contacts.dart'; 

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';

void main() => runApp(FlutterContactsExample());

class FlutterContactsExample extends StatefulWidget {
  @override
  _FlutterContactsExampleState createState() => _FlutterContactsExampleState();
}

class _FlutterContactsExampleState extends State<FlutterContactsExample> {
  List<Contact>? _contacts;
  bool _permissionDenied = false;

  @override
  void initState() {
    super.initState();
    _fetchContacts();
  }

  Future _fetchContacts() async {
    if (!await FlutterContacts.requestPermission()) {
      setState(() => _permissionDenied = true);
    } else {
      final contacts = await FlutterContacts.getContacts();
      setState(() => _contacts = contacts);
    }
  }

  @override
  Widget build(BuildContext context) => MaterialApp(
      home: Scaffold(
          appBar: AppBar(title: Text('flutter_contacts_example')),
          body: _body()));

  Widget _body() {
    if (_permissionDenied) return Center(child: Text('Permission denied'));
    if (_contacts == null) return Center(child: CircularProgressIndicator());
    return ListView.builder(
        itemCount: _contacts!.length,
        itemBuilder: (context, i) => ListTile(
            title: Text(_contacts![i].displayName),
            onTap: () async {
              final fullContact =
                  await FlutterContacts.getContact(_contacts![i].id);
              await Navigator.of(context).push(
                  MaterialPageRoute(builder: (_) => ContactPage(fullContact!)));
            }));
  }
}

class ContactPage extends StatelessWidget {
  final Contact contact;
  ContactPage(this.contact);

  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Text(contact.displayName)),
      body: Column(children: [
        Text('First name: ${contact.name.first}'),
        Text('Last name: ${contact.name.last}'),
        Text(
            'Phone number: ${contact.phones.isNotEmpty ? contact.phones.first.number : '(none)'}'),
        Text(
            'Email address: ${contact.emails.isNotEmpty ? contact.emails.first.address : '(none)'}'),
      ]));
} 

Download Details:

Author: QuisApp

Source Code: https://github.com/QuisApp/flutter_contacts

#flutter #contact 

 

Flutter Plugin to Create, Update, Delete and Observe Native Contacts

Working Contact Form in PHP with Validation & Email Sending

Learn the basics of creating a working contact form with email sending and basic email validation using PHP. We’ll learn about the ‘name’ attribute of our form input elements and use that to build and send an email when a user fills out the contact form. We’ll add some basic validation for the email as well as some conditional logic to display a ‘thank you’ message if the form was sent properly.

#php #contact #form

Working Contact Form in PHP with Validation & Email Sending

Personal Portfolio Website Design using HTML, CSS, and JavaScript

Personal Portfolio Website Design using HTML, CSS, and JavaScript

This video will show you how you can create a Portfolio website from scratch by using HTML, CSS, and JavaScript. You will also learn how to make a popup modal with some CSS animation.


How to Create a Portfolio Website Using HTML, CSS, JavaScript, and Bootstrap 5

In this post, I will discuss some of the benefits of creating a portfolio website. Then I'll show you how to create a beautiful responsive portfolio website for yourself using HTML, CSS, JavaScript and Bootstrap version 5.

Table Of Contents

  • Benefits of having a portfolio website
  • What is Bootstrap?
  • Folder Structure
  • How to Add a Navigation Menu to Your Portfolio
  • How to Add a Hero Header to the Portfolio
  • How to Make the About Section
  • How to Make the Services Section
  • How to Add Dark Background Color to Navbar on Page Scroll
  • How to Build the Portfolio Section
  • How to Build the Contact Section
  • How to Build the Footer Section
  • Adding Final Touches
  • Conclusion

Benefits of having a Portfolio Website

Having a portfolio website has several benefits, including:

  • it provides a platform to showcase your relevant skills and experience
  • it shows your personality
  • it lets hiring managers find you instead of you reaching out to them
  • you are easily searchable on search engines like Google

What is Bootstrap?

Bootstrap is a popular front-end CSS framework which is used to develop responsive and mobile friendly websites. The latest release of Bootstrap is version 5. You can find the official documentation of Bootstrap 5 here.

Folder Structure

We will now start working on creating the portfolio website.

First, let's create the folder structure. You can get the project starter files on GitHub. Also, you can visit here to see the live demo of this project.

Screenshot-from-2022-01-22-19-10-25

Project Folder Structure

The folder structure consists of index.html, style.css, and script.js files and an images folder. We'll write all CSS in the style.css file and the JavaScript in the script.js file .

In the index.html file, you can see the HTML boilerplate code with the Bootstrap CDN, font awesome kit, and a link to the external style sheet and JavaScript.

Here, the script.js file is loaded after loading all the HTML code.

How to Add a Navigation Menu to Your Portfolio

Now, let's work on adding a navigation menu in our project. It will help visitors find the relevant info they're looking for.

We will use Bootstrap's fixed-top class in nav element to keep the navbar at the top of the page. The navbar also has a navbar-brand class where we keep the name of the person as a brand.

<nav class="navbar navbar-expand-lg fixed-top navbarScroll">
        <div class="container">
            <a class="navbar-brand" href="#">Brad</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#home">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#about">About</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#services">Services</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#portfolio">Portfolio</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#contact">Contact</a>
                    </li>
                </ul>
                
            </div>
        </div>
    </nav>

The navbar has the following features:

  • It has six links: home, about, services, portfolio, contact, and footer
  • It has a transparent background. We will add a dark background on page scrolling later.
  • It toggles on smaller devices

You can find more details regarding Bootstrap 5 navbar features here.

However, the navbar has a problem while scrolling. It's fully transparent throughout the page which causes readability issues.  We will fix this issue after we complete the Services section to make you understand the issue properly.

How to Add a Hero Header to the Portfolio

Now, we will be adding a hero image with some text in the center. A hero image is a web design term which refers to a high quality full width image that displays the company or individual's main goals, a representative image, photo, or other eye-catching elements. It helps attract users to your site.

 <!-- main banner -->
    <section class="bgimage" id="home">
        <div class="container-fluid">
            <div class="row">
            <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 hero-text">
                <h2 class="hero_title">Hi, it's me Brad</h2>
                <p class="hero_desc">I am a professional freelancer in New York City</p>
            </div>
            </div>
        </div>
    </section>

 Also, let's add the CSS for the above code in the style.css file:

/* hero background image */
.bgimage {
    height:100vh;
    background: url('images/heroImage.jpeg');
    background-size:cover;
    position:relative;
}
/* text css above hero image*/
.hero_title {
    font-size: 4.5rem;
}
.hero_desc {
    font-size: 2rem;
}
.hero-text {
    text-align: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}

Here we can see that the section has an id named bgimage which is responsible for displaying the background hero image with full width. It also displays some text in the center above the background image with the help of the above CSS.

This is how the site looks so far with the navbar and the hero section:

Screenshot-from-2022-01-25-10-13-25

Hero Image with Navbar

How to Make the About Section

The About page contains important information about you and your background. Visitors to your portfolio site can get to know you through the information you provide in this page.

We will be adding an image to the left side of the row, and on the right side we will add our quick introduction in this section. Let's demonstrate it using the code below:

<!-- about section-->
    <section id="about">
        <div class="container mt-4 pt-4">
            <h1 class="text-center">About Me</h1>
            <div class="row mt-4">
                <div class="col-lg-4">
                    <img src="images/about.jpeg" class= "imageAboutPage" alt="">
                </div>

                <div class="col-lg-8">
                    <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged
                        
                    </p>
                    <div class="row mt-3">
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                        <div class="col-md-6">
                            <ul>
                                <li>Name: David Parker</li>
                                <li>Age: 28</li>
                                <li>Occupation: Web Developer</li>

                            </ul>
                        </div>
                    </div>
                    <div class="row mt-3">
                        <p> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                            Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
                        </p>
                    </div>
                </div>
            </div>
    </section>

Let's add some CSS for the left side image:

/* about section image css */
.imageAboutPage {
    width: 100%;
}

This will create an about section. You can modify the content based on your use cases. We have added classes named mt-4 and pt-4 with container class which will set the margin top and padding top to 1.5 rem.

The row has two columns. One has the col-lg-4 class for displaying the image which will occupy the left column with a 4-part grid for large screens.

The next column is assigned a class of col-lg-8 which will occupy the right column with an 8-part grid for larger screens. For medium and small screens they will overlap with each other which we can see in the below GIF file:

about

About Section

How to Make the Services Section

This section helps convert website visitors into potential clients. This is where you explain what specific services you offer, and where you niche down your offered services.

Let's add the code for this section and describe it below:

<!-- services section-->
    <section id="services">
        <div class="container">
            <h1 class="text-center">Services</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class="fas servicesIcon fa-clock"></i>
                            <h4 class="card-title mt-3">Website Development</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-layer-group'></i>
                            <h4 class="card-title mt-3">Website Design</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='far servicesIcon fa-check-circle'></i>
                            <h4 class="card-title mt-3">Website Deployment</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>

            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-search'></i>
                            <h4 class="card-title mt-3">SEO</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-shield-alt'></i>
                            <h4 class="card-title mt-3">DevOps</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card servicesText">
                        <div class="card-body">
                            <i class='fas servicesIcon fa-wrench'></i>
                            <h4 class="card-title mt-3">QA</h4>
                            <p class="card-text mt-3">Some quick example text to build on the card title and make up the bulk of the card's content.
                                Some quick example text to build on the card title and make up the bulk of the card's content.
                            </p>
                        </div>
                    </div>  
                </div>
            </div>
        </div>
    </section>

Since this website is targeted towards web developers and designers, I've included some of the services which a web developer or designer might offer.

We have used bootstrap cards to display services. Our services section has 2 rows and 3 columns each. For large screens with a width greater than or equal to 992px, three cards are displayed in a row. For screens less than 992px wide, only a single card is displayed in a row.

You can find more about bootstrap breakpoints here.

Also, there are fonts added in each card to make them look better.

Without CSS, the services section would look like this :

Screenshot-from-2022-01-23-14-01-00

So, let's add some CSS to increase the font icon font size and card height and add some extra color when a user hovers over a card.

/* services section css */
.servicesText.card {
    height: 280px;
    cursor: pointer;
  }
.servicesIcon {
    font-size: 36px;
    text-align: center;
    width: 100%;
}
.card-title {
    text-align: center;
}
.card:hover .servicesIcon {
    color: #008000;
}
.servicesText:hover {
    border: 1px solid #008000;
}

This is how our services section looks now:

services

Services 

How to Add Dark Background Color to Navbar on Page Scroll

If you look into the above gif properly you will see that the navbar is transparent throughout the page which causes readability issues. So let's work on fixing this issue.

We will write some JavaScript and CSS in order to resolve this problem. We will add a navbarDark class in order to show a dark background color for the navbar on page scroll.

For that we need to go to the script.js file and add the following code:

// add class navbarDark on navbar scroll
const header = document.querySelector('.navbar');

window.onscroll = function() {
    var top = window.scrollY;
    if(top >=100) {
        header.classList.add('navbarDark');
    }
    else {
        header.classList.remove('navbarDark');
    }
}

Now, let's break down the above code:

  • The header holds the value of the nav element since the querySelector method returns the first element that matches the CSS selector (which is .navbar in this case).
  • window.onscroll fires up when the scroll event happens.
  • window.scrollY returns the number of pixels that the document is scrolled vertically and its value is assigned to a variable named top.
  • If the value of top is greater than or equal to 100, it adds a class of navbarDark to the header.

Let's quickly add CSS for the navbarDark class. For that, go to your style.css file and add the following code:

/* display background color black on navbar scroll */
.navbarScroll.navbarDark {
    background-color: black;
}

This is how the navbar will look now:

navbar

Dark background color on Navbar on page scroll

How to Build the Portfolio Section

This section includes your best work. People can see what you are capable of doing, and showcasing strong past work will definitely attract more potential clients or recruiters. So only add your best work in this section.

We will use Bootstrap cards to display the portfolio projects. There will be 2 rows and each row will have 3 columns of cards.

This will be the code for portfolio section:

<!-- portfolio section-->
    <section id="portfolio">
        <div class="container mt-3">
            <h1 class="text-center">Portfolio</h1>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">YouTube Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Quiz App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage3.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Product Landing Page</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <br>
            <div class="row">
                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Messaging Service</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage1.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Twitter Clone</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="col-lg-4 mt-4">
                    <div class="card portfolioContent">
                        <img class="card-img-top" src="images/portfolioImage4.jpg" alt="Card image" style="width:100%">
                        <div class="card-body">
                            <h4 class="card-title">Blog App</h4>
                            <p class="card-text">Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>
                            <div class="text-center">
                                <a href="#" class="btn btn-success">Link</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </section>

Each card has an image, title, description, and link to the projects. Three cards are displayed in a row for large screens which have breakpoints of ≥ 992px wide, but for screens < 992px wide only a single card is displayed in a row.

The GIF below shows how the portfolio section looks now:

portfolio

Portfolio

How to Build the Contact Section

You should include your contact information in this section so that visitors can contact you if they want to hire you.

Our contact section will include 2 columns in a single row: Google maps for location and a contact form.

In order to embed the Google map, you need to follow these steps:

  • go to https://www.embed-map.com
  • enter your location
  • click on the Generate HTML Code button which will provide your Google Map HTML Code

Our code will look like this with the contact form included:

<!-- contact section-->
    <section id="contact">
        <div class="container mt-3 contactContent">
            <h1 class="text-center">Contact Me</h1>

            <div class="row mt-4">
                <div class="col-lg-6">
                    <!-- to edit google map goto https://www.embed-map.com type your location, generate html code and copy the html  -->
                    <div style="max-width:100%;overflow:hidden;color:red;width:500px;height:500px;">
                        <div id="embedmap-canvas" style="height:100%; width:100%;max-width:100%;">
                            <iframe style="height:100%;width:100%;border:0;" frameborder="0" src="https://www.google.com/maps/embed/v1/place?q=new+york&key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8">
                            </iframe>
                        </div>
                        <a class="googlemaps-html" href="https://www.embed-map.com" id="get-data-forembedmap">https://www.embed-map.com</a>
                        <style>#embedmap-canvas img{max-width:none!important;background:none!important;font-size: inherit;font-weight:inherit;}
                        </style>
                    </div>
                </div>

                <div class="col-lg-6">
                    <!-- form fields -->
                    <form>
                        <input type="text" class="form-control form-control-lg" placeholder="Name">
                        <input type="email" class="form-control mt-3" placeholder="Email">
                        <input type="text" class="form-control mt-3" placeholder="Subject">
                        <div class="mb-3 mt-3">
                            <textarea class="form-control" rows="5" id="comment" name="text" placeholder="Project Details"></textarea>
                        </div>
                    </form>
                    <button type="button" class="btn btn-success mt-3">Contact Me</button>
                    
                </div>

            </div>
        </div>
    </section>

The first column will display the Google map and the next one will display the contact form.

The form has four different form fields: name, email, subject and project details. The form doesn't submit the request itself. You will need to connect it with any back-end language. Or, you can simply use Netlify Form or Formspree form for this.

This is how the contact section will appear:

Screenshot-from-2022-01-25-11-31-56

Contact Section

How to Build the Footer Section

Now we have come to the last section of this post, which is the footer section. We have already added a link to the font awesome CDN in the index.html file.

In the Footer, we will add links to our social media through font awesome icons.

 <!-- footer section-->
    <footer id="footer">
        <div class="container-fluid">
            <!-- social media icons -->
            <div class="social-icons mt-4">
                <a href="https://www.facebook.com/" target="_blank"><i class="fab fa-facebook"></i></a>
                <a href="https://www.instagram.com/" target="_blank"><i class="fab fa-instagram"></i></a>
                <a href="https://www.twitter.com/" target="_blank"><i class="fab fa-twitter"></i></a>
                <a href="https://www.linkedin.com/" target="_blank"><i class="fab fa-linkedin"></i></a>
                <a href="https://www.twitch.tv/" target="_blank"><i class="fab fa-twitch"></i></a>
            </div>
        </div>
    </footer>

Without the CSS, our footer will look like this:

Screenshot-from-2022-01-23-17-56-37

footer without styling

So let's add some styling to the footer with this code:

/* social media icons styling */
.social-icons {
    font-size: 36px;
    cursor: pointer;
}
.fa-facebook:hover,.fa-instagram:hover,.fa-twitter:hover,.fa-linkedin:hover, .fa-twitch:hover {
    color: #008000;
}
.fab {
    color: #000000;
}
/* footer styling */
#footer {
    background-color: #808080;
    text-align: center;
}

The icons are now displayed in the center with a hover effect which we can see in the below GIF file.

footer

Footer

Final Touches

In order to add some spacing between all the sections, let's add some more styling:

/* spacing on all sections */
#about, #services, #portfolio, #contact {
    margin-top: 4rem;
    padding-top: 4rem;
}
#contact {
    padding-bottom: 4rem;
}

Now we're done making our complete portfolio website.

You can find the full source code of this project here.

Conclusion

This is how you can create a complete responsive portfolio website using HTML, CSS, JavaScript, and Bootstrap 5 .

Original article source at https://www.freecodecamp.org

#html #css #javascript #bootstrap #webdev 

Personal Portfolio Website Design using HTML, CSS, and JavaScript