{ "version": "https://jsonfeed.org/version/1", "title": "griffa.dev", "home_page_url": "https://griffa.dev", "feed_url": "https://griffa.dev/feed/feed.json", "description": "Tutorials and blog posts on web technologies.", "author": { "name": "George Griffiths", "url": "https://griffa.dev/about" }, "items": [{ "id": "https://griffa.dev/posts/getting-into-coding-how-to-get-started-(2023)/", "url": "https://griffa.dev/posts/getting-into-coding-how-to-get-started-(2023)/", "title": "Getting into Coding - How to get started (2023)", "tags": ["Learning","Programming"], "summary": "A collection of stories, career paths, links and resources that I have collated from colleagues and my own experiences to help those who ask me how to get into programming.", "content_html": "
If I could sum up this article series in short it would be: To get into programming:
\nIf you want to skip the waffle and just get links to stuff, skip straight to part 3: Resources.
\nPart 2 contains a description of different career paths: Getting into Coding - Career paths.
\n\n\nJust a heads up, my writing style is very informal, I like to write as if i'm just having a chat.
\n
I'm currently employed as a Lead Frontend Developer at Immersive Labs, It's my 3rd Software engineering role, previously at IBM and IDBS (I only work at companies beginning with I apparently), hopefully some of the information below might be helpful in getting started in the industry. Whilst I work as as Frontend developer right now I get just as much out of writing any piece of code, it all comes down to enjoying problem solving.
\nBy problem solving I also don't mean complex maths problems, that can be part of it if you want it to be, but programming does not mean maths, unless you want it to.
In high school I enjoyed building out crappy websites, they were poorly designed and poorly implemented but I tinkered with HTML and pasted snippets of JavaScript and JQuery that I didn't understand into the websites. But my "official" intro to programming was in 2012 when I started a Computer Science degree. This was a great intro into the wider world of software development. This introduced me to programming fundamentals, I was learning about Data structures and algorithms and learning how to build software in Java.
\nA degree is by no means the only way into programming, and to be honest it's probably not even the best way. I learnt most of my useful skills on the job or self taught, my degree just gave me structure.
\nWhat's funny about my degree is, it wasn't part of a big plan, I didn't even really know what Computer Science was, I liked playing games and messing about on a computer. But after a few assignments I got hooked on programming. During the first few months I had no idea what I was doing, when you are learning the low level programming concepts it's hard to imagine how adding numbers together or describing two animals and printing the noises they make to the screen can get you to building complex software, but it can, it's all about building knowledge on-top of existing knowledge.
\nDuring my degree when we were learning about web technologies I had a bit of a step up on others because of all the tinkering I'd done when I was younger. This has made me realize that I think the most important thing for being successful in programming is finding something that you enjoy and finding problems to solve that are interesting to you (the you is important i'll get back to this).
\nIn this article series I'll discuss some of the different paths into programming from stories i've collated from colleagues, as well as my own experiences.
\n\n\nDoes it matter what language I learn first? No not really, most programming concepts are entirely applicable regardless of the language. In particular the fundamentals. Learning the second language once you have some experience and projects under your belt is easier!
\n
You'll see programming languages be discussed as high
or low
level:
low
: Typically lends itself to more experienced engineers or those which want to grasp the pure fundamentals of how a piece of software ends up running on a piece of hardware. You have to think a lot more about managing memory allocation.
high
: Languages that are more typically taught to beginners to programming, these can massively range too in their ability to expose low level programming concepts.
High level languages examples: JavaScript, C#, Java, python, ruby, go.
\nLow level languages examples: C, Assembly.
\n\n\nBecause programmers can never agree on anything you will find that what some people consider a low level language is high level to another.
\n
Languages that sort of sit in the middle: C++, Rust.
\nProgramming languages also can be broadly split into another two camps: compiled
vs interpreted languages
.
A compiled language requires a compiler than converts the "code that you write" into another format before it can be executed and run.
\nIn an interpreted language source code the developer writes can be executed directly. There is a little more to it as the code still ultimately does get transformed but it happens at runtime "just in time".
\nCompiled languages: C++, Java, Rust, C#
\nInterpreted languages: JavaScript, ruby, python, PHP
\nWhen getting started for the first time you might find it easier to do some tutorials on Interpreted languages to begin with. However, with the quality of online tutorials out there even with a compiled language it might be a while before you even have to work with a compiler, so just pick something!
\nPython is one of the most prolific "first time" languages.
\nBut equally Java was my first programming language that I was taught, which is compiled, which was also a good learning experience, but compilers do complicate things a little, potential risk for getting overwhelmed initially.
\nThere is a third way to categorize programming languages static
vs dynamic
typing. It is a little too much to go into now, but some examples: Java
has static types whilst python and JavaScript has dynamic
types. One of the reasons Java can be a great first language is that static types can really help you understand programming fundamentals and help with debugging errors. Doing a mix of both to see the differences relatively early on might be a nice idea.
JavaScript also has a wealth of programming tutorials out there.
\nIf you want to understand some context into what you might use a language for in a job check out part 2
\nIf you are keen to dive into links for getting started, remember part 3: Resources is all that!
\nThere are a few things which are more or less universal in programming which gaining an understanding of at some point is a good idea.
\nYou don't have to do this first, in fact I'd probably come back to learning these, once you've cut your teeth on some initial tutorials or projects. But worth a note down at least.
\nIDE
: An IDE (Integrated development environment) is a fancy name for a code editor. I recommend VS Code. Some languages benefit from more specialized tooling though. Many coding education sites will embed IDE's into them so you don't have to download any software to learn.Version control
: Ever lost some work because you deleted it or your hard drive got corrupted? This is why we use version control software to write code. It's also what allows developers to collaborate and review each others code.Git
: Git is an implementation of version control, there is (usually) a main
branch of code, and developers checkout their own version into a branch
, make changes and merge their changes back into main
. Git is the defacto standard and the only one worth learning about. Find some tutorials when the time comes!GitHub
: GitHub is a website that hosts Git repositories that contain code. Git without a remote server for hosting the code wouldn't be that useful! Get an account and learn how it works at some point, it's free for most uses.Programmers can be very opinionated, you will often see takes like "this is the best way to do something", "X is bad", like everything, there are tradeoffs with all things, when you are getting started try not to get too hung up on doing things in one way because someone told you that's is the best way.
\nWhilst there are best practices, Experiment and try things out. The best way to learn "Best practices" is just through coding more and exposing yourself to different kinds of problems.
\n\n\nAlthough, if you see code examples where they name variables with a single letter all over the place, this is bad code. Your code should be descriptive, if only so that future you can understand WTF it's doing. Also write code comments for complex things.
\n
Earlier I mentioned finding something interesting to you, this is important I think, not everyone likes the same things, some people love to build games, some like websites, some like servers, some like working with data, some like managing servers and some like making tools to make other developers lives easier. When starting out it might be a good idea to try a few things out.
\nIn general it might be good to:
\nOver the years I've "found the fun" in some unexpected places as I've worked on more problems. For example I really love Testing, of all kinds. Testing is a whole huge discipline within programming, not one to get into now but every programmer eventually will run into it.
\nIf you want a nice intro some example problems that you could learn how to solve check out Automate the Boring Stuff with Python, it'll teach you some Python and give you a feel for programming.
\nIf you go for this it might be an idea to pair that book with some interactive learning on Free Code Camp.
I talked about my degree earlier in the article, there are so many resources out there now that you could quite happily build yourself an extensive self-guided curriculum. It would also have the benefit of being more up to date than many university curriculums.
\n\n\nThis sounds like a lot of work but there are many resources out there, i'll link a few in part 3.
\n
In a computer science degree you will general find that modules fall into two camps "theoretical" "practical".
\nExamples:
\nWhen learning to code I recommend planning out things to learn that fit into practical vs theoretical and try to introduce yourself to different things and see what you take a liking to. If something isn't working for you, try something else. I will say the first few tutorials might get overwhelming quickly. I remember the summer before uni trying to self-teach myself Java from a book and not really understanding anything. If it's not clicking it equally might be a shit tutorial, find what works for you, books do not work for me.
\nSometimes you can hit a wall, just like you might at school, and you can divert your attention to something else instead and loop back around to the thing that is causing you trouble. You will also be flexing different muscles, creating a project from scratch is very different from learning computer science theory or implementing an algorithm.
\nTry not to take on too much though, it's a fine balance between being overwhelmed and missing out on breadth of knowledge. Branch out once you start to feel like you're not sure what to do next, or you feel you have hit a roadblock in your learning, try something different.
\nFor example; stuck building a website and it's not going well? why not try to build a command line app or a server, its easier than it sounds to get something up a running!
\nCheck out part 3 for some project ideas.
\nMuch like degrees, bootcamps and paid for courses are not a hard requirement, there are many developers that have landed jobs by purely self guided learning. For some people the structure is great, for others not so much.
\nIf you are considering doing a Bootcamp or paid course I would strongly recommend self guided learning first to be sure that you enjoy coding before making an monetary commitments. The resources in part 3 of this series should help with that.
\nA colleague of mine has an article similar to this one that is a treasure trove of information and insights, whilst some of the technology learnt may not be applicable to everyone the key points made are still very relevant: How I landed a web development role with no formal experience.
\nAnother colleague of mine recently did a fantastic talk on her experiences of getting a job as a programmer, here is a YouTube video of that talk, I highly recommend it!
\nWhen it comes to degrees, you can always do one later, many places can offer part time degrees if you feel like that's a route you want to go down once you're in your first few years of working in industry.
\nThere is always more to learn and imposter syndrome is a real thing in tech, at every level, you'll get stuck, but try to find other things to focus on instead, programming is like any skill/art, like learning an instrument, it takes time and a bit of passion.
\nNext up let's take a look at some different career paths within programming and how those fit together with specific programming languages.
\n", "date_published": "2023-03-17T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/getting-into-coding-resources/", "url": "https://griffa.dev/posts/getting-into-coding-resources/", "title": "Getting into Coding - Resources", "tags": ["Learning","Programming"], "summary": "A long list of curated resources that I've sourced from my experiences and from my colleagues.", "content_html": "If you haven't checked out part 1 and 2 of this series I'd recommend it for some context. This article is a big brain dump of collated links by myself and colleagues.
\nCode Academy or Free Code Camp are pretty good starting points.
\n\nPython is just an example on Free Code Camp, they offer many languages but it might be a good starting point.
\nSome other cool things i've had recommended by colleagues.
\nThis site has opinionated and extensive Roadmaps for different developer roles, beware overwhelming information!
\n\nBeing a Frontend developer I'm asked from time to time about resources for that specifically,
\nhere is are recent article list:
Here are some bootcamps my colleagues have been on.
\n\nAmazing resource:
\n\nSome high level ideas:
\nSome of these are pretty menacing.
\n\nYouTube and Twitch have thriving tech communities.
\nIf you want to skip the waffle and just get links to stuff, skip straight to part 3: Resources.
\nThere are many different roles/job titles within programming here are a few along with the general thing they do.
\nThere isn't an exhaustive list, just some of the roles that came to my head.
\n\nIs a Software Engineer the same thing as a Developer? Yes, anyone that says otherwise is chattin' shit.
\n
In this section I'll discuss some different disciplines and some languages which tend to be used for these jobs.
\nThis might help with ideas for what to learn first.
When you start out you don't need to think about will learning X get me a job?
. Start out with finding what you find fun in programming. That being said, in order to do that, I think it's useful to get a general idea of the different tools/tech that might be used in different jobs.
HTML
, CSS
and JavaScript
. May also do some design work. You will see lots of talk about React
and other web frameworks. too in this area. You may come across TypeScript, this a big topic, it's "safer" JavaScript, doesn't run directly in the browser though has to be compiled.Java
, Python
, Ruby
, PHP
, Go
, Rust
, C#
. You may come across the term APIs a lot. May work with databases using SQL
or NOSQL
Database tech.\n\n\nFor JavaScript you may come across Node.JS, this a tool for running JavaScript on the Backend e.g. servers or command line apps. So you can use JavaScript for an entire application if you wanted, making it a good potential language to learn. But don't write off everything else.
\n
Python
, Go
, Kubernetes
, Terraform
, Continuous Integration
.AWS
.SQL
.I realise that was a hell of a lot of jargon, most of it doesn't matter, any of these programming languages would be useful when it comes to building websites/apps:
\nThese were very focused on web apps because those are the most common these days, outside of mobile apps. You will find that even some desktop apps use web technologies such as Discord and Slack.
\nIn general, as someone new to programming it's probably a good idea to start out looking to Frontend and Backend development.
\nBut there is a shortage of people with the skills to do Cloud Engineering/Dev Ops well.
If you are going to learn Frontend tech i'd recommend striking a nice balance of foundations in HTML
, CSS
and JavaScript
and also learning modern techniques for building Web Apps, such as using React
.
I'll confess I don't know much about this industry but it would be building tools in low level programming such as C
or Rust
, or maintaining super old software that never breaks or changes in old languages. Aim here is usually to have really fast or really robust code.
I also don't know much here but I think starting with web technologies is still a good starting point for learning games programming.
\nI hate to say it but it's true, games dev jobs are generally paid less well partly due to supply/demand and also partly due to companies over-exploiting passion.
\nC#
C++
JavaScript
, Java
, python
etc but generally not as common.Whilst Unity and Unreal are more production ready solutions, if you are wanting to learn to code and do something fun writing a game in any language is a perfectly good thing to do, there will be a tutorial out there in every language!
\nPython
, they all use python
. Some C++
if you need to make the code super fast, there is some cross over with Systems Engineers here.
\nYou may also find that a lot of Data engineers work with SQL
databases, building queries for complex data models, or for fetching data to train AI models.
There is a lot of cross over here with building for the web, in fact there are some tools which just use web technology to build mobile apps.
\nIn general though
Swift/C#
Android: Java/Kotlin
You can also use JavaScript
to build mobile apps, this is typically by using a framework like React Native
The tech market in 2023 is definitely un-certain, but I don't think this should stop anyone from trying to get into the industry, it might be harder to find a company willing to take a risk on developers without any commercial experience, but equally, when you do find ones are that are, it's likely that they will do so because they want to invest in you and your learning.
\nOnce you've started to gain confidence in your abilities and have created a few projects, it's never too early to start hunting (and applying) for entry level positions, typically these will be labelled as "Junior" or "Apprentice". There are also grad schemes from larger companies but many of the large tech companies are still stuck in the past with requiring university degrees, albeit not always Computer Science ones.
\nI was lucky enough during my degree to have a year in industry where I worked at IDBS (who I stayed with for 4 years!). I can attribute the majority of my knowledge to this first job. This year in industry was very much like an apprenticeship, and the good news is apprenticeship schemes are becoming more common, in particular in the UK, so definitely search for those.
\nFinally let's take a look in the next section a big list of resource i've gathered for where to go next.
\n", "date_published": "2023-03-17T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/how-i-learn-in-2023/", "url": "https://griffa.dev/posts/how-i-learn-in-2023/", "title": "How I learn in 2023", "tags": ["JavaScript","HTML","CSS","Learning"], "summary": "A collection of links and resources that I use or have used in the past to help me learn", "content_html": "Sometimes I get asked about places where you can go to learn about different topics. This post is my attempt to collate some of the resource that I've found helpful in my journey through software engineering. Some of these might also just be tools that I've used or I think are cool.
\nThese links aren't the full extent of the things I've been learning. I think the best way to learn new things is to be inquisitive on the job and try to get exposure to different aspects of software not just Frontend or Backend alone. Don't be afraid to dive in head first and just experiment and ask questions!
\n\n\nI may continue to update this list over time.
\n
There are so many tools and resources out there that It can be hard to keep up, here's some of the things that I do.
\nSome of this falls into discovering new stuff too.
\nTwitter used to be great for developer stuff but many people have left twitter after Elon.
\nMy follow list might give you some starting points, but not all are tech, it's not curated, but bio's should help with finding more stuff. https://twitter.com/griffadev/following
\n", "date_published": "2023-01-31T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/how-trip-yourself-up-with-react-hooks-and-break-production./", "url": "https://griffa.dev/posts/how-trip-yourself-up-with-react-hooks-and-break-production./", "title": "How trip yourself up with React hooks and break production.", "tags": ["JavaScript","React"], "summary": "Recently I started working on a new React codebase, here is a story of how I "corrected" some React hooks code and broke a feature in production, and then went on vacation.", "content_html": "React is so ubiquitous now it's pretty difficult to avoid it if you want to be employed and working in Web application development. React hooks in some ways pushed the ecosystem forward but in other ways have made React applications harder to debug and easier to subtly break things without realising, if you stop following the rules.
\nIn this article is a story and some lessons about how I "corrected" some React code to follow the "rules" of hooks and broke the feature because the existing code happened to violate some of the other React "rules", in a way that was not immediately obvious.
\nWhenever I write a React article I question whether or not it is a good idea to do so because with its popularity can come some unhelpful comments and responses. To get this out of the way early here some replies on some previous comments I've received on React articles:
\nAnyways... to the article, I promise the above is the only sarcastic/cynical part of the post and I think that there is some useful learning in this story to help me grow and hopefully others can find it useful to when working with the React framework.
\nThe codebase that I work on is not open source, however the issue that I ran into could be extracted distilled into some example code as the core problems with the code centre around using some of the built in React hooks such as 'useMemo' and handling the mutation of state.
\nTo demonstrate the problem I developed a completely unoriginal Todo-list example, hopefully the mundaneness of the code will help to skip over the boring bits and get to the issues with the code more quickly!
\nTo demonstrate the problem with the code I have developed 3 examples all implementing the same Todo application:
\n\n\nFull code examples will be available at the end of each section.
\n
In order to reproduce the bug I experienced in the other codebase I needed the following:
\nThe implementation of the TodoList isn't too important, I scaffolded it out using vite. The important aspects of the code in this article live inside of a custom Context provider that implements the functionality of the TodoList.
\nHere is the outline of the top level App
component:
import React from \"react\";
import { AddTodo } from \"./AddTodo\";
import \"./App.css\";
import { SaveMessage } from \"./SaveMessage\";
import { SaveTodos } from \"./SaveTodos\";
import { TodoProvider } from \"./TodoContext\";
import { TodoList } from \"./TodoList\";
function App() {
return (
<TodoProvider>
<div className=\"App\">
<div className=\"Flex\">
<h1>Todo list</h1>
<SaveTodos />
</div>
<SaveMessage />
<AddTodo />
<div>
<h2>Items to do</h2>
<TodoList />
</div>
</div>
</TodoProvider>
);
}
export default App;
\nThe TodoProvider
is where we will focus on this article, it allows me to implement the logic for adding/deleting and saving todo items and share this between components.
Each of the components use aspects of the context provided by TodoProvider
, for example, here is how the AddTodo
component accesses the functionality to add a todo item:
import { useTodos } from \"./TodoContext\";
export const AddTodo = () => {
const { addTodo } = useTodos();
return (
<form
onSubmit={(e) => {
e.preventDefault();
const formEntries = new FormData(e.target);
addTodo(formEntries.get(\"message\"));
}}
>
<input
className=\"Input SearchBox\"
name=\"message\"
placeholder=\"New item...\"
id=\"addItem\"
type=\"text\"
/>
<button className=\"Button\" type=\"submit\">
<span role=\"img\" aria-label=\"Add item\">
➕
</span>
</button>
</form>
);
};
\nI'll add in full code sandbox links at the end of each section.
\n\n\nRemember, this code is not the actual code from the codebase that I work on but is an example distilled from the issue that I came across.
\n
The first thing that the TodoProvider
does is fetch some data from the "server", in this case I've hardcoded a json file with some todo items in it.
{
\"todos\": [
{
\"id\": 1,
\"message\": \"Go to the supermarket\",
\"done\": false
},
{
\"id\": 2,
\"message\": \"Mow the lawn\",
\"done\": true
},
{
\"id\": 3,
\"message\": \"Clean the kitchen\",
\"done\": true
},
{
\"id\": 4,
\"message\": \"Book restaurant reservation\",
\"done\": false
}
]
}
\nBelow I setup a context and fetch the JSON. It might seem odd to you that I am setting the API response into useState
, this is a little contrived and just for the demo, before I get any "your code sucks comments". In the case of the real codebase this would have been a graphql useQuery
hook.
There is a good reason why I am storing the response and not the the TODOs themselves, because I need to do a transform on the data and this is the closest way I could mimic was the original code was doing without introducing some fancy http client hook like react-query.
\nexport const TodoContext = createContext();
export const TodoProvider = ({ children }) => {
const [apiResponse, setApiResponse] = useState(undefined);
const [draftTodos, setTodoList] = useState();
useEffect(() => {
const fetchTodos = async () => {
const res = await fetch(\"./todos.json\");
const response = await res.json();
setApiResponse(response);
};
fetchTodos();
}, []);
// Transforming the data, use of useCallback looks odd here...
// We'll get to that!
const existingTodos = useCallback(() => {
const todoMap = new Map();
apiResponse?.todos.forEach((todo) => {
todoMap.set(todo.id, todo);
});
return todoMap;
}, [apiResponse]);
return return (
<TodoContext.Provider value={{
/* code coming soon */
}}>
{children}
</TodoContext.Provider>
}
\nThe second part of the code uses useCallback
to create a function that converts the array of items into a map of todo items where the key is the id e.g.
{
1: {
\"id\": 1,
\"message\": \"Go to the supermarket\",
\"done\": false
},
2: {
\"id\": 2,
\"message\": \"Mow the lawn\",
\"done\": true
},
3: {
\"id\": 3,
\"message\": \"Clean the kitchen\",
\"done\": true
},
4: {
\"id\": 4,
\"message\": \"Book restaurant reservation\",
\"done\": false
}
}
\nIf you think the use of useCallback
here is strange and useMemo
would make sense, we both had the same thought and I wonder if you would end up introducing the same bug that I did by refactoring and correcting the code!
Now we're fetching Todo Items we want a way of adding new items. Before we do that I'm going to introduce a new concept to the above implementation, the notion of "draft todos", these are modified or new todo items which have not been saved back to the server just yet. To make that happen I add in:
\nexport const TodoProvider = ({ children }) => {
// New line here!
const [draftTodos, setTodoList] = useState();
const [apiResponse, setApiResponse] = useState(undefined);
useEffect(() => {
const fetchTodos = async () => {
const res = await fetch(\"./todos.json\");
const response = await res.json();
setApiResponse(response);
};
fetchTodos();
}, []);
const existingTodos = useCallback(() => {
const todoMap = new Map();
apiResponse?.todos.forEach((todo) => {
todoMap.set(todo.id, todo);
});
return todoMap;
}, [apiResponse]);
// Other new line!
useEffect(() => {
// pass in initial items from server
if (!draftTodos && existingTodos().size) {
// this () is strange because useCallback was used
setTodoList(existingTodos());
}
}, [existingTodos]);
return return (
<TodoContext.Provider value={{
todoItems: draftTodos ? Array.from(draftTodos.values()) : [],
}}>
{children}
</TodoContext.Provider>
}
\nThe purpose of the useEffect
is so that on initialisation the draft todos equal the existing todo items.
In the new lines, if it wasn't clear before, hopefully it would be now that useCallback here is quite strange indeed because in order to read the existing you need to execute the existingTodos
as a function.
\n\nThe keen eyed here will notice that this means a new map is being created each time you call the function, resetting the data back to the initial state from the json file. This also means the reference to the
\nMap
is different each time meaning unless you pass the output of the function around you aren't comparing the same map! This is key to the issue later on whenuseMemo
is used instead!
These next two lines are more or less the existing code which was in place to add or remove items, and were exported onto the context.
\n<TodoContext.Provider
value={{
todoItems: draftTodos ? Array.from(draftTodos.values()) : [],
removeTodo: (id) => {
if (draftTodos.delete(id)) {
setTodoList(new Map(draftTodos));
}
},
addTodo: (message) => {
if (!message) {
return;
}
const todo = {
// new web api! - Support gradually increasing
id: crypto.randomUUID(),
message,
done: false,
};
if (draftTodos.has(todo.id)) return;
draftTodos.set(todo.id, todo);
setTodoList(new Map(draftTodos));
},
}}
>
{children}
</TodoContext.Provider>
\nThese code examples also looked a little off to me too, but I couldn't quite understand why or put two and two together, at first. What the code is doing:
\nThere was in fact a comment next to the new Map()
line which mentioned that this is needed in order to cause React to update, but it hadn't quite clicked with me that this was a bit of a problem.
If you hadn't noticed already, the lines above were mutating the previous map in both the add and remove methods, and then making a copy of it, polluting the previous data. Without any other changes this might not be a big deal at all, a bit bad in terms of best practice of not mutating objects in React, especially those used as state, but it works, but now let's take a look at the code for saving the new items.
\nThe code below is quite contrived and is missing the context as to why it was needed. But what the code does is check if the data had changed before sending it, it was a bit of an optimisation and there were other reasons too, but that's not important, let's take a look.
\n<TodoContext.Provider
value={{
save: () => {
// contrived code for the demonstration
// in the real app this was responsible for deciding if a request should be sent to server or not
const existingTodoKeys = Array.from(existingTodos().keys());
const draftTodoKeys = Array.from(draftTodos.keys());
let todosHasChanges = existingTodoKeys.length !== draftTodoKeys.length;
// now check entries using ids, unless we know they have changed based on length
// there are better ways of detecting changes but this demonstrates the issue
if (!todosHasChanges) {
const existingTodoValues = Array.from(existingTodos().values());
const draftTodoValues = Array.from(draftTodos.values());
for (let todoIndex = 0; todoIndex < draftTodoKeys.length; todoIndex++) {
// now check each entry
if (
existingTodoKeys[todoIndex] !== draftTodoKeys[todoIndex] ||
existingTodoValues[todoIndex].done !==
draftTodoValues[todoIndex].done
) {
todosHasChanges = true;
break;
}
}
}
if (todosHasChanges) {
// send off request to server
}
},
}}
>
{children}
</TodoContext.Provider>
\nMost of the above code doesn't matter but the general idea is that the two maps are being compared to one another. Here again strikes the existingTodos()
which as we established before essentially "resets" the Map back to the original data from the server. It is in fact this property of code with the useCallback
ensuring that existingTodos()
is the original data from the server that makes this code work at all because the add and remove operations mutate the original array. If it wasn't for existingTodos()
always fetching the un-mutated data the code would not function!
I suggest that this is an extremely obscure bug that only happens to not presents itself because of the use of useCallback to expose a function negates the mutation operations on the original map, instead of useMemo
for example. Sadly, for me, I didn't notice this at the time... and I refactored it.
I've published a code-sandbox with the full code for this example that uses useCallback
.
As suggested to earlier, when working in this area of the code I took the chance to tidy up the code to use useMemo
instead of useCallback
. Unfortunately, this made the code fall over, and to make things even more fun I was going on vacation later that week, luckily someone in my team came in and reverted back to using useCallback, hopefully I'll be able to make the change to make the code less of a trap with the mutation of the Maps when I return, which I'll discuss at the end.
// main change - change to use useMemo which means existingTodos is the todos, not a function.
const existingTodos = useMemo(() => {
const todoMap = new Map();
apiResponse?.todos.forEach((todo) => {
todoMap.set(todo.id, todo);
});
return todoMap;
}, [apiResponse]);
useEffect(() => {
// removed `existingTodos` from being a function
if (!draftTodos && existingTodos.size) {
setTodoList(existingTodos);
}
}, [existingTodos]);
\n// And inside of the save function, removed existingTodos() as its not a function anymore
{
save: () => {
const existingTodoKeys = Array.from(existingTodos.keys());
/// ...
const existingTodoValues = Array.from(existingTodos.values());
};
}
\nThis change got through testing and code review, and was seemingly correct. However, due to the mutating of the original Map in the add/remove functions (which I'd missed), the first time you added or deleted something it would not be tracked as a change, meaning if you only added or removed one item in the list the request would not be sent to the server because the entries in the existingTodos
had been mutated making the Maps essentially equal (not in terms of reference but contents, which is what the server cared about).
Here is a code-sandbox with the full code and demo that introduces the bug.
\n\n\nNote that the checking and unchecking of items in the demo does not have an issue with mutating the original Map.
\n
As noted above the issue where changes are incorrectly not reported only appears for the first change. The reason being that if you make two changes the code happens to work (sort of) because the array set by useMemo
in this example never changes after its been mutated the one time, but the code for adding or removing items will stop modifying the original Map after the first mutation, instead it uses the latest draft todos map. Incidentally, the real testing scenario was a bit more complex and involved reordering items, so you might be able to see how this scenario could get missed.
This would be incredibly difficult to unit test as well, with the code being split across contexts and multiple components, unless you were testing the full end to end interactions you might not catch this at all!
\nThe fix for the code is quite straight-forward and goes back to the original highlighted issue, the add and remove functions mutating the original map, they should not do this. Alternatively before setting the existing entries to the draftTodos
variable you could make a copy then, either would work, although I think I prefer avoiding mutations at the point of new data - in the add and remove functions.
removeTodo: (id) => {
// make a copy first
const newTodos = new Map(draftTodos);
if (newTodos.delete(id)) {
setTodoList(newTodos);
}
},
\naddTodo: (message) => {
if (!message) {
return;
}
const todo = {
id: crypto.randomUUID(),
message,
done: false,
};
if (draftTodos.has(todo.id)) return;
// make a copy first
const newTodos = new Map(draftTodos);
newTodos.set(todo.id, todo);
setTodoList(new Map(newTodos));
},
\nHere is a code-sandbox of what I believe is functional code.
\nI suppose one of the main lessons here is to always be aware of where code could potentially be mutating state, especially before introducing something like a useMemo
into the code base.
More importantly, when refactoring code from an uncommon pattern (the useCallback) to something more typical it’s important to fully understand why that code might have happened in the first place, it's likely it might be a workaround for another issue. Another learning on my part is that this would had made good sense to start a dialogue with the person who wrote the original code, although that's easy to say in retrospect, there are many contributors.
\nPerhaps code comments might have saved this or some more unit tests, it’s hard to say. I can say for sure that we did do lots of testing in this area before shipping the bug to production but I suppose because it’s a bug that only happens on the first mutation and fixes itself if you do subsequent changes that might help to explain why it was missed. It's possible that some end to end test automation of this functionality might have caught this, as it seems likely that a human tester would go for the more complex scenario over a more basic test, e.g. a single mutation which might be present in an automated test.
\nVariable names can lie, the name of the variable also had the word existing
in it in the other codebase too making it seem like this variable reference was representative of the original unmodified state, but due to the mutations it was not. I suppose it's a learning that you cannot always trust that a variable is what is says it is.
Maybe just don’t useMemo or useCallback or things that complicate the code - the work this component was doing to convert an array into a map, and ensure it wasn’t happening every render could well be redundant or overkill, part of my issue with react hooks is they can force you to introduce complexity for something as simple as converting from one data type to another and in doing this you can get struck by subtle bugs such as code mutating objects incorrectly or the incorrect use of react hooks ghastly dependency arrays.
\nI suppose some people might read this and have the take that you should simply "Get Gud", but I'd suggest to take a step back and think about how ubiquitous React is and how many developers of differing experience can be working on a code base, I think it’s examples like this which help to show that it’s super easy to break things by modifying to use a different React hook or mutating something that should not have been mutated, and no Typescript can’t save you here, this was a Typescript codebase!
\nIt might also be a fair argument to suggest that the bug presented here is not React specific, but I can say for sure the complexity that React can bring just to avoid rendering, or not rendering, does not help.
\nIf you want to read about another instance of discussing bugs and areas of growth, I wrote about another scenario here from my previous role: Memory leaks and why should you never write such bad code.
\nThank you for reading! If you want to read more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2022-01-10T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/beautiful-drag-and-drop-interactions-with-react-hooks./", "url": "https://griffa.dev/posts/beautiful-drag-and-drop-interactions-with-react-hooks./", "title": "Beautiful drag and drop interactions with react hooks.", "tags": ["JavaScript","React"], "summary": "Recently I needed to enhance a table with drag and drop re-ordering in React. There are lots of solutions out there from roll your own to a library with large dependencies. In this article I explore using an exciting new library called dnd-kit as well as how to integrate it with react-table.", "content_html": "In this article we'll explore how you could build a drag and drop sortable table.
\n\nWhen looking for drag and drop libraries in React there are a lot of options, some of the popular ones are:
\nMany of the libraries out there are great but some come with some large downsides, namely:
\nOne common thing also brought up when looking at these libraries is those that opt to "use the platform" more by utilising the HTML drag and drop APIs and those that deliberately choose to not use it to overcome browser inconsistencies and help improve support for a variety of inputs such as touch, mouse and keyboard.
\nAs part of this investigation I seriously considered react-beautiful-dnd
, whilst it has great support and demos, I hit a few stumbling blocks along to way. It's footprint was also a hard sell to add into the application for a single use-case. I also encountered this issue whilst I was investigating a different use case where I wanted horizontal and vertical re-ordering in a grid.
After trying out some other libraries I eventually settled on @dnd-kit, which I have to say is absolutely fantastic. It is technically still in beta, but despite this its documentation is quite mature. On the docs site it says the following:
\n\n\ndnd kit is currently in beta. Issues and bugs should be expected for early releases. The core concepts are stable, but some of the APIs may change in the future.
\n
It is a lightweight, hooks-based, utility driven library which gives you helpful primitives and allows you to opt in for additional features, therefore making the library only as large as your use case requires it be, depending on what you need.
\nThe easiest way to get started I think is to work through some of the code in some of their code sandbox demos. I'm personally a big fan of the sortable image grid example, try breaking down the code to see how things work.
\nThe first step to using dnd-kit
is to install the packages you need. The library is separated into small micro-libraries, which each provide additional utilities.
\nThe minimum you will require is the package @dnd-kit/core
(~11KB gzipped).
Some other useful packages with dnd-kit
provides:
@dnd-kit/sortable
- building blocks to build sortable interfaces .(~3KB gzipped)@dnd-kit/utilities
- dnd-kit utilities to help with integration. (~1.3KB gzipped)@dnd-kit/modifiers
- modifiers to change behavior of drag and drop, for example restricting the drag and drop movement to vertical or horizontal. (~3.4KB gzipped)@dnd-kit/accessibility
- Hooks to assist with accessibility for example screen reader announcements. (~499B gzipped)Note, in my use I found that webpack had some issues de-duplicating @dnd-kit/core
which is used inside of @dnd-kit/sortable
. Aside from adding to bloat, if this dependency is not de-duplicated the DndContext
provided by dnd-kit
will not work. If you run into this you can force it use the same copy of the core library, using an alias in your webpack.config.js.
resolve: {
alias: {
// without this webpack dupes the dependencies when other dnd-kit libs use it which breaks context
\"@dnd-kit/core\": path.resolve(__dirname, \"node_modules/@dnd-kit/core\"),
}
}
\nThe best way that I can recommend to understand the library before we get to the example using react-table is to take a look at the example I mentioned earlier (sortable image grid example).
\nOne thing to highlight that is important and quite cool about the library is its DragOverlay
component, which you can use to render a custom react component to visualise your dragged item differently when it is being dragged, for example adding a drop shadow, which you'll notice in my sortable table example a little later. It also offers you the opportunity to change how the original rendered item is rendered when it is being dragged by using the isDragging
property from useSortable
(or useDraggable
), for example change it's opacity or color to help indicate the drop area to the user, this is also shown in my up-coming example.
If you prefer to learn through videos, whilst learning the API I watched some of this video before getting my hands stuck into the code.
\nThe library has some really fantastic examples of how to use it on its storybook, however they do suffer from the classic problem that storybook often presents, they are great as showcases but not very good at demonstrations of how to adapt the example to use the code for yourself. By that I mean, it's very hard to find and understand the code written, and often suffers from the problem of "too much code re-use for demos", although if I'm honest I think this is a problem with Storybook not the library itself.
\nFor example, it's really hard to un-pick what the code in the example is doing, you end up having to search through github and reverse engineer complex examples and across multiple files, for example:
\nThe other thing that makes me sad is that this great library is locked into the React ecosystem, but I must admit, the ergonomics of using custom hooks here is quite nice.
\nreact-table is another lightweight and customisable react library, it allows users to use utilities and hooks to build up tables for their UIs. This description may sounds quite similar to what I described before about dnd-kit
where you can use a series of utilities and hooks to implement drag and drop. It is for this reason thatreact-table
and dnd-kit
fit together really nicely.
To help demonstrate this, I took the "basic" example of react-table
from their examples here: https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/basic and then added in drag and drop support, let's have a look how.
In order to add support there are roughly these steps:
\ndnd-kit
packages: @dnd-kit/core, @dnd-kit/modifiers, @dnd-kit/sortable, @dnd-kit/utilities.DndContext
and SortableContext
.SortableContext
to allow items to be sorted.useSortable
hook passing in the id of the row.DragOverlay
for a custom view of the row when dragging.isDragging
property, to act as a "slot" for the user to drop into.Here is an implementation of how you could go about doing drag and drop re-orderable rows with dnd-kit
and react-table
: https://codesandbox.io/s/react-table-drag-and-drop-sort-rows-with-dnd-kit-btpy9
This example isn't really focused on making it look "pretty", it needs lots of styling to make it more production ready but hopefully you get the idea.
\nOne really important thing that is missing in the example is custom screen reader messages for accessibility, you will want to do this to help make sure that your application is inclusive of everybody. There is a great example on the dnd-kit accessibility page.
\nI'm excited to explore difference uses for this library, once you get the hang of it it's really powerful!
\nThank you for reading! If you want to read more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2021-12-20T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/just-a-friendly-reminder-that-react-isn't-really-just-javascript-(don't-nest-functional-components)/", "url": "https://griffa.dev/posts/just-a-friendly-reminder-that-react-isn't-really-just-javascript-(don't-nest-functional-components)/", "title": "Just a friendly reminder that React isn't really Just JavaScript (Don't nest Functional Components)", "tags": ["JavaScript","React","Accessibility"], "summary": "React is super popular and has often touted itself as "Just JavaScript", but in reality it has lots of quirks and gotchas that come along with it being a framework that adds limitation into JavaScript. Oh, and also write tests!", "content_html": "This is my first blog post I've written about React, despite reluctantly using it every day due to the UK frontend job market. I'll probably regret talking about such a popular Framework, oh and it is a Framework 😉.
\nRecently, I was writing an animated slide-in sidebar component, which could be opened to reveal some additional details for another component in the page. One of my goals was to ensure that the tab and focus order of the page made sense when the details pane was opened, namely, I wanted to be able to "steal" focus on open and restore focus back to the original button on close. For example on button press (with space-bar) you should be able to open the details panel, have the panel but focused and close it again with the space-bar. Focus is then returned back to the original button and you can hit "tab" to move to the next item.
\nHere is a simplified example of what I was building, with some HTTP Status code kitties, try it out for yourself over in this Code sandbox.
\n\nHere is the code for the App, this was hastily written HTML which I'm sure could do with being improved.
\nimport \"./styles.css\";
import { useState } from \"react\";
import { Sidebar } from \"./Sidebar\";
export default function App() {
const statusCodes = [500, 404, 403, 401, 418, 420, 301, 302, 200, 201, 204];
const [selectedCode, setSelectedCode] = useState(null);
const Codes = (
<ul>
{statusCodes.map((code) => (
<li key={code}>
<button onClick={() => setSelectedCode(code)}>{code}</button>
</li>
))}
</ul>
);
return (
<div className=\"App\">
<h1>HTTP Status Cats</h1>
{Codes}
<Sidebar
onClose={() => setSelectedCode(null)}
ariaLabel={`${selectedCode} status code info`}
open={Boolean(selectedCode)}
>
<h2>{selectedCode}</h2>
<img
alt={`Cat demonstrating HTTP status code: ${selectedCode}`}
src={`https://http.cat/${selectedCode}.jpg`}
/>
</Sidebar>
</div>
);
}
\nAnd the sidebar, which is where the "focus stealing/restoring" happens:
\nimport { useEffect, useRef, useState } from \"react\";
export const Sidebar = ({ ariaLabel, open, onClose, children }) => {
const [previousFocus, setPreviousFocus] = useState();
// now focus inside something, for arguments sake, the close button
const closeBtnRef = useRef(null);
useEffect(() => {
if (open) {
setPreviousFocus(document.activeElement);
closeBtnRef?.current?.focus();
}
// bit of a hack putting aria label in here so triggers if another option selected.
}, [open, ariaLabel, closeBtnRef]);
return (
<aside aria-label={ariaLabel} aria-hidden={open ? \"false\" : \"true\"}>
<button
disabled={!open}
ref={closeBtnRef}
onClick={() => {
// restore previous focus
previousFocus?.focus();
onClose();
}}
>
Close X
</button>
{open && children}
</aside>
);
};
\nThis code was working correctly, and then I was working on another pr based on a branch with new changes that I pulled in and I noticed that the focus navigation had started failing.
\n\n\nActually, a unit test, which tested the focus interactions started failing which is actually pretty awesome! React Testing Library moved unit testing forward SO MUCH in comparison to Enzyme it's great. If it could only run by default inside a browser instead of Jest, that would be great. By default I mean, commonplace in the industry, I'm sure it's achievable.
\n
In my example above the new PR had added the equivalent of the Codes
variable which is in the above snippet:
const Codes = (
<ul>
{statusCodes.map((code) => (
<li key={code}>
<button onClick={() => setSelectedCode(code)}>{code}</button>
</li>
))}
</ul>
);
\n<h1>HTTP Status Cats</h1>;
{
Codes;
}
\nExcept, that wasn't what was added, this was:
\nconst Codes = () => (
<ul>
{statusCodes.map((code) => (
<li key={code}>
<button onClick={() => setSelectedCode(code)}>{code}</button>
</li>
))}
</ul>
);
\n<h1>HTTP Status Cats</h1>;
{
<Codes />;
}
\nThe difference is very subtle, but very important, what had been added by making Codes
a function was a functional React component nested inside of another functional component. Remember Codes
was a variable inside of App
. This is the sort of thing which can easily not get picked up in code review but it breaks a lot of things.
Even more interestingly, is that if you the following everything is fine.:
\n<h1>HTTP Status Cats</h1>;
{
Codes();
}
\nWhich is interesting considering that React functionally components are widely considered just a JavaScript function call, clearly when using JSX and mounting it as a React component react does "stuff".
\nHere is a broken example: https://codesandbox.io/s/http-status-cats-broken-fiu72?file=/src/App.jsx:508-554
\n\nWhat's going on here is React is rendering the contents of the "App" component on each render and because the inner component is not memoized or anyway react is just throwing it in the bin and re-rendering. Internally this will be causing the DOM element to be removed and re-added thus breaking the focus state being returned to the original button.
\nSeeing this in the code, it was a non obvious fix, especially when reviewing another persons code, it got me thinking on some things:
\nI did some digging on the last two points:
\nThe only reference to nested functions that I could find in the official documentation comes from the old classic "Rules of Hooks" page: Don’t call Hooks inside loops, conditions, or nested functions
, no mention of nested components though.
\n\nAs an aside, I love the the fact that the mechanism that React is now mostly entirely based on, "hooks", requires a list of rules to help prevent you from writing "Just JavaScript".
\n
As for a lint rules, there does appear to be one which you can turn on in the popular eslint-plugin-react
- no-unstable-nested-components, maybe i'll suggest to my team that we do. I can't think of a good reason when you would want to nest functional component, even if you're getting fancy and using useMemo, surely you'd just be better off writing simpler code.
I find it fun to think that such a tiny change in the code that looks so innocent can break quite a lot in the rendering of React components and something I learnt from this is I'm really going to do my best to make sure I write good sets of unit tests that test both "interactions" and "accessibility" as it's so easy for these things to regress!
\nThank you for reading! If you want to read more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2021-11-06T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/building-progress-indicator-cards-with-a-single-css-property/", "url": "https://griffa.dev/posts/building-progress-indicator-cards-with-a-single-css-property/", "title": "Building progress indicator cards with a single css property", "tags": ["HTML","CSS"], "summary": "I recently had to implement an indicator of progress onto a card component, I challenged myself to see if this could be done with a single css property, here's how.", "content_html": "Here is a full demo of the css which I'll show you how to build, by learning the structure of the css background
shorthand property.
.card-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin: 1rem;
}
.progress-card {
width: 30ch;
padding: 2rem;
border-radius: 5px;
border: solid 1px #f1f1f1;
box-shadow: 0px 4px 6px -1px rgba(0, 0, 0, 0.5), 0px 2px 4px -1px rgba(0, 0, 0, 0.2);
color: #6c3d3d;
--progress-total: #faebd1;
--progress-indicator: #65ad60;
--progress-indicator-height: 10%;
--progress-card-background: #ffdc9b;
background: linear-gradient(
to right,
var(--progress-indicator) 0,
var(--progress-indicator) var(--progress, 30%),
var(--progress-total) var(--progress, 30%),
var(--progress-total) 100%
) no-repeat 0 0/100% var(--progress-indicator-height), var(
--progress-card-background
) no-repeat 0 0/100%;
}
p {
font-weight: 600;
font-style: italic;
}
\n<div class=\"card-container\">
<div class=\"progress-card\" style=\"--progress:40%;\">
<h4>Collect 5 Boar meat</h4>
<p>In progress: 2 of 5</p>
</div>
<div class=\"progress-card\" style=\"--progress:20%\">
<h4>Collect 5 pieces of Firewood</h4>
<p>In progress: 1 of 5</p>
</div>
<div class=\"progress-card\" style=\"--progress:86%\">
<h4>Collect 7 Raptor feathers</h4>
<p>In progress: 6 of 7</p>
</div>
<div class=\"progress-card\" style=\"--progress:60%\">
<h4>Collect five Iron ore</h4>
<p>In progress: 6 of 10</p>
</div>
</div>
\nBefore today I've always shied away from full understanding all of the shorthand property capability that is baked into the background attribute
but today I'm going to change that, let's work it out together, let's deconstruct this bit of css that constructs the progress indicator card together:
.progress-card {
background: linear-gradient(
to right,
#65ad60 0,
#65ad60 30%,
#faebd1 30%,
#faebd1 100%
) no-repeat 0 0/100% 10%, #ffdc9b no-repeat 0 0/100%;
}
\nYou might be thinking, why do this with a single css property? You could just do this with multiple css properties or html elements, heck even the built in meter
html element. These things are all true however there may be some cases where just manipulating a single css property to create effects like this is handy:
Prior to experimenting to create this effect i'd never really gone into too much detail about the capabilities of the background property in css, but it is really quite fantastic! The fact that you can apply multiple backgrounds and their properties using a single css property is very powerful and not something i'd really appreciated before.
\nTo understand css snippet from above it would probably be a good idea to expand into using the non-shorthand background properties, to help understand and breakdown what's going on above:
\n.progress-card {
background: linear-gradient(
to right,
#65ad60 0,
#65ad60 30%,
#faebd1 30%,
#faebd1 100%
) no-repeat 0 0/100% 10%, #ffdc9b no-repeat 0 0/100%;
}
\nis equivalent to:
\n.progress-card {
background-image: linear-gradient(
to right,
#65ad60 0,
#65ad60 30%,
#faebd1 30%,
#faebd1 100%
);
background-color: #ffdc9b;
background-size: 100% 10%, 100%;
background-repeat: no-repeat;
}
\nWhat is going on is that we have two backgrounds:
\nlinear-gradient
which starts off green #65ad60
, moving to the right and at 30% turns into light cream #faebd1
.#ffdc9b
.Note how we are separating the background with a comma ,
. The same is done for background-size
. background-repeat
has just one value because I want the same value for each background, but I could have done background-repeat: no-repeat, no-repeat
.
For the background-size
property we first have width, followed by height. So the first background size says a width of 100% and a height of 10% which gives us the height of the progress bar. The no-repeat
is important, without it the background would just duplicate itself until it filled 100% of the image, as repeat is the default. It's probably useful to note that you may have multiple background images too, which can be an image url or a gradient.
To convert the background-image
, background-color
, background-size
and background-repeat
to the shorthand background
it's similar to the long form properties. There is a slightly different syntax for background-size
as you also need to account for background-position
which is a property which I have omitted from my long hand, it would be something like:
.progress-card {
background-image: linear-gradient(
to right,
#65ad60 0,
#65ad60 30%,
#faebd1 30%,
#faebd1 100%
);
background-color: #ffdc9b;
background-size: 100% 10%, 100%;
background-repeat: no-repeat;
/* this is the default */
background-position: 0 0;
}
\nIn order to specify the background-size
when using the background
shorthand you must also specify the position, to do this you separate the position and the size with a forward-slash /
.
For example, here is the first of the backgrounds:
\nbackground: [image] [repeat] [position-x] [position-y] / [width] [height];
\n.progress-card {
background: linear-gradient(
to right,
#65ad60 0,
#65ad60 30%,
#faebd1 30%,
#faebd1 100%
) no-repeat 0 0/100% 10%;
}
\nWhilst writing this article I came across this handy website that generates the shorthand for you, test it out with the above css, you should get a single background
property generated for you!
\n\nInternally it looks like this website uses this package: https://github.com/frankmarineau/shrthnd.js.
\n
Hopefully you can now see how we got to the final css, by adding in thebackground-color
and position values after a comma ,
.
background: linear-gradient(
to right,
#65ad60 0,
#65ad60 30%,
#faebd1 30%,
#faebd1 100%
) no-repeat 0 0/100% 10%, #ffdc9b no-repeat 0 0/100%;
\nThere is a lot more to the css background property, as always, your best bet for research is MDN.
\nNow we've had a play with the background property, let's see if we can make the css a little more useful, by introducing css custom properties we now have the possibility of re-using the same css with different values for percentage "progress", or changing the background colors.
\n\n\nIf you are new to CSS Custom properties Kevin Powell does a great introduction over here.
\n
.progress-card {
width: 30ch;
padding: 2rem;
border-radius: 5px;
border: solid 1px #f1f1f1;
box-shadow: 0px 4px 6px -1px rgba(0, 0, 0, 0.5), 0px 2px 4px -1px rgba(0, 0, 0, 0.2);
color: #6c3d3d;
--progress-total: #faebd1;
--progress-indicator: #65ad60;
--progress-indicator-height: 10%;
--progress-card-background: #ffdc9b;
background: linear-gradient(
to right,
var(--progress-indicator) 0,
var(--progress-indicator) var(--progress, 30%),
var(--progress-total) var(--progress, 30%),
var(--progress-total) 100%
) no-repeat 0 0/100% var(--progress-indicator-height), var(
--progress-card-background
) no-repeat 0 0/100%;
}
\n<div class=\"card-container\">
<div class=\"progress-card\" style=\"--progress:40%;\">
<h4>Collect 5 Boar meat</h4>
<p>In progress: 2 of 5</p>
</div>
<div class=\"progress-card\" style=\"--progress:20%\">
<h4>Collect 5 pieces of Firewood</h4>
<p>In progress: 1 of 5</p>
</div>
<div class=\"progress-card\" style=\"--progress:86%\">
<h4>Collect 7 Raptor feathers</h4>
<p>In progress: 6 of 7</p>
</div>
<div class=\"progress-card\" style=\"--progress:60%\">
<h4>Collect five Iron ore</h4>
<p>In progress: 6 of 10</p>
</div>
</div>
\nHopefully you can see how you can change the look of each "card" just by modifying the css custom property for that card only.
\nHere is a live demo of the html and css: https://codepen.io/georgegriff/pen/RwZRBeJ
\nYou could next choose to create a re-usable component, for example a Web Component, that could set the css custom properties and add appropriate aria attributes.
\nIt's important to note that this css-based progress indicator is purely decoration, you will want to ensure that users with screen readers are able to understand what the component is conveying, using aria attributes in your HTML, or appropriate text labels.
\n\n\nYou can learn about ARIA basics on MDN here.
\n
Thank you for reading! If you want to read more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2021-10-19T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/scheduled-builds-a-for-website-on-netlify/", "url": "https://griffa.dev/posts/scheduled-builds-a-for-website-on-netlify/", "title": "Scheduled builds a for website on Netlify", "tags": ["Netlify","GithubActions","CircleCI","ContinuousIntegration"], "summary": "Sometimes you might want to periodically build your website on Netlify, the answer isn't that straight forward and some tools have gotchas.", "content_html": "I recently launched a re-write of my brothers Guitar teaching business website: cgguitar.co.uk, during this rewrite I implemented a feature where I wanted to fetch YouTube playlists at build time.
\nTo achieve my goals, I wanted to scheduled builds of my Netlify website periodically.
\nNetlify doesn't have this feature built in, however what Netlify does have is a 'webhook' which you can call trigger to your build.
In this post I offer two ways to trigger this build, using Github Actions or CircleCI. Using Github Actions can have a significant downside, depending on the use case, continue read to find more about that!
\nLogin to Netlify and navigate to your site settings and local the "Build & deploy" section, your build hook will be in there.
\n\nYou can test this out by making a curl request in your terminal, you should see it trigger your Netlify website build:
\ncurl -X POST -d {} https://api.netlify.com/build_hooks/$NETLIFY_BUILD_HOOK_TOKEN
\n\n\nImportant: Ensure you keep
\nNETLIFY_BUILD_HOOK_TOKEN
secret, otherwise anyone can call your build and potentially cause you to go over your build quota.
Github actions let you perform continuous integration in Github, they seem like a perfect fit here...
\nIn your Github repository, in the following folder .github/workflows
, you could create:
name: Scheduled build
on:
schedule:
- cron: \"00 15 * * *\"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger our build webhook on Netlify
run: curl -s -X POST \"https://api.netlify.com/build_hooks/${TOKEN}\"
env:
TOKEN: ${{ secrets.NETLIFY_BUILD_HOOK_TOKEN }}
\nThe above configuration will run every day at around 15:00.
\nYou will want to secure your build_hook
token in a secret on Github.
I did this originally and thought job done right? Well, not quite.
\n\n\nGithub Actions are disabled on projects after 60 days if there is no activity on the repository, meaning bye bye scheduled builds.
\n
The above limitation might be okay, depending on your use case, but for my case where I wanted to make sure I was fetching the latest videos from a YouTube playlist, this was no good because the website's code itself may not be updating very often, but there may be new videos added.
\nOne alternative to Github actions is to use CircleCI to do this instead, it has a generous free tier too so there should be no charge for this.
\nYou can create a configuration like this:
\n.circleci
config.yml
Populate the config.yml
with something like the following (you can use a different image if you wish). This will build every day at 3PM.
version: 2
defaults: &defaults
machine:
image: circleci/classic:201710-02
steps:
- run: curl -X POST -d {} https://api.netlify.com/build_hooks/$NETLIFY_BUILD_HOOK_TOKEN
jobs:
docker:
<<: *defaults
workflows:
version: 2
autobuild:
triggers:
- schedule:
cron: \"0 14 * * *\"
filters:
branches:
only:
- main
jobs:
- docker
\nNow you can create a project in CircleCI and you should be able to test your build.
\nNETLIFY_BUILD_HOOK_TOKEN
will need to be set as an environment variable, in a similar way to Github, in the setting of CircleCI for your project.
If you want to read more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2021-06-21T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/adding-dynamic-content-from-an-api-to-a-static-website-at-build-time/", "url": "https://griffa.dev/posts/adding-dynamic-content-from-an-api-to-a-static-website-at-build-time/", "title": "Adding dynamic content from an API to a static website at build time", "tags": ["11ty","HTML","JavaScript","WebComponents"], "summary": "You may not need client side JavaScript to add dynamic content from an API. In this article I will show the approach I recently took to embed YouTube playlists into an website created by a static site generator (Elventy), and how this approach lends itself to less JavaScript and progressive enhancement.", "content_html": "I recently launched a re-write of my brothers Guitar teaching business website: cgguitar.co.uk, during this rewrite I had some guiding principles which I believe are best practices when building any website:
\nIn this post I'll describe my approach to getting embedded YouTube playlist content into the website, at build time, reducing the number calls to YouTube client side to only the embedded video and thumbnails, no calls out to the YouTube Data API. In addition to this, i'll show you how you can keep the site up to date with easy to configure cron jobs (scheduled builds).
\nThe feature that I built, that I will explain, is an embedded YouTube playlist component which fetches all the data and stats for YouTube playlists at build time and renders their video metadata/thumbnails directly into the HTML. You can check out the feature live over at https://www.cgguitar.co.uk/videos/#guitar-lessons.
\n\nCalling out to external APIs/services from your client side JavaScript can introduce you many problems, to name a few:
\nSecurity - if you want to hide your token or keep it secure you either have to:
\nRate limiting/charges - most APIs have limits to the number of API calls you can make, or will start charging you for usage:
\nJavaScript needed - In order to show the data you want to show to user, you need to serve JavaScript to your users:
\nThis is approach is not a silver bullet, not every feature would support this, e.g. if you want to work with user submitted content.
\nHowever, if all you are showing is content that changes infrequently, moving the data fetching to build time can be a really great solution.
The static site I built for my brothers' business uses Eleventy, a fantastic static site generator.
\nI wrote about getting started with 11ty in How I got started with 11ty.
The next section will assume some knowledge about 11ty, or static site generators in general.
\n11ty has a plugin called @11ty/eleventy-cache-assets which you can use to fetch any data you like.
\nconst Cache = require(\"@11ty/eleventy-cache-assets\");
module.exports = async function () {
let url = \"https://api.github.com/repos/11ty/eleventy\";
/* This returns a promise */
return Cache(url, {
duration: \"1d\", // save for 1 day
type: \"json\", // we’ll parse JSON for you
});
};
\nThe awesome thing about this plugin is that once the data is fetched it is cached so future local builds do not have to re-fetch data, meaning your builds can remain lightning fast which is a common characteristic of any 11ty project.
\nFor my feature I decided I wanted to be able to pick and choose which YouTube playlists that I wanted to show in the website, it is however possible to fetch all YouTube playlists for an account too. I wanted to be able to choose so that I could add, order and describe new playlists in my CMS (Netlify CMS).
\nThe playlists in the website are defined as markdown in the code in a folder named playlists, Netlify CMS is configured to read these files e.g:
\n---
title: Beginner guitar lessons
name: beginner-guitar-lessons
id: PLA0cAQ-2uoeoJoFfUz9oq9qhmlnsjFRhU
---
\nThe first step to getting my playlists into 11ty is to define them as a collection, to do this inside of the src/playlists
folder I create a playlists.json.
{
\"tags\": [\"playlist\"],
\"permalink\": false
}
\nThis creates an 11ty collection of all of the playlists, with their "id", "name" and "descriptions".
\nInside of my videos page I can then work with this collection in my Nunjucks template:
\n{%- if collections.playlists %} {%- asyncEach collections.playlist in playlists
| fetchYouTubePlaylists %} {%- include 'partials/video-playlist.njk' %} {%-
endeach %} {%- endif %}
\n\n\nIf you are unfamiliar with template languages in 11ty you can read about them over here.
\n
I'll show what partials/video-playlist.njk
is later on in the article.
fetchYouTubePlaylists
is where the magic happens and where we will start to use @11ty/eleventy-cache-assets
.
\nThis is an 11ty filter which is defined in my .eleventy.js
config file.
eleventyConfig.addNunjucksAsyncFilter(
\"fetchYouTubePlaylists\",
async (playlists, callback) => {
const data = await getPlaylists(playlists);
callback(null, data);
}
);
\nLet's take a dive a layer deeper: getPlaylists
is making a call to getPlaylistItem
which is where I'm actually doing the data caching.
module.exports.getPlaylists = async (playlists) => {
if (!playlists) {
return [];
}
const lists = await Promise.all(
playlists.map(async ({ id, title, description }) => {
const content = await getPlaylistItem(id);
return {
title,
id,
description,
link: `https://www.youtube.com/playlist?list=${id}`,
...(content || {}),
};
})
);
return lists;
};
\nThis function is looping through all of my playlists and fetching the items (videos) in that playlist. It is also adding the name, description and direct link to YouTube for the whole playlist.
\nNow for getPlaylistItem
:
const getPlaylistItem = async (playlistId) => {
const apiUrl = \"https://www.googleapis.com/youtube/v3/playlistItems\";
const maxResults = 20;
const order = \"viewCount\";
const url = `${apiUrl}?key=${apiKey}&part=${encodeURIComponent(
\"snippet,contentDetails\"
)}&type=video%2C%20playlist&maxResults=${maxResults}&playlistId=${playlistId}&order=${order}`;
console.log(`Fetching YouTube videos for playlist: ${playlistId}`);
const videos = await Cache(url, {
duration: \"1d\", // 1 day
type: \"json\", // also supports \"text\" or \"buffer\"
});
const videoIds = videos.items.map(
({ contentDetails }) => contentDetails.videoId
);
const metaInfo = await fetchMetaInfo(videoIds);
return {
videos: await Promise.all(
videos.items.map(async ({ snippet, contentDetails }) => {
const hqThumbnail =
snippet.thumbnails.maxres ||
snippet.thumbnails.high ||
snippet.thumbnails.medium ||
snippet.thumbnails.default;
const smallThumbnail =
snippet.thumbnails.medium || snippet.thumbnails.default;
const defaultThumbnail = snippet.thumbnails.high;
return {
hqThumbnail,
smallThumbnail,
defaultThumbnail,
channelTitle: snippet.channelTitle,
channelId: snippet.channelId,
title: snippet.title,
id: contentDetails.videoId,
...(metaInfo[contentDetails.videoId] || {}),
};
})
),
hasMore: Boolean(videos.nextPageToken),
};
};
\nThe first few things this function does is:
\n\n\nYou will want to store your API key as an environment variable e.g.
\nconst apiKey = process.env.YT_API_KEY;
. For production you can add this environment variable where ever you choose to build/host the site e.g. on Netlify.
Next up it fetches some extra metadata. fetchMetaInfo
fetches things like view count and likes, this is another API call which we would be concerned about if this was client side, but since it's build time, who cares!
\nImplementation available on Github.
Finally I'm looping through all the data and returning an array of videos
for each playlist and a flag hasMore
if the playlist has more than then 20 items shown. In my HTML when I see this flag I add an link out to YouTube to watch the full playlist.
The above code a modified version of the original, where I'm doing a a few extra things you can checkout the full version on Github.
\nNow I have the website fetching the external data, let's see how I could approach displaying the content in the HTML.
\nWhen designing an dynamic experience its a good idea to think about what is the minimal experience you can provide without needing JavaScript, and build from there.
\nYou could start out very simply and just load a link <a>
to the YouTube videos, perhaps the thumbnail could open a tab to YouTube, this needs no JS at all, and is what I did:
{%- if playlist -%}
{%- set firstVideo = playlist.videos[0] -%}
{%- set description = playlist.description or (playlist.templateContent | safe) %}
<youtube-playlist id=\"{{playlist.title | slug }}\">
<div class=\"fallback\" slot=\"fallback\">
<div class=\"img-btn-wrapper\">
<img decoding=\"async\" loading=\"lazy\" width=\"{{firstVideo.hqThumbnailWidth}}\" height=\"{{firstVideo.hqThumbnaillWdith}}\" src=\"{{firstVideo.hqThumbnailUrl}}\" />
</div>
<a rel=\"noopener\" title=\"Play playlist: {{playlist.title}}\" class=\"\" target=\"_blank\" href=\"{{playlist.link}}\"></a>
</div>
{%- for video in playlist.videos -%}
<li {{helpers.spread(video, \"data-\") | safe}}></li>
{%- endfor -%}
{%- if playlist.hasMore -%}
<a slot=\"more-link\" href=\"{{playlist.link}}\">Watch more on YouTube.</a>
{%- endif -%}
</youtube-playlist>
{%- endif -%}
\nYou will see that I'm wrapping the whole code in a youtube-playlist
Custom Element.
\nWhen the component loads without JavaScript it is just a link out to YouTube, which is then upgraded to a full playlist experience. This will disable the default "link" behavior too.
I'm not going to go into the implementation of my Web Component in this post but you can check out the source code on Github. The general idea is to consume <li>
list items as child content inside of my <youtube-playlist>
and when JavaScript loads move this content in the Shadow DOM, and make them look pretty/interactive.
Here is my full Nunjucks template for my html:
\n{%- if playlist -%}
{%- set firstVideo = playlist.videos[0] -%}
{%- set description = playlist.description or (playlist.templateContent | safe) %}
<youtube-playlist id=\"{{playlist.title | slug }}\">
<a slot=\"heading\" href=\"#{{playlist.title | slug }}\"><h2>{{playlist.title | safe}}</h2></a>
<p slot=\"description\">{{description}}</p>
<div class=\"fallback\" slot=\"fallback\">
<div class=\"img-btn-wrapper\">
<img decoding=\"async\" loading=\"lazy\" width=\"{{firstVideo.hqThumbnailWidth}}\" height=\"{{firstVideo.hqThumbnaillWdith}}\" src=\"{{firstVideo.hqThumbnailUrl}}\" />
<svg style=\"pointer-events:none;\" class=\"playbtn\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">
<g transform=\"translate(-339 -150.484)\">
<path fill=\"var(--White, #fff)\" d=\"M-1978.639,24.261h0a1.555,1.555,0,0,1-1.555-1.551V9.291a1.555,1.555,0,0,1,1.555-1.551,1.527,1.527,0,0,1,.748.2l11.355,6.9a1.538,1.538,0,0,1,.793,1.362,1.526,1.526,0,0,1-.793,1.348l-11.355,6.516A1.52,1.52,0,0,1-1978.639,24.261Z\" transform=\"translate(2329 150.484)\"/>
<path fill=\"var(--Primary, #000)\" d=\"M16.563.563a16,16,0,1,0,16,16A16,16,0,0,0,16.563.563Zm7.465,17.548L12.672,24.627a1.551,1.551,0,0,1-2.3-1.355V9.853a1.552,1.552,0,0,1,2.3-1.355l11.355,6.9A1.553,1.553,0,0,1,24.027,18.111Z\" transform=\"translate(338.438 149.922)\" />
</g>
</svg>
</div>
<a rel=\"noopener\" title=\"Play playlist: {{playlist.title}}\" class=\"\" target=\"_blank\" href=\"{{playlist.link}}\"></a>
</div>
{%- for video in playlist.videos -%}
<li {{helpers.spread(video, \"data-\") | safe}}></li>
{%- endfor -%}
{%- if playlist.hasMore -%}
<a slot=\"more-link\" href=\"{{playlist.link}}\">Watch more on YouTube.</a>
{%- endif -%}
</youtube-playlist>
{%- endif -%}
\nUsing Web Components like this is a perfect way of enhancing a base HTML experience with limited JavaScript.
\nIn order to keep the YouTube playlists up to date I want to be able to build the website every day on schedule.
\nThere are many options when it comes to periodically building a website, I wrote about my approach to doing this in: Scheduling builds on Netlify. In brief, I opted to use Circle CI to call my Netlify build hook every day at 3 PM. I tried Github Actions but there is a major limitation to using an Action for this use case, which I go into in the linked article.
\nI hope this article was helpful and you can see some of the advantages to moving dynamic content that changes infrequently to be rendered at build time.
\nIf you want to read more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2021-06-21T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/natural-language-search-for-blog-posts-using-tensorflowjs/", "url": "https://griffa.dev/posts/natural-language-search-for-blog-posts-using-tensorflowjs/", "title": "Natural language search for blog posts using TensorflowJS", "tags": ["JavaScript","MachineLearning","TensorflowJS","11ty"], "summary": "I've been learning TensorflowJS and Machine Learning, as an experiment, I thought I would implement a search across my current blog posts using sentence similarity on natural language, running in the browser.", "content_html": "\n\n\nIn this post i'll go into how you can get started using pre-trained Tensorflow models to do Machine learning in the browser, examine some of the potential gotchas, such as not blocking the main thread with custom logic and consider the impact of the size of models on UX.
\nThe demo that I developed as part of this article is a "search engine" using my blog posts as a data set, which I converted into an API, the idea being: can I find blog posts based on a search query by a user, by comparing the similarity of the query with a blog posts title and description?
\nSearch is a solved problem and there are better ways of achieving the same thing, but I created this to learn and to have a bit of fun!
\nIf you want to check out a live demo for what I built in this post, I've hosted it on my website.
\nI'm going to explain how this all works with a smaller example rather than the full demo that I linked earlier, but the source code for the example is available on Github, it's the same code, just with things like UI simplified.
\nFirst up, let's load in the library we are going to use. We're just going to load them from a CDN, when you're just experimenting, you don't want to be messing around with build processes.
\nCreate a HTML file called index.html
, with the following content:
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\" />
<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
<title>Blog post search</title>
<script src=\"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/@tensorflow-models/universal-sentence-encoder\"></script>
</head>
<body>
<script type=\"module\" src=\"index.js\"></script>
</body>
</html>
\nWe're loading in two libraries here, the first is TensorflowJS and the second is a the Universal Sentence Encoder model, which uses TensforflowJS, you can read about over here.
\n\n\nIf you want to code along, host your files on a local dev server. I personally recommend the Live Server VS Code extension.
\n
Next, create index.js
add the following code:
// IIFE - because no top level await in all browsers at time of writing.
(async () => {
// download the model
const model = await use.load();
const blogPosts = [
\"How I got started with 11ty\",
\"Building a responsive, progressively enhanced, masonry layout with only CSS and HTML\",
\"Using the Web Share API and meta tags, for simple native sharing\",
\"Tips for debugging in 11ty\",
];
const userQuery = \"Sharing to social media\";
// embed the user input and the blog posts using the model - explained next!
const blogPostsTensor = await model.embed(blogPosts);
const userInputTensor = await model.embed([userQuery]);
})();
\n\n\nIn Chrome, and other browsers soon, you won't need to wrap the code in an IIFE because you could use top level await instead.
\n
This code is loading the model, and then passing our userQuery
of "Sharing to social media" and our array of blogPosts
into the model.
\nDoing this converts the sentences into vectors (arrays) with 512 entries in the vector for each sentence, this is how the model sees the sentence.
\nUniversal sentence encoder has been trained on a large vocabulary and is encoding the provided data based on the data it saw during training.
To help make this a bit clearer, blogPostsTensor
and userInputTensor
will be an instance of tensor2d.
\nThese are 2D arrays (on the GPU) with 512 entries in each of the arrays, which represents a provided phase.
// The following are example embedding output of 512 dimensions per sentence
// Embedding for user input: \"Sharing to social media\"
// userInputTensor = tf.tensor2d([[0.01305108 0.02235125 -0.03263278, ...]])
// Embedding for: I am a sentence for which I would like to get its embedding.
// blogPostsTensor = tf.tensor2d([[0.05833394 -0.0818501 0.06890938, ...], [0.05833394 -0.0818501 0.06890938, ...]])
\nNext, in order to find potentially good results based our input sentence we need to check how similar our input vector is to the vectors of the blog post titles, we can achieve this by calculating Cosine Similarity between the vectors, which will give us a value between -1 and 1. 1 being most similar and -1 being not very similar at all.
\nI'm not going to explain the mathematics of cosine similarity, but I've provided an implementation of it.
\nIf you want to know how it works, there are lots of great explanations on YouTube, such as this one..
Define these at the top of your index.js file.
\n// multiply with value with corresponding value in the other array at the same index, then sum.
const dotProduct = (vector1, vector2) => {
return vector1.reduce((product, current, index) => {
product += current * vector2[index];
return product;
}, 0);
};
// square each value in the array and add them all up, then square root.
const vectorMagnitude = (vector) => {
return Math.sqrt(
vector.reduce((sum, current) => {
sum += current * current;
return sum;
}, 0)
);
};
const cosineSimilarity = (vector1, vector2) => {
return (
dotProduct(vector1, vector2) /
(vectorMagnitude(vector1) * vectorMagnitude(vector2))
);
};
\nI tried to implement this maths purely in TensorflowJS, so that I could take advantage of the GPU, but after much trial and error, I could not find a solution. If anyone knows how to do this I'd love to hear about it. Doing this calculation myself is performing a large tradeoff of having these calculations happen on the main thread, which can cause bad UX, i'll explain this in more detail towards the end of the post, including ways around this.
\nNow lets use the functions in our code,
\n(async () => {
// download the model
const model = await use.load();
const blogPosts = [
\"How I got started with 11ty\",
\"Building a responsive, progressively enhanced, masonry layout with only CSS and HTML\",
\"Using the Web Share API and meta tags, for simple native sharing\",
\"Tips for debugging in 11ty\",
];
const userQuery = \"Sharing to social media\";
// embed the user input and the blog posts using the model - explained next!
const blogPostsTensor = await model.embed(blogPosts);
// wrap the user input in an array so model can work with it
const userInputTensor = await model.embed([userQuery]);
// == New code starts here //
// convert to JS arrays from the tensors
const inputVector = await userInputTensor.array();
const dataVector = await blogPostsTensor.array();
// this is an array of arrays, we only care about one piece of user input, one search query so
const userQueryVector = inputVector[0];
// how many results do i want to show
const MAX_RESULTS = 2;
// loop through the blog post data
const predictions = dataVector
.map((dataEntry, dataEntryIndex) => {
// COSINE SIMILARITY - compare the user input tensor with each blog post.
const similarity = cosineSimilarity(userQueryVector, dataEntry);
return {
similarity,
result: blogPosts[dataEntryIndex],
};
// sort descending
})
.sort((a, b) => b.similarity - a.similarity)
.slice(0, MAX_RESULTS);
document.querySelector(\"#initial-example-results\").innerText = JSON.stringify(
predictions,
null,
2
);
})();
\nOn the last line of the above example we're updating the text of an element with id "initial-example-results", to make this work, let's add the following to your html file, inside the <body>
tag.
<p>
This will take a few moments for the model to load and run. Query: \"Sharing to
social media\"
</p>
<pre id=\"initial-example-results\"></pre>
\nHere's a link to the code we've built so far: https://codesandbox.io/s/tensorflow-js-hardcoded-blog-search-0q5o9
\n\n<p>
This will take a few moments for the model to load and run. Query: \"Sharing to
social media\"
</p>
<pre id=\"initial-example-results\"></pre>
\nconst dotProduct = (vector1, vector2) => {
return vector1.reduce((product, current, index) => {
product += current * vector2[index];
return product;
}, 0);
};
const vectorMagnitude = (vector) => {
return Math.sqrt(
vector.reduce((sum, current) => {
sum += current * current;
return sum;
}, 0)
);
};
const cosineSimilarity = (vector1, vector2) => {
return (
dotProduct(vector1, vector2) /
(vectorMagnitude(vector1) * vectorMagnitude(vector2))
);
};
/* swap out this.querySelectorDeep for document.querySelector, this is custom for my website demos */
this.querySelectorDeep(\"#initial-example-results\").innerText =
\"Downloading model...\";
const model = await use.load();
const blogPosts = [
\"How I got started with 11ty\",
\"Building a responsive, progressively enhanced, masonry layout with only CSS and HTML\",
\"Using the Web Share API and meta tags, for simple native sharing\",
\"Tips for debugging in 11ty\",
];
const userQuery = \"Sharing to social media\";
this.querySelectorDeep(\"#initial-example-results\").innerText =
\"Encoding data...\";
const blogPostsTensor = await model.embed(blogPosts);
const userInputTensor = await model.embed([userQuery]);
/* convert to JS arrays from the tensors */
const inputVector = await userInputTensor.array();
const dataVector = await blogPostsTensor.array();
/* this is an array of arrays, we only care about one piece of user input, one search query so */
const userQueryVector = inputVector[0];
/* how many results do i want to show */
const MAX_RESULTS = 2;
this.querySelectorDeep(\"#initial-example-results\").innerText =
\"Cosine similarity calculations...\";
/* loop through the blog post data */
const predictions = dataVector
.map((dataEntry, dataEntryIndex) => {
/* compare the user input tensor with tensor of a blog post. */
const similarity = cosineSimilarity(userQueryVector, dataEntry);
return {
similarity,
result: blogPosts[dataEntryIndex],
};
/* sort descending */
})
.sort((a, b) => b.similarity - a.similarity)
.slice(0, MAX_RESULTS);
console.log(predictions);
this.querySelectorDeep(\"#initial-example-results\").innerText = JSON.stringify(
predictions,
null,
2
);
\nMy blog is written using the static site generator tool Eleventy. If you haven't heard of Eleventy and you're into building fast websites, seriously check it out, it's awesome. I'm not going to explain how Eleventy works, but I wrote a post about how I got started with Eleventy.
\nTo create an API out of my blog posts I generate a JSON file in the form of a JSON Feed, which can be hosted on my server.
\nHere's my template for my json feed, this template is based on the 11ty base blog. The templating syntax being used is Nunjucks, and comes supported out of the box with Eleventy.
\nIf you are curious and want to check out the source code of my blog it's over here on Github.
\n---
# Metadata comes from _data/metadata.json
permalink: \"{{ metadata.jsonfeed.path | url }}\"
eleventyExcludeFromCollections: true
---
{ \"version\": \"https://jsonfeed.org/version/1\", \"title\": \"{{ metadata.title }}\",
\"home_page_url\": \"{{ metadata.url }}\", \"feed_url\": \"{{ metadata.jsonfeed.url
}}\", \"description\": \"{{ metadata.description }}\", \"author\": { \"name\": \"{{
metadata.author.name }}\", \"url\": \"{{ metadata.author.url }}\" }, \"items\": [ {%-
for post in collections.posts | reverse %} {%- set absolutePostUrl %}{{ post.url
| url | absoluteUrl(metadata.url) }}{% endset -%} { \"id\": \"{{ absolutePostUrl
}}\", \"url\": \"{{ absolutePostUrl }}\", \"title\": \"{{ post.data.title }}\", \"tags\": [
{%- for tag in helpers.removeCollectionTags(post.data.tags) -%} \"{{tag}}\" {%- if
not loop.last %}, {%- endif %} {%- endfor %}], \"summary\": \"{{
post.data.description }}\", \"content_html\": {% if post.templateContent %}{{
post.templateContent | dump | safe }}{% else %}\"\"{% endif %}, \"date_published\":
\"{{ post.date | rssDate }}\" } {%- if not loop.last -%} , {%- endif -%} {%-
endfor %} ] }
\nThis template is iterating through my blog posts and populating a JSON array with post data, as well as some other site metadata, ultimately the result is a JSON file which i can request on my server: https://griffa.dev/feed/feed.json.
\nNow I have an API which I can use in my search, success!
\nWe can now update our code sample to pull data from this api instead of hard-coding it.
\nAdd this function to the top of "index.js".
const loadBlogPosts = async () => {
const res = await fetch(\"https://griffa.dev/feed/feed.json\");
const feed = await res.json();
return feed.items.map((item) => {
return {
/* search on title and summary */
searchData: `${item.title} ${item.summary}`,
title: item.title,
description: item.summary,
};
});
};
\nReplace the following code:
\nconst model = await use.load();
const blogPosts = [
\"How I got started with 11ty\",
\"Building a responsive, progressively enhanced, masonry layout with only CSS and HTML\",
\"Using the Web Share API and meta tags, for simple native sharing\",
\"Tips for debugging in 11ty\",
];
\nwith:
\nconst [model, blogPosts] = await Promise.all([use.load(), loadBlogPosts()]);
\nAlso replace
\nconst blogPostsTensor = await model.embed(blogPosts);
\nwith:
\nconst blogPostsTensor = await model.embed(
blogPosts.map(({ searchData }) => searchData)
);
\n<p>
This will take a few moments for the model to load and run. Query: \"Building a
blog with javascript\"
</p>
<pre id=\"initial-example-results\"></pre>
\nconst dotProduct = (vector1, vector2) => {
return vector1.reduce((product, current, index) => {
product += current * vector2[index];
return product;
}, 0);
};
const vectorMagnitude = (vector) => {
return Math.sqrt(
vector.reduce((sum, current) => {
sum += current * current;
return sum;
}, 0)
);
};
const cosineSimilarity = (vector1, vector2) => {
return (
dotProduct(vector1, vector2) /
(vectorMagnitude(vector1) * vectorMagnitude(vector2))
);
};
/* swap out this.querySelectorDeep for document.querySelector, this is custom for my website demos */
this.querySelectorDeep(\"#initial-example-results\").innerText =
\"Downloading model and blog posts...\";
const loadBlogPosts = async () => {
const res = await fetch(\"https://griffa.dev/feed/feed.json\");
const feed = await res.json();
return feed.items.map((item) => {
return {
/* search on title and summary */
searchData: `${item.title} ${item.summary}`,
title: item.title,
description: item.summary,
};
});
};
const [model, blogPosts] = await Promise.all([use.load(), loadBlogPosts()]);
const userQuery = \"Building a blog with javascript\";
this.querySelectorDeep(\"#initial-example-results\").innerText =
\"Encoding data...\";
/* extract the searchData from the blog posts */
const blogPostsTensor = await model.embed(
blogPosts.map(({ searchData }) => searchData)
);
/* wrap the user input in an array so model can work with it */
const userInputTensor = await model.embed([userQuery]);
/* convert to JS arrays from the tensors */
const inputVector = await userInputTensor.array();
const dataVector = await blogPostsTensor.array();
/* this is an array of arrays, we only care about one piece of user input, one search query so */
const userQueryVector = inputVector[0];
/* how many results do i want to show */
const MAX_RESULTS = 5;
this.querySelectorDeep(\"#initial-example-results\").innerText =
\"Cosine similarity calculations...\";
/* loop through the blog post data */
const predictions = dataVector
.map((dataEntry, dataEntryIndex) => {
/* compare the user input tensor with tensor of a blog post. */
const similarity = cosineSimilarity(userQueryVector, dataEntry);
return {
similarity,
result: blogPosts[dataEntryIndex],
};
/* sort descending */
})
.sort((a, b) => b.similarity - a.similarity)
.slice(0, MAX_RESULTS);
console.log(predictions);
this.querySelectorDeep(\"#initial-example-results\").innerText = JSON.stringify(
predictions,
null,
2
);
\nHere's a link to the code we've built so far: https://codesandbox.io/s/tensorflow-js-blog-search-3k7x2
\nHopefully the examples so far have made sense, I thought i'd take a moment to talk about some of benefits and tradeoffs of doing Machine learning in the browser with TensorflowJS.
\nOne of the first things you might think of when you think Machine learning in JavaScript is it's slow, well that's where one of the great things about TensorflowJS comes in, it performs all of its expensive calculations on the GPU, under the hood it's utilising WebGL shader programs to achieve this.
\nRunning Machine learning in the browser opens up the possibilities of offering Machine learning in applications without needing to build complex server architectures, or learning another language. It also means that it's possible to provide on-device Machine learning to users, without their data ever hitting a server.
\nOne of the other great things about the JavaScript ecosystem is its ability to not just run in the browser, but on the server too, with NodeJS. TensorflowJS is also available in Node JS, where it can be bound directly to the Tensorflow API, the same API that the python implementations of the library consume. I've considered the possibility of modifying my experiment in this blog post so that when I generate my static site at build time with Eleventy, I could run the model against my data and pre-generate the data for my blog posts, that might be cool.
\nThe final great thing is that it is possible to convert/re-use models created by the other Tensorflow ecosystems (Python etc) so that they run in the browser.
\nNow for one of the big trade offs, Machine learning models can be large, there is a lot of work going to make these models smaller and smaller, but the model used in this demo for example is approximately 28 MB. To be fair, for a general purpose natural language model, this is quite impressively small. Many of these models, are split into chunks so that the model can be downloaded in parallel, which improves things a bit. This tradeoff might be acceptable if it unlocks the ability to provide a good enough UX, without the need to hit a server, which once the model is downloaded can be lightning fast. The model can only be as fast the end-user machine it's running on, which, especially on mobile, can vary dramatically.
\nIn applications you might be able to do some different things to make this tradeoff worth it, for example:
\nWith the above tradeoffs in mind it might, or might not, make sense to do ML in the browser. Where you need to try and run your models immediately as the site/app loads, or end user device constraints are a problem, maybe server side is the better choice.
\nWhen using JavaScript it's always important to not block the main thread, I mentioned above that Tensorflow utilises the GPU for its calculations, but as soon as you stop using its API you're back in the JS main thread, and if you perform expensive calculations there ,you are at risk of providing a bad UX to your users.
\nThe sample in this post is guilty of this, when performing the cosineSimilarity
calculations, let's fix it.
In the browser you can create additional threads called "Workers", these are isolated threads, that do not have access to any DOM APIs, or variables in the main thread.
\nThe only way to communicate between the main thread is via postMessage
, which can be cumbersome.
There is an absolutely fantastic library Comlink that makes working with Worker threads basically invisible, it allows you to work with functions as if they were on the main thread, I believe it achieves this using Proxy objects, hiding the need to work with postMessage directly 🎉.
\nLet's convert our example to use Comlink and move our maths off the main thread.
\nWe're going to import the Tensorflow libraries in our worker instead so your HTML should look like this.
\nLet's also add in some user input, to make the demo a bit more spicy.
\n<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\" />
<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
<title>Blog post search</title>
</head>
<body>
<script type=\"module\" src=\"index.js\"></script>
<form id=\"search\">
<input disabled name=\"query\" type=\"text\" />
<button disabled>Search</button>
</form>
<pre id=\"initial-example-results\"></pre>
</body>
</html>
\nNext up, delete all of the code in "index.js". Now in "index.js" lets add the code to work with our new "worker.js" file and update the UI.
\nWe're going to add all of the same code, except this time, expose a function called "search" which returns our predictions.
\nThere are few other changes too, such as using importScripts to import the libraries into the Worker.
importScripts(\"https://unpkg.com/comlink/dist/umd/comlink.min.js\");
importScripts(\"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest\");
importScripts(
\"https://cdn.jsdelivr.net/npm/@tensorflow-models/universal-sentence-encoder\"
);
let model;
let blogPosts;
const loadBlogPosts = async () => {
// fetch + cache comparison data
const res = await fetch(\"https://griffa.dev/feed/feed.json\");
const feed = await res.json();
const data = feed.items.map((item) => {
return {
searchData: `${item.title} ${item.summary}`,
title: item.title,
description: item.summary,
};
});
return data;
};
const loadModel = async () => {
const model = await use.load();
return model;
};
const load = async () => {
[model, blogPosts] = await Promise.all([loadModel(), loadBlogPosts()]);
};
//// cosine similarity fns
const dotProduct = (vector1, vector2) => {
return vector1.reduce((product, current, index) => {
product += current * vector2[index];
return product;
}, 0);
};
const vectorMagnitude = (vector) => {
return Math.sqrt(
vector.reduce((sum, current) => {
sum += current * current;
return sum;
}, 0)
);
};
const cosineSimilarity = (vector1, vector2) => {
return (
dotProduct(vector1, vector2) /
(vectorMagnitude(vector1) * vectorMagnitude(vector2))
);
};
////
async function search(userQuery) {
const blogPostsTensor = await model.embed(
blogPosts.map(({ searchData }) => searchData)
);
const userInputTensor = await model.embed([userQuery]);
const inputVector = await userInputTensor.array();
const dataVector = await blogPostsTensor.array();
/* this is an array of arrays, we only care about one piece of user input, one search query so */
const userQueryVector = inputVector[0];
/* how many results do i want to show */
const MAX_RESULTS = 5;
/* loop through the blog post data */
const predictions = dataVector
.map((dataEntry, dataEntryIndex) => {
/* compare the user input tensor with tensor of a blog post. */
const similarity = cosineSimilarity(userQueryVector, dataEntry);
return {
similarity,
result: blogPosts[dataEntryIndex],
};
/* sort descending */
})
.sort((a, b) => b.similarity - a.similarity)
.slice(0, MAX_RESULTS);
return predictions;
}
const SearchService = {
search,
load,
};
/* expose the SearchService api to comlink */
Comlink.expose(SearchService);
\nNow, let's use our new SearchService
in "index.js".
import * as Comlink from \"https://unpkg.com/comlink@4.3.0/dist/esm/comlink.min.mjs\";
const worker = new Worker(\"worker.js\");
const SearchService = Comlink.wrap(worker);
(async () => {
document.querySelector(\"#initial-example-results\").innerText =
\"Loading model...\";
await SearchService.load();
document.querySelector('#search input[name=\"query\"]').disabled = false;
document.querySelector(\"#search button\").disabled = false;
document.querySelector(\"#initial-example-results\").innerText =
\"Model loaded, try out some queries e.g. Building a blog with JavaScript\";
document.querySelector(\"#search\").addEventListener(\"submit\", async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const query = data.get(\"query\");
document.querySelector(\"#initial-example-results\").innerText =
\"Searching...\";
const predictions = await SearchService.search(query);
document.querySelector(\"#initial-example-results\").innerText =
JSON.stringify(predictions, null, 2);
});
})();
\nIf you load this demo code up in the browser you should get similar result to before, but with the heavy-work offloaded to a Worker thread.
\nHere's a live demo project for reference: https://codesandbox.io/s/tensorflow-js-with-web-workers-blog-searching-1shbq
\nHopefully you can see from the example how you can offload work into a worker using Comlink, you can also build for production using popular tools such as Rollup, but I won't cover that here.
\nOne of the neat things about using Worker threads is because they don't have access to the DOM you are forced to separate your application logic from your UI, making your code more modular and reusable in the future.
\nIn case you missed the links earlier:
\nIf I was to continue this idea through i'd probably explore some of the following:
\nI hope to continue my Machine learning journey, I have some other blog related ideas that I might try to explore in the future:
\nI'm fairly early on in my AI learning journey, but one of the initial resources that helped me out and inspired me was watching content from Jason Lengstorf from his Learn with Jason series, which I highly recommend. One of the truly awesome things about this series is closed captioning is provided, making this content more accessible to everybody 🎉.
\nAt the time of writing there are 3 sessions relating to Machine Learning and TensorflowJS, here is one of them:
\nI hope this was a good read, if you feel like reading more of my work, please follow me on Twitter @griffadev, or get me a coffee if you feel like it ☕.
\n", "date_published": "2021-04-22T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/type-checking-global-javascript-libraries-in-vs-code-for-when-you-just-want-learn-and-code./", "url": "https://griffa.dev/posts/type-checking-global-javascript-libraries-in-vs-code-for-when-you-just-want-learn-and-code./", "title": "Type checking global JavaScript libraries in VS Code for when you just want learn and code.", "tags": ["JavaScript","VSCode","TypeScript","TensorflowJS"], "summary": "Sometimes when you're prototyping and learning a new library you want a quick way to get type-checking/intellisense/auto-complete to assist with your learning. Here is a quick way to enable type checking without fully opting into TypeScript.", "content_html": "I've been learning a little bit of artificial intelligence/machine learning in my spare time and I recently started learning a little TensorflowJS. The quickest and simplest way of getting TensorflowJS is a good old script tag. When you are experimenting and prototyping you really don't want to waste time with build tools.
\nWhilst this tutorial explains setting up basic type-checking for TensorflowJS it should work for any global library that has TypeScript documentation. When building a production project you may want to consider build tools, but for Machine learning experiments, I find you just want to get to the coding using documentation and examples.
\nThis post explains how you can prototype with files from a CDN whilst opting in to certain nice-to-have features of TypeScript that can assist with learning a library, without fully adopting TypeScript.
\nYou can just load TensorflowJS like so:
\n<script src=\"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest\"></script>
\nDoing so will make the library available on the global window
object as tf
e.g.
\nwindow.tf; // the Tensorflow library
\nOne downside to doing this is that when you are working with Tensorflow you won't get any auto-complete in your IDE/Editor, because there are no import references for VS Code (or other) to analyze, unlike if you were to use ES6 Imports or Common JS require statements.
\nLet's take a look at how we can make no changes to the code but give you some nice type checking, and the real bonus for learning a library: inline documentation, which is so fantastic.
\n\n\nWhilst this tutorial explains how to enable type-checking and library documentation in VS Code, it may work for other IDEs, the reason VS Code is so great here is its built in TypeScript support.
\n
Create a directory and open it up in VS Code.
\n\n\nIf you don't have it yet, you can download VS Code over here.
\n
Create a simple HTML file called index.html
with two script imports, one for the library and one for your code.
\nWe won't actually boot up this file on a server and load it in a browser in this tutorial, but I'm putting this in for completeness.
<!DOCTYPE html>
<html lang=\"en\">
<head> </head>
<body>
<script src=\"https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest\"></script>
<script src=\"./index.js\"></script>
</body>
</html>
\nThe reason for creating a separate index.js
instead of inline JavaScript in a <script>
, is as far as I could find, type checking does not seem to work in HTML files. If there is a way to get it to work easily, I'd love to know!
If you don't have one yet, first initialise an NPM project in your directory, you may be able to do this without an NPM project by using global libraries with -g
, I haven't tried this though.
\n\nIf you don't have NodeJS and NPM installed you can download it here: https://nodejs.org/en/.
\n
Install the TensorflowJS library, we won't actually be using the code in the library, this is for just referencing the types, which come along with the NPM module.
\nnpm i @tensorflow/tfjs
\nNext up we're going to create a TypeScript declaration file.
\nIn this file we're going to tell TypeScript that the TensorflowJS library will be available on the global Window
interface with a variable named tf
Create a file called index.d.ts
interface Global {
tf: typeof import(\"@tensorflow/tfjs\");
}
interface Window extends Global {}
\nNow finally create index.js
and open it up.
Inside the file put the following at the top:
\n/// <reference path=\"../index.d.ts\" />
const tf = window.tf;
\n\n\nIf you want find out more about the notation with the triple-slashes, you can find out more in the TypeScript documentation
\n
Now here comes the magic, when typing you will now get auto-complete, for example if you type tf.
you should get some suggestions 🥳.
But the really fantastic thing here, and where it can really help if you are learning the library, is if the library author provides good function documentation you have that available right in your editor.
\n\nYou can also add strict type-checking with single comment in a file.
\nIn index.js add the following to the top of your file.
\n// @ts-check
\nYou should now see that when you code you will get type warning, like so:
\n\nCongratulations, you have now adopted TypeScript!
\nIf you wanted to, you could even define your own types in index.d.ts
.
If you want to learn more about ways of adopting TypeScript in small ways, this is a really great video that shows the different "degrees" of adopting TypeScript in a project, without needing to go "all in".
\nIf you want to learn more about Machine learning/Tensorflow, I got inspired by watching content from Jason Lengstorf from his Learn with Jason series, which I highly recommend (if it wasn't obvious already). One of the truly awesome things about this series is closed captioning is provided, making this content more accessible to everybody 🎉.
\nAt the time of writing there are 3 sessions relating to Machine Learning and TensorflowJS, here is one of them:
\nI hope this was a good read, if you feel like reading more of my ramblings, please follow me on Twitter @griffadev.
\n", "date_published": "2021-04-18T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/memory-leaks-and-why-should-you-never-write-such-bad-code-even-for-tests/", "url": "https://griffa.dev/posts/memory-leaks-and-why-should-you-never-write-such-bad-code-even-for-tests/", "title": "Memory leaks and why should you never write such bad code, even for tests", "tags": ["JavaScript","NodeJS","ExpressJS","Kubernetes"], "summary": "That feeling when you see some awful code, and then realise you wrote it. Here's a story of how some bad code I wrote in a unit test, made it into production and caused a memory leak in a NodeJS application.", "content_html": "A project that I work on started showing crashed Pods in our Kubernetes (K8s) cluster, which runs a NodeJS server in a container, it was failing with a dreaded "OOMKilled" (Out of memory killed) error, which sent me down the path of learning about profiling NodeJS applications for memory leaks.
\nIf you don't know much about Kubernetes, that doesn't matter for this article, the fact the NodeJS application is running in K8s is incidental to the tale, and I will only mention it briefly when discussing debugging in Kubernetes.
\nI learnt a lot through investigating this memory leak, not just about memory profiling, but about the risk of writing "quick and dirty code", even if it's not for production use.
\nHere is an obfuscated and simplified explanation of the terrible, terrible code that I originally wrote.
\nSo, a few months ago I wrote some code some unit tests which exercised some file validation logic. When I wrote this code I needed to get access to the supported file extensions for the validator, for doing some checks, which lived somewhere else, so I very lazily dumped the file extensions onto an object that I was processing in the test, just for test purposes, right? it'll be fine.
\nMy simplified version of the production module:
\nconst supportedValidators = [\"name\"];
module.exports = {
validators: () => {
return supportedValidators.map((validator) => {
// imports ./validators/name.js what name.js does is not important.
return require(`./validators/${validator}.js`);
});
},
};
\nWhat's happening here is a function is exported which exposes other modules via a dynamic require statement. This dynamic require statement is very important
to the memory leak and i'll come back to it.
\n\nAs an aside using require like this in the code is blocking, so it might not be the best thing to use. For more modern code we could use
\nimport()
in ES Module code.
Now for the bad code, I had in my unit test:
\nconst { validators } = require(\"./validate\");
const badFunction = () => {
const myValidators = validators();
myValidators.map((validator) => {
// mutating someone elses object, yeah i know, this is super bad.
if (!validator.supportedExtensions) {
validator.supportedExtensions = [];
}
// the code didn't do this exactly this is demo
validator.supportedExtensions.push(\".pdf\");
});
};
\nThis code is terrible for so many reasons, but it was only for unit tests
. This code looks weird, I know, but in context of what I used this for in our tests does make sense, I promise, but this simplified sample is to demonstrate the memory leak.
Unfortunately, only for unit tests
ended up not being true, and here is my first learning from this experience:
\n\nAny code that you write anywhere, including inside of unit tests, could be copied by any well meaning developer adapting the code for their use case.
\n
This happened, and unfortunately the "just for test" code with the supportedExtensions = []
and .push(".pdf")
came along for the ride, even though it wasn't needed at all for the production use case.
So on the face of it if you were to look at the myValidators.map
code it doesn't look like I'm holding any references to the supportedExtensions
object once badFunction
its finished. But that assumption ignores that the Array, or the contents of it, that validators()
supplies could be retained in memory forever.
\n\nObjects in Memory are available for Garbage collection when there are no references to the object anymore and nothing is holding on to those objects. This is a massive over-simplification, I'll link a great talk later in this article which describes it much better than I ever could.
\n
Do you remember this line of code?
\nreturn require(`./validators/${validator}.js`);
\nA NodeJS module is being imported into the current scope, the important thing about NodeJs modules though is that:
\nNo matter how many times you import a module you always get the a reference to the same module object, so:
\nconst foo = require(\"./foo\");
const foo2 = require(\"./foo2\");
foo === foo2; // true
\nThis means that even if we lose all references to the imported module, it wont be garbage collected.
\nIn this case, this is extremely problematic because validator.supportedExtensions.push(".pdf");
will add a new entry to an array, that lives on this module object forever, every time the function is called. Imagine if this was on an REST API call (it was). Yikes.
A couple more learnings here:
\nOn a somewhat related note to the learnings above, Jake Archibald recently wrote about the risks of calling methods with potentially non-future proof parameters, it's a good read: https://jakearchibald.com/2021/function-callback-risks/.
\nWhen I first realised that we might have a Memory leak I first wanted to make sure I knew what tools where at my disposal,
\nI headed to YouTube and found this great video on the topic.
If you open the Developer Tools when you have a node --inspect
process running you should notice a new Node logo, like the one shown below:
When you activate this mode, a new window will open where one of the tabs in the new window is "Memory".
\n\nTo try and find the issue I selected the option "Allocation instrumentation timeline", I then proceeded to run our application tests which had caused the original "OOMKilled" on our cluster. After these tests ran I stopped the instrumentation and proceeded to sort through the results.
\nI found that sorting by size and then searching the largest allocated objects helped me find the problem.
Sure enough after filtering through a lot of noise I found something like this:
\n\nLuckily for me, I know our codebase quite well and was able to identify the problem area based on the variable name of the large array and also the array contents, which is where to my shame I found my awful code being used inside of an API call.
\nMemory profiling in Kubernetes is quite similar to when you are running locally, you need to end up with a server on your machine exposing a debugging session.
\nRecommendations:
\n--inspect
flagkubectl port-forward pod-name 9229
this will result in the same debugging session as earlier running on your machine, forwarded from the K8s cluster.If you want to have a play reproducing the memory leak you could do this:
\nvalidators/name.js
Can be empty module exports:module.exports = {};
\nvalidate.js
const supportedValidators = [\"name\"];
module.exports = {
validators: () => {
return supportedValidators.map((validator) => {
return require(`./validators/${validator}.js`);
});
},
};
\nbad-code.js
const { validators } = require(\"./validate\");
const badFunction = () => {
const myValidators = validators();
myValidators.map((validator) => {
if (!validator.supportedExtensions) {
validator.supportedExtensions = [];
}
// the code didnt do this exactly this is demo
validator.supportedExtensions.push(\".pdf\");
});
};
let index = 0;
setInterval(() => {
// even though theres no references to myValidators array
// there is a memory leak with the .push
badFunction();
index++;
console.log(`Running bad code cycle: ${index}`);
}, 0);
\nnode --inspect bad-code.js
You will need NodeJS installed https://nodejs.org.I learnt a lot through this experience.
\nI hope this was a good read, if only to laugh at my terrible code, if you feel like reading more of my ramblings, please follow me on Twitter @griffadev.
\n", "date_published": "2021-03-18T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/building-a-responsive-progressively-enhanced-masonry-layout-with-only-css-and-html/", "url": "https://griffa.dev/posts/building-a-responsive-progressively-enhanced-masonry-layout-with-only-css-and-html/", "title": "Building a responsive, progressively enhanced, masonry layout with only CSS and HTML", "tags": ["HTML","CSS"], "summary": "Masonry layouts, think bricks, think Pinterest, have had many solutions in the web over the years. Many use too much JavaScript, but there is some exciting new things coming to CSS grid to enable Masonry layouts. But you don't have to wait, you could implement today, using progressive enhancement. Progressive enhancement is like technical debt that fixes itself.", "content_html": "A few days ago I found that I had talked myself into re-designing a website. The website is for my brothers' guitar teaching business which I had previously worked a few years ago, when I was a student.
\nAs part of the redesign I was trying to think of ways to lay out testimonials from students, which may have varying length/content, I stumbled onto the idea of using a masonry layout (think bricks, think Pinterest).
\n\nThere are many ways to go about building out a Masonry layout, this CSS tricks article lays out a few of the options, Approaches for a CSS Masonry Layout.
\nWhen looking into the solution I knew I wanted to adhere to a couple of rules:
\ngrid-template-rows: masonry
This article will explain how to build out the following responsive masonry cards layout with only HTML and CSS.
\nThis final demo uses some features only available in Firefox, behind a flag, which is detailed in the article.
Here is the full finished demo code:
\n\n<div class=\"masonry\">
<div class=\"brick\">
<span class=\"num\">1</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">2</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">3</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">4</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">5</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">6</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">7</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">8</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">9</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">10</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">11</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">12</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">13</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">14</span> Adipisci tempora id veniam quos eligendi amet
aut beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut
quisquam dolor odio, quia expedita harum, laborum corrupti? Debitis corporis
quas odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">15</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">16</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
<div class=\"brick\">
<span class=\"num\">17</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">18</span> Adipisci tempora id veniam quos eligendi amet
aut beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut
quisquam dolor odio, quia expedita harum, laborum corrupti? Debitis corporis
quas odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">19</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">20</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
</div>
\n.masonry {
--masonry-gap: 1rem;
--masonry-brick-width: 180px;
column-gap: var(--masonry-gap);
column-fill: initial;
column-width: var(--masonry-brick-width);
}
.masonry > * {
break-inside: avoid;
margin-bottom: var(--masonry-gap);
}
@supports (grid-template-rows: masonry) {
.masonry {
display: grid;
gap: var(--masonry-gap);
grid-template-rows: masonry;
grid-template-columns: repeat(
auto-fill,
minmax(var(--masonry-brick-width), 1fr)
);
align-tracks: stretch;
}
.masonry > * {
margin-bottom: initial;
}
}
/* some styling not important */
.masonry {
background: #f3f3f3;
padding: 2rem;
}
.brick:nth-child(4n - 7) {
background: #5a363a;
}
.brick:nth-child(4n - 6) {
background: #82212c;
}
.brick:nth-child(4n - 5) {
background: #3a3e41;
}
.brick:nth-child(4n - 4) {
background: #292a2b;
}
.brick {
color: white;
padding: 1rem;
}
\nCSS Grid is amazing, and you'd think it would have shipped with a simple way of doing masonry layouts, the initial versions of the spec however, did not ship anything to really help out with these layouts.
\nBut fear not, Firefox has something cooking!
\n\n\nBefore I continue, the specification we're going to look into is experimental, and at the time of writing only is available in Firefox behind a flag, however, this article will outline how you could use the feature today and fallback to another approach for browsers without support.
\n
It's January 2021, I'm using Firefox 84.0.2, depending on how the spec has progressed, you may not need to enable this flag, it may even be available in other browsers, this link may have more information.
\nTo enable in Firefox:
\nabout:config
into your address barlayout.css.grid-template-masonry-value.enabled
and enable it.Over on MDN you can find this article: CSS Grid Layout > Masonry Layout
\nIt details, an example very similar to this:
\n\n.grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
/* this only works in firefox right now */
grid-template-rows: masonry;
margin: 1rem;
}
.item {
background: var(--Primary, #74baff);
}
\n<div class=\"grid\">
<div class=\"item\" style=\"block-size: 2em;\"></div>
<div class=\"item\" style=\"block-size: 3em;\"></div>
<div class=\"item\" style=\"block-size: 1.6em;\"></div>
<div class=\"item\" style=\"block-size: 4em;\"></div>
<div class=\"item\" style=\"block-size: 2.2em;\"></div>
<div class=\"item\" style=\"block-size: 3em;\"></div>
<div class=\"item\" style=\"block-size: 4.5em;\"></div>
<div class=\"item\" style=\"block-size: 1em;\"></div>
<div class=\"item\" style=\"block-size: 3.5em;\"></div>
<div class=\"item\" style=\"block-size: 2.8em;\"></div>
</div>
\nWith this small amount of CSS we can get a layout which is almost what we want already, exciting! It has hardcoded sizing, lets improve it, and enable responsive masonry layouts,
\nLet's start with a fresh example, starting with a little html and "lorem ipsum" to generate some "bricks" for masonry layout.
\nTo see the effect you will need to be view these examples in a browser that supports masonry layouts (at the time of writing Firefox behind a flag).
\n\n<div class=\"masonry\">
<div class=\"brick\">
<span class=\"num\">1</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">2</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">3</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">4</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">5</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">6</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">7</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">8</span> Adipisci tempora id veniam quos eligendi amet aut
beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut quisquam
dolor odio, quia expedita harum, laborum corrupti? Debitis corporis quas
odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">9</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">10</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
</div>
\n.masonry {
display: grid;
gap: 1rem;
grid-template-rows: masonry;
/* magic responsiveness */
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
/* some styling not important */
.brick {
color: var(--Text, #f0f0f0);
background: var(--Primary, #74baff);
padding: 1rem;
}
\nThis code will result in a masonry style layout, which automatically reduces the number of columns as the screen shrinks down, until you reach a single column.
\n\nIf you run this example you should notice that there is no clean edge on the bottom, it has a wonky bottom
.
\n\nNo one likes a
\nsoggywonky bottom.
The masonry specification also allows for a new property: align-tracks
, which you can set to stretch
to fix this problem.
align-tracks
also has some other support modes, beware of potential issues with align-tracks
if you want a masonry layout where a "brick" can span multiple columns, hopefully these things will get ironed out as the spec progresses.
<div class=\"masonry\">
<div class=\"brick\">
<span class=\"num\">1</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">2</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">3</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">4</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">5</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">6</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">7</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">8</span> Adipisci tempora id veniam quos eligendi amet aut
beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut quisquam
dolor odio, quia expedita harum, laborum corrupti? Debitis corporis quas
odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">9</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">10</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
</div>
\n.masonry {
display: grid;
gap: 1rem;
grid-template-rows: masonry;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
/* no more wonky bottom */
align-tracks: stretch;
/* just so you can see the edge in the example */
padding-bottom: 3rem;
}
/* some styling not important */
.brick {
color: var(--Text, #f0f0f0);
background: var(--Primary, #74baff);
padding: 1rem;
}
\nAnd now as if by magic, the bottom edge is now straight.
\n\nOkay, so now we made something work in a single browser only, and it's not even shipped there, not ideal.
\nLet's try to achieve something similar, this time with CSS columns.
I'd never used CSS columns before looking into them for implementing this masonry layout, they are actually quite clever, and they can made to be responsive, without media queries, which we will take a look at now.
\nFor this example, I'm going to start to make use of CSS Custom Properties which will become extremely useful once we enable our progressive enhancement, we can use these variables to share sizes and other config.
\nFor the css we're going to make use of some column properties:
\ncolumn-gap: 1rem;
column-fill: initial;
column-width: 180px;
\nSetting a column-width
will set a minimum width of a column before columns need to shrink down, meaning on smaller devices we can get a single column, the same as with our CSS grid implementation.
If you want to set a specific number of columns you can use the column-count
property. You could adjust the number of columns using CSS media queries too, if you wanted to:
Optional example:
\n/* larger */
@media (min-width: 1024px) {
.masonry {
column-count: 4;
}
}
/* medium */
@media (max-width: 1023px) and (min-width: 768px) {
.masonry {
column-count: 3;
}
}
/* small, anything smaller will be 1 column by default */
@media (max-width: 767px) and (min-width: 540px) {
.masonry {
column-count: 2;
}
}
\nWe're not going to do that, we're just going to make use of column-width
and allow our columns to grow up to a maximum card width.
Same html as before, with some changes to the css:
\n\n<div class=\"masonry\">
<div class=\"brick\">
<span class=\"num\">1</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">2</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">3</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">4</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">5</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">6</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">7</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">8</span> Adipisci tempora id veniam quos eligendi amet aut
beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut quisquam
dolor odio, quia expedita harum, laborum corrupti? Debitis corporis quas
odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">9</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">10</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
</div>
\n.masonry {
--masonry-gap: 1rem;
--masonry-brick-width: 180px;
column-gap: var(--masonry-gap);
column-fill: initial;
column-width: var(--masonry-brick-width);
}
.masonry > * {
/* Sometimes elements get stuck between columns. */
break-inside: avoid;
/* add gap at bottom */
margin-bottom: var(--masonry-gap);
}
/* some styling not important */
.brick {
color: var(--Text, #f0f0f0);
background: var(--Primary, #74baff);
padding: 1rem;
}
\nThe result is very similar, however, there are two main differences:
\nThe first difference may be a deal breaker, depending on the use case, for me, order didn't really matter very much.
\n\nNow we've seen how to implement a masonry layout in the future and with a fallback, lets put it all together with the magical @supports
keyword in css.
@supports (grid-template-rows: masonry) {
.masonry {
display: grid;
gap: var(--masonry-gap);
grid-template-rows: masonry;
grid-template-columns: repeat(
auto-fill,
minmax(var(--masonry-brick-width), 1fr)
);
align-tracks: stretch;
}
.masonry > * {
/* use this to reset the margin that the column variant set */
margin-bottom: initial;
}
}
\nAgain, with no changes to the HTML again, we're going to implement an @supports
just for the masonry layout using css grid:
<div class=\"masonry\">
<div class=\"brick\">
<span class=\"num\">1</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">2</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">3</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">4</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">5</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">6</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">7</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">8</span> Adipisci tempora id veniam quos eligendi amet aut
beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut quisquam
dolor odio, quia expedita harum, laborum corrupti? Debitis corporis quas
odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">9</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">10</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
</div>
\n.masonry {
--masonry-gap: 1rem;
--masonry-brick-width: 180px;
column-gap: var(--masonry-gap);
column-fill: initial;
column-width: var(--masonry-brick-width);
}
.masonry > * {
break-inside: avoid;
margin-bottom: var(--masonry-gap);
}
@supports (grid-template-rows: masonry) {
.masonry {
display: grid;
gap: var(--masonry-gap);
grid-template-rows: masonry;
grid-template-columns: repeat(
auto-fill,
minmax(var(--masonry-brick-width), 1fr)
);
align-tracks: stretch;
}
.masonry > * {
margin-bottom: initial;
}
}
/* some styling not important */
.brick {
color: var(--Text, #f0f0f0);
background: var(--Primary, #74baff);
padding: 1rem;
}
\nNow with this example, depending on what your browser supports, you will either get the CSS Grid or the CSS Columns version.
\nAs long as the grid-template-rows: masonry
doesn't change between now and more browsers shipping, we have just written some CSS that will automatically upgrade itself over time, fixing the technical debt/UX "bug" of a wonky bottom and vertical ordering instead of horizontal.
In my implementation I was toying around with having different cards backgrounds every nth "brick" in the masonry layout.
\n.brick:nth-child(4n - 7) {
background: #5a363a;
}
.brick:nth-child(4n - 6) {
background: #82212c;
}
.brick:nth-child(4n - 5) {
background: #3a3e41;
}
.brick:nth-child(4n - 4) {
background: #292a2b;
}
\nThere are some really neat tricks you can do with the nth-child
property in CSS, some useful background reading if you are new to this:
Here's the full demo:
\n\n<div class=\"masonry\">
<div class=\"brick\">
<span class=\"num\">1</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">2</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">3</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">4</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">5</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">6</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">7</span> Lorem ipsum dolor inventore saepe maiores
delectus? Quaerat excepturi repudiandae magnam in corporis? Corporis
veritatis necessitatibus rem veniam explicabo iure ea incidunt ut dicta?
</div>
<div class=\"brick\">
<span class=\"num\">8</span> Perspiciatis dolor qui deserunt animi, quidem
temporibus nam doloremque maiores quae ullam, quibusdam consequatur
consequuntur et corporis laboriosam est facere ratione quaerat odio quas
dolore fugit rem officiis odit! Ad.
</div>
<div class=\"brick\">
<span class=\"num\">9</span> Earum, voluptatum! Minima sapiente deserunt optio
blanditiis, non atque voluptates libero molestias ipsam officiis
reprehenderit ipsum tenetur deleniti quo dolore ut odio vero distinctio,
error temporibus doloribus. Fugiat, accusantium aperiam!
</div>
<div class=\"brick\">
<span class=\"num\">10</span> Voluptatibus vel magnam a cupiditate at veniam
eos voluptates harum incidunt minus. Nisi alias quis ab odit perspiciatis
quisquam officVoluptatibus vel magnam a cupiditate at veniam eos voluptates
harum incidunt minus.
</div>
<div class=\"brick\">
<span class=\"num\">11</span> Suscipit cum nihil id voluptatem omnis officia
dolor quia. Saepe perferendis, quas, fuga magnam iure soluta nemo eligendi
dignissimos repudiandae maxime, beatae sapiente possimus obcaecati ab
necessitatibus voluptatem numquam commodi.
</div>
<div class=\"brick\">
<span class=\"num\">12</span> Inventore maiores velit possimus dolore in ipsa
praesentium obcaecati. Velit nihil porro pariatur id culpa earum soluta ipsa
autem repellat quisquam iure facilis totam sint, quod a quae delectus
perferendis.
</div>
<div class=\"brick\">
<span class=\"num\">13</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">14</span> Adipisci tempora id veniam quos eligendi amet
aut beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut
quisquam dolor odio, quia expedita harum, laborum corrupti? Debitis corporis
quas odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">15</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">16</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
<div class=\"brick\">
<span class=\"num\">17</span> Accusamus voluptates hic assumenda magni
molestiae distinctio architecto, nihil error atque corporis, laudantium quo,
dolores voluptatum exercitationem vero aliquid tenetur illo doloribus
numquam! Autem impedit ab omnis qui optio rem!
</div>
<div class=\"brick\">
<span class=\"num\">18</span> Adipisci tempora id veniam quos eligendi amet
aut beatae laborum? Impedit tenetur consequuntur quae rerum numquam, ut
quisquam dolor odio, quia expedita harum, laborum corrupti? Debitis corporis
quas odit consectetur.
</div>
<div class=\"brick\">
<span class=\"num\">19</span> Labore vel asperiores at deleniti, optio
explicabo minus dolorum culpa, totam perspiciatis maiores voluptas dicta
excepturi facere nesciunt dolor ducimus aperiam nihil sint harum
necessitatibus? Ullam, cupiditate! Ab, tempora alias.
</div>
<div class=\"brick\">
<span class=\"num\">20</span> consequuntur temporibus omnis recusandae sunt,
ratione excepturi quos voluptates obcaecati quo? Illum.
</div>
</div>
\n.masonry {
--masonry-gap: 1rem;
--masonry-brick-width: 180px;
column-gap: var(--masonry-gap);
column-fill: initial;
column-width: var(--masonry-brick-width);
}
.masonry > * {
break-inside: avoid;
margin-bottom: var(--masonry-gap);
}
@supports (grid-template-rows: masonry) {
.masonry {
display: grid;
gap: var(--masonry-gap);
grid-template-rows: masonry;
grid-template-columns: repeat(
auto-fill,
minmax(var(--masonry-brick-width), 1fr)
);
align-tracks: stretch;
}
.masonry > * {
margin-bottom: initial;
}
}
/* some styling not important */
.masonry {
background: #f3f3f3;
padding: 2rem;
}
.brick:nth-child(4n - 7) {
background: #5a363a;
}
.brick:nth-child(4n - 6) {
background: #82212c;
}
.brick:nth-child(4n - 5) {
background: #3a3e41;
}
.brick:nth-child(4n - 4) {
background: #292a2b;
}
.brick {
color: white;
padding: 1rem;
}
\nThanks for reading all the way through, I hope this was useful and one day soonish we will be able to easily deal with wonky bottoms in just CSS.
\n", "date_published": "2021-01-13T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/setting-up-eslint-to-work-with-new-or-proposed-javascript-features-such-as-private-class-fields./", "url": "https://griffa.dev/posts/setting-up-eslint-to-work-with-new-or-proposed-javascript-features-such-as-private-class-fields./", "title": "Setting up ESLint to work with new or proposed JavaScript features such as private class fields.", "tags": ["QuickTip","JavaScript","ESLint","Jest","Babel"], "summary": "It turns out configuring ESLint to use Stage 3 proposals is actually a massive pain, and sends you down a rabbit hole of Babel, assumed knowledge, renamed packages and half answered questions.", "content_html": "Some members in my team this week wanted to make use of Private class fields in a NodeJS server. This proposal is currently shipped in Chrome, Edge, Firefox and NodeJS, with Safari notably absent. In this instance, we wanted to get them working for a backend server application, so support since Node 12, we're good to go, or so I thought, turns out linters aren't always here to save you time.
\nI summed my feelings on the whole process of figuring this out on Twitter.
\nhttps://twitter.com/anyuser/status/1333779719074955264
\nFeel free if you want to skip ahead past the story, and to head right to Configuring ESLint.
\nFor this article, i'll be using this code example of using Private class fields, the code used is irrelevant.
\nexport class Animal {
// this is a private class field!
#noise = \"\";
constructor(noise) {
this.#noise = noise;
}
makeNoise() {
console.log(this.#noise);
}
}
\nThe first issue we hit when writing this new code, was of course, the linter started failing, so off to Google we went!
\nA quick search for: eslint private class fields
you will most likely end up in this Stack Overflow issue.
\nIt will tell you that ESLint does not support experimental stage 3 features, which is indeed correct, and to:
npm install eslint babel-eslint --save-dev
\nand to update your ESLint config file over to use:
\n \"parser\": \"babel-eslint\",
\nSadly, it seems this is not an entire solution, it seems to make a couple of assumptions:
\nbabel-eslint
did indeed solve al the problems.If you are in a NodeJS server module, a lot of these assumptions are probably not met.
\nIf you are a developer that has never had to use Babel because you work on the backend or on a build-less frontend, all this stuff can get daunting very fast.
\nAdditionally, it seems since this answer was posted, things have moved on and the recommended parser
now lives at:
\"parser\": \"@babel/eslint-parser\",
\nThe ESLint website does have some information about the Past, Present and Future of the babel-eslint over on its website.
\nFinding this information out was a bit of an adventure, and even on the official babel or ESLint website, it's super unclear that you need to set up a babel config, and then still, what to put in it. I'm pretty sure the only reason I managed to figure it out in the end was because I'm familiar with the mess that is configuring Webpack, Jest and Babel.
\nLet's get our new syntax working!
\nFirst off, lets do the npm install
dance:
npm i eslint @babel/core @babel/eslint-parser @babel/preset-env -D
\nIt's nice to set up a linter task in your package json so you can run npm run lint
\"scripts\": {
\"lint\": \"eslint ./\"
},
\nI'm opting to use @babel/preset-env
because it has an easy way to enabled proposals that are shipped
in Browsers/Node. Other presets/plugins are available.
Next we need to construct an .eslintrc
file.
\nYou can generate one using: ./node_modules/.bin/eslint --init
or just copy this starter:
{
\"env\": {
\"browser\": true,
\"es2021\": true,
\"node\": true
},
\"extends\": \"eslint:recommended\",
\"parser\": \"@babel/eslint-parser\",
\"parserOptions\": {
\"ecmaVersion\": 12,
\"sourceType\": \"module\"
},
\"rules\": {}
}
\nNow if you run npm run lint
You will hit the following error:
/path/to/code/Animal.js
0:0 error Parsing error: No Babel config file detected for /path/to/code/Animal.js. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files
✖ 1 problem (1 error, 0 warnings)
\nIt's telling you we need to configure babel for @babel/eslint-parser
to work.
Lets set up a babel config file.
\nCreate a file called .babelrc
and populate it with:
{
\"presets\": [[\"@babel/preset-env\"]]
}
\nYou can read about @babel/preset-env
on the Babel website.
Now if you run npm run lint
again you will hit the final error:
/path/to/code/Animal.js
2:4 error Parsing error: /path/to/code/Animal.js: Support for the experimental syntax 'classPrivateProperties' isn't currently enabled (2:5):
1 | export class Animal {
> 2 | #noise = '';
| ^
3 |
4 | constructor(noise) {
5 | this.#noise = noise;
Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation.
If you want to leave it as-is, add @babel/plugin-syntax-class-properties (https://git.io/vb4yQ) to the 'plugins' section to enable parsing
✖ 1 problem (1 error, 0 warnings)
\n\n\nYou could proceed to add plugins for each of the proposals as the instructions say, alternatively you can opt to say
\nI want all shipped proposals
.
To do this change your .babelrc
over to:
{
\"presets\": [
[
\"@babel/preset-env\",
{
\"shippedProposals\": true
}
]
]
}
\n\n\nFrom the Babel docs: "set the shippedProposals option to true. This will enable polyfills and transforms for proposal which have already been shipped in browsers for a while."
\n
If you are using Jest, it will automatically pick up .babelrc
files, this might be problematic, as it will very helpfully start to try to transpile things like async/await
, potentially leading you down even more rabbit holes. With really helpful messages like:
ReferenceError: regeneratorRuntime is not defined
\nBy dumb luck, I've been through the pain of this message many times, and knew exactly what was wrong, Jest was trying to transform the perfectly valid code.
\nIt's almost 2021, and this is a server app, I certainly do not want to transpile async/await
especially not in unit tests!
One way to work around this is to use a non-standard name for your .babelrc
file e.g. .babel-eslintrc
. There may be better solutions, but I certainly don't want Jest unnecessarily transforming code.
In your .eslintrc
file you can update babelOptions
to use a custom configFile
\"babelOptions\": {
\"configFile\": \"./.babel-eslintrc\"
}
\nAnd there we go, Jest is now happy again because it's not using the Babel configuration.
\nAll in all this was a lot harder than I thought it would be, my guess is that many people don't hit this issue because they happen to already have Babel configured. But in the case of backend dev, getting along happily, just trying to make use of a shipped JavaScript feature in a server, you can get dragged into the hellscape of frontend development tooling, and no one has fun there.
\n", "date_published": "2020-12-01T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/using-the-web-share-api-and-meta-tags-for-simple-native-sharing/", "url": "https://griffa.dev/posts/using-the-web-share-api-and-meta-tags-for-simple-native-sharing/", "title": "Using the Web Share API and meta tags, for simple native sharing", "tags": ["JavaScript","11ty","Webdev"], "summary": "Adding share links for all the various places users can share content from Twitter, to LinkedIn or Reddit and many more, can be a real pain. The Web Share API is growing in support, making sharing content on the web easier than ever. For browsers that don't support the API you can fall back to self generated share targets.", "content_html": "Supporting all of various places that content can be shared on a web site is tricky, all sites have their own mechanisms for doing it, and you can never support all of the places that a user may want to share content.
\nThats where the Web Share API comes in, with it, sharing content on the Web is much more straight forward, using this API also gives your users the freedom to share content wherever they like, from chat apps and social media to a notes app.
\nAt the end of the article I go into the all important html meta tags, to make sure that sites/apps can pick up nice previews for your content.
\nif(navigator.share) {
navigator.share({
text:\"Some text to share\",
title: \"Title of what you are sharing\",
url: \"url to share\"
}).then(() => {
// do something on success
}).catch(() => {
// handle errors
});
})
}
\nAnd your user will end up with a "share sheet", where they can decide where they want to share.
\n\nAt this point you're probably thinking yeah, this is great and all but the browser support is terrible, but I think you might be surprised, the browser support is good in the place it matters most: mobile.
\n\nYeah that is a lot of red, but if you analyze the data, the support is decent in key places:
\nWe're at around 55% of global users, however if you filter to just mobile you're looking at 91.96% according to caniuse.com. Sure, the obvious missing pieces are cross platform support for Chrome and Firefox, as well as key OSs to capture non Western demographics, but you can always fallback to the older methods of sharing on browsers that don't support the API, and thats what i'll go into next.
\nThe rest of this article describes the approach that I took for implementing the Web Share API as a progressive enhancement to my blog. For browsers where the API is supported the option is offered, otherwise I added share buttons for common places where I might expect people to share e.g. Twitter.
\nFor my blog I'm using 11ty, a really flexible static site generator which encourages you to build lightweight and fast websites.
\nI wrote about my journey with 11ty over here: How I got started with 11ty.
Supporting lots of different websites for share targets is quite time consuming, each does it in different ways.
\nI started out with a bit of html templating:
\n<nav class=\"sharing\" aria-label=\"Social Sharing\">
<ul class=\"social-share\">
{%- for link in external.sharing %}
<li>
<a
class=\"icon-share url-share\"
title=\"{{link.text}}\"
target=\"_blank\"
href=\"{{link.url(title, tags, page) | url}}\"
class=\"action external\"
>
{% include link.icon %}
</a>
</li>
{%- endfor %}
</ul>
</nav>
\nI'm making use of a few things here in 11ty, Nunjucks templating and global data.
\nGlobal data files are either JSON or javascript, you can see from the code snippet that i have a sharing
array of objects and each object has the following:
Let's take a look at how this is implemented; I have the following file src/_data/external.js
, this is where the code earlier gets external
from as its variable name.
The file looks like this.
\nmodule.exports = {
sharing: [
{
icon: \"img/twitter.svg\",
text: \"Share to Twitter\",
url(title, tags = [], page) {
const twitterUrl = \"https://twitter.com/intent/tweet?text=\";
const { text, url } = genericShare.data(title, tags, page);
return `${twitterUrl}${encodeURIComponent(`${text} ${url}`)}`;
},
},
{
icon: \"img/linkedin.svg\",
text: \"Share to LinkedIn\",
url(title, tags = [], page) {
return `https://www.linkedin.com/shareArticle?mini=true&url=${getUrl(
page
)}&title=${encodeURIComponent(title)}&source=griffadev`;
},
},
{
icon: \"img/reddit.svg\",
text: \"Share to Reddit\",
url(title, tags = [], page) {
const baseUrl = \"https://www.reddit.com/submit?\";
return `${baseUrl}uri=${getUrl(page)}&title=${encodeURIComponent(
title
)}`;
},
},
{
icon: \"img/awful.svg\",
text: \"Share to Hacker News\",
url(title, tags = [], page) {
const baseUrl = \"https://news.ycombinator.com/submitlink?\";
return `${baseUrl}u=${getUrl(page)}&t=${encodeURIComponent(title)}`;
},
},
],
};
\nHopefully, you can see how the html maps over to the javascript. Each of the websites share the content in slightly different ways.
\nYou might be noticing genericShare.data
is missing, you'd be right.
Let's take a look at that function:
\nconst siteMeta = require(\"./metadata.json\");
const helpers = require(\"./helpers\");
const getUrl = (page) => encodeURIComponent(`${siteMeta.url}${page.url}`);
const genericShare = {
data(title, tags = [], page) {
const url = `${siteMeta.url}${page.url}`;
const text = `${title} ${
// get my twitter handle
siteMeta.author.twitter
} ${tags
// remove 11t built in tags from the tags for my bpost
.filter(helpers.filterCollectionTags)
// add a twitter style hashtag
.map((tag) => `#${tag}`)
// convert back to a string
.join(\" \")}`;
return {
text,
title,
url,
};
},
};
\nIn this function I'm getting the full url for my website siteMeta.url
from a metadata.json
and appending that to the current page.url
.
\nAlso in my metadata.json I have some more data about myself e.g. my twitter handle, when people share my posts I can get automatically tagged.
Finally, I added all of the tags from my front matter, that are on the blog post, over to the text for the share content too, I filter out tags that I don't want, for example tags for my 11ty collection names.
\nIf you aren't sure what i mean by front matter it's the metadata at the top of my markdown files:
\n---
title: \"Using the Web Share API for simple native sharing\"
tags:
- JavaScript
- 11ty
- Webdev
---
\nFor a full implementation reference, checkout my Github repo for my blog:
\n\nNow we've implemented the basic behavior for everyone, we can progressively enhance the website for browsers that support the Web Share API.
\nUpdating the HTML template:
\n<nav class=\"sharing\" aria-label=\"Social Sharing\">
<ul class=\"social-share\">
{% set genericShareData = external.genericShare.data(title, tags, page) %}
<li class=\"native-share\" style=\"display: none;\" hidden>
<button
data-title=\"{{genericShareData.title}}\"
data-url=\"{{genericShareData.url}}\"
data-text=\"{{genericShareData.text}}\"
aria-label=\"Native share\"
>
{% include 'img/share.svg' %}
</button>
</li>
{%- for link in external.sharing %}
<li>
<a
class=\"icon-share url-share\"
title=\"{{link.text}}\"
target=\"_blank\"
href=\"{{link.url(title, tags, page) | url}}\"
class=\"action external\"
>
{% include link.icon %}
</a>
</li>
{%- endfor %}
</ul>
</nav>
\nThe key thing that has changed from the original example is the addition of:
\n{% set genericShareData = external.genericShare.data(title, tags, page) %}
<li class=\"native-share\" style=\"display: none;\" hidden>
<button
data-title=\"{{genericShareData.title}}\"
data-url=\"{{genericShareData.url}}\"
data-text=\"{{genericShareData.text}}\"
aria-label=\"Native share\"
>
{% include 'img/share.svg' %}
</button>
</li>
\nYou'll see that similar to the example above I'm passing title
, url
and text
using data-*
attributes,
\nand executing a function my global data object external.genericShare.data(title, tags, page)
.
By default this content is hidden with display:none
, we're going to enable the content with a little bit of JavaScript.
if (navigator.share) {
const nativeShare = document.querySelector(\".native-share\");
if (nativeShare) {
// make the button visible
nativeShare.style.display = \"flex\";
nativeShare.querySelector(\"button\").addEventListener(\"click\", (e) => {
const button = e.currentTarget;
navigator
.share({
// grab the data attributes from the html
text: button.getAttribute(\"data-text\"),
title: button.getAttribute(\"data-title\"),
url: button.getAttribute(\"data-url\"),
})
.then(() => {
// show some content to say it was shared, e.g. thank the user.
nativeShare.classList.add(\"shared\");
})
.catch(() => {});
});
}
}
\nFirst, I'm checking that we have access to navigator.share
. If its available, the button is made visible, a click
handler is added and on click the data-*
attributes are read, finally navigator.share
is called.
If you wanted to, you could hide your fallback links when navigator.share
is available, I chose not to just yet, but might do down the road, when more desktop site get support for the Web Share API.
If you're wondering how to go about adding JavaScript into an 11ty project, there are many ways to go about it, I recently wrote about how to approach this for Web Components.
\nTo round all of this work out you will want to make sure that you have all of the appropriate meta tags set on your website so that sites can put the correct images/description in share link previews.
\nFor example:
\n<meta name=\"author\" content=\"{{author or metadata.author.name}}\" />
<meta property=\"og:site_name\" content=\"{{ siteTitle }}\" />
<meta property=\"og:title\" content=\"{{ pageTitle }}\" />
<meta property=\"og:type\" content=\"website\" />
<meta property=\"og:url\" content=\"{{ currentUrl }}\" />
{% if socialImage %}
<meta name=\"twitter:card\" content=\"summary_large_image\" />
<meta property=\"og:image\" content=\"{{ socialImage }}\" />
<meta name=\"twitter:image\" content=\"{{ socialImage }}\" />
<meta property=\"og:image:alt\" content=\"Page image for {{ metadata.title }}\" />
<meta name=\"twitter:image:alt\" content=\"Page image for {{ metadata.title }}\" />
{% endif %} {% if tagline %}
<meta name=\"description\" content=\"{{ tagline }}\" />
<meta name=\"twitter:description\" content=\"{{ tagline }}\" />
<meta property=\"og:description\" content=\"{{ tagline }}\" />
{% endif %}
\nYou can see my full metadata over on Github.
\nSupport for the Web Share API is growing and with it progressive enhancement is a really great approach to building modern websites, I wrote about using this approach for fast sites, with interactivity sprinkled in: Using Web Components With 11ty.
\n", "date_published": "2020-10-10T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/tips-for-debugging-in-11ty/", "url": "https://griffa.dev/posts/tips-for-debugging-in-11ty/", "title": "Tips for debugging in 11ty", "tags": ["QuickTip","JavaScript","11ty","VSCode"], "summary": "Some quick debugging tips that I came up with whilst building my personal blog.", "content_html": "The 11ty documentation is really great, but when it comes to developing and debugging, I really prefer to use a live debugger.
\nIt can help to understand a problem, but also can serve as a really great way of understanding how you might go about hooking into a framework, in this case 11ty.
In my config for 11ty projects I put in this little debugging filter.
\n eleventyConfig.addFilter(\"debugger\", (...args) => {
console.log(...args)
debugger;
})
\nYou could use this filter to find out the available attributes on a post, for you to use in your template, for example.
\nI have a hero object on my posts:
\n---
title: \"Tips for debugging in 11ty
hero:
image: \"/images/debugging-11ty.png\"
alt: 'Picture of vscode debugger and 11ty debugging filter'
---
\nIn my code I can check out the variables, and have them logged to the console.
\n{{ post.data.hero | debugger }}
\nThis might seem really silly, but I found this incredibly useful, especially when hooked up with a live debugger (shown later).
\nOn save of a file, I get the object that was passed to the filter, this can be really handy in seeing what variables you have to play with.
Example console.log
output:
{
[1] }
[1] Writing dist/tags/design/index.html from ./src/tags.md.
[1] {
[1] image: '/images/design-day1.png',
[1] alt: 'Picture of day 1 design of blog'
[1] }
[1] Writing dist/tags/research/index.html from ./src/tags.md.
[1] undefined
[1] Writing dist/tags/webcomponents/index.html from ./src/tags.md.
[1] {
[1] image: '/images/custom-theme.png',
[1] alt: 'Picture of using Chrome dev tools to adjust CSS variables',
[1] show: false
[1] }
[1] Writing dist/tags/accessibility/index.html from ./src/tags.md.
[1] {
[1] image: '/images/custom-theme.png',
[1] alt: 'Picture of using Chrome dev tools to adjust CSS variables',
[1] show: false
[1] }
[1] Writing dist/tags/ux/index.html from ./src/tags.md.
\nWhere this becomes really great is if you use the VSCode debugger to run 11ty, as soon as you use the filter and hit save because of the debugger
statement you go right to the debug filter.
Sure you could console.log
, but using the VS Code debugger for this can be extremely helpful to do some of that variable treasure hunting.
To do this, you will need to create a new debug task, you can do this in the UI, but a quick way is to:
\n.vscode
.vscode
folder called launch.json
eleventy
runner, here is an example: {
\"name\": \"11ty\",
\"type\": \"node\",
\"request\": \"launch\",
\"program\": \"${workspaceRoot}/node_modules/.bin/eleventy\",
\"stopOnEntry\": false,
\"args\": [],
\"cwd\": \"${workspaceRoot}\",
\"console\": \"internalConsole\",
}
\nIf you want to put it in live watch mode, set args to:
\n \"args\": [\"--watch\"],
\n11ty
, set your breakpoints up and good to debug.This approach also works really well with writing custom collections, or using data.
\n eleventyConfig.addCollection(\"series\", function(collection) {
// i dont know whats in collection.
debugger;
});
\nPutting this together, the following GIF shows how this can be a nice interactive workflow.
\n\nI hope this was helpful to people, 11ty is so lightning fast, that marrying it with the VS Code debugger when you're writing JavaScript is super interactive.
\n", "date_published": "2020-10-09T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/using-web-components-with-11ty/", "url": "https://griffa.dev/posts/using-web-components-with-11ty/", "title": "Using Web Components With 11ty", "tags": ["11ty","WebComponents","JavaScript"], "summary": "I took a progressive enhancement approach to using frontend javascript for my blog, Web Components are the perfect fit here.", "content_html": "I've talked about earlier in this series that I wanted to bring things back to basics with this blog, focusing on web fundamentals e.g. html and css. In addition to this, by using 11ty, I'm able to author in Markdown, meaning I'm free to add HTML anywhere in my posts.
\nHowever, whist I'm focusing on HTML/CSS, there are areas where it makes sense to sprinkle in JavaScript, for extra interactivity, this is where Web Components come in.
\n\nA Google engineer said it better than I could:
\nhttps://twitter.com/anyuser/status/1212847104718061569
\nIn this article I'll explain how I went about setting up a development environment for Web Components, as well as simple production optimizations.
\nBut first, I want to discuss the approach that I've taken for consuming web components in this site. All content should be available without JavaScript/Web Components available, but where they are available, the content should be progressively enhanced.
\nHere are a couple of uses cases I had for progressively enhanced content, using JavaScript.
\nTo embed a YouTube video via progressive enhancement, you first need to identify what is the minimal HTML-only implementation of the content, this is:
\nThe second part of this is identifying a component to use to embed the YouTube player, I wasn't going to re-invent the wheel here.
\nlite-youtube-embed from Paul Irish, is the perfect fit here.
\nnpm install lite-youtube-embed
<lite-youtube
class=\"video\"
videoid=\"j8mJrhhdHWc\"
style=\"background-image: url('https://i.ytimg.com/vi/j8mJrhhdHWc/hqdefault.jpg');\"
>
<a
onclick=\"('customElements' in window) && event.preventDefault()\"
title=\"Play Video\"
class=\"no-js\"
target=\"_blank\"
href=\"https://youtube.com?w=j8mJrhhdHWc\"
><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">
<g transform=\"translate(-339 -150.484)\">
<path fill=\"var(--White, #fff)\" d=\"M-1978.639,24.261h0a1.555,1.555,0,0,1-1.555-1.551V9.291a1.555,1.555,0,0,1,1.555-1.551,1.527,1.527,0,0,1,.748.2l11.355,6.9a1.538,1.538,0,0,1,.793,1.362,1.526,1.526,0,0,1-.793,1.348l-11.355,6.516A1.52,1.52,0,0,1-1978.639,24.261Z\" transform=\"translate(2329 150.484)\"/>
<path fill=\"var(--Primary, #000)\" d=\"M16.563.563a16,16,0,1,0,16,16A16,16,0,0,0,16.563.563Zm7.465,17.548L12.672,24.627a1.551,1.551,0,0,1-2.3-1.355V9.853a1.552,1.552,0,0,1,2.3-1.355l11.355,6.9A1.553,1.553,0,0,1,24.027,18.111Z\" transform=\"translate(338.438 149.922)\" />
</g>
</svg></a
>
</lite-youtube>
\nThere's a couple of things going on above:
\n<a>
by default, this will open the youtube video in a new tabExplaining the onclick: Whats happening here is.
\ncustomElements
is supported in the browser.lite-youtube
, resulting in a youtube embed.Like so:
\nAt some point I will have to do a post that goes into more detail of exactly how my live demos are authored using Markdown in 11ty, but they are ultimately rendered using a web component.
\nLet's get meta, here is a Live demo web component that renders itself.
\n\n
<live-demo id=\"my-live-demo\">
<div slot=\"html\">
<div class="my-div">styled by the css</div>
</div>
<div slot=\"css\">
.my-div {
color: var(--Primary, blue);
}
</div>
</div>
\nlive-demo {
width: 400px;
height: 300px;
margin: 3rem;
min-height: auto;
display: flex;
}
\nThe approach I've taken here is that when the web component is not available, the code is just rendered and syntax highlighted, but when JS is available a live demo component appears. If you were to disable JavaScript in your browser you should just see the code snippets instead.
\nI made use of slots, one for js
one for html
and one for css
. The web component then takes the text content and renders it appropriately.
This approach is a lot
like https://open-wc.org/mdjs/, which I hope to use in the future for my blog, but it was fun to see about how I could build this myself.
Setting up a development environment for 11ty and web components is pretty simple, especially if are using pure JavaScript, and don't need any build process. I found that having no build process was such a breath of fresh air, development tools should just get out of your way and let you code.
\n\n\nIf you are just working with vanilla web components and don't want to use any dependencies from NPM, then good news, you don't need to do anything special, just use the default 11ty dev server, and move on to create great content!
\n
If you want to use some components or libraries from NPM e.g. lit-html/lit-element you will need a way to transform bare imports
into relative urls that work in the browser, e.g.
import { LitElement } from \"lit\";
\nwould become something like:
\nimport { LitElement } from \"./../node_modules/lit-element/lit-element.js\";
\nThe best tool for doing this is https://www.npmjs.com/package/es-dev-server.
\n\n\nAt the time of writing this tool is in the process of getting moved over to @web/dev-server. For this example, i'll use
\n@web/dev-server
butes-dev-server
would work too.
npm i --save-dev @web/dev-server
\nFirst off, when serving an 11ty website you would normally use npx eleventy --serve
, however instead we're going to use npx eleventy --watch
.
\nThis will give us all the live building of your 11ty site, but without a server.
For our server, this is where @web/dev-server
will come in, which can be run like so:
web-dev-server --node-resolve --open
\nIn order to combine these two tasks we can use concurrently
npm i concurrently --save-dev
\nand combine them into a npm script:
\n \"start\": \"concurrently \\\"npx eleventy --watch\\\" \\\"web-dev-server --node-resolve\\\"\",
\nCombining the above will give us a dev server, however we have not told it how find our 11ty _site
folder, as well as resolving our node modules.
\nIn order to do this we will need to introduce a small config file and implement a simple middleware to do the following:
_site
by appending _site
to url._site
next()
which will allow JS files to be handled by logic to resolve ESM imports.Create a file call web-dev-server.config.js
module.exports = {
port: 8000,
watch: true,
rootDir: \".\",
middleware: [serve11tyAssets({ dist: \"_site_\" })],
nodeResolve: true,
};
\nThis should all be quite straight forward to understand hopefully:
\nnode_modules
and the 11ty _site
folder.import foo from 'bar'
serve11tyAssets
will look something like this.
const path = require(\"path\");
const fs = require(\"fs\").promises;
const URL = require(\"url\").URL;
/**
*
* Check if asset lives in 11ty _site folder, if not serve from root folder.
*/
const serve11tyAssets = ({ dist = \"_site\" } = {}) => {
return async (context, next) => {
// Node URL requires a full url so... whatever.com (url isnot important)
const pathName = new URL(`https://whatever.com${context.url}`).pathname;
// is the request for a html file?
const url = pathName.endsWith(\"/\") ? `${pathName}index.html` : pathName;
try {
// check if the file exists, if so, modify the url to come from `_site` folder.
const stats = await fs.stat(path.join(dist, url));
if (stats.isFile()) {
context.url = `/${dist}${pathName}`;
}
return next();
} catch {
return next();
}
};
};
\nHopefully this example makes sense, and shows how simple it is to add vanilla JavaScript modules into your 11ty development server.
\nYou can easily add new tools into this chain if you need as well e.g. gulp
\"start\": \"npx gulp && concurrently \\\"npx gulp watch\\\" \\\"npx eleventy --watch\\\" \\\"web-dev-server\\\"\"
\nWhen it comes to choosing tools to optimize your JavaScript for an 11ty project, the choice is entirely up to you, if like me you don't want to configure a complex build, you can leverage the great work of others, by using Open WC rollup config.
\nHere is my config.
\nnpm i rollup deepmerge rollup-plugin-output-manifest @open-wc/building-rollup -D
\nimport merge from \"deepmerge\";
import { createBasicConfig } from \"@open-wc/building-rollup\";
import outputManifest from \"rollup-plugin-output-manifest\";
const entrypoints = {
index: \"src/assets/index.js\",
};
const baseConfig = createBasicConfig({
outputDir: \"dist/assets\",
});
export default merge(baseConfig, {
input: entrypoints,
plugins: [
outputManifest({
// ../ to go outside of dist and into include
fileName: \"../../src/_includes/manifest.json\",
// assets is my folder of choice for js files
publicPath: \"assets/\",
}),
],
});
\nYou can add extra entrypoints, which is helpful, if you only want to load some components on some pages.
\nIn order to hook this back into 11ty
I'm making use of rollup-plugin-output-manifest
. This outputs a manifest.json
file.
\nYou could output this as a data file if you wanted to, but I wanted to add a little more logic to my scripts so I could do different things depending on if in production
mode or not.
Create a file called src/_data/assets.js
, which will be read as Global Data File.
module.exports = {
getPath: (assetName) => {
if (process.env.NODE_ENV === \"production\") {
const assets = require(\"../_includes/manifest.json\");
const modulePath = assets[assetName];
if (!modulePath) {
throw new Error(
`error with getAsset, ${assetName} does not exist in manifest.json`
);
}
return `/${modulePath}`;
} else {
return `/src/assets/${assetName}`;
}
},
};
\nThen in 11ty templates:
\n <script src=\"{{ assets.getPath(\"index.js\")}}\" type=\"module\"></script>
\nDoing this allowed me to just serve the unmodified src code when in development, but embed the production assets, which have hashes in their names for cache busting.
\nIf you are wondering how to set the NODE_ENV flag, here is my build script.
\n \"build\": \"rm -rf dist && NODE_ENV=production rollup -c rollup.config.js && NODE_ENV=production npx eleventy\"
\nAnd that's my setup, I'm sure there are better ways of doing this but it got the job done for me, hopefully this was useful.
\n", "date_published": "2020-10-07T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/how-i-got-started-with-11ty/", "url": "https://griffa.dev/posts/how-i-got-started-with-11ty/", "title": "How I got started with 11ty", "tags": ["11ty","JavaScript"], "summary": "I work with React and painfully slow Webpack builds every day, using 11ty for my blog was a breath of fresh air. By using the 11ty starter projects and awesome courses and tutorials for help, I felt like I had superpowers!", "content_html": "This article details some of the different resources that I used for learning 11ty to build out my personal website. This is not a tutorial, it is more of a description of the journey that I went on, with the hope some of the steps that I took might help others.
\nWhen building griffa.dev I wanted to bring things as back to basics as much as I could.
\nI wanted to write:
\nI wanted to have:
\nI did not want:
\nWith all of the above in mind, I did some research, some of which I went into in Part 1 of this series, and I came to the conclusion that I wanted to use a static site generator, and the tool that I chose was Eleventy (11ty).
\n\n\n\nOkay just to get this out of the way, the only thing i dislike about 11ty is, I'm never sure whether to write it as 11ty or Eleventy, and I think the creator is just trolling us because even the twitter account is
\n@eleven_ty
. Well, "ty" Zach, "ty" for Eleventy.
To quote its website, "Eleventy is a simpler static site generator", and simple is right, you can start with a single markdown file, and generate a HTML page from that.
\nnpm install -g @11ty/eleventy
echo '# Page header' > README.md
eleventy
\nSo thats a tick for:
\n✅ Author in a portable format e.g. HTML/Markdown, so I could post my content to other locations.
\n✅ As minimal build as possible.
On the build front, obviously things can get more complex, but starting from zero code and zero config is a big win for me, I personally really dislike when you get started on a project and the first advice is to install a few hundred dependencies (see Create React App).
\n"Eleventy is not a JavaScript framework—that means zero boilerplate client-side JavaScript."
\n✅ Little to no frontend JavaScript and frontend build tools (Webpack/Rollup etc)
For customization 11ty has:
\n.eleventy.js
which is a configuration format, which has a plugin architecture with an active ecosystem.// .eleventy.js
module.exports = (eleventyConfig) => {
const pluginSyntaxHighlight = require(\"@11ty/eleventy-plugin-syntaxhighlight\");
eleventyConfig.addPlugin(pluginSyntaxHighlight);
};
\nHere is an example use of data files: https://github.com/Georgegriff/griffadev/blob/main/src/_data/external.js
\nIn this code I am adding my external links and external share data, which will be available in my templates.
I can then use this external data in my templates, it's all pretty easy.
\n<nav aria-label=\"Social Sharing\">
<ul class=\"social-share\">
{% set genericShareData = external.genericShare.data(title, tags, page) %}
<li>
<button
class=\"native-share\"
style=\"display: none;\"
hidden
data-title=\"{{genericShareData.title}}\"
data-url=\"{{genericShareData.url}}\"
data-text=\"{{genericShareData.text}}\"
aria-label=\"Native share\"
>
{% include 'img/share.svg' %}
</button>
</li>
{%- for link in external.sharing %}
<li>
<a
class=\"icon-share url-share\"
title=\"{{link.text}}\"
target=\"_blank\"
href=\"{{link.url(title, tags, page) | url}}\"
class=\"action external\"
>{% include link.icon %}</a
>
</li>
{%- endfor %}
</ul>
</nav>
\nIf you are thinking doesn't look like HTML to me, you'd be right, these are templating languages,
\n11ty supports many of them, {% is Nunjucks and {{}} is liquid. You can quite happily swap between templating engines in 11ty, using multiple at once.
Also, if you are wondering what class="native-share" style="display: none;"
is all about in the HTML, this is a progressive enhancement for using the native navigator.share
browser API. If you are interested in my approach for implementing social sharing, I plan on writing about this... sometime.
Both config and data files can be NodeJS modules, unlocking the entire JavaScript ecosystem at build time, without passing any cost into users of your site.
\n✅ Ability to customize site generation in JavaScript, because that is what I'm most comfortable in.
\nIt's fair to say that even with the documentation site starting point, I wasn't feeling very confident in where to start to build out my website. That's not a slight on the docs, the docs are great, I just tend to learn best in tutorial based formats. To YouTube I go!
\nAfter a quick "learn 11ty" search in YouTube I came across the following video:
\nIt's a long video, but well worth it, the video format is a really relaxed pair programming session and has the creator of 11ty helping a newbie hack around in 11ty. One of things that I found most impressive with 11ty was that Jason was able to just write files and most things just worked.
\nI was so excited after this video, I felt like i'd been given superpowers!
\nhttps://twitter.com/anyuser/status/1292911029865906177
\nOne of the best things about the 11ty ecosystem is its curated collection of starter projects, these range from simple to very complex, for example by incorporating performance optimizations.
\nI know it might seem like I'm contradicting myself a bit from earlier, from when I complained about complex scaffolded projects, but here's how I tackled both learning and scaffolding out my own project.
\nSome of the most important concepts to get to grips with are:
\nA few days into this project, some drama happened over in React/Gatsby land on twitter, and as a result I came across this amazing course: Learn Eleventy From Scratch
\nThis course is seriously awesome, it's also a stealth CSS course. One thing to note is this is a text based course, which i'd never really done before, but I actually really liked because I was able to move at my own pace. I found that doing this course allowed me to really solidify my understanding of 11ty, so that I was able to bring together influences from various project and tutorials, and change them to work for me, to build out my site.
\nOver the course of a month or two, just using my evenings after my day job and some weekends, through the hell-scape of 2020, I built out griffa.dev, and had a lot of fun along the way. I won't lie and pretend that everything went smoothly, I get distracted very easily, the first few days working on it went something like this:
\nhttps://twitter.com/anyuser/status/1295057860397850625
\nThe source code for my blog is available here: https://github.com/Georgegriff/griffadev/
\nThe first commit where I scaffolded out my project. You'll notice some of the code is from the base starter and some from the awesome course from Piccalilli I mentioned earlier, and some from just hacking around and finding what works.
\nIn the next part in this series I'll go into some detail on how I added "a little" frontend JavaScript as progressive enhancements only, to keep the site as fast as possible.
\n", "date_published": "2020-10-06T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/using-css-custom-properties-for-color-and-theming/", "url": "https://griffa.dev/posts/using-css-custom-properties-for-color-and-theming/", "title": "Using css custom properties for color and theming", "tags": ["Design","Accessibility","UX"], "summary": "I'm by no means a designer, but with a bit of research into color contrast and accessibility, making I was able to build out a design that I was happy with.", "content_html": "In the previous post in this series I detailed my approach to research and design, for this building a personal blog series. This time, I'm going to provide some detail on how I approached choosing a color scheme for the site, which next to layout and font, is up there with the most important things to get right on a site.
\n\n\n\nOh, I forgot to mention, this site is just an excuse for me to share photos of my cats, the above is Beau, he loves to meow for attention when comes back inside the house.
\n
Choosing an initial color palette is something that I struggle with, thankfully, there are lots of sites out there for inspiration. One great one is colorinspo, a great way do use this site is to have a bit of a play with possible colors schemes to find ideas you like, if you are using a design tool like Adobe XD, you could experiment with colors by duplicating your designs to create variants.
\nThe video linked above from DesignCourse is a great example of doing exactly that.
\nWhen it comes to implementing your theme in code I strongly recommend that you use CSS custom properties, they are supported everywhere that matters, and are great way to introduce a variety of colors schemes for your site.
\nIf you want you can use SASS too, for example:
\nbody {
--Primary: #${myColor}
}
\n\n\nTip: also use custom properties as fills in your embedded svgs and have those automatically respond to your theme selection!
\n
If you build a site from the ground up with theming in mind, then you can rapidly prototype new experimental color schemes, here's a theme I generated just by having a quick play in chrome. In the picture you can really see the power of using CSS custom properties in everywhere, including in SVG.
\n\nWhen I use custom properties for theming, I tend to start off with a small set of colors e.g. --Text, --Background, --Primary, --Secondary and expand as needed, for example you might want --Primary50 which could be a darker variant of your primary color.
\nAs I mentioned, I'm no designer, I don't know any rules around making sure a variable maps perfectly to a shade on a color palette, but I suspect as your site grows you'll start to get a feel for how many colors you need.
The most important aspect of your color choice should be ensuring that you have sufficient color contrast for your text against your background color.
\nThis is not just to produce a nice looking design, but to also make sure that it is accessible to everyone.
You can go about this in many ways, if you like to design in the browser, or just like hacking around in dev tools, I found that using Chrome dev tools for checking color contrast was extremely effective. There are also plugins available in design applications, such as Adobe XD.
\n\nTo use chrome to debug color contrast, I open up a random website, inspect an element that contains text e.g. a title, and then set its background
and color
properties. Dev tools will then provide you with color contrast information.
When modifying the color you should see a Contrast Ratio:
\nOne of things I knew I wanted to do with my blog site was support a light and dark theme, using the CSS Custom properties technique above this is extremely simple to do, in a later article in this series I will show how to hook up the custom properties into a theme toggle.
\nSomething, however, that I found quite challenging was find a primary branding accent color that worked in both light and dark theme, as well as being AAA accessible.
\nA technique which I found to work quite well is to choose your light theme primary color, and then adding white to the color (move towards white in color picker), to either find a sweet spot that works for both, or to generate an accent color specifically for darker themes. I also found that sticking to neutral (black/white) colors for my text went a long way to making sure I could get accessible text colors.
\n\nTip: It is now possible to automatically check what users color scheme preference is using prefers color scheme, we will explore this later on the series.
\n
When you find colors you are happy with be sure to check them for sufficient contrast against your text colors.
\nThese the colors I went with for my first iteration:
\n* {
color: black;
--Primary: #dd0d00;
--Secondary: #e0dd6f;
}
div {
background: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
height: 100%;
width: 100%;
}
svg {
width: 50px;
height: 50px;
}
\n<div>
<p>
This content will change
<strong style=\"color:var(--Primary)\">color</strong>
<span style=\"color:var(--Secondary);\"
>when you change the --Primary or --Secondary</span
>
</p>
<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">
<g transform=\"translate(-339 -150.484)\">
<path
fill=\"var(--Secondary, #fff)\"
d=\"M-1978.639,24.261h0a1.555,1.555,0,0,1-1.555-1.551V9.291a1.555,1.555,0,0,1,1.555-1.551,1.527,1.527,0,0,1,.748.2l11.355,6.9a1.538,1.538,0,0,1,.793,1.362,1.526,1.526,0,0,1-.793,1.348l-11.355,6.516A1.52,1.52,0,0,1-1978.639,24.261Z\"
transform=\"translate(2329 150.484)\"
/>
<path
fill=\"var(--Primary, #000)\"
d=\"M16.563.563a16,16,0,1,0,16,16A16,16,0,0,0,16.563.563Zm7.465,17.548L12.672,24.627a1.551,1.551,0,0,1-2.3-1.355V9.853a1.552,1.552,0,0,1,2.3-1.355l11.355,6.9A1.553,1.553,0,0,1,24.027,18.111Z\"
transform=\"translate(338.438 149.922)\"
/>
</g>
</svg>
<div></div>
</div>
\nNow i'd explored how I chose colors for my personal blog, my next steps involved investigating illustrations, to make my design pop! Undraw is an awesome tool for this, however, I strongly recommend making your own illustrations, or heavily customizing illustrations that you find, them make them your own.
\n", "date_published": "2020-09-09T00:00:00+00:00" },{ "id": "https://griffa.dev/posts/good-research-planning-and-design-is-the-best-foundation-for-starting-a-new-project/", "url": "https://griffa.dev/posts/good-research-planning-and-design-is-the-best-foundation-for-starting-a-new-project/", "title": "Good research, planning and design is the best foundation for starting a new project", "tags": ["Design","Research","11ty"], "summary": "When I've started side projects in the past, I felt like I dug into code too quickly, without thinking about what I actually was building first, I wanted to try something a bit different this time.", "content_html": "I've been working with web technologies for the best part of a decade now, from my first terrible attempts at building websites in tools like Adobe Dreamweaver
, to building complex apps in my day jobs, using frontend and backend technologies (a lot of JavaScript
).
However, something I've never had is a place of my own on the internet to just put stuff, so that's what I wanted to build, also it's probably another place I can just post pictures of my cats.
\nThis article details some of the first steps that I took along the journey to building griffa.dev.
\n\n\nIt's at this point of writing this blog post I got very distracted, trying to find an appropriate picture. Maybe I should have titled this post: How to not get distracted whilst working on a new project, it even occurred to me to write a component to auto generate a picture of my cats, probably would've been worth it.
\n
Not very original, I know, but as part of this project I wanted to try and strip things back to basics, escape from the Webpacky Reacty Hook Hell
of modern web development, and just build a nice static site.
I watch a lot
of YouTube, mostly Gaming related content, but over the years I've watched several programming related channels, one of the best, and most well known, is Traversy Media.
The specific video that got me really inspired to build something myself was a guest video by Design Course, another great channel. This video spoke to me because I'm quite happy coding but I've always wanted to brush up on my design skills.
\nThe above video, sent me on a bit of a binge of Design Course content, it all helped me a lot with focusing on good design, instead of getting bogged down by code.
\nSome of the principles the videos taught me:
\nThis doesn't really do the videos justice, I can only recommend watching them:
\nA quick one, but I found setting up a simple project on Github really helped me with tracking my progress and ideas, it also gave a nice feeling moving things over to done.
\nI made a label for posts
, which meant it was easy for me to track ideas for things I wanted to write about.
I also made heavy use of a Notes app throughout the project, which really helped with not just a todo list, but documenting any bugs I found whilst using the site.
\n\nThere was a another recent guest video on Traversy Media which describes some of the techniques I have used for planning out this project, as well as some other great advice.
\nI don't want technology choice to be a focus of this post, I wanted to focus on things I did to plan and design my project.
\nHowever, it's a good idea to search around for prior art, or libraries/frameworks you might want to consider.
\n\nYou should be using a static site generator for a personal blog, starting out with a frontend JavaScript framework, is more than likely just a bad idea.
\n
The subject of the article mentions:
\n\n\nI felt like I dug into code too quickly, without thinking about what I actually was building first, I wanted to try something a bit different this time.
\n
By this, I mean I've tried diving straight into code and designing in the browser, for me, this hasn't worked, I'm sure it works for other people.
\nI think the main reason that this does not work for me is that I can get far too distracted too easily, especially when coding, focusing on things that don't really matter, instead I choose to use a design tool.
I looked around for what design tools are out there, the big one most people seem to recommend is Sketch, or React hipsters suggesting Figma.
\nFor my work, I chose Adobe XD, why - it has a free version!
.
This was the very first time i'd used Adobe XD and I got on extremely well with it, I think if you have used any of the other Adobe products e.g. Photoshop or Illustrator, you will get to grips pretty fast, especially since some of the shortcuts are the same!
\nIf you want to try it out I can only recommend (again) watching UI Design For Coders Adobe XD is used there, watching this video gave me most of the tools I needed to build out a design.
\nAfter first day of research and design I had a first pass at a mobile layout for a potential blog site.
\n\nIn the next post I'll explore how I iterated on the above design, implementing dark and light themes, using CSS variables.
\n", "date_published": "2020-09-08T00:00:00+00:00" } ] }