How to Build Your First Custom Svelte Transition

The Svelte transition API provides a first-class way to animate your components when they enter or leave the document, including custom Svelte transitions. By default, the transition directive uses CSS animations, which generally offer better performance and allow the browser’s main thread to remain unblocked. The API is as simple as this: <element transition:transitionFunction />. You can also specify in or out directives which are uni-directional transitions, only running when the element is mounted or unmounted.

An animated example of a custom Svelte transition showing a to do list. An item is typed and animated into the list of items when entered. Clicking a done button animates the item out of view.
Example of a working Svelte transition (jump to demo)

Svelte offers a runtime svelte/transition package that ships with seven prepackaged Svelte transition functions, all of which can be dropped in and tweaked to your heart’s desire. Pairing this with the svelte/easing package, allows for a wide swath of interactions, without writing any of the transition code yourself. Play around with different transitions and easing functions to get a feel for what is possible.

Looking for instructions on how to get started with Svelte? We have a solid overview for you to check out.

The Svelte Custom Transition API

If you need even more control than what the Svelte Transition API offers out of the box, Svelte permits you to specify your own custom transition function, as long as you adhere to a few conventions. From the docs, here’s what the custom transition API looks like:

transition = (node: HTMLElement, params: any) => 
  delay?: number,
  duration?: number,
  easing?: (t: number) => number,
  css?: (t: number, u: number) => string,
  tick?: (t: number, u: number) => void
 

Let’s break it down. A transition function takes a reference to the DOM node where the transition directive is used and returns an object with some parameters that control the animation and, most importantly, a css or tick function.

The css function’s job is to return a string of CSS that describes the animation, typically including some kind of transform or opacity change. Alternatively, you can opt to return a tick function, which lets you control every aspect of the animation with the power JavaScript, but pays a performance penalty since this type of transition does not use CSS animations.

Both the css and tick functions take two parameters called (t, u) by convention. t is a decimal number that travels from 0.00 to 1.00 while the element is entering the DOM and from 1.00 back to 0.00 when the element is leaving. The u parameter is the inverse of t or 1 - t at any given moment. For example, if you return a string of transform: scale($t), your element would smoothly animate from 0 to 1 on enter, and vice versa on exit.

These concepts may seem a bit abstract, so let’s solidify them by building our own custom Svelte transition!

Building your first custom Svelte transition

First, let’s set up some boilerplate that allows us to toggle an element’s existence in the DOM using a Svelte #if block. Remember, Svelte transitions only run when an element is actually leaving or entering the DOM.

<script>
  let showing = true
</script>

<label for="showing">
  Showing
</label>
<input id="showing" type="checkbox" bind:checked=showing />

#if showing
  <h1>Hello custom transition!</h1>
/if

You should be able to toggle the checkbox and see our element starkly appear and disappear in place.

Next, let’s set up our custom Svelte transition function and get it wired up to our element.

<script>
  let showing = true
  // Custom transition function
  function whoosh(node) 
    console.log(node)
  
</script>

<label for="showing">
  Showing
</label>
<input id="showing" type="checkbox" bind:checked=showing />

#if showing
  <h1 transition:whoosh>Hello custom transition!</h1>
/if

Now, if you toggle the checkbox, you will see the <h1> element logged to the console. This proves we have the custom transition connected properly! We won’t actually use the DOM node in our example, but it’s often useful to have access to the element to reference its current styles or dimensions.

For our element to do any animation at all, we need to return an object that contains a css (or tick) function. Let’s have our css function return a single line of CSS that scales our element. We’ll also return a duration property that controls how long the animation takes.

<script>
  function swoop() 
    return 
      duration: 1000,
      css: () => `transform: scale(.5)`
    
  
  let showing = true
</script>

<!-- markup -->

We’ve got something moving! You will notice our element jumps straight to .5 scale when toggling the checkbox. This is something, but it would feel much better if it smoothly transitioned. That’s where the (t, u) parameters come in.

<script>
  function swoop() 
    return 
      duration: 1000,
      css: (t) => `transform: scale($t)`
    
  
  let showing = true
</script>

<!-- markup -->

Now we are talking! Remember, t rolls smoothly from 0.00 to 1.00 when an element enters, and vice versa when it leaves. This allows us to achieve the smooth effect we want. In fact, what we just wrote is essentially the built-in scale transition from the svelte/transition package.

Let’s get a little bit fancier. To live up to our custom Svelte transition’s namesake, swoop, let’s add a translateX to our transform, so that our element zooms in and out from the side.

I want to challenge you to attempt the implementation first before we continue. Trust me, it will be fun! Assume that we want to translate to 100% when the element is leaving and back to 0% when it enters.

[waiting…]

How did it go? Want to compare answers?

Here’s what I got:

css: (t, u) => `transform: scale($t) translateX($u * 100%);`

It’s okay if you have something different! Let me break down what I did.

The key thing here is the usage of the second parameter in the css function. If we think about our animation while the element is entering the screen, we want to end up at scale(1) translateX(0%), so we can’t use unaltered t for both the scale and the transform. This is the convenience behind the u parameter — it is the inverse of t at any given moment, so we know it will be 0 when t is 1! I then multiplied u by 100 to get the percentage value and tacked on the % sign at the end.

Learning the interplay between t and u is an important piece of the custom transition puzzle in Svelte. These two parameters enable a world of dynamism for your animations; they can be divided, multiplied, twisted, or contorted into whatever needs you have.

Let’s slap my favorite svelte/easing function on our transition and call it a day:

<script>
  import  elasticOut  from 'svelte/easing'
  function swoop() 
    return 
      duration: 1000,
      easing: elasticOut,
      css: (t, u) => `transform: scale($t) translateX($u * 100%)`
    
  
  let showing = true
</script>

<label for="showing">
  Showing
</label>
<input id="showing" type="checkbox" bind:checked=showing />

#if showing
  <h1 transition:swoop>Hello custom transition!</h1>
/if

Wrapping up

Congratulations! You can now build a custom Svelte transition function. We have only scratched the surface of what is possible but I hope you feel equipped with the tools to explore even further. I would highly recommend reading the docs and going through the official tutorial to gain even more familiarity.


How to Build Your First Custom Svelte Transition originally published on CSS-Tricks. You should get the newsletter and become a supporter.


Source link

Build a video call app with Svelte in an afternoon

Daily’s JavaScript library, daily-js, is compatible with any frontend framework, which means choosing your stack is really up to you. Today we’ll be looking at one of our favourite frameworks at Daily…

drumroll

Svelte! 🎉

In today’s tutorial, we’ll rebuild our Daily Prebuilt demo, already built in React and Vue, but this time with Svelte! ✨

Specifically, we’ll cover how to:

  • Embed Daily Prebuilt in a Svelte app
  • Handle updating your components based on whether you’ve joined a Daily call or not
  • Manage your Daily Prebuilt call with a custom control panel

If you want to test the completed version of the demo first, check out the deployed version.



What exactly is Svelte?

Svelte is an open-source frontend component framework that can be used as an alternative to other frontend frameworks, like React or Vue.

It differs from other frameworks based on how it updates your app’s UI. Instead of using a virtual DOM to handle app changes (like React uses), Svelte is instead a compiler that converts app components to JavaScript and inserts any UI changes directly into the DOM itself.

Or, as the Svelte docs put it:

“Svelte runs at build time, converting your components into highly efficient imperative code that surgically updates the DOM. As a result, you’re able to write ambitious applications with excellent performance characteristics.”



Who is this tutorial for?

Since this is a Svelte tutorial, the following content will be most helpful for anyone already familiar with Svelte. In case you’re not, we’ll do our best to explain what is unique to Svelte.

Thankfully, Svelte also has amazing documentation and interactive tutorials to learn the basics, and we highly recommend giving those a read!



Getting started

To get started locally, clone the Daily Prebuilt Svelte demo app and run the following commands in your terminal:

npm i
npm run dev

You will also need to create a Daily account and a new Daily room for testing the demo.

Note: The demo README includes additional instructions for creating new Daily rooms locally via the app.

Once you have the app running locally, navigate to http://localhost:5000 in your browser of choice, and you should see the home page.

home screen



App.svelte: Determining which view to show

Our app is going to have two possible views:

  1. Our home page, which includes a form to join a call
  2. The call UI, which includes the Daily Prebuilt embed and our custom call controls

2 possible views: call UI and home screen

We know we’ll need some logic to determine which view should show. To determine this, let’s take a look at our parent App component. App will handle deciding whether the Home or Call component is rendered.

// App.svelte
<script>
 import Call from "./screens/Call.svelte";
 import Header from "./components/Header.svelte";
 import Home from "./screens/Home.svelte";

 let currentScreen = "home"; //  || 'call'
 let url;
 let userName;

 const handleJoinCall = ( detail ) => 
   currentScreen = "call";
   // set component vars with form submission values
   url = detail.url;
   userName = detail.name;

   // save in local storage
   localStorage.setItem("svelte-prebuilt-url", url);
   localStorage.setItem("svelte-prebuilt-name", userName);
 ;

 const handleLeaveCall = () => 
   currentScreen = "home";
 ;
</script>

<svelte:head>
 <script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
</svelte:head>

<div class="wrapper">
 <Header />
 #if currentScreen === "home"
   <Home on:submit=handleJoinCall />
 :else
   <Call userName url on:left=handleLeaveCall />
 /if
</div>

Let’s step through this:

  • In the script tag, we start by importing the components we’ll be using (Call, Home, and Header)
  • Then, we declare variables that will be used in this component but are assigned later on.
  • Next, we define handleJoinCall, which we’ll describe in more detail below. In short, it sets our currentScreen variable to call.
  • We then define handleLeaveCall, which simply resets currentScreen to home.
  • Lastly, we import our daily-js script tag to make the daily-js library available to the rest of the app.

Now let’s specifically look at the markdown in App to see how we render our components:

// App.svelte
<div class="wrapper">
 <Header />
 #if currentScreen === "home"
   <Home on:submit=handleJoinCall />
 :else
   <Call userName url on:left=handleLeaveCall />
 /if
</div>

In Svelte, you can conditionally render components based on your JavaScript values using an if statement.

We know we always want to render our Header, but we only want to render the Call component if the user is trying to join a call, i.e. the currentScreen equals call.

This is where we can use Svelte’s if syntax:

  • #if currentScreen === "home" to render the Home screen
  • :else we can show the Call component.

And don’t forget to close your if block with /if



Listening for custom events

Another handy feature of Svelte is being able to dispatch custom events that can be forwarded to other components.

Since the Home component has a form to join calls, we want to call handleJoinCall (defined in App) when the form’s submit event is dispatched.

Similarly, when you’re in a call and go to leave the call, we want to listen for the custom left event we’ll define in Call.

We handle both situations by adding a custom listener on each component, like so:
<Call on:left=handleLeaveCall />

Notice how it says on:left? This will listen for any left events dispatched. To listen to other custom events you’ve defined, you can just update the event name, such as on:fancy-event-name.

One other thing to note is being able to pass variables to child components. In Svelte, we can pass the userName and url values as props to the Call component like so:
<Call userName url />

Note: Since the prop name and variable being passed use the same name, we can use the shorthand notation. That is to say, url is shorthand for url=url.

These properties can then be made available in Call like so:

// Call.svelte
export let url;
export let userName;



Honey, I’m Home.svelte

Home screen again

By default, App will render the Home component first.

Our Home component contains the main content for our app’s home screen. The code for it is a bit longer than App so we’ll look at the most important sections.

// Home.svelte
<script>
 import  createEventDispatcher, onMount  from "svelte";
 import api from "../api";

 const dispatch = createEventDispatcher();
 ...

At the top of our component, we import Svelte’s createEventDispatcher method and assign it to the variable dispatch. This method is what we’ll use for dispatching our custom events discussed above.

Now let’s jump to our HTML and focus on the form.

// Home.svelte
<div class="home-screen">
 <h2>Daily Prebuilt Svelte demo</h2>
 <p>Start demo with a new unique room or paste in your own room URL</p>
…
 <form on:submit=goToCall>
   <label for="name">Name</label>
   <input id="name" type="text" required bind:value=name />
   <label for="url">Daily URL</label>
   <input
     id="url"
     type="text"
     required
     bind:value=url
     placeholder="https://your-domain.daily.co/room-name"
   />
   <input type="submit" value="Join call" />
 </form>
</div>

Here, we have a native HTML <form> element with the submit handler using goToCall:

<form on:submit=goToCall>

Note: This is not a custom event since forms have a native submit event.

The form has two inputs for a username and Daily room URL. To make each of these input values available in our component’s JavaScript, we can bind each value to a variable declared at the top of the component.

For example, the username input value will be bound to the variable name, which is declared at the top of the file:

// Home.svelte
<script>
   let name;
</script>

...
<form on:submit=goToCall>
    ...
    <input id="name" type="text" required bind:value=name />
    ...
</form>

This means when the form’s submit event occurs and the goToCall method is called, we can use the input values via our bound variables (e.g name).

This also makes the input values available to forward to the App component via the dispatch method we defined before like so:

// Home.svelte
const goToCall = (e) => 
   e.preventDefault();

   dispatch("submit", 
     name,
     url,
   );
 ;

In goToCall, we first prevent the form from refreshing the page with e.preventDefault().

Then we use our dispatch method to forward the submit event to our App component. Both name and url (our variables bound to the inputs) are passed as options to make those values available to App, as well.

If you recall from App, the Home component has an event listener on it for submit, which calls the handleJoinCall method.

<Home on:submit=handleJoinCall />

When our dispatched submit event registers in App, it will call handleJoinCall.

// App.svelte
 const handleJoinCall = ( detail ) => 
   currentScreen = "call";
   // set component vars with form submission values
   url = detail.url;
   userName = detail.name;

   // save in local storage
   localStorage.setItem("svelte-prebuilt-url", url);
   localStorage.setItem("svelte-prebuilt-name", userName);

   error = null;
 ;

In handleJoinCall we update currentScreen to equal call. This will cause the Call component to show instead of Home. We then set our url and userName variables to the values passed from the form and save them in local storage, as well.

Now that all the Home form’s information is shared with App — who also shares it with Call — we can move on to setting up our call UI.



Call on line 1, please

So far we’ve set up our App component and our Home screen. Now let’s build our Daily call in Call.svelte.

Diagram of how the Call component is split up

Let’s start with the Call component’s HTML markdown this time.

// Call.svelte
<div
 class=meetingState === "joined-meeting"
   ? "call-screen"
   : "call-screen prejoin"
>
 <div id="container" />
 <Controls
   url
   meetingState
   stats
   on:toggle-camera=toggleCamera
   on:toggle-mic=toggleMic
   on:toggle-screen-share=toggleScreenShare
   on:fullscreen=goFullscreen
   on:toggle-local-video=toggleLocalVideo
   on:toggle-remote-video=toggleRemoteVideo
 />
</div>

We have a container div with two children:

  • Another div, which is where we’ll embed Daily Prebuilt
  • Our Controls component, which receives the room url, meetingState, and call stats as props. It also has a list of custom event listeners added for each of the control’s buttons.

Let’s focus on the Daily call to start.



Embedding Daily Prebuilt in your Svelte app

Svelte has a handy onMount lifecycle method that gets called when the component is first rendered.

// Call.svelte

import  onMount  from "svelte";

onMount(() => 
   // assume if the Call component is showing, we should join
   initializeDaily();
 );

We know the Call component mounts when the form is submitted, so we want to initialize the call as soon as Call renders. We can do this by calling initializeDaily on mount.

// Call.svelte

 const initializeDaily = async () => 
   
   // select container element to embed Daily iframe in
   const container = document.getElementById("container");
   // create Daily iframe
   callFrame = window.DailyIframe.createFrame(container, 
     iframeStyle: IFRAME_OPTIONS,
     showLeaveButton: true,
     url,
     userName,
   );

   callFrame.on("joining-meeting", updateMeetingState);
   callFrame.on("joined-meeting", updateMeetingState);
   callFrame.on("left-meeting", handleLeftMeeting);
   callFrame.on("error", updateMeetingState);

   // set up interval for retrieving current network stats
   interval = setInterval(() => getNetworkStats(), 5000);

   // let the local user join the call, which will cause
   // the call to be displayed in our app UI
   await callFrame.join();
 ;

Stepping through this initializeDaily function:

  1. We first select our div element that will be the Daily Prebuilt iframe’s container:

const container = document.getElementById("container");

  1. Next we create our Daily Prebuilt iframe with the createFrame method via daily-js, and pass the container div and some call options to it.

window.DailyIframe.createFrame(container, options)

Recall: window.DailyIframe exists because we imported the daily-js script in App.

  1. Next, we add some Daily event listeners so we can update our UI if the local user joins a call, leaves, or there’s a critical error. We’ll explain this a bit more below.

e.g. callFrame.on("joined-meeting", updateMeetingState);

  1. Then we set up an interval to get the call’s network stats every 5 seconds so we can display it in our Control panel. (We won’t go into more detail about this, but ask us if you need help!)

interval = setInterval(() => getNetworkStats(), 5000);

  1. And, finally, we use Daily’s join method to actually join the call. 🎉

await callFrame.join();

Transitioning from the home screen to video call



Adding our custom control panel

As Bruce Springsteen once said, “Honey, I want the heart, I want the soul, I want control right now,” so let’s do just that and add a little more control to our app.

daily-js provides instance methods to programmatically do anything you can already do via Daily Prebuilt’s UI. This gives a bit more flexibility to how you want to set up your own app’s UI.

For example, if you want to add a big “Mute” button to your UI, you can! Let’s take a look at how.



Adding a Mute button to toggle local audio

As mentioned, our Call component has a bunch of custom event listeners added to Controls. This means all the actual Daily logic can stay contained in our Call component. The Controls component is basically just UI to dispatch the custom events.

// Call.svelte

<Controls
   
   on:toggle-mic=toggleMic

In Controls, we have a button to mute the local user:

// Controls.svelte

<button on:click=handleToggleMicClick>
    <span>
        Toggle mic
        <img src="/images/mic.svg" alt="Microphone" />
    </span>
</button>

On click, this button calls handleToggleMicClick. That method will then dispatch our toggle-mic custom event:

const handleToggleMicClick = () => dispatch("toggle-mic");

Looping back to Call, the toggle-mic custom event that is forwarded calls toggleMic (on:toggle-mic=toggleMic), a method defined in Call.

The toggleMic method is what contains our daily-js interaction.

// Call.svelte 

const toggleMic = () => 
   if (!callFrame) 
     logError(noCallFrameError);
     return;
   
   const localVideo = callFrame.localAudio();
   callFrame.setLocalAudio(!localVideo);
 ;

If there is no Daily callFrame – defined in initializeDaily – we do nothing. This shouldn’t ever happen but, you know, bugs. 🐛

Next, we get our current local audio state (muted or unmuted) via the localAudio Daily method:

const localVideo = callFrame.localAudio();

Whatever the current value is, we want to make it the opposite. To do this, we can use the setLocalAudio instance method and set our local audio to the inverse boolean value.

callFrame.setLocalAudio(!localVideo);

Once that’s called, your unmuted audio will mute, or vice versa! 💪

We won’t cover all the buttons in the control panel since they all follow a similar pattern, but if you need help, please let us know! 🤗

Muting yourself with a custom Mute button



Appetite for (call) destruction

To understand how the Daily event listeners work a bit more, let’s use the left-meeting event as an example.

In Call, we added an event listener for left-meeting, which calls a callback method (handleLeftMeeting) when triggered:

// Call.svelte

callFrame.on("left-meeting", handleLeftMeeting); // in initializeDaily()

 const handleLeftMeeting = (e) => 
   updateMeetingState(e);
   if (interval) clearInterval(interval);
   dispatch("left");
 ;

The callback, handleLeftMeeting, gets called when the local user clicks the Leave button via Daily Prebuilt’s UI. It then dispatches a custom event (left) that App is listening for.

It does a couple other clean up tasks as well, but we won’t worry about that for now. Essentially, it resets our app’s state.

If you recall, App has a listener on the Call component for a custom left event:
<Call userName url on:left=handleLeaveCall />

handleLeaveCall gets called, which resets our currentScreen value to home:

const handleLeaveCall = () => currentScreen = "home";

Once currentScreen gets reset to home, App will render the Home component instead of Call.

Leaving the call and going back to the home screen

We’ve come full circle back home! 🍩

full circle



Cleaning up after the party

Now that we have reset our state to get back to the home screen, we still have a little leftover clean up to do.

If you recall, we added some Daily event listeners to our callFrame.

As a final step, we can turn off those event listeners and destroy our Daily callFrame. It will be recreated when the next call is joined so we don’t need it hanging around after leaving this call.

// Call.svelte
import  onDestroy  from "svelte";

onDestroy(() => 
    if (callFrame) 
      // remove event listeners
      callFrame.off("joining-meeting", updateMeetingState);
      callFrame.off("joined-meeting", updateMeetingState);
      callFrame.off("left-meeting", handleLeftMeeting);
      callFrame.off("error", handleError);

      // destroy Daily callframe after call ends
      callFrame.destroy();
    
  );

Svelte provides an onDestroy lifecycle method that will be triggered when the Call component is destroyed.

In onDestroy, we can turn off our Daily event listeners and destroy the callFrame with Daily’s destroy method.



Wrapping up

We hope this tutorial helps you get started with building your own Daily video apps in Svelte. To learn more about Daily Prebuilt, check out our other Daily Prebuilt blog posts.

If you’d like to build something more custom, keep an eye out on Twitter for our next Svelte tutorial! 👀




Source link

How to Make a Component That Supports Multiple Frameworks in a Monorepo

Your mission — should you decide to accept it — is to build a Button component in four frameworks, but, only use one button.css file!

This idea is very important to me. I’ve been working on a component library called AgnosticUI where the purpose is building UI components that aren’t tied to any one particular JavaScript framework. AgnosticUI works in React, Vue 3, Angular, and Svelte. So that’s exactly what we’ll do today in this article: build a button component that works across all these frameworks.

The source code for this article is available on GitHub on the the-little-button-that-could-series branch.

Table of contents

Why a monorepo?

We’re going to set up a tiny Yarn workspaces-based monorepo. Why? Chris actually has a nice outline of the benefits in another post. But here’s my own biased list of benefits that I feel are relevant for our little buttons endeavor:

Coupling

We’re trying to build a single button component that uses just one button.css file across multiple frameworks. So, by nature, there’s some purposeful coupling going on between the various framework implementations and the single-source-of-truth CSS file. A monorepo setup provides a convenient structure that facilitates copying our single button.css component into various framework-based projects.

Workflow

Let’s say the button needs a tweak — like the “focus-ring” implementation, or we screwed up the use of aria in the component templates. Ideally, we’d like to correct things in one place rather than making individual fixes in separate repositories.

Testing

We want the convenience of firing up all four button implementations at the same time for testing. As this sort of project grows, it’s safe to assume there will be more proper testing. In AgnosticUI, for example, I’m currently using Storybook and often kick off all the framework Storybooks, or run snapshot testing across the entire monorepo.

I like what Leonardo Losoviz has to say about the monorepo approach. (And it just so happens to align with with everything we’ve talked about so far.)

I believe the monorepo is particularly useful when all packages are coded in the same programming language, tightly coupled, and relying on the same tooling.

Setting up

Time to dive into code — start by creating a top-level directory on the command-line to house the project and then cd into it. (Can’t think of a name? mkdir buttons && cd buttons will work fine.)

First off, let’s initialize the project:

$ yarn init
yarn init v1.22.15
question name (articles): littlebutton
question version (1.0.0): 
question description: my little button project
question entry point (index.js): 
question repository url: 
question author (Rob Levin): 
question license (MIT): 
question private: 
success Saved package.json

That gives us a package.json file with something like this:


  "name": "littlebutton",
  "version": "1.0.0",
  "description": "my little button project",
  "main": "index.js",
  "author": "Rob Levin",
  "license": "MIT"

Creating the baseline workspace

We can set the first one up with this command:

mkdir -p ./littlebutton-css

Next, we need to add the two following lines to the monorepo’s top-level package.json file so that we keep the monorepo itself private. It also declares our workspaces:

// ...
"private": true,
"workspaces": ["littlebutton-react", "littlebutton-vue", "littlebutton-svelte", "littlebutton-angular", "littlebutton-css"]

Now descend into the littlebutton-css directory. We’ll again want to generate a package.json with yarn init. Since we’ve named our directory littlebutton-css (the same as how we specified it in our workspaces in package.json) we can simply hit the Return key and accept all the prompts:

$ cd ./littlebutton-css && yarn init
yarn init v1.22.15
question name (littlebutton-css): 
question version (1.0.0): 
question description: 
question entry point (index.js): 
question repository url: 
question author (Rob Levin): 
question license (MIT): 
question private: 
success Saved package.json

At this point, the directory structure should look like this:

├── littlebutton-css
│   └── package.json
└── package.json

We’ve only created the CSS package workspace at this point as we’ll be generating our framework implementations with tools like vite which, in turn, generate a package.json and project directory for you. We will have to remember that the name we choose for these generated projects must match the name we’ve specified in the package.json for our earlier workspaces to work.

Baseline HTML & CSS

Let’s stay in the ./littlebutton-css workspace and create our simple button component using vanilla HTML and CSS files.

touch index.html ./css/button.css

Now our project directory should look like this:

littlebutton-css
├── css
│   └── button.css
├── index.html
└── package.json

Let’s go ahead and connect some dots with some boilerplate HTML in ./index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>The Little Button That Could</title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="css/button.css">
</head>
<body>
  <main>
    <button class="btn">Go</button>
  </main>
</body>
</html>

And, just so we have something visual to test, we can add a little color in ./css/button.css:

.btn 
  color: hotpink;
A mostly unstyled button with hot-pink text from the monorepo framework.

Now open up that index.html page in the browser. If you see an ugly generic button with hotpink text… success!

Framework-specific workspaces

So what we just accomplished is the baseline for our button component. What we want to do now is abstract it a bit so it’s extensible for other frameworks and such. For example, what if we want to use the button in a React project? We’re going to need workspaces in our monorepo for each one. We’ll start with React, then follow suit for Vue 3, Angular, and Svelte.

React

We’re going to generate our React project using vite, a very lightweight and blazingly fast builder. Be forewarned that if you attempt to do this with create-react-app, there’s a very good chance you will run into conflicts later with react-scripts and conflicting webpack or Babel configurations from other frameworks, like Angular.

To get our React workspace going, let’s go back into the terminal and cd back up to the top-level directory. From there, we’ll use vite to initialize a new project — let’s call it littlebutton-react — and, of course, we’ll select react as the framework and variant at the prompts:

$ yarn create vite
yarn create v1.22.15
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Installed "create-vite@2.6.6" with binaries:
      - create-vite
      - cva
✔ Project name: … littlebutton-react
✔ Select a framework: › react
✔ Select a variant: › react

Scaffolding project in /Users/roblevin/workspace/opensource/guest-posts/articles/littlebutton-react...

Done. Now run:

  cd littlebutton-react
  yarn
  yarn dev

✨  Done in 17.90s.

We initialize the React app with these commands next:

cd littlebutton-react
yarn
yarn dev

With React installed and verified, let’s replace the contents of src/App.jsx to house our button with the following code:

import "./App.css";

const Button = () => 
  return <button>Go</button>;
;

function App() 
  return (
    <div className="App">
      <Button />
    </div>
  );


export default App;

Now we’re going to write a small Node script that copies our littlebutton-css/css/button.css right into our React application for us. This step is probably the most interesting one to me because it’s both magical and ugly at the same time. It’s magical because it means our React button component is truly deriving its styles from the same CSS written in the baseline project. It’s ugly because, well, we are reaching up out of one workspace and grabbing a file from another. ¯_(ツ)_/¯

Add the following little Node script to littlebutton-react/copystyles.js:

const fs = require("fs");
let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
fs.writeFileSync("./src/button.css", css, "utf8");

Let’s place a node command to run that in a package.json script that happens before the dev script in littlebutton-react/package.json. We’ll add a syncStyles and update the dev to call syncStyles before vite:

"syncStyles": "node copystyles.js",
"dev": "yarn syncStyles && vite",

Now, anytime we fire up our React application with yarn dev, we’ll first be copying the CSS file over. In essence, we’re “forcing” ourselves to not diverge from the CSS package’s button.css in our React button.

But we want to also leverage CSS Modules to prevent name collisions and global CSS leakage, so we have one more step to do to get that wired up (from the same littlebutton-react directory):

touch src/button.module.css

Next, add the following to the new src/button.module.css file:

.btn 
  composes: btn from './button.css';

I find composes (also known as composition) to be one of the coolest features of CSS Modules. In a nutshell, we’re copying our HTML/CSS version of button.css over wholesale then composing from our one .btn style rule.

With that, we can go back to our src/App.jsx and import the CSS Modules styles into our React component with this:

import "./App.css";
import styles from "./button.module.css";

const Button = () => 
  return <button className=styles.btn>Go</button>;
;

function App() 
  return (
    <div className="App">
      <Button />
    </div>
  );


export default App;

Whew! Let’s pause and try to run our React app again:

yarn dev

If all went well, you should see that same generic button, but with hotpink text. Before we move on to the next framework, let’s move back up to our top-level monorepo directory and update its package.json:


  "name": "littlebutton",
  "version": "1.0.0",
  "description": "toy project",
  "main": "index.js",
  "author": "Rob Levin",
  "license": "MIT",
  "private": true,
  "workspaces": ["littlebutton-react", "littlebutton-vue", "littlebutton-svelte", "littlebutton-angular"],
  "scripts": 
    "start:react": "yarn workspace littlebutton-react dev"
  

Run the yarn command from the top-level directory to get the monorepo-hoisted dependencies installed.

The only change we’ve made to this package.json is a new scripts section with a single script to start the React app. By adding start:react we can now run yarn start:react from our top-level directory and it will fire up the project we just built in ./littlebutton-react without the need for cd‘ing — super convenient!

We’ll tackle Vue and Svelte next. It turns out that we can take a pretty similar approach for these as they both use single file components (SFC). Basically, we get to mix HTML, CSS, and JavaScript all into one single file. Whether you like the SFC approach or not, it’s certainly adequate enough for building out presentational or primitive UI components.

Vue

Following the steps from vite’s scaffolding docs we’ll run the following command from the monorepo’s top-level directory to initialize a Vue app:

yarn create vite littlebutton-vue --template vue

This generates scaffolding with some provided instructions to run the starter Vue app:

cd littlebutton-vue
yarn
yarn dev

This should fire up a starter page in the browser with some heading like “Hello Vue 3 + Vite.” From here, we can update src/App.vue to:

<template>
  <div id="app">
    <Button class="btn">Go</Button>
  </div>
</template>

<script>
import Button from './components/Button.vue'

export default 
  name: 'App',
  components: 
    Button
  

</script>

And we’ll replace any src/components/* with src/components/Button.vue:

<template>
  <button :class="classes"><slot /></button>
</template>

<script>
export default 
  name: 'Button',
  computed: 
    classes() 
      return 
        [this.$style.btn]: true,
      
    
  

</script>

<style module>
.btn 
  color: slateblue;

</style>

Let’s break this down a bit:

  • :class="classes" is using Vue’s binding to call the computed classes method.
  • The classes method, in turn, is utilizing CSS Modules in Vue with the this.$style.btn syntax which will use styles contained in a <style module> tag.

For now, we’re hardcoding color: slateblue simply to test that things are working properly within the component. Try firing up the app again with yarn dev. If you see the button with our declared test color, then it’s working!

Now we’re going to write a Node script that copies our littlebutton-css/css/button.css into our Button.vue file similar to the one we did for the React implementation. As mentioned, this component is a SFC so we’re going to have to do this a little differently using a simple regular expression.

Add the following little Node.js script to littlebutton-vue/copystyles.js:

const fs = require("fs");
let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
const vue = fs.readFileSync("./src/components/Button.vue", "utf8");
// Take everything between the starting and closing style tag and replace
const styleRegex = /<style module>([sS]*?)</style>/;
let withSynchronizedStyles = vue.replace(styleRegex, `<style module>n$cssn</style>`);
fs.writeFileSync("./src/components/Button.vue", withSynchronizedStyles, "utf8");

There’s a bit more complexity in this script, but using replace to copy text between opening and closing style tags via regex isn’t too bad.

Now let’s add the following two scripts to the scripts clause in the littlebutton-vue/package.json file:

"syncStyles": "node copystyles.js",
"dev": "yarn syncStyles && vite",

Now run yarn syncStyles and look at ./src/components/Button.vue again. You should see that our style module gets replaced with this:

<style module>
.btn 
  color: hotpink;

</style>

Run the Vue app again with yarn dev and verify you get the expected results — yes, a button with hotpink text. If so, we’re good to move on to the next framework workspace!

Svelte

Per the Svelte docs, we should kick off our littlebutton-svelte workspace with the following, starting from the monorepo’s top-level directory:

npx degit sveltejs/template littlebutton-svelte
cd littlebutton-svelte
yarn && yarn dev

Confirm you can hit the “Hello World” start page at http://localhost:5000. Then, update littlebutton-svelte/src/App.svelte:

<script>
  import Button from './Button.svelte';
</script>
<main>
  <Button>Go</Button>
</main>

Also, in littlebutton-svelte/src/main.js, we want to remove the name prop so it looks like this:

import App from './App.svelte';

const app = new App(
  target: document.body
);

export default app;

And finally, add littlebutton-svelte/src/Button.svelte with the following:

<button class="btn">
  <slot></slot>
</button>

<script>
</script>

<style>
  .btn 
    color: saddlebrown;
  
</style>

One last thing: Svelte appears to name our app: "name": "svelte-app" in the package.json. Change that to "name": "littlebutton-svelte" so it’s consistent with the workspaces name in our top-level package.json file.

Once again, we can copy our baseline littlebutton-css/css/button.css into our Button.svelte. As mentioned, this component is a SFC, so we’re going to have to do this using a regular expression. Add the following Node script to littlebutton-svelte/copystyles.js:

const fs = require("fs");
let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
const svelte = fs.readFileSync("./src/Button.svelte", "utf8");
const styleRegex = /<style>([sS]*?)</style>/;
let withSynchronizedStyles = svelte.replace(styleRegex, `<style>n$cssn</style>`);
fs.writeFileSync("./src/Button.svelte", withSynchronizedStyles, "utf8");

This is super similar to the copy script we used with Vue, isn’t it? We’ll add similar scripts to our package.json script:

"dev": "yarn syncStyles && rollup -c -w",
"syncStyles": "node copystyles.js",

Now run yarn syncStyles && yarn dev. If all is good, we once again should see a button with hotpink text.

If this is starting to feel repetitive, all I have to say is welcome to my world. What I’m showing you here is essentially the same process I’ve been using to build my AgnosticUI project!

Angular

You probably know the drill by now. From the monorepo’s top-level directory, install Angular and create an Angular app. If we were creating a full-blown UI library we’d likely use ng generate library or even nx. But to keep things as straightforward as possible we’ll set up a boilerplate Angular app as follows:

npm install -g @angular/cli ### unless you already have installed
ng new littlebutton-angular ### choose no for routing and CSS
? Would you like to add Angular routing? (y/N) N
❯ CSS 
  SCSS   [ https://sass-lang.com/documentation/syntax#scss ] 
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ] 
  Less   [ http://lesscss.org ]

cd littlebutton-angular && ng serve --open

With the Angular setup confirmed, let’s update some files. cd littlebutton-angular, delete the src/app/app.component.spec.ts file, and add a button component in src/components/button.component.ts, like this:

import  Component  from '@angular/core';

@Component(
  selector: 'little-button',
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.css'],
)
export class ButtonComponent 

Add the following to src/components/button.component.html:

<button class="btn">Go</button>

And put this in the src/components/button.component.css file for testing:

.btn 
  color: fuchsia;

In src/app/app.module.ts:

import  NgModule  from '@angular/core';
import  BrowserModule  from '@angular/platform-browser';

import  AppComponent  from './app.component';
import  ButtonComponent  from '../components/button.component';

@NgModule(
  declarations: [AppComponent, ButtonComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
)
export class AppModule 

Next, replace src/app/app.component.ts with:

import  Component  from '@angular/core';

@Component(
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
)
export class AppComponent 

Then, replace src/app/app.component.html with:

<main>
  <little-button>Go</little-button>
</main>

With that, let’s run yarn start and verify our button with fuchsia text renders as expected.

Again, we want to copy over the CSS from our baseline workspace. We can do that by adding this to littlebutton-angular/copystyles.js:

const fs = require("fs");
let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
fs.writeFileSync("./src/components/button.component.css", css, "utf8");

Angular is nice in that it uses ViewEncapsulation that defaults to to emulate which mimics, according to the docs,

[…] the behavior of shadow DOM by preprocessing (and renaming) the CSS code to effectively scope the CSS to the component’s view.

This basically means we can literally copy over button.css and use it as-is.

Finally, update the package.json file by adding these two lines in the scripts section:

"start": "yarn syncStyles && ng serve",
"syncStyles": "node copystyles.js",

With that, we can now run yarn start once more and verify our button text color (which was fuchsia) is now hotpink.

What have we just done?

Let’s take a break from coding and think about the bigger picture and what we’ve just done. Basically, we’ve set up a system where any changes to our CSS package’s button.css will get copied over into all the framework implementations as a result of our copystyles.js Node scripts. Further, we’ve incorporated idiomatic conventions for each of the frameworks:

  • SFC for Vue and Svelte
  • CSS Modules for React (and Vue within the SFC <style module> setup)
  • ViewEncapsulation for Angular

Of course I state the obvious that these aren’t the only ways to do CSS in each of the above frameworks (e.g. CSS-in-JS is a popular choice), but they are certainly accepted practices and are working quite well for our greater goal — to have a single CSS source of truth to drive all framework implementations.

If, for example, our button was in use and our design team decided we wanted to change from 4px to 3px border-radius, we could update the one file, and any separate implementations would stay synced.

This is compelling if you have a polyglot team of developers that enjoy working in multiple frameworks, or, say an offshore team (that’s 3× productive in Angular) that’s being tasked to build a back-office application, but your flagship product is built in React. Or, you’re building an interim admin console and you’d love to experiment with using Vue or Svelte. You get the picture.

Finishing touches

OK, so we have the monorepo architecture in a really good spot. But there’s a few things we can do to make it even more useful as far as the developer experience goes.

Better start scripts

Let’s move back up to our top-level monorepo directory and update its package.json scripts section with the following so we can kick any framework implementation without cd‘ing:

// ...
"scripts": 
  "start:react": "yarn workspace littlebutton-react dev",
  "start:vue": "yarn workspace littlebutton-vue dev ",
  "start:svelte": "yarn workspace littlebutton-svelte dev",
  "start:angular": "yarn workspace littlebutton-angular start"
,

Better baseline styles

We can also provide a better set of baseline styles for the button so it starts from a nice, neutral place. Here’s what I did in the littlebutton-css/css/button.css file.

View Full Snippet
.btn 
  --button-dark: #333;
  --button-line-height: 1.25rem;
  --button-font-size: 1rem;
  --button-light: #e9e9e9;
  --button-transition-duration: 200ms;
  --button-font-stack:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    Ubuntu,
    "Helvetica Neue",
    sans-serif;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  user-select: none;
  appearance: none;
  cursor: pointer;
  box-sizing: border-box;
  transition-property: all;
  transition-duration: var(--button-transition-duration);
  color: var(--button-dark);
  background-color: var(--button-light);
  border-color: var(--button-light);
  border-style: solid;
  border-width: 1px;
  font-family: var(--button-font-stack);
  font-weight: 400;
  font-size: var(--button-font-size);
  line-height: var(--button-line-height);
  padding-block-start: 0.5rem;
  padding-block-end: 0.5rem;
  padding-inline-start: 0.75rem;
  padding-inline-end: 0.75rem;
  text-decoration: none;
  text-align: center;


/* Respect users reduced motion preferences */
@media (prefers-reduced-motion) 
  .btn 
    transition-duration: 0.001ms !important;
  

Let’s test this out! Fire up each of the four framework implementations with the new and improved start scripts and confirm the styling changes are in effect.

Neutral (gray) styled button from the monorepo framework

One CSS file update proliferated to four frameworks — pretty cool, eh!?

Set a primary mode

We’re going to add a mode prop to each of our button’s and implement primary mode next. A primary button could be any color but we’ll go with a shade of green for the background and white text. Again, in the baseline stylesheet:

.btn 
  --button-primary: #14775d;
  --button-primary-color: #fff;
  /* ... */

Then, just before the @media (prefers-reduced-motion) query, add the following btn-primary to the same baseline stylesheet:

.btn-primary 
  background-color: var(--button-primary);
  border-color: var(--button-primary);
  color: var(--button-primary-color);

There we go! Some developer conveniences and better baseline styles!

Updating each component to take a mode property

Now that we’ve added our new primary mode represented by the .btn-primary class, we want to sync the styles for all four framework implementations. So, let’s add some more package.json scripts to our top level scripts:

"sync:react": "yarn workspace littlebutton-react syncStyles",
"sync:vue": "yarn workspace littlebutton-vue syncStyles",
"sync:svelte": "yarn workspace littlebutton-svelte syncStyles",
"sync:angular": "yarn workspace littlebutton-angular syncStyles"

Be sure to respect JSON’s comma rules! Depending on where you place these lines within your scripts: ..., you’ll want to make sure there are no missing or trailing commas.

Go ahead and run the following to fully synchronize the styles:

yarn sync:angular && yarn sync:react && yarn sync:vue && yarn sync:svelte

Running this doesn’t change anything because we haven’t applied the primary class yet, but you should at least see the CSS has been copied over if you go look at the framework’s button component CSS.

React

If you haven’t already, double-check that the updated CSS got copied over into littlebutton-react/src/button.css. If not, you can run yarn syncStyles. Note that if you forget to run yarn syncStyles our dev script will do this for us when we next start the application anyway:

"dev": "yarn syncStyles && vite",

For our React implementation, we additionally need to add a composed CSS Modules class in littlebutton-react/src/button.module.css that is composed from the new .btn-primary:

.btnPrimary 
  composes: btn-primary from './button.css';

We’ll also update littlebutton-react/src/App.jsx:

import "./App.css";
import styles from "./button.module.css";

const Button = ( mode ) => 
  const primaryClass = mode ? styles[`btn$mode.charAt(0).toUpperCase()$mode.slice(1)`] : '';
  const classes = primaryClass ? `$styles.btn $primaryClass` : styles.btn;
  return <button className=classes>Go</button>;
;

function App() 
  return (
    <div className="App">
      <Button mode="primary" />
    </div>
  );


export default App;

Fire up the React app with yarn start:react from the top-level directory. If all goes well, you should now see your green primary button.

A dark green button with white text positioning in the center of the screen.

As a note, I’m keeping the Button component in App.jsx for brevity. Feel free to tease out the Button component into its own file if that bothers you.

Vue

Again, double-check that the button styles were copied over and, if not, run yarn syncStyles.

Next, make the following changes to the <script> section of littlebutton-vue/src/components/Button.vue:

<script>
export default {
  name: 'Button',
  props: 
    mode: 
      type: String,
      required: false,
      default: '',
      validator: (value) => 
        const isValid = ['primary'].includes(value);
        if (!isValid) 
          console.warn(`Allowed types for Button are primary`);
        
        return isValid;
      ,
    
  ,
  computed: 
    classes() 
      return 
        [this.$style.btn]: true,
        [this.$style['btn-primary']]: this.mode === 'primary',
      
    
  
}
</script>

Now we can update the markup in littlebutton-vue/src/App.vue to use the new mode prop:

<Button mode="primary">Go</Button>

Now you can yarn start:vue from the top-level directory and check for the same green button.

Svelte

Let’s cd into littlebutton-svelte and verify that the styles in littlebutton-svelte/src/Button.svelte have the new .btn-primary class copied over, and yarn syncStyles if you need to. Again, the dev script will do that for us anyway on the next startup if you happen to forget.

Next, update the Svelte template to pass the mode of primary. In src/App.svelte:

<script>
  import Button from './Button.svelte';
</script>
<main>
  <Button mode="primary">Go</Button>
</main>

We also need to update the top of our src/Button.svelte component itself to accept the mode prop and apply the CSS Modules class:

<button class="classes">
  <slot></slot>
</button>
<script>
  export let mode = "";
  const classes = [
    "btn",
    mode ? `btn-$mode` : "",
  ].filter(cls => cls.length).join(" ");
</script>

Note that the <styles> section of our Svelte component shouldn’t be touched in this step.

And now, you can yarn dev from littlebutton-svelte (or yarn start:svelte from a higher directory) to confirm the green button made it!

Angular

Same thing, different framework: check that the styles are copied over and run yarn syncStyles if needed.

Let’s add the mode prop to the littlebutton-angular/src/app/app.component.html file:

<main>
  <little-button mode="primary">Go</little-button>
</main>

Now we need to set up a binding to a classes getter to compute the correct classes based on if the mode was passed in to the component or not. Add this to littlebutton-angular/src/components/button.component.html (and note the binding is happening with the square brackets):

<button [class]="classes">Go</button>

Next, we actually need to create the classes binding in our component at littlebutton-angular/src/components/button.component.ts:

import  Component, Input  from '@angular/core';

@Component(
  selector: 'little-button',
  templateUrl: './button.component.html',
  styleUrls: ['./button.component.css'],
)
export class ButtonComponent  undefined = undefined;

  public get classes(): string 
    const modeClass = this.mode ? `btn-$this.mode` : '';
    return [
      'btn',
      modeClass,
    ].filter(cl => cl.length).join(' ');
  

We use the Input directive to take in the mode prop, then we create a classes accessor which adds the mode class if it’s been passed in.

Fire it up and look for the green button!

Code complete

If you’ve made it this far, congratulations — you’ve reached code complete! If something went awry, I’d encourage you to cross-reference the source code over at GitHub on the the-little-button-that-could-series branch. As bundlers and packages have a tendency to change abruptly, you might want to pin your package versions to the ones in this branch if you happen to experience any dependency issues.

Take a moment to go back and compare the four framework-based button component implementations we just built. They’re still small enough to quickly notice some interesting differences in how props get passed in, how we bind to props, and how CSS name collisions are prevented among other subtle differences. As I continue to add components to AgnosticUI (which supports these exact same four frameworks), I’m continually pondering which offers the best developer experience. What do you think?

Homework

If you’re the type that likes to figure things out on your own or enjoys digging in deeper, here are ideas.

Button states

The current button styles do not account for various states, like :hover. I believe that’s a good first exercise.

/* You should really implement the following states
   but I will leave it as an exercise for you to 
   decide how to and what values to use.
*/
.btn:focus 
  /* If you elect to remove the outline, replace it
     with another proper affordance and research how
     to use transparent outlines to support windows
     high contrast
  */

.btn:hover  
.btn:visited  
.btn:active  
.btn:disabled  

Variants

Most button libraries support many button variations for things like sizes, shapes, and colors. Try creating more than the primary mode we already have. Maybe a secondary variation? A warning or success? Maybe filled and outline? Again, you can look at AgnosticUI’s buttons page for ideas.

CSS custom properties

If you haven’t started using CSS custom properties yet, I’d strongly recommend it. You can start by having a look at AgnosticUI’s common styles. I heavily lean on custom properties in there. Here are some great articles that cover what custom properties are and how you might leverage them:

  • A Complete Guide to Custom Properties
  • A DRY Approach to Color Themes in CSS

Types

No… not typings, but the <button> element’s type attribute. We didn’t cover that in our component but there’s an opportunity to extend the component to other use cases with valid types, like button, submit, and reset. This is pretty easy to do and will greatly improve the button’s API.

More ideas

Gosh, you could do so much — add linting, convert it to Typescript, audit the accessibility, etc.

The current Svelte implementation is suffering from some pretty loose assumptions as we have no defense if the valid primary mode isn’t passed — that would produce a garbage CSS class:

mode ? `btn-$mode` : "",

You could say, “Well, .btn-garbage as a class isn’t exactly harmful.” But it’s probably a good idea to style defensively when and where possible.

Potential pitfalls

There are some things you should be aware of before taking this approach further:

  • Positional CSS based on the structure of the markup will not work well for the CSS Modules based techniques used here.
  • Angular makes positional techniques even harder as it generates :host element representing each component view. This means you have these extra elements in between your template or markup structure. You’ll need to work around that.
  • Copying styles across workspace packages is a bit of an anti-pattern to some folks. I justify it because I believe the benefits outweigh the costs; also, when I think about how monorepos use symlinks and (not-so-failproof) hoisting, I don’t feel so bad about this approach.
  • You’ll have to subscribe to the decoupled techniques used here, so no CSS-in-JS.

I believe that all approaches to software development have their pros and cons and you ultimately have to decide if sharing a single CSS file across frameworks works for you or your specific project. There are certainly other ways you could do this (e.g. using littlebuttons-css as an npm package dependency) if needed.

Conclusion

Hopefully I’ve whet your appetite and you’re now really intrigued to create UI component libraries and/or design systems that are not tied to a particular framework. Maybe you have a better idea on how to achieve this — I’d love to hear your thoughts in the comments!

I’m sure you’ve seen the venerable TodoMVC project and how many framework implementations have been created for it. Similarly, wouldn’t it be nice to have a UI component library of primitives available for many frameworks? Open UI is making great strides to properly standardize native UI component defaults, but I believe we’ll always need to insert ourselves to some extent. Certainly, taking a good year to build a custom design system is quickly falling out of favor and companies are seriously questioning their ROI. Some sort of scaffolding is required to make the endeavor practical.

The vision of AgnosticUI is to have a relatively agnostic way to build design systems quickly that are not tied down to a particular frontend framework. If you’re compelled to get involved, the project is still very early and approachable and I’d love some help! Plus, you’re already pretty familiar with the how the project works now that you’ve gone through this tutorial!


How to Make a Component That Supports Multiple Frameworks in a Monorepo originally published on CSS-Tricks. You should get the newsletter and become a supporter.


Source link

Using WebRTC and Svelte

Hello everyone kindly check out this github repo which hosts the code for my tutorial on how to use Svelte and Google’s WebRTC.

What is WebRTC?

WebRTC is a protocol used for having real time communications in the web. With WebRTC, you can add real-time communication capabilities to your application that works on top of an open standard. It supports video, voice, and generic data to be sent between peers, allowing developers to build powerful voice- and video-communication solutions. The technology is available on all modern browsers as well as on native clients for all major platforms. The technologies behind WebRTC are implemented as an open web standard and available as regular JavaScript APIs in all major browsers. For native clients, like Android and iOS applications, a library is available that provides the same functionality. The WebRTC project is open-source and supported by Apple, Google, Microsoft and Mozilla, amongst others. This page is maintained by the Google WebRTC team.

Why I am doing this project?

I feel like svelte is new and has come with some solutions to some of the pain points in today’s front end frameworks like react.js and vue.js, and I felt inspired to implement a webRTC application using Svelte only.

If you like WebRTC and would like to learn more about or you want to support me with some code, here is the repo
.

This repo includes all the tutorials that are on https://webrtc.github.io/samples/. The aim of the project is to convert all the sample code to a Svelte app.

Please kindly star the project and also follow the account.
Thanks, Your support really means a lot to me 🙂


Source link

Per-user global stores in Sveltekit

Svelte is really nice with stores. You can have a separate folder like this with named global stores:

// stores/user.ts
import  writable  from "svelte/store";

export const user = writable<User | null>(null);
// stores/products.ts
import  writable, get as $  from "svelte/store";
import  user  from "../user";

export const products = writable<Product[] | null>(null);

// Remove products on log out
user.subscribe($user => 
  if (!$user) 
    products.set(null);
  
);

export async function loadProducts() 
  // Simple caching mechanism
  if (!$(products) && $(user)) 
    const response = await fetch('/api/products');
    products.set(await response.json());
  

And then you can directly import the stores in your components, no hassle!

On SvelteKit, though it doesn’t work. Or rather, it works, but the stores are shared among all users:

Mutating any shared state on the server will affect all clients, not just the current one.

So… how do we get around this?



Step 1: Session as the key

It already exists! Sort of…

It’s the session, available through input.session in a load(input) function, or through getStores().session within a component.

But the session can only contain POJO data, or JSON with Maps and Sets. It’s the limits imposed by devalue which is used by SvelteKit.

So, no dice for putting the store inside the session. But there’s a nifty feature of javascript called a WeakMap.

So you can do something like this:

const stores = new WeakMap<Session, ...>();

And to get the stores associated with the current user, you can do either stores.get(input.session) when in a load function or stores.get($session) when in a component.



Step 2: Adding a bit of tooling

Wouldn’t a standardized way of accessing the stores be great?

First, we need an easy way to get the current session, no matter where we are.

It’s easy when in a component: we can just call getStores(). Inside a load function, there’s no built-in way, however.

So, we’re going to need a constraint: create a useLoad function, that is called during load:

export function load(input) 
  useLoad(input, func1, func2, ...);

It will execute func1, func2 and so on with the session in input.session as the context.

And then we will create useSession() that can get the current session from anywhere.

// useLoad.ts
import type  LoadInput  from "@sveltejs/kit";

type SessionData =  stores: Map<unknown, unknown>; fetch: typeof fetch ;

export const loadSession = writable<Session | null>(null);
export const sessionData = new WeakMap<Session, SessionData>();

// Typescript magic
type ReturnTypes<T> = T extends [...infer U, infer A]
  ? A extends (...args: unknown[]) => unknown
    ? ReturnType<A> & ReturnTypes<U>
    : void
  : void;

export function useLoad<T extends Array<(...args: unknown[]) => unknown>>(input: LoadInput, ...fns: T): ReturnTypes<T> 
  if (!sessionData.has(input.session)) 
    sessionData.set(input.session,  stores: new Map(), fetch: input.fetch );
  
  loadSession.set(input.session);

  try 
    return Object.assign(, ...fns.map((fn) => fn()));
   finally 
    loadSession.set(null);
  

// useSession.ts
import  getStores  from "$app/stores";
import  get as $, writable  from "svelte/store";
import  loadSession, sessionData  from "../useLoad";

export function useSession():  session: Session; data: SessionData  
  const session = $(loadSession) ?? ($(getStores().session) as Session);

  const data = sessionData.get(session);

  if (!data) 
    throw new Error("Call useLoad before calls to useSession");
  

  return 
    session,
    data,
  ;

Now we have:

  • useLoad(input, ...) that needs to be called inside the load function.
  • useSession() which can get the current session, even within the load function.



Step 3: defineStore()

It’s inspired by the excellent Vue libary pinia

Svelte has a wonderfully simple way of creating global stores, unfortunately in the SSR context we’re going to need to recreate a defineStore function.

It’s going to look like this:

// useUser.ts
import  writable  from "svelte/store";
import  defineStore  from "./defineStore";

const useUser = defineStore(() => null>(null);

  return 
    user
  
);
// useProducts.ts
import  browser  from "$app/env";
import  writable  from "svelte/store";
import  defineStore  from "./defineStore";
import  useUser  from "./useUser";

const useProducts = defineStore(() => null>(null);

  // Remove products on log out. Only execute client-side
  // to prevent memory leaks
  if (browser) 
    user.subscribe($user => 
      if (!$user) 
        products.set(null);
      
    );
  

  return 
    products
  ;
);

We can see that the products store can even look up the user store!

How does this work? defineStore takes a function as a parameter. The first time the store is needed for the current session, the function is executed. The subsequent times, the cached result is returned:

import  useSession  from "./useSession";

export function defineStore<T>(fn: () => T): () => T 
  return () => 
    const  data  = useSession();

    if (!data.stores!.has(fn)) 
      data.stores!.set(fn, fn());
    

    return data.stores!.get(fn) as T;
  ;

Easy peasy.



Step 4: Using SvelteKit’s fetch

One particularity about SvelteKit is that it provides a fetch function that should be used within load. The reason is so that when the same calls are executed both serverside & clientside, the cached value can be used clientside instead of repeating the call.

Maybe you noticed, but in the above useLoad, we stored input.fetch in sessionData.

To access it, we can create a useFetch function:

export const useFetch = () => 
  const  data  = useSession();
  return  fetch: data.fetch ;

And we can now improve our useProducts function to add loadProducts:

// useProducts.ts
import  browser  from "$app/env";
import  writable, get as $  from "svelte/store";
import  defineStore  from "./defineStore";
import  useUser  from "./useUser";

const useProducts = defineStore(() => null>(null);

  if (browser) 
    user.subscribe($user => 
      if (!$user) 
        products.set(null);
      
    );
  

  const loadProducts = async () => 
    if (!$(products) && $(user)) 
      const response = await fetch('/api/products');
      products.set(await response.json());
    
  

  return 
    products,
    loadProducts
  ;
);



Step 5: Using our useXxx functions in the code

Some of you maybe wondering why the useXxx naming. Are we doing React? 😅

I find that this naming convention has been adopted for functions that need to be called in a particular context – the immediate context for the component.

It’s used by another excellent Vue library, vueuse.

Here we decide it means a function that can be called either:

  • In the immediate context of a component
  • Through useLoad

So, let’s say we wanted to load the products inside a load function. Here is what it would look like:

import  get as storeGet  from "svelte/store";
import  useLoad  from "$lib/use/useLoad";
import  useProduct  from "$lib/use/useProduct";

export async function load(input: LoadInput) 
  const products, loadProducts = useLoad(input, useProducts);

  await loadProducts();

  return 
    products: storeGet(products);
  

And here is what it would look like inside a component:

<script lang="ts">
import  useProduct  from "$lib/use/useProduct";

const products, loadProducts = useProducts();
const user = useUser();

$: loadProducts(), [$user] // reloads products each time user changes
</script>

#each ($products 
  <Product product/>
/each

So that’s it! We now have a uniform way of accessing per-user global stores, whether we’re serverside or clientside, and whether we’re in a load function or inside a component.

As a bonus, we also have a uniform way of accessing the correct fetch function 😉


Source link

My Django-Svelte setup for fullstack development



My Django-Svelte workflow for fullstack development



Motivation

I recently discovered Svelte and became my first choice for personal projects and MVP/POCs.

As a backend developer, I struggled a lot with frameworks like React or Vue, looking for a productive workflow. Svelte is dead simple compared to them, in my opinion. And yet very powerful.

However, coming from Django, I still struggled on stuff like security or form validation: if I want a SPA, do I really need to use only Django Rest Framework and implement everything through REST API?
Take authentication for example: Django has built-in views for doing it in a secure way. Do I really need to re-implement it every time client-side using JWT? Looks like a waste of time, for my productivity at least.

I really like Django approach “batteries included”, since it ships with best practices and it makes me a lot more focused on the goal I want to achieve.
On the other hand, I still want the flexibility and reactivity provided by a SPA framework.

So, after multiple projects, I’d like to share in this blog post my personal take aways and show how I scaffold my projects, in order to get:

  • a Django website, using the powerful template system for authentication, registration and password recovery (using Django built in views)
  • a Svelte app served by Django, with hot reloading during development
  • some Rest API to provide data to the SPA app using the awesome Django REST Framework

This way helps me a lot, because it keeps the best of the three worlds without compromising productivity.



About this post

Please consider the following as my personal vademecum for setting up a Django+Svelte project quickly with basic usefules dependencies (such as whitenoise, black…) and authentication (which I haven’t found much covered online).

It is a concentrated boilerplate of my favourites tweaks on Django that made me proficient. Feel free to ignore those who don’t interests you. Any feedback is very welcome.

I’m using Svelte because is my favourite, but the approach should work for Vue and React, with obvious adjustments.

A medium Django/Python and Svelte knowledge is required.



The app



Prerequisites

My tools for the job are:



Setup

My setup is very similar to the one in the DRF quickstart, but I prefer poetry to pip

mkdir django-svelte-sample-app
cd django-svelte-sample-app



Django setup

For poetry init, go for the defaults and add the basic dependencies for our Django project after

poetry init
poetry add django whitenoise
poetry add --dev black --allow-prereleases

Set Django up (note the final dot)

poetry run django-admin startproject myapp .



Svelte setup

npx degit sveltejs/template frontend
cd frontend
npm install
cd ..



Setup

You should have the following structure:

$ find .
.
./frontend
./frontend/README.md
./frontend/rollup.config.js
./frontend/public
./frontend/public/index.html
./frontend/public/global.css
./frontend/public/favicon.png
./frontend/.gitignore
./frontend/package-lock.json
./frontend/package.json
./frontend/scripts
./frontend/scripts/setupTypeScript.js
./frontend/src
./frontend/src/App.svelte
./frontend/src/main.js
./pyproject.toml
./myapp
./myapp/asgi.py
./myapp/__init__.py
./myapp/settings.py
./myapp/urls.py
./myapp/wsgi.py
./manage.py
./poetry.lock
./.venv/....
./frontend/node_modules/...



Code: Django – settings.py

We will configure some settings for Django in order to:

  • enable Django Debug using Environment Variables
  • configure logging
  • read templates from a root folder (instead of subapps templates)
  • managing static files with Whitenoise

We will modify the myapp/settings.py file



Introducing DJANGO_DEBUG

This is very useful when working with Docker or Heroku and you want to dinamically turn on/off the Django debug mode.

Replace

DEBUG=True

with

import os
DEBUG = "DJANGO_DEBUG" in os.environ and os.environ["DJANGO_DEBUG"] == "ON"



Configure logging

I usually configure logging as the following:

logging_level = (
    "INFO" if "LOGGING_LEVEL" not in os.environ else os.environ["LOGGING_LEVEL"]
)

LOGGING = 
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": 
        "console": 
            "format": "[%(asctime)s][%(levelname)8s][%(name)16.16s]@[%(lineno)5s]$ %(message)s"
        ,
    ,
    "handlers": 
        "console": 
            "class": "logging.StreamHandler",
            "formatter": "console",
        ,
    ,
    "root": 
        "handlers": ["console"],
        "level": "WARNING",
        "propagate": False,
    ,
    "loggers": 
        "django.server": 
            "level": "WARNING",
            "handlers": ["console"],
            "propagate": False,
        ,
        "myapp": 
            "level": logging_level,
            "handlers": ["console"],
            "propagate": False,
        ,
    ,

You can now easily change the log level using the LOGGING_LEVEL env variable. Feel free to change the format as you prefer!



Django Template folder

I generally prefer to have all my .html templates inside a root level templates folder instead of every app having appname/templates/appname folder.

Modify the TEMPLATES variable from:

TEMPLATES = [
    
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": 
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        ,
    ,
]

to

TEMPLATES = [
    
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": ["myapp/templates"], # <-- here
        ...
    ,
]

and create the folder:

mkdir -p myapp/templates



Django static files

As mentioned, I really love Whitenoise for serving static files directly from Django. This helps me a lot during development and deploying because I can stay within Django.

We now create a staticfiles folder inside myapp, where files will be collected when we go to production, and the folder that will contain the compiled Svelte files, that will be served as Django static files (we will see it later)

mkdir -p myapp/static/frontend

We need to configure out static configurations accordingly:

STATIC_ROOT = os.path.join(BASE_DIR, "myapp", "staticfiles")
STATIC_URL = "/static/"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

STATICFILES_DIRS = (os.path.join(BASE_DIR, "myapp", "static"),)

And do not forget to add Whitenoise to the list of middlewares:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware", # <-- here
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    ...
]



Finish Django configuration

Let’s complete Django configuration creating applying the migrations and starting up the server, just to be sure no warnings came up

If you want to just check that everything is ok, you can run:

DJANGO_DEBUG=ON poetry run python manage.py makemigrations
DJANGO_DEBUG=ON poetry run python manage.py migrate
DJANGO_DEBUG=ON poetry run python manage.py runserver

You should see

Watching for file changes with StatReloader
[2021-12-09 18:31:03,519][    INFO][django.utils.aut]@[  643]$ Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
December 09, 2021 - 18:31:03
Django version 4.0, using settings 'myapp.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

So far, so good. We just instructed Django to do some useful stuff through standard configuration. Let’s add the Svelte part in order to see something.



Code: Svelte

For now, we will just expose the Svelte default app from Django.

The idea here is: we will serve the files from Django static/frontend dir and we will develop from Django (ie. localhost:8000) instead of localhost:5000 (default rollup dev server).

All we have to do is configure rollup to emit the build files in the static django folder.
Go to frontend/rollup.config.js and change the export default like this:

export default 
    input: 'src/main.js',
    output: 
        sourcemap: true,
        format: 'iife',
        name: 'app',
        file: '../myapp/static/frontend/bundle.js' // <-- here
    ,
    plugins: [
        ...
        !production && livereload('../myapp/static/frontend'), // <-- here
        ...
    ],
    ...

Move the frontend/public/ files to django

mv frontend/public/favicon.png myapp/static/frontend/
mv frontend/public/global.css myapp/static/frontend/

Now in a terminal starts rollup dev server

cd frontend
npm run dev

And you will see the bundle files created under the Django folder

$ ls myapp/static/frontend
bundle.css     bundle.js      bundle.js.map  favicon.png    global.css     index.html

We will leave the rollup dev server up so that we will have the hot reload while working on the Django side, so leave the server up and change terminal.

In order to serve it from Django, we need to render the template using a standard view



Code: Django – the SPA view

We now create a dedicated Django app that will just render the Svelte app

cd myapp
poetry run django-admin startapp spa
cd ..
mkdir -p myapp/templates/spa
mv frontend/public/index.html myapp/templates/spa/

Replace the content of myapp/spa/views.py with the following:

from django.views.generic import TemplateView

class SpaView(TemplateView):
    template_name = "spa/index.html"

Modify the myapp/templates/spa/index.html using static resources from the frontend

% load static %

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>

    <title>Svelte app</title>

    <link rel='icon' type='image/png' href="% static 'frontend/favicon.png' %"> <!-- <-- here -->
    <link rel='stylesheet' href="% static 'frontend/global.css' %"> <!-- <-- here -->
    <link rel='stylesheet' href="% static 'frontend/bundle.css' %"> <!-- <-- here -->

    <script defer src="% static 'frontend/bundle.js' %"></script> <!-- <-- here -->
</head>

<body>
</body>
</html>

Add the view in the myapps/urls.py

# myapps/urls.py
from django.contrib import admin
from django.urls import path

from myapp.spa.views import SpaView  # <-- here

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", SpaView.as_view(), name="spa"),  # <-- here
]

and finally, in order to add it in the installed apps, we need to:

  • modify myapp/spa/apps.py
class SpaConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "myapp.spa" # <-- here
  • add myapp.spa in the INSTALLED_APPS in myapp/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    ...
    "django.contrib.staticfiles",
    "myapp.spa", # <-- here
]



Test it

From the root folder of the project (ie the one with the frontend and myapp folders inside), run in two different terminal windows:

# 1st shell - svelte
cd frontend
npm run dev
# 2nd shell - django
DJANGO_DEBUG=ON poetry run python manage.py runserver

Go to localhost:8000 and you will see the Svelte app served directly from Django.

Try changing frontend/src/App.svelte or frontend/src/main.js, hit Ctrl+R and you will see it hot reloaded (from Django. Remember to disable cache or force the browser to refresh the page if you don’t see any change).

Don’t forget that you need the “npm run dev” in background in order to keep sync the bundle.* files under Django static folder.



Secure our SPA the Django way

Now we can leverage the standard Django features, such as authentication.

First, secure our View extending the LoginRequiredMixin. In myapp/spa/views.py

from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin


class SpaView(LoginRequiredMixin, TemplateView):
    template_name = "spa/index.html"

To enable auth, add in your myapp/settings.py

from django.urls import reverse_lazy

LOGIN_URL = reverse_lazy("login")
LOGIN_REDIRECT_URL = reverse_lazy("spa")
LOGOUT_REDIRECT_URL = reverse_lazy("spa")

and in myapps/urls.py

from django.contrib import admin
from django.urls import path, include # <-- here

from myapp.spa.views import SpaView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")), # <-- here
    path("", SpaView.as_view(), name="spa"),
]

Create the login form template:

mkdir -p myapp/templates/registration
touch myapp/templates/registration/login.html

Add the following to myapp/templates/registration/login.html (taken from the django documentation)

<html>

<head></head>

<body>

    % if form.errors %
    <p>Your username and password didn't match. Please try again.</p>
    % endif %

    % if next %
    % if user.is_authenticated %
    <p>Your account doesn't have access to this page. To proceed,
        please login with an account that has access.</p>
    % else %
    <p>Please login to see this page.</p>
    % endif %
    % endif %

    <form method="post" action="% url 'login' %">
        % csrf_token %
        <table>
            <tr>
                <td> form.username.label_tag </td>
                <td> form.username </td>
            </tr>
            <tr>
                <td> form.password.label_tag </td>
                <td> form.password </td>
            </tr>
        </table>

        <input type="submit" value="login">
        <input type="hidden" name="next" value=" next ">
    </form>

    # Assumes you set up the password_reset view in your URLconf #
    <p><a href="% url 'password_reset' %">Lost password?</a></p>
</body>

</html>

Create a super user running in a terminal

poetry run python manage.py createsuperuser

Now if you try to hit localhost:8000, you will be redirected to http://localhost:8000/accounts/login/?next=/.

Try to login with the provided credentials and you will see the Svelte app again.

Hit localhost:8000/accounts/logout to logout.



Rest API

Now we will add a very simple REST API using Django Rest Framework and consume it from the Svelte App.

Note that our Svelte SPA app will leverage DRF SessionAuthentication, since it will invoke the API in the same context of the Django app. This is the most secure way, in my opinion

First, install DRF

poetry add djangorestframework

In addition, since Django 4.0 has just been released, we need to install pytz, since DRF still uses it

poetry add pytz

and add it in myapp/settings.py INSTALLED_APPS

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'rest_framework', # <-- here
    "myapp.spa",
]

Create a new Django app for the API

cd myapp
poetry run django-admin startapp api
cd ..

Add the mock api in myapp/api/views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from rest_framework.renderers import JSONRenderer


class GreetingApi(APIView):
    authentication_classes = [authentication.SessionAuthentication]
    permission_classes = [permissions.IsAuthenticated]

    renderer_classes = [JSONRenderer]

    def get(self, request, format=None):
        return Response("message": "Hello world")

Note that:

  • we are using the default Django Session Authentication (via login form)
  • the API is available only for authenticated users

Now adjust frontend/src/App.svelte

<script>
    import  onMount  from "svelte";

    export let name;

    let apimessage = "Waiting for server...";

    onMount(async () => 
        let resp = await fetch("/api/greet").then((res) => res.json());
        console.log(resp);
        apimessage = JSON.stringify(resp);
    );
</script>

<main>
    <h1>Hello name!</h1>
    <p>
        Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn
        how to build Svelte apps.
    </p>

    <h3>Data from server</h3>
    apimessage
</main>

Add the API to myapp/urls.py

from django.contrib import admin
from django.urls import path, include

from myapp.spa.views import SpaView
from myapp.api.views import GreetingApi  # <-- here

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
    path("api/greet", GreetingApi.as_view()),  # <-- here
    path("", SpaView.as_view(), name="spa"),
]

Hit localhost:8000, login and enjoy your Django based SPA using Server Side Authentication and Rest API



Building for Production

In order to make our app ready to Production, we need:

  1. Gunicorn installed
poetry add gunicorn
  1. collect all static files
poetry run python manage.py collectstatic
  1. configure the allowed hosts
#myapp/settings.py
# change ALLOWED_HOSTS = [] to
ALLOWED_HOSTS = ["localhost"]

Now you are ready to run

poetry run gunicorn myapp.wsgi

Go to localhost:8000 and enjoy your gunicorn served app



Conclusions

In this quite long walkthrough, we saw different things:

  1. set up a Django app with my personal useful tweaks
  2. set up an integrated Svelte app served by Django
  3. configure Django built-in authentication to secure our SPA
  4. providing some Rest API for the SPA to consume
  5. packing everything up for Production

You can find all the code here on github.

I hope this post can help you make your life as developer easier. I’m going to add other posts integrating Docker/Docker compose and some utility scripts for local development.

Feel free to comment, I’d be glad to receive any feedback your have.


Source link

Inertia

I’ve always like Jeremy’s categorization of developer tools:

I’ve mentioned two categories of tools for web development. I still don’t know quite what to call these categories. Internal and external? Developer-facing and user-facing?

The first category covers things like build tools, version control, transpilers, pre-processers, and linters. These are tools that live on your machine—or on the server—taking what you’ve written and transforming it into the raw materials of the web: HTML, CSS, and JavaScript.

The second category of tools are those that are made of the raw materials of the web: CSS frameworks and JavaScript libraries.

It’s a good way to think about things. There is nuance though, naturally. Sass is the first category since Sass never goes to users, it only makes CSS that goes to users. But it can still affect users because it could make CSS that is larger or smaller based on how you use it.

Jeremy mentions Svelte as a library where the goal is essentially compiling as much of itself away as it can before code goes to users. Some JavaScript is still there, but it doesn’t include the overhead of a developer-facing API. The nuance here is that Svelte can be used in such a way that all JavaScript is removed entirely. For example, SvelteKit can turn off it’s hydration entirely and do pre-rendering of pages, making a site that entirely JavaScript-free (or at least only opting in to it where you ask for it).

On React:

I know there are ways of getting React to behave more like a category one tool, but it is most definitely not the default behaviour. And default behaviour really, really matters. For React, the default behaviour is to assume all the code you write—and the tool you use to write it—will be sent over the wire to end users.

I think that’s fair to say, but it also seems like the story is slowly starting to change. I would think widespread usage is far off, but Server Components seem notable here because they are coming from the React team itself, just like SvelteKit is from the Svelte team itself.

And on Astro:

[…] unlike Svelte, Astro allows you to use the same syntax as the incumbent, React. So if you’ve learned React—because that’s what you needed to learn to get a job—you don’t have to learn a new syntax in order to use Astro.

I know you probably can’t take an existing React site and convert it to Astro with the flip of a switch, but at least there’s a clear upgrade path.

This isn’t just theoretically true, it’s demonstrably true!

I just converted our little serverless microsite from Gatsby to Astro. Gastby is React-based, so all the componentry is already built as React components. The Pull Request is messy but it’s here. I converted some of it to .astro files, but left a lot of the componentry largely untouched as .jsx React components. But React does not ship on the site to users. JavaScript is almost entirely removed from the site, save for some hand-written vanilla JavaScript for very light interactivity.

So there is some coin-flipping stuff happening here. Coin merging? Astro to me feels very much like a developer-facing tool. It helps me. It uses the Vite compiler and is super fast and pleasant to work with (Astro has rough edges, for sure, as it’s pre 1.0, but the DX is largely there). It scopes my styles. It lets me write SCSS. It lets me write components (in many different frameworks). But it also helps the user here. No more JavaScript bundle on the site at all.

I guess that means Astro doesn’t change the categories—it’s a developer-facing tool. It just happens to take what would be a user-facing tool (even Svelte) and makes them almost entirely developer-facing.


And just because I’ve had a couple of other Astro links burning a hole in my pocket, Flavio has a good intro tutorial and here’s Drew McLellan and Matthew Phillips chatting Astro on a recent Smashing Podcast.


Source link

Localize (i18n) your Svelte App with Tolgee. You will save a lot of time.

Localization is tricky part of application development. Using Tolgee open-source tool you can save a lot of time.



Tolgee Features

  • All in One localization solution for your Svelte application 🙌
  • Out of box in-context localization 🎉
  • Automated screenshot generation 📷
  • Translation management platform 🎈
  • Open-source and self-hosted 🔥



Svelte integration

We have just implemented and documented Svelte integration, so now you can translate your Svelte application in dead simple way.



Get started on Tolgee for Svelte! 🎉



Or check out Svelte + Tolgee Example app

We will be happy for any feedback! 🙏

Like it? 😊 Please star our projects on Github. It would help a lot 🙏

Disclaimer: I am founder of Tolgee.


Source link

Introducing Svelte, and Comparing Svelte with React and Vue

Josh Collingsworth is clearly a big fan of Svelte, so while this is a fun and useful comparison article, it’s here to crown Svelte the winner all the way through.

A few things I find compelling:

One of the things I like most about Svelte is its HTML-first philosophy. With few exceptions, Svelte code is entirely browser-readable HTML and JavaScript. In fact, technically, you could call Svelte code as a small superset of HTML.

And:

Svelte is reactive by default. This means when a variable is reassigned, every place it’s used or referenced also updates automatically. (React and Vue both require you to explicitly initialize reactive variables.)

I do find the component format nice to look at, like how you just write HTML. You don’t even need a <template> around it, or to return anything. I imagine Astro took inspiration from this in how you can also just chuck a <style> tag in there and scope styles if you want. But I think I prefer how the “fenced” JavaScript at the top only runs during the build by default.


P.S. I really like Josh’s header/footer random square motif so I tried to reverse engineer it:

To Shared Link — Permalink on CSS-Tricks


The post Introducing Svelte, and Comparing Svelte with React and Vue appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


Source link

How To Prevent Theme Colour From Flickering in Svelte Or SvelteKit

Ever since I started using Svelte, my theme colours has always been flickering on page load. Finally I found a workaround



Problem

At first, I was changing my website’s theme colour inside the onMount function but the theme colours were always flickering. The reason was that, the body loads before the onMount function sets the theme colour.
Here’s a gif showing the problem
How the screen was flickering before I found a solution

So as you can see, when the page is reloaded, the light theme is first shown, then after the document has finished loading, the onMount will then set the theme colour



Solution

The only probable solution is to set the theme colour before the body has even loaded. How do we do that❓, we insert a script tag inside the head element and this code will run before the body is loaded. That’s the best way we can prevent the flickering of colours.



The Code

Okay, so you can write this code in the component in which you use to toggle in-between the themes.

<svelte:head>
  <script>
    if (document) (window.matchMedia('(prefers-color-scheme: dark)').matches))
        document.documentElement.classList.add("dark");
        localStorage.theme = "dark";
      
      else 
        document.documentElement.classList.remove("dark");
        localStorage.theme = "light";
      
    
  </script>
</svelte:head>



Explanation

In order to access the head element, we used the <svelte:head> component. Then we created the script tag just as we would on our normal HTML pages. The next statements are the important ones, the reason why we used if (document) is that, this code first gets evaluated on the server before being rendered on the client, so if you try to access document on the server, it will pop up an error.
I know SvelteKit provides the browser constant through the $app/env module but mind you, this is not available in the custom script tag we made, you will need to use your own workaround and that’s why we are using document to check.
Then on the next line, we try to retrieve the theme from the localStorage, if it’s not set, it defaults to “light” theme;
Then the next steps are the addition of classes and setting of the theme in the localStorage.

Now look at how the page loads without flickering
Now the page loads without flickering

Happy Coding!😄


Source link