Post-Thanksgiving week was “short” in the sense that I was getting back into the swing of things. It’s that holiday slump period where you don’t care for more to happen. For me, the period marks a sense of reflection. Time to go outside in the cold and take a hard look at what this year has been like.
Thanksgiving holiday was this past week. I’m thankful for making through this hectic year, for friends and family going through the best and the worst, and for a loving partner. We’re taking this time of year to relax and take it easy.
Scott Adams (Cartoonist who produces Dilbert) write in his blog about how persuasive he sees Donald Trump. I can’t find the exact post, but he mentions how Trump is playing 4D chess against all of his opponents who don’t know his next moves. Trump supporters use this to boost their candidate. Source
At some point, I want to do a short review of “Between Two Kingdoms”, which chronicles Suleika’s journey in healing from her Luekemia, and the parallels I had with my father’s ongoing recovery
2FA Directory - I didn’t know there was a directory for 2FA / MFA. I’m certainly going to review this
I use a version of a resonance calendar, but it’s more like what sparks. I put it in my notes, wait a day, and see if I’m still interested. Time is usually my best filter.
The idea of a Resonance Calendar seems to have come from the community surrounding the notetaking app Notion, but awhile back I adapted it for my own uses and I’ve found it really useful as a casual periodic practice. The idea is to keep track of and reflect on the various things that you read, watch and listen to. I used to go back over what I read and review the summaries I wrote about why I bothered, but I fell out of the habit as my pregnancy progressed.
Eleanor Konik
Still Tasty - a website to find out what’s spoiled and what’s still good in your fridge
The announcement came earlier this year. As someone who loved going to these three theaters, UA, Shattack Cinema, and California Cinema, in Berkeley throughout the ’90s and ’00s, this was heartbreaking.
I remember seeing Lord of the Rings: The Twin Towers at the UA after school, and it was magical. Something about not having these theaters for the next generation breaks my heart
I also distinctly going to see Spirited Away at Shattack Cinema, and remembering how Miyazaki movies instill magic into them.
I’ve put my dad’s 30 day notice for senior living in today. He’s ready to go home around Thanksgiving time. He’s ready to go him.
My dad’s health is generally okay. He’s having some skin problems at the moment from not putting on enough lotion. His hygiene is terrible, and we are wondering if he needs to shower every day. He also is not shaving. We had dim sum yesterday, and are ready for other caretakers to help take care of him.
His recovery is stagnant. We want him to interact with more of the people. I’m starting to create a recovery team for him so he has support
Back to your regular notes
I’m looking into a way to embed songs to Obsidian so I can link a song. I might use a YouTube embed in the meantime.
Here’s a note I wrote about my LYT showcase.
I recognize perfection is the enemy of good. And the way I’ve wanted to present my work is hardly how it looks when I’m in the middle of the idea. In my writing, first drafts hardly resemble the published draft. And even the published draft may be upcycled to other ideas. I’m very happy to see many other students embrace the mess
We are back in our regular rotation. LYT Workshop 12 wrapped last week, as I mentioned in the previous week, so expect this to be more regular.
I’m finally getting back to doing a quarterly review, a month later than I would’ve hoped.
NaNoWriMo is starting this Wednesday, and I’m planning to participate. I put my Project Page up for everyone to see.
At work, we’ve been chugging along working on next product requirements that’s hush hush here
As some people are aware with my father, we’ve followed up with PT this past week where he can start walking supervised without his cane, which is a win all around.
I’ve been returning to this idea about systems, and when I encounter new ones (tagged: experimental), I try to incorporate them in my own workflows and see if they mesh. Most do (I’m looking at you, GTD)
Missed last two weeks, as I’m going through the LYT workshop. I decided to cease publishing anything until I complete it.
Now that it’s the last week, I’m turning my attention back from my PKM to this website again. My goal is to publish once a week. 🤞🏼
Therapy Remark
Share Your Calm
This really resonated with me when talking with my therapist. In times of others’ stress, it’s easy to get caught up in the moment and take that on yourself. Instead, take a moment and show your calmness, rather than echoing the stress.
Freewriting sessions
When I kept up my journaling experiences for a decade, some days, I’d let it all out on paper. Take my thoughts and feelings and let them bleed on paper. Sometimes I’d come up with barely anything. Other times, I’d pour my heart on the page.
I’m going to return to this practice because it’s a form of practicing my calm (tying the previous point back in). I’ll take a short amount of time, 3 minutes to be exact, and do the work in my journal.
Filed under “Challenge what you believe”, this was kind of an eye opener. Sometimes, I want to say I’ve read the evidence and find it very compelling. But I didn’t. I was in a staff development meeting when I was teaching at a Prep school, and we had to work in groups telling each other what learning styles we were.
I’m back from the very last Strangeloop and from visiting my cousin for her baby shower in Las Vegas. I forgot how it feels to do non-stop traveling back-to-back. Reminds me of the time I traveled across the US, then hopped on a plane to South Korea.
I took this idea of “Weekly Notes” from Jamie Tanna who I met at Strangeloop, and I thought this would be a wonderful recurring segment for the blog. Even if I have low readership, this is a nice capsule to look at for my monthly, quarterly, and annual reviews. 😁
Derek Sivers updated his post on Tech Independence, where it’s a single command now
DALL·E 3 - [[OpenAI]] updated DALLE where prompt engineering is needed much less. Positioning is a lot better, with context
There’s a new map style on OpenStreetMap.org! The Tracestrack Topo map from @tracestrack.
I’ve decided to share things that I’ve found throughout the week, curating for you. You will find things that have caught my attention, notes that I think are worth exploring, and thoughts that have been perculating.
Now the list is there for us to add to, revise, and to refer to! He feels more supported and capable in being supportive and actually helping and I feel more supported and, less depressed!
@sharon.a.life
Now the list is there for us to add to, revise, and to refer to! He feels more supported and capable in being supportive and actually helping and I feel more supported and, less depressed!
♬ original sound - Sharon.a.life
Get used to the bear behind you.
— Werner Herzog’s 24th and final maxim
I heard this quote on a podcast in respect to creating. While there are things you can’t control, it’s your attitude to it that matters the most.
Also, Justin Welsh has written enough where his “new” content is really an update to his old content. Hence, having a 730-day content library. I would love to aspire on my ideas like this, and refine, refine, refine.
My dad fell from the roof. It was 10pm, Friday night, July 7th, 2023. It’s the call you never want to receive. I was getting ready for the dog’s last walk, and then go to bed. Instead, I rushed over to Highland Hospital in Oakland.
Initially, it all seemed the head injury he sustained was minor. He had a concussion and some bruising, which did not appear to be a big deal. Matters got out of hand on the second day when the nurse treated the family as if he knew better than the doctors. The nurse forced my dad a standing position, even as he complained of naseua and dizziness. There was a lot of confusion, and the nurse thought he knew better than to ask the doctor if my dad needed another CT scan ordered.
The next day, I got a call from the night nurse that the staff rushed my dad into the operating room. A surgeon performed a craniotomy on my dad. Within 24 hours, they stabilized his intercranial pressure and removed excess blood that pushed his brain to one side. It was a life or death situation, and we were all praying for the best outcome. He was intubated post-operation until the next day. Mentally, he appeared barely existing for the next few days. After being in the ICU that week, he was moved to a step-down unit, a section of the hospital that is less intense than the ICU, where he was under routine supervision for the next week.
Reflecting on the situation, I took a few things away that many care-givers should know.
Don’t take the nurse’s or doctor’s word. If someone you love is in critical care, you are the first observer, especially if you are there 24/7. Having family members help in shifts are really important, as you can cover as much time as possible.
Seek out the patient advocate. If things are not going well, like the nurse has become combative, every hospital has a patient advocate. It may take time to dig up their information, but having someone who truly cares for patient safety can be the difference between life and death.
Don’t be afraid to ask questions. Although I might be an outlier in the amount of medical knowledge I have, I still ask questions, even qualifying ones. It’s important to know which medications are administered and why. It’s important to know when the nurse shift change is. It’s important to know when your loved one will be discharged. Much of what I’ve learned in the last month has helped me navigate what shouldn’t be such a complex healthcare system.
When writing this, I assume the following.
The critical care facility is in the U.S. (California)
Your loved one is in a life or death situation
You have others helping you (makes it a lot easier)
I hope I’ll never have to come back to this note, but in the worst case scenario, I would have loved to read this advice.
Every weekend, I’ve been slowly moving to San Jose, so I haven’t been able to spend enough time with my hobbies and side projects. This website has seen a down tick in content, and I’m looking forward to writing more in the near future. Certainly post-move.
To spend the in-between time not thinking about code, I’ve been playing Zelda: Tears of the Kingdom. We recently finished Cult of the Lamb, and it felt pretty special.
I went to the Figma conference last week at San Francisco Moscone Center, and it was a packed event. I’m excited to dive deep into more UX work.
In the late 2000s, Google Reader was my jam. The feeds were just beginning to
populate, and I remember enjoying my social feed from MySpace and early Facebook.
What I didn’t realize was how much I would miss the RSS feed aggregator once it
was killed.
But why was Google Reader killed? The Verge wrote an excellent article going over the inception and ultimate demise of the website. It reminds me of the website, KilledByGoogle which chronicles over 200 products Google killed since its inception.
I was one of those die-hard Google Reader users. Once I caught wind of it, I immediately exported my data. I tried alternatives like Feedly and The Old Reader, but it didn’t feel the same.
There were serious limitations, like limited subscriptions, unless I paid for the premium versions. And that leads me to a much more serious problem that I was facing: over-consumption.
The Feeds Take Over
I think I’ve been faced with this information over-consumption more times than I
can count. If Google Reader started my addiction to short-lived information, then
social media certainly made things much worse. My mind was craving the constant
attention of an infinite scroll — a feed that will never stop. With Google Reader,
at least I could control the RSS feeds that were coming in. I remember one time,
I wanted to be informed by the world news events so I subscribed to BBC,
but it backfired quickly to give me a deluge of distractable information. Facebook
and Twitter feeds were no better. They started innocently but quickly turned
into what an algorithm wants me to read. And that loss of control quickly turned me
off.
In more recent years, I thought I could substitute this with email newsletters.
What I quickly learned was email is no substitute for a personal feed. I was overwhelmed with 50, then 100, then 1000 unread messages. There were newsletter
articles I was extremely interested in reading combined with many others I had no
interest in. On top of that, when I needed to reply to an email, I’d be
distracted by something I needed to read.
I’ve heard Cal Newport call this the “Hyperactive Hive Mind” in his book A World Without Email.
What I’ve come to interpret this as is a constant need to be distracted without an
off switch. Every single social media with a feed has given me this same problem.
I remember the top three subreddits I would mindlessly scroll, the YouTube feed
with an endless “Watch Later” playlist, and the email feed with newsletters left
unread. What it always ended with was going cold turkey. Uninstall Reddit. Place
time limits on YouTube. Stop reviewing emails for weeks at a time. It wasn’t a permanent solution.
Confessions of an Information Hoarder
Another thing Google Reader started me off with is information hoarding. Like copying a whole article I want to read to make notes and a summary about it later, but never returning to it. My Evernote became cluttered with
many unrealized things in life I wanted to experience, but can’t do because there’s
limited time. I’m reminded of tsundoku, a Japanese word that means “to pile up”.
I have a pile of books that are left unread for the moment I can pick one up and
start reading. For the pile of articles, the stack is so high, it’s a chore to go
through and just find the articles I want to read. More time is spent
sorting through the ones I know I should read.
Prioritization has never been a strong suit. I tend to spread out my efforts across
multiple projects rather than focus on one. With reading, I’m the same way. I
constantly have 2 or 3 books I’m reading concurrently. With articles, forget them.
I have found when I rely solely on a feed to make decisions for me, I waste the
most time on it.
The Rise of the “Slow Feed”
I talked at length about these problems, and minor band-aids on how I’ve fixed it.
At the root of it, the problem is my behavior interacting with the feed. Unless the feed is short and filtered down, I’ll spend more time in it than what I think is normal (but hey, what is normal anyway?). So I propose the “Slow Feed”.
While I can’t stop myself from browsing feeds, there is a limit on how much
I can save for later. The “Slow Feed” has a hard limit of 15 or fewer items, taking
a lesson from the quick line at the grocery store. If I can’t through this feed,
I can’t add more items. It means I must be very picky and choosy with what I consume.
I’m not didactic about the other feed though. To make it to the “Slow Feed”, it
must be an article or video I want to “save it later”, go into my Readwise Reader feed, then go into my shortlist, which serves as my
“Slow Feed”. I get the best of both worlds. I limit the time spent in the mindless
scrolling portion (the endless feeds) and make time to read the longer articles.
It’s like curing myself of FOMO, without really curing it.
Got any ideas on how to improve this? Email me and let me know!
Errata
I read The Information Diet by Clay Johnson years ago
and may be time to scan it over and see if I can find other ways to improve this.
The book that prompted me to read more consciously is Chris Bailey’s book,
How to Calm Your Mind, which has a section about
anxiety in the age of information overload. As part of my “Year of Intentions”,
this is one of the things I’m working on.
The 7 GUIs is a benchmark for comparing different GUI frameworks, proposed by Eugen Kiss. See his explanation below.
There are countless GUI toolkits in different languages and with diverse approaches to GUI development.
Yet, diligent comparisons between them are rare.
Whereas in a traditional benchmark competing implementations are compared in terms of their resource consumption,
here implementations are compared in terms of their notation.
To that end, 7GUIs defines seven tasks that represent typical challenges in GUI programming.
In addition, 7GUIs provides a recommended set of evaluation dimensions.
— Eugen Kiss
I’m going to walkthrough each GUI using Svelte, and annotate the code.
Counter
The task is to build a frame containing a label or read-only textfield T and a button B.
Initially, the value in T is “0” and each click of B increases the value in T by one.
<script> // Initialize the counter with "0" (as it says in the spec) let count = 0;</script><!-- Display the count, as a number input --><input type="number" bind:value={count}/><!-- Add a button that will increment the counter by 1 with each click. --><button on:click={() => (count += 1)}>count</button>
Temperature
The task is to build a frame containing two textfields TC and
TF representing the temperature in Celsius and Fahrenheit,
respectively. Initially, both TC and TF are empty. When
the user enters a numerical value into TC the corresponding value
in TF is automatically updated and vice versa. When the user enters
a non-numerical string into TC the value in TF is not
updated and vice versa. The formula for converting a temperature C in Celsius
into a temperature F in Fahrenheit is C = (F - 32) * (5/9) and the dual
direction is F = C * (9/5) + 32.
<script lang="ts"> // Initialize the values of celsius and fahrenheit let c = 20; let f = 68; // Given the value from Celsius, update Fahrenheit function setBothFromC(value: number): void { // The + is to convert the string to a number c = +value; // Use the formula from the spec to update fahrenheit f = +(32 + (9 / 5) * c).toFixed(1); } // Given the value from Fahrenheit, update Celsius function setBothFromF(value: number): void { f = +value; // Use the formula from the spec to update celsius c = +((5 / 9) * (f - 32)).toFixed(1); }</script><!--Add two different inputs. Since the inputs are two-way bound by the values,`c` and `f`, we can add an event listener to run the function to convert theother value.--><input value={c} on:input={(e) => setBothFromC(e.target.value)} type="number"/>°C =<input value={f} on:input={(e) => setBothFromF(e.target.value)} type="number"/>°F
Flight Booker
The task is to build a frame containing a form with three textfields
The task is to build a frame containing a combobox C with the two options
“one-way flight” and “return flight”, two textfields T1 and
T2 representing the start and return date, respectively, and a
button B for submitting the selected flight. T2 is enabled iff C’s
value is “return flight”. When C has the value “return flight” and
T2’s date is strictly before T1’s then B is disabled.
When a non-disabled textfield T has an ill-formatted date then T is colored
red and B is disabled. When clicking B a message is displayed informing the
user of his selection (e.g. “You have booked a one-way flight on
04.04.2014.”). Initially, C has the value “one-way flight” and T1
as well as T2 have the same (arbitrary) date (it is implied that
T2 is disabled).
<script> const DAY_IN_MS = 86400000 const tomorrow = new Date(Date.now() + DAY_IN_MS); // Create an array of year, month, day, in this format: YYYY-MM-DD let start = [ tomorrow.getFullYear(), pad(tomorrow.getMonth() + 1, 2), pad(tomorrow.getDate(), 2), ].join("-"); // our reactive variables let end = start; let isReturn = false; // Running statements reactively, updating the variables when they are changed $: startDate = convertToDate(start); $: endDate = convertToDate(end); // Click handler for the button function bookFlight() { // Determine type of return const type = isReturn ? "return" : "one-way"; let message = `You have booked a ${type} flight, leaving ${startDate.toDateString()}`; if (type === "return") { message += ` and returning ${endDate.toDateString()}`; } alert(message); } // Convert a string in the format YYYY-MM-DD to a Date object function convertToDate(str) { const split = str.split("-"); return new Date(+split[0], +split[1] - 1, +split[2]); } // Pad a number with leading zeros function pad(x, len) { x = String(x); while (x.length < len) x = `0${x}`; return x; }</script><!-- Create your select input for one-way or return flight option --><select bind:value={isReturn}> <option value={false}>one-way flight</option> <option value={true}>return flight</option></select><!-- Bind the inputs --><input type="date" bind:value={start} /><input type="date" bind:value={end} disabled={!isReturn} /><!-- Attempt to book flight --><button on:click={bookFlight} disabled={isReturn && startDate >= endDate} >book</button>
Timer
The task is to build a frame containing a gauge G for the elapsed time e, a
label which shows the elapsed time as a numerical value, a slider S by which
the duration d of the timer can be adjusted while the timer is running and a
reset button R. Adjusting S must immediately reflect on d and not only when S
is released. It follows that while moving S the filled amount of G will
(usually) change immediately. When e ≥ d is true then the timer stops (and G
will be full). If, thereafter, d is increased such that d > e will be true
then the timer restarts to tick until e ≥ d is true again. Clicking R will
reset e to zero.
<script> // It turns out, you can't run this in Astro without saying this component is client only import { onDestroy } from "svelte"; // Start elapsed at 0 milliseconds let elapsed = 0; // Set the range input to be 5 seconds / 5000 milliseconds let duration = 5000; let last_time = window.performance.now(); let frame; // IIFE for animation loop (function update() { // pass in the function for the animation frame (infinite looping) frame = requestAnimationFrame(update); // performance.now() is like Date.now(), but more accurate to tenths of a milliseconds const time = window.performance.now(); // Take the minimum of the time elapsed and add it to the new elapsed time elapsed += Math.min(time - last_time, duration - elapsed); last_time = time; })(); // When the component is destroyed, cancel the animation frame onDestroy(() => { cancelAnimationFrame(frame); });</script><!-- Create the label and use the progress tag to show the time elapsed vs duration --><label> elapsed time: <progress value={elapsed / duration} /></label><div>{(elapsed / 1000).toFixed(1)}s</div><label> duration: <!-- Bind the input to the duration. Max 20 seconds --> <input type="range" bind:value={duration} min="1" max="20000" /></label><!-- Allow the user to reset the timer --><button on:click={() => (elapsed = 0)}>reset</button>
CRUD
The task is to build a frame containing the following elements: a textfield
Tprefix, a pair of textfields Tname and
Tsurname, a listbox L, buttons BC, BU and
BD and the three labels as seen in the screenshot. L presents a
view of the data in the database that consists of a list of names. At most one
entry can be selected in L at a time. By entering a string into
Tprefix the user can filter the names whose surname start with the
entered prefix—this should happen immediately without having to submit the
prefix with enter. Clicking BC will append the resulting name from
concatenating the strings in Tname and Tsurname to L.
BU and BD are enabled if an entry in L is selected. In
contrast to BC, BU will not append the resulting name
but instead replace the selected entry with the new name. BD will
remove the selected entry. The layout is to be done like suggested in the
screenshot. In particular, L must occupy all the remaining space.
<script> // Have some people to start with let people = [ { first: "Hans", last: "Emil" }, { first: "Max", last: "Mustermann" }, { first: "Roman", last: "Tisch" }, ]; // Initialize the bound variables let prefix = ""; let first = ""; let last = ""; // Initialize the selected item index let i = 0; // Reactive statements when the changes $: filteredPeople = prefix ? people.filter((person) => { const name = `${person.last}, ${person.first}`; // Filter based off first or last name return name.toLowerCase().startsWith(prefix.toLowerCase()); }) : people; // Reactively change the selected when filtered people $: selected = filteredPeople[i]; // Reset all inputs when new selection made $: reset_inputs(selected); // Create a new person function create() { people = people.concat({ first, last }); i = people.length - 1; first = last = ""; } // Update the selected person function update() { selected.first = first; selected.last = last; people = people; } // Remove the selected person function remove() { // Remove selected person from the source array (people), not the filtered array const index = people.indexOf(selected); people = [...people.slice(0, index), ...people.slice(index + 1)]; first = last = ""; i = Math.min(i, filteredPeople.length - 2); } // Reset the input for first and last names function reset_inputs(person) { first = person ? person.first : ""; last = person ? person.last : ""; }</script><input placeholder="filter prefix" bind:value={prefix}/><select bind:value={i} size={5}> <!-- Loop through the filtered people --> {#each filteredPeople as person, i} <option value={i}>{person.last}, {person.first}</option> {/each}</select><!-- Create inputs for first and last names --><label ><input bind:value={first} placeholder="first" /></label><label ><input bind:value={last} placeholder="last" /></label><!-- CRUD operators --><div class="buttons"> <button on:click={create} disabled={!first || !last}>create</button> <button on:click={update} disabled={!first || !last || !selected} >update</button > <button on:click={remove} disabled={!selected}>delete</button></div>
Circle Drawer
The task is to build a frame containing an undo and redo button as well as a
canvas area underneath. Left-clicking inside an empty area inside the canvas
will create an unfilled circle with a fixed diameter whose center is the
left-clicked point. The circle nearest to the mouse pointer such that the
distance from its center to the pointer is less than its radius, if it exists,
is filled with the color gray. The gray circle is the selected circle C.
Right-clicking C will make a popup menu appear with one entry “Adjust
diameter…”. Clicking on this entry will open another frame with a slider
inside that adjusts the diameter of C. Changes are applied immediately.
Closing this frame will mark the last diameter as significant for the
undo/redo history. Clicking undo will undo the last significant change (i.e.
circle creation or diameter adjustment). Clicking redo will reapply the last
undoed change unless new changes were made by the user in the meantime.
<script> // Initialize the bound variables let i = 0; let undoStack = [[]]; let circles = []; let selected; let adjusting = false; let adjusted = false; // On handling click, create circle with default radius 50 px function handleClick(event) { if (adjusting) { adjusting = false; // if circle was adjusted, // push to the stack if (adjusted) push(); return; } const circle = { cx: event.clientX, cy: event.clientY, r: 50, }; // Add circles to list of circles. The selected circle is the current circle circles = circles.concat(circle); selected = circle; push(); } function adjust(event) { selected.r = +event.target.value; circles = circles; adjusted = true; } function select(circle, event) { if (!adjusting) { event.stopPropagation(); selected = circle; } } // Use a stack for keeping track of the circles function push() { const newUndoStack = undoStack.slice(0, ++i); newUndoStack.push(clone(circles)); undoStack = newUndoStack; } function travel(d) { circles = clone(undoStack[(i += d)]); adjusting = false; } function clone(circles) { return circles.map(({ cx, cy, r }) => ({ cx, cy, r })); }</script><!-- Put in the buttons for controls --><div class="controls"> <button on:click={() => travel(-1)} disabled={i === 0}>undo</button> <button on:click={() => travel(+1)} disabled={i === undoStack.length - 1} >redo</button ></div><!-- Draw with an SVG. Bind the click handler --><!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions --><svg on:click={handleClick}> <!-- Draw all of the circles --> {#each circles as circle} <!-- svelte-ignore a11y-click-events-have-key-events --> <circle cx={circle.cx} cy={circle.cy} r={circle.r} on:click={(event) => select(circle, event)} on:contextmenu|stopPropagation|preventDefault={() => { // When right-clicking, open the adjuster adjusting = !adjusting; if (adjusting) selected = circle; }} fill={circle === selected ? "#ccc" : "white"} /> {/each}</svg><!-- Show the adjuster if adjusting a circle's size -->{#if adjusting} <div class="adjuster"> <p>adjust diameter of circle at {selected.cx}, {selected.cy}</p> <input type="range" value={selected.r} on:input={adjust} /> </div>{/if}
Cells
The task is to create a simple but usable spreadsheet application. The
spreadsheet should be scrollable. The rows should be numbered from 0 to 99 and
the columns from A to Z. Double-clicking a cell C lets the user change C’s
formula. After having finished editing the formula is parsed and evaluated and
its updated value is shown in C. In addition, all cells which depend on C must
be reevaluated. This process repeats until there are no more changes in the
values of any cell (change propagation). Note that one should not just
recompute the value of every cell but only of those cells that depend on
another cell’s changed value. If there is an already provided spreadsheet
widget it should not be used. Instead, another similar widget (like JTable in
Swing) should be customized to become a reusable spreadsheet widget.
This one isn’t in the Svelte documentation, so I found a different
implementation that went through it perfectly. Link
Cells is split up into two Svelte components: Cell and Cells.
<!-- Cell.svelte --><script> // Initialized props export let j; export let i; export let focused; export let data; export let p; export let handleFocus; export let handleBlur; export let handleKeydown; export let handleInput; // Keep track of the current key let key = j + i; // Keep track if a cell is focused let hasFocus = false; $: if (focused === key && !hasFocus) { hasFocus = true; } else if (focused !== key && hasFocus) { hasFocus = false; }</script><!-- When focused, change the cell into an input --><!-- Otherwise parse the formula -->{#if hasFocus} <input id={"input-" + key} value={$data[key] || ""} autofocus on:focus={() => handleFocus(key)} on:blur={() => handleBlur(key)} on:keydown={(e) => handleKeydown(e, j, i)} on:input={(e) => handleInput(e, key)} />{:else} <div>{p.parse($data[key]) || ""}</div>{/if}
<!-- Cells.svelte --><script> import Cell from "./Cell.svelte"; import { data } from "./store.js"; import { sampleData } from "./sampleData.js"; import { Parser } from "./parse.js"; // Initialize with the sample data set data.set(sampleData); // Create 26 columns w/ the letters of the alphabet const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // Max 100 x 100 cells export let shape = [100, 100]; const rows = range(shape[1]); const columns = letterRange(shape[0]); const p = new Parser(data, columns, rows); let focused; let tBody; // Create range array function range(n) { return [...Array(n).keys()]; } // Create letter range function letterRange(n) { return range(n).map(getNumberAsLetters); } // Loop through letters function getBase26(n) { let result = []; while (n > 25) { let remainder = n % 26; result.push(remainder); n = Math.floor(n / 26) - 1; } result.push(n); return result.reverse(); } // Get the letter range and join them function getNumberAsLetters(n) { let arr = getBase26(n); return arr.map((num) => LETTERS[num]).join(""); } function handleFocus(key) { if (focused !== key) { $data[key] = $data[key] || ""; focused = key; setTimeout(() => { let target = tBody.querySelector("#input-" + key); if (target) { target.focus(); target.setSelectionRange(0, 9999); } }, 10); } } function handleBlur(key) { if (focused === key) focused = undefined; } function handleInput(e, key) { $data[key] = e.target.value; } function handleKeydown(e, column, row) { // Navigate across the spreadsheet with arrow keys (and alt/option key) let selector; if (e.key === "ArrowUp") { let newRow = findAdjacent(rows, row, "before"); selector = newRow !== null ? column + newRow : null; } if (e.key === "ArrowDown" || e.key === "Enter") { let newRow = findAdjacent(rows, row, "after"); selector = newRow !== null ? column + newRow : null; } if (e.key === "ArrowLeft" && e.altKey) { let newColumn = findAdjacent(columns, column, "before"); selector = newColumn !== null ? newColumn + row : null; } if (e.key === "ArrowRight" && e.altKey) { let newColumn = findAdjacent(columns, column, "after"); selector = newColumn !== null ? newColumn + row : null; } if (selector) { e.preventDefault(); handleFocus(selector); } } function findAdjacent(arr, value, direction) { let index = arr.indexOf(value); if (index === -1) return null; if (direction === "before") return arr[index - 1] === undefined ? null : arr[index - 1]; if (direction === "after") return arr[index + 1] || null; return null; } function clear() { data.set({}); }</script><div class="wrapper"> <table> <thead> <tr> <td class="row-key" /> {#each columns as column} <td class="column-key">{column}</td> {/each} </tr> </thead> <tbody bind:this={tBody}> {#each rows as i} <tr id={"row-" + i}> <td class="row-key">{i}</td> {#each columns as j} <td id={j + i} on:click={() => handleFocus(j + i)}> <Cell {j} {i} {focused} {data} {p} {handleFocus} {handleBlur} {handleKeydown} {handleInput} /> </td> {/each} </tr> {/each} </tbody> </table></div><button on:click={clear}>Clear</button>
There are two utility functions to help out the operations: parse and store
(the latter being the Svelte store to save in local state).
// parse.jsexport class Parser { constructor(store, columns, rows) { this.cells = {} this.store = store this.columns = columns this.rows = rows this.operations = { sum: (a, b) => a + b, sub: (a, b) => a - b, mul: (a, b) => a * b, div: (a, b) => a / b, mod: (a, b) => a % b, exp: (a, b) => a ** b } // subscribe to store this.store.subscribe(value => { this.cells = value }) } cartesianProduct(letters, numbers) { var result = [] letters.forEach(letter => { numbers.forEach(number => { result.push(letter + number) }) }) return result } findArrRange(arr, start, end) { let startI = arr.indexOf(start) let endI = arr.indexOf(end) if (startI == -1 || endI == -1 || startI > endI) return [] return arr.slice(startI, endI + 1) } getRange(rangeStart, rangeEnd) { rangeStart = this.splitOperand(rangeStart) rangeEnd = this.splitOperand(rangeEnd) let letters = this.findArrRange(this.columns, rangeStart[0], rangeEnd[0]) let numbers = this.findArrRange(this.rows, rangeStart[1], rangeEnd[1]) return this.cartesianProduct(letters, numbers) } splitOperand(operand) { return [operand.match(/[a-zA-Z]+/)[0], Number(operand.match(/\d+/)[0])] } rangeOperation(op, rangeStart, rangeEnd) { if (!(this.isWellFormed(rangeStart) && this.isWellFormed(rangeEnd))) return this.originalString let range = this.getRange(rangeStart, rangeEnd) return range .map(address => Number(this.parse(this.cells[address]))) .reduce(this.operations[op]) } singleOperation(op, operand1, operand2) { let first = this.parseOperand(operand1) let second = this.parseOperand(operand2) if (first === null || second === null) return this.originalString return this.operations[op](first, second).toString() } isWellFormed(operand) { return /[a-zA-Z]+\d+/.test(operand) } parseOperand(operand) { if (!isNaN(Number(operand))) return Number(operand) if (operand in this.cells) return Number(this.parse(this.cells[operand])) if (this.isWellFormed(operand)) return 0 return null } parseOperation(op, formula) { if (!(formula.startsWith('(') && formula.endsWith(')'))) return this.originalString formula = formula.slice(1, formula.length - 1) let operationType let formulaArr if (formula.includes(',')) { operationType = 'single' formulaArr = formula.split(',') } else if (formula.includes(':')) { operationType = 'range' formulaArr = formula.split(':') } if (formulaArr.length !== 2) return this.originalString if (operationType === 'single') return this.singleOperation(op, formulaArr[0], formulaArr[1]) if (operationType === 'range') return this.rangeOperation(op, formulaArr[0], formulaArr[1]) return this.originalString } parse(str) { this.originalString = str if (typeof str !== 'string') return '' if (!str.startsWith('=')) return str let formula = str.slice(1) if (formula.slice(0, 3).toLowerCase() in this.operations) { return this.parseOperation( formula.slice(0, 3).toLowerCase(), formula.slice(3).toUpperCase() ) } else { return this.cells[formula] || str } }}
// store.jsimport { writable } from "svelte/store";export const data = writable({});
The last file is to load prefilled data, but we don’t need to go over that.
// An example of the sampleData fileexport let sampleData = { A0: "Data", A1: "20", A2: "15"};
SolidJS is a Javascript framework for building fast, declarative UIs on the web. It shares many ideas with React, but does not use the virtual DOM to deliver a more performant and pragmatic developer experience.
In the playground, you can view the compiled output.
Also, you can change the compile mode, between “Client side rendering”, “Server side rendering”, and “Client side rendering with hydration”
Any code that you write in the playground can be exported to a coding sandbox, like Codesandbox. So helpful!
Philosophy - Think Solid
Declarative Data
Vanishing Components
Solid updates are completely independent of the components. Component functions are called once and then cease to exist.
Read/Write segregation
We don’t need true immutability to enforce unidirectional flow, just the ability to make the conscious decision which consumers may write and which may not.
Simple is better than easy
Compilation
Solid’s JSX compiler doesn’t just compile JSX to JavaScript; it also extracts reactive values (which we’ll get to later in the tutorial) and makes things more efficient along the way.
This is more involved than React’s JSX compiler, but much less involved than something like Svelte’s compiler. Solid’s compiler doesn’t touch your JavaScript, only your JSX.
Destructuring props is usually a bad idea in Solid. Under the hood, Solid uses proxies to hook into props objects to know when a prop is accessed. When we destructure our props object in the function signature, we immediately access the object’s properties and lose reactivity.
So in general, avoid the following:
function Bookshelf({ name }) { return ( <div> <h1>{name}'s Bookshelf</h1> <Books /> <AddBook /> </div> );}
And replace with props instead.
Dependency Arrays
In React, you’d declare the dependencies explicitly using the dependency array. If you didn’t, the effect would rerun whenever any state in the component changes. In Solid, dependencies are tracked automatically, and you don’t have to worry about extra reruns.
Looping with array.map
If we used array.map in Solid, every element inside the book would have to rerender whenever the books signal changes. The For component checks the array when it changes, and only updates the necessary element. It’s the same kind of checking that React’s VDOM rendering system does for us when we use .map.
Conditional if statements on re-rendering
In the Building UI with Components section of this tutorial, we noted that component functions run only once in Solid. This means the JSX returned from that initial function return is the only JSX that will ever be returned from the function.
In Solid, if we want to conditionally display JSX in a component, we need that condition to reside within the returned JSX. While this takes some adjustment when coming from React, we have found that the fine-grained control afforded by Solid’s reactive system is worth the trade-off.
Reactivity and proxy objects
In Solid, props and stores are proxy objects that rely on property access for tracking and reactive updates. Watch out for destructuring or early property access, which can cause these properties to lose reactivity or trigger at the wrong time.
onChange vs. onInput
In React, onChange fires whenever an input field is modified, but this isn’t how onChangeworks natively. In Solid, use onInput to subscribe to each value change.
No VDOM or VDOM APIs
Finally, there is no VDOM so imperative VDOM APIs like React.Children and React.cloneElement have no equivalent in Solid. Instead of creating or modifying DOM elements directly, express your intentions declaratively.
Solid Primitives
Signals - The basic way to manage state in the application
Similar to useState in React, but as a reactive value
Derived state - you can track a computed state from a signal, which is also reactive
You can pass the signal as a prop like this: <BookList books={books()} />. It’s not a typo to use the function as it’s passing an accessor, which is important for reactivity.
Effects - ability to react to signal changes
A driving philosophy of Solid is that, by treating everything as a signal or an effect, we can better reason about our application.
Looping
Solid has a component called <For /> with an each prop. You can pass in a signal that will make this reactive.
As you can see, onInput is the event handler that takes in the event. In this case, we are setting the new book for each input (the title and author).
The onClick handler for the button uses the addBook function where it can prevent the form from submitting, set the books using a new array, then resetting the new book. It should be noted that setBooks is using a callback function where you access the current state. Also, it should be noted not to mutate state by creating that new array (much like in Redux practice).
The primitive for any external data source is createResource. The function returns a deconstructed array with the data. It takes two arguments: the signal and the data fetching function.
Putting it all together, query is the signal. searchBooks is the data fetching function. Once the data is returned, we can loop over it, and for each item, we can set the books if selected.
The following is a code example introducing how Reactivity or Reactive Programming works.
import { createSignal, createEffect } from "solid-js";const [count, setCount] = createSignal(2);const [multipler, setMultiplier] = createSignal(2);const product = () => count() * multipler();// Change the count every secondsetInterval(() => { setCount(count() + 1);}, 1000);// Change the multiplier every 2.5 secondssetInterval(() => { setCount(multipler() + 1);}, 2500);// Effect automatically detects when a signal has changed// So you don't have to add a dependency array.// This is defined as "reactivity"createEffect(() => { console.log(`${count()} * ${multiplier()} = ${product()}`);});
createSignal works by creating a data structure that can read and write. To each of these functions, subscribers are added and updated
CreateEffect works by executing on a context queue (JS array). It takes its queue, executes it, then pops it (at least in pseudocode)
SolidJS uses “granular updates” so only the variables that change only update the DOM, and not entire components.
In this example, we extracted Multipler into its own component, added props, like React, and called it multiple times in App. As you will also notice, signals do not have to be in the function scope! This is counter to what you do in React. Of course, if you don’t want to share the signal across other components, you can keep it in the functional lexical scope.