5
\$\begingroup\$

Problem

I keep trying to use various todo lists. But I keep coming back to the same situation; not having an active todo list. This can come in two forms:

  1. I don't have a habit to open bespoke software.
    I've tried using Excel, notes and calendars as they're close to my current work flows. But I tend to always forget about them. This is because even though I check my emails, my calendar and my notes are on different windows that I just slowly forget to check.

  2. I frequently move away from the 'todo page' in my pad of paper
    I have a pad of paper that I see every day. I can't forget about the pad of paper because it's constantly in my face. However as I normally jot down ideas on the paper I sometimes go to the next page, and it's like the 'todo page' never existed.

I have a local website that serves links to frequently used pages. Like Firefox's new page tab, but better™. For example when I want to double check itertools docs I just click on the Python new tab and there's the link.

This solves both problems:

  1. I'm in control of the page, and my todo list will always be front and center so I can't forget about it.
  2. My new tab page won't ever change from my local website. Even if I navigate away my new tab will always stay the same.

Environment

My local website is just a single page and I'm making it fairly modular. To achieve this I'm having a 'compiler server' that builds the HTML from various other servers. As such I'm keeping my assets really basic: a single CSS file and a single JS file.

Both scripts are split into two sections:

  • The beginning of a very basic component library. I've used a couple of other component libraries and they've normally had a prefix to avoid collisions so I've done the same.

  • The bespoke code to build the todo list information and bind to the REST database server.
    Note: the connection to the REST database hasn't been built yet, which allows for a better stack snippet.

Features

  • Menu (in the bottom left)


    • Refresh the list with the REST database server. (currently a static JSON string)
      This is ran on load so the list is populated on start.

    • +
      Add new task. This will change the view port to a form to add a new task. Clicking again will close the form.

  • List (in the top left)

    • A list of tasks. You can click on these to open up the task. Clicking again will close the task.

    • This is scrollable when 6 or more tasks are entered.
      Add a new task to see it yourself :)

  • View port (on the right)
    This has three modes.

    • Default
      This is the default mode with nothing displayed. I am planning on putting some information in there that is related to my tasks so I have provided an empty scope.

    • Item
      This displays the item with the title in a title bar. And the body in the body.

      • Clicking the ✔️ or ❌ will allow you to change the status of the task.
      • Clicking the × in the top right will go back to the default view.
    • New task
      This is almost identical to the item view, but the title is always "New task" and the body is always a form to fill out.

      Pressing the "Cancel" button will also change back to the default view.

Review

I would appreciate any comments on my design. I think it looks nice and I've attempted to provide all visual feedback. The feedback is mostly around hovering as I change the mouse pointer and highlight boxes.

I would appreciate most comments on my JavaScript. I've tried to follow what I think is standard JavaScript style. With two main exceptions.

  • I have used 4 tabs as I don't want to fiddle with my IDE's settings. My code is in a Python project and I don't want the headache.

  • My code is an old-school god file, rather than modern compiled JS. The simplicity of having two files to maintain greatly outweighs the modern environment I'd set up. Having JS environments for each and every widget with dodgy build processes that automatically move the built code into the relevant server's static directory. Not exactly what I'd call KISS.

Code

const pTodo = {
    props: ['todos', 'default'],
    emits: ['refresh'],
    data: function () {
        return {
            selected: this.default === undefined ? null : this.default,
        };
    },
    methods: {
        setSelected(value) {
            this.selected = value === this.selected ? null : value;
        }
    },
    template: `
        <div class="p-todo">
            <div class="p-aside">
                <div class="p-list">
                    <div
                        :class="'p-item' + (index === selected ? ' selected' : '')"
                        @click="setSelected(index)"
                        v-for="(todo, index) in todos"
                    >
                        <slot v-bind:todo="todo" name="list-item"></slot>
                    </div>
                </div>
                <div class="p-menu">
                    <span
                        @click="setSelected(null); $emit('refresh', null)"
                    >
                        ↻
                    </span>
                    <span
                        @click="setSelected('new')"
                    >
                        +
                    </span>
                </div>
            </div>
            <div class="p-view">
                <slot
                    v-if="selected === 'new'"
                    v-bind:setSelected="setSelected"
                    name="new"
                >
                </slot>
                <slot
                    v-else-if="selected !== null"
                    v-bind:todo="todos[selected]"
                    v-bind:index="selected"
                    v-bind:setSelected="setSelected"
                >
                </slot>
                <slot
                    v-else
                    v-bind:setSelected="setSelected"
                    name="overview"
                >
                </slot>
            </div>
        </div>
    `,
};

const pInput = {
    props: ['label', 'value'],
    emits: ['input'],
    template: `
        <div class="p-input">
            <label>{{ label }}</label>
            <input
                :value="value"
                @input="$emit('input', $event.target.value)"
            >
        </div>
    `,
};

const TODOS = `[
    {
        "task": "Task 1",
        "body": "Body 1",
        "completed": true
    },
    {
        "task": "Task 2",
        "body": "Body 2",
        "completed": false
    },
    {
        "task": "Task 3",
        "body": "Body 3",
        "completed": false
    },
    {
        "task": "Task 4",
        "body": "Body 4",
        "completed": false
    },
    {
        "task": "Task 5",
        "body": "Body 5",
        "completed": false
    }
]`;

(function(){
    const newTask = {
        components: {
            'p-input': pInput,
        },
        data: function () {
            return {
                task: "",
                body: "",
                completed: false,
            };
        },
        emits: ['new', 'cancel'],
        template: `
            <div class="p-form">
                <div class="header">
                    <span>New task</span>
                    <span class="close" @click="$emit('cancel', null)">×</span>
                </div>
                <p class="content">
                    <p-input label="Task" v-model="task"></p-input>
                    <p-input label="Body" v-model="body"></p-input>
                    <button
                        class="p-button--default"
                        @click="$emit('new', {task: task, body: body, completed: completed})"
                    >
                        Submit
                    </button>
                    <button
                        @click="$emit('cancel', null)"
                    >
                        Cancel
                    </button>
                </p>
            </div>
        `,
    };
    const pane = {
        components: {
            'p-todo-pane': pTodo,
            'new-task': newTask,
        },
        data: function () {
            return {
                todos: [],
            };
        },
        created() {
            this.refresh();
        },
        methods: {
            refresh() {
                this.todos = JSON.parse(TODOS);
            },
            add(todo, setSelected) {
                this.$set(this, 'todos', [...this.todos, todo]);
                setSelected(this.todos.length - 1);
            },
            toggle(todo, index) {
                this.$set(this.todos, index, {...todo, completed: !todo.completed});
            },
        },
        template: `
            <p-todo-pane
                :todos="todos"
                @refresh="refresh"
            >
                <template #list-item="{ todo, setSelected }">
                    <span>{{ todo.completed ? "✔️" : "❌" }}</span>
                    &nbsp;<span>{{ todo.task }}</span>
                </template>
                <template #default="{ todo, setSelected, index }">
                    <div class="header">
                        <span
                            @click="toggle(todo, index)"
                        >
                            {{ todo.completed ? "✔️" : "❌" }}
                        </span>
                        &nbsp;<span>{{ todo.task }}</span>
                        <span class="close" @click="setSelected(null)">×</span>
                    </div>
                    <p class="content">
                        {{ todo.body }}
                    </p>
                </template>
                <template #new="{ setSelected }">
                    <new-task
                        @new="add($event, setSelected)"
                        @cancel="setSelected(null)"
                    >
                    </new-task>
                </template>
            </p-todo-pane>
        `,
    };
    new Vue({
        el: '#todo',
        components: {
            'todo-pane': pane,
        },
        data() {
            return {};
        }
    });   
})();
html {
    background-color: #181a1b;
    color: #e8e6e3;
    font-family: "DejaVu Serif", "Twemoji Mozilla";
}

@font-face {
    font-family: 'Twemoji Mozilla';
    /* Changed for the stack snippet */
    /* src: url('/static/TwemojiMozilla.ttf') format('truetype'); */
    src: url('https://github.com/mozilla/twemoji-colr/releases/download/v0.5.1/TwemojiMozilla.ttf') format('truetype')
}

#todo .p-todo .p-item {
    padding: 0.5em;
}

#todo .header {
    border-bottom: 1px dotted #333333;
    padding-bottom: 0.2em;
}

#todo .header > .close {
    float: right;
    color: #333333;
    cursor: pointer;
}

#todo .content {
    margin-bottom: 0;
}


/* Peilonrayz' Components */
.p-todo {
    width: 30em;
    display: flex;
    max-height: 15em;
}

.p-todo > * {
    border: 1px solid #111111;
    background-color: #222426;
    border-right-width: 0;
    overflow: hidden;
    max-height: inherit;
    box-sizing: border-box;
    height: 15em;
    min-height: 15em;
    max-height: 15em;
}

.p-todo > *:first-child {
    border-top-left-radius: 0.5em;
    border-bottom-left-radius: 0.5em;
}

.p-todo > *:last-child {
    border-top-right-radius: 0.5em;
    border-bottom-right-radius: 0.5em;
    border-right-width: 1px;
}

.p-todo > .p-aside {
    flex-grow: 1;
    width: 33.33333%;
}

.p-todo > .p-view {
    flex-grow: 2;
    width: 66.66666%;
    padding: 0.5em;
}

.p-todo .p-list {
    height: calc(100% - 2em);
    overflow-y: scroll;
    scrollbar-color: #282a2c #222426;
}

.p-todo .p-list::-webkit-scrollbar
{
    width: 12px;
    background-color: #222426;
}

.p-todo .p-list::-webkit-scrollbar-thumb
{
    background-color: #282a2c;
}

.p-todo .p-menu {
    height: 2em;
    background-color: #111111;
    padding: 0.5em;
    display: flex;
    width: 100%;
    box-sizing: border-box;
    justify-content: space-around;
}

.p-todo .p-menu > * {
    cursor: pointer;
}

.p-todo .p-list > .p-item {
    border-bottom: 1px dotted #333333;
    border-top: 1px dotted #333333;
    cursor: pointer;
}

.p-todo .p-list > .p-item:first-child {
    border-top-width: 0px;
}

.p-todo .p-list > .p-item:last-child {
    border-bottom-width: 0px;
}

.p-todo .p-list > .p-item.selected {
    background-color: #26282a;
}

.p-todo .p-list > .p-item:not(.selected):hover {
    background-color: #242628;
}

.p-input label {
    position: absolute;
    font-size: 0.8em;
}

.p-input input, .p-input textarea {
    background-color : #222426;
    border: 0;
    border-bottom: 1px dotted #333333;
    color: e8e6e3;
    padding-top: 1.2em;
    margin-bottom: 0.5em;
}

.p-form button {
    background-color : #222426;
    border: 2px solid #282a2c;
    color: e8e6e3;
}

.p-form button:hover {
    background-color: #242628;
}

.p-form button.p-button--default {
    background-color: #242628;
}

.p-form button.p-button--default:hover {
    background-color: #26282a;
}
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<html>
    <body>
        <div id="todo"><todo-pane></todo-pane></div>
    </body>
</html>

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

UI

I noticed that when I add a new task I tried to click on the text "New task" to replace that with the text of the task but then I didn't get a cursor, so then I clicked on the text labeled "Task" and started typing - I could barely tell my text was added but it was black so it was difficult to see it against the black background. Perhaps a light contrast color would make it easier to see the text as I type. The same is also true for the buttons labeled Submit and Cancel. See the CSS section below where the flaw with the style is mentioned.

new task screenshot

I also noticed that if I didn't enter text for a task then I could still add it and it would show with no text. I felt stuck because I couldn't edit it 😏. Maybe that is not an issue for you because you always enter text but think about people who are not you, or yourself many years ahead... somebody will skip the text....

enter image description here

Review

Initial feedback

Great job using templates, slots and other features of Vue, as well as using const for variables that don’t need to be re-assigned, lest there be room for accidental re-assignment. Keeping everything in a “god” file seems fine by me.

JS

selected data property in Ptodo:

emits: ['refresh'],'refresh'],
data: function () {
    return {
        selected: this.default === undefined ? null : this.default,
    };

Idiomatic JS would typically involve short-circuit evaluation:

emits: ['refresh'],
data: function () {
    return {
        selected: this.default || null,
    };

Or the nullish coalescing operator could be used:

        selected: this.default ?? null

duplicate key names in $emit() for submit button

 <button
    class="p-button--default"
     @click="$emit('new', {task: task, body: body, completed: completed})"
 >

could be simplified using the shorthand property definition notation to:

<button
    class="p-button--default"
    @click="$emit('new', {task, body, completed})"
>

empty data object when instantiating new Vue().

Maybe this is just boilerplate but it isn't needed:

data() {
       return {};
   }

CSS

Lets look at this ruleset:

.p-input input, .p-input textarea {
     background-color : #222426;
     border: 0;
     border-bottom: 1px dotted #333333;
     color: e8e6e3;
     padding-top: 1.2em;
     margin-bottom: 0.5em;
 }
  • there is no hashtag/octothorpe on the color rule. That is why the text entered by the user is black ( as mentioned earlier). The same applies to the color in ruleset for .p-form button - i.e. the Submit and Cancel buttons.
  • there are no units on the border rule, which is fine though inconsistent with previous rules where units were specified with a zero value. Some argue a unit should be specified in case the value is ever changed though opponents point out that 0px would create a border with 0px width and hold that readability is better with no units1. Perhaps it is only an issue if a calc() or similar function is needed2.

Repeated styles for different selectors

There are three different rulesets with this single rule:

background-color: #242628;

These could be combined:

.p-todo .p-list > .p-item:not(.selected):hover,
.p- form button:hover,
.p-form button.p-button--default {
    background-color: #26282;
}
\$\endgroup\$
1
  • \$\begingroup\$ Nice! I completely forgot about the missing #s. The code for some bizzare reason worked in my Firefox... 😐I may have Dark Reader enabled and tainting my website... There's some good stuff here, thanks! I'm not a fan of foo || bar because I've been snakebit by it so many times in Python, but I look forward to using ?? instead! Also I agree there really should be an edit button. I kinda just deleted and made a new task if I ever made a mistake... 😅 I'll revamp my code to take into account your response, thanks Sam! \$\endgroup\$
    – Peilonrayz
    Jan 16, 2021 at 1:20
1
\$\begingroup\$

I've managed to join the links and the todo list in my modular 'compiler server'. I've added some more features too and I've had some growing pains with the code.

  1. Components have a minimum size for good interactions. Changing the todo component to the width of the links module makes the todo component horrible to use. Whilst I've dropped the width from 480px to 436px, not even a 10% reduction, the affect is clearly noticeable.

    Too small example image.

    This highlights a couple of problems:

    1. The task preview is far too small. With a couple of solutions.

      • If the todo component's width is reduced to an unusable size make both the task list and the preview occupy 100% of the component.

        This is possible through media queries. This has the downside that we'd need to know where the component gets its width, as component queries don't exist.

      • Allocate 2/3 of the width to both the list and the view and manually select which gets the joint 1/3 to be shown. For example always show the view unless the view is empty (default) or the user is hovering over the list.

        This would start to look quite ugly if the tasks component is much larger than it is now. To fix this we could use either media queries or by specifying a minimum width. To use a minimum width we can set the width to 1/3 for the list and 2/3 for the view; but by setting both to a minimum width of say 20em we can get them both to have a width of 2/3 if the width of the component is 30em.

      • Don't support smaller sizes, so force the consumer to make the component bigger.

    2. When a task overflows to the next line the description shouldn't go under the ✔️ or ❌. This is because the "7p" looks completely out of place. And reminds me of a question I asked when I was younger; "why do all old books start with a ridiculously big first letter that looks ugly?" Not all old books have lettrines.

      This can be solved by using a flexbox rather than inline blocks.

  2. To fix 1.1 I went with the simple solution of, just make it biggerer. But I ran into another problem.

    Too short example image.

    Just because you've used relative length units, em, doesn't mean your code is responsive. If the font size never changes - and we don't want it to or we'll be back at square 1 - then your design is static.

    I think it should be noted that this doesn't mean you should replace em with px. em allows for a design to resize depending on the font size. This means if you need to enlarge a page, say you have bad eyesight, then all the relative distances stay the same - a border doesn't start to move towards your font.

    I liked the 2 width : 1 height ratio originally selected so we can exploit the fact that margin/padding-top/bottom when specified in % goes off the width.

    .p-todo--aspect-ratio {
        position: relative;
        width: 100%;
        height: 0;
        padding-bottom: calc(100% / 2);
    }
    
    .p-todo--aspect-ratio > .p-todo {
        position: absolute;
        width: 100%;
        height: 100%;
    }
    

    I will call .p-todo--aspect-ratio "the parent" and .p-todo as "the child".

    The way this works is:

    1. The containing box includes all of the margin, border, padding and content.

      For example the containing width is the sum of the left and right margin, left and right border, left and right padding and the width of the content.

    2. Set the parent's containing box's width to the width we want.

      width: 100%
      

      Be careful if any margin, border or padding is added to the box as these will be included in the calculations - which you may not want.

    3. Set the parent's containing box's height to the height we want.

      It works because margin/padding-top/bottom, when specified as a percentage, is determined according to the width of the containing element. Essentially, you have a situation where "vertical" properties can be sized with respect to a "horizontal" property. This isn't an exploit or a hack, because this is how the CSS specification is defined.
      Nathan Ryan 2012-12-26 18:25:00Z, License: CC BY-SA 3.0

      Note: It doesn't always work this way, we have to specify the position as relative to achieve this. Otherwise it goes off a different element's height.

      position: relative;
      height: 0;
      padding-bottom: calc(100% / 2);
      
    4. Set the child's containing box to the size we want. Since height and width are based off the parent's containing box this is super simple. The one gotcha is that you have to specify the position as absolute otherwise the height isn't set correctly.

      position: absolute;
      width: 100%;
      height: 100%;
      
    5. Whilst we could have used padding-top rather than padding-bottom this moves the position of the parent's content box, moving the child to where that is now located. You can get around that with setting top: 0 in the child. But that's just more CSS for more CSS.

    Now the box maintains the desired 1:2 aspect ratio. And the child has the height specified so everything can stretch correctly; it's like the code isn't even there!

  3. The CSS as currently specified will conflict with other components that get added. .p-list for example was mutating the list component I later added, and vice versa. To prevent this you can prefix the style with the name of the component it's attached to.

    .p-todo .p-list
    
    .p-todo .p-todo--list
    

    I'm not sold on this style as I'm used to it meaning a change in state, rather than a new class. Take for example .p-radio and .p-radio--checked.

Currently I've been pretty content with the code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.