Adobe Firefly in Beta

I'm currently at Adobe Summit and this morning at the keynote we announced the beta of Adobe Firefly. Firefly is a generative AI service, which by itself isn't new, but Firefly has a strong commitment to being a more responsible AI service. You can read more about that and Adobe Sensei here if you would like. As an Adobe employee, I'm obviously biased, but the focus on having a responsible service that respects creators feels like a pretty important differentiator. While there are a lot of good uses for this, I decided to have some fun with it and see how well it would do something business-critical... with cats.For my test, I looked up a list of Dungeons and Dragons classes and found this excellent list here. I then went to Firefly and used prompts like so:dungeons and dragons <class> as a catIn general, this worked well, but sometimes I added a bit to get things a bit closer to what I had expected. The "physical" classes looked pretty similar and I probably could have given a bit more context to help Firefly out, but I still found the results delightful.Here's a Fighter:Here's a Paladin:The Barbarian, which is very similar, and could have been improved if I asked for a common weapon like an axe:Now for something really fun - the Bard. The double-sided lute was crazy and I absolutely loved it:Next up is the Cleric:For Druid, I specifically asked to add a "leafy staff", and the result wasn't what I expected, but I loved it:Here's the Ranger - I added "bow and arrow" and the result was much more stylish than I expected:Next is the Monk:The Rogue ended up being my favorite, it looks like they're carrying the severed head of someone they just assasinated:Finally, while there are multiple magic-using classes in D&D, I went with simple and just used "wizard" as a prompt. The eyes on this one are crazy good:This was fun, and if you want to try it yourself, head over to the site and request Beta access. I'm not sure how long it will take to get in, but if you are interested, sign up as soon as possible.Photo by Amauri Mejía on Unsplash

From: Raymond Camden

Links For You

Hello friends, tomorrow I'm heading out to Vegas for Adobe Summit, so I expect the posting to be a bit light this week.Automating your Mastodon profile with Pipedream.comHere's a great article that talks about using one of my favorite services, Pipedream to automate the updating of a Mastodon profile. I really like Mastodon and the flexibility of its API is pretty great. I've been focused on writing bots, but I love how Stefan uses it to update his profile instead. Check out his article and see for yourself.WebComponent * 2I use an Evernote note to keep track of the links I want to share, and for some reason, these two links have been in my queue for a few months now. They kept getting pushed down by new awesomeness. Today I look to fix that.First up is Awesome Web Components, a huge list of web component articles hosted as a GitHub repository. Sometime this week I need to find time to contribute a few of my articles to it.Next up is a set of toots tagged, WebComponentsAdvent. I'm a big fan of the "Advent of X" type format as a way of sharing daily tips about a technical topic. (I also like my beer and wine Advent collections too, neither of which we've finished.) When clicking the link, be sure to scroll down to the bottom to read in the right order.And now for something completely unnecessary...I know what you're thinking, "My office is missing something, Raymond, do you have any suggestions?" Why yes, I do. What about some lit-up jellyfish that dance in a tube of water? Just head over to Amazon to pick up the jellyfish lava lamp that's been in my office this past week and I absolutely love it. My wife and I saw it recently in the background of a YouTube video, searched on Amazon, and pick it up literally mid-video. It's cheap, and a lot of these kinda things don't work as well as the product page says, but this one's been a true delight.Here is a picture of it in my office, but honestly it's not a great picture and it looks a heck of a lot cooler in motion:

From: Raymond Camden

Another Week, Another Mastodon Bot - Random Album Cover

Last September, I blogged about how I used the Spotify API and Pipedream to discover new music: Discover New Music with the Spotify API and Pipedream. I used a Pipedream workflow to select a random track from Spotify and email me a track every morning. I've still got this process running and I enjoy seeing it every morning. More recently, I noticed a cool bit of album art in my Spotify client and it occurred to me that it would be kind of cool to see more. With that in mind, I present to you my latest Mastodon bot, Random Album Cover. You can see an example toot here: Random Album Cover @randomalbumcover@botsin.space Album: Steven Universe The Movie (Original Soundtrack) (https://open.spotify.com/album/3RDksSu4beLjc45EKe5vYO)Artist(s): Steven UniverseReleased: 2019-09-02.img-3bb9733e11f448523670adcbd1248d3f {aspect-ratio: 640 / 640} 5:16 PM • March 17, 2023 (UTC) I have no idea what you'll see when viewing this post as it will be generated during a build, but I'm looking at a striking album cover from an artist I've never heard of, NLE Choppa. So, how was it built?For the most part, it follows the logic of my previous post, doing the following:Select a random letterRandomly decide to make it the beginning of a search string ("A something") or in the middle ("something A something")Select a random number between 0 and 1000Hit the Spotify API. Their API doesn't have a "real" random search, but we use the random letter and offset to search.Given our set of results, select a random record from that.All of the above hasn't changed from the previous post, except I switched the search from track to album. Next, I download the image to a temporary directory. This is straight from the Pipedream samples:import stream from "stream";import { promisify } from "util";import fs from "fs";import got from "got";export default defineComponent({ async run({ steps, $ }) { const pipeline = promisify(stream.pipeline); return await pipeline( got.stream(steps.select_random_album.$return_value.images[0].url), fs.createWriteStream('/tmp/cover.jpg') ); },})And then I post the toot. This code is pretty short as it makes use of the excellent mastodon-api package. My only real work is crafting the text to go along with the image.import Mastodon from 'mastodon-api'import fs from 'fs'export default defineComponent({ async run({ steps, $ }) { const M = new Mastodon({ access_token: process.env.RANDOMALBUMCOVER_MASTODON, api_url: 'https://botsin.space/api/v1/', }); let artists = steps.select_random_album.$return_value.artists.reduce((cur, art) => { if(cur == '') return art.name; return cur + ', ' + art.name },''); let toot = `Album: ${steps.select_random_album.$return_value.name} (${steps.select_random_album.$return_value.external_urls.spotify})Artist(s): ${artists}Released: ${steps.select_random_album.$return_value.release_date} `.trim() let resp = await M.post('media', { file: fs.createReadStream('/tmp/cover.jpg') }); await M.post('statuses', { status: toot, media_ids: [resp.data.id] }); },})I just want to go on record as saying that this is like the third or fourth time I've used reduce without checking the docs and I'm definitely a JavaScript expert now. Definitely.I'll point out that I spent maybe thirty minutes total on this. The longest wait for was the Mastodon instance to approve my bot (maybe 1.5 hours). I also spent more than a few minutes wondering why my Python code wasn't running in a Node step, so maybe I'm not an expert. Maybe.If you want to check out the complete workflow, you can do so here: https://pipedream.com/new?h=tch_m5ofq7

From: Raymond Camden

Progressively Enhancing a Table with a Web Component

Back nearly a year ago (holy smokes time goes fast), one of my first articles about web components involved building a component to create a paginated/sorted table: Building Table Sorting and Pagination in a Web Component. In that example, the component looked like so in your HTML:<data-table src="https://www.raymondcamden.com/.netlify/functions/get-cats" cols="name,age"></data-table>I thought this was cool, but one big issue with it is that if JavaScript is disabled, or if something else goes wrong with the code, then absolutely nothing is rendered to the page. This got me thinking - what if I could build a web component that enhanced a regular HTML table? Here's what I came up with.First, I set up a table of simple data:<table> <thead> <tr> <th>Name</th> <th>Breed</th> <th>Gender</th> <th>Age</th> </thead> <tbody> <tr> <td>Luna</td> <td>Domestic Shorthair</td> <td>Female</td> <td>11</td> </tr> <tr> <td>Elise</td> <td>Domestic Longhair</td> <td>Female</td> <td>12</td> </tr> <tr> <td>Pig</td> <td>Domestic Shorthair</td> <td>Female</td> <td>8</td> </tr> <tr> <td>Crackers</td> <td>Maine Coon</td> <td>Male</td> <td>5</td> </tr> <tr> <td>Zuma</td> <td>Ragdoll</td> <td>Male</td> <td>8</td> </tr> <tr> <td>Lord Fluffybottom, the Third</td> <td>Domestic Longhair</td> <td>Male</td> <td>8</td> </tr> <tr> <td>Zelda</td> <td>Domestic Shorthair</td> <td>Female</td> <td>7</td> </tr> <tr> <td>Apollo</td> <td>Persian</td> <td>Male</td> <td>3</td> </tr> </tbody></table>Note that I make use of both thead and tbody. I'm going to require this for my component to work, but outside of that, there's nothing special here, just a vanilla table. Now let's look at my component. First, I'll name it table-sort:class TableSort extends HTMLElement { // stuff here..}if(!customElements.get('table-sort')) customElements.define('table-sort', TableSort);In my constructor, I'm just going to set up a few values. One will hold a copy of the table data, one will remember the last column sorted, and one will be a boolean that indicates if we're sorting ascending or descending:constructor() { super(); this.data = []; this.lastSort = null; this.sortAsc = true;}Alright, now for some real work. In my connectedCallback, I'm going to do a few things. First, I'll do a sanity check for a table, thead and tbody inside myself:connectedCallback() { let table = this.querySelector('table'); // no table? end! if(!table) { console.warn('table-sort: No table found. Exiting.'); return; } // require tbody and thead let tbody = table.querySelector('tbody'); let thead = table.querySelector('thead'); if(!tbody || !thead) { console.warn('table-sort: No tbody or thead found. Exiting.'); return; }Next, I look at the body of the table and get a copy of the data: let rows = tbody.querySelectorAll('tr'); rows.forEach(r => { let datum = []; let row = r.querySelectorAll('td'); row.forEach((r,i) => { datum[i] = r.innerText; }); this.data.push(datum); });For the next portion, I look at the head. For each column, I want to do two things. First, set a CSS style to make it more obvious you can click on the header. Then I add an event handler for sorting: // Get our headers let headers = thead.querySelectorAll('th'); headers.forEach((h,i) => { h.style.cursor = 'pointer'; h.addEventListener('click', e => { this.sortCol(e,i); }); });Finally, I copy over a reference to the body. This will be helpful later when I render the table on sort: // copy body to this scope so we can use it again later this.tbody = tbody;}Alright. At this point, the component is set up. Now let's look at the sorting event handler:sortCol(e,i) { let sortToggle = 1; if(this.lastSort === i) { this.sortAsc = !this.sortAsc; if(!this.sortDir) sortToggle = -1; } this.lastSort = i; this.data.sort((a,b) => { if(a[i] < b[i]) return -1 * sortToggle; if(a[i] > b[i]) return 1 * sortToggle; return 0; }); this.renderTable();}The event is passed a numeric index for a column which makes sorting our data simpler. The only really fancy part here is how I remember what I sorted last time, which lets me reverse the sort if you click two or more times on the same column. If you are noticing a potential issue here, good, you are absolutely right and I'll show the issue in a sec.Alright, the final part of the code is rendering the table:renderTable() { let newHTML = ''; for(let i=0;i<this.data.length;i++) { let row = '<tr>'; for(let c in this.data[i]) { row += `<td>${this.data[i][c]}</td>`; } row += '</tr>'; newHTML += row; } this.tbody.innerHTML = newHTML;}This is pretty boilerplate. It does have one issue - if the original table cells had other stuff, for example, inline styles, or data attributes, then that is lost. I could have made a copy of the DOM node itself and sorted them around, but for this simple component, I thought it was ok.Whew! The final thing to do is to wrap my table:<table-sort><table> <thead> <tr> <th>Name</th> <th>Breed</th> <th>Gender</th> <th>Age</th> </thead> <tbody> <tr> <td>Luna</td> <td>Domestic Shorthair</td> <td>Female</td> <td>11</td> </tr> <!-- more rows ---> </tbody></table></table-sort>Now let's test it out in the CodePen below: See the Pen PE Table for Sorting by Raymond Camden (@cfjedimaster) on CodePen.Hopefully, it worked fine for you. Of course, if it failed for some reason, you still saw a table right? But maybe you tried sorting on age and saw this:Oops. The age column, which is a number, is sorted as a string. So how do we fix that? Remember that my goal was to have you not touch your original table at all. I initially thought I'd maybe have you add a data- attribute to the table, but that didn't feel right. Instead, I came up with another solution - an attribute to the web component:<table-sort numeric="4">In this case, I'm specifying that the fourth column is numeric. Here's how I supported this in code. In connectedCallback, I look for the attribute:let numericColumns = [];if(this.hasAttribute('numeric')) { numericColumns = this.getAttribute('numeric').split(',').map(x => parseInt(x-1,10));}Since the value in the HTML is 1-based, I take your input (which can be comma-delimited), split it, convert each value to a real number and subtract one. The end result with my sample input is an array with one value, 3.The final bit is to check for this when I create a copy of the data:let rows = tbody.querySelectorAll('tr');rows.forEach(r => { let datum = []; let row = r.querySelectorAll('td'); row.forEach((r,i) => { if(numericColumns.indexOf(i) >= 0) datum[i] = parseInt(r.innerText,10); else datum[i] = r.innerText; }); this.data.push(datum);});And that's it. You can test that version below: See the Pen PE Table for Sorting (2) by Raymond Camden (@cfjedimaster) on CodePen.

From: Raymond Camden

Not Another CV

People who've been following me for a while will know that a key feature of my site has always been the dynamically generated CV button on the homepage. It's made it through multiple itterations and I'd like to think I've gotten pretty good at building that sort of thing. Something I've been asked for years however, is if I'd be willing to licence that to others so that they can have a dynamic CV on their own site.

From: Michael Walter Van Der Velden

Reminder about Web Components and Attributes

After my post yesterday about web component lifecycle events, I had an interesting conversation with Thomas Broyer on Mastodon. He brought up an issue with web components that I covered before on this blog, but as it was a very appropriate thing to discuss immediately after yesterday's post, I thought a bit of repetition would be ok. And heck, I'll take any chance to write more web component code as it gives me more practice.So as a reminder, yesterday's post specifically dealt with what code is best used in a web component's constructor versus the connectedCallback event. Specifically, it dealt with the use case of checking attributes and handling web component elements created via JavaScript. To be clear, I don't mean the definition of the web component, but creating an instance of one, like so:let mc = document.createElement('my-component');document.body.appendChild(mc); While I didn't bother setting a title in that example, I could have done so like this:let mc = document.createElement('my-component');mc.setAttribute('title','My title');document.body.appendChild(mc); And it works as expected. But here's an interesting question. What if later on I change the title? Imagine this code:setTimeout(() => { console.log('timer done, lets do this'); mc.setAttribute('title','New title'); console.log(`title for the mc is ${mc.getAttribute('title')}`);}, 3 * 1000);When run, what will it do? Check out the CodePen below to see: See the Pen WC Tests (5) by Raymond Camden (@cfjedimaster) on CodePen.As you can see, it does not work. Remember you can open your browser's console here if you want to see the messages. It will clearly say that the title attribute matches the update, but that's what you'll see reflected in the DOM.The good (?) news is that this is completely expected and easily (for the most part) addressed. When defining a web component, you need to define which attributes you care about it (in terms of them changing) and write code to listen for those changes.The first part is simple:``jsstatic get observedAttributes() { return ['title'] };The next part involves adding an event handler named `attributeChangedCallback`:```jsattributeChangedCallback(name, oldValue, newValue) { console.log(`changing the value of ${name} from ${oldValue} to ${newValue}`);}If you try this, you'll see that it's fired multiple times. I had a "hard-coded" instance of the component in the DOM and it will message that the title is changing from null to the hard-coded value, reflecting the immediate change of the web component being added to the DOM. You will also see this run with the instance of the component created in JavaScript.Now for the fun part. The event handler needs to actually update the display to reflect the new value. In the first iteration of my example component, I skipped the Shadow DOM and just wrote it out directly to the main DOM. Since I now need to (possibly) update the DOM multiple times, I made two more changes. I switched to the Shadow DOM and built a new method, updateDisplay, that handles updating the display. Here's the entire class:class MyComponent extends HTMLElement { constructor() { super(); console.log('constructor called'); const shadow = this.attachShadow({ mode: "open" }); const div = document.createElement('div'); const h2 = document.createElement('h2'); div.appendChild(h2); shadow.appendChild(div); } connectedCallback() { console.log('connected callback called'); if(!this.getAttribute('title')) this.setAttribute('title', 'No title'); this.updateDisplay(); } updateDisplay() { this.shadowRoot.querySelector('h2').innerText = `My Component: ${this.getAttribute('title')}`; } static get observedAttributes() { return ['title'] }; attributeChangedCallback(name, oldValue, newValue) { console.log(`changing the value of ${name} from ${oldValue} to ${newValue}`); this.updateDisplay(); }}Notice that updateDisplay just uses querySelector to find its h2 node and update the text. Now our code that updates the title after a few seconds will work correctly: See the Pen WC Tests (5) by Raymond Camden (@cfjedimaster) on CodePen.If you don't see the switch, just click the "Rerun" button on the bottom right. Anyway, as I said, I've discussed this before, but it definitely tripped me up the first time I ran into it so hopefully this helps others!Photo by Chris Lawton on Unsplash

From: Raymond Camden

Interesting Caveat with Web Components and the Event Lifecycle

I've been exploring web components the last few months and as part of that exploration, I've been reading "Web Components in Action" by fellow Adobian Ben Farrell. I'm still at the beginning of the book but so far it's been great. It is a few years old now, but for the most part, the only thing I've seen out of date is that at the time of publication, Microsoft Edge didn't have complete support for web components yet. That's been corrected (good thing, I switched to Edge a while back) so it's not really a concern.However yesterday I read something that didn't quite jive with my understanding. The fourth chapter, "The component lifecycle", deals with the various hooks you get into web components when they are used on a page. In this chapter, he spends a good amount of time comparing the constructor of a web component to the connectedCallback event. The constructor is called when the component is created, but connectedCallback is not fired until the component is added to the browser's DOM. That last bit is important. If you add an instance of a web component to a DOM element, let's say a div you created in JavaScript, but that div itself is not in the browser's DOM, the event won't fire.Before going further, let's look at a quick example. Assume this JavaScript for a trivial component:class MyComponent extends HTMLElement { constructor() { super(); console.log('constructor called'); this.innerHTML = '<h2>My Component</h2>'; } connectedCallback() { console.log('connected callback called'); }}if(!customElements.get('my-component')) customElements.define('my-component', MyComponent);If we use <my-component> in the DOM, we will see both console messages for each instance of the tag. Here's a CodePen that demonstrates this. Note that you will need to "Edit on CodePen" to actually see console messages, or open your console right here on my site. See the Pen WC Tests by Raymond Camden (@cfjedimaster) on CodePen.All of this made sense, and really touched on something I've been noodling over - what should I put in the constructor versus connectedCallback. He made one point that didn't seem right to me - that if you check the value of an attribute in the constructor, it will be null. I've been doing this in my previous examples, and heck, even MDN shows it in one of their examples:class PopUpInfo extends HTMLElement { constructor() { // Always call super first in constructor super(); // Create a shadow root const shadow = this.attachShadow({mode: 'open'}); // Create spans const wrapper = document.createElement('span'); wrapper.setAttribute('class', 'wrapper'); // stuff deleted... // Take attribute content and put it inside the info span const text = this.getAttribute('data-text'); info.textContent = text; //lot more stuff...Here's an example where it clearly works just fine:class MyComponent extends HTMLElement { constructor() { super(); console.log('constructor called'); if(!this.getAttribute('title')) this.setAttribute('title', 'No title'); } connectedCallback() { console.log('connected callback called'); this.innerHTML = `<h2>My Component: ${this.getAttribute('title')}</h2>`; }}if(!customElements.get('my-component')) customElements.define('my-component', MyComponent);And if called like so:<my-component title="ray"></my-component><my-component></my-component>I get:My Component: rayMy Component: No titleAs I said, this matched my expectations. Here's a complete CodePen for this: See the Pen WC Tests (2) by Raymond Camden (@cfjedimaster) on CodePen.So, Ben and I talked about this over Slack, and initially, we just figured it was a change since he released his book, but then he made a really important point. What happens if you create an instance of your component via JavaScript? Consider:class MyComponent extends HTMLElement { constructor() { super(); console.log('constructor called'); if(!this.getAttribute('title')) this.setAttribute('title', 'No title'); } connectedCallback() { console.log('connected callback called2'); this.innerHTML = `<h2>My Component: ${this.getAttribute('title')}</h2>`; }}if(!customElements.get('my-component')) customElements.define('my-component', MyComponent);let mc = document.createElement('my-component');document.body.appendChild(mc); In this case, I've made a new my-component and added it to my DOM. I would have assumed this just worked, but instead, you get an error:Uncaught DOMException: Failed to construct 'CustomElement': The result must not have attributesIf you want to see this yourself, open up this CodePen, and open your browser's console, not the CodePen one. The error doesn't get floated up right to the 'virtual' console CodePen uses.Now it makes sense, and it's an easy enough correction to move that logic to connectedCallback:class MyComponent extends HTMLElement { constructor() { super(); console.log('constructor called'); } connectedCallback() { console.log('connected callback called'); if(!this.getAttribute('title')) this.setAttribute('title', 'No title'); this.innerHTML = `<h2>My Component: ${this.getAttribute('title')}</h2>`; }}if(!customElements.get('my-component')) customElements.define('my-component', MyComponent);And in doing so, I can then create instances in JavaScript, and even set my title:let mc = document.createElement('my-component');document.body.appendChild(mc); let mc2 = document.createElement('my-component');mc2.setAttribute('title','Custom title');document.body.appendChild(mc2); In the CodePen below, you can see I used both a "regular" instance of the component in HTML as well as the two defined here and all three act correctly: See the Pen WC Tests (3) by Raymond Camden (@cfjedimaster) on CodePen.I hope this makes sense, and as always, reach out if it doesn't. Going forward, I'll be doing more of my attribute validation and setting in connectedCallback.

From: Raymond Camden

Adding a Chart to an Apline.js Application

For a while now my blog queue has had an item in there suggesting I take a look at adding a basic chart to an Alpine.js application. I finally got a chance to play around with this over the weekend and I thought I'd share the result. For this post, I've used Chart.js, which is a free, open-source charting library that's relatively easy enough to use. Certainly, others could be used as well and as always, if you've got an example, I'd love to see it. With that out of the way, let's take a look at the application.Before the ChartI'll start by sharing what I built before I added a chart to the display. This application consists of a list of cities. For each city, we use the Pirate Weather API to get an hourly forecast and from that, I display the temperature over the next twelve hours. Here's how that looks:I probably should have included a timestamp but for now, this gets the point across. Let's take a look at the code. I begin by defining my cities. This probably would be dynamic, loaded from a database or API, etc.cities: [ { label: "Lafayette, LA", latitude: 30.22, longitude: -92.02 }, { label: "Bellingham, WA", latitude: 48.768, longitude: -122.485 }, { label: "Chicago, IL", latitude: 41.881, longitude: -87.623 }, { label: "Washington, DC", latitude: 38.895, longitude: -77.036 } ]When the application starts, I want to fire off requests to get forecasts. I did this in two methods. The first top-level method fires off the requests:async getForecasts(locs) { console.log('get forecasts for my locations'); let requests = []; locs.forEach(l => { requests.push(this.getHourlyForecast(l.latitude, l.longitude)); }); let data = await Promise.all(requests); data.forEach((d, i) => { this.cities[i].forecast = d; }); this.numForecasts = this.cities[0].forecast.length;},Here I make use of promises to fire all four requests at once and then wait for them to finish. Yes, I should have error handling here. The result of Promise.all will be one array item per promise and will be in the same order I created them, so I can assign the results to my cities by just looping over them.The actual API call is done here:async getHourlyForecast(lat, lng) { let req = await fetch(`https://api.pirateweather.net/forecast/${APIKEY}/${lat},${lng}?exclude=alerts,daily,currently,minutely&units=us`); let data = await req.json(); return data.hourly.data.slice(0,12);}I pass in my API key and the location to the API. The result contains a lot of information, but all I want is the hourly records and only the first twelve. I could probably simplify the result even more but this is good enough.With the forecast information ready, the table can now be displayed. Here's the UI:<div x-data="app"> <table> <thead> <tr> <template x-for="city in cities"> <th x-text="city.label"></th> </template> </tr> </thead> <tbody> <template x-for="i in numForecasts"> <tr> <template x-for="city in cities"> <td x-text="city.forecast[i-1].temperature"></td> </template> </tr> </template> </tbody> </table></div>Basically one loop over the cities to build the table header, and then a loop over the number of forecasts with an inner loop over each city to build each row.Here's a CodePen demonstrating the complete application. See the Pen Alpine + ChartJS (Initial) by Raymond Camden (@cfjedimaster) on CodePen.Adding the ChartFor my chart, I thought it would be nice to visualize both the highest and lowest temperatures for each of the cities. That would give us an idea of the range over our time period as well as the relative difference in warmth between the cities. (Spoiler - Louisiana is hot. Always hot.) Here's the chart I came up with:Note that this was me doing the bare minimum in terms of "design". Chart.js seems really powerful and I could absolutely do more to make this prettier, but honestly, it works, and I was pleased with how quickly I got this working. Here's what I had to do.First, I added the library, https://cdn.jsdelivr.net/npm/chart.js. And hey, thank you Chart.js for not forcing me to npm anything. I appreciate it.Next, I added a canvas to my HTML. Because I'm lazy, I used the same ID as their docs, but this can be changed of course.<canvas id="myChart"></canvas>Next, I added a new method to my code, renderChart, to handle the process. Here's that code.renderChart() { const ctx = document.getElementById('myChart'); let names = this.cities.map(c => c.label); let highestTemps = this.cities.map(c => { return c.forecast.reduce((highest,f) => { if(f.temperature > highest) return f.temperature; return highest; },0); }); let lowestTemps = this.cities.map(c => { return c.forecast.reduce((lowest,f) => { if(f.temperature < lowest) return f.temperature; return lowest; },999); }); new Chart(ctx, { type: 'line', data: { labels: names, datasets: [ { label: 'Highest Temp', data: highestTemps, borderWidth: 1 }, { label: 'Lowest Temp', data: lowestTemps, borderWidth: 1 } ] }, options: { scales: { y: { min: -20, max: 120 } } } }); }Let's examine this. The very first line simply gets a reference to the canvas tag where Chart.js will do its work. The next few lines of code are all me "prepping" my data for the chart. First I get a list of cities. Then I get both the highest and lowest temps for each city with the crafty use of both map and reduce. I am a JavaScript master and I will absolutely pass the next arbitrary coding challenge I get for a job interview. Honest.The net result of the above three blocks of code is three arrays. Each of these can then be passed to my chart declaration. You'll see names passed in for the labels and then my two datasets. This is all pretty much boilerplate demo code from Chart.js, the only thing I did custom was to specify a scale for my Y-axis. My range there isn't perfect, I know some places were below negative twenty recently, but it works for now.You can demo this version here: See the Pen Alpine + ChartJS (Chart) by Raymond Camden (@cfjedimaster) on CodePen.Some Quick NotesOk, all of the following does not actually apply to the main point of this post, but I had some thoughts about what I built and wanted to share them.First, I'm still relatively new to Alpine and still trying to figure out the "best" (for me) way to work with it. I like that Alpine is flexible in its definition and lets you specify methods and data all at once. That being said - I'm not sure I'm happy with how I organized my code. I think my feeling is that I should use the following rules:Put the init() method on top.Put any and all simple variable declarations next.Put methods after.Second, you may or may not notice I added a simple cache to the forecast function in the second CodePen. I did this to ensure I didn't kill my access to the API as CodePen tends to rerun stuff quite a bit. (I need to disable that I think. I just did. Will remember for next time. Honest.)

From: Raymond Camden

Links For You

Good morning readers! I'm writing this in a hotel room in Tuscaloosa where my wife and I are visiting our son. He was presented with a significant award a few nights ago (the Algernon Sydney Sullivan award) and we stayed up a few extra days. We're about to head back to Louisiana so I thought I'd share a few quick links with folks. Have a great Sunday.Taking Eleventy into the Spiderverse with eleventy-fetchHere's a great post by Jeff Sikes where he describes how he made use of the Marvel API in an Eleventy site. That's mixing two of my favorite things, Marvel and Eleventy! I really wish Marvel would continue working on their API. The last update was nearly a decade ago, but on the other hand, I'm happy they just didn't shut it down.Eleventy Collection SchemasYet another awesome Eleventy tip from Stephanie Eckles, this post documents an Eleventy plugin for enforcing your frontmatter setup in Eleventy collections. This is really a good idea since Eleventy gives you complete freedom over your frontmatter, being able to enforce certain rules in your site will help prevent issues in your site. Heck, I made a mistake with my frontmatter a few weeks ago and this would have really helped!Frontend Development Projects with Vue 3 - Second EditionAbout a year or so ago I was involved in the writing of a Vue 2 book for Packt, and now that book has been updated for Vue 3 by myself and Maya Shavin. I have to say - I've been kinda... not again... but not terribly excited about Vue lately. That being said, having used Vue 3 (obviously) for working on this book, I'm feeling somewhat better about it. I still don't think Vue is going to be my framework of choice going forward, I really prefer Alpine, but for any "application", I'd definitely build it in Vue 3.You can get the book from Packt here, https://www.packtpub.com/product/frontend-development-projects-with-vuejs-3-second-edition/9781803234991, or if you buy from Amazon here I'll get an Amazon Associates kickback. Either way, check it out and let me know what you think.

From: Raymond Camden

Standalone Components in Angular

import(`./subsection/subsection.module`).then( m => m.SubsectionModule ) } ]; The module would have to load all components, providers, and could even include it's own router. I always found this to be a lot of boilerplate code just to implement lazy loading. Now with a standalone component, we can make it simpler, which I love. First, let's create a simple routes that does not include lazy loading. Open up the app-routing.module.ts file: const routes: Routes = [ { path: 'view1', component: WrapperComponent }, { path: '**', redirectTo: 'view1' } ]; The first route is named view1, and points to WrapperComponent. I also added a catch all route, distinguished by the two asterisks in the path. The catch all route redirects to the view1. Now, open up app.component.html: Remove this and replace it with the router outlet: With this approach, we can remove the component import from the app.module.ts. Instead of this: imports: [ BrowserModule, AppRoutingModule, WrapperComponent ], We can just do this: imports: [ BrowserModule, AppRoutingModule ], Recompile the app, and load it in the browser: You can see the route in the browser at view1. Start typing to make sure it all works: All good to go; so, we've proved that the stand-alone components are working fine with the router. But, the use of the router here mirrors what we did in the main books, which did not investigate lazy loading at all. How do we set up lazy loading of the module? Back to the app-routing.module.ts and replace the view1 route with this: { path: `view1`, loadComponent: () => import(`./wrapper/wrapper.component`).then( m => m.WrapperComponent ) }, The syntax is very similar to the lazy loading segment I shared earlier. Instead of a loadChildren() method, we now use a loadComponent() method. The value is an arrow functions. The arrow function uses the import command to load in the component. The import returns, surprisingly, a promise. We resolve the promise with the then() method, and the result handler returns the component. You can re-run the app right now to see that it is continuing to work, but to see the real power of lazy loading, let's create another route and view. Let's create a new component: ng generate component view2 --standalone You'll see something like this: We're not going to implement a lot of functionality in the View2Component, only using it to link between the two respective views. Take a look at the default view2.component.ts: import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-view2', standalone: true, imports: [CommonModule], templateUrl: './view2.component.html', styleUrls: ['./view2.component.css'] }) export class View2Component { } To tell Angular to link between routes, we'll need to load in the RouterModule as part of the imports configuration: imports: [CommonModule, RouterModule], And don't forget the TypeScript import: import {RouterModule} from "@angular/router"; Open up view2.component.html. Replace all the contents with a link back to View1: Goto View1 For View2Component that's all we need. Let's go back to View1 which is powered by WrapperComponent. First, we'll need to tell it about the RouterModule in the component config imports: imports: [CommonModule, DisplayComponent, EditComponent, RouterModule], Don't forget the TypeScript import: import { RouterModule } from '@angular/router'; This is the same as what we did in the View2Component. We'll made a similar addition in the wrapper.component.html: Goto View2 I added a link to view2 in front of the other two components in the WrapperComponent. Now, open up the app-routing.module.ts. I'm going to show you the two cases, and how the browser interacts differently. First, with no lazy loading setup: const routes: Routes = [ { path: 'view1', component: WrapperComponent }, { path: 'view2', component: View2Component }, { path: '**', redirectTo: 'view1'} ]; Run the app, and take a look at the network console: Click in the browser between the two views and you'll notice nothing changes in the network tab. Everything is loaded all at once. Now, let's switch back to lazy loading in the app-routing.module.ts: const routes: Routes = [ { path: `view1`, loadComponent: () => import(`./wrapper/wrapper.component`).then( m => m.WrapperComponent ) }, { path: `view2`, loadComponent: () => import(`./view2/view2.component`).then( m => m.View2Component ) }, { path: '**', redirectTo: 'view1' } ]; We've already explored the view1 route and the catch all redirect route. This includes a view2 route, which loads the View2Component. Now reload the app in the browser and review the network tab: We load most of the same files. One thing you'll notice is that main.js is a lot smaller. And there is also a an additional js file loaded, which corresponds to the WrapperComponent. The end result is that we have less data loaded on the initial app load--which means your users are using the application much quicker. Click over to View2: I didn't clear out the network tab before clicking, but primarily you see View2Component loaded, but not until we clicked on the link to load that view. It is beyond the scope of this article to go into the benefits and specifics of lazy loading, but let's just say I love this use case for stand-alone components, because it is something that clearly removes a lot of boilerplate. Final Thoughts I learned a lot by exploring this post, and writing the chapter in my book. I hope you learned a lot reading it. I'm not sure how I feel about standalone components yet, but I do see them as a one way to make Angular applications easier to build, especially around lazy loading. I also see a lot of potential using them in libraries, where you want to optimize a component for reuse. If you've gotten this far, thank you for reading. Be sure to check out the bonus book from the Learn With Series on Angular 15 , which includes a more in depth version of this post with more samples including using services with standalone components, bootstrapping an application without using a module, and learning how to test stand alone components.

From: Jeffry Houser's Blog

Supporting PDF Embeds in an Eleventy WebC Component

Way back in the old days, in August of 2021, I wrote up an example of adding support for Adobe's PDF Embed API as an Eleventy plugin: "An Adobe PDF Embed Plugin for Eleventy". When I find time, I need to update that to the newest URL for the library, but more recently I was curious if I could recreate support using the WebC template language. While it was a bit difficult at times (and a big thank you goes to Zach for patiently helping me), I think it's at a point now where it can be shared. I will warn folks that I'm still struggling a bit with the best way to work with WebC, and at least one feature I'm showing isn't documented yet (but I've confirmed it will 100% ship), but hopefully this example will be useful for folks.Before we start, know that if you want to try this yourself, you will need a free credential. Credentials are host-based, which means a credential for raymondcamden.com will not work for localhost. You can definitely create a few, and if you really want to use one key for development and production, consider setting the host for your domain, minus the www, and use dev dot your domain locally. If you've never used your local hosts file for something like this, reach out and I'll help explain it. (Or heck, that may be my next post.)Alright, so I began actually by writing a template that called my component. I kinda figured I'd write the example code in what felt like the most logical manner, and then I'd actually build the WebC component to match it. Here's how I did it:<pdfembed width="700" height="600" clientid="912a3ba447664592bfbffb224b74a371" pdf="https://static.raymondcamden.com/images/2023/03/cat.pdf"></pdfembed>I've got two attributes to define the size of my PDF, my client credential (it's host-based, so safe to share here, go ahead, steal it, I won't tell), and then the URL for my PDF. Note that when you point to a PDF, it needs to be in a CORS-accessible location. You can also use file promises in the API, but I'm keeping it simple here.Now let's look at the actual component:<template webc:is="noscript"><a :href="pdf"><span @text="pdf"></span></a></template><div :id="uid"></div><script webc:keep src="https://documentservices.adobe.com/view-sdk/viewer.js"></script><template webc:type="11ty" 11ty:type="liquid">{% assign pdfname = pdf | split:"/" | last %}<script webc:keep>let clientID = "{{ clientid }}";document.addEventListener("adobe_dc_view_sdk.ready", () => { let adobeDCView = new AdobeDC.View({clientId: clientID, divId: "{{uid}}"}); adobeDCView.previewFile( { content: {location: {url: "{{pdf}}"}}, metaData: {fileName: "{{pdfname}}"}, focusOnRendering: false });});</script></template><script webc:is="style" webc:type="js" webc:keep>`div#${uid} { width: ${width}px; height: ${height}px;}`</script>Let's discuss. I begin by adding a noscript tag that links to the PDF. That way a user without JavaScript enabled will still be able to get to the document. In order for this to work in WebC, I couldn't use the noscript tag directly but had to use a template instead with the webc:is directive. Zach explained why... but I honestly don't quite get it. Not yet anyway.Next, note the div:<div :id="uid"></div>This is the part not yet documented. Every instance of a WebC component will automatically get provided a unique ID. This is especially useful for me as I need a way to associate a unique ID with the PDF library. This ID is safe for DOM IDs so it made sense to use it.Now for the slightly more complex part. I switch to Liquid so I can be a bit dynamic. Our library has a weird thing where it requires a URL (or file promise) as well as a PDF file name. I've already requested we get rid of this requirement, but for now, I get it by simply popping off the last part of the URL.The rest of the code in that Liquid block just outputs boilerplate PDF Embed code for the library and uses variables for the div ID, the PDF URL, and the filename.The final part is probably the most confusing. I needed to apply CSS to style the size of the div where the PDF is rendered. To do so, I used a JavaScript string literal. I'm using a script tag as, technically, it's JavaScript, but it comes out as CSS, so I map it with webc:is as I did on top. Again, thanks go to Zach for this tip. Finally, I need to use webc:keep because the default behavior in WebC is to toll up and bundle all JavaScript and CSS. In this case, I need the block to stay and be used in every particular instance I call the component.Whew. Hopefully, this made some sense. If you want to test this yourself, I made a Glitch, because Glitch is freaking cool. You can see it here: https://glitch.com/edit/#!/impossible-early-system?

From: Raymond Camden

Update to My Eleventy Blog Guide

Last January, I announced the release of a guide I had written for building a simple blog in Eleventy. Now that Eleventy has hit 2.0, I took some time this morning to look at the guide and see what could be updated. The first thing I noticed was that I had a heck of a lot of typos. I fixed those. I then went through the two main versions of the blog (before and after UI was added) and updated the dependencies to the 2.0 release of Eleventy.That being said, I didn't do anything else. This is not to say that the 2.0 release wasn't lacking in new features, but as my guide is meant to be as simple as possible, I wasn't on the lookout to add any new features if it didn't make sense for the blog. Of course, one of the big features of 2.0 is a big reduction in dependencies, so right away people will benefit from it.Anyway, you can find the guide here: https://cfjedimaster.github.io/eleventy-blog-guide/guide.html.And you can find the repository here: https://github.com/cfjedimaster/eleventy-blog-guide.Feedback is always welcome!

From: Raymond Camden

My First Bug

I've told this story a few times before, but I don't think I've actually ever shared it on my blog. My interest in and introduction to computers came at a very early age. My mother's employer sent her home with an Apple 2 (either the Plus or E model, I forget which), and while it was supposed to be for her, it also included a bunch of games, so I immediately became attached to it. At around the same time, I saw a movie that had a huge impact on me. No, not Star Wars, but instead, TRON. While I was pretty young, I definitely knew it was fiction, and working with computers wouldn't be quite that cool, but it really fired up my interest. I mean, just look at this...So at some point, I stopped playing the games (err, well, stopped playing the games exclusively) and took a stab at trying to learn to program. Applesoft BASIC was a simple language, and best of all, you could literally turn on your machine and immediately begin writing programs. It's hard to describe just how exciting that was - having a development environment as a default meant I spent a heck of a lot of time writing programs. Shoot, I'd sometimes write the same simple program multiple times just to see the result again.My manual was the Applesoft BASIC Programming Reference Manual:This was a good manual, but I quickly ran into an issue, and by quickly I mean on page 2. Here's where I got stuck:BASIC programs consist of lines of code preceded by line numbers. By default, execution will go from the lowest number to the highest, but basic jumping around was supported as well. The typical program would use line numbers counted by ten. This lets you "slip in" lines of code you may have forgotten. Never complain about writing code in Notepad again - this was truly old-school coding. (And to be fair, it was a hell of a lot better than using punch cards. I'm old, but not that old.)Anyway, I followed that text very carefully, and when I ran it, I got an error. Here it is recreated in the Windows AppleWin emulator:I swear I looked at this for hours (most likely it was far less than that) and I just couldn't figure it out. I'd look at the manual, look at the screen, go back to the manual, and I just had no clue.Then... I went back to the manual, and read past the lines of code...See that highlighted line? Yeah, young Ray didn't notice it. I had entered the first line of code... and then used the spacebar to wrap the cursor to the next line.Dumb, right? Of course, with a few years of developer relations and technical writing experience, I look at that and immediately think I'd have moved that statement above the lines of code to make it more obvious.Getting it right gave me such a feeling of complete and utter joy. It's that feeling that has had me hooked on writing code.Luckily, I've not made that same dumb mistake since. I've made huge numbers of other, more unique dumb mistakes. I look forward to what I'll screw up next!Photo by Neringa Hünnefeld on Unsplash

From: Raymond Camden

Links For You

Normally I write these "Links For You" posts on the weekend, but my family and I will be on vacation for the next few days and the laptop is not invited. With that in mind, here's what I'd like to share with yall, enjoy!Eleventy 2.0I'm super excited that Eleventy has hit 2.0! You can read about all the changes on the blog post, and there's quite a bit, but for me the biggest updates are:Dramatic reduction (nearly a third) in dependencies.Faster builds (especially important on my large site)Async friendly shortcodes and filtersThere's a lot more, so again, check the post for details. I'm running 2.0 here, and in the new version of my site that I'm working on now.Hiding DOM ElementsI love working with the web and even more so, I love being surprised by what's possible. Here's a great example of that: Šime Vidas @simevidas@mastodon.social Basically, swap <div hidden> for <div hidden=until-found> to allow the user to search within the hidden text via find-in-page. https://webplatform.news/#1675864782000 2:17 PM • February 8, 2023 (UTC) In this toot, Šime Vidas is sharing a new value available to the hidden attribute... which I didn't even know existed! It's basically the HTML version of hiding an element via CSS. And of course, MDN has docs for it: HTMLElement.hiddenConfiguring an Alias for MastodonIn this post by the awesome David Neal, he describes how to create an alias for your Mastodon account with Eleventy and Netlify. I used this myself to create the alias @raymondcamden@raymondcamden.com for my "real" account @raymondcamden@mastodon.social. It took all of five minutes and honestly it's a really cool trick!As an aside, David does commissioned artwork. Here's one he did for me:

From: Raymond Camden

JPFTFSS - Just Pay For The Faster Server Stupid!

I could also have called this article "How to Speed Up Rust Compilation on Google Cloud Build" but that wasn't as fun. The setup for this article is quite straight forward. As apposed to previous projects of mine, I've been diving much deeper into Google Cloud Build recently. I've been building a collection of Rust Micro-Services that will be running on Cloud Run and as such I've been dealing with incredibly slow build times with some regularity. Rust notoriously concedes to extremely slow compilation time so as to achieve efficiency and safety at run time. As such, I've taken steps to decrease the build time as much as possible - because sitting there waiting for a build is not how I like to spend my evenings.

From: Michael Walter Van Der Velden

Building a Mastodon Bot Listing Page in Eleventy

Chalk this up for yet another thing most folks probably won't need, but it was fun to build so I figured I'd share. I've had a lot of fun building bots for Mastodon. If you're curious about the process, you can read about my experience here: "Building a Mastodon Bot on Pipedream". It occurred to me that it might be cool to build a page on my blog that shows off the bots I've built. It also occurred to me that it's 100% possible I'd build a bot and forget about it.So with that in mind, I built a page that does just that. For each of my bots, it displays their last post. You can see this in action here: https://www.raymondcamden.com/bots. Here's how I built it.I began by looking at the Mastodon embed code written by Bryce Wray, "Static embeds in Eleventy". In his post, he defines a simple shortcode named stoot. Once you make this available in your Eleventy site, you can then do this to embed a toot:{% stoot "mastodon.social", "108241788606585248" %}Simple enough, right? And in theory that could have been enough. All I'd need to do is go to my bots, find one toot, and get the ID. But I thought it would be cool to embed the last toot they did, or at least the last toot at time of building. To enable that, I created another shortcode, lasttoot. It works like so in the template:{% capture "lasttoot_rcb" %}{% lasttoot "botsin.space", "randomcomicbook" %}{% endcapture %}Technically the capture isn't required per se, but as the shortcode returns the ID of the last toot, it would be useless without it. As you can see, I pass in both the server and the username of the bot. Now let's look at the code./*Given a mastodon user, I get their RSS and return the ID of the last toot.*/let Parser = require('rss-parser');let parser = new Parser();module.exports = async (instance, user) => { let rssFeedURL = `https://${instance}/users/${user}.rss`; let feed = await parser.parseURL(rssFeedURL); return feed.items[0].guid.split('/').pop();}Pretty simple, right? All Mastodon user's have an RSS feed of their activity. I used an RSS Parser to bring it in, get the most recent toot, and get the GUID value. That value looks like this (line breaks added for readability):<guid isPermaLink="true"> https://mastodon.social/@raymondcamden/109863869556714822</guid>As all I need is the ID, I do the split/pop calls to grab it. By the way, I wrote this code, and was satisfied with it, but then thought, do I really need an RSS parser? The RSS feed type won't change, maybe I could just use vanilla JavaScript. I did some searching, found solutions using DOMParser, and gave it a shot. It's then when I discovered that it was a client-side JavaScript solution, not server-side. Sigh. I also then remembered I was using rss-parser elsewhere on my site so I wasn't really hurting anything by using it again.To render my bots, I just repeated a bunch of calls to both shortcodes:{% capture "lasttoot_rcb" %}{% lasttoot "botsin.space", "randomcomicbook" %}{% endcapture %}{% stoot "botsin.space", lasttoot_rcb %}{% capture "lasttoot_sjc" %}{% lasttoot "botsin.space", "superjoycat" %}{% endcapture %}{% stoot "botsin.space", lasttoot_sjc %}{% capture "lasttoot_fra" %}{% lasttoot "botsin.space", "rulesofacquisition" %}{% endcapture %}{% stoot "botsin.space", lasttoot_fra %}{% capture "lasttoot_tbs" %}{% lasttoot "botsin.space", "tbshoroscope" %}{% endcapture %}{% stoot "botsin.space", lasttoot_tbs %}{% capture "lasttoot_tdh" %}{% lasttoot "botsin.space", "thisdayinhistory" %}{% endcapture %}{% stoot "botsin.space", lasttoot_tdh %}And that's it. I did run into one interesting issue. For RandomComicBook, the links to the particular comic book were being auto "previewed" in the embed, and since I also show the attached media, the cover, it ended up being displayed twice. I commented out that of the stoot embed for now as it solves it for me. Also, as one more quick aside, the CSS you'll see on my bot page is a bit of a mess. I took Bryce's CSS, messed with it a bit, and got it to "good enough" for my site. It's even embedded directly on the page which is bad practice, but as I'm planning on moving to a new theme soon and doing a big rewrite, I figured it was ok to leave it a bit messy for now.Anyway, let me know if you've got any questions on this, and pour one out for all the fun Twitter bots that have been destroyed by the Muskman!

From: Raymond Camden

Lessons Learned in Twenty Years of Blogging

Way back in 2003, I wrote my first blog post, it was short and sweet and I can share the entirety of it here:Welcome to my blog. I've been working in software development for many years now. Mostly in ColdFusion, although recently I've been working in Java as well. I plan to use this space to share ColdFusion tips - as well as share what I'm learning with Java (and hopefully save others from the stupid mistakes I make). As always, comments are welcome, and I hope this blog becomes an informative resource for all.Funny that I thought I had "many years" of development experience back then. While I've been coding since I was 9 or 10 or so, I didn't start writing code professionally till 1994 or so.My blog was completely custom-built, in ColdFusion, and while I can't seem to find an original picture of the first design, I did find this from 2005:It may be hard to read, but apparently, on February 3, 2005, I posted not once, not twice, but three times. To be fair, this was pre-Twitter, and as I've said before, I used to post a lot of notes, links, and so forth, as a way of sharing cool stuff with others.That custom blogware turned into an open-source project called BlogCFC. I'm proud to say that it became one of the most popular open-source blogging projects for ColdFusion users with hundreds, if not thousands, of instances out in the wild. I had a lot of people contribute to it, provide feedback, and just generally help make it a great platform. I was torn when I finally walked away from it, but still very proud of what I achieved with it.I eventually decided to move off of the ColdFusion platform and try something new, well new to me, WordPress. I was immediately impressed by how polished the administration was. I was also immediately turned off by how quickly WordPress (or PHP, or my database) would crash and burn, usually while I was asleep. My blog has always had "decent" traffic, but nothing so high as to really require a lot of special tuning, or at least I thought as much.It was around this time (2016) that I made my first move to the Jamstack (Welcome to RaymondCamden.com 2016). Back then we just called them static websites. My first iteration used Hugo, which handled the size of my site rather well but was way too restrictive in terms of how it worked. I then moved to Jekyll which gave me the development freedom I needed but unfortunately also showed me the pain of managing Ruby. Finally, in February (not sure I do big things in February so often) of 2020, I moved to Eleventy and have been incredibly happy with my platform since then. I've changed designs once or twice in that time (and am contemplating another change soon), but don't see myself ever leaving Eleventy. (Of course, give me another twenty years and who knows.)A lot has happened in these past 20 years. Here's a random list, in no particular order:I wrote 6,338 blog posts. Some of them were good. I also wrote 2.3 million words. Some of them were even spelled correctly.When I started the blog, my wife and I had adopted 3 kids from South Korea. In the twenty years since we adopted 4 more from China.I wrote some books. Honestly, a few of my books were written before the blog started, but overall I've contributed to, co-authored, or solo-authored, 18 books. As someone who has been a lifelong reader, I always dreamed of being a famous author. In my young mind, that was Stephen King, and while I didn't end up being published for fiction, the fact that I can say I'm a professional writer makes me incredibly proud.My LinkedIn history only goes to 2004, but in that time, I've had 10 jobs. I've been in developer relations since 2011 or so and it is my dream job.I lost my wife, which feels a bit weird to say in a bullet point, but it happened. I'm happily remarried and with my stepson, our family is now 10 strong. I could write an entire blog on we manage such a large family, but that's for another day.Related to the above, I learned how important, and helpful, therapy can be. I learned that asking young developers to code in their free time to buff up their resumes for future jobs is absolutely impossible for some people. As a single father, I had absolutely no mental energy at night to do anything but blank out in front of the TV.I grew as a father, a husband, and as a person.As I look around myself now, I've got a lot to be grateful for. I have a wife who has helped me realize it is ok to enjoy life, to be myself (as crazy as I'd like), and to just plain live. I had kids who continuously surprise me, bring joy to my life, and teach me. I have friends who don't judge me, who are there for me, and who inspire me. I've got a good job, a home for my family, and food. All in all, I am so incredibly lucky, and while I suffer incredible anxiety and fear at times, I remind myself that I am blessed.So that's a lot of pre-amble, let me get around to what I actually wanted to discuss, lessons learned from two decades of blogging.Follow Your PassionFor people launching a personal blog, figure out what makes you excited, and focus on that. It won't always be easy, and sometimes I'm not passionate about anything. Your personal and work life can absolutely impact your energy to write, but when you find something that makes you giddy, focus your attention on that.At times, I feel like a cat chasing a laser pointer. People who have been visiting my blog for a long time will know that I get super excited about a topic, write about 20 posts on it, and then never mention it again. It's not that I started to dislike a particular topic, but once I feel like I've played with something enough and have learned it enough, I move on. My blog started off as a "ColdFusion blog", but in that time I've covered hundreds of different technical topics as well as the books, movies, and games I'm excited about.I can say there's some merit to the idea of focusing on a particular topic, especially if you are hoping to get recognized in that field, but I also know that I can't force myself to write on something if the passion isn't there. If you want to focus, absolutely do so, but keep yourself open to where your mind (and heart) lead you. You are a person, not a robot, and you're going to have a lot of things that make up who you are.Don't Worry about Your Content Being "High Level" EnoughLike many in our field, I struggle with imposter syndrome. I look at the experts in our field, the "big names", and worry I'll never be as good as they are. But one thing I keep in mind is that even if I can't get to some mythical "peak" of my career, I can help others on their journey. I'm never going to be the person writing deep, complex posts about big O notation and the like (ask me about my multiple failed Google interviews), but if I can help a beginner become slightly better than I've helped.In general, if I struggle with something, or if something isn't clear, I'm 100% going to turn that into a blog post. And nearly every, single, time, I'll eventually hear that I wasn't alone in needing help.All of us are in different phases of our learning, and all of us can help others improve.A lot of times it's just a different perspective. To this day, I remember sitting in a presentation involving ColdFusion. This was when I was, in my own admission, really really darn good at it. The person giving the presentation was not. But what they were doing is sharing how they solved a problem. Their approach was absolutely unique, and I learned something that made it worth my time.Don't let the experts gatekeep you from sharing and helping others. Just share - someone will appreciate it.On Blogging Platforms and Owning Your ContentWhen I began web development, in roughly 1990 or so, it was somewhat more difficult to get a website online than it is now. A lot of people in our industry talk about how complex web development is and whether or not it needs to be that way. While there's absolutely merit to that discussion (and again, happy to get on your podcast and talk about it), the basic process of going from idea to live production website (with https!) is infinitely simpler than when it was when I started.That being said, while I think it would behoove budding bloggers (alliteration ftw) to host their own blog on their own domain on their own server, there are some good platforms out there to make it simpler. Dev.to is one of the best platforms for developers who want to get started blogging and offers a quick sign-up, zero code, and easy methods of writing. Most importantly, they offer an export option:No matter where you choose to write, ensure you have the ability to keep your content. Sites come and go, and yuo don't want to wake up one day to find that all of your hard work has disappeared.No matter what platform you choose, I'd urge you to keep a copy of your content. Or heck, write locally and then copy and paste the content. Ideally, you would keep that content backed up in a GitHub repository (I do, and my blogging platform automatically reads from it to publish), but at minimum, you can use a folder in Dropbox/OneDrive.On Writing More GooderI've got a Bachelor's degree in English. I've been writing well before I began blogging. I still misspell words (especially "misspell"), still make grammar mistakes, and still need help.One of my favorite authors, John Birmingham, has a Patreon where you can get early access to his new works, as well as random thoughts and updates. I love it. I help support him with a small donation every month, and I get to see how the sausage is made. Whenever I see one of his drafts and notice a few mistakes, I'm reminded - editors are some of the best people in the world.Years ago, my first time at Adobe, our group had an editor on staff to help with our content. Mine (Jack Wilber, hope you are still reading this!) would graciously offer feedback on my personal posts as well.While most of us probably can't afford an editor, definitely lean on your tools to help with spelling and grammar. I'm a huge fan of Grammarly and even on their free tier, I get a huge amount of feedback and help with my writing. Now, you have to be sure you actually follow through. I don't (typically) blog unless I check what Grammarly says and consciously choose to fix or ignore issues, but I had to make it part of my process for it to really be helpful.I do most of my writing in Visual Studio Code and the Grammarly extension has made this process much simpler.Keep Track of IdeasI've forgotten way more good ideas than I've actually got done on (electronic) paper, so for a very long time now I've used various tools to write ideas. I get my best ideas when exercising, walking my dog, or taking a shower, and I will immediately make note of the idea, even if I don't have time to actually work on or write the post. This is also an excellent way to find inspiration when the creative well is running dry.While I've used various tools in the past, for the past year or so I've been really enjoying Microsoft To Do. Here's my current list, and you'll notice the second to last item is something I wrote a day or so ago, I just haven't gotten around to checking it off.Getting the Word OutUnless you are famous, you will need some way of letting folks know about your content. I'm so bad at this aspect, even after twenty years, that when I wrote this blog post yesterday, I forgot to include it. Early in my career, I was well known in the ColdFusion community, so a typical blog post would get 500+ page views within the first twenty-four hours with little to no promotion on my part.A while ago when I moved to different areas of tech, that was no longer the case and I really began to think more about promotion.Up until a few months ago, my main strategy was to write a tweet about the article and then create a "ICYMI" version scheduled for one week later. I found that the repeat tweet later tended to get some traction and was a nice way to get folks back to older content.I then ditched the scheduled tweet and went with an automated method where I used Pipedream to post to both Twitter and Mastodon. That was in effect for a whole month I think before Muskman decided to kill off free Twitter API access so now my workflow only posts to Mastodon. I manually write a tweet, because even though I think Twitter is days away from completely crashing, I've got a good size audience there and it's worth the effort. (Basically, I just copy and paste the toot.)More recently I've also done the following:Post to LinkedIn.Share in Slack where appropriate. There's a Slack for Jamstack, one for Pipedream, even a front-end focused channel at work.Share in Discord. Honestly I'm feeling overwhelmed by all the chat options available and I sometimes just ignore them, but for Eleventy, there's a Discord server and channel specifically for sharing work. When I remember, I'll post there.Finally, I'll post on Reddit, sometimes.Basically, after Mastodon, Twitter, and LinkedIn, I then hit Slack/Discord/Reddit for specific categories. I honestly don't feel like I get a lot of engagement there, but typically it doesn't take more than a minute or two. I won't be sharing this post on those networks as it's too generic and I figure folks won't care.Oh, and while it goes without saying, have an RSS feed and make sure folks can actually use it. (I.e., publish it in your HTML source code.) I also set up an email subscription service via MailChimp a while ago. That list is currently at 100 members and is growing (slowly). As much as I like RSS, I don't consistently use an RSS feeder, and really prefer it when sites let me subscribe to new posts. (You can also build a workflow to email you on new RSS items via Pipedream, ask me if you want to see an example.)Be Honest, and ListenFinally, be honest. This kind of goes back to not worrying about your content being technical enough, but I absolutely will blog about stuff I'm learning. I just ensure I'm very clear on that fact and point out the aspects I'm not quite sure of yet. Regular readers know I'm learning web components and have hopefully seen that in action. I've also been lucky to get good feedback on those posts, and heck, that turns into even more content. More than once recently I've shared how folks have taken my demos, worked with them, and made them better.Thank YouI say thank you to my readers often, but I'd like to really express just how happy I am to have had an audience for so many years. You've kept me on my toes. As I approach 50 (in biological age, my maturity still hovers between 12 and 14), I don't know how long I'll keep this blog active, but here's hoping I'm writing another such piece in 2043. Also, I have always respected and admired our robotic overlords.

From: Raymond Camden

Progressively Enhancing a Form with Web Components

I've been loving playing with web components lately and today I'm excited to share another one. Especially excited as this one is a great example (I think!) of using a web component to enhance HTML, but that fails gracefully for a user with JavaScript disabled. Before I begin, a quick thank you to Simon MacDonald for helping me get over the hump at the end of this one. For folks curious, I'll share where I got stuck and what he and I discussed after I get through the main part of this post.Alright, so what did I build? I was curious if it would be possible to use a web component to turn a "long" form into a multistep process. Much like a typical e-commerce checkout flow, I'd want to show a part of a form one at a time, and when done, submit everything. The idea is to make the process a bit less intimidating to the user. So for example, consider this form:<form action="https://postman-echo.com/post" method="post"> <p> <label for="firstname"> First Name: </label> <input type="text" id="firstname" name="firstname"> </p> <p> <label for="lastname"> Last Name: </label> <input type="text" id="lastname" name="lastname"> </p> <p> <label for="email"> Email: </label> <input type="email" id="email" name="email"> </p> <p> <label for="ccnumber"> Credit Card Number: </label> <input type="text" id="ccnumber" name="ccnumber"> </p> <p> <label for="ccv"> CCV: </label> <input type="text" id="ccv" name="ccv"> </p> <p> <label for="expdate"> Expiration Date: </label> <input type="text" id="expdate" name="expdate"> </p> <p> <label for="street"> Street: </label> <input type="text" id="street" name="street"> </p> <p> <label for="City"> City: </label> <input type="text" id="city" name="city"> </p> <p> <label for="state"> State: </label> <input type="text" id="state" name="state"> </p> <p> <label for="postalcode"> Postal Code: </label> <input type="text" id="postalcode" name="postalcode"> </p> <input type="submit"></form>This isn't terribly long, here's how this looks with a bit of CSS:Right away, we can improve this a bit by adding a bit of natural grouping with the fieldset and legend tags:<form action="https://postman-echo.com/post" method="post"> <fieldset> <legend>Your Info</legend> <p> <label for="firstname"> First Name: </label> <input type="text" id="firstname" name="firstname"> </p> <p> <label for="lastname"> Last Name: </label> <input type="text" id="lastname" name="lastname"> </p> <p> <label for="email"> Email: </label> <input type="email" id="email" name="email"> </p> </fieldset> <fieldset> <legend>Payment Info</legend> <p> <label for="ccnumber"> Credit Card Number: </label> <input type="text" id="ccnumber" name="ccnumber"> </p> <p> <label for="ccv"> CCV: </label> <input type="text" id="ccv" name="ccv"> </p> <p> <label for="expdate"> Expiration Date: </label> <input type="text" id="expdate" name="expdate"> </p> </fieldset> <fieldset> <legend>Shipping Info</legend> <p> <label for="street"> Street: </label> <input type="text" id="street" name="street"> </p> <p> <label for="City"> City: </label> <input type="text" id="city" name="city"> </p> <p> <label for="state"> State: </label> <input type="text" id="state" name="state"> </p> <p> <label for="postalcode"> Postal Code: </label> <input type="text" id="postalcode" name="postalcode"> </p> <input type="submit"> </fieldset></form>And here's how this looks:Nicer! Looking at this, what if we could display one fieldset at a time, and dynamically add navigation? If you read my Slideshow web component post, you saw an example of this. Given a list of images for input, I add a Previous and Next button to let you navigate the images. I built something similar for this - MultistepForm:class MultistepForm extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({mode:'open'}); this.totalSets = this.querySelectorAll('fieldset').length; this.current = 0; const wrapper = document.createElement('div'); wrapper.innerHTML = ` <slot></slot> <p> <button id="prevButton">Previous</button> Step <span id="currentSetNum">1</span> of <span id="totalPictures">${this.totalSets}</span> <button id="nextButton">Next</button> </p> `; this.$nextButton = wrapper.querySelector('#nextButton'); this.$prevButton = wrapper.querySelector('#prevButton'); this.$currentSetNum = wrapper.querySelector('#currentSetNum'); shadow.appendChild(wrapper); } connectedCallback() { this.$nextButton.addEventListener('click', e => this.nextSet(e)); this.$prevButton.addEventListener('click', e => this.prevSet(e)); this.$sets = this.querySelectorAll('fieldset'); this.$sets.forEach(s => { s.style.display='none'; }); this.updateDisplay(); } nextSet() { if(this.current+1 == this.totalSets) return; this.current++; this.updateDisplay(); } prevSet() { if(this.current == 0) return; this.current--; this.updateDisplay(); } updateDisplay() { this.$sets.forEach((s, x) => { if(x === this.current) this.$sets[x].style.display = 'block'; else this.$sets[x].style.display = 'none'; }); this.$currentSetNum.innerText = this.current+1; }}customElements.define('multistep-form', MultistepForm);Taking this from the top, I begin by counting how many fieldset tags I have wrapped in my tag. I then set my current page to 0. The layout defined in the tag is defined by the content passed in and loaded via <slot></slot>, with the navigation added to the bottom. Notice the two spans in there, they will be dynamic based on the current step and the total number of steps, as defined by the total blocks of fieldsets.In my connectedCallback, I add event listeners, and then grab the fieldset blocks. For each, I hide them with display="none" and call updateDisplay. The previous and next handlers are the same as I used in my Slideshow component. The real change is in updateDisplay, which loops through the fieldsets and shows/hides based on the right value. As this is non-destructive, the user can type stuff into the form fields, navigate the form, and finally submit it.To make use of this component, I simply wrapped my HTML in <multistep-form>` tags, and was done!You can try this out yourself below: See the Pen Multistep Form WC (V2) by Raymond Camden (@cfjedimaster) on CodePen.All in all, I really dig this component. I love that it 'breaks' into a regular form so in theory, this is completely safe for any user. In theory. :)Ok, so that's the main post, feel free to stop reading. Now for the issue that stumped me and Simon helped me figure out. In my initial build of this component, I did not use the slot tag. I figured my component could edit the "regular" DOM items, not the shadow dom, and weirdly, I had no issues hiding the fieldsets, but couldn't bring them back. I assumed (guessed) that by not using slot, the original content was lost in some nether world of the DOM. I'm not sure. But using slot and manipulating the content directly in the shadow dom of the component had things working right away.

From: Raymond Camden

The State of Twitter and an Update on mikevdv.dev

I've done a few blog posts about features that rely on Twitter built into this site, from Custom Twitter Embeds to WebMentions. Given the recent news of Twitter's API becoming paid (and from what I've seen it's not cheap), I'm likely going to have to rework this functionality in the near future. It's not a good situation for Twitter. It was always a favourite of developers due to the ease by which we could build on top of Twitter or into it and it seems that's now coming to an end.

From: Michael Walter Van Der Velden

Another Update to my Slideshow Web Component - JavaScript Support

Last month I shared a simple web component I built to embed slideshows onto web pages. If you didn't get a chance to read that, you can see it in action in this CodePen below: See the Pen Slideshow Web Component by Raymond Camden (@cfjedimaster) on CodePen.After I wrote this, Šime Vidas shared an excellent update to my component with some great modifications. I talked about this version in a blog post, and it's the version I'll be using for my post today. What am I covering today?When I demonstrated how to use my web component, it was done via a script include (well, it's on CodePen, but you get the idea), and then a bit of HTML. Here's an example. (And again, thank you to Šime for his improvements.)<slide-show> <img src="https://placekitten.com/500/500" alt=""> <img src="https://picsum.photos/id/1/500/500" alt=""> <img src="https://via.placeholder.com/500" alt=""> <img src="https://placebear.com/500/500" alt=""> <img src="https://baconmockup.com/500/500" alt=""></slide-show>This works well, but, what if you wanted to modify the contents of the slideshow with other images? Let's see what happens when I try that.First, I'll add a button to the bottom:<button id="switchButton">Switch to Other Show</button>The idea here is that a user would click this to load other content. Now let's write a bit of JavaScript:document.querySelector('#switchButton').addEventListener('click', () => { console.log('Lets switch the images.'); let newUrls = 'https://via.placeholder.com/500?text=Alpha,https://via.placeholder.com/500?text=Beta,https://via.placeholder.com/500?text=Gamma,https://via.placeholder.com/500?text=Delta'; // find the slideshow, could cache this let show = document.querySelector('#myShow'); show.images = newUrls; });In this code, I've set up a click event handler and when it's used, it sets the images attribute of my web component to new URLs. Remember, even though Šime changed my component to allow for inline images, the JavaScript code of the component still actually updates the attribute, like so:this.setAttribute('images', [...this.querySelectorAll('img')].map(img => img.src).join(','));this.innerHTML = '';So, in theory, our code should work, right? Check it out below and see for yourself: See the Pen Slideshow Web Component by Raymond Camden (@cfjedimaster) on CodePen.Yep, not working. You can open up your console too and clearly see the console message being displayed and no errors, but the change doesn't have the effect we would have assumed. So what went wrong? As usual with my code, multiple things.First off, web components will not, by default, monitor changes to attributes. Your web component class needs to specify what attributes it will care about when it comes to things changing. This can be done with one line of code:static get observedAttributes() { return ['images']; }The getter observerAttributes returns an array of attributes that should be monitored for changes. For this demo, I'm specifying just the images attribute, but in theory, you could imagine responding to changes in width and height as well.Next, we have to write code to notice those changes. This is done via the attributeChangedCallback function:attributeChangedCallback(name, oldValue, newValue) { console.log(`Attribute Changed callback: ${name}, ${oldValue}, ${newValue}`); //do stuff}For now I'm just logging the attributes. As you can tell, this gets called for any change, but for us, it will always be images.Finally, there's a problem with how I changed the images:show.images = newUrls;It may be immediately obvious, but this changes a property of the DOM element, not an attribute. There's a great StackOverflow post you can read on the topic, but my code should have done this instead:show.setAttribute('images', newUrls);Whew. So with just that done, if the button is clicked, the following will show up in the console:"Attribute Changed callback: images, https://placekitten.com/500/500,https://picsum.photos/id/1/500/500,https://via.placeholder.com/500,https://placebear.com/500/500,https://baconmockup.com/500/500, https://via.placeholder.com/500?text=Alpha,https://via.placeholder.com/500?text=Beta,https://via.placeholder.com/500?text=Gamma,https://via.placeholder.com/500?text=Delta"It's a bit hard to read as it's two long lists, but you can definitely see the previous and new values. Now let's make the changes.Here's my updated attributeChangedCallback:attributeChangedCallback(name, oldValue, newValue) { this.images = this.getAttribute('images').split(',').map(i => i.trim()); this.totalImages = this.images.length; this.current = 0; this.shadowRoot.querySelector('#totalPictures').innerText = this.totalImages; this.updateImage();}As I said, I can assume the change will always be to images, but if I wanted to I could check the name value. I then copied some code from the constructor - split the string into an array, update the total number of images and reset us back to the first. The totalPictures line there is not from the constructor, but is a change I made to the HTML being generated:Picture <span id="currentPicture">1</span> of <span id="totalPictures">${this.totalImages}</span>Having the span there lets me update it easily. Here's a demo: See the Pen Slideshow Web Component (v3) by Raymond Camden (@cfjedimaster) on CodePen.Ok, better, right? There's still a bit that can be done. I definitely do not like the fact that I copied a few lines of code when updating my display. That code in both the constructor as well as the attribute changed callback could be abstracted out into a render function so there's no code duplication. As the intent for this post was to demonstrate an example of updating a component via JavaScript, I'm fine leaving that out so the focus is on just that, but feel free to fork the CodePen and share with me your update!

From: Raymond Camden

What is a Typical Day for a Manager?

I was buried deep in some side thread in an on-line forum, and someone asked me what a typical day was like for me. Nowadays, I'm a leadership role, with team-mates who report to me, so my day. or week, or year is a lot different than what it used to be when I programed every day. Defining a typical day is pretty hard, since the day is very variable, but I thought I'd give it a shot. But, to get a full scope of my job, I can't stop at just a day. I'll start by looking at a typical day, but then I'm going to look at a typical sprint, and then a typical year. What is a Typical Day? I'm not sure I would call any day typical; unfortunately, but let's give it a shot. 8am – 10am I get into my home office somewhere between 8am and 9am. I'll sign in and start going through my Slack notifications. A lot of messages to me or my team can build up and I try to spend an hour or two in the morning clearing out my queue. For non-urgent requests from the past, I may have used the slack reminder feature to remind me tomorrow (or next week), to come back to this. I don't want to forget. These reminders show up in the morning, and I try to catch them and respond. These may be questions about our systems, or how they work. They could be some non-priority issues. It might be a casual conversation with someone else I didn't want to forget about. Note that email is not the primary means of communication in my current environment. Eventually I'll get around to checking email, but it is usually company related newsletters or meeting invites and is relatively easily cleaned. 10am -11am My meetings do not usually start before 10am, and some days are busier than others. My team does not have daily standups, but rather we do two in person standups per week, and Geekbot on the other days. Usually morning meetings are 1:1 discussion with someone. More details on those meetings when I talk about my sprint. 11am-12 noon Around 11:00 we do a team deploy to prod. Production deploys are not automated, I believe due to some type of compliance. The company institutes a stakeholder review step, and in most cases I am the stakeholder. I have to review all the tickets completed by the team and approve them to be pushed to production. Thankfully our Dev and QA deploys are entirely automated. 12 noon to 1 Between 12 and 1 I have lunch. Most days. Some days my lunch stop is 3 minutes, other times I can relax for the full hour. Often this time slot often overlaps with lunch and learn, town halls, or other educational team meetings. 1pm to 4pm After lunch, the meetings will start up again. Some meetings are planned and recurring, and I'll talk more about that in the sprint section. A lot of ad hoc meetings to fill the time. It might be a sidebar with product owners to clarify requirements. It might be with a subset of my engineering team to talk about some issue. It might be an introductory meeting with another engineering team to discuss a new initiative we'll work on together. It might be a team review meeting to discuss the latest spike and RFC that will define what the team will build for the next few weeks or months. 4pm My wife is a school teacher, so if the stars align I'll break for dinner when she gets home from work. This usually happens 2-3 times a week. 5pm-6pm Sometimes after dinner, I'll be done for the day. Other times I'll check back-in to work. Research Research In my time around meetings, some of it is research or education. Here are some of the things that may occur: Team members often ask me questions to come out of 1:1s, for example I spent a few hours researching the expense policy for external classes, corporate travel, or recent return to office guidelines. I'll also read and comment on new product briefs from our product team Sometimes I'm reading and commenting on research spike from other teams we work with, if that work is relevant to my team. Sometimes I'm reviewing management documents, such as guidelines on new procedures or other communications from upper management, and I have to figure out what is relevant to my team, and a way to communicate that. When the team was hiring, I'd be spending time at least once a week to review resumes and give feedback to our recruiter. Some of my time goes to team building activities. Team lunches, or other team building experiences such as escape rooms. Sometimes I'm working with a designer planning branded gifts for my team. The company has sporadic training opportunities, including required courses and optional courses. I try to find time for stuff, especially if it is relevant to my job. Making myself better on my company's dime is a win-win. Writing I write a lot more than I expected in my current job. Here are a few things that may require documentation: I might write an intake document to bring a new product brief to our team. This summarizes the information from our product owner, along with other tech details from other teams about how this initiative comes together. It will feed into the team's engineering research spike. I may be crafting a new team procedure, such as documenting on call responsibilities, updating onboarding docs, creating Service Level Objectives (SLOs) and Indicators (SLIs), or creating key results related to objectives (OKRs). I might be writing up my own product brief for a tech driven initiative. This allows me to sell it to my manager, and our product owners as a priority the team should take on. To Summarize: My day is meetings, researching, and writing. I miss coding, which is primarily done in my spare time as part of this blog or writing. What is a Typical Sprint? It is a lot easier to tell you what my typical sprint will look like than a typical day. A Sprint is a two-week development cycle, with a defined scope of work for the team and clear goals. Our sprints are done on a two-week cadence, so 10 working days. Day 1 I start with a pre-planning meeting with my team's project manager and my manager. This is to take a look at the goals for the sprint, and choose the related tickets. The full team also does retro from the last sprint. What went well, what didn't, and what can we do better? If you're a programmer you're probably used to this type of meeting. Then we do real sprint planning, where we communicate out the sprint goals for the team, talk about velocity, available points for each person, and choose tickets. Day 1 coincides with our on-call person rotation, so we do on-call transition meeting to talk about issues from the last week, and double check on the schedule for the upcoming week. Sometimes work may come out of this meeting, such as documenting things for our runbook. Day 2 This is a no meeting across my org, so I catch up on a lot of the research and writing I talk about above. Day 3 This is one of the days we do in person standup. An agile ceremony where the team shares their status. We also have a backlog refinement meeting, where the team will discuss and point tickets. We also perform a sync with our product owners to talk about the ongoing work and long term roadmap. The team is invited, but optional on this one. Day 4 This day contains a leadership meeting, which is just a status all ongoing work in our department. Day 5 This day is a different leadership meeting that is a check-in on the org. Leadership is asking the people managers and project manager's about quality of life, any issues that need to be raised, or any other ongoing problems. Day 6 My team, once again, does in person standup on day six. We also have a sync with our designer, to talk about ongoing work, or more likely to jump in on future work. There is another on call transition meeting, where we once again talk about issues of the last week and check the schedule for the previous week. I also have a leadership meeting, focused on upcoming work to our division, and the various teams involved. Day 7 This is the other no meeting day across our org, so once again I try to catch up on a lot of research and writing that I mentioned above. Day 8 This is a repeat of Day 3, with in person standup, a sync with our product owners, and ticket backlog refinement. Day 9 This is a leadership meeting all about my team, similar to the one on day 5, but instead of just the org it is focused on my team only. Day 10 This is a repeat of day 5. Then we go back to day 1, and repeat. On days, where the team does not meet in person we do Geekbot for standup, and I do read through all the updates. I like this form of async status updates, because it allows the devs to get focus time. One to One Meetings Now you know that my two-week cadence contains a lot of meetings, but as you probably guessed this isn't 8 hours of a day of meetings. Thankfully! On any given day I'll have at least a single 1:1 with someone, let's talk about them. My Dev Team: I might have a 1:1 with a team member who reports to me. I Try to keep these focused on professional development and less about the current work status. There are plenty of avenues to find out about the work status, including JIRA boards, application deployments, standups (or GeekBot statuses), or any given ad hoc meeting if a problem does persist. I also have 1:1 meetings with my project manager to discuss team stuff. We may be checking in the roadmap, or making a plan for our next product checkin, or chatting about how to organize the wiki, or about policies we may need to institute with the team. There is also a 1:1 with my boss. More often than not this is a status meeting about ongoing team work, while also touching on company topics such as return to office, vacation, or other corporate policies. We may brainstorm on various issues going on with the team. There is another 1:1 meeting with my product owner, to help plan for future projects, or clarify requirements around upcoming stuff. We do a lot of chatting async as needed, but it is nice to do so zoom to zoom when needed. Although, less frequent I'll have a 1:1 with my manager's manager, which is often about team status. Sometimes, I'll get together with our designer, which is often about ongoing projects. This is less consistent, but does happen. I'm also active in the company's mentoring program. At any given time I have a mentee or two, along with a mentor of my own. Depending on the person, and how long our relationship has been, I meet with them anywhere from every 2 weeks to every other month. I appreciate these relationships with people who expose me to completely different jobs, or other aspects of the company. Many to Many Meetings I already spoke about recurring team meetings in my typical sprint, and dove deep into the type of 1:1s I have. But, there are some other meetings, that didn't quite fit into either of those categories. The first is a manager status meeting with all of my manager's direct reports. This is primarily communication about company high level procedures, or a place to have open discussions about team feedback. If I'm involved in any DE&I working groups or other extra-curricular activities, there will usually be a meeting around that. The free time I collected around all these meetings is all about the research and writing I mentioned above. Typical Year I've been a manager for about a year and a half, and I'm starting to see some patterns. There are a bunch of meetings, or initiatives, that are routine, but not sprint focused. On a monthly basis, we have team demos, both for our sibling teams in the same department and for the wider company. It is a great way to tell what we're doing. I'm not usually the one giving these, but rather coordinating with the team so one of them can give the presentation. On a quarterly basis, we'll perform ICE reviews of our roadmap with our product owners. ICE stands for Impact, Confidence, and Ease, and is a way of evaluating all the planned or upcoming projects to determine business priority. Different Town Halls meetings happen routinely. This is where everyone gets together to listen to leadership talk about initiatives, outline a plan, and commend teams or developers who have been doing awesome. These can happen within my department, or within my org, or at any given leadership level of the company. There is usually at least one a month. For some of them, I may be presenting about my team, or submitting developers for spotlight callouts. This is part of my ad-hoc writing time around the other meetings. Sometimes we do midyear reviews, and we always do end of year reviews. As part of this, I Write up a manager review of every team member that reports to me. I may collect 360 feedback from other people they've worked with--on their team and external to the team. I combine all this info into a doc, that is hopefully informative, and actionable. Highlight their strengths and point out areas for improvement. These are hard to write, and it takes me up to two weeks. Out of year end reviews, will come a professional development plan. How can we make the team members better this year? What are they interested in learning about, or improving? These are primarily driven by the employee, but there do provide things to check up on throughout the year. I may have to prepare promotion docs for my team members to present to my leadership. This includes collecting feedback from others in the org, writing information about how they excelled and making a case for how they were working at the next level. Once again, this is usually a couple of weeks to put this all together, summarizing each expectation at the next level and proving my teammate meets the criteria for the next level. Near the end of the year there is something called a team calibration meeting. I evaluate each employee against the current expectations at their job role. I believe they use an approach called 4 box, so each employee, so on each expectation they get a 1-4. One means needs improvement; two means expectations; three means excelling; and four means doing freakin' amazing. The expectation is most people will land between 2 and 3 I hate to call this procedure stack ranking, because we are only evaluating employees against the expectations of their role. But, at the end each one has a number, which in essence compares them against each other. With the rise of distributed workforces and so much work from home, I'll routinely have "virtual water cooler" group meetings with others in the org. This is the time to hang out, chat about the weekend, latest shows, sports, and sometimes play games. Final Thoughts I still like being me, so in my spare time above all of this, I still keep myself busy with non-recurring extracurricular activities. It might be contributing to the company blog or preparing an internal presentation. I have been leading a team for about a year and a half, and sometimes I feel like I'm still figuring this out. I miss being knee deep in code and architecture every day, however, leading a team managerially has been a rewarding experience and it is nice to see the other side of the curtain.

From: Jeffry Houser's Blog

Links For You

Hey folks, I'm writing to you from another world. A world where I somehow got eight-plus hours of sleep two days in a row. Now, previously I would have told you this was a make-believe world, but now I know the truth, it exists. (And I'll just do my best to forget the nightmare I had a bit before I work up.) With that out of the way, here are a few links to start your week.TheJam.dev VideosI was privileged to speak at TheJam.dev last month, a free online conference covering the Jamstack. The sessions are slowly making their way to YouTube, and while mine isn't on there yet (I was the last session), I know it's coming soon. You can watch a good chunk of the presentations now and I've embedded the playlist below. I just bugged Brian and he let me know everything should be there by mid-week.The State of Developer ConferencesSpeaking of Brian, I wanted to share an article he wrote about the current state of conferences, nicely titled, "The State of Developer Conferences". As someone who both speaks at and runs conferences, he has a great perspective on how things have changed since COVID.As for myself... I just want to share what I've learned. I love to travel, and miss the fact that I don't do as much, but at the end of the day, I'll use whatever means necessary to reach folks and help. Between you and me, I've been posting more videos to YouTube lately, and while they're just short little snippets, I'm hoping it helps reach folks who prefer video content to writing.Eleventy Tip - Anchor Links and HeadersLast up is a great post by Rhian van Esch, "Adding heading anchor links to an Eleventy site". In this post, he describes a few options by which headers defined in Markdown can automatically become anchor links. I plan on adding this to my own blog in the next few weeks. Plus, the article is a good reminder that Markdown can be modified to support new features, and unsurprisingly, Eleventy makes it easy to do so.

From: Raymond Camden

Using JavaScript in a WebC Component

A week or so ago (time is so weird these days), I gave a presentation on Eleventy's WebC plugin. While working on the slides, I built a bunch of demos of various things and knew I'd share a few on the blog. Here's one in particular I wanted to write about. This isn't anything not covered by the docs, but like most things, I needed to try it myself first to see it in action. Specifically for this post, I want to talk about JavaScript in WebC components.Let's begin by creating a simple tag, cat, that will render a random cat:<template webc:type="11ty" 11ty:type="ejs"><%// credit:function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive}let width = getRandomInt(200,600);let height = getRandomInt(200,600);%><p class='myCatImage'><img src="https://placekitten.com/<%= width %>/<%= height %>"></p></template>The component uses EJS to generate a random width and height and then outputs it using the PlaceKitten image holder service. Not terribly useful, but it's cats, and cats always serve a purpose. If we add this to a page:<h2>Cats</h2><cat></cat><cat></cat>We will end up with two random cats. (Random at build time of course, once deployed it's going to be static HTML.) Here's a completely gratuitous screenshot just so I can have some cat pictures in the post:Ok, so far so good, but let's add a bit of interactivity to the component. I want to make it so that when you mouseover the picture, the browser plays a sound file of a cat meowing. I would never do this in real life because it would be annoying af as the kids say, but whatever. Here's the new version:<template webc:type="11ty" 11ty:type="ejs"><%// credit:function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min) + min); // The maximum is exclusive and the minimum is inclusive}let width = getRandomInt(200,600);let height = getRandomInt(200,600);%><p class='myCatImage'><img src="https://placekitten.com/<%= width %>/<%= height %>"></p></template><script>const meow = new Audio('meow.wav');document.addEventListener('DOMContentLoaded', () => { const catPics = document.querySelectorAll('.myCatImage img'); catPics.forEach(c => { c.addEventListener('mouseover', () => { console.log('over da cat'); /* You will get a DOMException if you don't click on the page first, which is good, but we also don't care about it, so try/catch and ignore */ try { meow.play(); } catch { // do nothing } }, false); });}, false);</script>The code basically just looks for cat images by class and adds the event listener. If we add this, and check our site, we'll get... nothing. If you view source, you'll see why:<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title></title><script type="module" integrity="sha512-bSCWm+/TC6zrRk+5XcKszZefvT4Aqwa8b0XOmkMfIirtfzTRqgxEkpAurbYHXD+q0VGBS5e4C4U3XEuBeIOUrA==" src="/.11ty/reload-client.js"></script></head><body><body><h1>Cat Test</h1><cat><p class="myCatImage"><img src="https://placekitten.com/488/206"></p></cat><h2>Another Cat</h2><cat><p class="myCatImage"><img src="https://placekitten.com/405/204"></p></cat><h3>Hello</h3><cat><p class="myCatImage"><img src="https://placekitten.com/501/455"></p></cat></body></body></html>What happened to our JavaScript? Well, it turns out, WebC automatically detects this, and "gathers" up the JavaScript into one bundle you can use in your code. To use the bundle, you can add a bit of code to your layout:<script>{{ page.url | webcGetJs }}</script>This is Liquid code, but similar code exists for layouts written in WebC, and other languages. Once added, we get the correct behavior, and you can see it in source now:<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title></title> <script>const meow = new Audio('meow.wav');document.addEventListener('DOMContentLoaded', () => { const catPics = document.querySelectorAll('.myCatImage img'); catPics.forEach(c => { c.addEventListener('mouseover', () => { console.log('over da cat'); /* You will get a DOMException if you don't click on the page first, which is good, but we also don't care about it, so try/catch and ignore */ try { meow.play(); } catch { // do nothing } }, false); });}, false);</script><script type="module" integrity="sha512-bSCWm+/TC6zrRk+5XcKszZefvT4Aqwa8b0XOmkMfIirtfzTRqgxEkpAurbYHXD+q0VGBS5e4C4U3XEuBeIOUrA==" src="/.11ty/reload-client.js"></script></head><body><body><h1>Cats</h1><cat><p class="myCatImage"><img src="https://placekitten.com/477/484"></p></cat><cat><p class="myCatImage"><img src="https://placekitten.com/327/432"></p></cat></body></body></html>What's nice is, despite cat being run twice, only one copy of the code is used, which makes sense, but is still nice to see. And since the code is checking for multiple items matching the class, only one copy of the code makes sense.As an aside, you may not want WebC to bundle your code. If so, simply add webc:keep to your script tag, like so:<script webc:keep>// awesome code in here that's totally important for the user, totally.</script>Read more about this at the docs: Asset BundlingWant to play with this? You can see the code here, https://glitch.com/edit/#!/webc-javascript-example, or run the released version here, https://webc-javascript-example.glitch.me. Enjoy!

From: Raymond Camden

My town sure seems to have a lot of...

Ok, so I realize this will make me sound old (spoiler, I am old), but I swear I feel like my town (Lafayette, LA) has about ten thousand or so storage businesses. And banks. Oh, and hotels too. For a while now I thought it would be interesting to see if I could build a tool that would actually do that - count the number of a type of business. This week I took a stab at it and while the results aren't perfect, it was fun, and that's all that matters, right?For my demo, I decided to use Google Map's Places API, or more accurately, that part of the JavaScript library. (Google's Maps APIs don't support CORS so if I wanted to do a direct call I'd need to setup a serverless proxy. Overkill for a dumb little demo.)The Places API supports a few different ways of searching, and for my first attempt I tried the Text Search version. This supports free form queries including things like, "banks lafayette, la", and I thought it would be a good way to start. I began with some simple HTML, asking for a type of business and a location:<p><label for="business">Enter a business type:</label> <input type="text" id="business" placeholder="car wash,storage place,etc"></p><p><label for="location">Enter a location:</label> <input type="text" id="location" placeholder="lafayette,la or washington,dc"></p><button id="searchBtn">So just how many are there?</button><div id="results"></div><div id="map"></div>I've got two form fields, a button, and then a div for the results. You'll notice I also have a div for a map. I'm not using a map, but the Google Maps JavaScript library requires a div for a map. Even if you don't show it. Seems a bit weird, but what's one more div between friends, right?Now let's consider the code. First off, the Google Maps JavaScript library is usually loaded via a script tag where the url includes your key, the libraries you need, and the name of a callback function. I was building on CodePen and that didn't quite work well. Instead, I simply appended a script tag to the end of my DOM using this:// Load Google Maps _after_ initMap setupvar script = document.createElement('script');script.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyCA0y7kh6V8poL-faDyVf3TpnLDNf9XtQY&libraries=places&callback=initMap';script.async = true;// Append the 'script' element to 'head'document.head.appendChild(script);I could have made that a nice function but I just dropped it at the end of my code. Now let's look at the function called by the library:function initMap() { console.log('initMap called'); let pyrmont = new google.maps.LatLng(-33.8665433,151.1956316); map = new google.maps.Map(document.getElementById('map'), { center: pyrmont, zoom: 15 }); service = new google.maps.places.PlacesService(map); $business = document.querySelector('#business'); $location = document.querySelector('#location'); $results = document.querySelector('#results'); $searchBtn = document.querySelector('#searchBtn'); $searchBtn.addEventListener('click', doSearch);}As I mentioned above, you must have a map div even if you don't plan on showing it. I created a Map object based on sample code from their docs and I've got zero clue where that latitude and longitude is. In the end, it doesn't matter as it won't be used. The rest of the code creates my service object and assigns some DOM elements to variables for use later.When the search button is clicked, doSearch is run:function doSearch() { console.log('doSearch'); $results.innerHTML = ''; let biz = $business.value.trim(); let loc = $location.value.trim(); if(biz === '' || loc === '') return; total = []; $results.innerHTML = '<p><i>Currently searching...</i></p>'; service.textSearch({ query: `${biz} ${loc}` }, handleResults);}I grab the values, do a bit of simple validation (the user has to enter something), and then call the text search API via the service object I created. Note how query is crafted from the user input.Now for the fun part, handling the results. Google's library supports pagination, and actually makes it quite easy to use. I defined total as an empty array in doSearch and it's global to the page. Here's how I use it:function handleResults(r, status, pagination) { console.log('results', r); console.log('pagination.hasNextPage', pagination.hasNextPage); let open = r.filter(b => { return b.business_status === 'OPERATIONAL'; }); total = total.concat(open); if(pagination && pagination.hasNextPage) pagination.nextPage(); else { console.log('total total is ', total.length); let finalResult = `<p>I found a total of ${total.length} results. Remember the max is 60.</p><p>`; total.forEach(t => { finalResult += `${t.name}, ${t.formatted_address}<br/>`; }); $results.innerHTML = finalResult; }}As I said, the service handles pagination well. When I call nextPage(), it automatically knows to run handleResults again. So all I need to do is keep adding to the total array (but only after filtering out closed businesses) and when done, render out to HTML.As you can see in the text, there's a max of 60, which is unfortunate, because even in our mid-sized town, there's a crap ton of results for some of my searches. Still though it was kind of fun. For our town, searching for church returns the max, searching for bar returns 56. But I think it would have been 60 too if I didn't have the filter for closed businesses. Test it out yourself here: See the Pen Testing Places API (2) by Raymond Camden (@cfjedimaster) on CodePen.So that was round one. Let's make it simpler. The Google Places API also supports a Nearby Search operation. This lets you pass in a location and a business type, where the types are a nice long list of, well just about everything. In my second version, I switched my HTML to a drop down:<p><label for="business">Select a business type:</label><select id="businessType"></select></p>Which is populated via JavaScript:const TYPES = [ 'accounting', 'airport', 'amusement_park', 'aquarium', 'art_gallery', 'atm', 'bakery', 'bank', 'bar', 'beauty_salon', 'bicycle_store', 'book_store', 'bowling_alley', 'bus_station', 'cafe', 'campground', 'car_dealer', 'car_rental', 'car_repair', 'car_wash', 'casino', 'cemetery', 'church', 'city_hall', 'clothing_store', 'convenience_store', 'courthouse', 'dentist', 'department_store', 'doctor', 'drugstore', 'electrician', 'electronics_store', 'embassy', 'fire_station', 'florist', 'funeral_home', 'furniture_store', 'gas_station', 'gym', 'hair_care', 'hardware_store', 'hindu_temple', 'home_goods_store', 'hospital', 'insurance_agency', 'jewelry_store', 'laundry', 'lawyer', 'library', 'light_rail_station', 'liquor_store', 'local_government_office', 'locksmith', 'lodging', 'meal_delivery', 'meal_takeaway', 'mosque', 'movie_rental', 'movie_theater', 'moving_company', 'museum', 'night_club', 'painter', 'park', 'parking', 'pet_store', 'pharmacy', 'physiotherapist', 'plumber', 'police', 'post_office', 'primary_school', 'real_estate_agency', 'restaurant', 'roofing_contractor', 'rv_park', 'school', 'secondary_school', 'shoe_store', 'shopping_mall', 'spa', 'stadium', 'storage', 'store', 'subway_station', 'supermarket', 'synagogue', 'taxi_stand', 'tourist_attraction', 'train_station', 'transit_station', 'travel_agency', 'university', 'veterinary_care', 'zoo' ];// later in the code...$businessType = document.querySelector('#businessType');let optionsString = TYPES.reduce((s, t) => { return s + `<option>${t}</option>`;}, '');$businessType.innerHTML = optionsString;I just want to go on record as saying I've now used reduce twice this week and I'm definitely now a leet coder. Or lute coder? Whatever.I then removed the address and simply got your location via geolocation:let location = await getLocation();// later in the code...async function getLocation() { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(pos => { resolve(pos.coords); }, e => { reject(e); }, { enableHighAccuracy: true}); });}I love using async/await to "patch" over old APIs like geolocation and make them a bit nicer to use. The final change was to switch to the Nearby API:service.nearbySearch({ location: mylocation, radius: 1609, type: [biz]}, handleResults);The radius value is in meters and 1609 is roughly equal to a mile. Here's a screen shot of in action:I'm sharing a picture and not embedding CodePen because geolocation is blocked when embedding the CodePen. I checked, and as far as I know there's no workaround, so for now, you'll need to click a link, sorry about that: https://codepen.io/cfjedimaster/pen/MWBZJWZLet me know what you think. Keep in mind, the results are based on the data Google has, and it's not always going to be accurate. I know I saw things in my test that were incorrect.

From: Raymond Camden

Cloudinary Debugging Tip

I've been blogging about Cloudinary here for the past few months, and I wanted to share a quick tip. A few weeks ago, I was privileged to be interviewed on the Cloudinary podcast, Dev Jams:While showing some code, I came across an image being loaded by Cloudinary that was returning a broken image. Obviously I'd done something wrong, but what? I began by opening the image in a tab, but that just gave me a 400 error:Turns out - there's a simple way to get to the issue. Open up your browser devtools and switch to the Network tab. Click on the request and go into the response headers. Scroll down until you see x-cld-error, and you'll have your error message there:You'll notice it's also present in server-timing, but x-cld-error is the clearer message. Hope this helps!

From: Raymond Camden