Tag: Building

Performant Expandable Animations: Building Keyframes on the Fly

Animations have come a long way, continuously providing developers with better tools. CSS Animations, in particular, have defined the ground floor to solve the majority of uses cases. However, there are some animations that require a little bit more work.

You probably know that animations should run on the composite layer. (I won’t extend myself here, but if you want to know more, check this article.) That means animating transform or opacity properties that don’t trigger layout or paint layers. Animating properties like height and width is a big no-no, as they trigger those layers, which force the browser to recalculate styles.

On top of that, even when animating transform properties, if you want to truly hit 60 FPS animations, you probably should get a little help from JavaScript, using the FLIP technique for extra smoother animations! 

However, the problem of using transform for expandable animations is that the scale function isn’t exactly the same as animating width/height properties. It creates a skewed effect on the content, as all elements get stretched (when scaling up) or squeezed (when scaling down).

So, because of that, my go-to solution has been (and probably still is, for reasons I will detail later), technique #3 from Brandon Smith’s article. This still has a transition on height, but uses Javascript to calculate the content size, and force a transition using requestAnimationFrame. At OutSystems, we actually used this to build the animation for the OutSystems UI Accordion Pattern.

Generating keyframes with JavaScript

Recently, I stumbled on another great article from Paul Lewis, that details a new solution for expanding and collapsing animations, which motivated me to write this article and spread this technique around.

Using his words, the main idea consists of generating dynamic keyframes, stepping…

[…] from 0 to 100 and calculate what scale values would be needed for the element and its contents. These can then be boiled down to a string, which can be injected into the page as a style element. 

To achieve this, there are three main steps.

Step 1: Calculate the start and end states

We need to calculate the correct scale value for both states. That means we use getBoundingClientRect() on the element that will serve as a proxy for the start state, and divide it with the value from the end state. It should be something like this:

function calculateStartScale () {   const start= startElement.getBoundingClientRect();   const end= endElement.getBoundingClientRect();   return {     x: start.width / end.width,     y: start.height / end.height   }; }

Step 2: Generate the Keyframes

Now, we need to run a for loop, using the number of frames needed as the length. (It shouldn’t really be less than 60 to ensure a smooth animation.) Then, in each iteration, we calculate the correct easing value, using an ease function:

function ease (v, pow=4) {   return 1 - Math.pow(1 - v, pow); }  let easedStep = ease(i / frame);

With that value, we’ll get the scale of the element on the current step, using the following math:

const xScale = x + (1 - x) * easedStep; const yScale = y + (1 - y) * easedStep;

And then we add the step to the animation string:

animation += `$ {step}% {   transform: scale($ {xScale}, $ {yScale}); }`;

To avoid the content to get stretched/ skewed, we should perform a counter animation on it, using the inverted values:

const invXScale = 1 / xScale; const invYScale = 1 / yScale;  inverseAnimation += `$ {step}% {   transform: scale($ {invXScale}, $ {invYScale}); }`;

Finally, we can return the completed animations, or directly inject them in a newly created style tag.

Step 3: Enable the CSS animations 

On the CSS side of things, we need to enable the animations on the correct elements:

.element--expanded {   animation-name: animation;   animation-duration: 300ms;   animation-timing-function: step-end; }  .element-contents--expanded {   animation-name: inverseAnimation ;   animation-duration: 300ms;   animation-timing-function: step-end; }

You can check the example of a Menu from Paul Lewis article, on Codepen (courtesy of Chris Coyer):

Building an expandable section 

After grasping these baseline concepts, I wanted to check if I could apply this technique to a different use case, like a expandable section.

We only need to animate the height in this case, specifically on the function to calculate scales. We’re getting the Y value from the section title, to serve as the collapsed state, and the whole section to represent the expanded state:

    _calculateScales () {       var collapsed = this._sectionItemTitle.getBoundingClientRect();       var expanded = this._section.getBoundingClientRect();              // create css variable with collapsed height, to apply on the wrapper       this._sectionWrapper.style.setProperty('--title-height', collapsed.height + 'px');        this._collapsed = {         y: collapsed.height / expanded.height       }     }

Since we want the expanded section to have absolute positioning (in order to avoid it taking space when in a collapsed state), we are setting the CSS variable for it with the collapsed height, applied on the wrapper. That will be the only element with relative positioning.

Next comes the function to create the keyframes: _createEaseAnimations(). This doesn’t differ much from what was explained above. For this use case, we actually need to create four animations:

  1. The animation to expand the wrapper
  2. The counter-expand animation on the content
  3. The animation to collapse the wrapper
  4. The counter-collapse animation on the content

We follow the same approach as before, running a for loop with a length of 60 (to get a smooth 60 FPS animation), and create a keyframe percentage, based on the eased step. Then, we push it to the final animations strings:

outerAnimation.push(`   $ {percentage}% {     transform: scaleY($ {yScale});   }`);    innerAnimation.push(`   $ {percentage}% {     transform: scaleY($ {invScaleY});   }`);

We start by creating a style tag to hold the finished animations. As this is built as a constructor, to be able to easily add multiple patterns, we want to have all these generated animations on the same stylesheet. So, first, we validate if the element exists. If not, we create it and add a meaningful class name. Otherwise, you would end up with a stylesheet for each section expandable, which is not ideal.

 var sectionEase = document.querySelector('.section-animations');  if (!sectionEase) {   sectionEase = document.createElement('style');   sectionEase.classList.add('section-animations');  }

Speaking of that, you may already be wondering, “Hmm, if we have multiple expandable sections, wouldn’t they still be using the same-named animation, with possibly wrong values for their content?” 

You’re absolutely right! So, to prevent that, we are also generating dynamic animation names. Cool, right?

We make use of the index passed to the constructor from the for loop when making the querySelectorAll('.section') to add a unique element to the name:

var sectionExpandAnimationName = "sectionExpandAnimation" + index; var sectionExpandContentsAnimationName = "sectionExpandContentsAnimation" + index;

Then we use this name to set a CSS variable on the current expandable section. As this variable is only in this scope, we just need to set the animation to the new variable in the CSS, and each pattern will get its respective animation-name value.

.section.is--expanded {   animation-name: var(--sectionExpandAnimation); }  .is--expanded .section-item {   animation-name: var(--sectionExpandContentsAnimation); }  .section.is--collapsed {   animation-name: var(--sectionCollapseAnimation); }  .is--collapsed .section-item {   animation-name: var(--sectionCollapseContentsAnimation); }

The rest of the script is related to adding event listeners, functions to toggle the collapse/expand status and some accessibility improvements.

About the HTML and CSS: it needs a little bit of extra work to make the expandable functionality work. We need an extra wrapper to be the relative element that doesn’t animate. The expandable children have an absolute position so that they don’t occupy space when collapsed.

Remember, since we need to make counter animations, we make it scale full size in order to avoid a skew effect on the content.

.section-item-wrapper {   min-height: var(--title-height);   position: relative; }  .section {   animation-duration: 300ms;   animation-timing-function: step-end;   contain: content;   left: 0;   position: absolute;   top: 0;   transform-origin: top left;   will-change: transform; }  .section-item {   animation-duration: 300ms;   animation-timing-function: step-end;   contain: content;   transform-origin: top left;   will-change: transform;   }

I would like to highlight the importance of the animation-timing-functionproperty. It should be set to linear or step-end to avoid easing between each keyframe.

The will-change property — as you probably know — will enable GPU acceleration for the transform animation for an even smoother experience. And using the contains property, with a value of contents, will help the browser treat the element independently from the rest of the DOM tree, limiting the area before it recalculates the layout, style, paint and size properties.

We use visibility and opacity to hide the content, and stop screen readers to access it, when collapsed.

.section-item-content {   opacity: 1;   transition: opacity 500ms ease; }  .is--collapsed .section-item-content {   opacity: 0;   visibility: hidden; }

And finally, we have our section expandable! Here’s the complete code and demo for you to check:

Performance check

Anytime we work with animations, performance ought to be in the back of our mind. So, let’s use developer tools to check if all this work was worthy, performance-wise. Using the Performance tab (I’m using Chrome DevTools), we can analyze the FPS and the CPU usage, during the animations.

And the results are great!

The higher the green bar, the higher the frames. And there’s no junk either, which would be signed by red sections.

Using the FPS meter tool to check the values at greater detail, we can see that it constantly hits the 60 FPS mark, even with abusive usage.

Final considerations

So, what’s the verdict? Does this replace all other methods? Is this the “Holy Grail” solution?

In my opinion, no. 

But… that’s OK, really! It’s another solution on the list. And, as is true with any other method, it should be analyzed if it’s the best approach for the use-case.

This technique definitely has its merits. As Paul Lewis says, this does take a lot of work to prepare. But, on the flip side, we only need to do it once, when the page loads. During interactions, we are merely toggling classes (and attributes in some cases, for accessibility).

However, this brings some limitations for the UI of the elements. As you could see for the expandable section element, the counter-scale makes it much more reliable for absolute and off-canvas elements, like floating-actions or menus. It’s also difficult to styled borders because it’s using overflow: hidden.

Nevertheless, I think there’s tons of potential with this approach. Let me know what you think!

The post Performant Expandable Animations: Building Keyframes on the Fly appeared first on CSS-Tricks.

CSS-Tricks

, , , ,

Building a Real-Time Chat App with React and Firebase

In this article, we’ll cover key concepts for authenticating a user with Firebase in a real-time chat application. We’ll integrate third-party auth providers (e.g. Google, Twitter and GitHub) and, once users are signed in, we’ll learn how to store user chat data in the Firebase Realtime Database, where we can sync data with a NoSQL cloud database.

The client application is going to be built in React, as it is one of the most popular JavaScript frameworks out there, but the concepts can also be applied to other frameworks.

But first, what is Firebase?

Firebase is Google’s mobile platform for quickly developing apps. Firebase provides a suite of tools for authenticating applications, building reactive client apps, reporting analytics, as well as a host of other helpful resources for managing apps in general. It also provides back-end management for web, iOS, Android, and Unity, a 3D development platform.

Out of the box, Firebase is packaged with features that help developers like ourselves focus on building apps while it handles all server-side logic. Things like:

  • Authentication: This includes support for email and password authentication as well as single sign-on capabilities (via Facebook, Twitter and Google).
  • Realtime database: This is a “NoSQL” database that updates in real time.
  • Cloud functions: These run extra server-side logic.
  • Static hosting: This is a means of serving assets pre-built instead of rendering at runtime.
  • Cloud storage: This gives us a place to store media assets.

Firebase offers a generous free tier that includes authentication and access to their Realtime Database. The authentication providers we’ll be covering email and password — Google and GitHub — are free on that side as well. The Realtime Database allows up to 100 simultaneous connections and 1 gigabyte storage per month. A full table of pricing can be found on the Firebase website.

Here’s what we’re making

We’re going to build an application called Chatty. It will allow only authenticated users to send and read messages and users can sign up by providing their email and creating a password, or by authenticating through a Google or GitHub account. Check out source code if you want to refer to it or take a peek as we get started.

We’ll end up with something like this:

Setting up

You’re going to need a Google account to use Firebase, so snag one if you haven’t already. And once you do, we can officially kick the tires on this thing.

First off, head over to the Firebase Console and click the “Add project” option.

Next, let’s enter a name for the project. I’m going with Chatty.

You can choose to add analytics to your project, but it’s not required. Either way, click continue to proceed and Firebase will take a few seconds to delegate resources for the project.

Once that spins up, we are taken to the Firebase dashboard But, before we can start using Firebase in our web app, we have to get the configuration details down for our project. So, click on the web icon in the dashboard.

Then, enter a name for the app and click Register app.

Next up, we’ll copy and store the configuration details on the next screen in a safe place. That will come in handy in the next step.

Again, we’re going to authenticate users via email and password, with additional options for single sign-on with a Google or GitHub account. We need to enable these from the Authentication tab in the dashboard, but we’ll go through each of them one at a time.

Email and password authentication

There’s a Sign-in method tab in the Firebase dashboard. Click the Email/Password option and enable it.

Now we can use it in our app!

Setting up the web app

For our web app, we’ll be using React but most of the concepts can be applied to any other framework. Well need Node.js for a React setup, so download and install it if you haven’t already.

We’ll use create-react-app to bootstrap a new React project. This downloads and installs the necessary packages required for a React application. In the terminal, cd into where you’d like our Chatty project to go and run this to initialize it:

npx create-react-app chatty

This command does the initial setup for our react app and installs the dependencies in package.json. We’ll also install some additional packages. So, let’s cd into the project itself and add packages for React Router and Firebase.

cd chatty yarn add react-router-dom firebase

We already know why we need Firebase, but why React Router? Our chat app will have a couple of views we can use React Router to handle navigating between pages.

With that done, we can officially start the app:

yarn start

This starts a development server and opens a URL in your default browser. If everything got installed correctly, you should see a screen like this:

Looking at the folder structure, you would see something similar to this:

For our chat app, this is the folder structure we’ll be using:

  • /components: contains reusable widgets used in different pages
  • /helpers: a set of reusable functions
  • /pages: the app views
  • /services: third-party services that we’re using (e.g. Firebase)
  • App.js: the root component

Anything else in the folder is unnecessary for this project and can safely be removed. From here, let’s add some code to src/services/firebase.js so the app can talk with Firebase.

import firebase from 'firebase';

Let’s get Firebase into the app

We’ll import and initialize Firebase using the configuration details we copied earlier when registering the app in the Firebase dashboard. Then, we’ll export the authentication and database modules.

const config = {   apiKey: "ADD-YOUR-DETAILS-HERE",   authDomain: "ADD-YOUR-DETAILS-HERE",   databaseURL: "ADD-YOUR-DETAILS-HERE" }; firebase.initializeApp(config); export const auth = firebase.auth; export const db = firebase.database();

Let’s import our dependencies in src/App.js:

import React, { Component } from 'react'; import {   Route,   BrowserRouter as Router,   Switch,   Redirect, } from "react-router-dom"; import Home from './pages/Home'; import Chat from './pages/Chat'; import Signup from './pages/Signup'; import Login from './pages/Login'; import { auth } from './services/firebase';

These are ES6 imports. Specifically, we’re importing React and other packages needed to build out the app. We’re also importing all the pages of our app that we’ll configure later to our router.

Next up is routing

Our app has public routes (accessible without authentication) and a private route (accessible only with authentication). Because React doesn’t provide a way to check the authenticated state, we’ll create higher-order components (HOCs) for both types of routes.

Our HOCs will:

  • wrap a <Route>,
  • pass props from the router to the <Route>,
  • render the component depending on the authenticated state, and
  • redirect the user to a specified route if the condition is not met

Let’s write the code for our <PrivateRoute> HOC.

function PrivateRoute({ component: Component, authenticated, ...rest }) {   return (     <Route       {...rest}       render={(props) => authenticated === true         ? <Component {...props} />         : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}     />   ) }

It receives three props: the component to render if the condition is true, the authenticated state, and the ES6 spread operator to get the remaining parameters passed from the router. It checks if authenticated is true and renders the component passed, else it redirects to/login.

function PublicRoute({ component: Component, authenticated, ...rest }) {   return (     <Route       {...rest}       render={(props) => authenticated === false         ? <Component {...props} />         : <Redirect to='/chat' />}     />   ) }

The <PublicRoute> is pretty much the same. It renders our public routes and redirects to the /chat path if the authenticated state becomes true. We can use the HOCs in our render method:

render() {   return this.state.loading === true ? <h2>Loading...</h2> : (     <Router>       <Switch>         <Route exact path="/" component={Home}></Route>         <PrivateRoute path="/chat" authenticated={this.state.authenticated} component={Chat}></PrivateRoute>         <PublicRoute path="/signup" authenticated={this.state.authenticated} component={Signup}></PublicRoute>         <PublicRoute path="/login" authenticated={this.state.authenticated} component={Login}></PublicRoute>       </Switch>     </Router>   ); }

Checking for authentication

It would be nice to show a loading indicator while we verify if the user is authenticated. Once the check is complete, we render the appropriate route that matches the URL. We have three public routes — <Home>, <Login> and <Signup> — and a private one called <Chat>.

Let’s write the logic to check if the user is indeed authenticated.

class App extends Component {   constructor() {     super();     this.state = {       authenticated: false,       loading: true,     };   } }  export default App;

Here we’re setting the initial state of the app. Then, we’re using the componentDidMount lifecycle hook to check if the user is authenticated. So, let’s add this after the constructor:

componentDidMount() {   this.removelistener = auth().onAuthStateChanged((user) => {     if (user) {       this.setState({         authenticated: true,         loading: false,       });     } else {       this.setState({         authenticated: false,         loading: false,       });     }   }) }

Firebase provides an intuitive method called onAuthStateChanged that is triggered when the authenticated state changes. We use this to update our initial state. user is null if the user is not authenticated. If the user is true, we update authenticated to true; else we set it to false. We also set loading to false either way.

Registering users with email and password

Users will be able to register for Chatty through email and password. The helpers folder contains a set of methods that we’ll use to handle some authentication logic. Inside this folder, let’s create a new file called auth.js and add this:

import { auth } from "../services/firebase";

We import the auth module from the service we created earlier.

export function signup(email, password) {   return auth().createUserWithEmailAndPassword(email, password); } 
 export function signin(email, password) {   return auth().signInWithEmailAndPassword(email, password); }

We have two methods here: signup andsignin:

  • signup will create a new user using their email and password. 
  • signin will log in an existing user created with email and password.

Let’s create our <Signup> page by creating a new file Signup.js file in the pages folder. This is the markup for the UI:

import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import { signup } from '../helpers/auth'; 
 export default class SignUp extends Component { 
   render() {     return (       <div>         <form onSubmit={this.handleSubmit}>           <h1>             Sign Up to           <Link to="/">Chatty</Link>           </h1>           <p>Fill in the form below to create an account.</p>           <div>             <input placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>           </div>           <div>             <input placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>           </div>           <div>             {this.state.error ? <p>{this.state.error}</p> : null}             <button type="submit">Sign up</button>           </div>           <hr></hr>           <p>Already have an account? <Link to="/login">Login</Link></p>         </form>       </div>     )   } }
Email? Check. Password? Check. Submit button? Check. Our form is looking good.

The form and input fields are bound to a method we haven’t created yet, so let’s sort that out. Just before the render() method, we’ll add the following:

constructor(props) {   super(props);   this.state = {     error: null,     email: '',     password: '',   };   this.handleChange = this.handleChange.bind(this);   this.handleSubmit = this.handleSubmit.bind(this); }

We’re setting the initial state of the page. We’re also binding the handleChange and handleSubmit methods to the component’s this scope.

handleChange(event) {   this.setState({     [event.target.name]: event.target.value   }); }

Next up, we’ll add the handleChange method that our input fields are bound to. The method uses computed properties to dynamically determine the key and set the corresponding state variable.

async handleSubmit(event) {   event.preventDefault();   this.setState({ error: '' });   try {     await signup(this.state.email, this.state.password);   } catch (error) {     this.setState({ error: error.message });   } }

For handleSubmit, we’re preventing the default behavior for form submissions (which simply reloads the browser, among other things). We’re also clearing up the error state variable, then using the signup() method imported from helpers/auth to pass the email and password entered by the user.

If the registration is successful, users get redirected to the /Chats route. This is possible with the combination of onAuthStateChanged and the HOCs we created earlier. If registration fails, we set the error variable which displays a message to users.

Authenticating users with email and password

The login page is identical to the signup page. The only difference is we’ll be using the signin method from the helpers we created earlier. That said, let’s create yet another new file in the pages directory, this time called Login.js, with this code in it:

import React, { Component } from "react"; import { Link } from "react-router-dom"; import { signin, signInWithGoogle, signInWithGitHub } from "../helpers/auth"; 
 export default class Login extends Component {   constructor(props) {     super(props);     this.state = {       error: null,       email: "",       password: ""     };     this.handleChange = this.handleChange.bind(this);     this.handleSubmit = this.handleSubmit.bind(this);   } 
   handleChange(event) {     this.setState({       [event.target.name]: event.target.value     });   } 
   async handleSubmit(event) {     event.preventDefault();     this.setState({ error: "" });     try {       await signin(this.state.email, this.state.password);     } catch (error) {       this.setState({ error: error.message });     }   } 
   render() {     return (       <div>         <form           autoComplete="off"           onSubmit={this.handleSubmit}         >           <h1>             Login to             <Link to="/">               Chatty             </Link>           </h1>           <p>             Fill in the form below to login to your account.           </p>           <div>             <input               placeholder="Email"               name="email"               type="email"               onChange={this.handleChange}               value={this.state.email}             />           </div>           <div>             <input               placeholder="Password"               name="password"               onChange={this.handleChange}               value={this.state.password}               type="password"             />           </div>           <div>             {this.state.error ? (               <p>{this.state.error}</p>             ) : null}             <button type="submit">Login</button>           </div>           <hr />           <p>             Don't have an account? <Link to="/signup">Sign up</Link>           </p>         </form>       </div>     );   } }

Again, very similar to before. When the user successfully logs in, they’re redirected to /chat.

Authenticating with a Google account

Firebase allows us to authenticate users with a valid Google account. We’ve got to enable it in the Firebase dashboard just like we did for email and password.

Select the Google option and enable it in the settings.

On that same page, we also need to scroll down to add a domain to the list of domains that are authorized to access feature. This way, we avoid spam from any domain that is not whitelisted. For development purposes, our domain is localhost, so we’ll go with that for now.

We can switch back to our editor now. We’ll add a new method to helpers/auth.js to handle Google authentication.

export function signInWithGoogle() {   const provider = new auth.GoogleAuthProvider();   return auth().signInWithPopup(provider); }

Here, we’re creating an instance of the GoogleAuthProvider. Then, we’re calling signInWithPopup with the provider as a parameter. When this method is called, a pop up will appear and take the user through the Google sign in flow before redirecting them back to the app. You’ve likely experienced it yourself at some point in time.

Let’s use it in our signup page by importing the method:

import { signin, signInWithGoogle } from "../helpers/auth";

Then, let’s add a button to trigger the method, just under the Sign up button:

<p>Or</p> <button onClick={this.googleSignIn} type="button">   Sign up with Google </button>

Next, we’ll add the onClick handler:

async googleSignIn() {   try {     await signInWithGoogle();   } catch (error) {     this.setState({ error: error.message });   } }

Oh, and we should remember to bind the handler to the component:

constructor() {   // ...   this.githubSignIn = this.githubSignIn.bind(this); }

That’s all we need! When the button is clicked, it takes users through the Google sign in flow and, if successful, the app redirects the user to the chat route.

Authenticating with a GitHub account

We’re going to do the same thing with GitHub. May as well give folks more than one choice of account.

Let’s walk through the steps. First, we’ll enable GitHub sign in on Firebase dashboard, like we did for email and Google.

You will notice both the client ID and client secret fields are empty, but we do have our authorization callback URL at the bottom. Copy that, because we’ll use it when we do our next thing, which is register the app on GitHub.

Once that’s done, we’ll get a client ID and secret which we can now add to the Firebase console.

Let’s switch back to the editor and add a new method to helpers/auth.js:

export function signInWithGitHub() {   const provider = new auth.GithubAuthProvider();   return auth().signInWithPopup(provider); }

It’s similar to the Google sign in interface, but this time we’re creating a GithubAuthProvider. Then, we’ll call signInWithPopup with the provider.

In pages/Signup.js, we update our imports to include the signInWithGitHub method:

import { signup, signInWithGoogle, signInWithGitHub } from "../helpers/auth";

We add a button for GitHub sign up:

<button type="button" onClick={this.githubSignIn}>   Sign up with GitHub </button>

Then we add a click handler for the button which triggers the GitHub sign up flow:

async githubSignIn() {   try {     await signInWithGitHub();   } catch (error) {     this.setState({ error: error.message });   } }

Let’s remember again to bind the handler to the component:

constructor() {   // ...   this.githubSignIn = this.githubSignIn.bind(this); }

Now we’ll get the same sign-in and authentication flow that we have with Google, but with GitHub.

Reading data from Firebase

Firebase has two types of databases: A product they call Realtime Database and another called Cloud Firestore. Both databases are NoSQL-like databases, meaning the database is structured as key-value pairs. For this tutorial, we’ll use the Realtime Database.

This is the structure we’ll be using for our app. We have a root node chats with children nodes. Each child has a content, timestamp, and user ID. One of the tabs you’ll notice is Rules which is how we set permissions on the contents of the database.

Firebase database rules are defined as key-value pairs as well. Here, we’ll set our rules to allow only authenticated users to read and write to the chat node. There are a lot more firebase rules. worth checking out.

Let’s write code to read from the database. First, create a new file called Chat.js  in the pages  folder and add this code to import React, Firebase authentication, and Realtime Database:

import React, { Component } from "react"; import { auth } from "../services/firebase"; import { db } from "../services/firebase"

Next, let’s define the initial state of the app:

export default class Chat extends Component {   constructor(props) {     super(props);     this.state = {       user: auth().currentUser,       chats: [],       content: '',       readError: null,       writeError: null     };   }   async componentDidMount() {     this.setState({ readError: null });     try {       db.ref("chats").on("value", snapshot => {         let chats = [];         snapshot.forEach((snap) => {           chats.push(snap.val());         });         this.setState({ chats });       });     } catch (error) {       this.setState({ readError: error.message });     }   } }

The real main logic takes place in componentDidMount. db.ref("chats") is a reference to the chats path in the database. We listen to the value event which is triggered anytime a new value is added to the chats node. What is returned from the database is an array-like object that we loop through and push each object into an array. Then, we set the chats state variable to our resulting array. If there is an error, we set the readError state variable to the error message.

One thing to note here is that a connection is created between the client and our Firebase database because we used the .on() method. This means any time a new value is added to the database, the client app is updated in real-time which means users can see new chats without a page refresh Nice!.

After componentDidMount, we can render our chats like so:

render() {   return (     <div>       <div className="chats">         {this.state.chats.map(chat => {           return <p key={chat.timestamp}>{chat.content}</p>         })}       </div>       <div>         Login in as: <strong>{this.state.user.email}</strong>       </div>     </div>   ); }

This renders the array of chats. We render the email of the currently logged in user.

Writing data to Firebase

At the moment, users can only read from the database but are unable to send messages. What we need is a form with an input field that accepts a message and a button to send the message to the chat.

So, let’s modify the markup like so:

return (     <div>       <div className="chats">         {this.state.chats.map(chat => {           return <p key={chat.timestamp}>{chat.content}</p>         })}       </div>       {# message form #}       <form onSubmit={this.handleSubmit}>         <input onChange={this.handleChange} value={this.state.content}></input>         {this.state.error ? <p>{this.state.writeError}</p> : null}         <button type="submit">Send</button>       </form>       <div>         Login in as: <strong>{this.state.user.email}</strong>       </div>     </div>   ); }

We have added a form with an input field and a button. The value of the input field is bound to our state variable content and we call handleChange when its value changes.

handleChange(event) {   this.setState({     content: event.target.value   }); }

handleChange gets the value from the input field and sets on our state variable. To submit the form, we call handleSubmit:

async handleSubmit(event) {   event.preventDefault();   this.setState({ writeError: null });   try {     await db.ref("chats").push({       content: this.state.content,       timestamp: Date.now(),       uid: this.state.user.uid     });     this.setState({ content: '' });   } catch (error) {     this.setState({ writeError: error.message });   } }

We set any previous errors to null. We create a reference to the chats node in the database and use push() to create a unique key and pushe the object to it.

As always, we have to bind our methods to the component:

constructor(props) {   // ...   this.handleChange = this.handleChange.bind(this);   this.handleSubmit = this.handleSubmit.bind(this); }

Now a user can add new messages to the chats and see them in real-time! How cool is that?

Demo time!

Enjoy your new chat app!

Congratulations! You have just built a chat tool that authenticates users with email and password, long with options to authenticate through a Google or GitHub account.

I hope this give you a good idea of how handy Firebase can be to get up and running with authentication on an app. We worked on a chat app, but the real gem is the signup and sign-in methods we created to get into it. That’s something useful for many apps.

Questions? Thoughts? Feedback? Let me know in the comments!

The post Building a Real-Time Chat App with React and Firebase appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Get Started Building GraphQL APIs With Node

We all have a number of interests and passions. For example, I’m interested in JavaScript, 90’s indie rock and hip hop, obscure jazz, the city of Pittsburgh, pizza, coffee, and movies starring John Lurie. We also have family members, friends, acquaintances, classmates, and colleagues who also have their own social relationships, interests, and passions. Some of these relationships and interests overlap, like my friend Riley who shares my interest in 90’s hip hop and pizza. Others do not, like my colleague Harrison, who prefers Python to JavaScript, only drinks tea, and prefers current pop music. All together, we each have a connected graph of the people in our lives, and the ways that our relationships and interests overlap.

These types of interconnected data are exactly the challenge that GraphQL initially set out to solve in API development. By writing a GraphQL API we are able to efficiently connect data, which reduces the complexity and number of requests, while allowing us to serve the client precisely the data that it needs. (If you’re into more GraphQL metaphors, check out Meeting GraphQL at a Cocktail Mixer.)

In this article, we’ll build a GraphQL API in Node.js, using the Apollo Server package. To do so, we’ll explore fundamental GraphQL topics, write a GraphQL schema, develop code to resolve our schema functions, and access our API using the GraphQL Playground user interface.

What is GraphQL?

GraphQL is an open source query and data manipulation language for APIs. It was developed with the goal of providing single endpoints for data, allowing applications to request exactly the data that is needed. This has the benefit of not only simplifying our UI code, but also improving performance by limiting the amount of data that needs to be sent over the wire.

What we’re building

To follow along with this tutorial, you’ll need Node v8.x or later and some familiarity with working with the command line. 

We’re going to build an API application for book highlights, allowing us to store memorable passages from the things that we read. Users of the API will be able to perform “CRUD” (create, read, update, delete) operations against their highlights:

  • Create a new highlight
  • Read an individual highlight as well as a list of highlights
  • Update a highlight’s content
  • Delete a highlight

Getting started

To get started, first create a new directory for our project, initialize a new node project, and install the dependencies that we’ll need:

# make the new directory mkdir highlights-api # change into the directory cd highlights-api # initiate a new node project npm init -y # install the project dependencies npm install apollo-server graphql # install the development dependencies npm install nodemon --save-dev

Before moving on, let’s break down our dependencies:

  • apollo-server is a library that enables us to work with GraphQL within our Node application. We’ll be using it as a standalone library, but the team at Apollo has also created middleware for working with existing Node web applications in ExpresshapiFastify, and Koa.
  • graphql includes the GraphQL language and is a required peer dependency of apollo-server.
  • nodemon is a helpful library that will watch our project for changes and automatically restart our server.

With our packages installed, let’s next create our application’s root file, named index.js. For now, we’ll console.log() a message in this file:

console.log("📚 Hello Highlights");

To make our development process simpler, we’ll update the scripts object within our package.json file to make use of the nodemon package:

"scripts": {   "start": "nodemon index.js" },

Now, we can start our application by typing npm start in the terminal application. If everything is working properly, you will see 📚 Hello Highlights logged to your terminal.

GraphQL schema types

A schema is a written representation of our data and interactions. By requiring a schema, GraphQL enforces a strict plan for our API. This is because the API can only return data and perform interactions that are defined within the schema. The fundamental component of GraphQL schemas are object types. GraphQL contains five built-in types:

  • String: A string with UTF-8 character encoding
  • Boolean: A true or false value
  • Int: A 32-bit integer
  • Float: A floating-point value
  • ID: A unique identifier

We can construct a schema for an API with these basic components. In a file named schema.js, we can import the gql library and prepare the file for our schema syntax:

const { gql } = require('apollo-server');  const typeDefs = gql`   # The schema will go here `;  module.exports = typeDefs;

To write our schema, we first define the type. Let’s consider how we might define a schema for our highlights application. To begin, we would create a new type with a name of Highlight:

const typeDefs = gql`   type Highlight {   } `;

Each highlight will have a unique ID,  some content, a title, and an author. The Highlight schema will look something like this:

const typeDefs = gql`   type Highlight {     id: ID     content: String     title: String     author: String   } `;

We can make some of these fields required by adding an exclamation point:

const typeDefs = gql`   type Highlight {     id: ID!     content: String!     title: String     author: String   } `;

Though we’ve defined an object type for our highlights, we also need to provide a description of how a client will fetch that data. This is called a query. We’ll dive more into queries shortly, but for now let’s describe in our schema the ways in which someone will retrieve highlights. When requesting all of our highlights, the data will be returned as an array (represented as [Highlight]) and when we want to retrieve a single highlight we will need to pass an ID as a parameter.

const typeDefs = gql`   type Highlight {     id: ID!     content: String!     title: String     author: String   }   type Query {     highlights: [Highlight]!     highlight(id: ID!): Highlight   } `;

Now, in the index.js file, we can import our type definitions and set up Apollo Server:

const {ApolloServer } = require('apollo-server'); const typeDefs = require('./schema');  const server = new ApolloServer({ typeDefs });  server.listen().then(({ url }) => {   console.log(`📚 Highlights server ready at $ https://css-tricks.com/get-started-building-graphql-apis-with-node/`); });

If we’ve kept the node process running, the application will have automatically updated and relaunched, but if not, typing npm start  from the project’s directory in the terminal window will start the server. If we look at the terminal, we should see that nodemon is watching our files and the server is running on a local port:

[nodemon] 2.0.2 [nodemon] to restart at any time, enter `rs` [nodemon] watching dir(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node index.js` 📚 Highlights server ready at http://localhost:4000/

Visiting the URL in the browser will launch the GraphQL Playground application, which provides a user interface for interacting with our API.

GraphQL Resolvers

Though we’ve developed our project with an initial schema and Apollo Server setup, we can’t yet interact with our API. To do so, we’ll introduce resolvers. Resolvers perform exactly the action their name implies; they resolve the data that the API user has requested. We will write these resolvers by first defining them in our schema and then implementing the logic within our JavaScript code. Our API will contain two types of resolvers: queries and mutations.

Let’s first add some data to interact with. In an application, this would typically be data that we’re retrieving and writing to from a database, but for our example let’s use an array of objects. In the index.js file add the following:

let highlights = [   {     id: '1',     content: 'One day I will find the right words, and they will be simple.',     title: 'Dharma Bums',     author: 'Jack Kerouac'   },   {     id: '2',     content: 'In the limits of a situation there is humor, there is grace, and everything else.',     title: 'Arbitrary Stupid Goal',     author: 'Tamara Shopsin'   } ]

Queries

A query requests specific data from an API, in its desired format. The query will then return an object, containing the data that the API user has requested. A query never modifies the data; it only accesses it. We’ve already written a two queries in our schema. The first returns an array of highlights and the second returns a specific highlight. The next step is to write the resolvers that will return the data.

In the index.js file, we can add a resolvers object, which can contain our queries:

const resolvers = {   Query: {     highlights: () => highlights,     highlight: (parent, args) => {       return highlights.find(highlight => highlight.id === args.id);     }   } };

The highlights query returns the full array of highlights data. The highlight query accepts two parameters: parent and args. The parent is the first parameter of any GraqhQL query in Apollo Server and provides a way of accessing the context of the query. The args parameter allows us to access the user provided arguments. In this case, users of the API will be supplying an id argument to access a specific highlight.

We can then update our Apollo Server configuration to include the resolvers:

const server = new ApolloServer({ typeDefs, resolvers });

With our query resolvers written and Apollo Server updated, we can now query API using the GraphQL Playground. To access the GraphQL Playground, visit http://localhost:4000 in your web browser.

A query is formatted as so:

query {   queryName {       field       field     } }

With this in mind, we can write a query that requests the ID, content, title, and author for each our highlights:

query {   highlights {     id     content     title     author   } }

Let’s say that we had a page in our UI that lists only the titles and authors of our highlighted texts. We wouldn’t need to retrieve the content for each of those highlights. Instead, we could write a query that only requests the data that we need:

query {   highlights {     title     author   } }

We’ve also written a resolver to query for an individual note by including an ID parameter with our query. We can do so as follows:

query {   highlight(id: "1") {     content   } }

Mutations

We use a mutation when we want to modify the data in our API. In our highlight example, we will want to write a mutation to create a new highlight, one to update an existing highlight, and a third to delete a highlight. Similar to a query, a mutation is also expected to return a result in the form of an object, typically the end result of the performed action.

The first step to updating anything in GraphQL is to write the schema. We can include mutations in our schema, by adding a mutation type to our schema.js file:

type Mutation {   newHighlight (content: String! title: String author: String): Highlight!   updateHighlight(id: ID! content: String!): Highlight!   deleteHighlight(id: ID!): Highlight! }

Our newHighlight mutation will take the required value of content along with optional title and author values and return a Highlight. The updateHighlight mutation will require that a highlight id and content be passed as argument values and will return the updated Highlight. Finally, the deleteHighlight mutation will accept an ID argument, and will return the deleted Highlight.

With the schema updated to include mutations, we can now update the resolvers in our index.js file to perform these actions. Each mutation will update our highlights array of data.

const resolvers = {   Query: {     highlights: () => highlights,     highlight: (parent, args) => {       return highlights.find(highlight => highlight.id === args.id);     }   },   Mutation: {     newHighlight: (parent, args) => {       const highlight = {         id: String(highlights.length + 1),         title: args.title || '',         author: args.author || '',         content: args.content       };       highlights.push(highlight);       return highlight;     },     updateHighlight: (parent, args) => {       const index = highlights.findIndex(highlight => highlight.id === args.id);       const highlight = {         id: args.id,         content: args.content,         author: highlights[index].author,         title: highlights[index].title       };       highlights[index] = highlight;       return highlight;     },     deleteHighlight: (parent, args) => {       const deletedHighlight = highlights.find(         highlight => highlight.id === args.id       );       highlights = highlights.filter(highlight => highlight.id !== args.id);       return deletedHighlight;     }   } };

With these mutations written, we can use the GraphQL Playground to practice mutating the data. The structure of a mutation is nearly identical to that of a query, specifying the name of the mutation, passing the argument values, and requesting specific data in return. Let’s start by adding a new highlight:

mutation {   newHighlight(author: "Adam Scott" title: "JS Everywhere" content: "GraphQL is awesome") {     id     author     title     content   } }

We can then write mutations to update a highlight:

mutation {   updateHighlight(id: "3" content: "GraphQL is rad") {     id     content   } }

And to delete a highlight:

mutation {   deleteHighlight(id: "3") {     id   } }

Wrapping up

Congratulations! You’ve now successfully built a GraphQL API, using Apollo Server, and can run GraphQL queries and mutations against an in-memory data object. We’ve established a solid foundation for exploring the world of GraphQL API development.

Here are some potential next steps to level up:

The post Get Started Building GraphQL APIs With Node appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Building an Images Gallery using PixiJS and WebGL

Sometimes, we have to go a little further than HTML, CSS and JavaScript to create the UI we need, and instead use other resources, like SVG, WebGL, canvas and others.

For example, the most amazing effects can be created with WebGL, because it’s a JavaScript API designed to render interactive 2D and 3D graphics within any compatible web browser, allowing GPU-accelerated image processing.

That said, working with WebGL can be very complex. As such, there’s a variety of libraries that to make it relatively easier, such as PixiJSThree.js, and Babylon.js, among others.We’re going to work with a specific one of those, PixiJS, to create a gallery of random images inspired by this fragment of a Dribbble shot by Zhenya Rynzhuk.

This looks hard, but you actually don’t need to have advanced knowledge of WebGL or even PixiJS to follow along, though some basic knowledge of Javascript (ES6) will come in handy. You might even want to start by getting familiar with  the basic concept of fragment shaders used in WebGL, with The Book of Shaders as a good starting point.

With that, let’s dig into using PixiJS to create this WebGL effect!

Initial setup

Here’s what we’ll need to get started:

  1. Add the PixiJS library as a script in the HTML.
  2. Have a <canvas> element (or add it dynamically from Javascript), to render the application.
  3. Initialize the application with new PIXI.Application(options).

See, nothing too crazy yet. Here’s the JavaScript we can use as a boilerplate:

// Get canvas view const view = document.querySelector('.view') let width, height, app  // Set dimensions function initDimensions () {   width = window.innerWidth   height = window.innerHeight }  // Init the PixiJS Application function initApp () {   // Create a PixiJS Application, using the view (canvas) provided   app = new PIXI.Application({ view })   // Resizes renderer view in CSS pixels to allow for resolutions other than 1   app.renderer.autoDensity = true   // Resize the view to match viewport dimensions   app.renderer.resize(width, height) }  // Init everything function init () {   initDimensions()   initApp() }  // Initial call init()

When executing this code, the only thing that we will see is a black screen as well as a message like this in the if we open up the console:
PixiJS 5.0.2 - WebGL 2 - http://www.pixijs.com/.

We are ready to start drawing on the canvas using PixiJS and WebGL!

Creating the grid background with a WebGL Shader

Next we will create a background that contains a grid, which will allow us to clearly visualize the distortion effect we’re after. But first, we must know what a shader is and how it works.I recommended The Book of Shaders earlier as a starting point to learn about them and this is those concepts will come into play. If you have not done it yet, I strongly recommend that you review that material, and only then continue here.

We are going to create a fragment shader that prints a grid background on the screen:

// It is required to set the float precision for fragment shaders in OpenGL ES // More info here: https://stackoverflow.com/a/28540641/4908989 #ifdef GL_ES precision mediump float; #endif  // This function returns 1 if `coord` correspond to a grid line, 0 otherwise float isGridLine (vec2 coord) {   vec2 pixelsPerGrid = vec2(50.0, 50.0);   vec2 gridCoords = fract(coord / pixelsPerGrid);   vec2 gridPixelCoords = gridCoords * pixelsPerGrid;   vec2 gridLine = step(gridPixelCoords, vec2(1.0));   float isGridLine = max(gridLine.x, gridLine.y);   return isGridLine; }  // Main function void main () {   // Coordinates for the current pixel   vec2 coord = gl_FragCoord.xy;   // Set `color` to black   vec3 color = vec3(0.0);   // If it is a grid line, change blue channel to 0.3   color.b = isGridLine(coord) * 0.3;   // Assing the final rgba color to `gl_FragColor`   gl_FragColor = vec4(color, 1.0); }

This code is drawn from a demo on Shadertoy,  which is a great source of inspiration and resources for shaders.

In order to use this shader, we must first load the code from the file it is in and — only after it has been loaded correctly— we will initialize the app.

// Loaded resources will be here const resources = PIXI.Loader.shared.resources  // Load resources, then init the app PIXI.Loader.shared.add([   'shaders/backgroundFragment.glsl' ]).load(init)

Now, for our shader to work where we can see the result, we will add a new element (an empty Sprite) to the stage, which we will use to define a filter. This is the way PixiJS lets us execute custom shaders like the one we just created.

// Init the gridded background function initBackground () {   // Create a new empty Sprite and define its size   background = new PIXI.Sprite()   background.width = width   background.height = height   // Get the code for the fragment shader from the loaded resources   const backgroundFragmentShader = resources['shaders/backgroundFragment.glsl'].data   // Create a new Filter using the fragment shader   // We don't need a custom vertex shader, so we set it as `undefined`   const backgroundFilter = new PIXI.Filter(undefined, backgroundFragmentShader)   // Assign the filter to the background Sprite   background.filters = [backgroundFilter]   // Add the background to the stage   app.stage.addChild(background) }

And now we see the gridded background with blue lines. Look closely because the lines are a little faint against the dark background color.

The distortion effect 

Our background is now ready, so let’s see how we can add the desired effect (Cubic Lens Distortion) to the whole stage, including the background and any other element that we add later, like images. For this, need to create a new filter and add it to the stage. Yes, we can also define filters that affect the entire stage of PixiJS!

This time, we have based the code of our shader on this awesome Shadertoy demo that implements the distortion effectusing different configurable parameters.

#ifdef GL_ES precision mediump float; #endif  // Uniforms from Javascript uniform vec2 uResolution; uniform float uPointerDown;  // The texture is defined by PixiJS varying vec2 vTextureCoord; uniform sampler2D uSampler;  // Function used to get the distortion effect vec2 computeUV (vec2 uv, float k, float kcube) {   vec2 t = uv - 0.5;   float r2 = t.x * t.x + t.y * t.y;   float f = 0.0;   if (kcube == 0.0) {     f = 1.0 + r2 * k;   } else {     f = 1.0 + r2 * (k + kcube * sqrt(r2));   }   vec2 nUv = f * t + 0.5;   nUv.y = 1.0 - nUv.y;   return nUv; }  void main () {   // Normalized coordinates   vec2 uv = gl_FragCoord.xy / uResolution.xy;    // Settings for the effect   // Multiplied by `uPointerDown`, a value between 0 and 1   float k = -1.0 * uPointerDown;   float kcube = 0.5 * uPointerDown;   float offset = 0.02 * uPointerDown;      // Get each channel's color using the texture provided by PixiJS   // and the `computeUV` function   float red = texture2D(uSampler, computeUV(uv, k + offset, kcube)).r;   float green = texture2D(uSampler, computeUV(uv, k, kcube)).g;   float blue = texture2D(uSampler, computeUV(uv, k - offset, kcube)).b;      // Assing the final rgba color to `gl_FragColor`   gl_FragColor = vec4(red, green, blue, 1.0); }

We are using two uniforms this time. Uniforms are variables that we pass to the shader via JavaScript:

  • uResolution: This is a JavaScript object thaincludes {x: width, y: height}. This uniform allows us to normalize the coordinates of each pixel in the range [0, 1].
  • uPointerDown: This is a float in the range [0, 1], which allows us to animate the distortion effect, increasing its intensity proportionally.

Let’s see the code that we have to add to our JavaScript to see the distortion effect caused by our new shader:

// Target for pointer. If down, value is 1, else value is 0 // Here we set it to 1 to see the effect, but initially it will be 0 let pointerDownTarget = 1 let uniforms  // Set initial values for uniforms function initUniforms () {   uniforms = {     uResolution: new PIXI.Point(width, height),     uPointerDown: pointerDownTarget   } }  // Set the distortion filter for the entire stage const stageFragmentShader = resources['shaders/stageFragment.glsl'].data const stageFilter = new PIXI.Filter(undefined, stageFragmentShader, uniforms) app.stage.filters = [stageFilter]

We can already enjoy our distortion effect!

This effect is static at the moment, so it’s not terribly fun just yet. Next, we’ll see how we can make the effect dynamically respond to pointer events.

Listening to pointer events

PixiJS makes it’s surprisingly simple to listen to events, even multiple events that respond equally to mouse and touch interactions. In this case, we want our animation to work just as well on desktop as on a mobile device, so we must listen to the events corresponding to both platforms.

PixiJs provides an interactive attribute that lets us do just that. We apply it to an element and start listening to events with an API similar to jQuery:

// Start listening events function initEvents () {   // Make stage interactive, so it can listen to events   app.stage.interactive = true    // Pointer & touch events are normalized into   // the `pointer*` events for handling different events   app.stage     .on('pointerdown', onPointerDown)     .on('pointerup', onPointerUp)     .on('pointerupoutside', onPointerUp)     .on('pointermove', onPointerMove) }

From here, we will start using a third uniform (uPointerDiff), which will allow us to explore the image gallery using drag and drop. Its value will be equal to the translation of the scene as we explore the gallery. Below is the code corresponding to each of the event handling functions:

// On pointer down, save coordinates and set pointerDownTarget function onPointerDown (e) {   console.log('down')   const { x, y } = e.data.global   pointerDownTarget = 1   pointerStart.set(x, y)   pointerDiffStart = uniforms.uPointerDiff.clone() }  // On pointer up, set pointerDownTarget function onPointerUp () {   console.log('up')   pointerDownTarget = 0 }  // On pointer move, calculate coordinates diff function onPointerMove (e) {   const { x, y } = e.data.global   if (pointerDownTarget) {     console.log('dragging')     diffX = pointerDiffStart.x + (x - pointerStart.x)     diffY = pointerDiffStart.y + (y - pointerStart.y)   } }

We still will not see any animation if we look at our work, but we can start to see how the messages that we have defined in each event handler function are correctly printed in the console.

Let’s now turn to implementing our animations!

Animating the distortion effect and the drag and drop functionality

The first thing we need to start an animation with PixiJS (or any canvas-based animation) is an animation loop. It usually consists of a function that is called continuously, using requestAnimationFrame, which in each call renders the graphics on the canvas element, thus producing the desired animation.

We can implement our own animation loop in PixiJS, or we can use the utilities included in the library. In this case, we will use the add method of app.ticker, which allows us to pass a function that will be executed in each frame. At the end of the init function we will add this:

// Animation loop // Code here will be executed on every animation frame app.ticker.add(() => {   // Multiply the values by a coefficient to get a smooth animation   uniforms.uPointerDown += (pointerDownTarget - uniforms.uPointerDown) * 0.075   uniforms.uPointerDiff.x += (diffX - uniforms.uPointerDiff.x) * 0.2   uniforms.uPointerDiff.y += (diffY - uniforms.uPointerDiff.y) * 0.2 })

Meanwhile, in the Filter constructor for the background, we will pass the uniforms in the stage filter. This allows us to simulate the translation effect of the background with this tiny modification in the corresponding shader:

uniform vec2 uPointerDiff;  void main () {   // Coordinates minus the `uPointerDiff` value   vec2 coord = gl_FragCoord.xy - uPointerDiff;    // ... more code here ... }

And now we can see the distortion effect in action, including the drag and drop functionality for the gridd background. Play with it!

Randomly generate a masonry grid layout

To make our UI more interesting, we can randomly generate the sizing and dimensions of the grid cells. That is, each image can have different dimensions, creating a kind of masonry layout.

Let’s use Unsplash Source, which will allow us to get random images from Unsplash and define the dimensions we want. This will facilitate the task of creating a random masonry layout, since the images can have any dimension that we want, and therefore, generate the layout beforehand.

To achieve this, we will use an algorithm that executes the following steps:

  1. We will start with a list of rectangles.
  2. We will select the first rectangle in the list divide it into two rectangles with random dimensions, as long as both rectangles have dimensions equal to or greater than the minimum established limit. We’ll add a check to make sure it’s possible and, if it is, add both resulting rectangles to the list.
  3. If the list is empty, we will finish executing. If not, we’ll go back to step two.

I think you’ll get a much better understanding of how the algorithm works in this next demo. Use the buttons to see how it runs: Next  will execute step two, All will execute the entire algorithm, and Reset will reset to step one.

Drawing solid rectangles

Now that we can properly generate our random grid layout, we will use the list of rectangles generated by the algorithm to draw solid rectangles in our PixiJS application. That way, we can see if it works and make adjustments before adding the images using the Unsplash Source API.

To draw those rectangles, we will generate a random grid layout that is five times bigger than the viewport and position it in the center of the stage. That allows us to move with some freedom to any direction in the gallery.

// Variables and settings for grid const gridSize = 50 const gridMin = 3 let gridColumnsCount, gridRowsCount, gridColumns, gridRows, grid let widthRest, heightRest, centerX, centerY, rects  // Initialize the random grid layout function initGrid () {   // Getting columns   gridColumnsCount = Math.ceil(width / gridSize)   // Getting rows   gridRowsCount = Math.ceil(height / gridSize)   // Make the grid 5 times bigger than viewport   gridColumns = gridColumnsCount * 5   gridRows = gridRowsCount * 5   // Create a new Grid instance with our settings   grid = new Grid(gridSize, gridColumns, gridRows, gridMin)   // Calculate the center position for the grid in the viewport   widthRest = Math.ceil(gridColumnsCount * gridSize - width)   heightRest = Math.ceil(gridRowsCount * gridSize - height)   centerX = (gridColumns * gridSize / 2) - (gridColumnsCount * gridSize / 2)   centerY = (gridRows * gridSize / 2) - (gridRowsCount * gridSize / 2)   // Generate the list of rects   rects = grid.generateRects() }

So far, we have generated the list of rectangles. To add them to the stage, it is convenient to create a container, since then we can add the images to the same container and facilitate the movement when we drag the gallery.

Creating a container in PixiJS is like this:

let container  // Initialize a Container element for solid rectangles and images function initContainer () {   container = new PIXI.Container()   app.stage.addChild(container) }

Now we can now add the rectangles to the container so they can be displayed on the screen.

// Padding for rects and images const imagePadding = 20  // Add solid rectangles and images // So far, we will only add rectangles function initRectsAndImages () {   // Create a new Graphics element to draw solid rectangles   const graphics = new PIXI.Graphics()   // Select the color for rectangles   graphics.beginFill(0xAA22CC)   // Loop over each rect in the list   rects.forEach(rect => {     // Draw the rectangle     graphics.drawRect(       rect.x * gridSize,       rect.y * gridSize,       rect.w * gridSize - imagePadding,       rect.h * gridSize - imagePadding     )   })   // Ends the fill action   graphics.endFill()   // Add the graphics (with all drawn rects) to the container   container.addChild(graphics) }

Note that we have added to the calculations a padding (imagePadding) for each rectangle. In this way the images will have some space among them.

Finally, in the animation loop, we need to add the following code to properly define the position for the container:

// Set position for the container container.x = uniforms.uPointerDiff.x - centerX container.y = uniforms.uPointerDiff.y - centerY

And now we get the following result:

But there are still some details to fix, like defining limits for the drag and drop feature. Let’s add this to the onPointerMove event handler, where we effectively check the limits according to the size of the grid we have calculated:

diffX = diffX > 0 ? Math.min(diffX, centerX + imagePadding) : Math.max(diffX, -(centerX + widthRest)) diffY = diffY > 0 ? Math.min(diffY, centerY + imagePadding) : Math.max(diffY, -(centerY + heightRest))

Another small detail that makes things more refined is to add an offset to the grid background. That keeps the blue grid lines in tact. We just have to add the desired offset (imagePadding / 2 in our case) to the background shader this way:

// Coordinates minus the `uPointerDiff` value, and plus an offset vec2 coord = gl_FragCoord.xy - uPointerDiff + vec2(10.0);

And we will get the final design for our random grid layout:

Adding images from Unsplash Source

We have our layout ready, so we are all set to add images to it. To add an image in PixiJS, we need a Sprite, which defines the image as a Texture of it. There are multiple ways of doing this. In our case, we will first create an empty Sprite for each image and, only when the Sprite is inside the viewport, we will load the image, create the Texture and add it to the Sprite. Sound like a lot? We’ll go through it step-by-step.

To create the empty sprites, we will modify the initRectsAndImages function. Please pay attention to the comments for a better understanding:

// For the list of images let images = []  // Add solid rectangles and images function initRectsAndImages () {   // Create a new Graphics element to draw solid rectangles   const graphics = new PIXI.Graphics()   // Select the color for rectangles   graphics.beginFill(0x000000)   // Loop over each rect in the list   rects.forEach(rect => {     // Create a new Sprite element for each image     const image = new PIXI.Sprite()     // Set image's position and size     image.x = rect.x * gridSize     image.y = rect.y * gridSize     image.width = rect.w * gridSize - imagePadding     image.height = rect.h * gridSize - imagePadding     // Set it's alpha to 0, so it is not visible initially     image.alpha = 0     // Add image to the list     images.push(image)     // Draw the rectangle     graphics.drawRect(image.x, image.y, image.width, image.height)   })   // Ends the fill action   graphics.endFill()   // Add the graphics (with all drawn rects) to the container   container.addChild(graphics)   // Add all image's Sprites to the container   images.forEach(image => {     container.addChild(image)   }) }

So far, we only have empty sprites. Next, we will create a function that’s responsible for downloading an image and assigning it as Texture to the corresponding Sprite. This function will only be called if the Sprite is inside the viewport so that the image only downloads when necessary.

On the other hand, if the gallery is dragged and a Sprite is no longer inside the viewport during the course of the download, that request may be aborted, since we are going to use an AbortController (more on this on MDN). In this way, we will cancel the unnecessary requests as we drag the gallery, giving priority to the requests corresponding to the sprites that are inside the viewport at every moment.

Let’s see the code to land the ideas a little better:

// To store image's URL and avoid duplicates let imagesUrls = {}  // Load texture for an image, giving its index function loadTextureForImage (index) {   // Get image Sprite   const image = images[index]   // Set the url to get a random image from Unsplash Source, given image dimensions   const url = `https://source.unsplash.com/random/$ {image.width}x$ {image.height}`   // Get the corresponding rect, to store more data needed (it is a normal Object)   const rect = rects[index]   // Create a new AbortController, to abort fetch if needed   const { signal } = rect.controller = new AbortController()   // Fetch the image   fetch(url, { signal }).then(response => {     // Get image URL, and if it was downloaded before, load another image     // Otherwise, save image URL and set the texture     const id = response.url.split('?')[0]     if (imagesUrls[id]) {       loadTextureForImage(index)     } else {       imagesUrls[id] = true       image.texture = PIXI.Texture.from(response.url)       rect.loaded = true     }   }).catch(() => {     // Catch errors silently, for not showing the following error message if it is aborted:     // AbortError: The operation was aborted.   }) }

Now we need to call the loadTextureForImage function for each image whose corresponding Sprite is intersecting with the viewport. In addition, we will cancel the fetch requests that are no longer needed, and we will add an alpha transition when the rectangles enter or leave the viewport.

// Check if rects intersects with the viewport // and loads corresponding image function checkRectsAndImages () {   // Loop over rects   rects.forEach((rect, index) => {     // Get corresponding image     const image = images[index]     // Check if the rect intersects with the viewport     if (rectIntersectsWithViewport(rect)) {       // If rect just has been discovered       // start loading image       if (!rect.discovered) {         rect.discovered = true         loadTextureForImage(index)       }       // If image is loaded, increase alpha if possible       if (rect.loaded && image.alpha < 1) {         image.alpha += 0.01       }     } else { // The rect is not intersecting       // If the rect was discovered before, but the       // image is not loaded yet, abort the fetch       if (rect.discovered && !rect.loaded) {         rect.discovered = false         rect.controller.abort()       }       // Decrease alpha if possible       if (image.alpha > 0) {         image.alpha -= 0.01       }     }   }) }

And the function that verifies if a rectangle is intersecting with the viewport is the following:

// Check if a rect intersects the viewport function rectIntersectsWithViewport (rect) {   return (     rect.x * gridSize + container.x <= width &&     0 <= (rect.x + rect.w) * gridSize + container.x &&     rect.y * gridSize + container.y <= height &&     0 <= (rect.y + rect.h) * gridSize + container.y   ) }

Last, we have to add the checkRectsAndImages function to the animation loop:

// Animation loop app.ticker.add(() => {   // ... more code here ...    // Check rects and load/cancel images as needded   checkRectsAndImages() })

Our animation is nearly ready!

Handling changes in viewport size

When initializing the application, we resized the renderer so that it occupies the whole viewport, but if the viewport changes its size for any reason (for example, the user rotates their mobile device), we should re-adjust the dimensions and restart the application.

// On resize, reinit the app (clean and init) // But first debounce the calls, so we don't call init too often let resizeTimer function onResize () {   if (resizeTimer) clearTimeout(resizeTimer)   resizeTimer = setTimeout(() => {     clean()     init()   }, 200) } // Listen to resize event window.addEventListener('resize', onResize)

The clean function will clean any residuals of the animation that we were executing before the viewport changed its dimensions:

// Clean the current Application function clean () {   // Stop the current animation   app.ticker.stop()   // Remove event listeners   app.stage     .off('pointerdown', onPointerDown)     .off('pointerup', onPointerUp)     .off('pointerupoutside', onPointerUp)     .off('pointermove', onPointerMove)   // Abort all fetch calls in progress   rects.forEach(rect => {     if (rect.discovered && !rect.loaded) {       rect.controller.abort()     }   }) }

In this way, our application will respond properly to the dimensions of the viewport, no matter how it changes. This gives us the full and final result of our work!

Some final thoughts

Thanks for taking this journey with me! We walked through a lot but we learned a lot of concepts along the way and walked out with a pretty neat piece of UI. You can check the code on GitHub, or play with demos on CodePen.

If you have worked with WebGL before (with or without using other libraries), I hope you saw how nice it is working with PixiJS. It abstracts the complexity associated with the WebGL world in a great way, allowing us to focus on what we want to do rather than the technical details to make it work.

Bottom line is that PixiJS brings the world of WebGL closer for front-end developers to grasp, opening up a lot of possibilities beyond HTML, CSS and JavaScript.

The post Building an Images Gallery using PixiJS and WebGL appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Building the Web We Want

On the Microsoft Edge team, we’re committed to an open web and helping to drive innovation forward, which is why we’ve kicked off a new initiative in collaboration with Google, Mozilla, Samsung Internet, Igalia and — most importantly — the web community, called The Web We Want.

The Web We Want is an open initiative for web developers and designers (or anyone who builds things for the web) to tell browser vendors what we should focus on building or fixing. The overarching question we’re asking is this: If you had a magic wand and could change anything about the web platform, what would it be? What are problems you’ve encountered with the web that you need to hack around or what tools need to be improved or built to help you do your job better? What’s something you think you should be able to do natively but can’t? These are just a few questions to get you thinking about submissions to the initiative.

Some of the submissions we’ve had so far span from specific feature requests to broader problems with the web platform:

This is just a snapshot of the feedback we’re getting. The whole list is available for you to browse.

Why should I submit to the Web We Want? 

This is an opportunity to make your voice heard and help us determine where the future of the web platform is headed. Once you’ve submitted your problem or feature, we’ll determine if your want is something that browser vendors can tackle directly or if it’s something that needs more scoping and is suited for Web Standards discussions.

This is an opportunity for all of us to take a step back and reassess where the future of the web is heading and figure out where the gaps are that make building for the web difficult today.

How can I participate?

There are a few different ways you can participate and there are a few different components to the Web We Want. First, think about what you think browser vendors should go fix and submit your ideas at webwewant.fyi. And that could be all that you want to do, which is great! We want any and all feedback about the platform and we have folks from different browsers constantly watching what’s being submitted. 

There’s a second, optional aspect to the Web We Want, which is a great way to get involved in the web community and dip your toes in the public speaking pool. We’ve been running the Web We Want as a community-focused panel session at conferences around the world. 

If you submit and are attending one of the events we’ll be at, your submission could be picked to be presented in a 3-5 minute lightning pitch to a panel of judges during the session, like an episode of Shark Tank but for tech. The live sessions we run are a way to get feedback and opinions from industry experts like Rachel AndrewJen SimmonsMiriam Suzanne, and Marcy Sutton.

Even if you’re not attending one of the events in person, you can still participate! We want to be mindful that not everyone can attend conferences so if you your submission is selected and you indicate you’re not attending an event,you’ll have an opportunity to record your lightning talk ahead of time and we’ll include it in the live session. 

The live session culminates with the judges deliberating and deciding on the most pressing “want” pitched during the session. We also have a community voting aspect that allows the event audience to rank what they think is the most pressing thing for browser vendors (or standards bodies) to work on. Even if you’re not attending an event, you can still vote for your favorite “wants” on the website as well as by sharing them on social media. 

So far we’ve run sessions at An Event Apart DC, Smashing Conf, and View Source, and the community participation and panel discussion of wants has been fantastic. If you run a meetup or a conference and are interested in facilitating this session, reach out to Stephanie Stimac or Aaron Gustafson

The Web We Want

We’re at a point in the web platform where browser vendors and standards bodies are eager to know what it is web developers and designers need in the platform. We need to know where to invest resources. Hop over to the Web We Want and let us know what you think the web platform needs so that we can shape the future of the web together. 

The post Building the Web We Want appeared first on CSS-Tricks.

CSS-Tricks

,
[Top]

Building an accessible autocomplete control

Here’s a great in-depth post from Adam Silver about his journey to create an autocomplete field that’s as accessible as possible. There are so many edge cases to consider! There are old browsers and their peculiar quirks, there are accessibility best practices for screen readers, and not to mention dealing with the component design when there’s no JavaScript, etc.

Adam offers a warning before he begins:

[…] I’ve been looking at ways to let users enter a destination country. Unfortunately, native HTML form controls just aren’t good enough for this type of interaction. And so we need to build a custom autocomplete control from scratch. A word of warning though: this is one of the hardest UI components I’ve ever had to make—they’re just way harder than they look.

I also just bought Adam’s book, Form Design Patterns, and this post now makes me extra excited to read it.

Direct Link to ArticlePermalink

The post Building an accessible autocomplete control appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Building Multi-Directional Layouts

There are some new features in CSS that can assist us with building layouts for different directions and languages with ease. This article is about CSS logical properties and values (e.g. margin-inline-start).  These are a W3C working draft that still going under heavy editing, but have shipped in many browsers. I want to talk about this because I’ve been using these properties for a little while and have seen a significant boost to my workflow after switching to them.

I’ll talk about the specifications and how can you use it today in your work. I live in Egypt where we use Arabic as a primary language. Arabic is written from right to left, which means Arabic websites look like a mirror image of an English version. Most of the websites we create are bilingual, which means we provide a stylesheet specific for each direction. We do that by flipping values and properties of almost everything! I will not talk in details about this part but you can talk a quick look about a past article I wrote on the topic. 

It starts with declaring the dir attribute on the HTML tag. 

 <html dir="rtl">

This attribute accepts one of two values: ltr (which is the default value if none is specified) and rtl. According to its value, the browser starts to paint the elements following a specific algorithm. Text will be written with respect to the direction and punctuations will be placed in their correct location. Some elements, like tables, will have their direction switched (for example, a <td> starting from the right in rtl). Thankfully, some new specifications, like CSS Grid, and flexbox follow a similar approach to the table. That means we don’t have to change the order of anything because the browser will take care of it!

HTML5 introduced a new auto value for the dir attribute. It will check for the first character within the element and, if it belongs to a language that is written from left-to-right (like Latin characters), the element will have an ltr direction and vice versa. The W3C urges authors to avoid relying on this value to determine text direction and use a server-side solution instead. 

An interesting use case for the auto value is when you’re unsure about the direction of the content, such user-generated content, like a comment thread. I see a lot of people contributing to discussions in Arabic websites in English. The support for auto is pretty good except, for Internet Explorer and Edge.

Introducing the :dir() pseudo-class

The :dir() pseudo-class is a new selector modifier that selects an element by evaluating its direction value. It works like this:

/* Select all paragraphs that have their computed direction value set to rtl */ p:dir(rtl) {   font-size: 16px; /* Sometimes Arabic glyphs need a size boost to feel right. */ } 
 /* Select all paragraphs that have their computed direction value set to ltr */ p:dir(ltr) {   font-size: 14px; }

The beauty of this selector is that it’s the first one to select elements based on a computed value. Whether the direction of the element was inherited from the HTML dir attribute or set using CSS like html:lang("ar") { direction: rtl; }, the selector will match the element. Even better, if you have the direction of the element set to auto, it will still correctly the element based on its content!

<style>   p {     direction: auto;   }   p:dir(ltr) {     background: green;   }   p:dir(rtl) {     background: red;   } </style> 
 <!-- The following paragraph will have a green background --> <p>This is a paragraph that starts with a latin character</p> <!-- The following paragraph will have a red background --> <p>هذا النص يستخدم حروف عربية</p>

Sadly, the support for :dir() isn’t great and limited only to Firefox.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

Chrome Opera Firefox IE Edge Safari
No No 17* No No No

Mobile / Tablet

iOS Safari Opera Mobile Opera Mini Android Android Chrome Android Firefox
No No No No No 68

Even if the browser support was great, the selector only allows you to target elements and manually apply certain styles to them. That means that we still should flip the values for everything (like margins, paddings, floats, positions, etc.) which doesn’t really enhance our workflow or reduce the effort to produce multi-directional layouts.

Introducing CSS logical properties and values

As defined by the W3C, logical properties and values provide us with the ability to control layout through logical, rather than physical, direction and dimension mappings. Let’s skip the technical jargon and jump directly to the details. These provide us with new properties and values that will evaluate differently according to certain conditions.

Logical values

Let’s say we have a paragraph that we want to align in a direction that’s opposite to the natural direction of the language. Let’s say this is in English that follows the ltr direction. We would do something like this:

<article>   <p class="opposite">     Lorem ipsum dolor sit amis ..   </p> </article>

And the CSS file would be like this:

.opposite {   text-align: right; }

To provide the opposite for the rtl version, we would override the selector by targeting the <html> tag with the dir attribute, or simply provide a different file for the rtl version, like this:

html[dir="rtl"] .opposite {   text-align : left; }

Logical properties and values were created to solve this problem. Why don’t we use values that evaluates to the correct context instead of using left and right? In an ltr element, the value left means the beginning or the start of the element while on the rtl element, the value right means the start! It’s simple, right?

So instead of what we wrote before, we can use:

.opposite {   text-align: end; }

And that’s it! If the element’s computed direction is ltr, the text would be aligned right; and the computed direction would be opposite for the rtl elements. So, instead of using left and right values for text-align, we can simply replace it with start and end. This is a lot easier and more convenient than our previous options.

Logical properties

What we just looked at were logical values, so let’s turn now to logical properties. Logical properties are new properties that have the same idea; they evaluate differently according to the direction of the element. Take a look at margin as an example. Previously, we wanted to add some space toward the start of the paragraph. We can do so in the ltr document by using:

article img {   margin-left: 15px; }

Now, in the case of the rtl version, we will need to add the margin to the opposite direction in addition to resetting the left value:

html[dir="rtl"] article img {   margin-left: 0;   margin-right: 15px; }

We can do better with logical properties. Consider the following:

article img {   margin-inline-start: 15px; }

The -inline-start part evaluates to the beginning of the horizontal axis of the image. In the case of ltr, that means left, and in the case of rtl, that means right.

The start and end are probably obvious by now, but what is with the word inline and why do we need it? To understand it, we need to talk about something called CSS writing modes. Jen Simmons wrote an excellent article on that topic. I won’t regurgitate everything explained there, but the bottom line is that we can use writing modes to define the writing direction. Some languages, like the Chinese, Korean, and Japanese, can be written vertically from top to bottom. CSS writing modes allow us to control that flow. Take a look at the following paragraph:

You can clearly identify the top, bottom, left and right edges of the block. What will happen if we change the direction of the paragraph using CSS writing modes to flow vertically?

When we talk about the “top” of this rotated paragraph, do we mean the where the content begins or what was the right edge when it wasn’t rotated? Identifying the four directions becomes confusing. Let’s look at them from a different perspective. In “normal” writing conditions, the vertical axis will have the suffix -block and the horizontal axis will have the suffix -inline, both followed by start or end:

And if we rotate it, it should be like this:

Since we are talking about normal horizontal layout, we will be using -inline-start and -inline-end, but it is good to know about the other properties as well. We can also write logical shorthand values by using the logical keyword. Consider the following:

article img {   margin: logical 10px 20px 30px 40px; }

The computed value of the image’s margin will be the following:

article img {   margin-block-start: 10px;   margin-inline-start: 20px;   margin-block-end: 30px;   margin-inline-end: 40px; }

The logical keyword is an experimental feature at this point in time, which means it may change or it may be replaced by the time the spec becomes official. There’s an open discussion on the topic that you can follow in the meantime.

Logical properties also allow us to apply values to a certain axis, meaning we have margin-inline and margin-block for the horizontal and vertical axises, respectively.

Property Logical Property
margin-top margin-block-start
margin-left margin-inline-start
margin-right margin-inline-end
margin-bottom margin-block-end

Just like with the logical margin properties, we also have logical padding properties which follow the same rules as the margin.

Property Logical Property
padding-top padding-block-start
padding-left padding-inline-start
padding-right padding-inline-end
padding-bottom padding-block-end

Logical positioning properties

In the previous examples we were able to modify the meaning of the property by appending suffixes, but what about the positions? The properties names changed completely from what we know now as top, right, bottom, and left.

.element {   position: absolute;   inset-block-start: 0;  /* evaluates to top */   inset-block-end: 0;    /* evaluates to bottom */   inset-inline-start: 0; /* evaluates to left in ltr and right in rtl */   inset-inline-end: 0;   /* evaluates to right in ltr and left in rtl */ }

Learning new properties and values can be hard but, hey, we get a shorthand property called inset to make it a little easier:

/* Shorthand FTW! */ .element {   position: absolute;   inset: logical 10px 20px 30px 40px; } 
 /* It evaluates to this */ .element {   position: absolute;   inset-block-start: 10px;   inset-inline-start: 20px;   inset-block-end: 30px;   inset-inline-end: 40px; }

inset supports both inset-block and inset-inline just like margin and padding.

Property Logical Property
top inset-block-start
left inset-inline-start
right inset-inline-end
bottom inset-block-end

Logical border properties

Border properties can also become logical by appending the -inline-start and -block-start.

Property Logical Property
border-top{-size|style|color} border-block-start{-size|style|color}
border-left{-size|style|color} border-inline-start{-size|style|color}
border-right{-size|style|color} border-inline-end{-size|style|color}
border-bottom{-size|style|color} border-block-end{-size|style|color}

In her deep dive on logical properties and values, Rachel Andrew includes an excellent demo that demonstrates logical border properties and how they respond to different writing modes.

How can we start using all this today?

We can start using all the magic of logical properties and value today, thanks to the power of PostCSS! Jonathan Neal wrote this lovely PostCSS plugin that enables us to write logically and compile the code to something today’s browsers will understand. The plugin works in three stages:

  • It translates the new syntax to existing standards that unsupported browsers will recognize, using the :dir pseudo-class to create output to ltr and rtl.
  • It uses another one of Neal’s plugins to translate :dir to an attribute selector, like this:
 .element:dir(ltr) {    ...  }  [dir="ltr"] .element {    ...  }
  • It uses the postcss-nested plugin to transform nested selectors to one-line selectors, the same way other CSS preprocessors do.

PostCSS works with any workflow. You can try it with Grunt, Gulp, and webpack.


I will close by saying I have seen a lot of benefits since making the shift to logical properties and values. Sure, building multi-directional layouts takes time. There’s the learning curve, the addition of more properties to write, and of course, testing. Our previous methods for creating multi-directional layouts were either taking care of both directions in development or working on one direction at a time — neither of which is all that suitable for big projects. With logical properties and values you write your code once and it works for both directions without any consideration.

References

The post Building Multi-Directional Layouts appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

How Building in the Open Can Change Our Industry

I have to admit, I’m a developer who hasn’t built a website. When I first read Chris’s question, I sat in silence for at least a minute. Which technical topic did I want to discuss? A new library, programming language or best practice? Nothing, in particular, came to mind. Is that because I’m a new developer?

I’ve been coding for about one year now and got my first job a month ago. Even though I’ve been coding for that time, I wouldn’t say I’ve built a website. I’ve contributed to a couple of open-source projects whose output was websites, but I’ve spent a lot of time practicing technical tests in order to get into the industry and now I’m writing Kotlin for the Guardian Newspaper’s Android application.

After a couple minutes thinking about this question, I realized I wanted to write about who gets to build websites and how and where we choose to build them in order to welcome new people. I’ve spent this year giving conference talks on this topic because I have first-hand knowledge of what it’s like to become a developer with little time and money. It’s not easy being on the “outside” trying to get into our industry. How can we make it easier for new people to join us here? How can we welcome under-represented groups to the table? In 2020 you can make a huge difference to our industry by welcoming new developers, especially those from under-represented groups.

It’s been five years since the most well-known tech companies first released diversity reports, revealing that workforces were overwhelmingly white or Asian men. Despite their business successes, though, none of these big tech companies have made much progress in diversifying their workforces.

In 2014, Apple, one of the largest tech companies by revenue, had 20% women in its technical staff. This increased to only 23% in 2018 (Apple). At Google, the share of US technical employees who are black was 2.0% in 2014 and only rose to 2.8% in 2018 (Google). At Facebook in the US, there were 3% Hispanic technical staff in 2014. Last year there were 3.1% (Facebook).

Continuing our homogenous engineering community is a risk. We are less likely to build products best for our diverse user groups. For example, there have been numerous reports of facial recognition systems misidentifying black people. A US Government study found a top-performing system misidentified black people 5-10x more than white people. In addition, “according to a 2011 study by the National Institute of Standards and Technologies (Nist), facial recognition software is actually more accurate on Asian faces when it’s created by firms in Asian countries, suggesting that who makes the software strongly affects how it works” (Guardian 2017).

Thankfully, there are a number of things you can do in 2020 to contribute to a more diverse engineering community. Building websites in the open, in ways that welcome new people, can have a hugely positive impact on our industry and on the websites that we as an engineering industry produce.

First, how can building open source websites help us welcome new people? You can help with this by being a great open-source citizen and upholding best practices by giving positive and constructive code reviews, keeping documentation up to date and useful, offering help to new contributors and remote pairing with them if at all possible. Some amazing developers like Suz Hinton (@noopkat) live stream their open source contributions, which is phenomenal.

GitHub’s research has shown that documentation is highly valued, but frequently overlooked. As a new contributor, I really value clear and useful documentation, especially on installation, raising a PR and where to get help if I get stuck. Mozilla found that the most significant barrier to engaging in on-ramping others is unclear communications and unfriendly community. Using positive language in your documentation can really encourage first-time contributors to your project. For example, by expressly indicating that you welcome new contributors with instructions on how they can get involved.

We want to create an engineering community highly attractive to under-represented groups. One of the ways that we can do this is by supporting new developers to get a foothold into the profession. There are so many ways we can do this!

The first thing we can do with new developers is to help them by pairing and giving code reviews. Open source projects are perfect for this! By giving constructive code reviews you can help newbies level up their coding skills. You can also set up a Slack channel for your repo where you can answer technical questions. I have met superstar open source heroes who spent time pairing with me, which, if available to you, is an amazing way to help new people.

The greatest challenge I faced when teaching myself to code was finding a job. You can give new developers an insight into your company or your day to day working life by writing an article on Medium, posting a Twitter thread, or making a YouTube video. Some developers offer their own office hours which they do on a live stream or some offer a few one-off mentoring calls with new developers.

Who we welcome into the room to build websites with us says something about who we are and our values. In 2020, let’s welcome new people, especially those from under-represented groups to join us. We’ve discussed how amazing open-source projects are for this and how we can practically contribute to supporting new people. Let’s challenge ourselves to support at least one person from an under-represented group trying to get into the engineering industry in 2020. Together we can change who has the privilege to build the web.

The post How Building in the Open Can Change Our Industry appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

We asked web developers we admire: “What about building websites has you interested this year?”

For the first time ever here on CSS-Tricks, we’re going to do an end-of-year series of posts. Like an Advent calendar riff, only look at us, we’re beating the Advent calendar rush! We’ll be publishing several articles a day from a variety of web developers we look up to, where gave them all the same prompt:

What about building websites has you interested this year?

We’re aiming for a bit of self-reflection and real honesty. As in, not what you think you should care about or hot takes on current trends, but something that has quite literally got you thinking. Our hope is that all put together, the series paints an interesting picture of where we are and where we’re going in the web development industry.

We didn’t ask people for their future predictions directly. Our hope is that the future is glimpsable through seeing what is commanding developer attention. I mention that as this series take some inspiration from NeimanLab’s series that run each year (e.g. 2019, 2018, 2017…) which directly asks for people’s predictions about journalism. Maybe we’ll try that one year!

Automattic has a been a wonderful partner to us for a while now, and so I’m using this series as another way to thank them for that. Automattic are the makers of WordPress.com and big contributors to WordPress itself, which this site runs on, and make premium plugins like WooCommerce and Jetpack, which we also use.

Stay tuned here on the blog for all the wonderful thoughts from developers we’ll be publishing this week (hey even RSS is still cool I heard) or bookmark the homepage for the series.

The post We asked web developers we admire: “What about building websites has you interested this year?” appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , , , ,
[Top]

Let Mavo Shine in Building Interactive Web Applications

As you could guess from the title, this tutorial is dedicated to Mavo: a new, approachable way to create complex, reactive, persistent web applications just by writing HTML and CSS, without a single line of JavaScript and no server backend.

The app we will build together is a flashcard study app for foreign language learners. It is a fully-fledged CRUD application that lets you:

  • Create, delete, update flashcards and re-arrange them via drag and drop.
  • Import and export flashcards.
  • Evaluate how well you’ve learned the word on a flashcard.

Here is what our finished application looks like:

In the tutorial, I will guide you through the entire process of building the app.

At the end of some steps, I provide suggestions for you to experiment with Mavo—to learn a bit more—and to make some enhancements to the application we are building.

Are you ready? Let’s get started! 😀

Static template

See the Pen
01. Static template
by Dmitry Sharabin (@dsharabin)
on CodePen.

In order to illustrate how Mavo enhances standard HTML, we will first create a purely static HTML page and then use Mavo to turn this static HTML into a fully-functional web application.

Assume we have the following HTML code inside <body>:

<header>   <h1>Flashcards</h1> </header>  <main> <article>   <p>Word or phrase</div>   <p>Translation</div> </article> </main>

In that code, the <article> element represents a single flashcard.

Let’s add some styling in order to make our HTML look more like an actual flashcards app. You can take a look at the source code and play with it here.

Getting started with Mavo

Right now, all we have is a static template. It’s time to add functionality, so it can actually work like a flashcard application. Here comes Mavo!

In order to use Mavo, we first need to include its JavaScript and CSS files in our page’s <head> section:

<head>   ...   <script src="https://get.mavo.io/mavo.min.js"></script>   <link rel="stylesheet" href="https://get.mavo.io/mavo.css">   ... </head>

Defining a Mavo app

To enable Mavo functionality on an HTML structure, we must use the mv-app attribute on the element that contains our Mavo app (which may even be the <body> or <html> element, that’s fine!). Its value is an ID for our app that should be unique in the page.

Considering that the <main> element is representing our Mavo app, let’s add the mv-app attribute to it and give our app the ID “flashcards”:

<main mv-app="flashcards">   ... </main>

The property attribute

See the Pen
02. The property attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

It’s time to tell Mavo what elements of our app are important, i.e., which elements we want to be editable and be saved.

Now we have two such elements, and they are the <p> elements. Let’s add the property attribute to those elements to tell Mavo they contain data. Elements with the property attribute are called properties.

Keep in mind that the value of the property attribute should describe the element, similarly to an id or class attribute:

...    <p property="source">Word or phrase</div>   <p property="translation">Translation</div>  ...

Notice any changes in our app? The Mavo toolbar with an Edit button appeared at the top of the page. The Edit button lets the user switch between read and edit modes. Now our app is in read mode. That means we can’t edit the data on the page.

Now lets us switch to edit mode by clicking the Edit button. What has changed? The text of the Edit button becomes Editing to indicate that we are in edit mode. If you hover over the paragraphs, Mavo communicates that you can click to edit them by highlighting them in yellow. Go ahead! Click the text and edit it. Wow! We can change the content of the page right in place!

💻 Let’s get our hands dirty!

Assume that in addition to the word and its translation, the flashcard should have an example of the word’s usage in a sentence. Enhance the app by adding the appropriate elements to the flashcard.

The mv-multiple attribute

See the Pen
03. The mv-multiple attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

At this point, we have only one flashcard in our app. That’s not very useful! For a working flashcard application, we need the ability to add, delete, and rearrange flashcards. How can we do that? We could create more flashcards by adding more <article> elements to our code, but then how does an end user create and delete flashcards?

Fortunately, Mavo has something to offer that makes this a breeze: the mv-multiple attribute, which tells Mavo that certain elements can be multiplied. It converts the element it’s used on to an editable collection of items and generates (customizable) UI for adding, deleting, and rearranging items.

Let’s use the mv-multiple attribute in our app to convert our lonely flashcard into a collection of flashcards:

<article property="flashcard" mv-multiple>   ...     </article>

Now switch the app to edit mode. Note that below the flashcard, there is now an Add flashcard button. Let’s give it a try: create a few flashcards with the help of that button. Now we can dynamically add new elements right in the app, even though there are no corresponding elements in the HTML. But that is not all!

Try hovering over a flashcard and notice the three buttons that appear near its top right corner for adding, deleting and rearranging elements via a drag and drop handle. And by hovering over any item bar button, we can understand which flashcard they correspond: Mavo highlights it. Isn’t that amazing?

The mv-storage attribute

See the Pen
04. The mv-storage attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

Now that we have the basic UI in place, let’s try the following:

  • Switch to edit mode (if you haven’t already done so).
  • Edit the first flashcard’s source word and translation. Add a couple more flashcards too.
  • Switch the app back to read mode.
  • And finally… refresh the page.

What?! Where did our data go? Wasn’t Mavo supposed to save it? What happened?

Actually, we never told Mavo if or where to store our data!

To do so, we need to use the mv-storage attribute. What options do we have? Well, Mavo opens great possibilities for us, and Mavo plugins open up even more!

In our application, we are going to store the data in the browser’s localStorage, which is one of the simplest options available, so it’s good for our first Mavo app. We just need to add the attribute mv-storage with the value local to the element with the mv-app attribute (also called the Mavo root).

<main mv-app="flashcards" mv-storage="local">   ... </main>

Have a look at the Mavo toolbar. Notice something? Another button appeared—the Save button.

Try to edit the app data one more time. Note that the Save button is now highlighted. Hover over the Save button, and Mavo will highlight the properties with the unsaved data. Isn’t that cool?

Click the Save button and refresh the page (there is no need to switch to read mode before refreshing the page). Is your data still there? Great! We are one step closer to our goal—a fully-fledged flashcard application.

The mv-autosave attribute

Now we have to click the Save button every time we need our data to be saved? That may be safer, to prevent destroying valuable data, but it can often be inconvenient. Can we just save the data automatically? Sure! To save the data automatically every time it is changed, we can use the mv-autosave attribute on our Mavo root. Its value is the number of seconds to throttle saving by. Let’s add mv-autosave="3" to the root element of our app:

<main mv-app="flashcard" mv-storage="local" mv-autosave="3">   ... </main>

Change the data one more time and have a look at the Save button. See? In the beginning, it was highlighted but after 3 seconds–it is not. All our data is now saved automatically!

So, now the main part of our app would look like that:

<main mv-app="flashcards" mv-storage="local" mv-autosave="3">   <article property="flashcard" mv-multiple>     <p property="source">Word or phrase</div>     <p property="translation">Translation</div>   </article> </main>
💻 Let’s get our hands dirty!

We are almost done with the alpha version of our app. Now it’s your turn to make the app even better. No worries, you have all the knowledge you need.

Enhance the app so as flashcards could be organized by end users in different groups related to various topics, e.g., the users could gather all the flashcards corresponding to clothing in one group, all the flashcards associated with kitchen utensils in the other one, etc.

💡 Hints!

There are many ways to achieve that goal, and it’s up to you to decide what to follow. However, I’d like you to think about some questions before proceeding:

  1. What HTML element would you use as a grouping element? It would be convenient for the users if they could see the name of the group of flashcards (topic name) and could collapse the group up to the title.
  2. What Mavo attribute(s) are you going to add to that element, if any? Will the element be a property or a collection?
  3. Will end users be able to add new topics, delete and rearrange them, change the title of a topic and move flashcards between different topics?

What if you decide not to organize flashcards in groups, but instead just label them with tags that correspond to various topics? Well, that is perfectly fine. The solution with tags is also appropriate. For the sake of practice, try to accomplish that approach too.

The mv-bar attribute

See the Pen
05. The mv-bar attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

As our app stores the data locally, by default, the users of the app won’t be able to share their cards with other users. Wouldn’t it be great if we would let them export their flashcards and import somebody else’s flashcards? Thankfully, these features are already implemented in Mavo, and we can very easily add them to our app!

The mv-bar attribute controls which buttons are going to appear in the toolbar, if any. It’s typically specified on the Mavo root (an element with the mv-app attribute). Buttons are represented by their ids (which are very logical): edit, import, export, etc.

As we only want to add a few buttons to the default set, we can use the so-called relative syntax, which allows us to add and remove buttons from the default set without having to explicitly list everything out. All we need is to start the mv-bar attribute’s value with the with keyword.

By doing that, we would get the following:

<main mv-app="flashcards"       mv-storage="local"       mv-autosave="3"       mv-bar="with import export">       ... </main>
💻 Let’s get our hands dirty!

Give those features a try: add some flashcards, try to export them in a file. Then delete existing flashcards and import the flashcards from the previously exported file.

Expressions and MavoScript

See the Pen
06. Expressions and MavoScript
by Dmitry Sharabin (@dsharabin)
on CodePen.

Let’s now add some statistics to our app, such as the number of flashcards! Sounds interesting? I hoped so. 😀

To do that, we need to learn something new about Mavo.

We can dynamically refer to the value of any property we have defined, anywhere in our Mavo app (including in HTML attributes), by putting its name inside brackets, like this: [propertyName]. This is an example of a simple expression, which allows us to dynamically calculate things, reactively as they change.

Now let’s experiment and add a [source] expression inside the flashcard property, e.g., between two properties: the source and the translation.

...   <p property="source">Word or phrase</div>   [source]   <p property="translation">Translation</div> ...

What has changed in our app? The value of the flashcard source property is now shown on the page twice.

Switch to edit mode and try to change the value of the source property. Can you see that? The page content updates while you are changing the property value! That’s why I said earlier that Mavo lets us develop reactive web applications.

That’s indeed cool, but unfortunately, in our case, it’s not really helpful: we can’t use this expression to count the number of flashcards—we would always have only one value.

What if we put the [source] expression outside the flashcard property? We will have something like that:

...   [source]   <article property="flashcard" mv-multiple>     ...   </article> ...

How does this differ from the previous case? To see the difference add some flashcards if you haven’t done so yet. Now instead of one value we have a list of comma separated values: the source property of all flashcards. That’s exactly we were looking for: the number of items in the list corresponds the number of flashcards in the app.

Makes sense? Well, yes, but it wouldn’t it be more logical if we would count the number of flashcards, not the number of values of its source property? After all, a flashcard added exists even before we fill in its source or translation. I suggest you do the following: let’s substitute the [source] expression with [flashcard]:

...   [flashcard]   <article property="flashcard" mv-multiple>     ...   </article> ...

Noticed the difference? We still have a list, but its values are not simple values but objects, i.e., complex values containing all data that pertains to each flashcard. The good news is that the number of these objects is equal to the number of flashcards, since there is one for each flashcard, even when it’s completely empty. So, right now we have an object for each flashcard, but how do we count them and display the total count?

Now let’s get familiar with the MavoScript functions and find the one that would let us count the number of flashcards. Remember, we have a list of flashcards, so we need to find a function that would let us count the number of items in a list. And here it is—the count() function does exactly that!

But how can we use functions in expressions? Are there any rules we need to be aware of? Well, yes, there is a couple:

  1. Expressions are denoted by brackets.
  2. Do not nest brackets.

Let’s try using the count() function to count the number of flashcards:

... [count(flashcard)] <article property="flashcard" mv-multiple>   ... </article> ...

And that’s exactly what we were aiming for—now we have some statistics in our app! Isn’t that cool?

💻 Let’s get our hands dirty!

I hope you’ve already warmed up and ready to continue experimenting with Mavo.

Improve the application so that the statistics are displayed not only for the total number of flashcards in the app but also for the number of flashcards in each topic separately if there are any topics.

💡Hint!
Want to filter a list based on some criteria? The where operator will help.

The self-evaluation feature

We already have an application that lets us create, edit and store multiple flashcards. But how do we keep track of which ones we have already learned and which ones we need to practice more? Any respectable flashcards application needs a self-evaluation feature. Let’s investigate how we can add that!

Suppose in our app we have two buttons for the self-evaluation: the Bad and the Good. What exactly do we want to happen every time an end user clicks the buttons? Well, the idea is rather simple:

  • Clicking the Bad button would indicate the user hasn’t learned the word yet and we want our app to move the corresponding flashcard to the beginning of the list so they could see it as soon as possible after launching the app.
  • Clicking the Good button would indicate the user has learned the word and the corresponding flashcard needs to move to the end of the list to let them work with other flashcards which they haven’t learned yet.

“Are you sure we can do that without JavaScript?” you may ask. Yep! Mavo is extremely powerful and is able to equip us with all the tools we need!

Now when we know what we are going to implement, let’s set the UI in place first and then move on to the next step. Our markup would look something like this:

... <article property="flashcard" mv-multiple>   ...   <section>     <h2>Evaluate Yourself</h2>     <button>Bad</button>     <button>Good</button>   </section> </article> ...

The mv-action attribute

See the Pen
07. The mv-action attribute
by Dmitry Sharabin (@dsharabin)
on CodePen.

Mavo actions allow us to create our very own controls that modify data in custom ways when the user interacts with them. Sounds promising right?

To define a custom action we need to use the mv-action attribute on an appropriate element inside our Mavo app. The action is performed every time the element is clicked. That’s exactly what we were looking for.

The value of the mv-action attribute is an expression. We can use any of the expression functions and syntax that MavoScript provides to us, as well as a few more to facilitate data manipulation, such as add(), set(), move(), or delete(). It is important to note that unlike normal expressions which are evaluated reactively, these expressions are only evaluated each time the action is triggered.

So, we need to move flashcards inside the collection, and Mavo has an appropriate function that lets us do that—the move() function. Its first argument refers to the item we are moving, and the second is its position in the collection. Bear in mind that elements of the collection are numbered starting from 0.

Let’s implement the first point of the outline we discussed earlier: while self-evaluating, an end user clicks the Bad button and the corresponding flashcard moves to the beginning of the collection, i.e., it becomes the first one. So in the code, we have:

... <article property="flashcard" mv-multiple>   ...   <button mv-action="move(flashcard, 0)">Bad</button>   ... </article> ...

Pay attention that in the mv-action attribute we refer to the flashcard property inside the property itself, since we want to deal with the current flashcard.

If we try to implement the second point of the outline, we will face a problem. Can you suggest what problem exactly will it be?

Let’s remember that if an end user clicks the Good button the corresponding flashcard moves to the end of the collection, i.e., it becomes the last one. To make a flashcard last in the collection we need to know the number of items in it.

Thankfully, a bit earlier we’ve already solved that task and implemented the corresponding feature. But could we use that solution to solve our current problem? Unfortunately, we can’t: as we already know, we can refer to the collection of flashcards by its name (and evaluate its size) only outside the flashcard property. But in our case, we need to do that inside it: the Good button for which we need to write an expression is inside the flashcard property.

What should we do then? I’m glad you ask. Mavo has the solution.

Using the meta element to hold intermediate values

See the Pen
08. The <meta> element
by Dmitry Sharabin (@dsharabin)
on CodePen.

So, on the one hand, we know that the [count(flashcards)] expression gives us the number of flashcards if it is evaluated outside the flashcard property. On the other hand, we need to use that value inside the flashcard property.

To solve that dilemma, we need to evaluate the number of flashcards outside the flashcard property and somehow hold the result to be able to use it elsewhere in the app, precisely inside the flashcard property. For cases like that, in Mavo, there are so-called computed properties.

To hold an intermediate result so we can refer to it, we need an HTML element in our code. It is recommended to use the <meta> element for that purpose, like so: <meta property="propertyName" content="[expression]">. The advantage of using this element is that it is hidden outside edit mode, both semantically and visually.

Now let’s add the flashcardCount computed property in our app. Remember, we must place it outside the flashcard property, but then we can refer to it from anywhere:

... <meta property="flashcardCount" content="[count(flashcard)]"> <article property="flashcard" mv-multiple>     ... </article> ...

Only one step left to finish the implementation of the self-evaluation feature: if an end user clicks the Good button the corresponding flashcard moves to the end of the collection, i.e., it becomes the last one. Let’s add the relevant action in the code of our app:

... <meta property="flashcardCount" content="[count(flashcard)]"> <article property="flashcard" mv-multiple>   ...   <button mv-action="move(flashcard, flashcardCount)">Good</button> </article> ...

We are done! Congratulations! 😀

💻 Let’s get our hands dirty!

There is another way to solve that task: with the help of the $ all special property. The $ all property represents a collection itself if it is placed inside the collection. So there is no need to use any computed property in this case. Try to implement that solution on your own.

There is only one more tiny thing left that we need to fix. Remember the part where we added some stats to our app? Remember the expression we built to evaluate the number of flashcards in the app: [count(flashcard)]? Instead, we can (and should) now use the computed property we defined. Make the appropriate changes in the app.

Takeaways

So what have we learned so far? Let’s recap. In order to turn any static HTML page into a Mavo app we need to:

  1. Include the Mavo JavaScript and CSS files in the page’s <head> section.
  2. Add the mv-app attribute to the Mavo root element.
  3. Tell Mavo what elements of our app are important by adding the property attribute to them.
  4. Place the mv-multiple attribute on the element that will be multiplied and converted into a collection.
  5. Tell Mavo where to store our data by adding mv-storage attribute to the Mavo root.
  6. Decide whether Mavo should save our data automatically or not. If yes, add the mv-autosave attribute to the Mavo root.

    We also know that:

  7. The Mavo toolbar is fully-customizable. The mv-bar attribute controls which buttons are going to appear there.
  8. Expressions let us present the current value of properties in other elements and perform computations. An expression value (and type) vary depending on the place the expression takes in the code. Mavo’s expression syntax is called MavoScript.
  9. Custom actions allow creating controls that modify data in custom ways. To define a custom action set the mv-action attribute on an appropriate element inside a Mavo app
  10. Properties whose values are expressions are called computed properties. To hold an intermediate result to be able to refer to it elsewhere in the app, it is recommended to use the <meta> element.

Instead of an epilogue

So we built our app. Is it already perfect? Of course not, nothing is! There are so many things that can be improved, and so many features that can be added (with the help of Mavo, we can even make our app multilingual!). Go ahead, enhance it more, don’t hesitate to try something new!

What we’ve learned so far about Mavo is just the tip of the iceberg, and there is so much more. I encourage you to give it a closer look, by reading the documentation, by examining examples (on the Mavo site, or on CodePen: made by Lea Verou and a few made by myself), and by creating new stuff! Good luck! 😉

Acknowledgments

I want to thank two great people. First of all, my huge thanks go to Lea Verou, who not only inspired me to write this tutorial (and helped me make it happen) but also inspires me all the time by the way she makes the world of web development a better place. I’ve never met such a gifted human being, and I am happy having an opportunity to make some stuff with her!

I also thank James Moore. The examples he uses in his course “Functional Programming for Beginners with JavaScript” on Udemy pushed me to make my very own version of a flashcard study app. He is a wonderful teacher!

The post Let Mavo Shine in Building Interactive Web Applications appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]