Tag: Building

Building a Form in PHP Using DOMDocument

Templating makes the web go round. The synthesis of data and structure into content. It’s our coolest superpower as developers — grab some data, then make it work for us, in whatever presentation we need. An array of objects can become a table, a list of cards, a chart, or whatever we think is most useful to the user. Whether the data is our own blog posts in Markdown files, or on-the-minute global exchange rates, the markup and resulting UX are up to us as front-end developers.

PHP is an amazing language for templating, providing many ways to merge data with markup. Let’s get into an example of using data to build out an HTML form in this post.

Want to get your hands dirty right away? Jump to the implementation.

In PHP, we can inline variables into string literals that use double quotes, so if we have a variable $ name = 'world', we can write echo "Hello, {$ name}", and it prints the expected Hello, world. For more complex templating, we can always concatenate strings, like: echo "Hello, " . $ name . ".".

For the old-schoolers, there’s printf("Hello, %s", $ name). For multiline strings, you can use Heredoc (the one that starts like <<<MYTEXT). And, last but certainly not least, we can sprinkle PHP variables inside HTML, like <p>Hello, <?= $ name ?></p>.

All of these options are great, but things can get messy when a lot of inline logic is required. If we need to build compound HTML strings, say a form or navigation, the complexity is potentially infinite, since HTML elements can nest inside each other.

What we’re trying to avoid

Before we go ahead and do the thing we want to do, it’s worth taking a minute to consider what we don’t want to do. Consider the following abridged passage from the scripture of WordPress Core, class-walker-nav-menu.php, verses 170-270:

<?php // class-walker-nav-menu.php // ... $  output .= $  indent . '<li' . $  id . $  class_names . '>'; // ... $  item_output  = $  args->before; $  item_output .= '<a' . $  attributes . '>'; $  item_output .= $  args->link_before . $  title . $  args->link_after; $  item_output .= '</a>'; $  item_output .= $  args->after; // ... $  output .= apply_filters( 'walker_nav_menu_start_el', $  item_output, $  item, $  depth, $  args ); // ... $  output .= "</li>{$  n}";

In order to build out a navigation <ul> in this function, we use a variable, $ output, which is a very long string to which we keep adding stuff. This type of code has a very specific and limited order of operations. If we wanted to add an attribute to the <a>, we must have access to $ attributes before this runs. And if we wanted to optionally nest a <span> or an <img> inside the <a>, we’d need to author a whole new block of code that would replace the middle of line 7 with about 4-10 new lines, depending on what exactly we want to add. Now imagine you need to optionally add the <span> , and then optionally add the <img>, either inside the <span> or after it. That alone is three if statements, making the code even less legible.

It’s very easy to end up with string spaghetti when concatenating like this, which is as fun to say as it is painful to maintain.

The essence of the problem is that when we try to reason about HTML elements, we’re not thinking about strings. It just so happens that strings are what the browser consumes and PHP outputs. But our mental model is more like the DOM — elements are arranged into a tree, and each node has many potential attributes, properties, and children.

Wouldn’t it be great if there were a structured, expressive way to build our tree?

Enter…

The DOMDocument class

PHP 5 added the DOM module to it’s roster of Not So Strictly Typed™ types. Its main entry point is the DOMDocument class, which is intentionally similar to the Web API’s JavaScript DOM. If you’ve ever used document.createElement or, for those of us of a certain age, jQuery’s $ ('<p>Hi there!</p>') syntax, this will probably feel quite familiar.

We start out by initializing a new DOMDocument:

$  dom = new DOMDocument();

Now we can add a DOMElement to it:

$  p = $  dom->createElement('p');

The string 'p' represents the type of element we want, so other valid strings would be 'div', 'img' , etc.

Once we have an element, we can set its attributes:

$  p->setAttribute('class', 'headline');

We can add children to it:

$  span = $  dom->createElement('span', 'This is a headline'); // The 2nd argument populates the element's textContent $  p->appendChild($  span);

And finally, get the complete HTML string in one go:

$  dom->appendChild($  p); $  htmlString = $  dom->saveHTML(); echo $  htmlString;

Notice how this style of coding keeps our code organized according to our mental model — a document has elements; elements can have any number of attributes; and elements nest inside one another without needing to know anything about each other. The whole “HTML is just a string” part comes in at the end, once our structure is in place.

The “document” here is a bit different from the actual DOM, in that it doesn’t need to represent an entire document, just a block of HTML. In fact, if you need to create two similar elements, you could save a HTML string using saveHTML(), modify the DOM “document” some more, and then save a new HTML string by calling saveHTML() again.

Getting data and setting the structure

Say we need to build a form on the server using data from a CRM provider and our own markup. The API response from the CRM looks like this:

{   "submit_button_label": "Submit now!",   "fields": [     {       "id": "first-name",       "type": "text",       "label": "First name",       "required": true,       "validation_message": "First name is required.",       "max_length": 30     },     {       "id": "category",       "type": "multiple_choice",       "label": "Choose all categories that apply",       "required": false,       "field_metadata": {         "multi_select": true,         "values": [           { "value": "travel", "label": "Travel" },           { "value": "marketing", "label": "Marketing" }         ]       }     }   ] }

This example doesn’t use the exact data structure of any specific CRM, but it’s rather representative.

And let’s suppose we want our markup to look like this:

<form>   <label class="field">     <input type="text" name="first-name" id="first-name" placeholder=" " required>     <span class="label">First name</span>     <em class="validation" hidden>First name is required.</em>   </label>   <label class="field checkbox-group">     <fieldset>       <div class="choice">         <input type="checkbox" value="travel" id="category-travel" name="category">         <label for="category-travel">Travel</label>       </div>       <div class="choice">         <input type="checkbox" value="marketing" id="category-marketing" name="category">         <label for="category-marketing">Marketing</label>       </div>     </fieldset>     <span class="label">Choose all categories that apply</span>   </label> </form>

What’s that placeholder=" "? It’s a small trick that allows us to track in CSS whether the field is empty, without needing JavaScript. As long as the input is empty, it matches input:placeholder-shown, but the user doesn’t see any visible placeholder text. Just the kind of thing you can do when we control the markup!

Now that we know what our desired result is, here’s the game plan:

  1. Get the field definitions and other content from the API
  2. Initialize a DOMDocument
  3. Iterate over the fields and build each one as required
  4. Get the HTML output

So let’s stub out our process and get some technicalities out of the way:

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   $  formResult = file_get_contents($  endpoint);   $  formContent = json_decode($  formResult);   $  formFields = $  formContent->fields;    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // TODO: Do something with the field data   }    // Get the HTML output   $  dom->appendChild($  form);   $  htmlString = $  dom->saveHTML();   echo $  htmlString; }

So far, we’ve gotten the data and parsed it, initialized our DOMDocument and echoed its output. What do we want to do for each field? First off, let’s build the container element which, in our example, should be a <label>, and the labelling <span> which is common to all field types:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  label = null;    // Add a `<span>` for the label if it is set   if ($  field->label) {     $  label = $  dom->createElement('span', $  field->label);     $  label->setAttribute('class', 'label');   }    // Add the label to the `<label>`   if ($  label) $  element->appendChild($  label); }

Since we’re in a loop, and PHP doesn’t scope variables in loops, we reset the $ label element on each iteration. Then, if the field has a label, we build the element. At the end, we append it to the container element.

Notice that we set classes using the setAttribute method. Unlike the Web API, there unfortunately is no special handing of class lists. They’re just another attribute. If we had some really complex class logic, since It’s Just PHP™, we could create an array and then implode it:
$ label->setAttribute('class', implode($ labelClassList)).

Single inputs

Since we know that the API will only return specific field types, we can switch over the type and write specific code for each one:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     case 'text':     case 'email':     case 'telephone':       $  input = $  dom->createElement('input');       $  input->setAttribute('placeholder', ' ');       if ($  field->type === 'email') $  input->setAttribute('type', 'email');       if ($  field->type === 'telephone') $  input->setAttribute('type', 'tel');       break;   } }

Now let’s handle text areas, single checkboxes and hidden fields:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'text_area':       $  input = $  dom->createElement('textarea');       $  input->setAttribute('placeholder', ' ');       if ($  rows = $  field->field_metadata->rows) $  input->setAttribute('rows', $  rows);       break;      case 'checkbox':       $  element->setAttribute('class', 'field single-checkbox');       $  input = $  dom->createElement('input');       $  input->setAttribute('type', 'checkbox');       if ($  field->field_metadata->initially_checked === true) $  input->setAttribute('checked', 'checked');       break;      case 'hidden':       $  input = $  dom->createElement('input');       $  input->setAttribute('type', 'hidden');       $  input->setAttribute('value', $  field->field_metadata->value);       $  element->setAttribute('hidden', 'hidden');       $  element->setAttribute('style', 'display: none;');       $  label->textContent = '';       break;   } }

Notice something new we’re doing for the checkbox and hidden cases? We’re not just creating the <input> element; we’re making changes to the container <label> element! For a single checkbox field we want to modify the class of the container, so we can align the checkbox and label horizontally; a hidden <input>‘s container should also be completely hidden.

Now if we were merely concatenating strings, it would be impossible to change at this point. We would have to add a bunch of if statements regarding the type of element and its metadata in the top of the block. Or, maybe worse, we start the switch way earlier, then copy-paste a lot of common code between each branch.

And here is the real beauty of using a builder like DOMDocument — until we hit that saveHTML(), everything is still editable, and everything is still structured.

Nested looping elements

Let’s add the logic for <select> elements:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'select':       $  element->setAttribute('class', 'field select');       $  input = $  dom->createElement('select');       $  input->setAttribute('required', 'required');       if ($  field->field_metadata->multi_select === true)         $  input->setAttribute('multiple', 'multiple');            $  options = [];            // Track whether there's a pre-selected option       $  optionSelected = false;            foreach ($  field->field_metadata->values as $  value) {         $  option = $  dom->createElement('option', htmlspecialchars($  value->label));              // Bail if there's no value         if (!$  value->value) continue;              // Set pre-selected option         if ($  value->selected === true) {           $  option->setAttribute('selected', 'selected');           $  optionSelected = true;         }         $  option->setAttribute('value', $  value->value);         $  options[] = $  option;       }            // If there is no pre-selected option, build an empty placeholder option       if ($  optionSelected === false) {         $  emptyOption = $  dom->createElement('option');              // Set option to hidden, disabled, and selected         foreach (['hidden', 'disabled', 'selected'] as $  attribute)           $  emptyOption->setAttribute($  attribute, $  attribute);         $  input->appendChild($  emptyOption);       }            // Add options from array to `<select>`       foreach ($  options as $  option) {         $  input->appendChild($  option);       }   break;   } }

OK, so there’s a lot going on here, but the underlying logic is the same. After setting up the outer <select>, we make an array of <option>s to append inside it.

We’re also doing some <select>-specific trickery here: If there is no pre-selected option, we add an empty placeholder option that is already selected, but can’t be selected by the user. The goal is to place our <label class="label"> as a “placeholder” using CSS, but this technique can be useful for all kinds of designs. By appending it to the $ input before appending the other options, we make sure it is the first option in the markup.

Now let’s handle <fieldset>s of radio buttons and checkboxes:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     // ...       case 'multiple_choice':       $  choiceType = $  field->field_metadata->multi_select === true ? 'checkbox' : 'radio';       $  element->setAttribute('class', "field {$  choiceType}-group");       $  input = $  dom->createElement('fieldset');            // Build a choice `<input>` for each option in the fieldset       foreach ($  field->field_metadata->values as $  choiceValue) {         $  choiceField = $  dom->createElement('div');         $  choiceField->setAttribute('class', 'choice');              // Set a unique ID using the field ID + the choice ID         $  choiceID = "{$  field->id}-{$  choiceValue->value}";              // Build the `<input>` element         $  choice = $  dom->createElement('input');         $  choice->setAttribute('type', $  choiceType);         $  choice->setAttribute('value', $  choiceValue->value);         $  choice->setAttribute('id', $  choiceID);         $  choice->setAttribute('name', $  field->id);         $  choiceField->appendChild($  choice);              // Build the `<label>` element         $  choiceLabel = $  dom->createElement('label', $  choiceValue->label);         $  choiceLabel->setAttribute('for', $  choiceID);         $  choiceField->appendChild($  choiceLabel);              $  input->appendChild($  choiceField);       }   break;   } }

So, first we determine if the field set should be for checkboxes or radio button. Then we set the container class accordingly, and build the <fieldset>. After that, we iterate over the available choices and build a <div> for each one with an <input> and a <label>.

Notice we use regular PHP string interpolation to set the container class on line 21 and to create a unique ID for each choice on line 30.

Fragments

One last type we have to add is slightly more complex than it looks. Many forms include instruction fields, which aren’t inputs but just some HTML we need to print between other fields.

We’ll need to reach for another DOMDocument method, createDocumentFragment(). This allows us to add arbitrary HTML without using the DOM structuring:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // Build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;    // Add a `<span>` for the label if it is set   // ...    // Build the input element   switch ($  field->type) {     //...       case 'instruction':       $  element->setAttribute('class', 'field text');       $  fragment = $  dom->createDocumentFragment();       $  fragment->appendXML($  field->text);       $  input = $  dom->createElement('p');       $  input->appendChild($  fragment);       break;   } }

At this point you might be wondering how we found ourselves with an object called $ input, which actually represents a static <p> element. The goal is to use a common variable name for each iteration of the fields loop, so at the end we can always add it using $ element->appendChild($ input) regardless of the actual field type. So, yeah, naming things is hard.

Validation

The API we are consuming kindly provides an individual validation message for each required field. If there’s a submission error, we can show the errors inline together with the fields, rather than a generic “oops, your bad” message at the bottom.

Let’s add the validation text to each element:

<?php // ... // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // build the container `<label>`   $  element = $  dom->createElement('label');   $  element->setAttribute('class', 'field');    // Reset input values   $  input = null;   $  label = null;   $  validation = null;    // Add a `<span>` for the label if it is set   // ...    // Add a `<em>` for the validation message if it is set   if (isset($  field->validation_message)) {     $  validation = $  dom->createElement('em');     $  fragment = $  dom->createDocumentFragment();     $  fragment->appendXML($  field->validation_message);     $  validation->appendChild($  fragment);     $  validation->setAttribute('class', 'validation-message');     $  validation->setAttribute('hidden', 'hidden'); // Initially hidden, and will be unhidden with Javascript if there's an error on the field   }    // Build the input element   switch ($  field->type) {     // ...   } }

That’s all it takes! No need to fiddle with the field type logic — just conditionally build an element for each field.

Bringing it all together

So what happens after we build all the field elements? We need to add the $ input, $ label, and $ validation objects to the DOM tree we’re building. We can also use the opportunity to add common attributes, like required. Then we’ll add the submit button, which is separate from the fields in this API.

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   // ...    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // Build the container `<label>`     $  element = $  dom->createElement('label');     $  element->setAttribute('class', 'field');        // Reset input values     $  input = null;     $  label = null;     $  validation = null;        // Add a `<span>` for the label if it is set     // ...        // Add a `<em>` for the validation message if it is set     // ...        // Build the input element     switch ($  field->type) {       // ...     }        // Add the input element     if ($  input) {       $  input->setAttribute('id', $  field->id);       if ($  field->required)         $  input->setAttribute('required', 'required');       if (isset($  field->max_length))         $  input->setAttribute('maxlength', $  field->max_length);       $  element->appendChild($  input);          if ($  label)         $  element->appendChild($  label);          if ($  validation)         $  element->appendChild($  validation);          $  form->appendChild($  element);     }   }      // Build the submit button   $  submitButtonLabel = $  formContent->submit_button_label;   $  submitButtonField = $  dom->createElement('div');   $  submitButtonField->setAttribute('class', 'field submit');   $  submitButton = $  dom->createElement('button', $  submitButtonLabel);   $  submitButtonField->appendChild($  submitButton);   $  form->appendChild($  submitButtonField);    // Get the HTML output   $  dom->appendChild($  form);   $  htmlString = $  dom->saveHTML();   echo $  htmlString; }

Why are we checking if $ input is truthy? Since we reset it to null at the top of the loop, and only build it if the type conforms to our expected switch cases, this ensures we don’t accidentally include unexpected elements our code can’t handle properly.

Hey presto, a custom HTML form!

Bonus points: rows and columns

As you may know, many form builders allow authors to set rows and columns for fields. For example, a row might contain both the first name and last name fields, each in a single 50% width column. So how would we go about implementing this, you ask? By exemplifying (once again) how loop-friendly DOMDocument is, of course!

Our API response includes the grid data like this:

{   "submit_button_label": "Submit now!",   "fields": [     {       "id": "first-name",       "type": "text",       "label": "First name",       "required": true,       "validation_message": "First name is required.",       "max_length": 30,       "row": 1,       "column": 1     },     {       "id": "category",       "type": "multiple_choice",       "label": "Choose all categories that apply",       "required": false,       "field_metadata": {         "multi_select": true,         "values": [           { "value": "travel", "label": "Travel" },           { "value": "marketing", "label": "Marketing" }         ]       },       "row": 2,       "column": 1     }   ] }

We’re assuming that adding a data-column attribute is enough for styling the width, but that each row needs to be it’s own element (i.e. no CSS grid).

Before we dive in, let’s think through what we need in order to add rows. The basic logic goes something like this:

  1. Track the latest row encountered.
  2. If the current row is larger, i.e. we’ve jumped to the next row, create a new row element and start adding to it instead of the previous one.

Now, how would we do this if we were concatenating strings? Probably by adding a string like '</div><div class="row">' whenever we reach a new row. This kind of “reversed HTML string” is always very confusing to me, so I can only imagine how my IDE feels. And the cherry on top is that thanks to the browser auto-closing open tags, a single typo will result in a gazillion nested <div>s. Just like fun, but the opposite.

So what’s the structured way to handle this? Thanks for asking. First let’s add row tracking before our loop and build an additional row container element. Then we’ll make sure to append each container $ element to its $ rowElement rather than directly to $ form.

<?php function renderForm ($  endpoint) {   // Get the data from the API and convert it to a PHP object   // ...    // Start building the DOM   $  dom = new DOMDocument();   $  form = $  dom->createElement('form');    // init tracking of rows   $  row = 0;   $  rowElement = $  dom->createElement('div');   $  rowElement->setAttribute('class', 'field-row');    // Iterate over the fields and build each one   foreach ($  formFields as $  field) {     // Build the container `<label>`     $  element = $  dom->createElement('label');     $  element->setAttribute('class', 'field');     $  element->setAttribute('data-row', $  field->row);     $  element->setAttribute('data-column', $  field->column);          // Add the input element to the row     if ($  input) {       // ...       $  rowElement->appendChild($  element);       $  form->appendChild($  rowElement);     }   }   // ... }

So far we’ve just added another <div> around the fields. Let’s build a new row element for each row inside the loop:

<?php // ... // Init tracking of rows $  row = 0; $  rowElement = $  dom->createElement('div'); $  rowElement->setAttribute('class', 'field-row');  // Iterate over the fields and build each one foreach ($  formFields as $  field) {   // ...   // If we've reached a new row, create a new $  rowElement   if ($  field->row > $  row) {     $  row = $  field->row;     $  rowElement = $  dom->createElement('div');     $  rowElement->setAttribute('class', 'field-row');   }    // Build the input element   switch ($  field->type) {     // ...       // Add the input element to the row       if ($  input) {         // ...         $  rowElement->appendChild($  element);          // Automatically de-duped         $  form->appendChild($  rowElement);       }   } }

All we need to do is overwrite the $ rowElement object as a new DOM element, and PHP treats it as a new unique object. So, at the end of every loop, we just append whatever the current $ rowElement is — if it’s still the same one as in the previous iteration, then the form is updated; if it’s a new element, it is appended at the end.

Where do we go from here?

Forms are a great use case for object-oriented templating. And thinking about that snippet from WordPress Core, an argument might be made that nested menus are a good use case as well. Any task where the markup follows complex logic makes for a good candidate for this approach. DOMDocument can output any XML, so you could also use it to build an RSS feed from posts data.

Here’s the entire code snippet for our form. Feel free it adapt it to any form API you find yourself dealing with. Here’s the official documentation, which is good for getting a sense of the available API.

We didn’t even mention DOMDocument can parse existing HTML and XML. You can then look up elements using the XPath API, which is kinda similar to document.querySelector, or cheerio on Node.js. There’s a bit of a learning curve, but it’s a super powerful API for handling external content.

Fun(?) fact: Microsoft Office files that end with x (e.g. .xlsx) are XML files. Don’t tell the marketing department, but it’s possible to parse Word docs and output HTML on the server.

The most important thing is to remember that templating is a superpower. Being able to build the right markup for the right situation can be the key to a great UX.


The post Building a Form in PHP Using DOMDocument appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,

Building Your Own Subscription Newsletter

(This is a sponsored post.)

I did a sponsored video the other week explaining how to build a paid subscription newsletter using WordPress (we did it on WordPress.com but it could be hosted anywhere), MailPoet (a plugin to visually author the emails, as well as send them), and WooCommerce (to manage the payments and subscriptions).

I published the video here and there is a landing page for the whole concept here.

I spent a lot of time on it! I feel personally compelled by the idea because I’m pretty big on having a website you control be the home base for your business. If a paid newsletter is part of your business, awesome, might as well use your own website to do it.

If you’d like to be more slowly guided through the process, watch that video above. I literally do the entire thing from start to finish in that video. But I know some folks like a more rapid-fire explanation, so allow me to do that quick.


1. Have a WordPress site

Self-hosted works. You can also use WordPress.com so long as you’re on the Business or eCommerce plan, because you’ll need to install plugins.

2. Install WooCommerce & WooCommerce Subscriptions on it.

These are two separate plugins. WooCommerce is free. WooCommerce Subscriptions is $ 199/year.

3) Install MailPoet

MailPoet is a plugin. On WordPress.com, you might even be prompted to install it while installing the WooCommerce plugins. But if not, you can install it anytime.

4) Create a product that is a subscription

This is straightforward stuff in WooCommerce. Add a product and set the type to “Simple subscription.”

5. Craft an awesome newsletter in MailPoet

This is what MailPoet excels at. It’s a visual email builder right in your WordPress admin. It has excellent templates to start you started.

6. Send the newsletter only to people with an active subscription.

The trick is to make a “segment” of a list that is specifically created from WooCommerce customers that have an active subscription.


That’s it, really. You should know that MailPoet is an email sending service as well, and you’ll probably want to use that, since it’s free up to 1,000 subscribers anyway, is very easy to hook up, and will provide you with analytics data on the sends. But you can also wire it up to other email sending services as well.

Again, the beauty of all this is that it all happens from your website — meaning you could also be sending out a non-subscription newsletter with the same system. You could publish the newsletters on your website as an SEO play. You could sell other products and services since you have 100% of the infrastructure to do that now. You’re really in good hands here with a lot of power and flexibility.


The post Building Your Own Subscription Newsletter appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Building a Cool Front End Thing Generator

Whether you are just starting out on the front end, or you’ve been doing it for a long time, building a tool that can generate some cool front-end magic can help you learn something new, develop your skills and maybe even get you a little notoriety.

You might have run across some of these popular online generators:

I’ve had fun building a few of these myself over the years. Basically, any time you run across some cool front-end thing, there might be an opportunity to make an interactive generator for that thing.

In this case, we are going to make an Animated Background Gradient Generator.

Scaffolding the project in Next

A nice thing about these projects is that they’re all yours. Choose whatever stack you want and get going. I’m a big fan of Next.js, so for this project, I’m going to start as a basic Create Next App project.

npx create-next-app animated-gradient-background-generator 

This generates all the files we need to get started. We can edit pages/index.js to be the shell for our project.

import Head from "next/head" import Image from "next/image" export default function Home() {   return (     <>       <Head>         <title>Animated CSS Gradient Background Generator</title>         <meta name="description" content="A tool for creating animated background gradients in pure CSS." />         <link rel="icon" href="/favicon.ico" />       </Head>       <main>         <h1>           Animated CSS Gradient Background Generator         </h1>       </main>     </>   ) }

Animated gradients?

At the time I’m writing this article, if you do a search for animated CSS gradient background, the first result is this Pen by Manuel Pinto.

Let’s take a look at the CSS:

body {   background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);   background-size: 400% 400%;   animation: gradient 15s ease infinite; }  @keyframes gradient {   0% {     background-position: 0% 50%;   }   50% {     background-position: 100% 50%;   }   100% {     background-position: 0% 50%;   } } 

This is a great example that we can use as the foundation for the generated animation.

A React component to describe an animated gradient

We can break out a few possible configurable options for the generator:

  • An array of gradient colors
  • The angle of the gradient
  • The speed of the animation

To put in context, we want to provide these settings as data throughout our little app using a higher-order component, context/SettingsContext.js, along with some defaults.

import React, { useState, createContext } from "react"  const SettingsContext = createContext({ colorSelection: [] })  const SettingsProvider = ({ children }) => {   const [colorSelection, setColorSelection] = useState([     "deepskyblue",     "darkviolet",     "blue",   ])   const [angle, setAngle] = useState(300)   const [speed, setSpeed] = useState(5)      return (     <SettingsContext.Provider       value={{         colorSelection,         setColorSelection,         angle,         setAngle,         speed,         setSpeed,       }}     >       {children}     </SettingsContext.Provider>   ) }  export { SettingsContext, SettingsProvider }

For our generator’s components, we want to create:

  • a control components to adjust these settings,
  • a visual display component for generated animated gradient, and
  • a component for the CSS code output.

Let’s start with a Controls component that contains the various inputs we used to adjust the settings.

import Colors from "./Colors"  const Controls = (props) => (   <>     <Colors />   </> )  export default Controls

We can add our SettingsProvider and Controls components to pages/index.js:

import Head from "next/head" import Image from "next/image" import { SettingsProvider } from "../context/SettingsContext" import Controls from "../components/Controls" import Output from "../components/Output"  export default function Home() {   return (     <>       <Head>         ...       </Head>        <SettingsProvider>         <main style={{ textAlign: "center", padding: "64px" }}>           <h1>Animated CSS Gradient Background Generator</h1>           <Controls />           <Output />         </main>       </SettingsProvider>     </>   ) }

Our SettingsProvider begins with the three colors from our CodePen example as defaults. We can verify that we are getting the color settings via our SettingsContext in a new Colors component.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const Colors = () => {   const { colorSelection } = useContext(SettingsContext)   return (     <>       {colorSelection.map((color) => (         <div>{color}</div>       ))}     </>   ) }  export default Colors

Let’s use the Colors component to display individual color swatches with a small button to delete via our SettingsContext.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const Colors = () => {   const { colorSelection, setColorSelection } = useContext(SettingsContext)    const onDelete = (deleteColor) => {     setColorSelection(colorSelection.filter((color) => color !== deleteColor))   }    return (     <div>       {colorSelection.map((color) => (         <div           key={color}           style={{             background: color,             display: "inline-block",             padding: "32px",             margin: "16px",             position: "relative",             borderRadius: "4px",           }}         >           <button             onClick={() => onDelete(color)}             style={{               background: "crimson",               color: "white",               display: "inline-block",               borderRadius: "50%",               position: "absolute",               top: "-8px",               right: "-8px",               border: "none",               fontSize: "18px",               lineHeight: 1,               width: "24px",               height: "24px",               cursor: "pointer",               boxShadow: "0 0 1px #000",             }}           >             ×           </button>         </div>       ))}     </div>   ) }  export default Colors

You may notice that we have been using inline styles for CSS at this point. Who cares! We’re having fun here, so we can do whatever floats our boats.

Handling colors

Next, we create an AddColor component with a button that opens a color picker used to add more colors to the gradient.

For the color picker, we will install react-color and use the ChromePicker option.

npm install react-color

Once again, we will utilize SettingsContext to update the gradient color selection.

import React, { useState, useContext } from "react" import { ChromePicker } from "react-color" import { SettingsContext } from "../context/SettingsContext"  const AddColor = () => {   const [color, setColor] = useState("white")   const { colorSelection, setColorSelection } = useContext(SettingsContext)    return (     <>       <div style={{ display: "inline-block", paddingBottom: "32px" }}>         <ChromePicker           header="Pick Colors"           color={color}           onChange={(newColor) => {             setColor(newColor.hex)           }}         />       </div>       <div>         <button           onClick={() => {             setColorSelection([...colorSelection, color])           }}           style={{             background: "royalblue",             color: "white",             padding: "12px 16px",             borderRadius: "8px",             border: "none",             fontSize: "16px",             cursor: "pointer",             lineHeight: 1,           }}         >           + Add Color         </button>       </div>     </>   ) }  export default AddColor

Handling angle and speed

Now that our color controls are finished, let’s add some components with range inputs for setting the angle and animation speed.

Here’s the code for AngleRange, with SpeedRange being very similar.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const AngleRange = () => {   const { angle, setAngle } = useContext(SettingsContext)    return (     <div style={{ padding: "32px 0", fontSize: "18px" }}>       <label         style={{           display: "inline-block",           fontWeight: "bold",           width: "100px",           textAlign: "right",         }}         htmlFor="angle"       >         Angle       </label>       <input         type="range"         id="angle"         name="angle"         min="-180"         max="180"         value={angle}         onChange={(e) => {           setAngle(e.target.value)         }}         style={{           margin: "0 16px",           width: "180px",           position: "relative",           top: "2px",         }}       />       <span         style={{           fontSize: "14px",           padding: "0 8px",           position: "relative",           top: "-2px",           width: "120px",           display: "inline-block",         }}       >         {angle} degrees       </span>     </div>   ) }  export default AngleRange

Now for the fun part: rendering the animated background. Let’s apply this to the entire background of the page with an AnimatedBackground wrapper component.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext" const AnimatedBackground = ({ children }) => {   const { colorSelection, speed, angle } = useContext(SettingsContext) const background =   "linear-gradient(" + angle + "deg, " + colorSelection.toString() + ")" const backgroundSize =   colorSelection.length * 60 + "%" + " " + colorSelection.length * 60 + "%" const animation =   "gradient-animation " +   colorSelection.length * Math.abs(speed - 11) +   "s ease infinite" return (   <div style={{ background, "background-size": backgroundSize, animation, color: "white" }}>     {children}   </div>   ) } export default AnimatedBackground

We’re calling the CSS animation for the gradient gradient-animation. We need to add that to styles/globals.css to trigger the animation:

@keyframes gradient-animation {   0% {     background-position: 0% 50%;   }   50% {     background-position: 100% 50%;   }   100% {     background-position: 0% 50%;   } } 

Making it useful to users

Next, let’s add some code output so people can copy and paste the generated CSS and use in their own projects.

import React, { useContext, useState } from "react" import { SettingsContext } from "../context/SettingsContext" const Output = () => {   const [copied, setCopied] = useState(false) const { colorSelection, speed, angle } = useContext(SettingsContext) const background =   "linear-gradient(" + angle + "deg," + colorSelection.toString() + ")" const backgroundSize =   colorSelection.length * 60 + "%" + " " + colorSelection.length * 60 + "%" const animation =   "gradient-animation " +   colorSelection.length * Math.abs(speed - 11) +   "s ease infinite" const code = `.gradient-background {   background: $  {background};   background-size: $  {backgroundSize};   animation: $  {animation}; } @keyframes gradient-animation {   0% {     background-position: 0% 50%;   }   50% {     background-position: 100% 50%;   }   100% {     background-position: 0% 50%;   } }` return (     <div       style={{ position: "relative", maxWidth: "640px", margin: "64px auto" }}     >       <pre         style={{           background: "#fff",           color: "#222",           padding: "32px",           width: "100%",           borderRadius: "4px",           textAlign: "left",           whiteSpace: "pre",           boxShadow: "0 2px 8px rgba(0,0,0,.33)",           overflowX: "scroll",         }}       >         <code>{code}</code>         <button           style={{             position: "absolute",             top: "8px",             right: "8px",             background: "royalblue",             color: "white",             padding: "8px 12px",             borderRadius: "8px",             border: "none",             fontSize: "16px",             cursor: "pointer",             lineHeight: 1,           }}           onClick={() => {             setCopied(true)             navigator.clipboard.writeText(code)           }}         >           {copied ? "copied" : "copy"}         </button>       </pre>     </div>   ) } export default Output

Making it fun

It is sometimes fun (and useful) to add a button that sets random values on a generator like this. That gives people a way to quickly experiment and see what kinds of results they can get out of the tool. It is also an opportunity to look up cool stuff like how to generate random hex colors.

import React, { useContext } from "react" import { SettingsContext } from "../context/SettingsContext"  const Random = () => {   const { setColorSelection, setAngle, setSpeed } = useContext(SettingsContext)    const goRandom = () => {     const numColors = 3 + Math.round(Math.random() * 3)     const colors = [...Array(numColors)].map(() => {       return "#" + Math.floor(Math.random() * 16777215).toString(16)     })     setColorSelection(colors)     setAngle(Math.floor(Math.random() * 361))     setSpeed(Math.floor(Math.random() * 10) + 1)   }    return (     <div style={{ padding: "48px 0 16px" }}>       <button         onClick={goRandom}         style={{           fontSize: "24px",           fontWeight: 200,           background: "rgba(255,255,255,.9)",           color: "blue",           padding: "24px 48px",           borderRadius: "8px",           cursor: "pointer",           boxShadow: "0 0 4px #000",           border: "none",         }}       >         RANDOM       </button>     </div>   ) }  export default Random

Wrapping up

There will be a few final things you’ll want to do to wrap up your project for initial release:

  • Update package.json with your project information.
  • Add some links to your personal site, the project’s repository and give credit where its due.
  • Update the README.md file that was generated with default content by Create Next App.

That’s it! We’re ready to release our new cool front end thing generator and reap the rewards of fame and fortune that await us!

You can see the code for this project on GitHub and the demo is hosted on Netlify.


The post Building a Cool Front End Thing Generator appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Building a Command Line Tool with Nodejs and Fauna

Command line tools are one of the most popular applications we have today. We use command line tools every day, and they range from git, npm or yarn. Command line tools are very fast and useful for automating applications and workflows.

We will be building a command line tool with Node.js and Fauna for our database in this post. In addition, we will be creating a random quotes application using Node.js, and add permission and a keyword for our app.

Prerequisites

To take full advantage of this tutorial, make sure you have the following installed on your local development environment:

  • Node.js version >= 16.x.x installed.
  • Have access to one package manager such as npm or yarn.
  • Access to Fauna dashboard.

Getting Started with Fauna

Register a new account using email credentials or a GitHub account. You can register a new account here. Once you have created a new account or signed in, you are going to be welcomed by the dashboard screen:

Creating a New Fauna Instance

To create a new database instance using Fauna services, you have to follow some simple steps. On the dashboard screen, press the button New Database:

Next, enter the name of the database and save. Once a database instance is set up, you are ready to access the key. Use access keys to connect authorization and a connection to the database from a single-page application. To create your access key, navigate to the side menu, and go to the Security tab and click on the New Key button.

Creating a Collection

Navigate to your dashboard, click on the Collections tab from the side menu, press the New Collection, button, input your desired name for the new collection, and save.

Creating Indexes

To complete setup, create indexes for our application. Indexes are essential because searching documents are done using indexes in Fauna by matching the user input against the tern field. Create an index by navigating to the Indexes tab of our Fauna dashboard.

Now, we are ready to build our notes command-line application using Node.js and our database.

Initializing a Node.js App and Installing Dependencies

This section will initialize a Node.js application and install the dependencies we need using the NPM package. We are also going to build a simple quotes application from this link.

Getting Started

To get started, let’s create a folder for our application inside the project folder using the code block below on our terminal:

mkdir quotes_cli cd quotes_cli touch quotes_app npm init -y

In the code block above, we created a new directory, navigated into the directory, and created a new file called quotes_app, and ended by initializing the npm dependencies. Next, add a package to make requests to the quotes server using axios.

npm i axios

Add a package for coloring our texts, chalk is an NPM package that helps us add color to print on the terminal. To add chalk, use the code block below

npm i chalk Let’s also import a dotenv package using the code block:

npm i dotenv

Building the Quotes App

In our quotes_app file, let’s add the code block below

const axios = require('axios') const chalk = require('chalk'); const dotenv = require('dotenv'); const url = process.env.APP_URL axios({   method: 'get',   url: url,   headers: { 'Accept': 'application/json' }, }).then(res => {   const quote = res.data.contents.quotes[0].quote   const author = res.data.contents.quotes[0].author   const log = chalk.red(`$ {quote} - $ Fortune Ikechi`)    console.log(log) }).catch(err => {   const log = chalk.red(err)    console.log(log) })

In the code block above, we imported axios, chalk, and dotenv. We added the URL of our database, our Fauna database, and using axios, we made a GET request to the URL and added headers to enable us to get our response in json.

To log a quote, we use JavaScript promises to log the quote and its author on our console and added a catch method for catching errors.

Before we run, let’s change the permissions on our file using the code below:

chmod +x quotes_app

Next, run the application using our keyword below:

./quotes_app

We should get a result similar to the image below

Conclusion

In this article, we learned more about Fauna and Node.js command-line tools. You can extend the application to be able to add date reminders in real-time.

Here is a list of some resources that you might like after reading this post:


The post Building a Command Line Tool with Nodejs and Fauna appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

A Look at Building with Astro

Astro is a brand new framework for building websites. To me, the big thing is that it allows you to build a site like you’re using a JavaScript framework (and you are), but the output is a zero-JavaScript static site. You can opt-in to client-side JavaScript as needed, and there are clever options for doing so. Notably, the learning curve is somewhat flattened by the fact that it supports componentry you may already know: React/Preact (JSX), Svelte, Vue, or web components.

Table stakes

Starting a new project is as easy as it should be:

npm init astro npm install npm start

There is a helpful little process and output:

As expected (like you would get with Next or Nuxt or any other site builder kind of project) you get a dev server at a local port you can pop right up:

From here, I consider the table stakes to be CSS injection / Hot Module Reloading. No worries there:

A static site generator with honest-to-god real actual components

This is such a wonderful thing to me. I really like the idea of static site generators—I think they make a lot of sense in a lot of situations. Sending HTML over-the-wire is just a good move for resiliency, CDN-efficiency, SEO, accessibility, you name it. But in the past a lot of the options were either:

  • A JavaScript powered static site generator, that does generate a “static” site, but also ships a JavaScript bundle (e.g. Next or Gatsby)
  • A static site generator that is more focused on HTML and has its own templating/formats that aren’t JavaScript components (e.g. Eleventy or Jekyll)

I know there are exceptions, but this covers the vast majority of the site generator market.

But I want both!

  • I want to craft sites from JavaScript-components, because the syntax and tooling around them is just better than any other component system we have right now.
  • I want static output that is actually zero-JavaScript (unless I manually opt-in to things).

That’s what happens with Astro.

Those components?

  • They can be .jsx files
  • They can be .svelte files
  • They can be .vue files
  • These are “renderers” and you can BYO.

Astro also has it’s own format (.astro) and it’s also very compelling because:

  • It’s obviously a first-class citizen of how Astro works
  • It’s comfortably JSX-like…
  • …except better because it does stuff like makes the <head> work automatically
  • Styled scoping works out of the box, through a normal <style> tag
  • “Fenced” JavaScript runs during build. Let’s look at that next.

Astro files

I mentioned some of the cool parts about the .astro syntax right above. At a higher level, I just like how they look. So little boilerplate! Just gets right to it.

--- import SomeComponent from "../components/SomeComponent";  // This runs in Node, so you look at your command line to see it. console.log("Hi.");  // Example: <SomeComponent greeting="(Optional) Hello" name="Required Name" /> const { greeting = 'Hello', name } = Astro.props; const items = ["Dog", "Cat", "Platipus"]; --- <!-- JSX-like, but also more pleasantly HTML like, like this comment  --> <div class="module">   <h1>{greeting}, {name}!</h1>   <ul>     {items.map((item) => (       <li>{item}</li>     ))} </ul> </div>  <SomeComponent regular="props" />  <style>    /* Scoped! */   .module {     padding: 1rem;   } </style>

The “fences” (---) at the top is where the initial JavaScriptin’ goes. That’s where I yank in the props for this component if it needs any (they can be typed if you like that), do imports/exports, and set up data for the template below.

What feels a little funky, but is in-line with the Astro vibe, is that this is essentially Node JavaScript. It runs in the build process. So that console.log() statement I don’t see in my browser console, I see it in my command line.

pages-style routing

It’s tempting to say Next.js popularized this, but really the concept is as old as file systems. Think of how a classic Apache server works. If you have a file system like:

index.html /about/   index.html

In a browser, you can visit http://website.com/about and that will render that index.html page under the /about folder. That’s what the routing is like here. By virtue of me having:

/pages/   index.astro   about.astro

I’ll have a homepage as well as an /about/ page on my site. That’s just a refreshingly nice way to deal with routing—as opposed to needing to build your own routing with component-ry all to itself.

If you want to do that thing where all the content of your site lives in Markdown files right in the repo, that’s a first-class citizen.

I think this is super common for stuff like blogs and documentation, especially as those are already popular targets for static site generators. And in these early days, I think we’re going to see a lot of Astro sites along those lines while people wait to see if it’s ready for bigger undertakings.

One way to use Markdown is to make Pages in Markdown straight away. The Markdown will also have “fences” (Frontmatter) where you chuck what layout you want to use (best to use an .astro file) and pass in data if you need to. Then the entire content of the Markdown file will flow into the <slot />. Pretty darn slick:

Another incredibly satisfying way to use Markdown in Astro is using the built-in <Markdown /> component. Import it and use it:

--- import { Markdown } from 'astro/components'; ---  <main>   <Markdown>     # Hello world!          - Do thing     - Another thing in my *cool list*   </Markdown>    <div>Outside Markdown</div> </main>

You can also go snag some Markdown from elsewhere in your project and barf that into a component. That leads into fetching data, so let’s look at that next.

I suppose it’s kind of weird how Astro supports all these different frameworks out of the box.

I’ve overheard some pushback that Astro is inefficient at the npm install level since you have to bring down a bunch of stuff you likely won’t need or use. I’ve overheard some pushback on the idea that mixing-matching JavaScript frameworks is a terrible idea.

I agree it’s weird-feeling, but I’m not particularly worried about non-user-facing things. When things are happening only during the build process and all the user ever gets is HTML, use whatever feels good! If you ultimately do load the components-based frameworks to do on-page interactive things, surely it makes sense to limit it to one. And since you’re getting so much at build time, maybe it makes sense to use something designed for super light on-rendered-page interactivity.

Fetching data rules

We were just talking about Markdown so let’s close the loop there. You can “fetch” data internally in Astro by using fetchContent. Look how straightforward it is:

I fetch it the raw Markdown, then I could use the HTML it returns if I want, or slap it into a <Markdown /> component if that makes sense for whatever reason:

--- import { Markdown } from 'astro/components'; const localData = Astro.fetchContent('../content/data.md'); ---  <div class="module">   <Markdown content={localData[0].astro.source} /> </div>

But I don’t have to fetch internal data only. I’m a fan of Eleventy. During an Eleventy build, you can certainly go fetch data from an outside source, but I’d argue it’s a little finnicky. You fetch the data with code in a separate JavaScript file, pulling in your own network library, then processing and returning the data to use elsewhere in a template. Like this. In Astro, that fetching can happen right alongside the component where you need it.

Check out this real-world-ish example where I yank in data from right here from CSS-Tricks and display it as cards.

--- import Card from '../components/Card.astro'; import Header from '../components/Header';  const remoteData = await fetch('https://css-tricks.com/wp-json/wp/v2/posts?per_page=12&_embed').then(response => response.json()); ---  <!doctype html> <html lang="en">  <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1">     <title>CSS-Trickzz</title>     <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⭐️</text></svg>">     <link rel="stylesheet" href="/style/global.css">      <style lang="scss">       .grid {         margin: 4rem;         display: grid;         grid-template-columns: repeat(2, 1fr);         @media (max-width: 650px) {           grid-template-columns: repeat(1, 1fr);           margin: 2rem;         }         gap: 3rem;       }     </style> </head>  <body>   <main>     <Header />      <div class="grid">       {remoteData.map((post) => {         return(           <Card              title={post.title.rendered}             link={post.link}             excerpt={post.excerpt.rendered}             featured_img={post.featured_media_src_url}           />         )       })}     </div>    </main> </body>  </html>

Check it out, I can build a page from CSS-Tricks data just that easily:

What’s fascinating about that is that the data happens:

  1. in Node, not client-side, and
  2. during the build process.

So, in order to keep a website like this updated, I’d have to run the build/deploy process regularly.

Styling

Let’s say you want to use Sass to style your site. With many site generators, they punt on this, as a philosophy. Like saying “nah, we don’t want to be opinionated here, you style however you want to”. And I get that, it might be a strength as sometimes frameworks that are too opinionated lose people. But to me, it often feels unfortunate as now I’m on my own to wire up some style-processing build processes (e.g. Gulp) that I really just don’t want to deal with.

With Astro, the philosophy seems to be to support a wide swath of popular styling techniques out of the box right away.

  • Just import "./style.css"; vanilla stylesheets
  • Use a <style> block anywhere in .astro files and the CSS will be scoped to that component…
  • … which is like CSS modules, but that’s only needed if you go for a .jsx file, and if you do, it’s supported.
  • The styling capabilities of .svelte and .vue files work as expected.
  • Sass is built in, just put <style lang="scss"> on the styling blocks wherever.

The styling doc has more detail.

The fancy opt-in JavaScript tricks

Allow me to blockquote this from the README:

  • <MyComponent /> will render an HTML-only version of MyComponent (default)
  • <MyComponent:load /> will render MyComponent on page load
  • <MyComponent:idle /> will use requestIdleCallback() to render MyComponent as soon as main thread is free
  • <MyComponent:visible /> will use an IntersectionObserver to render MyComponent when the element enters the viewport

That’s some fancy dancing. HTML by default, and you opt-in to running your components client-side (JavaScript) only when you specifically want to, and even then, under efficient conditions.

I put a little Vue-based counter (from their examples) onto my demo site and used the :visible modifier to see it work. Check it out:

The Vue stuff only loads when it needs to. Like the blog post says:

Of course, sometimes client-side JavaScript is inevitable. Image carousels, shopping carts, and auto-complete search bars are just a few examples of things that require some JavaScript to run in the browser. This is where Astro really shines: When a component needs some JavaScript, Astro only loads that one component (and any dependencies). The rest of your site continues to exist as static, lightweight HTML.

The Discord is poppin’

I should point out that Astro is super duper new. As I write, they don’t even have real documentation up. It might feel a bit early to be using a framework with docs that only exist as a README. They are working on it though! I’ve seen previews of it because I happen to be in the Discord.

I think they are very smart to have a public Discord as it means there is a hot-n-fast feedback loop for them to improve the framework. I’ve found that being in it is super useful.

I believe they are hoping that Astro grows up into much more than a framework, but a complete platform, where Astro is just the open-source core. You can hear Fred talk with Jason about that on a Learn with Jason episode.


The post A Look at Building with Astro appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Scaling Organizations Should Consider Building a Website Backed by a CRM Platform

To make some terminology clear here:

  • CMS = Content Management System
  • CRM = Customer Relationship Management

Both are essentially database-backed systems for managing data. HubSpot is both, and much more. Where a CMS might be very focused on content and the metadata around making content useful, a CRM is focused on leads and making communicating with current and potential customers easier.

They can be brothers-in-arms. We’ll get to that.

Say a CRM is set up for people. You run a Lexus dealership. There is a quote form on the website. People fill it out and enter the CRM. That lead can go to your sales team for taking care of that customer.

But a CRM could be based on other things. Say instead of people it’s based on real estate listings. Each main entry is a property, with essentially metadata like photos, address, square footage, # of bedrooms/baths, etc. Leads can be associated with properties.

That would be a nice CRM setup for a real estate agency, but the data that is in that CRM might be awfully nice for literally building a website around those property listings. Why not tap into that CRM data as literal data to build website pages from?

That’s what I mean by a CRM and CMS being brothers-in-arms. Use them both! That’s why HubSpot can be an ideal home for websites like this.

To keep that tornado of synergy going, HubSpot can also help with marketing, customer service, and integrations. So there is a lot of power packed into one platform.

And with that power, also a lot of comfort and flexibility.

  • You’re still developing locally.
  • You’re still using Git.
  • You can use whatever framework or site-building tools you want.
  • You’ve got a CLI to control things.
  • There is a VS Code Extension for super useful auto-complete of your data.
  • There is a staging environment.

And the feature just keep coming. HubSpot really has a robust set of tools to make sure you can do what you need to do.

As developer-rich as this all is, it doesn’t mean that it’s developer-only. There are loads of tools for working with the website you build that require no coding at all. Dashboard for content management, data wrangling, style control, and even literal drag-and-drop page builders.

It’s all part of a very learnable system.

Themestemplatesmodules, and fields are the objects you’ll work with most in HubSpot CMS as a developer. Using these different objects effectively lets you give content creators the freedom to work and iterate on websites independently while staying inside style and layout guardrails you set.


The post Scaling Organizations Should Consider Building a Website Backed by a CRM Platform appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , ,
[Top]

Building a Headless CMS with Fauna and Vercel Functions

This article introduces the concept of the headless CMS, a backend-only content management system that allows developers to create, store, manage and publish the content over an API using the Fauna and Vercel functions. This improves the frontend-backend workflow, that enables developers to build excellent user experience quickly.

In this tutorial, we will learn and use headless CMS, Fauna, and Vercel functions to build a blogging platform, Blogify🚀. After that, you can easily build any web application using a headless CMS, Fauna and Vercel functions.

Introduction

According to MDN, A content management system (CMS) is a computer software used to manage the creation and modification of digital content. CMS typically has two major components: a content management application (CMA), as the front-end user interface that allows a user, even with limited expertise, to add, modify, and remove content from a website without the intervention of a webmaster; and a content delivery application (CDA), that compiles the content and updates the website.

The Pros And Cons Of Traditional vs Headless CMS

Choosing between these two can be quite confusing and complicated. But they both have potential advantages and drawbacks.

Traditional CMS Pros

  • Setting up your content on a traditional CMS is much easier as everything you need (content management, design, etc) are made available to you.
  • A lot of traditional CMS has drag and drop, making it easy for a person with no programming experience to work easily with them. It also has support for easy customization with zero to little coding knowledge.

Traditional CMS Cons

  • The plugins and themes which the traditional CMS relies on may contain malicious codes or bugs and slow the speed of the website or blog.
  • The traditional coupling of the front-end and back-end definitely would more time and money for maintenance and customization.

Headless CMS Pros

  • There’s flexibility with choice of frontend framework to use since the frontend and backend are separated from each other, it makes it possible for you to pick which front-end technology suits your needs. It gives the freewill to choose the tools need to build the frontend—flexibility during the development stage.
  • Deploying works easier with headless CMS. The applications (blogs, websites, etc) built with headless CMS can be easily be deployed to work on various displays such as web device, mobile devices, AR/VR devices.

Headless CMS Cons

  • You are left with the worries of managing your back-end infrastructures, setting up the UI component of your site, app.
  • Implementation of headless CMS are known to be more costly against the traditional CMS. Building headless CMS application that embodies analytics are not cost-effective.

Fauna uses a preexisting infrastructure to build web applications without the usually setting up a custom API server. This efficiently helps to save time for developers, and the stress of choosing regions and configuring storage that exists among other databases; which is global/multi-region by default, are nonexistent with Fauna. All maintenance we need are actively taken care of by engineers and automated DevOps at Fauna. We will use Fauna as our backend-only content management system.

Pros Of Using Fauna

  • The ease to use and create a Fauna database instance from within development environment of the hosting platforms like Netlify or Vercel.
  • Great support for querying data via GraphQL or use Fauna’s own query language. Fauna Query Language (FQL), for complex functions.
  • Access data in multiple models including relational, document, graph and temporal.
  • Capabilities like built-in authentication, transparent scalability and multi-tenancy are fully available on Fauna.
  • Add-on through Fauna Console as well as Fauna Shell makes it easy to manage database instance very easily.

Vercel Functions, also known as Serverless Functions, according to the docs are pieces of code written with backend languages that take an HTTP request and provide a response.

Prerequisites

To take full advantage of this tutorial, ensure the following tools are available or installed on your local development environment:

  • Access to Fauna dashboard
  • Basic knowledge of React and React Hooks
  • Have create-react-app installed as a global package or use npx to bootstrap the project.
  • Node.js version >= 12.x.x installed on your local machine.
  • Ensure that npm or yarn is also installed as a package manager

Database Setup With Fauna

Sign in into your fauna account to get started with Fauna, or first register a new account using either email credentials/details or using an existing Github account as a new user. You can register for a new account here. Once you have created a new account or signed in, you are going to be welcomed by the dashboard screen. We can also make use of the fauna shell if you love the shell environment. It easily allows you to create
and/or modify resources on Fauna through the terminal.

Using the fauna shell, the command is:

npm install --global fauna-shell fauna cloud-login

But we will use the website throughout this tutorial. Once signed in, the dashboard screen welcomes you:

Now we are logged in or have our accounts created, we can go ahead to create our Fauna. We’ll go through following simple steps to create the new fauna database using Fauna services. We start with naming our database, which we’ll use as our content management system. In this tutorial, we will name our database blogify.

With the database created, next step is to create a new data collection from the Fauna dashboard. Navigate to the Collection tab on the side menu and create a new collection by clicking on the NEW COLLECTION button.

We’ll then go ahead to give whatever name well suiting to our collection. Here we will call it blogify_posts.

Next step in getting our database ready is to create a new index. Navigate to the Indexes tab to create an index. Searching documents in Fauna can be done by using indexes, specifically by matching inputs against an index’s terms field. Click on the NEW INDEX button to create an index. Once in create index screen, fill out the form: selecting the collection we’ve created previously, then giving a name to our index. In this tutorial, we will name ours all_posts. We can now save our index.

After creating an index, now it’s time to create our DOCUMENT, this will contain the contents/data we want to use for our CMS website. Click on the NEW DOCUMENT button to get started. With the text editor to create our document, we’ll create an object data to serve our needs for the website.

The above post object represents the unit data we need to create our blog post. Your choice of data can be so different from what we have here, serving the purpose whatever you want it for within your website. You can create as much document you may need for your CMS website. To keep things simple, we just have three blog posts.

Now that we have our database setup complete to our choice, we can move on to create our React app, the frontend.

Create A New React App And Install Dependencies

For the frontend development, we will need dependencies such as Fauna SDK, styled-components and vercel in our React app. We will use the styled-components for the UI styling, use the vercel within our terminal to host our application. The Fauna SDK would be used to access our contents at the database we had setup. You can always replace the styled-components for whatever library you decide to use for your UI styling. Also use any UI framework or library you preferred to others.

npx create-react-app blogify # install dependencies once directory is done/created yarn add fauna styled-components # install vercel globally yarn global add vercel

The fauna package is Fauna JavaScript driver for Fauna. The library styled-components allows you to write actual CSS code to style your components. Once done with all the installation for the project dependencies, check the package.json file to confirm all installation was done
successfully.

Now let’s start an actual building of our blog website UI. We’ll start with the header section. We will create a Navigation component within the components folder inside the src folder, src/components, to contain our blog name, Blogify🚀.

import styled from "styled-components"; function Navigation() {   return (     <Wrapper>       <h1>Blogify🚀</h1>     </Wrapper>   ); } const Wrapper = styled.div`   background-color: #23001e;   color: #f3e0ec;   padding: 1.5rem 5rem;   & > h1 {     margin: 0px;   } `; export default Navigation;

After being imported within the App components, the above code coupled with the stylings through the styled-components library, will turn out to look like the below UI:

Now time to create the body of the website, that will contain the post data from our database. We structure a component, called Posts, which will contains our blog posts created on the backend.

import styled from "styled-components"; function Posts() {   return (     <Wrapper>       <h3>My Recent Articles</h3>       <div className="container"></div>     </Wrapper>   ); } const Wrapper = styled.div`   margin-top: 3rem;   padding-left: 5rem;   color: #23001e;   & > .container {     display: flex;     flex-wrap: wrap;   }   & > .container > div {     width: 50%;     padding: 1rem;     border: 2px dotted #ca9ce1;     margin-bottom: 1rem;     border-radius: 0.2rem;   }   & > .container > div > h4 {     margin: 0px 0px 5px 0px;   }   & > .container > div > button {     padding: 0.4rem 0.5rem;     border: 1px solid #f2befc;     border-radius: 0.35rem;     background-color: #23001e;     color: #ffffff;     font-weight: medium;     margin-top: 1rem;     cursor: pointer;   }   & > .container > div > article {     margin-top: 1rem;   } `; export default Posts;

The above code contains styles for JSX that we’ll still create once we start querying for data from the backend to the frontend.

Integrate Fauna SDK Into Our React App

To integrate the fauna client with the React app, you have to make an initial connection from the app. Create a new file db.js at the directory path src/config/. Then import the fauna driver and define a new client.
The secret passed as the argument to the fauna.Client() method is going to hold the access key from .env file:

import fauna from 'fauna'; const client = new fauna.Client({   secret: process.env.REACT_APP_DB_KEY, }); const q = fauna.query; export { client, q };

Inside the Posts component create a state variable called posts using useState React Hooks with a default value of an array. It is going to store the value of the content we’ll get back from our database using the setPosts function. Then define a second state variable, visible, with a default value of false, that we’ll use to hide or show more post content using the handleDisplay function that would be triggered by a button we’ll add later in the tutorial.

function App() {   const [posts, setPosts] = useState([]);   const [visible, setVisibility] = useState(false);   const handleDisplay = () => setVisibility(!visible);   // ... }

Creating A Serverless Function By Writing Queries

Since our blog website is going to perform only one operation, that’s to get the data/contents we created on the database, let’s create a new directory called src/api/ and inside it, we create a new file called index.js. Making the request with ES6, we’ll use import to import the client and the query instance from the config/db.js file:

export const getAllPosts = client   .query(q.Paginate(q.Match(q.Ref('indexes/all_posts'))))     .then(response => {       const expenseRef = response.data;       const getAllDataQuery = expenseRef.map(ref => {         return q.Get(ref);       });      return client.query(getAllDataQuery).then(data => data);    })    .catch(error => console.error('Error: ', error.message));
 })  .catch(error => console.error('Error: ', error.message));

The query above to the database is going to return a ref that we can map over to get the actual results need for the application. We’ll make sure to append the catch that will help check for an error while querying the database, so we can log it out.

Next is to display all the data returned from our CMS, database—from the Fauna collection. We’ll do so by invoking the query getAllPosts from the ./api/index.js file inside the useEffect Hook inside our Posts component. This is because when the Posts component renders for the first time, it iterates over the data, checking if there are any post in the database:

useEffect(() => {   getAllPosts.then((res) => {     setPosts(res);     console.log(res);   }); }, []);

Open the browser’s console to inspect the data returned from the database. If all things being right, and you’re closely following, the return data should look like the below:

With these data successfully returned from the database, we can now complete our Posts components, adding all necessary JSX elements that we’ve styled using styled-components library. We’ll use JavaScript map to loop over the posts state, array, only when the array is not empty:

import { useEffect, useState } from "react"; import styled from "styled-components"; import { getAllPosts } from "../api";  function Posts() {   useEffect(() => {     getAllPosts.then((res) => {       setPosts(res);       console.log(res);     });   }, []);    const [posts, setPosts] = useState([]);   const [visible, setVisibility] = useState(false);   const handleDisplay = () => setVisibility(!visible);    return (     <Wrapper>       <h3>My Recent Articles</h3>       <div className="container">         {posts &&           posts.map((post) => (             <div key={post.ref.id} id={post.ref.id}>               <h4>{post.data.post.title}</h4>               <em>{post.data.post.date}</em>               <article>                 {post.data.post.mainContent}                 <p style={{ display: visible ? "block" : "none" }}>                   {post.data.post.subContent}                 </p>               </article>               <button onClick={handleDisplay}>                 {visible ? "Show less" : "Show more"}               </button>             </div>           ))}       </div>     </Wrapper>   ); }  const Wrapper = styled.div`   margin-top: 3rem;   padding-left: 5rem;   color: #23001e;   & > .container {     display: flex;     flex-wrap: wrap;   }   & > .container > div {     width: 50%;     padding: 1rem;     border: 2px dotted #ca9ce1;     margin-bottom: 1rem;     border-radius: 0.2rem;   }   & > .container > div > h4 {     margin: 0px 0px 5px 0px;   }   & > .container > div > button {     padding: 0.4rem 0.5rem;     border: 1px solid #f2befc;     border-radius: 0.35rem;     background-color: #23001e;     color: #ffffff;     font-weight: medium;     margin-top: 1rem;     cursor: pointer;   }   & > .container > div > article {     margin-top: 1rem;   } `;  export default Posts;

With the complete code structure above, our blog website, Blogify🚀, will look like the below UI:

Deploying To Vercel

Vercel CLI provides a set of commands that allow you to deploy and manage your projects. The following steps will get your project hosted from your terminal on vercel platform fast and easy:

vercel login

Follow the instructions to login into your vercel account on the terminal

vercel

Using the vercel command from the root of a project directory. This will prompt questions that we will provide answers to depending on what’s asked.

vercel ? Set up and deploy “~/Projects/JavaScript/React JS/blogify”? [Y/n]  ? Which scope do you want to deploy to? ikehakinyemi ? Link to existing project? [y/N] n ? What’s your project’s name? (blogify)    # click enter if you don't want to change the name of the project ? In which directory is your code located? ./    # click enter if you running this deployment from root directory ? ? Want to override the settings? [y/N] n

This will deploy your project to vercel. Visit your vercel account to complete any other setup needed for CI/CD purpose.

Conclusion

I’m glad you followed the tutorial to this point, hope you’ve learnt how to use Fauna as Headless CMS. The combination of Fauna with Headless CMS concepts you can build great web application, from e-commerce application to Notes keeping application, any web application that needs data to be stored and retrieved for use on the frontend. Here’s the GitHub link to code sample we used within our tutorial, and the live demo which is hosted on vercel.


The post Building a Headless CMS with Fauna and Vercel Functions appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

What I Learned Building a Word Game App With Nuxt on Google Play

I fell in love with coding the moment I created my first CSS :hover effect. Years later, that initial bite into interactivity on the web led me to a new goal: making a game.


Those early moments playing with :hover were nothing special, or even useful. I remember making a responsive grid of blue squares (made with float, if that gives you an idea of the timeline), each of which turned orange when the cursor moved over them. I spent what felt like hours mousing over the boxes, resizing the window to watch them change size and alignment, then doing it all over again. It felt like pure magic.

What I built on the web naturally became more complex than that grid of <div> elements over the years, but the thrill of bringing something truly interactive to life always stuck with me. And as I learned more and more about JavaScript, I especially loved making games.

Sometimes it was just a CodePen demo; sometimes it was a small side project deployed on Vercel or Netlify. I loved the challenge of recreating games like color flood, hangman, or Connect Four in a browser.

After a while, though, the goal got bigger: what if I made an actual game? Not just a web app; a real live, honest-to-goodness, download-from-an-app-store game. Last August, I started working on my most ambitious project to date, and four months later, I released it to the world (read: got tired of fiddling with it): a word game app that I call Quina.

What’s the game (and what’s that name)?

The easiest way to explain Quina is: it’s Mastermind, but with five-letter words. In fact, Mastermind is actually a version of a classic pen-and-paper game; Quina is simply another variation on that same original game.

The object of Quina is to guess a secret five-letter word. After each guess, you get a clue that tells you how close your guess is to the code word. You use that clue to refine your next guess, and so on, but you only get ten total guesses; run out and you lose.

Example Quina gameplay

The name “Quina” came about because it means “five at a time” in Latin (or so Google told me, anyway). The traditional game is usually played with four-letter words, or sometimes four digits (or in the case of Mastermind, four colors); Quina uses five-letter words with no repeated letters, so it felt fitting that the game should have a name that plays by its own rules. (I have no idea how the original Latin word was pronounced, but I say it “QUINN-ah,” which is probably wrong, but hey, it’s my game, right?)

I spent my evenings and weekends over the course of about four months building the app. I’d like to spend this article talking about the tech behind the game, the decisions involved, and lessons learned in case this is a road you’re interested in traveling down yourself.

Choosing Nuxt

I’m a huge fan of Vue, and wanted to use this project as a way to expand my knowledge of its ecosystem. I considered using another framework (I’ve also built projects in Svelte and React), but I felt Nuxt hit the sweet spot of familiarity, ease of use, and maturity. (By the way, if you didn’t know or hadn’t guessed: Nuxt could be fairly described as the Vue equivalent of Next.js.)

I hadn’t gone too deep with Nuxt previously; just a couple of very small apps. But I knew Nuxt can compile to a static app, which is just what I wanted — no (Node) servers to worry about. I also knew Nuxt could handle routing as easily as dropping Vue components into a /pages folder, which was very appealing.

Plus, though Vuex (the official state management in Vue) isn’t terribly complex on its own, I appreciated the way that Nuxt adds just a little bit of sugar to make it even simpler. (Nuxt makes things easy in a variety of ways, by the way, such as not requiring you to explicitly import your components before you can use them; you can just put them in the markup and Nuxt will figure it out and auto-import as needed.)

Finally, I knew ahead of time I was building a Progressive Web App (PWA), so the fact that there’s already a Nuxt PWA module to help build out all the features involved (such as a service worker for offline capability) already packaged up and ready to go was a big draw. In fact, there’s an impressive array of Nuxt modules available for any unseen hurdles. That made Nuxt the easiest, most obvious choice, and one I never regretted.

I ended up using more of the modules as I went, including the stellar Nuxt Content module, which allows you to write page content in Markdown, or even a mixture of Markdown and Vue components. I used that feature for the “FAQs” page and the “How to Play” page as well (since writing in Markdown is so much nicer than hard-coding HTML pages).

Achieving native app feel with the web

Quina would eventually find a home on the Google Play Store, but regardless of how or where it was played, I wanted it to feel like a full-fledged app from the get-go.

To start, that meant an optional dark mode, and a setting to reduce motion for optimal usability, like many native apps have (and in the case of reduced motion, like anything with animations should have).

Under the hood, both of the settings are ultimately booleans in the app’s Vuex data store. When true, the setting renders a specific class in the app’s default layout. Nuxt layouts are Vue templates that “wrap” all of your content, and render on all (or many) pages of your app (commonly used for things like shared headers and footers, but also useful for global settings):

<!-- layouts/default.vue --> <template>   <div     :class="[       {         'dark-mode': darkMode,         'reduce-motion': reduceMotion,       },       'dots',     ]"   >     <Nuxt />   </div> </template>  <script> import { mapGetters } from 'vuex'  export default {   computed: {     ...mapGetters(['darkMode', 'reduceMotion']),   },   // Other layout component code here } </script>

Speaking of settings: though the web app is split into several different pages — menu, settings, about, play, etc. — the shared global Vuex data store helps to keep things in sync and feeling seamless between areas of the app (since the user will adjust their settings on one page, and see them apply to the game on another).

Every setting in the app is also synced to both localStorage and the Vuex store, which allows saving and loading values between sessions, on top of keeping track of settings as the user navigates between pages.

And speaking of navigation: moving between pages is another area where I felt there was a lot of opportunity to make Quina feel like a native app, by adding full-page transitions.

Nuxt page transitions in action

Vue transitions are fairly straightforward in general—you just write specifically-named CSS classes for your “to” and “from” transition states—but Nuxt goes a step further and allows you to set full page transitions with only a single line in a page’s Vue file:

<!-- A page component, e.g., pages/Options.vue --> <script> export default {   transition: 'page-slide'   // ... The rest of the component properties } </script>

That transition property is powerful; it lets Nuxt know we want the page-slide transition applied to this page whenever we navigate to or away from it. From there, all we need to do is define the classes that handle the animation, as you would with any Vue transition. Here’s my page-slide SCSS:

/* assets/css/_animations.scss */  .page-slide {   &-enter-active {     transition: all 0.35s cubic-bezier(0, 0.25, 0, 0.75);   }    &-leave-active {     transition: all 0.35s cubic-bezier(0.75, 0, 1, 0.75);   }    &-enter,   &-leave-to {     opacity: 0;     transform: translateY(1rem);      .reduce-motion & {       transform: none !important;     }   }    &-leave-to {     transform: translateY(-1rem);   } }

Notice the .reduce-motion class; that’s what we talked about in the layout file just above. It prevents visual movement when the user has indicated they prefer reduced motion (either via media query or manual setting), by disabling any transform properties (which seemed to warrant usage of the divisive !important flag). The opacity is still allowed to fade in and out, however, since this isn’t really movement.

Comparing default motion (left) with reduced motion (right)

Side note on transitions and handling 404s: The transitions and routing are, of course, handled by JavaScript under the hood (Vue Router, to be exact), but I ran into a frustrating issue where scripts would stop running on idle pages (for example, if the user left the app or tab open in the background for a while). When coming back to those idle pages and clicking a link, Vue Router would have stopped running, and so the link would be treated as relative and 404.

Example: the /faq page goes idle; the user comes back to it and clicks the link to visit the /options page. The app would attempt to go to /faq/options, which of course doesn’t exist.

My solution to this was a custom error.vue page (this is a Nuxt page that automatically handles all errors), where I’d run validation on the incoming path and redirect to the end of the path.

// layouts/error.vue mounted() {   const lastPage = '/' + this.$ route.fullPath.split('/').pop()   // Don't create a redirect loop   if (lastPage !== this.$ route.fullPath) {     this.$ router.push({       path: lastPage,     })   } }

This worked for my use case because a) I don’t have any nested routes; and b) at the end of it, if the path isn’t valid, it still hits a 404.

Vibration and sound

Transitions are nice, but I also knew Quina wouldn’t feel like a native app — especially on a smartphone — without both vibration and sound.

Vibration is relatively easy to achieve in browsers these days, thanks to the Navigator API. Most modern browsers simply allow you to call window.navigator.vibrate() to give the user a little buzz or series of buzzes — or, using a very short duration, a tiny bit of tactile feedback, like when you tap a key on a smartphone keyboard.

Obviously, you want to use vibration sparingly, for a few reasons. First, because too much can easily become a bad user experience; and second, because not all devices/browsers support it, so you need to be very careful about how and where you attempt to call the vibrate() function, lest you cause an error that shuts down the currently running script.

Personally, my solution was to set a Vuex getter to verify that the user is allowing vibration (it can be disabled from the settings page); that the current context is the client, not the server; and finally, that the function exists in the current browser. (ES2020 optional chaining would have worked here as well for that last part.)

// store/getters.js vibration(state) {   if (     process.client &&     state.options.vibration &&     typeof window.navigator.vibrate !== 'undefined'   ) {     return true   }   return false },

Side note: Checking for process.client is important in Nuxt — and many other frameworks with code that may run on Node — since window won’t always exist. This is true even if you’re using Nuxt in static mode, since the components are validated in Node during build time. process.client (and its opposite, process.server ) are Nuxt niceties that just validate the code’s current environment at runtime, so they’re perfect for isolating browser-only code.

Sound is another key part of the app’s user experience. Rather than make my own effects (which would’ve undoubtedly added dozens more hours to the project), I mixed samples from a few artists who know better what they’re doing in that realm, and who offered some free game sounds online. (See the app’s FAQs for full info.)

Users can set the volume they prefer, or shut the sound off entirely. This, and the vibration, are also set in localStorage on the user’s browser as well as synced to the Vuex store. This approach allows us to set a “permanent” setting saved in the browser, but without the need to retrieve it from the browser every time it’s referenced. (Sounds, for example, check the current volume level each time one is played, and the latency of waiting on a localStorage call every time that happens could be enough to kill the experience.)

An aside on sound

It turns out that for whatever reason, Safari is extremely laggy when it comes to sound. All the clicks, boops and dings would take a noticeable amount of time after the event that triggered them to actually play in Safari, especially on iOS. That was a deal-breaker, and a rabbit hole I spent a good amount of hours despairingly tunneling down.

Fortunately, I found a library called Howler.js that solves cross-platform sound issues quite easily (and that also has a fun little logo). Simply installing Howler as a dependency and running all of the app’s sounds through it — basically one or two lines of code — was enough to solve the issue.

The Howler.js logo, featuring a cute little howler monkey wearing headphones

If you’re building a JavaScript app with synchronous sound, I’d highly recommend using Howler, as I have no idea what Safari’s issue was or how Howler solves it. Nothing I tried worked, so I’m happy just having the issue resolved easily with very little overhead or code modification.

Gameplay, history, and awards

Quina can be a difficult game, especially at first, so there are a couple of ways to adjust the difficulty of the game to suit your personal preference:

  1. You can choose what kind of words you want to get as code words: Basic (common English words), Tricky (words that are either more obscure or harder to spell), or Random (a weighted mix of the two).
  2. You can choose whether to receive a hint at the start of each game, and if so, how much that hint reveals.
Quina offers several different ways to play, to accommodate players of all skill levels

These settings allow players of various skill, age, and/or English proficiency to play the game on their own level. (A Basic word set with strong hints would be the easiest; Tricky or Random with no hints would be the hardest.)

“Soft hints” reveal one letter in the code word (but not its position)

While simply playing a series of one-off games with adjustable difficulty might be enjoyable enough, that would feel more like a standard web app or demo than a real, full-fledged game. So, in keeping with the pursuit of that native app feel, Quina tracks your game history, shows your play statistics in a number of different ways, and offers several “awards” for various achievements.

Quina tracks the results of all games played, your longest win streaks, and many other stats

Under the hood, each game is saved as an object that looks something like this:

{   guessesUsed: 3,   difficulty: 'tricky',   win: true,   hint: 'none', }

The app catalogues your games played (again, via Vuex state synced to localStorage) in the form of a gameHistory array of game objects, which the app then uses to display your stats — such as your win/loss ratio, how many games you’ve played, and your average guesses — as well as to show your progress towards the game’s “awards.”

This is all done easily enough with various Vuex getters, each of which utilizes JavaScript array methods, like .filter() and .reduce(), on the gameHistory array. For example, this is the getter that shows how many games the user has won while playing on the “tricky” setting:

// store/getters.js trickyGamesWon(state) {   return state.gameHistory.filter(     (game) => game.win && game.difficulty === 'tricky'   ).length },

There are many other getters of varying complexity. (The one to determine the user’s longest win streak was particularly gnarly.)

Adding awards was a matter of creating an array of award objects, each tied to a specific Vuex getter, and each with a requirement.threshold property indicating when that award was unlocked (i.e., when the value returned by the getter was high enough). Here’s a sample:

// assets/js/awards.js export default [   {     title: 'Onset',     requirement: {       getter: 'totalGamesPlayed',       threshold: 1,       text: 'Play your first game of Quina',     }   },   {     title: 'Sharp',     requirement: {       getter: 'trickyGamesWon',       threshold: 10,       text: 'Win ten total games on Tricky',     },   }, ]

From there, it’s a pretty straightforward matter of looping over the achievements in a Vue template file to get the final output, using its requirement.text property (though there’s a good deal of math and animation added to fill the gauges to show the user’s progress towards achieving the award):

Awards are unlocked and displayed if a player’s progress is above the threshold; otherwise, the game displays the progress bar with an indication of how much is left until the award is unlocked.

There are 25 awards in all (that’s 5 × 5, in keeping with the theme) for various achievements like winning a certain number of games, trying out all the game modes, or even winning a game within your first three guesses. (That one is called “Lucky” — as an added little Easter egg, the name of each award is also a potential code word, i.e., five letters with no repeats.)

Unlocking awards doesn’t do anything except give you bragging rights, but some of them are pretty difficult to achieve. (It took me a few weeks after releasing to get them all!)

Pros and cons of this approach

There’s a lot to love about the “build once, deploy everywhere” strategy, but it also comes with some drawbacks:

Pros

  • You only need to deploy your store app once. After that, all updates can just be website deploys. (This is much quicker than waiting for an app store release.)
  • Build once. This is sorta true, but turned out to be not quite as straightforward as I thought due to Google’s payments policy (more on that later).
  • Everything is a browser. Your app is always running in the environment you’re used to, whether the user realizes it or not.

Cons

  • Event handlers can get really tricky. Since your code is running on all platforms simultaneously, you have to anticipate any and all types of user input at once. Some elements in the app can be tapped, clicked, long-pressed, and also respond differently to various keyboard keys; it can be tricky to handle all of those at once without any of the handlers stepping on each other’s toes.
  • You may have to split experiences. This will depend on what your app is doing, but there were some things I needed to show only for users on the Android app and others that were only for web. (I go into a little more detail on how I solved this in another section below.)
  • Everything is a browser. You’re not worried about what version of Android your users are on, but you are worried about what their default browser is (because the app will use their default browser behind the scenes). Typically on Android this will mean Chrome, but you do have to account for every possibility.

Logistics: turning a web app into a native app

There’s a lot of technology out there that makes the “build for the web, release everywhere” promise — React Native, Cordova, Ionic, Meteor, and NativeScript, just to name a few.

Generally, these boil down to two categories:

  1. You write your code the way a framework wants you to (not exactly the way you normally would), and the framework transforms it into a legitimate native app;
  2. You write your code the usual way, and the tech just wraps a native “shell” around your web tech and essentially disguises it as a native app.

The first approach may seem like the more desirable of the two (since at the end of it all you theoretically end up with a “real” native app), but I also found it comes with the biggest hurdles. Every platform or product requires you to learn its way of doing things, and that way is bound to be a whole ecosystem and framework unto itself. The promise of “just write what you know” is a pretty strong overstatement in my experience. I’d guess in a year or two a lot of those problems will be solved, but right now, you still feel a sizable gap between writing web code and shipping a native app.

On the other hand, the second approach is viable because of a thing called “TWA,” which is what makes it possible to make a website into an app in the first place.

What is a TWA app?

TWA stands for Trusted Web Activity — and since that answer is not likely to be helpful at all, let’s break that down a bit more, shall we?

A TWA app basically turns a website (or web app, if you want to split hairs) into a native app, with the help of a little UI trickery.

You could think of a TWA app as a browser in disguise. It’s an Android app without any internals, except for a web browser. The TWA app is pointed to a specific web URL, and whenever the app is booted, rather than doing normal native app stuff, it just loads that website instead  —  full-screen, with no browser controls, effectively making the website look and behave as though it were a full-fledged native app.

TWA requirements

It’s easy to see the appeal of wrapping up a website in a native app. However, not just any old site or URL qualifies; in order to launch your web site/app as a TWA native app, you’ll need to check the following boxes:

  • Your site/app must be a PWA. Google offers a validation check as part of Lighthouse, or you can check with Bubblewrap (more on that in a bit).
  • You must generate the app bundle/APK yourself; it’s not quite as easy as just submitting the URL of your progressive web app and having all the work done for you. (Don’t worry; we’ll cover a way to do this even if you know nothing about native app development.)
  • You must have a matching secure key, both in the Android app and uploaded to your web app at a specific URL.

That last point is where the “trusted” part comes in; a TWA app will check its own key, then verify that the key on your web app matches it, to ensure it’s loading the right site (presumably, to prevent malicious hijacking of app URLs). If the key doesn’t match or isn’t found, the app will still work, but the TWA functionality will be gone; it will just load the web site in a plain browser, chrome and all. So the key is extremely important to the experience of the app. (You could say it’s a key part. Sorry not sorry.)

Advantages and drawbacks of building a TWA app

The main advantage of a TWA app is that it doesn’t require you to change your code at all — no framework or platform to learn; you’re just building a website/web app like normal, and once you’ve got that done, you’ve basically got the app code done, too.

The main drawback, however, is that (despite helping to usher in the modern age of the web and JavaScript), Apple is not in favor of TWA apps; you can’t list them in the Apple App store. Only Google Play.

This may sound like a deal-breaker, but bear a few things in mind:

  • Remember, to list your app in the first place, it needs to be a PWA — which means it’s installable by default. Users on any platform can still add it to their device’s home screen from the browser. It doesn’t need to be in the Apple App Store to be installed on Apple devices (though it certainly misses out on the discoverability). So you could still build a marketing landing page into your app and prompt users to install it from there.
  • There’s also nothing to prevent you from developing a native iOS app using a completely different strategy. Even if you wanted both iOS and Android apps, as long as a web app is also part of the plan, having a TWA effectively cuts out half of that work.
  • Finally, while iOS has about a 50% market share in predominantly English-speaking countries and Japan, Android has well over 90% of the rest of the world. So, depending on your audience, missing out on the App Store may not be as impactful as you might think.

How to generate the Android App APK

At this point you might be saying, this TWA business sounds all well and good, but how do I actually take my site/app and shove it into an Android app?

The answer comes in the form of a lovely little CLI tool called Bubblewrap.

You can think of Bubblewrap as a tool that takes some input and options from you, and generates an Android app (specifically, an APK, one of the file formats allowed by the Google Play Store) out of the input.

Installing Bubblewrap is a little tricky, and while using it is not quite plug-and-play, it’s definitely far more within reach for an average front-end dev than any other comparable options that I found. The README file on Bubblewrap’s NPM page goes into the details, but as a brief overview:

Install Bubblewrap by running npm i -g @bubblewrap/cli (I’m assuming here you’re familiar with NPM and installing packages from it via the command line). That will allow you to use Bubblewrap anywhere.

Once it’s installed, you’ll run:

bubblewrap init --manifest https://your-webapp-domain/manifest.json

Note: the manifest.json file is required of all PWAs, and Bubblewrap needs the URL to that file, not just your app. Also be warned: depending on how your manifest file is generated, its name may be unique to each build. (Nuxt’s PWA module appends a unique UUID to the file name, for example.)

Also note that by default, Bubblewrap will validate that your web app is a valid PWA as part of this process. For some reason, when I was going through this process, the check kept coming back negative, despite Lighthouse confirming that it was in fact a fully functional progressive web app. Fortunately, Bubblewrap allows you to skip this check with the --skipPwaValidation flag.

If this is your first time using Bubblewrap, it will then ask if you want it to install the Java Development Kit (JDK) and Android Software Development Kit (SDK) for you. These two are the behind-the-scenes utilities required to generate an Android app. If you’re not sure, hit “Y” for yes.

Note: Bubblewrap expects these two development kits to exist in very specific locations, and won’t work properly if they’re not there. You can run bubblewrap doctor to verify, or see the full Bubblewrap CLI README.

After everything’s installed — assuming it finds your manifest.json file at the provided URL — Bubblewrap will ask some questions about your app.

Many of the questions are either preference (like your app’s main color) or just confirming basic details (like the domain and entry point for the app), and most will be pre-filled from your site’s manifest file.

Other questions that may already be pre-filled by your manifest include where to find your app’s various icons (to use as the home screen icon, status bar icon, etc.), what color the splash screen should be while the app is opening, and the app’s screen orientation, in case you want to force portrait or landscape. Bubblewrap will also ask if you want to request permission for your user’s geolocation, and whether you’re opting into Play Billing.

However, there are a few important questions that may be a little confusing, so let’s cover those here:

  • Application ID: This appears to be a Java convention, but each app needs a unique ID string that’s generally 2–3 dot-separated sections (e.g., collinsworth.quina.app). It doesn’t actually matter what this is; it’s not functional, it’s just convention. The only important thing is that you remember it, and that it’s unique. But do note that this will become part of your app’s unique Google Play Store URL. (For this reason, you cannot upload a new bundle with a previously used App ID, so make sure you’re happy with your ID.)
  • Starting version: This doesn’t matter at the moment, but the Play Store will require you to increment the version as you upload new bundles, and you cannot upload the same version twice. So I’d recommend starting at 0 or 1.
  • Display mode: There are actually a few ways that TWA apps can display your site. Here, you most likely want to choose either standalone (full-screen, but with the native status bar at the top), or fullscreen (no status bar). I personally chose the default standalone option, as I didn’t see any reason to hide the user’s status bar in-app, but you might choose differently depending on what your app does.

The signing key

The final piece of the puzzle is the signing key. This is the most important part. This key is what connects your progressive web app to this Android app. If the key the app is expecting doesn’t match what’s found in your PWA, again: your app will still work, but it will not look like a native app when the user opens it; it’ll just be a normal browser window.

There are two approaches here that are a little too complex to go into in detail, but I’ll try to give some pointers:

  1. Generate your own keystore. You can have Bubblewrap do this, or use a CLI tool called keytool (appropriately enough), but either way: be very careful. You need to explicitly track the exact name and passwords for your keystores, and since you’re creating both on the command line, you need to be extremely careful of special characters that could mess up the whole process. (Special characters may be interpreted differently on the command line, even when input as part of a password prompt.)
  2. Allow Google to handle your keys. This honestly isn’t dramatically simpler in my experience, but it saves some of the trouble of wrangling your own signing keys by allowing you to go into the Google Play Developer console, and download a pre-generated key for your app.

Whichever option you choose, there’s in-depth documentation on app signing here (written for Android apps, but most of it is still relevant).

The part where you get the key onto your personal site is covered in this guide to verifying Android app links. To crudely summarize: Google will look for a /.well-known/assetlinks.json file at that exact path on your site. The file needs to contain your unique key hash as well as a few other details:

[{   "relation": ["delegate_permission/common.handle_all_urls"],   "target" : { "namespace": "android_app", "package_name": "your.app.id",                "sha256_cert_fingerprints": ["your:unique:hash:here"] } }]

What you should know about listing an app

Before you get started, there are also some hurdles to be aware of on the app store side of things:

  • First and foremost, you need to sign up before you can publish to the Google Play Store. This eligibility costs a one-time $ 25 USD fee.
  • Once approved, know that listing an app is neither quick nor easy. It’s more tedious than difficult or technical, but Google reviews every single app and update on the store, and requires you to fill out a lot of forms and info about both yourself and your app before you can even start the review process — which itself can take many days, even if your app isn’t even public yet. (Friendly heads-up: there’s been a “we’re experiencing longer than usual review times” warning banner in the Play console dashboard for at least six months now.)
    • Among the more tedious parts: you must upload several images of your app in action before your review can even begin. These will eventually become the images shown in the store listing — and bear in mind that changing them will also kick off a new review, so come to the table prepared if you want to minimize turnaround time.
    • You also need to provide links to your app’s terms of service and privacy policy (which is the only reason my app even has them, since they’re all but pointless).
    • There are lots of things you can’t undo. For example, you can never change a free app to paid, even if it hasn’t publicly launched yet and/or has zero downloads. You also have to be strict on versioning and naming with what you upload, because Google doesn’t let you overwrite or delete your apps or uploaded bundles, and doesn’t always let you revert other settings in the dashboard, either. If you have a “just jump in and work out the kinks later” approach (like me), you may find yourself starting over from scratch at least once or twice.
  • With a few exceptions, Google has extremely restrictive policies about collecting payments in an app. When I was building, it was charging a 30% fee on all transactions (they’ve since conditionally lowered that to 15% — better, but still five times more than most other payment providers would charge). Google also forces developers (with a few exceptions) to use its own native payment platform; no opting for Square, Stripe, PayPal, etc. in-app.
    • Fun fact: this policy had been announced but wasn’t in effect yet while I was trying to release Quina, and it still got flagged by the reviewer for being in violation. So they definitely take this policy very seriously.

Monetization, unlockables, and getting around Google

While my goal with Quina was mostly personal — challenge myself, prove I could, and learn more about the Vue ecosystem in a complex real-world app — I had also hoped as a secondary goal that my work might be able to make a little money on the side for me and my family.

Not a lot. I never had illusions of building the next Candy Crush (nor the ethical void required to engineer an addiction-fueled micro-transaction machine). But since I had poured hundreds of hours of my time and energy into the game, I had hoped that maybe I could make something in return, even if it was just a little beer money.

Initially, I didn’t love the idea of trying to sell the app or lock its content, so I decided to add a simple “would you care to support Quina if you like it?” prompt after every so many games, and make some of the content unlockable specifically for supporters. (Word sets are limited in size bu default, and some game settings are initially locked as well.) The prompt to support Quina can be permanently dismissed (I’m not a monster), and any donation unlocks everything; no tiered access or benefits.

This was all fairly straightforward to implement thanks to Stripe, even without a server; it’s all completely client-side. I just import a bit of JavaScript on the /support page, using Nuxt’s handy head function (which adds items to the <head> element specifically on the given page):

// pages/support.vue head() {   return {     script: [       {         hid: 'stripe',         src: 'https://js.stripe.com/v3',         defer: true,         callback: () => {           // Adds all Stripe methods like redirectToCheckout to page component           this.stripe = Stripe('your_stripe_id')         },       },     ],   } },

With that bit in place (along with a sprinkle of templating and logic), users can choose their donation amount — set up as products on the Stripe side — and be redirected to Stripe to complete payment, then returned when finished. For each tier, the return redirect URL is slightly different via query parameters. Vue Router parses the URL to adjust the user’s stored donation history, and unlock features accordingly.

You might wonder why I’m revealing all of this, since it exposes the system as fairly easy to reverse-engineer. The answer is: I don’t care. In fact, I added a free tier myself, so you don’t even have to go to the trouble. I decided that if somebody really wanted the unlockables but couldn’t or wouldn’t pay for whatever reason, that’s fine. Maybe they live in a situation where $ 3 is a lot of money. Maybe they gave on one device already. Maybe they’ll do something else nice instead. But honestly, even if their intentions aren’t good: so what?

I appreciate support, but this isn’t my living, and I’m not trying to build a dopamine tollbooth. Besides, I’m not personally comfortable with the ethical implications of using a stack of totally open-source and/or free software (not to mention the accompanying mountain of documentation, blog posts, and Stack Overflow answers written about all of it) to build a closed garden for personal profit.

So, if you like Quina and can support it: sincerely, thank you. That means a ton to me. I love to see my work being enjoyed. But if not: that’s cool. If you want the “free” option, it’s there for you.

Anyway, this whole plan hit a snag when I learned about Google Play’s new monetization policy, effective this year. You can read it yourself, but to summarize: if you make money through a Google Play app and you’re not a nonprofit, you gotta go through Google Pay and pay a hefty fee — you are not allowed to use any other payment provider.

This meant I couldn’t even list the app; it would be blocked just for having a “support” page with payments that don’t go through Google. (I suppose I probably could have gotten around this by registering a nonprofit, but that seemed like the wrong way to go about it, on a number of levels.)

My eventual solution was to charge for the app itself on Google Play, by listing it for $ 2.99 (rather than my previously planned price of “free”), and simply altering the app experience for Android users accordingly.

Customizing the app experience for Google Play

Fortunately enough, Android apps send a custom header with the app’s unique ID when requesting a website. Using this header, it was easy enough to differentiate the app’s experience on the web and in the actual Android app.

For each request, the app checks for the Android ID; if present, the app sets a Vuex state boolean called isAndroid to true. This state cascades throughout the app, working to trigger various conditionals to do things like hide and show various FAQ questions, and (most importantly) to hide the support page in the nav menu. It also unlocks all content by default (since the user’s already “donated” on Android, by purchasing). I even went so far as to make simple <WebOnly> and <AndroidOnly> Vue wrapper components to wrap content only meant for one of the two. (Obviously, users on Android who can’t visit the support page shouldn’t see FAQs on the topic, as an example.)

<!-- /src/components/AndroidOnly.vue --> <template>   <div v-if="isAndroid">     <slot />   </div> </template>  <script> export default {   computed: {     isAndroid() {       return this.$ store.state.isAndroid     },   }, } </script>

Accounting for accounts

For a time while building Quina, I had Firebase set up for logins and storing user data. I really liked the idea of allowing users to play on all their devices and track their stats everywhere, rather than have a separate history on each device/browser.

In the end, however, I scrapped that idea, for a few reasons. One was complexity; it’s not easy maintaining a secure accounts system and database, even with a nice system like Firebase, and that kind of overhead isn’t something I took lightly. But mainly: the decision boiled down to security and simplicity.

At the end of the day, I didn’t want to be responsible for users’ data. Their privacy and security is guaranteed by using localStorage, at the small cost of portability. I hope players don’t mind the possibility of losing their stats from time to time if it means they have no login or data to worry about. (And hey, it also gives them a chance to earn those awards all over again.)

Plus, it just feels nice. I get to honestly say there’s no way my app can possibly compromise your security or data because it knows literally nothing about you. And also, I don’t need to worry about compliance or cookie warnings or anything like that, either.

Wrapping up

Building Quina was my most ambitious project to date, and I had as much fun designing and engineering it as I have seeing players enjoy it.

I hope this journey has been helpful for you! While getting a web app listed in the Google Play Store has a lot of steps and potential pitfalls, it’s definitely within reach for a front-end developer. I hope you take this story as inspiration, and if you do, I’m excited to see what you build with your newfound knowledge.


The post What I Learned Building a Word Game App With Nuxt on Google Play appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,
[Top]

Building a Settings Component

This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!

I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):

.el {   display: grid;   grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch)); }

There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.

Direct Link to ArticlePermalink


The post Building a Settings Component appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]

Building a Settings Component

This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!

I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):

.el {   display: grid;   grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch)); }

There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.

Direct Link to ArticlePermalink


The post Building a Settings Component appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]