Links For You
19 March 2023 | 6:00 pm

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.com

Here'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 * 2

I 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:

Jellyfish lava lamp


Another Week, Another Mastodon Bot - Random Album Cover
17 March 2023 | 6:00 pm

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:

Album: Saturday Night Wrist (open.spotify.com/album/4o1KnoV)
Artist(s): Deftones
Released: 2006-10-30

.img-521981b675c9b3fa5917288705d1a12b {aspect-ratio: 640 / 640}Image 110051077652658327 from toot 110051077666753231 on botsin.space

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 letter
  • Randomly 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 1000
  • Hit 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


Progressively Enhancing a Table with a Web Component
14 March 2023 | 6:00 pm

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:

Table sorted incorrectly for age

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.



More News from this Feed See Full Web Site