Adding a stylesheet

Cascading stylesheets represent one of the most common ways to customize the user experience for an application. This tutorial illustrates how to add a stylesheet when you use React to create a new front-end for your project. If you already know how to add cascading stylesheets (CSS) to a React-based project, you can skip this tutorial.

Currently, you can only use Javascript to implement the front-end for your canister. This tutorial illustrates using the React framework to manage the Document Object Model (DOM) for your canister. Because React has its own custom DOM syntax, you need to modify the webpack configuration to compile the front-end code, which is written in JSX. For more information about learning to use React and JSX, see Getting started on the React website.

Before you begin

Before starting the tutorial, verify the following:

  • You have node.js installed for front-end development and can install packages using npm install in your project. For information about installing node for your local operating system and package manager, see the Node website.

  • You have downloaded and installed the DFINITY Canister SDK package as described in Download and install.

  • You have installed the Visual Studio Code plugin for Motoko as described in Install the language editor plug-in if you are using Visual Studio Code as your IDE.

  • You have stopped any Internet Computer network processes running on the local computer.

Create a new project

To create a new project directory for your custom front-end application:

  1. Open a terminal shell on your local computer, if you don’t already have one open.

  2. Change to the folder you are using for your Internet Computer sample projects.

  3. Verify that you have node.js installed locally, if necessary.

  4. Create a new project by running the following command:

    dfx new contacts
  5. Change to your project directory by running the following command:

    cd contacts

Install the React framework

if you’ve never used React before, you might want to explore the Intro to React tutorial or the React website before editing the front-end code.

To install required framework modules:

  1. Install the React module by running the following command:

    npm install --save react react-dom
  2. Install the required TypeScript language compiler loader by running the following command:

    npm install --save typescript ts-loader
  3. Install the required style loaders by running the following command:

    npm install --save style-loader css-loader

Modify the default configuration

For this tutorial, you need to modify the default front-end settings in the dfx.json configuration file for your project.

To modify settings in the default dfx.json configuration file:

  1. Open the dfx.json configuration file in a text editor.

  2. Under the canisters.contacts section, change the name of the main program file from main.mo to contacts.mo.

  3. Under the canisters.contacts_assets section, change the frontend.entrypoint file name from index.js to index.jsx to enable adding HTML directly inside JavaScript.

  4. Save your changes and close the dfx.json file to continue.

Modify the default program

For this tutorial, you are going to rename the main program file and modify the file content to replace the default program with a program that allow you to store and look up contact information.

To modify the default program:

  1. Change to the src/contacts directory.

    cd src/contacts
  2. Rename the main.mo file as contacts.mo by running the following command:

    mv main.mo contacts.mo
  3. Open the contacts.mo file in a text editor and delete the existing content.

  4. Copy and paste the following sample code into the file:

    import List "mo:base/List";
    import AssocList "mo:base/AssocList";
    
    type Name = Text;
    type Phone = Nat;
    
    type Entry = {
        name: Name;
        address1: Text;
        address2: Text;
        email: Text;
        phone: Phone;
    };
    
    type ContactsMap = AssocList.AssocList<Name, Entry>;
    
    actor {
        flexible var contact: ContactsMap = List.nil<(Name, Entry)>();
    
        func nameEq(lhs: Name, rhs: Name): Bool {
            return lhs == rhs;
        };
    
        public func insert(name0: Name, address10: Text, address20: Text, email0: Text, phone0: Phone): async () {
            let newEntry : Entry = {
                name = name0;
                address1 = address10;
                address2 = address20;
                email = email0;
                phone = phone0;
            };
    
            let (newContacts, _) = AssocList.replace<Name, Entry>(
                contact,
                name0,
                func(n: Name, m: Name) = n == m,
                ?newEntry
            );
            contact := newContacts;
        };
    
        public query func lookup(name: Name): async ?Entry {
            return AssocList.find<Name, Entry>(contact, name, nameEq);
        };
    };
  5. Save your changes and close the contacts.mo file to continue.

Modify the front-end files

You are now ready to create a new front-end for your program.

  1. Navigate back to the root of your project directory.

  2. Open the webpack configuration file (webpack.config.js) in a text editor.

  3. Locate the commented example for the module key above the plugins section, then uncomment the following lines:

    module: {
      rules: [
        { test: /\.(js|ts)x?$/, loader: "ts-loader" },
        { test: /\.css$/, use: ['style-loader','css-loader'] }
      ]
    },

    These settings enable your program to use the ts-loader compiler and to import CSS files.

  4. Save your changes and close the webpack.config.js file to continue.

  5. Create a new file named tsconfig.json, open the file in a text editor, then copy and paste the following into the file:

    {
        "compilerOptions": {
          "target": "es2018",        /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
          "module": "commonjs",      /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
          "lib": ["ES2018", "DOM"],  /* Specify library files to be included in the compilation. */
          "allowJs": true,           /* Allow javascript files to be compiled. */
          "jsx": "react",            /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
        }
    }
  6. Save your changes and close the tsconfig.json file to continue.

Add a stylesheet to your project

You are now ready to create a new cascading stylesheet and add it to your project.

To add a stylesheet:

  1. Change to the src/contacts_assets/public directory.

    cd src/contacts_assets/public/
  2. Create a new file named mycontacts.css and open the file in a text editor.

  3. Define some style properties for the front-end.

    For example, copy and paste the following sample styles into the file:

    html {
        background-color: bisque;
    }
    
    body {
        font-family: Arial, Helvetica, sans-serif;
        display: block;
        margin: 10px;
    }
    
    h1 {
        color: darkblue;
        font-size: 32px;
    }
    
    div.new-entry {
        margin: 30px 20px 30px 20px;
    }
    
    .new-entry > div {
        margin-bottom: 15px;
    }
    
    table {
        margin-top: 12px;
        border-top: 1px solid darkblue;
        border-bottom: 1px solid darkblue;
    }
    
    #form {
        margin: 30px 0 30px 20px;
    }
    
    button {
        line-height: 20px;
    }
    
    #lookupName {
        margin-right: 12px;
    }
  4. Save your changes and close the mycontacts.css file to continue.

  5. Open the default index.js file in a text editor and delete the existing content.

  6. Copy and paste the following sample code into the index.js file:

    import contact from 'ic:canisters/contacts';
    import * as React from 'react';
    import { render } from 'react-dom';
    
    import './mycontacts.css'; // Import custom styles
    
    class Contact extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
        };
      }
    
      async doInsert() {
        let name = document.getElementById("newEntryName").value;
        let add1 = document.getElementById("newEntryAddress1").value;
        let add2 = document.getElementById("newEntryAddress2").value;
        let email = document.getElementById("newEntryEmail").value;
        let phone = document.getElementById("newEntryPhone").value;
    
        contact.insert(name, add1, add2, email, parseInt(phone, 10));
      }
    
      async lookup() {
        let name = document.getElementById("lookupName").value;
        contact.lookup(name).then(opt_entry => {
          let entry;
    
          if (opt_entry.length == 0) {
            entry = { name: "", description: "", phone: ""};
          } else {
            entry = opt_entry[0]
          }
    
          document.getElementById("newEntryName").value = entry.name;
          document.getElementById("newEntryAddress1").value = entry.address1;
          document.getElementById("newEntryAddress2").value = entry.address2;
          document.getElementById("newEntryEmail").value = entry.email;
          document.getElementById("newEntryPhone").value = entry.phone.toString();
        });
      }
    
      render() {
        return (
          <div class="new-entry">
            <h1>My Contacts</h1>
            <div>
              Add or update contact information:
    	      <form id="contact">
              <table>
                <tr><td>Name:</td><td><input id="newEntryName"></input></td></tr>
                <tr><td>Address 1 (street):</td><td><input id="newEntryAddress1"></input></td></tr>
                <tr><td>Address 2 (city and state):</td><td><input id="newEntryAddress2"></input></td></tr>
                <tr><td>Email:</td><td><input id="newEntryEmail"></input></td></tr>
                <tr><td>Phone:</td><td><input id="newEntryPhone" type="number"></input></td></tr>
              </table>
            </form>
            </div>
            <div>
              <button onClick={() => this.doInsert()}>Add Contact</button>
            </div>
            <div>
              Lookup name: <input id="lookupName" style={{ "line-height": "20px" }}></input>
              <button onClick={() => this.lookup()}>Lookup</button>
            </div>
          </div>
        );
      }
    }
    
    document.title = "DFINITY CONTACT EXAMPLE";
    
    render(<Contact />, document.getElementById('app'));
  7. Rename the modified index.js file as index.jsx by running the following command:

    mv index.js index.jsx
  8. Navigate back to the root of your project directory.

Start the local network

Before you can build the contacts project, you need to connect to the Internet Computer network either running locally in your development environment or running remotely on a sub-network that you can access.

To start the network locally:

  1. Open a new terminal window or tab on your local computer and navigate to your project directory.

    For example, you can do either of the following if running Terminal on macOS:

    • Click Shell, then select New Tab to open a new terminal in your current working directory.

    • Click Shell and select New Window, then run cd ~/ic-projects/contacts in the new terminal if your contacts project is in the ic-projects working folder.

    You should now have two terminals open with your project directory as your current working directory.

  2. Start the Internet Computer network on your local computer by running the following command:

    dfx start

    After you start the local network, the terminal displays messages about network operations.

  3. Leave the terminal that displays network operations open and switch your focus to your original terminal where you created your project.

Register canister identifiers

After you connect to the Internet Computer network running locally in your development environment, you can register with the network to generate unique canister identifiers for your project.

To register canister identifiers for the local network:

  1. Check that you are still in your project directory, if needed.

  2. Register unique canister identifiers for the project by running the following command:

    dfx canister create --all

    The command displays the network-specific canister identifiers for the canisters defined in the dfx.json configuration file.

    "contacts" canister created with canister id: "75hes-oqbaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q"
    "contacts_assets" canister created with canister id: "cxeji-wacaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q"

    Keep in mind that because you are running the Internet Computer locally, these identifiers are only valid on the local network. To deploy canisters on a remote network, you must connect to that network using the --network command-line option and a specific network name or address to register identifiers on that network.

Build and deploy the program

You are now ready to compile the program into a WebAssembly module and deploy it on your local network.

To build and deploy the program:

  1. Open a terminal shell on your local computer, if you don’t already have one open.

  2. Build the executable canister by running the following command:

    dfx build

    The command displays output indicating that the build is successful.

    Building canisters...
    Building frontend...
  3. Deploy your contacts project on the local network by running the following command:

    dfx canister install --all

    The command output displays output similar to the following:

    Installing code for canister contacts, with canister_id 75hes-oqbaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q
    Installing code for canister contacts_assets, with canister_id cxeji-wacaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q
  4. Copy the canister identifier for the contacts_assets canister to the clipboard or a notepad application.

View the front-end

You can now access the front-end for the contacts program by entering the canister identifier for the assets canister in a browser.

To view the front-end:

  1. Open a browser and navigate to the local network address and port number specified in the dfx.json configuration file.

    For example, if you are using the default binding for the local network, navigate to 127.0.0.1:8000/.

    To specify the canister you want the web server to display, add the canisterId parameter and the contacts_assets canister identifier to the URL using the following syntax:

    ?canisterId=<YOUR-CANISTER-IDENTIFIER>

    For example, the full URL should look similar to the following but with the _canister_identifier_ that was returned by the dfx canister install command:

http://127.0.0.1:8000/?canisterId=cxeji-wacaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q
  1. Verify that you are prompted with a My Contacts form.

    For example:

    Sample front-end

  2. Create one or more test records by entering text in the Name, Address, and Email input fields and a number in the Phone input field, then clicking Add Contact.

  3. Clear the form fields and type a contact name in the Lookup name field, then click Lookup to see the stored contact information.

    Keep in mind that the Lookup name you type must be an exact match for the name of a contact you added.

Modify the stylesheet and test your changes

After viewing the Contacts application, you might want to make some changes.

To change stylesheet properties:

  1. Open the mycontacts.css file in a text editor and modify its style settings.

    For example, you might want to change the background color or style the input form.

  2. Rebuild the project with your changes by running the following command.

    dfx build
  3. Deploy your project changes by running the following command:

    dfx canister install --all --mode upgrade
  4. View the results in the browser.

Modify the front-end or back-end code

If you want to explore further, you might want to experiment with modifying the front-end or back-end code for this tutorial. For example, you might want try modifying the tutorial to do the following:

  • Change the front-end code to clear the input fields after adding a new contact, for example, as part of an onClick event.

  • Change the Motoko program functions to do partial instead of exact string matching on the Name field.

  • Change the Motoko program to allow lookups based on a different field.

Stop the local network

After you finish experimenting with your program, you can stop the local Internet Computer network so that it doesn’t continue running in the background.

To stop the local network:

  1. In the terminal that displays network operations, press Control-C to interrupt the local network process.

  2. Stop the Internet Computer network by running the following command:

    dfx stop