Tag: mobile

View Source (on Mobile)

Have you ever wished you could see the HTML source of a web page while on a mobile browser, which generally doesn’t offer that feature? If you have a desktop machine around, there are ways, but what I mean is getting the source without anything but the device itself.

The little View Source tool by Neatnik does the trick.

You enter the URL in the little bar to see the source of that URL. Or add the URL to the the tool’s URL itself to link right to it. Here’s CSS-Tricks (without line wrapping and tidyied up!):

Direct Link to ArticlePermalink


The post View Source (on Mobile) appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,

View Source (on Mobile)

Have you ever wished you could see the HTML source of a web page while on a mobile browser, which generally doesn’t offer that feature? If you have a desktop machine around, there are ways, but what I mean is getting the source without anything but the device itself.

The little View Source tool by Neatnik does the trick.

You enter the URL in the little bar to see the source of that URL. Or add the URL to the the tool’s URL itself to link right to it. Here’s CSS-Tricks (without line wrapping and tidyied up!):


The post View Source (on Mobile) appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL

(This is a sponsored post.)

Flutter is Google’s UI framework used to create flexible, expressive cross-platform mobile applications. It is one of the fastest-growing frameworks for mobile app development. On the other hand, Fauna is a transactional, developer-friendly serverless database that supports native GraphQL. Flutter + Fauna is a match made in Heaven. If you are looking to build and ship a feature-rich full-stack application within record time, Flutter and Fauna is the right tool for the job. In this article, we will walk you through building your very first Flutter application with Fauna and GraphQL back-end.

You can find the complete code for this article, on GitHub.

Learning objective

By the end of this article, you should know how to:

  1. set up a Fauna instance,
  2. compose GraphQL schema for Fauna,
  3. set up GraphQL client in a Flutter app, and
  4. perform queries and mutations against Fauna GraphQL back-end.

Fauna vs. AWS Amplify vs. Firebase: What problems does Fauna solve? How is it different from other serverless solutions? If you are new to Fauna and would like to learn more about how Fauna compares to other solutions, I recommend reading this article.

What are we building?

We will be building a simple mobile application that will allow users to add, delete and update their favorite characters from movies and shows.

Setting up Fauna

Head over to fauna.com and create a new account. Once logged in, you should be able to create a new database.

Give a name to your database. I am going to name mine flutter_demo. Next, we can select a region group. For this demo, we will choose classic. Fauna is a globally distributed serverless database. It is the only database that supports low latency read and writes access from anywhere. Think of it as CDN (Content Delivery Network) but for your database. To learn more about region groups, follow this guide.

Generating an admin key

Once the database is created head, over to the security tab. Click on the new key button and create a new key for your database. Keep this key secure as we need this for our GraphQL operations.

We will be creating an admin key for our database. Keys with an admin role are used for managing their associated database, including the database access providers, child databases, documents, functions, indexes, keys, tokens, and user-defined roles. You can learn more about Fauna’s various security keys and access roles in the following link.

Compose a GraphQL schema

We will be building a simple app that will allow the users to add, update, and delete their favorite TV characters.

Creating a new Flutter project

Let’s create a new flutter project by running the following commands.

flutter create my_app

Inside the project directory, we will create a new file called graphql/schema.graphql.

In the schema file, we will define the structure of our collection. Collections in Fauna are similar to tables in SQL. We only need one collection for now. We will call it Character.

### schema.graphql type Character {     name: String!     description: String!     picture: String } type Query {     listAllCharacters: [Character] } 

As you can see above, we defined a type called Character with several properties (i.e., name, description, picture, etc.). Think of properties as columns of SQL database or key-value paid of an NoSQL database. We have also defined a Query. This query will return a list of the characters.

Now let’s go back to Fauna dashboard. Click on GraphQL and click on import schema to upload our schema to Fauna.

Once the importing is done, we will see that Fauna has generated the GraphQL queries and mutations.

Don’t like auto-generated GraphQL? Want more control over your business logic? In that case, Fauna allows you to define your custom GraphQL resolvers. To learn more, follow this link.

Setup GraphQL client in Flutter app

Let’s open up our pubspec.yaml file and add the required dependencies.

... dependencies:   graphql_flutter: ^4.0.0-beta   hive: ^1.3.0   flutter:     sdk: flutter ... 

We added two dependencies here. graphql_flutter is a GraphQL client library for flutter. It brings all the modern features of GraphQL clients into one easy-to-use package. We also added the hive package as our dependency. Hive is a lightweight key-value database written in pure Dart for local storage. We are using hive to cache our GraphQL queries.

Next, we will create a new file lib/client_provider.dart. We will create a provider class in this file that will contain our Fauna configuration.

To connect to Fauna’s GraphQL API, we first need to create a GraphQLClient. A GraphQLClient requires a cache and a link to be initialized. Let’s take a look at the code below.

// lib/client_provider.dart import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:flutter/material.dart';  ValueNotifier<GraphQLClient> clientFor({   @required String uri,   String subscriptionUri, }) {    final HttpLink httpLink = HttpLink(     uri,   );   final AuthLink authLink = AuthLink(     getToken: () async => 'Bearer fnAEPAjy8QACRJssawcwuywad2DbB6ssrsgZ2-2',   );   Link link = authLink.concat(httpLink);   return ValueNotifier<GraphQLClient>(     GraphQLClient(       cache: GraphQLCache(store: HiveStore()),       link: link,     ),   ); }  

In the code above, we created a ValueNotifier to wrap the GraphQLClient. Notice that we configured the AuthLink in lines 13 – 15 (highlighted). On line 14, we have added the admin key from Fauna as a part of the token. Here I have hardcoded the admin key. However, in a production application, we must avoid hard-coding any security keys from Fauna.

There are several ways to store secrets in Flutter application. Please take a look at this blog post for reference.

We want to be able to call Query and Mutation from any widget of our application. To do so we need to wrap our widgets with GraphQLProvider widget.

// lib/client_provider.dart  ....  /// Wraps the root application with the `graphql_flutter` client. /// We use the cache for all state management. class ClientProvider extends StatelessWidget {   ClientProvider({     @required this.child,     @required String uri,   }) : client = clientFor(           uri: uri,         );   final Widget child;   final ValueNotifier<GraphQLClient> client;   @override   Widget build(BuildContext context) {     return GraphQLProvider(       client: client,       child: child,     );   } } 

Next, we go to our main.dart file and wrap our main widget with the ClientProvider widget. Let’s take a look at the code below.

// lib/main.dart ...  void main() async {   await initHiveForFlutter();   runApp(MyApp()); } final graphqlEndpoint = 'https://graphql.fauna.com/graphql'; class MyApp extends StatelessWidget {    @override   Widget build(BuildContext context) {     return ClientProvider(       uri: graphqlEndpoint,       child: MaterialApp(         title: 'My Character App',         debugShowCheckedModeBanner: false,         initialRoute: '/',         routes: {           '/': (_) => AllCharacters(),           '/new': (_) => NewCharacter(),         }       ),     );   } } 

At this point, all our downstream widgets will have access to run Queries and Mutations functions and can interact with the GraphQL API.

Application pages

Demo applications should be simple and easy to follow. Let’s go ahead and create a simple list widget that will show the list of all characters. Let’s create a new file lib/screens/character-list.dart. In this file, we will write a new widget called AllCharacters.

// lib/screens/character-list.dart.dart  class AllCharacters extends StatelessWidget {   const AllCharacters({Key key}) : super(key: key);   @override   Widget build(BuildContext context) {     return Scaffold(       body: CustomScrollView(         slivers: [           SliverAppBar(             pinned: true,             snap: false,             floating: true,             expandedHeight: 160.0,             title: Text(               'Characters',               style: TextStyle(                 fontWeight: FontWeight.w400,                  fontSize: 36,               ),             ),             actions: <Widget>[               IconButton(                 padding: EdgeInsets.all(5),                 icon: const Icon(Icons.add_circle),                 tooltip: 'Add new entry',                 onPressed: () {                    Navigator.pushNamed(context, '/new');                 },               ),             ],           ),           SliverList(             delegate: SliverChildListDelegate([                 Column(                   children: [                     for (var i = 0; i < 10; i++)                        CharacterTile()                   ],                 )             ])           )         ],       ),     );   } }  // Character-tile.dart class CharacterTile extends StatefulWidget {   CharacterTilee({Key key}) : super(key: key);   @override   _CharacterTileState createState() => _CharacterTileeState(); } class _CharacterTileState extends State<CharacterTile> {   @override   Widget build(BuildContext context) {     return Container(        child: Text(&quot;Character Tile&quot;),     );   } } 

As you can see in the code above, [line 37] we have a for loop to populate the list with some fake data. Eventually, we will be making a GraphQL query to our Fauna backend and fetch all the characters from the database. Before we do that, let’s try to run our application as it is. We can run our application with the following command

flutter run 

At this point we should be able to see the following screen.

Performing queries and mutations

Now that we have some basic widgets, we can go ahead and hook up GraphQL queries. Instead of hardcoded strings, we would like to get all the characters from our database and view them in AllCharacters widget.

Let’s go back to the Fauna’s GraphQL playground. Notice we can run the following query to list all the characters.

query ListAllCharacters {   listAllCharacters(_size: 100) {     data {       _id       name       description       picture     }     after   } } 

To perform this query from our widget we will need to make some changes to it.

import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:todo_app/screens/Character-tile.dart';  String readCharacters = ";";"; query ListAllCharacters {   listAllCharacters(_size: 100) {     data {       _id       name       description       picture     }     after   } } ";";";;  class AllCharacters extends StatelessWidget {   const AllCharacters({Key key}) : super(key: key);   @override   Widget build(BuildContext context) {     return Scaffold(       body: CustomScrollView(         slivers: [           SliverAppBar(             pinned: true,             snap: false,             floating: true,             expandedHeight: 160.0,             title: Text(               'Characters',               style: TextStyle(                 fontWeight: FontWeight.w400,                  fontSize: 36,               ),             ),             actions: <Widget>[               IconButton(                 padding: EdgeInsets.all(5),                 icon: const Icon(Icons.add_circle),                 tooltip: 'Add new entry',                 onPressed: () {                    Navigator.pushNamed(context, '/new');                 },               ),             ],           ),           SliverList(             delegate: SliverChildListDelegate([               Query(options: QueryOptions(                 document: gql(readCharacters), // graphql query we want to perform                 pollInterval: Duration(seconds: 120), // refetch interval               ),                builder: (QueryResult result, { VoidCallback refetch, FetchMore fetchMore }) {                 if (result.isLoading) {                   return Text('Loading');                 }                 return Column(                   children: [                     for (var item in result.data['listAllCharacters']['data'])                       CharacterTile(Character: item, refetch: refetch),                   ],                 );               })             ])           )         ],       ),     );   } }  

First of all, we defined the query string for getting all characters from the database [line 5 to 17]. We have wrapped our list widget with a Query widget from flutter_graphql.

Feel free to take a look at the official documentation for flutter_graphql library.

In the query options argument we provide the GraphQL query string itself. We can pass in any float number for the pollInterval argument. Poll Interval defines how often we would like to refetch data from our backend. The widget also has a standard builder function. We can use a builder function to pass the query result, refetch callback function and fetch more callback function down the widget tree.

Next, I am going to update the CharacterTile widget to display the character data on screen.

// lib/screens/character-tile.dart ... class CharacterTile extends StatelessWidget {   final Character;   final VoidCallback refetch;   final VoidCallback updateParent;   const CharacterTile({     Key key,      @required this.Character,      @required this.refetch,     this.updateParent,   }) : super(key: key);   @override   Widget build(BuildContext context) {     return InkWell(       onTap: () {       },       child: Padding(         padding: const EdgeInsets.all(10),         child: Row(           children: [             Container(               height: 90,               width: 90,               decoration: BoxDecoration(                 color: Colors.amber,                 borderRadius: BorderRadius.circular(15),                 image: DecorationImage(                   fit: BoxFit.cover,                   image: NetworkImage(Character['picture'])                 )               ),             ),             SizedBox(width: 10),             Expanded(               child: Column(                 mainAxisAlignment: MainAxisAlignment.center,                 crossAxisAlignment: CrossAxisAlignment.start,                 children: [                   Text(                     Character['name'],                     style: TextStyle(                       color: Colors.black87,                       fontWeight: FontWeight.bold,                     ),                   ),                   SizedBox(height: 5),                   Text(                     Character['description'],                     style: TextStyle(                       color: Colors.black87,                     ),                     maxLines: 2,                   ),                 ],               )             )           ],         ),       ),     );   } } 

Adding new data

We can add new characters to our database by running the mutation below.

mutation CreateNewCharacter($ data: CharacterInput!) {     createCharacter(data: $ data) {       _id       name       description       picture     } } 

To run this mutation from our widget we can use the Mutation widget from flutter_graphql library. Let’s create a new widget with a simple form for the users to interact with and input data. Once the form is submitted the createCharacter mutation will be called.

// lib/screens/new.dart ... String addCharacter = ";";";   mutation CreateNewCharacter($ data: CharacterInput!) {     createCharacter(data: $ data) {       _id       name       description       picture     }   } ";";";; class NewCharacter extends StatelessWidget {   const NewCharacter({Key key}) : super(key: key);   @override   Widget build(BuildContext context) {     return Scaffold(       appBar: AppBar(         title: const Text('Add New Character'),       ),       body: AddCharacterForm()     );   } } class AddCharacterForm extends StatefulWidget {   AddCharacterForm({Key key}) : super(key: key);   @override   _AddCharacterFormState createState() => _AddCharacterFormState(); } class _AddCharacterFormState extends State<AddCharacterForm> {   String name;   String description;   String imgUrl;   @override   Widget build(BuildContext context) {     return Form(       child: Padding(         padding: EdgeInsets.all(20),         child: Column(           crossAxisAlignment: CrossAxisAlignment.start,           children: [             TextField(               decoration: const InputDecoration(                 icon: Icon(Icons.person),                 labelText: 'Name *',               ),               onChanged: (text) {                 name = text;               },             ),             TextField(               decoration: const InputDecoration(                 icon: Icon(Icons.post_add),                 labelText: 'Description',               ),               minLines: 4,               maxLines: 4,               onChanged: (text) {                 description = text;               },             ),             TextField(               decoration: const InputDecoration(                 icon: Icon(Icons.image),                 labelText: 'Image Url',               ),               onChanged: (text) {                 imgUrl = text;               },             ),             SizedBox(height: 20),             Mutation(               options: MutationOptions(                 document: gql(addCharacter),                 onCompleted: (dynamic resultData) {                   print(resultData);                   name = '';                   description = '';                   imgUrl = '';                   Navigator.of(context).push(                     MaterialPageRoute(builder: (context) => AllCharacters())                   );                 },               ),                builder: (                 RunMutation runMutation,                 QueryResult result,               ) {                 return Center(                   child: ElevatedButton(                     child: const Text('Submit'),                     onPressed: () {                       runMutation({                         'data': {                           ";picture";: imgUrl,                           ";name";: name,                           ";description";: description,                         }                       });                     },                   ),                 );               }             )           ],         ),       ),     );   } } 

As you can see from the code above Mutation widget works very similar to the Query widget. Additionally, the Mutation widget provides us with a onComplete function. This function returns the updated result from the database after the mutation is completed.

Removing data

To remove a character from our database we can run the deleteCharacter mutation. We can add this mutation function to our CharacterTile and fire it when a button is pressed.

// lib/screens/character-tile.dart ...  String deleteCharacter = ";";";   mutation DeleteCharacter($ id: ID!) {     deleteCharacter(id: $ id) {       _id       name     }   } ";";";;  class CharacterTile extends StatelessWidget {   final Character;   final VoidCallback refetch;   final VoidCallback updateParent;   const CharacterTile({     Key key,      @required this.Character,      @required this.refetch,     this.updateParent,   }) : super(key: key);   @override   Widget build(BuildContext context) {     return InkWell(       onTap: () {         showModalBottomSheet(           context: context,           builder: (BuildContext context) {             print(Character['picture']);             return Mutation(               options: MutationOptions(                 document: gql(deleteCharacter),                 onCompleted: (dynamic resultData) {                   print(resultData);                   this.refetch();                 },               ),                builder: (                 RunMutation runMutation,                 QueryResult result,               ) {                 return Container(                   height: 400,                   padding: EdgeInsets.all(30),                   child: Center(                     child: Column(                       mainAxisAlignment: MainAxisAlignment.center,                       mainAxisSize: MainAxisSize.min,                       children: <Widget>[                         Text(Character['description']),                         ElevatedButton(                           child: Text('Delete Character'),                           onPressed: () {                             runMutation({                               'id': Character['_id'],                             });                             Navigator.pop(context);                           },                         ),                       ],                     ),                   ),                 );                }             );           }         );       },       child: Padding(         padding: const EdgeInsets.all(10),         child: Row(           children: [             Container(               height: 90,               width: 90,               decoration: BoxDecoration(                 color: Colors.amber,                 borderRadius: BorderRadius.circular(15),                 image: DecorationImage(                   fit: BoxFit.cover,                   image: NetworkImage(Character['picture'])                 )               ),             ),             SizedBox(width: 10),             Expanded(               child: Column(                 mainAxisAlignment: MainAxisAlignment.center,                 crossAxisAlignment: CrossAxisAlignment.start,                 children: [                   Text(                     Character['name'],                     style: TextStyle(                       color: Colors.black87,                       fontWeight: FontWeight.bold,                     ),                   ),                   SizedBox(height: 5),                   Text(                     Character['description'],                     style: TextStyle(                       color: Colors.black87,                     ),                     maxLines: 2,                   ),                 ],               )             )           ],         ),       ),     );   } } 

Editing data

Editing data works same as add and delete. It is just another mutation in the GraphQL API. We can create an edit character form widget similar to the new character form widget. The only difference is that the edit form will run updateCharacter mutation. For editing I created a new widget lib/screens/edit.dart. Here’s the code for this widget.

// lib/screens/edit.dart  String editCharacter = """ mutation EditCharacter($ name: String!, $ id: ID!, $ description: String!, $ picture: String!) {   updateCharacter(data:    {      name: $ name      description: $ description     picture: $ picture   }, id: $ id) {     _id     name     description     picture   } } """; class EditCharacter extends StatelessWidget {   final Character;   const EditCharacter({Key key, this.Character}) : super(key: key);   @override   Widget build(BuildContext context) {     return Scaffold(       appBar: AppBar(         title: const Text('Edit Character'),       ),       body: EditFormBody(Character: this.Character),     );   } } class EditFormBody extends StatefulWidget {   final Character;   EditFormBody({Key key, this.Character}) : super(key: key);   @override   _EditFormBodyState createState() => _EditFormBodyState(); } class _EditFormBodyState extends State<EditFormBody> {   String name;   String description;   String picture;   @override   Widget build(BuildContext context) {     return Container(        child: Padding(          padding: const EdgeInsets.all(8.0),          child: Column(            crossAxisAlignment: CrossAxisAlignment.start,            children: [             TextFormField(                initialValue: widget.Character['name'],                 decoration: const InputDecoration(                   icon: Icon(Icons.person),                   labelText: 'Name *',                 ),                 onChanged: (text) {                   name = text;                 }             ),             TextFormField(               initialValue: widget.Character['description'],               decoration: const InputDecoration(                 icon: Icon(Icons.person),                 labelText: 'Description',               ),               minLines: 4,               maxLines: 4,               onChanged: (text) {                 description = text;               }             ),             TextFormField(               initialValue: widget.Character['picture'],               decoration: const InputDecoration(                 icon: Icon(Icons.image),                 labelText: 'Image Url',               ),               onChanged: (text) {                 picture = text;               },             ),             SizedBox(height: 20),             Mutation(               options: MutationOptions(                 document: gql(editCharacter),                 onCompleted: (dynamic resultData) {                   print(resultData);                   Navigator.of(context).push(                     MaterialPageRoute(builder: (context) => AllCharacters())                   );                 },               ),               builder: (                 RunMutation runMutation,                 QueryResult result,               ) {                 print(result);                 return Center(                   child: ElevatedButton(                     child: const Text('Submit'),                     onPressed: () {                        runMutation({                         'id': widget.Character['_id'],                         'name': name != null ? name : widget.Character['name'],                         'description': description != null ? description : widget.Character['description'],                         'picture': picture != null ? picture : widget.Character['picture'],                       });                     },                   ),                 );               }             ),            ]          )        ),     );   } } 

You can take a look at the complete code for this article below.

Where to go from here

The main intention of this article is to get you up and running with Flutter and Fauna. We have only scratched the surface here. Fauna ecosystem provides a complete, auto-scaling, developer-friendly backend as a service for your mobile applications. If your goal is to ship a production-ready cross-platform mobile application in record time give Fauna and Flutter is the way to go.

I highly recommend checking out Fauna’s official documentation site. If you are interested in learning more about GraphQL clients for Dart/Flutter checkout the official GitHub repo for graphql_flutter.

Happy hacking and see you next time.


The post How to Build a Full-Stack Mobile Application With Flutter, Fauna, and GraphQL appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Yet Another Mobile Context Menu With No Indication it Can Scroll

Remember Tyler Hall’s personal story of a UX moment where the popup sharing context menu on iOS had no visible indication that the content inside was scrollable? The thing his mom wanted to do seemed impossible iOS isn’t alone here — Terence Eden documents essentially the same problem on Android:

I tried sharing a website using Google Chrome for Android. I hit the share button, and a panel popped-up from the bottom of the screen.

Hmmm. It didn’t have the share destination that I wanted. It was early in the morning – when I’m not at my cognitive best – and I was stumped. There is nothing on this screen – other than the icons – to tell me how I can interact with it. There’s no scrollbar, no handle, no “more” icon, nothing.

I would think even just fairly subtle “scroll shadows” would go a long way in both cases, but some serious user testing should be in order here.

Android menu with no indication of scrolling potential.

iOS menu with no indication of scrolling potential.


The post Yet Another Mobile Context Menu With No Indication it Can Scroll appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

The Mobile Performance Inequality Gap

Alex Russell made some interesting notes about performance and how it impacts folks on mobile:

[…] CPUs are not improving fast enough to cope with frontend engineers’ rosy resource assumptions. If there is unambiguously good news on the tooling front, multiple popular tools now include options to prevent sending first-party JS in the first place (Next.jsGatsby), though the JS community remains in stubborn denial about the costs of client-side script. Hopefully, toolchain progress of this sort can provide a more accessible bridge as we transition costs to a reduced-script-emissions world.

A lot of the stuff I read when it comes to performance is focused on America, but what I like about Russell’s take here is that he looks at a host of other countries such as India, too. But how does the rollout of 5G networks impact performance around the world? Well, we should be skeptical of how improved networks impact our work. Alex argues:

5G looks set to continue a bumpy rollout for the next half-decade. Carriers make different frequency band choices in different geographies, and 5G performance is heavily sensitive to mast density, which will add confusion for years to come. Suffice to say, 5G isn’t here yet, even if wealthy users in a few geographies come to think of it as “normal” far ahead of worldwide deployment

This is something I try to keep in mind whenever I’m thinking about performance: how I’m viewing my website is most likely not how other folks are viewing it.

Direct Link to ArticlePermalink


The post The Mobile Performance Inequality Gap appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

WooCommerce on Mobile

Whether you use the eCommerce features on WordPress.com or use WooCommerce on your self-hosted WordPress site (like we do), you can use the WooCommerce mobile app. That’s right WooCommerce has native apps for iOS and Android. They’ve just released some nice upgrades to both, making them extra useful.

Perhaps you know we use WooCommerce around here. We use it to sell some physical products like posters that get mailed to you, as well as for MVP Supporter memberships that give you access to things like our book and features like no ads.

Here’s a little behind the scenes look at some of the useful screens from the WooCommerce mobile app for our store (iOS app):

The top new feature is being able to add products.

There are all sorts of reasons you might want to do this, but imagine this one. Say you’re a ceramic artist (did you know that was my undergrad focus?) and you’ve just opened a kiln full of wonderful work. You’re the kind of artist who makes a living from what you do, so you’re going to sell all these new pieces.

Maybe in the past you’d take some notes on paper about what the pieces are, what you want to charge for them, etc. Then you take some photos. Then, next time you’re at the computer, you go to your store and get them posted. With this latest WooCommerce update, you could get them posted without even leaving the studio.

Photo from Emily Murphy’s blog

Get your photos (probably right from your phone), create the product in the WooCommerce app, price it, describe it, and get it published immediately.

When orders come in, you’ll know, because you can get a push notification (if you want) and can manage the process of fulfilling the order right there. You can basically run your business life right from your phone or tablet. In addition to adding products, you can:

  • View and modify orders in real-time
  • Monitor customer reviews and baseline stats
  • Confirm order statuses and make edits when needed
  • Enable push notifications to stay constantly up-to-date
  • Switch WooCommerce stores on the fly

If you’re interesting in another look, Aaron Douglas wrote up the latest release on the WooCommerce blog (adding products was the #1 requested feature!).


The post WooCommerce on Mobile appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

CSS fix for 100vh in mobile WebKit

A surprisingly common response when asking people about things they’d fix about anything in CSS, is to improve the handling of viewport units.

One thing that comes up often is how they relate to scrollbars. For example, if an element is sized to 100vw and stretches edge-to-edge, that’s fine so long as the page doesn’t have a vertical scrollbar. If it does have a vertical scrollbar, then 100vw is too wide, and the presence of that vertical scrollbar triggers a horizontal scrollbar because viewport units don’t have an elegant/optional way of handling that. So you might be hiding overflow on the body when you otherwise wouldn’t need to, for example. (Demo)

Another scenario involves mobile browsers. You might use viewport units to help you position a fixed footer along the bottom of the screen. But then browser chrome might come up (e.g. navigation, keyboard, etc), and it may cover the footer, because the mobile browser doesn’t consider anything changed about the viewport size.

Matt Smith documents this problem:

On the left, the browser navigation bar (considered browser chrome) is covering up the footer making it appear that the footer is beyond 100vh when it is not. On the right, the -webkit-fill-available property is being used rather than viewport units to fix the problem.

And a solution of sorts:

body {   min-height: 100vh;   /* mobile viewport bug fix */   min-height: -webkit-fill-available; }

Does this really work? […] I’ve had no problems with any of the tests I’ve run and I’m using this method in production right now. But I did receive a number of responses to my tweet pointing to other possible problems with using this (the effects of rotating devices, Chrome not completely ignoring the property, etc.)

It would be better to get some real cross-browser solution for this someday, but I don’t see any issues using this as an improvement. It’s weird to use a vendor-prefixed property as a progressive enhancement, but hey, the world is weird.

Direct Link to ArticlePermalink

The post CSS fix for 100vh in mobile WebKit appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Better Form Inputs for Better Mobile User Experiences

Here’s one simple, practical way to make apps perform better on mobile devices: always configure HTML input fields with the correct type, inputmode, and autocomplete attributes. While these three attributes are often discussed in isolation, they make the most sense in the context of mobile user experience when you think of them as a team. 

There’s no question that forms on mobile devices can be time-consuming and tedious to fill in, but by properly configuring inputs, we can ensure that the data entry process is as seamless as possible for our users. Let’s take a look at some examples and best practices we can use to create better user experiences on mobile devices.

Use this demo to experiment on your own, if you’d like.

Using the correct input type

This is the easiest thing to get right. Input types, like email, tel, and url, are well-supported across browsers. While the benefit of using a type, like tel over the more generic text, might be hard to see on desktop browsers, it’s immediately apparent on mobile.

Choosing the appropriate type changes the keyboard that pops up on Android and iOS devices when a user focuses the field. For very little effort, just by using the right type, we will show custom keyboards for email, telephone numbers, URLs, and even search inputs

Text input type on iOS (left) and Android (right)
Email input type on iOS (left) and Android (right)
URL input type on iOS (left) and Android (right)
Search input type on iOS (left) and Android (right)

One thing to note is that both input type="email" and input type="url" come with validation functionality, and modern browsers will show an error tooltip if their values do not match the expected formats when the user submits the form. If you’d rather turn this functionality off, you can simply add the novalidate attribute to the containing form.

A quick detour into date types

HTML inputs comprise far more than specialized text inputs — you also have radio buttons, checkboxes, and so on. For the purposes of this discussion, though, I’m mostly talking about the more text-based inputs

There is a type of input that sits in the liminal space between the more free-form text inputs and input widgets like radio buttons: date. The date input type comes in a variety of flavors that are well-supported on mobile, including date, time, datetime-local, and month. These pop up custom widgets in iOS and Android when they are focused. Instead of triggering a specialized keyboard, they show a select-like interface in iOS, and various different types of widgets on Android (where the date and time selectors are particularly slick). 

I was excited to start using native defaults on mobile, until I looked around and realized that most major apps and mobile websites use custom date pickers rather than native date input types. There could be a couple reasons for this. First, I find the native iOS date selector to be less intuitive than a calendar-type widget. Second, even the beautifully-designed Android implementation is fairly limited compared to custom components — there’s no easy way to input a date range rather than a single date, for instance. 

Still, the date input types are worth checking out if the custom datepicker you’re using doesn’t perform well on mobile. If you’d like to try out the native input widgets on iOS and Android while making sure that desktop users see a custom widget instead of the default dropdown, this snippet of CSS will hide the calendar dropdown for desktop browsers that implement it:

::-webkit-calendar-picker-indicator {   display: none; }
Date input type on iOS (left) and Android (right)
Time input type on iOS (left) and Android (right)

One final thing to note is that date types cannot be overridden by the inputmode attribute, which we’ll discuss next.

Why should I care about inputmode?

The inputmode attribute allows you to override the mobile keyboard specified by the input’s type and directly declare the type of keyboard shown to the user. When I first learned about this attribute, I wasn’t impressed — why not just use the correct type in the first place? But while inputmode is often unnecessary, there are a few places where the attribute can be extremely helpful. The most notable use case that I’ve found for inputmode is building a better number input.

While some HTML5 input types, like url and email, are straightforward, input type="number" is a different matter. It has some accessibility concerns as well as a somewhat awkward UI. For example, desktop browsers, like Chrome, show tiny increment arrows that are easy to trigger accidentally by scrolling.

So here’s a pattern to memorize and use going forwards. For most numeric inputs, instead of using this: 

<input type="number" />

…you actually want to use this:

<input type="text" inputmode="decimal" />

Why not inputmode="numeric" instead of inputmode="decimal"

The numeric and decimal attribute values produce identical keyboards on Android. On iOS, however, numeric displays a keyboard that shows both numbers and punctuation, while decimal shows a focused grid of numbers that almost looks exactly like the tel input type, only without extraneous telephone-number focused options. That’s why it’s my preference for most types of number inputs.

iOS numeric input (left) and decimal input (right)
Android numeric input (left) and decimal input (right)

Christian Oliff has written an excellent article dedicated solely to the inputmode attribute.

Don’t forget autocomplete

Even more important than showing the correct mobile keyboard is showing helpful autocomplete suggestions. That can go a long way towards creating a faster and less frustrating user experience on mobile.

While browsers have heuristics for showing autocomplete fields, you cannot rely on them, and should still be sure to add the correct autocomplete attribute. For instance, in iOS Safari, I found that an input type="tel" would only show autocomplete options if I explicitly added a autocomplete="tel" attribute.

You may think that you are familiar with the basic autocomplete options, such as those that help the user fill in credit card numbers or address form fields, but I’d urge you to review them to make sure that you are aware of all of the options. The spec lists over 50 values! Did you know that autocomplete="one-time-code" can make a phone verification user flow super smooth?

Speaking of autocomplete…

I’d like to mention one final element that allows you to create your own custom autocomplete functionality: datalist. While it creates a serviceable — if somewhat basic — autocomplete experience on desktop Chrome and Safari, it shines on iOS by surfacing suggestions in a convenient row right above the keyboard, where the system autocomplete functionality usually lives. Further, it allows the user to toggle between text and select-style inputs.

On Android, on the other hand, datalist creates a more typical autocomplete dropdown, with the area above the keyboard reserved for the system’s own typeahead functionality. One possible advantage to this style is that the dropdown list is easily scrollable, creating immediate access to all possible options as soon as the field is focused. (In iOS, in order to view more than the top three matches, the user would have to trigger the select picker by pressing the down arrow icon.)

You can use this demo to play around with datalist:

And you can explore all the autocomplete options, as well as input type and inputmode values, using this tool I made to help you quickly preview various input configurations on mobile.

In summary

When I’m building a form, I’m often tempted to focus on perfecting the desktop experience while treating the mobile web as an afterthought. But while it does take a little extra work to ensure forms work well on mobile, it doesn’t have to be too difficult. Hopefully, this article has shown that with a few easy steps, you can make forms much more convenient for your users on mobile devices.

The post Better Form Inputs for Better Mobile User Experiences appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Client-Side Image Editing on Mobile

Michael Scharnagl:

Ever wanted to easily convert an image to a grayscale image on your phone? I do sometimes, and that’s why I build a demo using the Web Share Target API to achieve exactly that.

For this I used the Service Worker way to handle the data. Once the data is received on the client, I use drawImage from canvas to draw the image in canvas, use the grayscale filter to convert it to a grayscale image and output the final image.

So you “install” the little microsite like a PWA, then you natively “share” an image to it and it comes back edited. Clever. Android on Chrome only at the moment.

Reminds me of this “Browser Functions” idea in reverse. That was a server that did things a browser can do, this is a browser doing things a server normally does.

Direct Link to ArticlePermalink

The post Client-Side Image Editing on Mobile appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

A Web Component with Different HTML for Desktop and Mobile

Christian Schaefer has a great big write-up about dealing with web advertisements. The whole thing is interesting, first documenting all the challenges that ads present, and then presenting modern solutions to each of them.

One code snippet that caught my eye was a simple way to design a component that renders different HTML depending on the screen size.

<div class="ad">   <template class="ad__mobile">     // Mobile ad HTML code with inline script   </template>   <template class="ad__desktop">     // Desktop ad HTML code with inline script   </template>   <script>     const isMobile = matchMedia('(max-device-width: 20em)').matches;     const ad = document.currentScript.closest('.ad');     const content = ad       .querySelector(isMobile ? '.ad__mobile' : '.ad__desktop')       .content;          ad.appendChild(document.importNode(content, true));   </script> </div> 

Clever. Although note that Christian ends up going a totally different route in the article.

Here’s that same code where I use a custom element and move the JavaScript to JavaScript just ‘cuz.

See the Pen
A Web Component with Different HTML for Desktop and Mobile
by Chris Coyier (@chriscoyier)
on CodePen.

The post A Web Component with Different HTML for Desktop and Mobile appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]